[
  {
    "path": ".eslintignore",
    "content": "data/\nlogs/\nvendor/\n"
  },
  {
    "path": ".eslintrc.js",
    "content": "module.exports = {\n  // When adding items to this file please check for effects on sub-directories.\n  \"parser\": \"babel-eslint\",\n  \"parserOptions\": {\n    \"ecmaVersion\": 2018,\n    \"ecmaFeatures\": {\n      \"jsx\": true\n    },\n    \"sourceType\": \"module\"\n  },\n  \"env\": {\n    \"node\": true\n  },\n  \"plugins\": [\n    \"import\", // require(\"eslint-plugin-import\")\n    \"react\", // require(\"eslint-plugin-react\")\n    \"jsx-a11y\", // require(\"eslint-plugin-jsx-a11y\")\n    // Temporarily disabled since they aren't vendored into in mozilla central yet\n    // \"react-hooks\", // require(\"react-hooks\")\n  ],\n  \"settings\": {\n    \"react\": {\n      \"version\": \"16.2.0\"\n    }\n  },\n  \"extends\": [\n    \"eslint:recommended\",\n    \"plugin:jsx-a11y/recommended\", // require(\"eslint-plugin-jsx-a11y\")\n    \"plugin:mozilla/recommended\", // require(\"eslint-plugin-mozilla\") require(\"eslint-plugin-fetch-options\") require(\"eslint-plugin-html\") require(\"eslint-plugin-no-unsanitized\")\n    \"plugin:mozilla/browser-test\",\n    \"plugin:mozilla/mochitest-test\",\n    \"plugin:mozilla/xpcshell-test\",\n    \"plugin:prettier/recommended\", // require(\"eslint-plugin-prettier\")\n    \"prettier/react\", // require(\"eslint-config-prettier\")\n  ],\n  \"globals\": {\n    // Remove this when m-c updates their eslint: See https://github.com/mozilla/activity-stream/pull/4219\n    \"RPMSendAsyncMessage\": true,\n    \"NewTabPagePreloading\": true,\n  },\n  \"overrides\": [\n    {\n      // These files use fluent-dom to insert content\n      \"files\": [\n        \"content-src/asrouter/templates/OnboardingMessage/**\",\n        \"content-src/asrouter/templates/FirstRun/**\",\n        \"content-src/asrouter/templates/Trailhead/**\",\n        \"content-src/asrouter/templates/FullPageInterrupt/FullPageInterrupt.jsx\",\n        \"content-src/asrouter/components/FxASignupForm/FxASignupForm.jsx\",\n        \"content-src/components/TopSites/**\",\n        \"content-src/components/MoreRecommendations/MoreRecommendations.jsx\",\n        \"content-src/components/CollapsibleSection/CollapsibleSection.jsx\",\n        \"content-src/components/DiscoveryStreamComponents/DSEmptyState/DSEmptyState.jsx\",\n        \"content-src/components/DiscoveryStreamComponents/DSPrivacyModal/DSPrivacyModal.jsx\"\n      ],\n      \"rules\": {\n        \"jsx-a11y/anchor-has-content\": 0,\n        \"jsx-a11y/heading-has-content\": 0,\n      }\n    },\n    {\n      // Use a configuration that's more appropriate for JSMs\n      \"files\": \"**/*.jsm\",\n      \"parserOptions\": {\n        \"sourceType\": \"script\"\n      },\n      \"env\": {\n        \"node\": false\n      },\n      \"rules\": {\n        \"no-implicit-globals\": 0\n      }\n    }\n  ],\n  \"rules\": {\n    // \"react-hooks/rules-of-hooks\": 2,\n\n    \"fetch-options/no-fetch-credentials\": 2,\n\n    \"react/jsx-boolean-value\": [2, \"always\"],\n    \"react/jsx-key\": 2,\n    \"react/jsx-no-bind\": 2,\n    \"react/jsx-no-comment-textnodes\": 2,\n    \"react/jsx-no-duplicate-props\": 2,\n    \"react/jsx-no-target-blank\": 2,\n    \"react/jsx-no-undef\": 2,\n    \"react/jsx-pascal-case\": 2,\n    \"react/jsx-uses-react\": 2,\n    \"react/jsx-uses-vars\": 2,\n    \"react/no-access-state-in-setstate\": 2,\n    \"react/no-danger\": 2,\n    \"react/no-deprecated\": 2,\n    \"react/no-did-mount-set-state\": 2,\n    \"react/no-did-update-set-state\": 2,\n    \"react/no-direct-mutation-state\": 2,\n    \"react/no-is-mounted\": 2,\n    \"react/no-unknown-property\": 2,\n    \"react/require-render-return\": 2,\n\n    \"accessor-pairs\": [2, {\"setWithoutGet\": true, \"getWithoutSet\": false}],\n    \"array-callback-return\": 2,\n    \"block-scoped-var\": 2,\n    \"callback-return\": 0,\n    \"camelcase\": 0,\n    \"capitalized-comments\": 0,\n    \"class-methods-use-this\": 0,\n    \"consistent-this\": [2, \"use-bind\"],\n    \"default-case\": 0,\n    \"eqeqeq\": 2,\n    \"for-direction\": 2,\n    \"func-name-matching\": 2,\n    \"func-names\": 0,\n    \"func-style\": 0,\n    \"getter-return\": 2,\n    \"global-require\": 0,\n    \"guard-for-in\": 2,\n    \"handle-callback-err\": 2,\n    \"id-blacklist\": 0,\n    \"id-length\": 0,\n    \"id-match\": 0,\n    \"init-declarations\": 0,\n    \"line-comment-position\": 0,\n    \"lines-between-class-members\": 2,\n    \"max-depth\": [2, 4],\n    \"max-lines\": 0,\n    \"max-nested-callbacks\": [2, 4],\n    \"max-params\": [2, 6],\n    \"max-statements\": [2, 50],\n    \"max-statements-per-line\": [2, {\"max\": 2}],\n    \"multiline-comment-style\": 0,\n    \"new-cap\": [2, {\"newIsCap\": true, \"capIsNew\": false}],\n    \"newline-after-var\": 0,\n    \"newline-before-return\": 0,\n    \"no-alert\": 2,\n    \"no-await-in-loop\": 0,\n    \"no-bitwise\": 0,\n    \"no-buffer-constructor\": 2,\n    \"no-catch-shadow\": 2,\n    \"no-console\": 1,\n    \"no-continue\": 0,\n    \"no-div-regex\": 2,\n    \"no-duplicate-imports\": 2,\n    \"no-empty-function\": 0,\n    \"no-eq-null\": 2,\n    \"no-extend-native\": 2,\n    \"no-extra-label\": 2,\n    \"no-implicit-coercion\": [2, {\"allow\": [\"!!\"]}],\n    \"no-implicit-globals\": 2,\n    \"no-inline-comments\": 0,\n    \"no-invalid-this\": 0,\n    \"no-label-var\": 2,\n    \"no-loop-func\": 2,\n    \"no-magic-numbers\": 0,\n    \"no-mixed-requires\": 2,\n    \"no-multi-assign\": 2,\n    \"no-multi-str\": 2,\n    \"no-negated-condition\": 0,\n    \"no-negated-in-lhs\": 2,\n    \"no-new\": 2,\n    \"no-new-func\": 2,\n    \"no-new-require\": 2,\n    \"no-octal-escape\": 2,\n    \"no-param-reassign\": 2,\n    \"no-path-concat\": 2,\n    \"no-plusplus\": 0,\n    \"no-process-env\": 0,\n    \"no-process-exit\": 2,\n    \"no-proto\": 2,\n    \"no-prototype-builtins\": 2,\n    \"no-restricted-globals\": 0,\n    \"no-restricted-imports\": 0,\n    \"no-restricted-modules\": 0,\n    \"no-restricted-properties\": 0,\n    \"no-restricted-syntax\": 0,\n    \"no-return-assign\": [2, \"except-parens\"],\n    \"no-script-url\": 2,\n    \"no-shadow\": 2,\n    \"no-sync\": 0,\n    \"no-template-curly-in-string\": 2,\n    \"no-ternary\": 0,\n    \"no-undef-init\": 2,\n    \"no-undefined\": 0,\n    \"no-underscore-dangle\": 0,\n    \"no-unmodified-loop-condition\": 2,\n    \"no-unused-expressions\": 2,\n    \"no-use-before-define\": 2,\n    \"no-useless-computed-key\": 2,\n    \"no-useless-constructor\": 2,\n    \"no-useless-rename\": 2,\n    \"no-var\": 2,\n    \"no-void\": 2,\n    \"no-warning-comments\": 0, // TODO: Change to `1`?\n    \"one-var\": [2, \"never\"],\n    \"operator-assignment\": [2, \"always\"],\n    \"padding-line-between-statements\": 0,\n    \"prefer-const\": 0, // TODO: Change to `1`?\n    \"prefer-destructuring\": [2, {\"AssignmentExpression\": {\"array\": true}, \"VariableDeclarator\": {\"array\": true, \"object\": true}}],\n    \"prefer-numeric-literals\": 2,\n    \"prefer-promise-reject-errors\": 2,\n    \"prefer-reflect\": 0,\n    \"prefer-rest-params\": 2,\n    \"prefer-spread\": 2,\n    \"prefer-template\": 2,\n    \"radix\": [2, \"always\"],\n    \"require-await\": 2,\n    \"require-jsdoc\": 0,\n    \"sort-keys\": 0,\n    \"sort-vars\": 2,\n    \"strict\": 0,\n    \"symbol-description\": 2,\n    \"valid-jsdoc\": [0, {\"requireReturn\": false, \"requireParamDescription\": false, \"requireReturnDescription\": false}],\n    \"vars-on-top\": 2,\n    \"yoda\": [2, \"never\"]\n  }\n};\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\nnpm-debug.log\n.DS_Store\n.eslintcache\n*.sw[po]\n*.xpi\n*.pyc\nlogs/\ndist/\nfirefox/\n*.update.rdf\ndata/content/activity-stream.bundle.js\ncss/*.css\nprerendered/\naboutlibrary/content/aboutlibrary.bundle.js\naboutlibrary/content/*.map\naboutlibrary/content/*.css\n"
  },
  {
    "path": ".mcignore",
    "content": "npm-debug.log\n.DS_Store\n*.sw[po]\n*.xpi\n*.pyc\n*.update.rdf\n.gitignore\n.eslintcache\n\n/.git/\n/dist/\n/logs/\n/node_modules/\n\n# ignore README since it's GitHub specific\n/README.md\n\n# also ignores ping centre tests\nping-centre/\n\n# ignore things from about:library for now\naboutlibrary/\ncontent-src/aboutlibrary/\n"
  },
  {
    "path": ".nvmrc",
    "content": "8.16\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"printWidth\": 80,\n  \"tabWidth\": 2,\n  \"trailingComma\": \"es5\"\n}\n"
  },
  {
    "path": ".sass-lint.yml",
    "content": "options:\n  merge-default-rules: true\n  max-warnings: 0\n\nfiles:\n  include: 'content-src/**/*.scss'\n\nrules:\n  class-name-format: 0\n  extends-before-declarations: 2\n  extends-before-mixins: 2\n  force-element-nesting: 0\n  force-pseudo-nesting: 0\n  hex-notation: [2, {style: uppercase}]\n  indentation: [2, {size: 2}]\n  leading-zero: [2, {include: true}]\n  mixins-before-declarations: [2, {exclude: [breakpoint, mq]}]\n  nesting-depth: [2, {max-depth: 4}]\n  no-debug: 1\n  no-disallowed-properties: [1, {properties: [margin-left, margin-right, text-transform]}]\n  no-duplicate-properties: 2\n  no-misspelled-properties: [2, {extra-properties: [-moz-context-properties]}]\n  no-url-domains: 0\n  no-vendor-prefixes: 0\n  no-warn: 1\n  placeholder-in-extend: 2\n  property-sort-order: 0\n"
  },
  {
    "path": ".taskcluster.yml",
    "content": "version: 1\npolicy:\n  pullRequests: public\ntasks:\n  $if: 'tasks_for in [\"github-push\", \"github-pull-request\"]'\n  then:\n    $let:\n      repo_url:\n        $if: 'tasks_for == \"github-push\"'\n        then: ${event.repository.clone_url}\n        else: ${event.pull_request.head.repo.clone_url}\n      ref:\n        $if: 'tasks_for == \"github-push\"'\n        then: ${event.after}\n        else: ${event.pull_request.head.sha}\n    in:\n    - provisionerId: proj-misc\n      workerType: ci\n      deadline: ${fromNow('1 day')}\n      payload:\n        maxRunTime: 7200\n        image: piatra/asmochitests\n        command:\n          - /bin/bash\n          - '--login'\n          - '-c'\n          - >-\n            git clone ${repo_url} /activity-stream && cd /activity-stream &&\n            git checkout ${ref} && bash ./mochitest.sh\n      metadata:\n        name: activitystream\n        description: run mochitests for PRs\n        owner: noreply@mozilla.com\n        source: ${repo_url}\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: node_js\n\nnode_js:\n  # when changing this, be sure to edit .nvrmc and package.json too\n  - 8\n\npython:\n  - \"2.7\"\n\naddons:\n  # Run unit tests in Nightly to be in line with what Firefox tests would run against\n  firefox: \"latest-nightly\"\n\ncache:\n  directories:\n    - node_modules\n\nbefore_install:\n  # see https://docs.travis-ci.com/user/gui-and-headless-browsers/#Using-xvfb-to-Run-Tests-That-Require-a-GUI\n  - \"export DISPLAY=:99.0\"\n  - \"/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x16 -extension RANDR\"\n  - export PATH=\"$PATH:$HOME/.rvm/bin\"\n  - export PATH=\"$PATH:./node_modules/.bin\"\n  - sleep 3\n\ninstall:\n  - npm config set spin false\n  - npm install\n\nscript:\n  - npm test\n\nnotifications:\n  email: false\n"
  },
  {
    "path": "AboutNewTabService.jsm",
    "content": "/*\n * This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/.\n */\n\n\"use strict\";\n\nconst { Services } = ChromeUtils.import(\"resource://gre/modules/Services.jsm\");\nconst { AppConstants } = ChromeUtils.import(\n  \"resource://gre/modules/AppConstants.jsm\"\n);\nconst { E10SUtils } = ChromeUtils.import(\n  \"resource://gre/modules/E10SUtils.jsm\"\n);\n\nChromeUtils.defineModuleGetter(\n  this,\n  \"AboutNewTab\",\n  \"resource:///modules/AboutNewTab.jsm\"\n);\n\nconst TOPIC_APP_QUIT = \"quit-application-granted\";\nconst TOPIC_CONTENT_DOCUMENT_INTERACTIVE = \"content-document-interactive\";\n\nconst ABOUT_URL = \"about:newtab\";\nconst BASE_URL = \"resource://activity-stream/\";\nconst ACTIVITY_STREAM_PAGES = new Set([\"home\", \"newtab\", \"welcome\"]);\n\nconst IS_MAIN_PROCESS =\n  Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT;\nconst IS_PRIVILEGED_PROCESS =\n  Services.appinfo.remoteType === E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE;\n\nconst IS_RELEASE_OR_BETA = AppConstants.RELEASE_OR_BETA;\n\nconst PREF_SEPARATE_PRIVILEGEDABOUT_CONTENT_PROCESS =\n  \"browser.tabs.remote.separatePrivilegedContentProcess\";\nconst PREF_ACTIVITY_STREAM_DEBUG = \"browser.newtabpage.activity-stream.debug\";\n\nfunction AboutNewTabService() {\n  Services.obs.addObserver(this, TOPIC_APP_QUIT);\n  Services.prefs.addObserver(\n    PREF_SEPARATE_PRIVILEGEDABOUT_CONTENT_PROCESS,\n    this\n  );\n  if (!IS_RELEASE_OR_BETA) {\n    Services.prefs.addObserver(PREF_ACTIVITY_STREAM_DEBUG, this);\n  }\n\n  // More initialization happens here\n  this.toggleActivityStream(true);\n  this.initialized = true;\n  this.alreadyRecordedTopsitesPainted = false;\n\n  if (IS_MAIN_PROCESS) {\n    AboutNewTab.init();\n  } else if (IS_PRIVILEGED_PROCESS) {\n    Services.obs.addObserver(this, TOPIC_CONTENT_DOCUMENT_INTERACTIVE);\n  }\n}\n\n/*\n * A service that allows for the overriding, at runtime, of the newtab page's url.\n *\n * There is tight coupling with browser/about/AboutRedirector.cpp.\n *\n * 1. Browser chrome access:\n *\n * When the user issues a command to open a new tab page, usually clicking a button\n * in the browser chrome or using shortcut keys, the browser chrome code invokes the\n * service to obtain the newtab URL. It then loads that URL in a new tab.\n *\n * When not overridden, the default URL emitted by the service is \"about:newtab\".\n * When overridden, it returns the overriden URL.\n *\n * 2. Redirector Access:\n *\n * When the URL loaded is about:newtab, the default behavior, or when entered in the\n * URL bar, the redirector is hit. The service is then called to return the\n * appropriate activity stream url based on prefs.\n *\n * NOTE: \"about:newtab\" will always result in a default newtab page, and never an overridden URL.\n *\n * Access patterns:\n *\n * The behavior is different when accessing the service via browser chrome or via redirector\n * largely to maintain compatibility with expectations of add-on developers.\n *\n * Loading a chrome resource, or an about: URL in the redirector with either the\n * LOAD_NORMAL or LOAD_REPLACE flags yield unexpected behaviors, so a roundtrip\n * to the redirector from browser chrome is avoided.\n */\nAboutNewTabService.prototype = {\n  _newTabURL: ABOUT_URL,\n  _activityStreamEnabled: false,\n  _activityStreamDebug: false,\n  _privilegedAboutContentProcess: false,\n  _overridden: false,\n  willNotifyUser: false,\n\n  classID: Components.ID(\"{dfcd2adc-7867-4d3a-ba70-17501f208142}\"),\n  QueryInterface: ChromeUtils.generateQI([\n    Ci.nsIAboutNewTabService,\n    Ci.nsIObserver,\n  ]),\n\n  observe(subject, topic, data) {\n    switch (topic) {\n      case \"nsPref:changed\":\n        if (data === PREF_SEPARATE_PRIVILEGEDABOUT_CONTENT_PROCESS) {\n          this._privilegedAboutContentProcess = Services.prefs.getBoolPref(\n            PREF_SEPARATE_PRIVILEGEDABOUT_CONTENT_PROCESS\n          );\n          this.notifyChange();\n        } else if (!IS_RELEASE_OR_BETA && data === PREF_ACTIVITY_STREAM_DEBUG) {\n          this._activityStreamDebug = Services.prefs.getBoolPref(\n            PREF_ACTIVITY_STREAM_DEBUG,\n            false\n          );\n          this.notifyChange();\n        }\n        break;\n      case TOPIC_CONTENT_DOCUMENT_INTERACTIVE: {\n        const win = subject.defaultView;\n\n        // It seems like \"content-document-interactive\" is triggered multiple\n        // times for a single window. The first event always seems to be an\n        // HTMLDocument object that contains a non-null window reference\n        // whereas the remaining ones seem to be proxied objects.\n        // https://searchfox.org/mozilla-central/rev/d2966246905102b36ef5221b0e3cbccf7ea15a86/devtools/server/actors/object.js#100-102\n        if (win === null) {\n          break;\n        }\n\n        // We use win.location.pathname instead of win.location.toString()\n        // because we want to account for URLs that contain the location hash\n        // property or query strings (e.g. about:newtab#foo, about:home?bar).\n        // Asserting here would be ideal, but this code path is also taken\n        // by the view-source:// scheme, so we should probably just bail out\n        // and do nothing.\n        if (!ACTIVITY_STREAM_PAGES.has(win.location.pathname)) {\n          break;\n        }\n\n        const onLoaded = () => {\n          const debugString = this._activityStreamDebug ? \"-dev\" : \"\";\n\n          // This list must match any similar ones in render-activity-stream-html.js.\n          const scripts = [\n            \"chrome://browser/content/contentSearchUI.js\",\n            \"chrome://browser/content/contentTheme.js\",\n            `${BASE_URL}vendor/react${debugString}.js`,\n            `${BASE_URL}vendor/react-dom${debugString}.js`,\n            `${BASE_URL}vendor/prop-types.js`,\n            `${BASE_URL}vendor/react-transition-group.js`,\n            `${BASE_URL}vendor/redux.js`,\n            `${BASE_URL}vendor/react-redux.js`,\n            `${BASE_URL}data/content/activity-stream.bundle.js`,\n          ];\n\n          for (let script of scripts) {\n            Services.scriptloader.loadSubScript(script, win); // Synchronous call\n          }\n        };\n        subject.addEventListener(\"DOMContentLoaded\", onLoaded, { once: true });\n\n        // There is a possibility that DOMContentLoaded won't be fired. This\n        // unload event (which cannot be cancelled) will attempt to remove\n        // the listener for the DOMContentLoaded event.\n        const onUnloaded = () => {\n          subject.removeEventListener(\"DOMContentLoaded\", onLoaded);\n        };\n        subject.addEventListener(\"unload\", onUnloaded, { once: true });\n        break;\n      }\n      case TOPIC_APP_QUIT:\n        this.uninit();\n        if (IS_MAIN_PROCESS) {\n          AboutNewTab.uninit();\n        } else if (IS_PRIVILEGED_PROCESS) {\n          Services.obs.removeObserver(this, TOPIC_CONTENT_DOCUMENT_INTERACTIVE);\n        }\n        break;\n    }\n  },\n\n  notifyChange() {\n    Services.obs.notifyObservers(null, \"newtab-url-changed\", this._newTabURL);\n  },\n\n  /**\n   * React to changes to the activity stream being enabled or not.\n   *\n   * This will only act if there is a change of state and if not overridden.\n   *\n   * @returns {Boolean} Returns if there has been a state change\n   *\n   * @param {Boolean}   stateEnabled    activity stream enabled state to set to\n   * @param {Boolean}   forceState      force state change\n   */\n  toggleActivityStream(stateEnabled, forceState = false) {\n    if (\n      !forceState &&\n      (this.overridden || stateEnabled === this.activityStreamEnabled)\n    ) {\n      // exit there is no change of state\n      return false;\n    }\n    if (stateEnabled) {\n      this._activityStreamEnabled = true;\n    } else {\n      this._activityStreamEnabled = false;\n    }\n    this._privilegedAboutContentProcess = Services.prefs.getBoolPref(\n      PREF_SEPARATE_PRIVILEGEDABOUT_CONTENT_PROCESS\n    );\n    if (!IS_RELEASE_OR_BETA) {\n      this._activityStreamDebug = Services.prefs.getBoolPref(\n        PREF_ACTIVITY_STREAM_DEBUG,\n        false\n      );\n    }\n    this._newtabURL = ABOUT_URL;\n    return true;\n  },\n\n  /*\n   * Returns the default URL.\n   *\n   * This URL depends on various activity stream prefs. Overriding\n   * the newtab page has no effect on the result of this function.\n   */\n  get defaultURL() {\n    // Generate the desired activity stream resource depending on state, e.g.,\n    // \"resource://activity-stream/prerendered/activity-stream.html\"\n    // \"resource://activity-stream/prerendered/activity-stream-debug.html\"\n    // \"resource://activity-stream/prerendered/activity-stream-noscripts.html\"\n    return [\n      \"resource://activity-stream/prerendered/\",\n      \"activity-stream\",\n      // Debug version loads dev scripts but noscripts separately loads scripts\n      this._activityStreamDebug && !this._privilegedAboutContentProcess\n        ? \"-debug\"\n        : \"\",\n      this._privilegedAboutContentProcess ? \"-noscripts\" : \"\",\n      \".html\",\n    ].join(\"\");\n  },\n\n  /*\n   * Returns the about:welcome URL\n   *\n   * This is calculated in the same way the default URL is.\n   */\n  get welcomeURL() {\n    return this.defaultURL;\n  },\n\n  get newTabURL() {\n    return this._newTabURL;\n  },\n\n  set newTabURL(aNewTabURL) {\n    let newTabURL = aNewTabURL.trim();\n    if (newTabURL === ABOUT_URL) {\n      // avoid infinite redirects in case one sets the URL to about:newtab\n      this.resetNewTabURL();\n      return;\n    } else if (newTabURL === \"\") {\n      newTabURL = \"about:blank\";\n    }\n\n    this.toggleActivityStream(false);\n    this._newTabURL = newTabURL;\n    this._overridden = true;\n    this.notifyChange();\n  },\n\n  get overridden() {\n    return this._overridden;\n  },\n\n  get activityStreamEnabled() {\n    return this._activityStreamEnabled;\n  },\n\n  get activityStreamDebug() {\n    return this._activityStreamDebug;\n  },\n\n  resetNewTabURL() {\n    this._overridden = false;\n    this._newTabURL = ABOUT_URL;\n    this.toggleActivityStream(true, true);\n    this.notifyChange();\n  },\n\n  maybeRecordTopsitesPainted(timestamp) {\n    if (this.alreadyRecordedTopsitesPainted) {\n      return;\n    }\n\n    const SCALAR_KEY = \"timestamps.about_home_topsites_first_paint\";\n\n    let startupInfo = Services.startup.getStartupInfo();\n    let processStartTs = startupInfo.process.getTime();\n    let delta = Math.round(timestamp - processStartTs);\n    Services.telemetry.scalarSet(SCALAR_KEY, delta);\n    this.alreadyRecordedTopsitesPainted = true;\n  },\n\n  uninit() {\n    if (!this.initialized) {\n      return;\n    }\n    Services.obs.removeObserver(this, TOPIC_APP_QUIT);\n    Services.prefs.removeObserver(\n      PREF_SEPARATE_PRIVILEGEDABOUT_CONTENT_PROCESS,\n      this\n    );\n    if (!IS_RELEASE_OR_BETA) {\n      Services.prefs.removeObserver(PREF_ACTIVITY_STREAM_DEBUG, this);\n    }\n    this.initialized = false;\n  },\n};\n\nconst EXPORTED_SYMBOLS = [\"AboutNewTabService\"];\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Community Participation Guidelines\n\nThis repository is governed by Mozilla's code of conduct and etiquette guidelines. \nFor more details, please read the\n[Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). \n\n## How to Report\nFor more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page.\n\n<!--\n## Project Specific Etiquette\n\nIn some cases, there will be additional project etiquette i.e.: (https://bugzilla.mozilla.org/page.cgi?id=etiquette.html).\nPlease update for your project.\n-->\n"
  },
  {
    "path": "LICENSE",
    "content": "Mozilla Public License Version 2.0\n==================================\n\n1. Definitions\n--------------\n\n1.1. \"Contributor\"\n    means each individual or legal entity that creates, contributes to\n    the creation of, or owns Covered Software.\n\n1.2. \"Contributor Version\"\n    means the combination of the Contributions of others (if any) used\n    by a Contributor and that particular Contributor's Contribution.\n\n1.3. \"Contribution\"\n    means Covered Software of a particular Contributor.\n\n1.4. \"Covered Software\"\n    means Source Code Form to which the initial Contributor has attached\n    the notice in Exhibit A, the Executable Form of such Source Code\n    Form, and Modifications of such Source Code Form, in each case\n    including portions thereof.\n\n1.5. \"Incompatible With Secondary Licenses\"\n    means\n\n    (a) that the initial Contributor has attached the notice described\n        in Exhibit B to the Covered Software; or\n\n    (b) that the Covered Software was made available under the terms of\n        version 1.1 or earlier of the License, but not also under the\n        terms of a Secondary License.\n\n1.6. \"Executable Form\"\n    means any form of the work other than Source Code Form.\n\n1.7. \"Larger Work\"\n    means a work that combines Covered Software with other material, in \n    a separate file or files, that is not Covered Software.\n\n1.8. \"License\"\n    means this document.\n\n1.9. \"Licensable\"\n    means having the right to grant, to the maximum extent possible,\n    whether at the time of the initial grant or subsequently, any and\n    all of the rights conveyed by this License.\n\n1.10. \"Modifications\"\n    means any of the following:\n\n    (a) any file in Source Code Form that results from an addition to,\n        deletion from, or modification of the contents of Covered\n        Software; or\n\n    (b) any new file in Source Code Form that contains any Covered\n        Software.\n\n1.11. \"Patent Claims\" of a Contributor\n    means any patent claim(s), including without limitation, method,\n    process, and apparatus claims, in any patent Licensable by such\n    Contributor that would be infringed, but for the grant of the\n    License, by the making, using, selling, offering for sale, having\n    made, import, or transfer of either its Contributions or its\n    Contributor Version.\n\n1.12. \"Secondary License\"\n    means either the GNU General Public License, Version 2.0, the GNU\n    Lesser General Public License, Version 2.1, the GNU Affero General\n    Public License, Version 3.0, or any later versions of those\n    licenses.\n\n1.13. \"Source Code Form\"\n    means the form of the work preferred for making modifications.\n\n1.14. \"You\" (or \"Your\")\n    means an individual or a legal entity exercising rights under this\n    License. For legal entities, \"You\" includes any entity that\n    controls, is controlled by, or is under common control with You. For\n    purposes of this definition, \"control\" means (a) the power, direct\n    or indirect, to cause the direction or management of such entity,\n    whether by contract or otherwise, or (b) ownership of more than\n    fifty percent (50%) of the outstanding shares or beneficial\n    ownership of such entity.\n\n2. License Grants and Conditions\n--------------------------------\n\n2.1. Grants\n\nEach Contributor hereby grants You a world-wide, royalty-free,\nnon-exclusive license:\n\n(a) under intellectual property rights (other than patent or trademark)\n    Licensable by such Contributor to use, reproduce, make available,\n    modify, display, perform, distribute, and otherwise exploit its\n    Contributions, either on an unmodified basis, with Modifications, or\n    as part of a Larger Work; and\n\n(b) under Patent Claims of such Contributor to make, use, sell, offer\n    for sale, have made, import, and otherwise transfer either its\n    Contributions or its Contributor Version.\n\n2.2. Effective Date\n\nThe licenses granted in Section 2.1 with respect to any Contribution\nbecome effective for each Contribution on the date the Contributor first\ndistributes such Contribution.\n\n2.3. Limitations on Grant Scope\n\nThe licenses granted in this Section 2 are the only rights granted under\nthis License. No additional rights or licenses will be implied from the\ndistribution or licensing of Covered Software under this License.\nNotwithstanding Section 2.1(b) above, no patent license is granted by a\nContributor:\n\n(a) for any code that a Contributor has removed from Covered Software;\n    or\n\n(b) for infringements caused by: (i) Your and any other third party's\n    modifications of Covered Software, or (ii) the combination of its\n    Contributions with other software (except as part of its Contributor\n    Version); or\n\n(c) under Patent Claims infringed by Covered Software in the absence of\n    its Contributions.\n\nThis License does not grant any rights in the trademarks, service marks,\nor logos of any Contributor (except as may be necessary to comply with\nthe notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\nNo Contributor makes additional grants as a result of Your choice to\ndistribute the Covered Software under a subsequent version of this\nLicense (see Section 10.2) or under the terms of a Secondary License (if\npermitted under the terms of Section 3.3).\n\n2.5. Representation\n\nEach Contributor represents that the Contributor believes its\nContributions are its original creation(s) or it has sufficient rights\nto grant the rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\nThis License is not intended to limit any rights You have under\napplicable copyright doctrines of fair use, fair dealing, or other\nequivalents.\n\n2.7. Conditions\n\nSections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted\nin Section 2.1.\n\n3. Responsibilities\n-------------------\n\n3.1. Distribution of Source Form\n\nAll distribution of Covered Software in Source Code Form, including any\nModifications that You create or to which You contribute, must be under\nthe terms of this License. You must inform recipients that the Source\nCode Form of the Covered Software is governed by the terms of this\nLicense, and how they can obtain a copy of this License. You may not\nattempt to alter or restrict the recipients' rights in the Source Code\nForm.\n\n3.2. Distribution of Executable Form\n\nIf You distribute Covered Software in Executable Form then:\n\n(a) such Covered Software must also be made available in Source Code\n    Form, as described in Section 3.1, and You must inform recipients of\n    the Executable Form how they can obtain a copy of such Source Code\n    Form by reasonable means in a timely manner, at a charge no more\n    than the cost of distribution to the recipient; and\n\n(b) You may distribute such Executable Form under the terms of this\n    License, or sublicense it under different terms, provided that the\n    license for the Executable Form does not attempt to limit or alter\n    the recipients' rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\nYou may create and distribute a Larger Work under terms of Your choice,\nprovided that You also comply with the requirements of this License for\nthe Covered Software. If the Larger Work is a combination of Covered\nSoftware with a work governed by one or more Secondary Licenses, and the\nCovered Software is not Incompatible With Secondary Licenses, this\nLicense permits You to additionally distribute such Covered Software\nunder the terms of such Secondary License(s), so that the recipient of\nthe Larger Work may, at their option, further distribute the Covered\nSoftware under the terms of either this License or such Secondary\nLicense(s).\n\n3.4. Notices\n\nYou may not remove or alter the substance of any license notices\n(including copyright notices, patent notices, disclaimers of warranty,\nor limitations of liability) contained within the Source Code Form of\nthe Covered Software, except that You may alter any license notices to\nthe extent required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\nYou may choose to offer, and to charge a fee for, warranty, support,\nindemnity or liability obligations to one or more recipients of Covered\nSoftware. However, You may do so only on Your own behalf, and not on\nbehalf of any Contributor. You must make it absolutely clear that any\nsuch warranty, support, indemnity, or liability obligation is offered by\nYou alone, and You hereby agree to indemnify every Contributor for any\nliability incurred by such Contributor as a result of warranty, support,\nindemnity or liability terms You offer. You may include additional\ndisclaimers of warranty and limitations of liability specific to any\njurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n---------------------------------------------------\n\nIf it is impossible for You to comply with any of the terms of this\nLicense with respect to some or all of the Covered Software due to\nstatute, judicial order, or regulation then You must: (a) comply with\nthe terms of this License to the maximum extent possible; and (b)\ndescribe the limitations and the code they affect. Such description must\nbe placed in a text file included with all distributions of the Covered\nSoftware under this License. Except to the extent prohibited by statute\nor regulation, such description must be sufficiently detailed for a\nrecipient of ordinary skill to be able to understand it.\n\n5. Termination\n--------------\n\n5.1. The rights granted under this License will terminate automatically\nif You fail to comply with any of its terms. However, if You become\ncompliant, then the rights granted under this License from a particular\nContributor are reinstated (a) provisionally, unless and until such\nContributor explicitly and finally terminates Your grants, and (b) on an\nongoing basis, if such Contributor fails to notify You of the\nnon-compliance by some reasonable means prior to 60 days after You have\ncome back into compliance. Moreover, Your grants from a particular\nContributor are reinstated on an ongoing basis if such Contributor\nnotifies You of the non-compliance by some reasonable means, this is the\nfirst time You have received notice of non-compliance with this License\nfrom such Contributor, and You become compliant prior to 30 days after\nYour receipt of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\ninfringement claim (excluding declaratory judgment actions,\ncounter-claims, and cross-claims) alleging that a Contributor Version\ndirectly or indirectly infringes any patent, then the rights granted to\nYou by any and all Contributors for the Covered Software under Section\n2.1 of this License shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all\nend user license agreements (excluding distributors and resellers) which\nhave been validly granted by You or Your distributors under this License\nprior to termination shall survive termination.\n\n************************************************************************\n*                                                                      *\n*  6. Disclaimer of Warranty                                           *\n*  -------------------------                                           *\n*                                                                      *\n*  Covered Software is provided under this License on an \"as is\"       *\n*  basis, without warranty of any kind, either expressed, implied, or  *\n*  statutory, including, without limitation, warranties that the       *\n*  Covered Software is free of defects, merchantable, fit for a        *\n*  particular purpose or non-infringing. The entire risk as to the     *\n*  quality and performance of the Covered Software is with You.        *\n*  Should any Covered Software prove defective in any respect, You     *\n*  (not any Contributor) assume the cost of any necessary servicing,   *\n*  repair, or correction. This disclaimer of warranty constitutes an   *\n*  essential part of this License. No use of any Covered Software is   *\n*  authorized under this License except under this disclaimer.         *\n*                                                                      *\n************************************************************************\n\n************************************************************************\n*                                                                      *\n*  7. Limitation of Liability                                          *\n*  --------------------------                                          *\n*                                                                      *\n*  Under no circumstances and under no legal theory, whether tort      *\n*  (including negligence), contract, or otherwise, shall any           *\n*  Contributor, or anyone who distributes Covered Software as          *\n*  permitted above, be liable to You for any direct, indirect,         *\n*  special, incidental, or consequential damages of any character      *\n*  including, without limitation, damages for lost profits, loss of    *\n*  goodwill, work stoppage, computer failure or malfunction, or any    *\n*  and all other commercial damages or losses, even if such party      *\n*  shall have been informed of the possibility of such damages. This   *\n*  limitation of liability shall not apply to liability for death or   *\n*  personal injury resulting from such party's negligence to the       *\n*  extent applicable law prohibits such limitation. Some               *\n*  jurisdictions do not allow the exclusion or limitation of           *\n*  incidental or consequential damages, so this exclusion and          *\n*  limitation may not apply to You.                                    *\n*                                                                      *\n************************************************************************\n\n8. Litigation\n-------------\n\nAny litigation relating to this License may be brought only in the\ncourts of a jurisdiction where the defendant maintains its principal\nplace of business and such litigation shall be governed by laws of that\njurisdiction, without reference to its conflict-of-law provisions.\nNothing in this Section shall prevent a party's ability to bring\ncross-claims or counter-claims.\n\n9. Miscellaneous\n----------------\n\nThis License represents the complete agreement concerning the subject\nmatter hereof. If any provision of this License is held to be\nunenforceable, such provision shall be reformed only to the extent\nnecessary to make it enforceable. Any law or regulation which provides\nthat the language of a contract shall be construed against the drafter\nshall not be used to construe this License against a Contributor.\n\n10. Versions of the License\n---------------------------\n\n10.1. New Versions\n\nMozilla Foundation is the license steward. Except as provided in Section\n10.3, no one other than the license steward has the right to modify or\npublish new versions of this License. Each version will be given a\ndistinguishing version number.\n\n10.2. Effect of New Versions\n\nYou may distribute the Covered Software under the terms of the version\nof the License under which You originally received the Covered Software,\nor under the terms of any subsequent version published by the license\nsteward.\n\n10.3. Modified Versions\n\nIf you create software not governed by this License, and you want to\ncreate a new license for such software, you may create and use a\nmodified version of this License if you rename the license and remove\nany references to the name of the license steward (except to note that\nsuch modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary\nLicenses\n\nIf You choose to distribute Source Code Form that is Incompatible With\nSecondary Licenses under the terms of this version of the License, the\nnotice described in Exhibit B of this License must be attached.\n\nExhibit A - Source Code Form License Notice\n-------------------------------------------\n\n  This Source Code Form is subject to the terms of the Mozilla Public\n  License, v. 2.0. If a copy of the MPL was not distributed with this\n  file, You can obtain one at http://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular\nfile, then You may include the notice in a location (such as a LICENSE\nfile in a relevant directory) where a recipient would be likely to look\nfor such a notice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - \"Incompatible With Secondary Licenses\" Notice\n---------------------------------------------------------\n\n  This Source Code Form is \"Incompatible With Secondary Licenses\", as\n  defined by the Mozilla Public License, v. 2.0.\n\n"
  },
  {
    "path": "README.md",
    "content": "# Firefox Home (New Tab) [Deprecated Version]\n\nThis repository is no longer updated or used. We're keeping it around for\nthose few occasions when it's useful for doing code & bugfix archaeology by\nlooking at issues and PRs.  \n\nPlease do not file new issues or PRs; they will not be triaged.  Issues are now\ntracked on Bugzilla, in `Firefox: New Tab Page` and `Firefox: Messaging\nSystem`.\n\nMore current links:\n\n* Docs for [Firefox Home (New Tab)](https://firefox-source-docs.mozilla.org/browser/components/newtab/docs/index.html)\n* Docs for [Messaging System](https://firefox-source-docs.mozilla.org/browser/components/newtab/content-src/asrouter/docs/index.html)\n* [Code](https://searchfox.org/mozilla-central/source/browser/components/newtab)\n\n--------------\n\nThe files in this directory, including vendor dependencies, are exported to the\nbrowser/components/newtab/ directory in mozilla central.\n\nRead [docs/v2-system-addon](https://github.com/mozilla/activity-stream/tree/master/docs/v2-system-addon/1.GETTING_STARTED.md) for more detail on how to develop on and use this repository.\n\n## Where should I file bugs?\n\nWe regularly check the ActivityStream:NewTab component on Bugzilla.\n\n## For Developers\n\nIf you are interested in contributing, take a look at [this guide](contributing.md) on where to find us and how to contribute,\nand [this guide](docs/v2-system-addon/1.GETTING_STARTED.md) for getting your development environment set up.\n\n## For Localizers\n\nFirefox Home localization is managed via [Pontoon](https://pontoon.mozilla.org/projects/activity-stream-new-tab/), not direct pull requests to the repository. If you want to fix a typo, add a new language, or simply know more about localization, please get in touch with the [existing localization team](https://pontoon.mozilla.org/teams/) for your language, or Mozilla’s [l10n-drivers](https://wiki.mozilla.org/L10n:Mozilla_Team#Mozilla_Corporation) for guidance.\n"
  },
  {
    "path": "aboutlibrary/content/aboutlibrary.xhtml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n# This Source Code Form is subject to the terms of the Mozilla Public\n# License, v. 2.0. If a copy of the MPL was not distributed with this\n# file, You can obtain one at http://mozilla.org/MPL/2.0/.\n-->\n<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n  <head>\n    <meta http-equiv=\"Content-Security-Policy\" content=\"default-src data: chrome: resource:\" />\n    <title>Library</title>\n    <link rel=\"stylesheet\" href=\"chrome://browser/content/aboutlibrary.css\" type=\"text/css\" media=\"all\"/>\n    <link rel=\"icon\" type=\"image/png\" href=\"chrome://branding/content/icon32.png\"/>\n  </head>\n  <body>\n    <script src=\"resource://activity-stream/vendor/react.js\"> </script>\n    <script src=\"resource://activity-stream/vendor/react-dom.js\"> </script>\n    <script src=\"resource://activity-stream/vendor/prop-types.js\"> </script>\n    <script src=\"resource://activity-stream/vendor/react-intl.js\"> </script>\n    <script src=\"resource://activity-stream/vendor/redux.js\"> </script>\n    <script src=\"resource://activity-stream/vendor/react-redux.js\"> </script>\n    <script src=\"chrome://browser/content/aboutlibrary.bundle.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "aboutlibrary/jar.mn",
    "content": "# This Source Code Form is subject to the terms of the Mozilla Public\n# License, v. 2.0. If a copy of the MPL was not distributed with this\n# file, You can obtain one at http://mozilla.org/MPL/2.0/.\nbrowser.jar:\n   content/browser/             (content/*)\n"
  },
  {
    "path": "aboutlibrary/moz.build",
    "content": "# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-\n# vim: set filetype=python:\n# This Source Code Form is subject to the terms of the Mozilla Public\n# License, v. 2.0. If a copy of the MPL was not distributed with this\n# file, You can obtain one at http://mozilla.org/MPL/2.0/.\n\nJAR_MANIFESTS += ['jar.mn']\nFINAL_LIBRARY = 'browsercomps'\n\nwith Files('**'):\n    BUG_COMPONENT = ('Firefox', 'Library')\n"
  },
  {
    "path": "bin/bootstrap",
    "content": "#!/bin/sh -x\n\n# bootstrap an activity-stream repo\nln -s ../../hooks/pre-commit .git/hooks/pre-commit\nln -s ../../hooks/post-commit .git/hooks/post-commit\n"
  },
  {
    "path": "bin/download-firefox-artifact",
    "content": "#!/usr/bin/env bash -x\n\n# Forked from https://github.com/devtools-html/debugger.html/blob/master/bin/download-firefox-artifact\n#\n# This looks for a mozilla-central artifact build as a sibling of the\n# activity-stream tree.  If it's not there, it creates it.  If it is there, it\n# updates it.\n\n# If AS_GIT_BIN_REPO (the git repo from which prepare-mochitests-dev and\n# friends will be executed) isn't set in the environment, just use the repo\n#  we're running from.\nif [ -z ${AS_GIT_BIN_REPO+x} ]; then\n  ROOT=`dirname $0`\n  AS_GIT_BIN_REPO=\"../../../../activity-stream\"\nelse\n  ROOT=${AS_GIT_BIN_REPO}/bin\nfi\n\n# Compute the mozilla-central path based on whether AS_PINE_TEST_DIR is set\n# (i.e. whether this script has been called from test-merges.js)\nif [ -z ${AS_PINE_TEST_DIR+x} ]; then\n  FIREFOX_PATH=\"$ROOT/../../mozilla-central\"\nelse\n  FIREFOX_PATH=${AS_PINE_TEST_DIR}/mozilla-central\nfi\n\n# check that mercurial is installed\nif [ -z \"`command -v hg`\" ]; then\n  echo >&2 \"mercurial is required for mochitests, use 'brew install mercurial' on MacOS\";\n  exit 1;\nfi\n\nif [ -d \"$FIREFOX_PATH\" ]; then\n    # convert path to absolute path\n    FIREFOX_PATH=$(cd \"$FIREFOX_PATH\"; pwd)\n\n    # If we already have Firefox locally, just update it\n    cd \"$FIREFOX_PATH\";\n\n    if [ -n \"`hg status`\" ]; then\n        read -p \"There are local changes to Firefox which will be overwritten. Are you sure? [Y/n] \" -r\n        if [[ $REPLY == \"n\" ]]; then\n            exit 0;\n        fi\n\n        hg revert -a\n    fi\n\n    hg pull\n    hg update -C\nelse\n    echo \"Downloading Firefox source code, requires about 10-30min depending on connection\"\n    hg clone https://hg.mozilla.org/mozilla-central/ \"$FIREFOX_PATH\"\n    # if somebody cancels (ctrl-c) out of the long download don't continue\n    exit_code=$?\n    if [ $exit_code -ne 0 ]; then\n      exit $exit_code\n    fi\n    cd \"$FIREFOX_PATH\"\n\n    # Make an artifact build so it builds much faster\n    echo \"\nac_add_options --enable-artifact-builds\nmk_add_options AUTOCLOBBER=1\nmk_add_options MOZ_OBJDIR=./objdir-frontend\n\" > .mozconfig\nfi\n"
  },
  {
    "path": "bin/prepare-mochitests-dev",
    "content": "#!/usr/bin/env bash -x -e\n#\n# -e means \"exit on error\", so that we don't have to constantly\n# check exit codes\n#\n# Forked from https://github.com/devtools-html/debugger.html/blob/master/bin/prepare-mochitests-dev\n#\n# This sets up a mozilla-central build for local mochitest development with an\n# exported activity-stream tree and test directory.\n\n# If AS_GIT_BIN_REPO (the git repo from which prepare-mochitests-dev and\n# friends will be executed) isn't set in the environment, just use the repo\n#  we're running from.\nif [ -z ${AS_GIT_BIN_REPO+x} ]; then\n  ROOT=`dirname $0`\n  AS_GIT_BIN_REPO=\"../activity-stream\" # as seen from mozilla-central\nelse\n  ROOT=${AS_GIT_BIN_REPO}/bin\nfi\n\n# Compute the mozilla-central path based on whether AS_PINE_TEST_DIR is set\n# (i.e. whether this script has been called from test-merges.js)\nif [ -z ${AS_PINE_TEST_DIR+x} ]; then\n  FIREFOX_PATH=\"$ROOT/../../mozilla-central\"\nelse\n  FIREFOX_PATH=${AS_PINE_TEST_DIR}/mozilla-central\nfi\n\nMC_MODULE_PATH=\"$FIREFOX_PATH/browser/components/newtab\"\n\n# By default, just use mozilla-central + the export.  If ENABLE_MC_AS is set to\n# 1, patch on top of mozilla-central + the export to turn on the AS pref and\n# turn on the tests.  Once AS is on by default in mozilla-central, stuff\n# related to ENABLE_MC_AS can go away entirely.\nENABLE_MC_AS=${ENABLE_MC_AS-0}\n\n# This will either download or update the local Firefox repo\n\"$ROOT/download-firefox-artifact\"\n\n# blow away any old bits in order to workaround bug 1335976 for users\n# who are using the default objdir-frontend\nrm -f ${FIREFOX_PATH}/objdir-frontend/dist/bin/browser/features/@activity-streams/*\n\n# Clean, package, and copy the activity stream files.\nnpm run buildmc\n\n# Patch mozilla-central (on top of the export) so that AS is preffed on, and\n# the mochitests are turned on.\nshopt -s nullglob # don't explode if there are no patches right now\nif [ $ENABLE_MC_AS ]; then\n  PATCHES=$AS_GIT_BIN_REPO/mozilla-central-patches/*.diff\n  for p in $PATCHES\n  do\n    patch --directory=\"$FIREFOX_PATH\" -p1 --force --no-backup-if-mismatch \\\n    --input=$p\n  done\nfi\nshopt -u nullglob\n\n# Be sure that we've built, and that the test glop in the objdir has been\n# created.\n#\ncd \"$FIREFOX_PATH\"\n./mach build\nexit $?\n"
  },
  {
    "path": "bin/render-activity-stream-html.js",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\n/* eslint-disable no-console */\nconst fs = require(\"fs\");\nconst { mkdir } = require(\"shelljs\");\nconst path = require(\"path\");\n\n// Note: DEFAULT_OPTIONS.baseUrl should match BASE_URL in aboutNewTabService.js\n//       in mozilla-central.\nconst DEFAULT_OPTIONS = {\n  addonPath: \"..\",\n  baseUrl: \"resource://activity-stream/\",\n};\n\n/**\n * templateHTML - Generates HTML for activity stream, given some options and\n * prerendered HTML if necessary.\n *\n * @param  {obj} options\n *         {str} options.baseUrl        The base URL for all local assets\n *         {bool} options.debug         Should we use dev versions of JS libraries?\n *         {bool} options.noscripts     Should we include scripts in the prerendered files?\n * @return {str}         An HTML document as a string\n */\nfunction templateHTML(options) {\n  const debugString = options.debug ? \"-dev\" : \"\";\n  const scripts = [\n    \"chrome://browser/content/contentSearchUI.js\",\n    \"chrome://browser/content/contentTheme.js\",\n    `${options.baseUrl}vendor/react${debugString}.js`,\n    `${options.baseUrl}vendor/react-dom${debugString}.js`,\n    `${options.baseUrl}vendor/prop-types.js`,\n    `${options.baseUrl}vendor/redux.js`,\n    `${options.baseUrl}vendor/react-redux.js`,\n    `${options.baseUrl}vendor/react-transition-group.js`,\n    `${options.baseUrl}data/content/activity-stream.bundle.js`,\n  ];\n\n  // Add spacing and script tags\n  const scriptRender = `\\n${scripts\n    .map(script => `    <script src=\"${script}\"></script>`)\n    .join(\"\\n\")}`;\n\n  return `\n<!-- This Source Code Form is subject to the terms of the Mozilla Public\n   - License, v. 2.0. If a copy of the MPL was not distributed with this file,\n   - You can obtain one at http://mozilla.org/MPL/2.0/. -->\n\n<!doctype html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'none'; object-src 'none'; script-src resource: chrome:; connect-src https:; img-src https: data: blob:; style-src 'unsafe-inline';\">\n    <title data-l10n-id=\"newtab-page-title\"></title>\n    <link rel=\"icon\" type=\"image/png\" href=\"chrome://branding/content/icon32.png\"/>\n    <link rel=\"localization\" href=\"branding/brand.ftl\" />\n    <link rel=\"localization\" href=\"browser/branding/brandings.ftl\" />\n    <link rel=\"localization\" href=\"browser/newtab/newtab.ftl\" />\n    <link rel=\"stylesheet\" href=\"chrome://browser/content/contentSearchUI.css\" />\n    <link rel=\"stylesheet\" href=\"${options.baseUrl}css/activity-stream.css\" />\n  </head>\n  <body class=\"activity-stream\">\n    <div id=\"header-asrouter-container\" role=\"presentation\"></div>\n    <div id=\"root\"></div>\n    <div id=\"footer-asrouter-container\" role=\"presentation\"></div>${\n      options.noscripts ? \"\" : scriptRender\n    }\n  </body>\n</html>\n`.trimLeft();\n}\n\n/**\n * writeFiles - Writes to the desired files the result of a template given\n * various prerendered data and options.\n *\n * @param {string} destPath      Path to write the files to\n * @param {Map}    filesMap      Mapping of a string file name to templater\n * @param {Object} options       Various options for the templater\n */\nfunction writeFiles(destPath, filesMap, options) {\n  for (const [file, templater] of filesMap) {\n    console.log(\"\\x1b[32m\", `✓ ${file}`, \"\\x1b[0m\");\n    fs.writeFileSync(path.join(destPath, file), templater({ options }));\n  }\n}\n\nconst STATIC_FILES = new Map([\n  [\"activity-stream.html\", ({ options }) => templateHTML(options)],\n  [\n    \"activity-stream-debug.html\",\n    ({ options }) => templateHTML(Object.assign({}, options, { debug: true })),\n  ],\n  [\n    \"activity-stream-noscripts.html\",\n    ({ options }) =>\n      templateHTML(Object.assign({}, options, { noscripts: true })),\n  ],\n]);\n\n/**\n * main - Parses command line arguments, generates html and js with templates,\n *        and writes files to their specified locations.\n */\nfunction main() {\n  // eslint-disable-line max-statements\n  // This code parses command line arguments passed to this script.\n  // Note: process.argv.slice(2) is necessary because the first two items in\n  // process.argv are paths\n  const args = require(\"minimist\")(process.argv.slice(2), {\n    alias: {\n      addonPath: \"a\",\n      baseUrl: \"b\",\n    },\n  });\n\n  const options = Object.assign({ debug: false }, DEFAULT_OPTIONS, args || {});\n  const addonPath = path.resolve(__dirname, options.addonPath);\n  const prerenderedPath = path.join(addonPath, \"prerendered\");\n  console.log(`Writing prerendered files to ${prerenderedPath}:`);\n\n  mkdir(\"-p\", prerenderedPath);\n  writeFiles(prerenderedPath, STATIC_FILES, options);\n}\n\nmain();\n"
  },
  {
    "path": "bin/try-runner.js",
    "content": "/* eslint-disable no-console */\n/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */\n\n/*\n * A small test runner/reporter for node-based tests,\n * which are run via taskcluster node(debugger).\n *\n * Forked from\n * https://searchfox.org/mozilla-central/source/devtools/client/debugger/bin/try-runner.js\n */\n\nconst { execFileSync } = require(\"child_process\");\nconst { readFileSync } = require(\"fs\");\nconst path = require(\"path\");\n\nfunction logErrors(tool, errors) {\n  for (const error of errors) {\n    console.log(`TEST-UNEXPECTED-FAIL ${tool} | ${error}`);\n  }\n  return errors;\n}\n\nfunction execOut(...args) {\n  let out;\n  let err;\n\n  try {\n    out = execFileSync(...args, {\n      silent: false,\n    });\n  } catch (e) {\n    // For debugging on (eg) try server...\n    //\n    // if (e) {\n    //   logErrors(\"execOut\", [\"execFileSync returned exception: \", e]);\n    // }\n\n    out = e && e.stdout;\n    err = e && e.stderr;\n  }\n  return { out: out && out.toString(), err: err && err.toString() };\n}\n\nfunction logStart(name) {\n  console.log(`TEST START | ${name}`);\n}\n\nfunction karma() {\n  logStart(\"karma\");\n\n  const { out } = execOut(\"npm\", [\n    \"run\",\n    \"testmc:unit\",\n    // , \"--\", \"--log-level\", \"--verbose\",\n    // to debug the karma integration, uncomment the above line\n  ]);\n\n  // karma spits everything to stdout, not stderr, so if nothing came back on\n  // stdout, give up now.\n  if (!out) {\n    return false;\n  }\n\n  let jsonContent;\n  try {\n    // Note that this will be overwritten at each run, but that shouldn't\n    // matter.\n    jsonContent = readFileSync(path.join(\"logs\", \"karma-run-results.json\"));\n  } catch (ex) {\n    console.error(\"exception reading karma-run-results.json: \", ex);\n    return false;\n  }\n\n  const results = JSON.parse(jsonContent);\n\n  const failed = results.summary.failed === 0;\n\n  let errors = [];\n  // eslint-disable-next-line guard-for-in\n  for (let testArray in results.result) {\n    let failedTests = Array.from(results.result[testArray]).filter(\n      test => !test.success && !test.skipped\n    );\n\n    let errs = failedTests.map(test => {\n      return `${test.suite.join(\":\")} ${test.description}: ${test.log[0]}`;\n    });\n\n    errors = errors.concat(errs);\n  }\n\n  logErrors(\"karma\", errors);\n  return failed;\n}\n\nfunction sasslint() {\n  logStart(\"sasslint\");\n  const { out } = execOut(\"npm\", [\n    \"run\",\n    \"--silent\",\n    \"lint:sasslint\",\n    \"--\",\n    \"--format\",\n    \"json\",\n  ]);\n\n  if (!out.length) {\n    return true;\n  }\n\n  let fileObjects = JSON.parse(out);\n  let filesWithIssues = fileObjects.filter(\n    file => file.warningCount || file.errorCount\n  );\n\n  let errs = [];\n  let errorString;\n  filesWithIssues.forEach(file => {\n    file.messages.forEach(messageObj => {\n      errorString = `${file.filePath}(${messageObj.line}, ${\n        messageObj.column\n      }): ${messageObj.message} (${messageObj.ruleId})`;\n      errs.push(errorString);\n    });\n  });\n\n  const errors = logErrors(\"sasslint\", errs);\n  return errors.length === 0;\n}\n\nconst karmaPassed = karma();\nconst sasslintPassed = sasslint();\n\nconst success = karmaPassed && sasslintPassed;\n\nconsole.log({\n  karmaPassed,\n  sasslintPassed,\n});\n\nprocess.exitCode = success ? 0 : 1;\nconsole.log(\"CODE\", process.exitCode);\n"
  },
  {
    "path": "bin/vendor.js",
    "content": "#!/usr/bin/env node\n/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\n/* eslint-disable no-console */\n\nconst { cp, set } = require(\"shelljs\");\nconst path = require(\"path\");\n\nconst filesToVendor = {\n  // XXX currently these two licenses are identical.  Perhaps we should check\n  // in case that changes at some point in the future.\n  \"react/LICENSE\": \"REACT_AND_REACT_DOM_LICENSE\",\n  \"react/umd/react.production.min.js\": \"react.js\",\n  \"react/umd/react.development.js\": \"react-dev.js\",\n  \"react-dom/umd/react-dom.production.min.js\": \"react-dom.js\",\n  \"react-dom/umd/react-dom.development.js\": \"react-dom-dev.js\",\n  \"react-redux/LICENSE.md\": \"REACT_REDUX_LICENSE\",\n  \"react-redux/dist/react-redux.min.js\": \"react-redux.js\",\n  \"react-transition-group/dist/react-transition-group.min.js\":\n    \"react-transition-group.js\",\n  \"react-transition-group/LICENSE\": \"REACT_TRANSITION_GROUP_LICENSE\",\n};\n\nset(\"-v\"); // Echo all the copy commands so the user can see what's going on\nfor (let srcPath of Object.keys(filesToVendor)) {\n  cp(\n    path.join(\"node_modules\", srcPath),\n    path.join(\"vendor\", filesToVendor[srcPath])\n  );\n}\n\nconsole.log(`\nCheck to see if any license files have changed, and, if so, be sure to update\nhttps://searchfox.org/mozilla-central/source/toolkit/content/license.html`);\n"
  },
  {
    "path": "common/Actions.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\"use strict\";\n\nthis.MAIN_MESSAGE_TYPE = \"ActivityStream:Main\";\nthis.CONTENT_MESSAGE_TYPE = \"ActivityStream:Content\";\nthis.PRELOAD_MESSAGE_TYPE = \"ActivityStream:PreloadedBrowser\";\nthis.UI_CODE = 1;\nthis.BACKGROUND_PROCESS = 2;\n\n/**\n * globalImportContext - Are we in UI code (i.e. react, a dom) or some kind of background process?\n *                       Use this in action creators if you need different logic\n *                       for ui/background processes.\n */\nconst globalImportContext =\n  typeof Window === \"undefined\" ? BACKGROUND_PROCESS : UI_CODE;\n// Export for tests\nthis.globalImportContext = globalImportContext;\n\n// Create an object that avoids accidental differing key/value pairs:\n// {\n//   INIT: \"INIT\",\n//   UNINIT: \"UNINIT\"\n// }\nconst actionTypes = {};\nfor (const type of [\n  \"ADDONS_INFO_REQUEST\",\n  \"ADDONS_INFO_RESPONSE\",\n  \"ARCHIVE_FROM_POCKET\",\n  \"AS_ROUTER_INITIALIZED\",\n  \"AS_ROUTER_PREF_CHANGED\",\n  \"AS_ROUTER_TARGETING_UPDATE\",\n  \"AS_ROUTER_TELEMETRY_USER_EVENT\",\n  \"BLOCK_URL\",\n  \"BOOKMARK_URL\",\n  \"CLEAR_PREF\",\n  \"COPY_DOWNLOAD_LINK\",\n  \"DELETE_BOOKMARK_BY_ID\",\n  \"DELETE_FROM_POCKET\",\n  \"DELETE_HISTORY_URL\",\n  \"DIALOG_CANCEL\",\n  \"DIALOG_OPEN\",\n  \"DISCOVERY_STREAM_CONFIG_CHANGE\",\n  \"DISCOVERY_STREAM_CONFIG_RESET_DEFAULTS\",\n  \"DISCOVERY_STREAM_CONFIG_SETUP\",\n  \"DISCOVERY_STREAM_CONFIG_SET_VALUE\",\n  \"DISCOVERY_STREAM_FEEDS_UPDATE\",\n  \"DISCOVERY_STREAM_FEED_UPDATE\",\n  \"DISCOVERY_STREAM_IMPRESSION_STATS\",\n  \"DISCOVERY_STREAM_LAYOUT_RESET\",\n  \"DISCOVERY_STREAM_LAYOUT_UPDATE\",\n  \"DISCOVERY_STREAM_LINK_BLOCKED\",\n  \"DISCOVERY_STREAM_LOADED_CONTENT\",\n  \"DISCOVERY_STREAM_RETRY_FEED\",\n  \"DISCOVERY_STREAM_SPOCS_CAPS\",\n  \"DISCOVERY_STREAM_SPOCS_ENDPOINT\",\n  \"DISCOVERY_STREAM_SPOCS_FILL\",\n  \"DISCOVERY_STREAM_SPOCS_PLACEMENTS\",\n  \"DISCOVERY_STREAM_SPOCS_UPDATE\",\n  \"DISCOVERY_STREAM_SPOC_BLOCKED\",\n  \"DISCOVERY_STREAM_SPOC_IMPRESSION\",\n  \"DOWNLOAD_CHANGED\",\n  \"FAKE_FOCUS_SEARCH\",\n  \"FILL_SEARCH_TERM\",\n  \"HANDOFF_SEARCH_TO_AWESOMEBAR\",\n  \"HIDE_PRIVACY_INFO\",\n  \"HIDE_SEARCH\",\n  \"INIT\",\n  \"NEW_TAB_INIT\",\n  \"NEW_TAB_INITIAL_STATE\",\n  \"NEW_TAB_LOAD\",\n  \"NEW_TAB_REHYDRATED\",\n  \"NEW_TAB_STATE_REQUEST\",\n  \"NEW_TAB_UNLOAD\",\n  \"OPEN_DOWNLOAD_FILE\",\n  \"OPEN_LINK\",\n  \"OPEN_NEW_WINDOW\",\n  \"OPEN_PRIVATE_WINDOW\",\n  \"OPEN_WEBEXT_SETTINGS\",\n  \"PLACES_BOOKMARK_ADDED\",\n  \"PLACES_BOOKMARK_REMOVED\",\n  \"PLACES_HISTORY_CLEARED\",\n  \"PLACES_LINKS_CHANGED\",\n  \"PLACES_LINK_BLOCKED\",\n  \"PLACES_LINK_DELETED\",\n  \"PLACES_SAVED_TO_POCKET\",\n  \"POCKET_CTA\",\n  \"POCKET_LINK_DELETED_OR_ARCHIVED\",\n  \"POCKET_LOGGED_IN\",\n  \"POCKET_WAITING_FOR_SPOC\",\n  \"PREFS_INITIAL_VALUES\",\n  \"PREF_CHANGED\",\n  \"PREVIEW_REQUEST\",\n  \"PREVIEW_REQUEST_CANCEL\",\n  \"PREVIEW_RESPONSE\",\n  \"REMOVE_DOWNLOAD_FILE\",\n  \"RICH_ICON_MISSING\",\n  \"SAVE_SESSION_PERF_DATA\",\n  \"SAVE_TO_POCKET\",\n  \"SCREENSHOT_UPDATED\",\n  \"SECTION_DEREGISTER\",\n  \"SECTION_DISABLE\",\n  \"SECTION_ENABLE\",\n  \"SECTION_MOVE\",\n  \"SECTION_OPTIONS_CHANGED\",\n  \"SECTION_REGISTER\",\n  \"SECTION_UPDATE\",\n  \"SECTION_UPDATE_CARD\",\n  \"SETTINGS_CLOSE\",\n  \"SETTINGS_OPEN\",\n  \"SET_PREF\",\n  \"SHOW_DOWNLOAD_FILE\",\n  \"SHOW_FIREFOX_ACCOUNTS\",\n  \"SHOW_PRIVACY_INFO\",\n  \"SHOW_SEARCH\",\n  \"SKIPPED_SIGNIN\",\n  \"SNIPPETS_BLOCKLIST_CLEARED\",\n  \"SNIPPETS_BLOCKLIST_UPDATED\",\n  \"SNIPPETS_DATA\",\n  \"SNIPPETS_PREVIEW_MODE\",\n  \"SNIPPETS_RESET\",\n  \"SNIPPET_BLOCKED\",\n  \"SUBMIT_EMAIL\",\n  \"SUBMIT_SIGNIN\",\n  \"SYSTEM_TICK\",\n  \"TELEMETRY_IMPRESSION_STATS\",\n  \"TELEMETRY_PERFORMANCE_EVENT\",\n  \"TELEMETRY_UNDESIRED_EVENT\",\n  \"TELEMETRY_USER_EVENT\",\n  \"TOP_SITES_CANCEL_EDIT\",\n  \"TOP_SITES_CLOSE_SEARCH_SHORTCUTS_MODAL\",\n  \"TOP_SITES_EDIT\",\n  \"TOP_SITES_INSERT\",\n  \"TOP_SITES_OPEN_SEARCH_SHORTCUTS_MODAL\",\n  \"TOP_SITES_PIN\",\n  \"TOP_SITES_PREFS_UPDATED\",\n  \"TOP_SITES_UNPIN\",\n  \"TOP_SITES_UPDATED\",\n  \"TOTAL_BOOKMARKS_REQUEST\",\n  \"TOTAL_BOOKMARKS_RESPONSE\",\n  \"TRAILHEAD_ENROLL_EVENT\",\n  \"UNINIT\",\n  \"UPDATE_PINNED_SEARCH_SHORTCUTS\",\n  \"UPDATE_SEARCH_SHORTCUTS\",\n  \"UPDATE_SECTION_PREFS\",\n  \"WEBEXT_CLICK\",\n  \"WEBEXT_DISMISS\",\n]) {\n  actionTypes[type] = type;\n}\n\n// These are acceptable actions for AS Router messages to have. They can show up\n// as call-to-action buttons in snippets, onboarding tour, etc.\nconst ASRouterActions = {};\nfor (const type of [\n  \"HIGHLIGHT_FEATURE\",\n  \"INSTALL_ADDON_FROM_URL\",\n  \"OPEN_APPLICATIONS_MENU\",\n  \"OPEN_PRIVATE_BROWSER_WINDOW\",\n  \"OPEN_URL\",\n  \"OPEN_ABOUT_PAGE\",\n  \"OPEN_PREFERENCES_PAGE\",\n  \"SHOW_FIREFOX_ACCOUNTS\",\n  \"PIN_CURRENT_TAB\",\n  \"ENABLE_FIREFOX_MONITOR\",\n  \"OPEN_PROTECTION_PANEL\",\n  \"OPEN_PROTECTION_REPORT\",\n  \"DISABLE_STP_DOORHANGERS\",\n  \"SHOW_MIGRATION_WIZARD\",\n]) {\n  ASRouterActions[type] = type;\n}\n\n// Helper function for creating routed actions between content and main\n// Not intended to be used by consumers\nfunction _RouteMessage(action, options) {\n  const meta = action.meta ? { ...action.meta } : {};\n  if (!options || !options.from || !options.to) {\n    throw new Error(\n      \"Routed Messages must have options as the second parameter, and must at least include a .from and .to property.\"\n    );\n  }\n  // For each of these fields, if they are passed as an option,\n  // add them to the action. If they are not defined, remove them.\n  [\"from\", \"to\", \"toTarget\", \"fromTarget\", \"skipMain\", \"skipLocal\"].forEach(\n    o => {\n      if (typeof options[o] !== \"undefined\") {\n        meta[o] = options[o];\n      } else if (meta[o]) {\n        delete meta[o];\n      }\n    }\n  );\n  return { ...action, meta };\n}\n\n/**\n * AlsoToMain - Creates a message that will be dispatched locally and also sent to the Main process.\n *\n * @param  {object} action Any redux action (required)\n * @param  {object} options\n * @param  {bool}   skipLocal Used by OnlyToMain to skip the main reducer\n * @param  {string} fromTarget The id of the content port from which the action originated. (optional)\n * @return {object} An action with added .meta properties\n */\nfunction AlsoToMain(action, fromTarget, skipLocal) {\n  return _RouteMessage(action, {\n    from: CONTENT_MESSAGE_TYPE,\n    to: MAIN_MESSAGE_TYPE,\n    fromTarget,\n    skipLocal,\n  });\n}\n\n/**\n * OnlyToMain - Creates a message that will be sent to the Main process and skip the local reducer.\n *\n * @param  {object} action Any redux action (required)\n * @param  {object} options\n * @param  {string} fromTarget The id of the content port from which the action originated. (optional)\n * @return {object} An action with added .meta properties\n */\nfunction OnlyToMain(action, fromTarget) {\n  return AlsoToMain(action, fromTarget, true);\n}\n\n/**\n * BroadcastToContent - Creates a message that will be dispatched to main and sent to ALL content processes.\n *\n * @param  {object} action Any redux action (required)\n * @return {object} An action with added .meta properties\n */\nfunction BroadcastToContent(action) {\n  return _RouteMessage(action, {\n    from: MAIN_MESSAGE_TYPE,\n    to: CONTENT_MESSAGE_TYPE,\n  });\n}\n\n/**\n * AlsoToOneContent - Creates a message that will be will be dispatched to the main store\n *                    and also sent to a particular Content process.\n *\n * @param  {object} action Any redux action (required)\n * @param  {string} target The id of a content port\n * @param  {bool} skipMain Used by OnlyToOneContent to skip the main process\n * @return {object} An action with added .meta properties\n */\nfunction AlsoToOneContent(action, target, skipMain) {\n  if (!target) {\n    throw new Error(\n      \"You must provide a target ID as the second parameter of AlsoToOneContent. If you want to send to all content processes, use BroadcastToContent\"\n    );\n  }\n  return _RouteMessage(action, {\n    from: MAIN_MESSAGE_TYPE,\n    to: CONTENT_MESSAGE_TYPE,\n    toTarget: target,\n    skipMain,\n  });\n}\n\n/**\n * OnlyToOneContent - Creates a message that will be sent to a particular Content process\n *                    and skip the main reducer.\n *\n * @param  {object} action Any redux action (required)\n * @param  {string} target The id of a content port\n * @return {object} An action with added .meta properties\n */\nfunction OnlyToOneContent(action, target) {\n  return AlsoToOneContent(action, target, true);\n}\n\n/**\n * AlsoToPreloaded - Creates a message that dispatched to the main reducer and also sent to the preloaded tab.\n *\n * @param  {object} action Any redux action (required)\n * @return {object} An action with added .meta properties\n */\nfunction AlsoToPreloaded(action) {\n  return _RouteMessage(action, {\n    from: MAIN_MESSAGE_TYPE,\n    to: PRELOAD_MESSAGE_TYPE,\n  });\n}\n\n/**\n * UserEvent - A telemetry ping indicating a user action. This should only\n *                   be sent from the UI during a user session.\n *\n * @param  {object} data Fields to include in the ping (source, etc.)\n * @return {object} An AlsoToMain action\n */\nfunction UserEvent(data) {\n  return AlsoToMain({\n    type: actionTypes.TELEMETRY_USER_EVENT,\n    data,\n  });\n}\n\n/**\n * ASRouterUserEvent - A telemetry ping indicating a user action from AS router. This should only\n *                     be sent from the UI during a user session.\n *\n * @param  {object} data Fields to include in the ping (source, etc.)\n * @return {object} An AlsoToMain action\n */\nfunction ASRouterUserEvent(data) {\n  return AlsoToMain({\n    type: actionTypes.AS_ROUTER_TELEMETRY_USER_EVENT,\n    data,\n  });\n}\n\n/**\n * DiscoveryStreamSpocsFill - A telemetry ping indicating a SPOCS Fill event.\n *\n * @param  {object} data Fields to include in the ping (spoc_fills, etc.)\n * @param  {int} importContext (For testing) Override the import context for testing.\n * @return {object} An AlsoToMain action\n */\nfunction DiscoveryStreamSpocsFill(data, importContext = globalImportContext) {\n  const action = {\n    type: actionTypes.DISCOVERY_STREAM_SPOCS_FILL,\n    data,\n  };\n  return importContext === UI_CODE ? AlsoToMain(action) : action;\n}\n\n/**\n * UndesiredEvent - A telemetry ping indicating an undesired state.\n *\n * @param  {object} data Fields to include in the ping (value, etc.)\n * @param  {int} importContext (For testing) Override the import context for testing.\n * @return {object} An action. For UI code, a AlsoToMain action.\n */\nfunction UndesiredEvent(data, importContext = globalImportContext) {\n  const action = {\n    type: actionTypes.TELEMETRY_UNDESIRED_EVENT,\n    data,\n  };\n  return importContext === UI_CODE ? AlsoToMain(action) : action;\n}\n\n/**\n * PerfEvent - A telemetry ping indicating a performance-related event.\n *\n * @param  {object} data Fields to include in the ping (value, etc.)\n * @param  {int} importContext (For testing) Override the import context for testing.\n * @return {object} An action. For UI code, a AlsoToMain action.\n */\nfunction PerfEvent(data, importContext = globalImportContext) {\n  const action = {\n    type: actionTypes.TELEMETRY_PERFORMANCE_EVENT,\n    data,\n  };\n  return importContext === UI_CODE ? AlsoToMain(action) : action;\n}\n\n/**\n * ImpressionStats - A telemetry ping indicating an impression stats.\n *\n * @param  {object} data Fields to include in the ping\n * @param  {int} importContext (For testing) Override the import context for testing.\n * #return {object} An action. For UI code, a AlsoToMain action.\n */\nfunction ImpressionStats(data, importContext = globalImportContext) {\n  const action = {\n    type: actionTypes.TELEMETRY_IMPRESSION_STATS,\n    data,\n  };\n  return importContext === UI_CODE ? AlsoToMain(action) : action;\n}\n\n/**\n * DiscoveryStreamImpressionStats - A telemetry ping indicating an impression stats in Discovery Stream.\n *\n * @param  {object} data Fields to include in the ping\n * @param  {int} importContext (For testing) Override the import context for testing.\n * #return {object} An action. For UI code, a AlsoToMain action.\n */\nfunction DiscoveryStreamImpressionStats(\n  data,\n  importContext = globalImportContext\n) {\n  const action = {\n    type: actionTypes.DISCOVERY_STREAM_IMPRESSION_STATS,\n    data,\n  };\n  return importContext === UI_CODE ? AlsoToMain(action) : action;\n}\n\n/**\n * DiscoveryStreamLoadedContent - A telemetry ping indicating a content gets loaded in Discovery Stream.\n *\n * @param  {object} data Fields to include in the ping\n * @param  {int} importContext (For testing) Override the import context for testing.\n * #return {object} An action. For UI code, a AlsoToMain action.\n */\nfunction DiscoveryStreamLoadedContent(\n  data,\n  importContext = globalImportContext\n) {\n  const action = {\n    type: actionTypes.DISCOVERY_STREAM_LOADED_CONTENT,\n    data,\n  };\n  return importContext === UI_CODE ? AlsoToMain(action) : action;\n}\n\nfunction SetPref(name, value, importContext = globalImportContext) {\n  const action = { type: actionTypes.SET_PREF, data: { name, value } };\n  return importContext === UI_CODE ? AlsoToMain(action) : action;\n}\n\nfunction WebExtEvent(type, data, importContext = globalImportContext) {\n  if (!data || !data.source) {\n    throw new Error(\n      'WebExtEvent actions should include a property \"source\", the id of the webextension that should receive the event.'\n    );\n  }\n  const action = { type, data };\n  return importContext === UI_CODE ? AlsoToMain(action) : action;\n}\n\nthis.actionTypes = actionTypes;\nthis.ASRouterActions = ASRouterActions;\n\nthis.actionCreators = {\n  BroadcastToContent,\n  UserEvent,\n  ASRouterUserEvent,\n  UndesiredEvent,\n  PerfEvent,\n  ImpressionStats,\n  AlsoToOneContent,\n  OnlyToOneContent,\n  AlsoToMain,\n  OnlyToMain,\n  AlsoToPreloaded,\n  SetPref,\n  WebExtEvent,\n  DiscoveryStreamImpressionStats,\n  DiscoveryStreamLoadedContent,\n  DiscoveryStreamSpocsFill,\n};\n\n// These are helpers to test for certain kinds of actions\nthis.actionUtils = {\n  isSendToMain(action) {\n    if (!action.meta) {\n      return false;\n    }\n    return (\n      action.meta.to === MAIN_MESSAGE_TYPE &&\n      action.meta.from === CONTENT_MESSAGE_TYPE\n    );\n  },\n  isBroadcastToContent(action) {\n    if (!action.meta) {\n      return false;\n    }\n    if (action.meta.to === CONTENT_MESSAGE_TYPE && !action.meta.toTarget) {\n      return true;\n    }\n    return false;\n  },\n  isSendToOneContent(action) {\n    if (!action.meta) {\n      return false;\n    }\n    if (action.meta.to === CONTENT_MESSAGE_TYPE && action.meta.toTarget) {\n      return true;\n    }\n    return false;\n  },\n  isSendToPreloaded(action) {\n    if (!action.meta) {\n      return false;\n    }\n    return (\n      action.meta.to === PRELOAD_MESSAGE_TYPE &&\n      action.meta.from === MAIN_MESSAGE_TYPE\n    );\n  },\n  isFromMain(action) {\n    if (!action.meta) {\n      return false;\n    }\n    return (\n      action.meta.from === MAIN_MESSAGE_TYPE &&\n      action.meta.to === CONTENT_MESSAGE_TYPE\n    );\n  },\n  getPortIdOfSender(action) {\n    return (action.meta && action.meta.fromTarget) || null;\n  },\n  _RouteMessage,\n};\n\nconst EXPORTED_SYMBOLS = [\n  \"actionTypes\",\n  \"actionCreators\",\n  \"actionUtils\",\n  \"ASRouterActions\",\n  \"globalImportContext\",\n  \"UI_CODE\",\n  \"BACKGROUND_PROCESS\",\n  \"MAIN_MESSAGE_TYPE\",\n  \"CONTENT_MESSAGE_TYPE\",\n  \"PRELOAD_MESSAGE_TYPE\",\n];\n"
  },
  {
    "path": "common/Dedupe.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nthis.Dedupe = class Dedupe {\n  constructor(createKey) {\n    this.createKey = createKey || this.defaultCreateKey;\n  }\n\n  defaultCreateKey(item) {\n    return item;\n  }\n\n  /**\n   * Dedupe any number of grouped elements favoring those from earlier groups.\n   *\n   * @param {Array} groups Contains an arbitrary number of arrays of elements.\n   * @returns {Array} A matching array of each provided group deduped.\n   */\n  group(...groups) {\n    const globalKeys = new Set();\n    const result = [];\n    for (const values of groups) {\n      const valueMap = new Map();\n      for (const value of values) {\n        const key = this.createKey(value);\n        if (!globalKeys.has(key) && !valueMap.has(key)) {\n          valueMap.set(key, value);\n        }\n      }\n      result.push(valueMap);\n      valueMap.forEach((value, key) => globalKeys.add(key));\n    }\n    return result.map(m => Array.from(m.values()));\n  }\n};\n\nconst EXPORTED_SYMBOLS = [\"Dedupe\"];\n"
  },
  {
    "path": "common/PerfService.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\n\"use strict\";\n\nif (typeof ChromeUtils !== \"undefined\") {\n  // Use a var here instead of let outside to avoid creating a locally scoped\n  // variable that hides the global, which we modify for testing.\n  // eslint-disable-next-line no-var, vars-on-top\n  var { Services } = ChromeUtils.import(\"resource://gre/modules/Services.jsm\");\n}\n\nlet usablePerfObj;\n\n/* istanbul ignore else */\n// eslint-disable-next-line block-scoped-var\nif (typeof Services !== \"undefined\") {\n  // Borrow the high-resolution timer from the hidden window....\n  // eslint-disable-next-line block-scoped-var\n  usablePerfObj = Services.appShell.hiddenDOMWindow.performance;\n} else {\n  // we must be running in content space\n  // eslint-disable-next-line no-undef\n  usablePerfObj = performance;\n}\n\nfunction _PerfService(options) {\n  // For testing, so that we can use a fake Window.performance object with\n  // known state.\n  if (options && options.performanceObj) {\n    this._perf = options.performanceObj;\n  } else {\n    this._perf = usablePerfObj;\n  }\n}\n\n_PerfService.prototype = {\n  /**\n   * Calls the underlying mark() method on the appropriate Window.performance\n   * object to add a mark with the given name to the appropriate performance\n   * timeline.\n   *\n   * @param  {String} name  the name to give the current mark\n   * @return {void}\n   */\n  mark: function mark(str) {\n    this._perf.mark(str);\n  },\n\n  /**\n   * Calls the underlying getEntriesByName on the appropriate Window.performance\n   * object.\n   *\n   * @param  {String} name\n   * @param  {String} type eg \"mark\"\n   * @return {Array}       Performance* objects\n   */\n  getEntriesByName: function getEntriesByName(name, type) {\n    return this._perf.getEntriesByName(name, type);\n  },\n\n  /**\n   * The timeOrigin property from the appropriate performance object.\n   * Used to ensure that timestamps from the add-on code and the content code\n   * are comparable.\n   *\n   * @note If this is called from a context without a window\n   * (eg a JSM in chrome), it will return the timeOrigin of the XUL hidden\n   * window, which appears to be the first created window (and thus\n   * timeOrigin) in the browser.  Note also, however, there is also a private\n   * hidden window, presumably for private browsing, which appears to be\n   * created dynamically later.  Exactly how/when that shows up needs to be\n   * investigated.\n   *\n   * @return {Number} A double of milliseconds with a precision of 0.5us.\n   */\n  get timeOrigin() {\n    return this._perf.timeOrigin;\n  },\n\n  /**\n   * Returns the \"absolute\" version of performance.now(), i.e. one that\n   * should ([bug 1401406](https://bugzilla.mozilla.org/show_bug.cgi?id=1401406)\n   * be comparable across both chrome and content.\n   *\n   * @return {Number}\n   */\n  absNow: function absNow() {\n    return this.timeOrigin + this._perf.now();\n  },\n\n  /**\n   * This returns the absolute startTime from the most recent performance.mark()\n   * with the given name.\n   *\n   * @param  {String} name  the name to lookup the start time for\n   *\n   * @return {Number}       the returned start time, as a DOMHighResTimeStamp\n   *\n   * @throws {Error}        \"No Marks with the name ...\" if none are available\n   *\n   * @note Always surround calls to this by try/catch.  Otherwise your code\n   * may fail when the `privacy.resistFingerprinting` pref is true.  When\n   * this pref is set, all attempts to get marks will likely fail, which will\n   * cause this method to throw.\n   *\n   * See [bug 1369303](https://bugzilla.mozilla.org/show_bug.cgi?id=1369303)\n   * for more info.\n   */\n  getMostRecentAbsMarkStartByName(name) {\n    let entries = this.getEntriesByName(name, \"mark\");\n\n    if (!entries.length) {\n      throw new Error(`No marks with the name ${name}`);\n    }\n\n    let mostRecentEntry = entries[entries.length - 1];\n    return this._perf.timeOrigin + mostRecentEntry.startTime;\n  },\n};\n\nthis.perfService = new _PerfService();\nconst EXPORTED_SYMBOLS = [\"_PerfService\", \"perfService\"];\n"
  },
  {
    "path": "common/Reducers.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\"use strict\";\n\nconst { actionTypes: at } = ChromeUtils.import(\n  \"resource://activity-stream/common/Actions.jsm\"\n);\nconst { Dedupe } = ChromeUtils.import(\n  \"resource://activity-stream/common/Dedupe.jsm\"\n);\n\nconst TOP_SITES_DEFAULT_ROWS = 1;\nconst TOP_SITES_MAX_SITES_PER_ROW = 8;\n\nconst dedupe = new Dedupe(site => site && site.url);\n\nconst INITIAL_STATE = {\n  App: {\n    // Have we received real data from the app yet?\n    initialized: false,\n  },\n  ASRouter: { initialized: false },\n  Snippets: { initialized: false },\n  TopSites: {\n    // Have we received real data from history yet?\n    initialized: false,\n    // The history (and possibly default) links\n    rows: [],\n    // Used in content only to dispatch action to TopSiteForm.\n    editForm: null,\n    // Used in content only to open the SearchShortcutsForm modal.\n    showSearchShortcutsForm: false,\n    // The list of available search shortcuts.\n    searchShortcuts: [],\n  },\n  Prefs: {\n    initialized: false,\n    values: {},\n  },\n  Dialog: {\n    visible: false,\n    data: {},\n  },\n  Sections: [],\n  Pocket: {\n    isUserLoggedIn: null,\n    pocketCta: {},\n    waitingForSpoc: true,\n  },\n  // This is the new pocket configurable layout state.\n  DiscoveryStream: {\n    // This is a JSON-parsed copy of the discoverystream.config pref value.\n    config: { enabled: false, layout_endpoint: \"\" },\n    layout: [],\n    lastUpdated: null,\n    isPrivacyInfoModalVisible: false,\n    feeds: {\n      data: {\n        // \"https://foo.com/feed1\": {lastUpdated: 123, data: []}\n      },\n      loaded: false,\n    },\n    spocs: {\n      spocs_endpoint: \"\",\n      spocs_per_domain: 1,\n      lastUpdated: null,\n      data: {}, // {spocs: []}\n      loaded: false,\n      frequency_caps: [],\n      blocked: [],\n      placements: [],\n    },\n  },\n  Search: {\n    // When search hand-off is enabled, we render a big button that is styled to\n    // look like a search textbox. If the button is clicked, we style\n    // the button as if it was a focused search box and show a fake cursor but\n    // really focus the awesomebar without the focus styles (\"hidden focus\").\n    fakeFocus: false,\n    // Hide the search box after handing off to AwesomeBar and user starts typing.\n    hide: false,\n  },\n};\n\nfunction App(prevState = INITIAL_STATE.App, action) {\n  switch (action.type) {\n    case at.INIT:\n      return Object.assign({}, prevState, action.data || {}, {\n        initialized: true,\n      });\n    default:\n      return prevState;\n  }\n}\n\nfunction ASRouter(prevState = INITIAL_STATE.ASRouter, action) {\n  switch (action.type) {\n    case at.AS_ROUTER_INITIALIZED:\n      return { ...action.data, initialized: true };\n    default:\n      return prevState;\n  }\n}\n\n/**\n * insertPinned - Inserts pinned links in their specified slots\n *\n * @param {array} a list of links\n * @param {array} a list of pinned links\n * @return {array} resulting list of links with pinned links inserted\n */\nfunction insertPinned(links, pinned) {\n  // Remove any pinned links\n  const pinnedUrls = pinned.map(link => link && link.url);\n  let newLinks = links.filter(link =>\n    link ? !pinnedUrls.includes(link.url) : false\n  );\n  newLinks = newLinks.map(link => {\n    if (link && link.isPinned) {\n      delete link.isPinned;\n      delete link.pinIndex;\n    }\n    return link;\n  });\n\n  // Then insert them in their specified location\n  pinned.forEach((val, index) => {\n    if (!val) {\n      return;\n    }\n    let link = Object.assign({}, val, { isPinned: true, pinIndex: index });\n    if (index > newLinks.length) {\n      newLinks[index] = link;\n    } else {\n      newLinks.splice(index, 0, link);\n    }\n  });\n\n  return newLinks;\n}\n\nfunction TopSites(prevState = INITIAL_STATE.TopSites, action) {\n  let hasMatch;\n  let newRows;\n  switch (action.type) {\n    case at.TOP_SITES_UPDATED:\n      if (!action.data || !action.data.links) {\n        return prevState;\n      }\n      return Object.assign(\n        {},\n        prevState,\n        { initialized: true, rows: action.data.links },\n        action.data.pref ? { pref: action.data.pref } : {}\n      );\n    case at.TOP_SITES_PREFS_UPDATED:\n      return Object.assign({}, prevState, { pref: action.data.pref });\n    case at.TOP_SITES_EDIT:\n      return Object.assign({}, prevState, {\n        editForm: {\n          index: action.data.index,\n          previewResponse: null,\n        },\n      });\n    case at.TOP_SITES_CANCEL_EDIT:\n      return Object.assign({}, prevState, { editForm: null });\n    case at.TOP_SITES_OPEN_SEARCH_SHORTCUTS_MODAL:\n      return Object.assign({}, prevState, { showSearchShortcutsForm: true });\n    case at.TOP_SITES_CLOSE_SEARCH_SHORTCUTS_MODAL:\n      return Object.assign({}, prevState, { showSearchShortcutsForm: false });\n    case at.PREVIEW_RESPONSE:\n      if (\n        !prevState.editForm ||\n        action.data.url !== prevState.editForm.previewUrl\n      ) {\n        return prevState;\n      }\n      return Object.assign({}, prevState, {\n        editForm: {\n          index: prevState.editForm.index,\n          previewResponse: action.data.preview,\n          previewUrl: action.data.url,\n        },\n      });\n    case at.PREVIEW_REQUEST:\n      if (!prevState.editForm) {\n        return prevState;\n      }\n      return Object.assign({}, prevState, {\n        editForm: {\n          index: prevState.editForm.index,\n          previewResponse: null,\n          previewUrl: action.data.url,\n        },\n      });\n    case at.PREVIEW_REQUEST_CANCEL:\n      if (!prevState.editForm) {\n        return prevState;\n      }\n      return Object.assign({}, prevState, {\n        editForm: {\n          index: prevState.editForm.index,\n          previewResponse: null,\n        },\n      });\n    case at.SCREENSHOT_UPDATED:\n      newRows = prevState.rows.map(row => {\n        if (row && row.url === action.data.url) {\n          hasMatch = true;\n          return Object.assign({}, row, { screenshot: action.data.screenshot });\n        }\n        return row;\n      });\n      return hasMatch\n        ? Object.assign({}, prevState, { rows: newRows })\n        : prevState;\n    case at.PLACES_BOOKMARK_ADDED:\n      if (!action.data) {\n        return prevState;\n      }\n      newRows = prevState.rows.map(site => {\n        if (site && site.url === action.data.url) {\n          const { bookmarkGuid, bookmarkTitle, dateAdded } = action.data;\n          return Object.assign({}, site, {\n            bookmarkGuid,\n            bookmarkTitle,\n            bookmarkDateCreated: dateAdded,\n          });\n        }\n        return site;\n      });\n      return Object.assign({}, prevState, { rows: newRows });\n    case at.PLACES_BOOKMARK_REMOVED:\n      if (!action.data) {\n        return prevState;\n      }\n      newRows = prevState.rows.map(site => {\n        if (site && site.url === action.data.url) {\n          const newSite = Object.assign({}, site);\n          delete newSite.bookmarkGuid;\n          delete newSite.bookmarkTitle;\n          delete newSite.bookmarkDateCreated;\n          return newSite;\n        }\n        return site;\n      });\n      return Object.assign({}, prevState, { rows: newRows });\n    case at.PLACES_LINK_DELETED:\n      if (!action.data) {\n        return prevState;\n      }\n      newRows = prevState.rows.filter(site => action.data.url !== site.url);\n      return Object.assign({}, prevState, { rows: newRows });\n    case at.UPDATE_SEARCH_SHORTCUTS:\n      return { ...prevState, searchShortcuts: action.data.searchShortcuts };\n    case at.SNIPPETS_PREVIEW_MODE:\n      return { ...prevState, rows: [] };\n    default:\n      return prevState;\n  }\n}\n\nfunction Dialog(prevState = INITIAL_STATE.Dialog, action) {\n  switch (action.type) {\n    case at.DIALOG_OPEN:\n      return Object.assign({}, prevState, { visible: true, data: action.data });\n    case at.DIALOG_CANCEL:\n      return Object.assign({}, prevState, { visible: false });\n    case at.DELETE_HISTORY_URL:\n      return Object.assign({}, INITIAL_STATE.Dialog);\n    default:\n      return prevState;\n  }\n}\n\nfunction Prefs(prevState = INITIAL_STATE.Prefs, action) {\n  let newValues;\n  switch (action.type) {\n    case at.PREFS_INITIAL_VALUES:\n      return Object.assign({}, prevState, {\n        initialized: true,\n        values: action.data,\n      });\n    case at.PREF_CHANGED:\n      newValues = Object.assign({}, prevState.values);\n      newValues[action.data.name] = action.data.value;\n      return Object.assign({}, prevState, { values: newValues });\n    default:\n      return prevState;\n  }\n}\n\nfunction Sections(prevState = INITIAL_STATE.Sections, action) {\n  let hasMatch;\n  let newState;\n  switch (action.type) {\n    case at.SECTION_DEREGISTER:\n      return prevState.filter(section => section.id !== action.data);\n    case at.SECTION_REGISTER:\n      // If section exists in prevState, update it\n      newState = prevState.map(section => {\n        if (section && section.id === action.data.id) {\n          hasMatch = true;\n          return Object.assign({}, section, action.data);\n        }\n        return section;\n      });\n      // Otherwise, append it\n      if (!hasMatch) {\n        const initialized = !!(action.data.rows && !!action.data.rows.length);\n        const section = Object.assign(\n          { title: \"\", rows: [], enabled: false },\n          action.data,\n          { initialized }\n        );\n        newState.push(section);\n      }\n      return newState;\n    case at.SECTION_UPDATE:\n      newState = prevState.map(section => {\n        if (section && section.id === action.data.id) {\n          // If the action is updating rows, we should consider initialized to be true.\n          // This can be overridden if initialized is defined in the action.data\n          const initialized = action.data.rows ? { initialized: true } : {};\n\n          // Make sure pinned cards stay at their current position when rows are updated.\n          // Disabling a section (SECTION_UPDATE with empty rows) does not retain pinned cards.\n          if (\n            action.data.rows &&\n            !!action.data.rows.length &&\n            section.rows.find(card => card.pinned)\n          ) {\n            const rows = Array.from(action.data.rows);\n            section.rows.forEach((card, index) => {\n              if (card.pinned) {\n                // Only add it if it's not already there.\n                if (rows[index].guid !== card.guid) {\n                  rows.splice(index, 0, card);\n                }\n              }\n            });\n            return Object.assign(\n              {},\n              section,\n              initialized,\n              Object.assign({}, action.data, { rows })\n            );\n          }\n\n          return Object.assign({}, section, initialized, action.data);\n        }\n        return section;\n      });\n\n      if (!action.data.dedupeConfigurations) {\n        return newState;\n      }\n\n      action.data.dedupeConfigurations.forEach(dedupeConf => {\n        newState = newState.map(section => {\n          if (section.id === dedupeConf.id) {\n            const dedupedRows = dedupeConf.dedupeFrom.reduce(\n              (rows, dedupeSectionId) => {\n                const dedupeSection = newState.find(\n                  s => s.id === dedupeSectionId\n                );\n                const [, newRows] = dedupe.group(dedupeSection.rows, rows);\n                return newRows;\n              },\n              section.rows\n            );\n\n            return Object.assign({}, section, { rows: dedupedRows });\n          }\n\n          return section;\n        });\n      });\n\n      return newState;\n    case at.SECTION_UPDATE_CARD:\n      return prevState.map(section => {\n        if (section && section.id === action.data.id && section.rows) {\n          const newRows = section.rows.map(card => {\n            if (card.url === action.data.url) {\n              return Object.assign({}, card, action.data.options);\n            }\n            return card;\n          });\n          return Object.assign({}, section, { rows: newRows });\n        }\n        return section;\n      });\n    case at.PLACES_BOOKMARK_ADDED:\n      if (!action.data) {\n        return prevState;\n      }\n      return prevState.map(section =>\n        Object.assign({}, section, {\n          rows: section.rows.map(item => {\n            // find the item within the rows that is attempted to be bookmarked\n            if (item.url === action.data.url) {\n              const { bookmarkGuid, bookmarkTitle, dateAdded } = action.data;\n              return Object.assign({}, item, {\n                bookmarkGuid,\n                bookmarkTitle,\n                bookmarkDateCreated: dateAdded,\n                type: \"bookmark\",\n              });\n            }\n            return item;\n          }),\n        })\n      );\n    case at.PLACES_SAVED_TO_POCKET:\n      if (!action.data) {\n        return prevState;\n      }\n      return prevState.map(section =>\n        Object.assign({}, section, {\n          rows: section.rows.map(item => {\n            if (item.url === action.data.url) {\n              return Object.assign({}, item, {\n                open_url: action.data.open_url,\n                pocket_id: action.data.pocket_id,\n                title: action.data.title,\n                type: \"pocket\",\n              });\n            }\n            return item;\n          }),\n        })\n      );\n    case at.PLACES_BOOKMARK_REMOVED:\n      if (!action.data) {\n        return prevState;\n      }\n      return prevState.map(section =>\n        Object.assign({}, section, {\n          rows: section.rows.map(item => {\n            // find the bookmark within the rows that is attempted to be removed\n            if (item.url === action.data.url) {\n              const newSite = Object.assign({}, item);\n              delete newSite.bookmarkGuid;\n              delete newSite.bookmarkTitle;\n              delete newSite.bookmarkDateCreated;\n              if (!newSite.type || newSite.type === \"bookmark\") {\n                newSite.type = \"history\";\n              }\n              return newSite;\n            }\n            return item;\n          }),\n        })\n      );\n    case at.PLACES_LINK_DELETED:\n    case at.PLACES_LINK_BLOCKED:\n      if (!action.data) {\n        return prevState;\n      }\n      return prevState.map(section =>\n        Object.assign({}, section, {\n          rows: section.rows.filter(site => site.url !== action.data.url),\n        })\n      );\n    case at.DELETE_FROM_POCKET:\n    case at.ARCHIVE_FROM_POCKET:\n      return prevState.map(section =>\n        Object.assign({}, section, {\n          rows: section.rows.filter(\n            site => site.pocket_id !== action.data.pocket_id\n          ),\n        })\n      );\n    case at.SNIPPETS_PREVIEW_MODE:\n      return prevState.map(section => ({ ...section, rows: [] }));\n    default:\n      return prevState;\n  }\n}\n\nfunction Snippets(prevState = INITIAL_STATE.Snippets, action) {\n  switch (action.type) {\n    case at.SNIPPETS_DATA:\n      return Object.assign({}, prevState, { initialized: true }, action.data);\n    case at.SNIPPET_BLOCKED:\n      return Object.assign({}, prevState, {\n        blockList: prevState.blockList.concat(action.data),\n      });\n    case at.SNIPPETS_BLOCKLIST_CLEARED:\n      return Object.assign({}, prevState, { blockList: [] });\n    case at.SNIPPETS_RESET:\n      return INITIAL_STATE.Snippets;\n    default:\n      return prevState;\n  }\n}\n\nfunction Pocket(prevState = INITIAL_STATE.Pocket, action) {\n  switch (action.type) {\n    case at.POCKET_WAITING_FOR_SPOC:\n      return { ...prevState, waitingForSpoc: action.data };\n    case at.POCKET_LOGGED_IN:\n      return { ...prevState, isUserLoggedIn: !!action.data };\n    case at.POCKET_CTA:\n      return {\n        ...prevState,\n        pocketCta: {\n          ctaButton: action.data.cta_button,\n          ctaText: action.data.cta_text,\n          ctaUrl: action.data.cta_url,\n          useCta: action.data.use_cta,\n        },\n      };\n    default:\n      return prevState;\n  }\n}\n\nfunction DiscoveryStream(prevState = INITIAL_STATE.DiscoveryStream, action) {\n  // Return if action data is empty, or spocs or feeds data is not loaded\n  const isNotReady = () =>\n    !action.data || !prevState.spocs.loaded || !prevState.feeds.loaded;\n\n  const handlePlacements = handleSites => {\n    const { data, placements } = prevState.spocs;\n    const result = {};\n\n    const forPlacement = placement => {\n      const placementSpocs = data[placement.name];\n\n      if (!placementSpocs || !placementSpocs.length) {\n        return;\n      }\n\n      result[placement.name] = handleSites(placementSpocs);\n    };\n\n    if (!placements || !placements.length) {\n      [{ name: \"spocs\" }].forEach(forPlacement);\n    } else {\n      placements.forEach(forPlacement);\n    }\n    return result;\n  };\n\n  const nextState = handleSites => ({\n    ...prevState,\n    spocs: {\n      ...prevState.spocs,\n      data: handlePlacements(handleSites),\n    },\n    feeds: {\n      ...prevState.feeds,\n      data: Object.keys(prevState.feeds.data).reduce(\n        (accumulator, feed_url) => {\n          accumulator[feed_url] = {\n            data: {\n              ...prevState.feeds.data[feed_url].data,\n              recommendations: handleSites(\n                prevState.feeds.data[feed_url].data.recommendations\n              ),\n            },\n          };\n          return accumulator;\n        },\n        {}\n      ),\n    },\n  });\n\n  switch (action.type) {\n    case at.DISCOVERY_STREAM_CONFIG_CHANGE:\n    // Fall through to a separate action is so it doesn't trigger a listener update on init\n    case at.DISCOVERY_STREAM_CONFIG_SETUP:\n      return { ...prevState, config: action.data || {} };\n    case at.DISCOVERY_STREAM_LAYOUT_UPDATE:\n      return {\n        ...prevState,\n        lastUpdated: action.data.lastUpdated || null,\n        layout: action.data.layout || [],\n      };\n    case at.HIDE_PRIVACY_INFO:\n      return {\n        ...prevState,\n        isPrivacyInfoModalVisible: false,\n      };\n    case at.SHOW_PRIVACY_INFO:\n      return {\n        ...prevState,\n        isPrivacyInfoModalVisible: true,\n      };\n    case at.DISCOVERY_STREAM_LAYOUT_RESET:\n      return { ...INITIAL_STATE.DiscoveryStream, config: prevState.config };\n    case at.DISCOVERY_STREAM_FEEDS_UPDATE:\n      return {\n        ...prevState,\n        feeds: {\n          ...prevState.feeds,\n          loaded: true,\n        },\n      };\n    case at.DISCOVERY_STREAM_FEED_UPDATE:\n      const newData = {};\n      newData[action.data.url] = action.data.feed;\n      return {\n        ...prevState,\n        feeds: {\n          ...prevState.feeds,\n          data: {\n            ...prevState.feeds.data,\n            ...newData,\n          },\n        },\n      };\n    case at.DISCOVERY_STREAM_SPOCS_CAPS:\n      return {\n        ...prevState,\n        spocs: {\n          ...prevState.spocs,\n          frequency_caps: [...prevState.spocs.frequency_caps, ...action.data],\n        },\n      };\n    case at.DISCOVERY_STREAM_SPOCS_ENDPOINT:\n      return {\n        ...prevState,\n        spocs: {\n          ...INITIAL_STATE.DiscoveryStream.spocs,\n          spocs_endpoint:\n            action.data.url ||\n            INITIAL_STATE.DiscoveryStream.spocs.spocs_endpoint,\n          spocs_per_domain:\n            action.data.spocs_per_domain ||\n            INITIAL_STATE.DiscoveryStream.spocs.spocs_per_domain,\n        },\n      };\n    case at.DISCOVERY_STREAM_SPOCS_PLACEMENTS:\n      return {\n        ...prevState,\n        spocs: {\n          ...prevState.spocs,\n          placements:\n            action.data.placements ||\n            INITIAL_STATE.DiscoveryStream.spocs.placements,\n        },\n      };\n    case at.DISCOVERY_STREAM_SPOCS_UPDATE:\n      if (action.data) {\n        return {\n          ...prevState,\n          spocs: {\n            ...prevState.spocs,\n            lastUpdated: action.data.lastUpdated,\n            data: action.data.spocs,\n            loaded: true,\n          },\n        };\n      }\n      return prevState;\n    case at.DISCOVERY_STREAM_SPOC_BLOCKED:\n      return {\n        ...prevState,\n        spocs: {\n          ...prevState.spocs,\n          blocked: [...prevState.spocs.blocked, action.data.url],\n        },\n      };\n    case at.DISCOVERY_STREAM_LINK_BLOCKED:\n      return isNotReady()\n        ? prevState\n        : nextState(items =>\n            items.filter(item => item.url !== action.data.url)\n          );\n\n    case at.PLACES_SAVED_TO_POCKET:\n      const addPocketInfo = item => {\n        if (item.url === action.data.url) {\n          return Object.assign({}, item, {\n            open_url: action.data.open_url,\n            pocket_id: action.data.pocket_id,\n            context_type: \"pocket\",\n          });\n        }\n        return item;\n      };\n      return isNotReady()\n        ? prevState\n        : nextState(items => items.map(addPocketInfo));\n\n    case at.DELETE_FROM_POCKET:\n    case at.ARCHIVE_FROM_POCKET:\n      return isNotReady()\n        ? prevState\n        : nextState(items =>\n            items.filter(item => item.pocket_id !== action.data.pocket_id)\n          );\n\n    case at.PLACES_BOOKMARK_ADDED:\n      const updateBookmarkInfo = item => {\n        if (item.url === action.data.url) {\n          const { bookmarkGuid, bookmarkTitle, dateAdded } = action.data;\n          return Object.assign({}, item, {\n            bookmarkGuid,\n            bookmarkTitle,\n            bookmarkDateCreated: dateAdded,\n            context_type: \"bookmark\",\n          });\n        }\n        return item;\n      };\n      return isNotReady()\n        ? prevState\n        : nextState(items => items.map(updateBookmarkInfo));\n\n    case at.PLACES_BOOKMARK_REMOVED:\n      const removeBookmarkInfo = item => {\n        if (item.url === action.data.url) {\n          const newSite = Object.assign({}, item);\n          delete newSite.bookmarkGuid;\n          delete newSite.bookmarkTitle;\n          delete newSite.bookmarkDateCreated;\n          if (!newSite.context_type || newSite.context_type === \"bookmark\") {\n            newSite.context_type = \"removedBookmark\";\n          }\n          return newSite;\n        }\n        return item;\n      };\n      return isNotReady()\n        ? prevState\n        : nextState(items => items.map(removeBookmarkInfo));\n\n    default:\n      return prevState;\n  }\n}\n\nfunction Search(prevState = INITIAL_STATE.Search, action) {\n  switch (action.type) {\n    case at.HIDE_SEARCH:\n      return Object.assign({ ...prevState, hide: true });\n    case at.FAKE_FOCUS_SEARCH:\n      return Object.assign({ ...prevState, fakeFocus: true });\n    case at.SHOW_SEARCH:\n      return Object.assign({ ...prevState, hide: false, fakeFocus: false });\n    default:\n      return prevState;\n  }\n}\n\nthis.INITIAL_STATE = INITIAL_STATE;\nthis.TOP_SITES_DEFAULT_ROWS = TOP_SITES_DEFAULT_ROWS;\nthis.TOP_SITES_MAX_SITES_PER_ROW = TOP_SITES_MAX_SITES_PER_ROW;\n\nthis.reducers = {\n  TopSites,\n  App,\n  ASRouter,\n  Snippets,\n  Prefs,\n  Dialog,\n  Sections,\n  Pocket,\n  DiscoveryStream,\n  Search,\n};\n\nconst EXPORTED_SYMBOLS = [\n  \"reducers\",\n  \"INITIAL_STATE\",\n  \"insertPinned\",\n  \"TOP_SITES_DEFAULT_ROWS\",\n  \"TOP_SITES_MAX_SITES_PER_ROW\",\n];\n"
  },
  {
    "path": "components.conf",
    "content": "# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-\n# vim: set filetype=python:\n# This Source Code Form is subject to the terms of the Mozilla Public\n# License, v. 2.0. If a copy of the MPL was not distributed with this\n# file, You can obtain one at http://mozilla.org/MPL/2.0/.\n\nClasses = [\n    {\n        'cid': '{dfcd2adc-7867-4d3a-ba70-17501f208142}',\n        'contract_ids': ['@mozilla.org/browser/aboutnewtab-service;1'],\n        'jsm': 'resource:///modules/AboutNewTabService.jsm',\n        'constructor': 'AboutNewTabService',\n    },\n]\n"
  },
  {
    "path": "content-src/.eslintrc.js",
    "content": "module.exports = {\n  rules: {\n    \"import/no-commonjs\": 2\n  }\n}\n"
  },
  {
    "path": "content-src/aboutlibrary/aboutlibrary.jsx",
    "content": "import React from \"react\";\nimport ReactDOM from \"react-dom\";\n\nclass LibraryRouter extends React.PureComponent {\n  render() {\n    return <div className=\"under-construction\" />;\n  }\n}\nReactDOM.render(<LibraryRouter />, document.body);\n"
  },
  {
    "path": "content-src/aboutlibrary/aboutlibrary.scss",
    "content": "\n.under-construction {\n  background-image: url('chrome://browser/content/illustrations/under-construction.svg');\n  background-repeat: no-repeat;\n  background-position: center;\n  min-height: 300px;\n  min-width: 300px;\n  margin-top: 10%;\n}\n"
  },
  {
    "path": "content-src/activity-stream.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport { actionCreators as ac, actionTypes as at } from \"common/Actions.jsm\";\nimport { Base } from \"content-src/components/Base/Base\";\nimport { DetectUserSessionStart } from \"content-src/lib/detect-user-session-start\";\nimport { initStore } from \"content-src/lib/init-store\";\nimport { Provider } from \"react-redux\";\nimport React from \"react\";\nimport ReactDOM from \"react-dom\";\nimport { reducers } from \"common/Reducers.jsm\";\n\nconst store = initStore(reducers);\n\nnew DetectUserSessionStart(store).sendEventOrAddListener();\n\nstore.dispatch(ac.AlsoToMain({ type: at.NEW_TAB_STATE_REQUEST }));\n\nReactDOM.hydrate(\n  <Provider store={store}>\n    <Base\n      isFirstrun={global.document.location.href === \"about:welcome\"}\n      locale={global.document.documentElement.lang}\n      strings={global.gActivityStreamStrings}\n    />\n  </Provider>,\n  document.getElementById(\"root\")\n);\n"
  },
  {
    "path": "content-src/asrouter/README.md",
    "content": "# Activity Stream Router\n\n## Preferences `browser.newtab.activity-stream.asrouter.*`\n\nName | Used for | Type | Example value\n---  | ---      | ---  | ---\n`whitelistHosts` | Whitelist a host in order to fetch messages from its endpoint | `[String]` |  `[\"gist.github.com\", \"gist.githubusercontent.com\", \"localhost:8000\"]`\n`providers.snippets` | Message provider options for snippets | `Object` | [see below](#message-providers)\n`providers.cfr` | Message provider options for cfr | `Object` | [see below](#message-providers)\n`providers.onboarding` | Message provider options for onboarding | `Object` | [see below](#message-providers)\n`useRemoteL10n` | Controls whether to use the remote Fluent files for l10n, default as `true` | `Boolean` | `[true|false]`\n\n### Message providers examples\n\n```json\n{\n  \"id\" : \"snippets\",\n  \"type\" : \"remote\",\n  \"enabled\": true,\n  \"url\" : \"https://snippets.cdn.mozilla.net/us-west/bundles/bundle_d6d90fb9098ce8b45e60acf601bcb91b68322309.json\",\n  \"updateCycleInMs\" : 14400000\n}\n```\n\n```json\n{\n  \"id\" : \"onboarding\",\n  \"enabled\": true,\n  \"type\" : \"local\",\n  \"localProvider\" : \"OnboardingMessageProvider\"\n}\n```\n\n### [Snippet message format documentation](https://github.com/mozilla/activity-stream/blob/master/content-src/asrouter/schemas/message-format.md)\n"
  },
  {
    "path": "content-src/asrouter/asrouter-content.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport {\n  actionCreators as ac,\n  actionTypes as at,\n  ASRouterActions as ra,\n} from \"common/Actions.jsm\";\nimport { OUTGOING_MESSAGE_NAME as AS_GENERAL_OUTGOING_MESSAGE_NAME } from \"content-src/lib/init-store\";\nimport { generateBundles } from \"./rich-text-strings\";\nimport { ImpressionsWrapper } from \"./components/ImpressionsWrapper/ImpressionsWrapper\";\nimport { LocalizationProvider } from \"fluent-react\";\nimport { NEWTAB_DARK_THEME } from \"content-src/lib/constants\";\nimport React from \"react\";\nimport ReactDOM from \"react-dom\";\nimport { SnippetsTemplates } from \"./templates/template-manifest\";\nimport { FirstRun } from \"./templates/FirstRun/FirstRun\";\n\nconst INCOMING_MESSAGE_NAME = \"ASRouter:parent-to-child\";\nconst OUTGOING_MESSAGE_NAME = \"ASRouter:child-to-parent\";\nconst TEMPLATES_ABOVE_PAGE = [\n  \"trailhead\",\n  \"full_page_interrupt\",\n  \"return_to_amo_overlay\",\n  \"extended_triplets\",\n];\nconst FIRST_RUN_TEMPLATES = TEMPLATES_ABOVE_PAGE;\nconst TEMPLATES_BELOW_SEARCH = [\"simple_below_search_snippet\"];\n\nexport const ASRouterUtils = {\n  addListener(listener) {\n    if (global.RPMAddMessageListener) {\n      global.RPMAddMessageListener(INCOMING_MESSAGE_NAME, listener);\n    }\n  },\n  removeListener(listener) {\n    if (global.RPMRemoveMessageListener) {\n      global.RPMRemoveMessageListener(INCOMING_MESSAGE_NAME, listener);\n    }\n  },\n  sendMessage(action) {\n    if (global.RPMSendAsyncMessage) {\n      global.RPMSendAsyncMessage(OUTGOING_MESSAGE_NAME, action);\n    }\n  },\n  blockById(id, options) {\n    ASRouterUtils.sendMessage({\n      type: \"BLOCK_MESSAGE_BY_ID\",\n      data: { id, ...options },\n    });\n  },\n  dismissById(id) {\n    ASRouterUtils.sendMessage({ type: \"DISMISS_MESSAGE_BY_ID\", data: { id } });\n  },\n  executeAction(button_action) {\n    ASRouterUtils.sendMessage({\n      type: \"USER_ACTION\",\n      data: button_action,\n    });\n  },\n  unblockById(id) {\n    ASRouterUtils.sendMessage({ type: \"UNBLOCK_MESSAGE_BY_ID\", data: { id } });\n  },\n  unblockBundle(bundle) {\n    ASRouterUtils.sendMessage({ type: \"UNBLOCK_BUNDLE\", data: { bundle } });\n  },\n  overrideMessage(id) {\n    ASRouterUtils.sendMessage({ type: \"OVERRIDE_MESSAGE\", data: { id } });\n  },\n  sendTelemetry(ping) {\n    if (global.RPMSendAsyncMessage) {\n      const payload = ac.ASRouterUserEvent(ping);\n      global.RPMSendAsyncMessage(AS_GENERAL_OUTGOING_MESSAGE_NAME, payload);\n    }\n  },\n  getPreviewEndpoint() {\n    if (global.location && global.location.href.includes(\"endpoint\")) {\n      const params = new URLSearchParams(\n        global.location.href.slice(global.location.href.indexOf(\"endpoint\"))\n      );\n      try {\n        const endpoint = new URL(params.get(\"endpoint\"));\n        return {\n          url: endpoint.href,\n          snippetId: params.get(\"snippetId\"),\n          theme: this.getPreviewTheme(),\n        };\n      } catch (e) {}\n    }\n\n    return null;\n  },\n  getPreviewTheme() {\n    return new URLSearchParams(\n      global.location.href.slice(global.location.href.indexOf(\"theme\"))\n    ).get(\"theme\");\n  },\n};\n\n// Note: nextProps/prevProps refer to props passed to <ImpressionsWrapper />, not <ASRouterUISurface />\nfunction shouldSendImpressionOnUpdate(nextProps, prevProps) {\n  return (\n    nextProps.message.id &&\n    (!prevProps.message || prevProps.message.id !== nextProps.message.id)\n  );\n}\n\nexport class ASRouterUISurface extends React.PureComponent {\n  constructor(props) {\n    super(props);\n    this.onMessageFromParent = this.onMessageFromParent.bind(this);\n    this.sendClick = this.sendClick.bind(this);\n    this.sendImpression = this.sendImpression.bind(this);\n    this.sendUserActionTelemetry = this.sendUserActionTelemetry.bind(this);\n    this.onUserAction = this.onUserAction.bind(this);\n    this.fetchFlowParams = this.fetchFlowParams.bind(this);\n\n    this.state = { message: {}, interruptCleared: false };\n    if (props.document) {\n      this.headerPortal = props.document.getElementById(\n        \"header-asrouter-container\"\n      );\n      this.footerPortal = props.document.getElementById(\n        \"footer-asrouter-container\"\n      );\n    }\n  }\n\n  async fetchFlowParams(params = {}) {\n    let result = {};\n    const { fxaEndpoint, dispatch } = this.props;\n    if (!fxaEndpoint) {\n      const err =\n        \"Tried to fetch flow params before fxaEndpoint pref was ready\";\n      console.error(err); // eslint-disable-line no-console\n    }\n\n    try {\n      const urlObj = new URL(fxaEndpoint);\n      urlObj.pathname = \"metrics-flow\";\n      Object.keys(params).forEach(key => {\n        urlObj.searchParams.append(key, params[key]);\n      });\n      const response = await fetch(urlObj.toString(), { credentials: \"omit\" });\n      if (response.status === 200) {\n        const { deviceId, flowId, flowBeginTime } = await response.json();\n        result = { deviceId, flowId, flowBeginTime };\n      } else {\n        console.error(\"Non-200 response\", response); // eslint-disable-line no-console\n        dispatch(\n          ac.OnlyToMain({\n            type: at.TELEMETRY_UNDESIRED_EVENT,\n            data: {\n              event: \"FXA_METRICS_FETCH_ERROR\",\n              value: response.status,\n            },\n          })\n        );\n      }\n    } catch (error) {\n      console.error(error); // eslint-disable-line no-console\n      dispatch(\n        ac.OnlyToMain({\n          type: at.TELEMETRY_UNDESIRED_EVENT,\n          data: { event: \"FXA_METRICS_ERROR\" },\n        })\n      );\n    }\n    return result;\n  }\n\n  sendUserActionTelemetry(extraProps = {}) {\n    const { message } = this.state;\n    const eventType = `${message.provider}_user_event`;\n    ASRouterUtils.sendTelemetry({\n      message_id: message.id,\n      source: extraProps.id,\n      action: eventType,\n      ...extraProps,\n    });\n  }\n\n  sendImpression(extraProps) {\n    if (this.state.message.provider === \"preview\") {\n      return;\n    }\n\n    ASRouterUtils.sendMessage({ type: \"IMPRESSION\", data: this.state.message });\n    this.sendUserActionTelemetry({ event: \"IMPRESSION\", ...extraProps });\n  }\n\n  // If link has a `metric` data attribute send it as part of the `event_context`\n  // telemetry field which can have arbitrary values.\n  // Used for router messages with links as part of the content.\n  sendClick(event) {\n    const metric = {\n      event_context: event.target.dataset.metric,\n      // Used for the `source` of the event. Needed to differentiate\n      // from other snippet or onboarding events that may occur.\n      id: \"NEWTAB_FOOTER_BAR_CONTENT\",\n    };\n    const action = {\n      type: event.target.dataset.action,\n      data: { args: event.target.dataset.args },\n    };\n    if (action.type) {\n      ASRouterUtils.executeAction(action);\n    }\n    if (\n      !this.state.message.content.do_not_autoblock &&\n      !event.target.dataset.do_not_autoblock\n    ) {\n      ASRouterUtils.blockById(this.state.message.id);\n    }\n    if (this.state.message.provider !== \"preview\") {\n      this.sendUserActionTelemetry({ event: \"CLICK_BUTTON\", ...metric });\n    }\n  }\n\n  onBlockById(id) {\n    return options => ASRouterUtils.blockById(id, options);\n  }\n\n  onDismissById(id) {\n    return () => ASRouterUtils.dismissById(id);\n  }\n\n  clearMessage(id) {\n    // Request new set of dynamic triplet cards when click on a card CTA clear\n    // message and 'id' matches one of the cards in message bundle\n    if (\n      this.state.message &&\n      this.state.message.bundle &&\n      this.state.message.bundle.find(card => card.id === id)\n    ) {\n      this.requestMessage();\n    }\n\n    if (id === this.state.message.id) {\n      this.setState({ message: {} });\n      // Remove any styles related to the RTAMO message\n      document.body.classList.remove(\"welcome\", \"hide-main\", \"amo\");\n    }\n  }\n\n  onMessageFromParent({ data: action }) {\n    switch (action.type) {\n      case \"SET_MESSAGE\":\n        this.setState({ message: action.data });\n        break;\n      case \"CLEAR_INTERRUPT\":\n        this.setState({ interruptCleared: true });\n        break;\n      case \"CLEAR_MESSAGE\":\n        this.clearMessage(action.data.id);\n        break;\n      case \"CLEAR_PROVIDER\":\n        if (action.data.id === this.state.message.provider) {\n          this.setState({ message: {} });\n        }\n        break;\n      case \"CLEAR_ALL\":\n        this.setState({ message: {} });\n        break;\n      case \"AS_ROUTER_TARGETING_UPDATE\":\n        action.data.forEach(id => this.clearMessage(id));\n        break;\n    }\n  }\n\n  requestMessage(endpoint) {\n    // If we are loading about:welcome we want to trigger the onboarding messages\n    if (\n      this.props.document &&\n      this.props.document.location.href === \"about:welcome\"\n    ) {\n      ASRouterUtils.sendMessage({\n        type: \"TRIGGER\",\n        data: { trigger: { id: \"firstRun\" } },\n      });\n    } else {\n      ASRouterUtils.sendMessage({\n        type: \"NEWTAB_MESSAGE_REQUEST\",\n        data: { endpoint },\n      });\n    }\n  }\n\n  componentWillMount() {\n    const endpoint = ASRouterUtils.getPreviewEndpoint();\n    if (endpoint && endpoint.theme === \"dark\") {\n      global.window.dispatchEvent(\n        new CustomEvent(\"LightweightTheme:Set\", {\n          detail: { data: NEWTAB_DARK_THEME },\n        })\n      );\n    }\n    ASRouterUtils.addListener(this.onMessageFromParent);\n    this.requestMessage(endpoint);\n  }\n\n  componentWillUnmount() {\n    ASRouterUtils.removeListener(this.onMessageFromParent);\n  }\n\n  async getMonitorUrl({ url, flowRequestParams = {} }) {\n    const flowValues = await this.fetchFlowParams(flowRequestParams);\n\n    // Note that flowParams are actually added dynamically on the page\n    const urlObj = new URL(url);\n    [\"deviceId\", \"flowId\", \"flowBeginTime\"].forEach(key => {\n      if (key in flowValues) {\n        urlObj.searchParams.append(key, flowValues[key]);\n      }\n    });\n\n    return urlObj.toString();\n  }\n\n  async onUserAction(action) {\n    switch (action.type) {\n      // This needs to be handled locally because its\n      case ra.ENABLE_FIREFOX_MONITOR:\n        const url = await this.getMonitorUrl(action.data.args);\n        ASRouterUtils.executeAction({ type: ra.OPEN_URL, data: { args: url } });\n        break;\n      default:\n        ASRouterUtils.executeAction(action);\n    }\n  }\n\n  renderSnippets() {\n    const { message } = this.state;\n    if (!SnippetsTemplates[message.template]) {\n      return null;\n    }\n    const SnippetComponent = SnippetsTemplates[message.template];\n    const { content } = this.state.message;\n\n    return (\n      <ImpressionsWrapper\n        id=\"NEWTAB_FOOTER_BAR\"\n        message={this.state.message}\n        sendImpression={this.sendImpression}\n        shouldSendImpressionOnUpdate={shouldSendImpressionOnUpdate}\n        // This helps with testing\n        document={this.props.document}\n      >\n        <LocalizationProvider bundles={generateBundles(content)}>\n          <SnippetComponent\n            {...this.state.message}\n            UISurface=\"NEWTAB_FOOTER_BAR\"\n            onBlock={this.onBlockById(this.state.message.id)}\n            onDismiss={this.onDismissById(this.state.message.id)}\n            onAction={this.onUserAction}\n            sendClick={this.sendClick}\n            sendUserActionTelemetry={this.sendUserActionTelemetry}\n          />\n        </LocalizationProvider>\n      </ImpressionsWrapper>\n    );\n  }\n\n  renderPreviewBanner() {\n    if (this.state.message.provider !== \"preview\") {\n      return null;\n    }\n\n    return (\n      <div className=\"snippets-preview-banner\">\n        <span className=\"icon icon-small-spacer icon-info\" />\n        <span>Preview Purposes Only</span>\n      </div>\n    );\n  }\n\n  renderFirstRun() {\n    const { message } = this.state;\n    if (FIRST_RUN_TEMPLATES.includes(message.template)) {\n      return (\n        <ImpressionsWrapper\n          id=\"FIRST_RUN\"\n          message={this.state.message}\n          sendImpression={this.sendImpression}\n          shouldSendImpressionOnUpdate={shouldSendImpressionOnUpdate}\n          // This helps with testing\n          document={this.props.document}\n        >\n          <FirstRun\n            document={this.props.document}\n            interruptCleared={this.state.interruptCleared}\n            message={message}\n            sendUserActionTelemetry={this.sendUserActionTelemetry}\n            executeAction={ASRouterUtils.executeAction}\n            dispatch={this.props.dispatch}\n            onBlockById={ASRouterUtils.blockById}\n            onDismiss={this.onDismissById(this.state.message.id)}\n            fxaEndpoint={this.props.fxaEndpoint}\n            appUpdateChannel={this.props.appUpdateChannel}\n            fetchFlowParams={this.fetchFlowParams}\n          />\n        </ImpressionsWrapper>\n      );\n    }\n    return null;\n  }\n\n  render() {\n    const { message } = this.state;\n    if (!message.id) {\n      return null;\n    }\n    const shouldRenderBelowSearch = TEMPLATES_BELOW_SEARCH.includes(\n      message.template\n    );\n    const shouldRenderInHeader = TEMPLATES_ABOVE_PAGE.includes(\n      message.template\n    );\n\n    return shouldRenderBelowSearch ? (\n      // Render special below search snippets in place;\n      <div className=\"below-search-snippet-wrapper\">\n        {this.renderSnippets()}\n      </div>\n    ) : (\n      // For onboarding, regular snippets etc. we should render\n      // everything in our footer container.\n      ReactDOM.createPortal(\n        <>\n          {this.renderPreviewBanner()}\n          {this.renderFirstRun()}\n          {this.renderSnippets()}\n        </>,\n        shouldRenderInHeader ? this.headerPortal : this.footerPortal\n      )\n    );\n  }\n}\n\nASRouterUISurface.defaultProps = { document: global.document };\n"
  },
  {
    "path": "content-src/asrouter/components/Button/Button.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport React from \"react\";\n\nconst ALLOWED_STYLE_TAGS = [\"color\", \"backgroundColor\"];\n\nexport const Button = props => {\n  const style = {};\n\n  // Add allowed style tags from props, e.g. props.color becomes style={color: props.color}\n  for (const tag of ALLOWED_STYLE_TAGS) {\n    if (typeof props[tag] !== \"undefined\") {\n      style[tag] = props[tag];\n    }\n  }\n  // remove border if bg is set to something custom\n  if (style.backgroundColor) {\n    style.border = \"0\";\n  }\n\n  return (\n    <button\n      onClick={props.onClick}\n      className={props.className || \"ASRouterButton secondary\"}\n      style={style}\n    >\n      {props.children}\n    </button>\n  );\n};\n"
  },
  {
    "path": "content-src/asrouter/components/Button/_Button.scss",
    "content": ".ASRouterButton {\n  font-weight: 600;\n  font-size: 14px;\n  white-space: nowrap;\n  border-radius: 2px;\n  border: 0;\n  font-family: inherit;\n  padding: 8px 15px;\n  margin-inline-start: 12px;\n  color: inherit;\n  cursor: pointer;\n\n  .tall & {\n    margin-inline-start: 20px;\n  }\n\n  &.primary {\n    border: 1px solid var(--newtab-button-primary-color);\n    background-color: var(--newtab-button-primary-color);\n    color: $grey-10;\n\n    &:hover {\n      background-color: $blue-70;\n    }\n\n    &:active {\n      background-color: $blue-80;\n    }\n  }\n\n  &.secondary {\n    background-color: $grey-90-10;\n\n    &:hover {\n      background-color: $grey-90-20;\n    }\n\n    &:active {\n      background-color: $grey-90-30;\n    }\n\n    &:focus {\n      box-shadow: 0 0 0 1px $blue-50 inset, 0 0 0 1px $blue-50, 0 0 0 4px $blue-50-30;\n    }\n  }\n}\n\n[lwt-newtab-brighttext] {\n  .secondary {\n    background-color: $grey-10-10;\n\n    &:hover {\n      background-color: $grey-10-20;\n    }\n\n    &:active {\n      background-color: $grey-10-30;\n    }\n  }\n\n  // Snippets scene 2 footer\n  .footer {\n    .secondary {\n      background-color: $grey-10-30;\n\n      &:hover {\n        background-color: $grey-10-40;\n      }\n\n      &:active {\n        background-color: $grey-10-50;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "content-src/asrouter/components/ConditionalWrapper/ConditionalWrapper.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\n// lifted from https://gist.github.com/kitze/23d82bb9eb0baabfd03a6a720b1d637f\nconst ConditionalWrapper = ({ condition, wrap, children }) =>\n  condition ? wrap(children) : children;\n\nexport default ConditionalWrapper;\n"
  },
  {
    "path": "content-src/asrouter/components/FxASignupForm/FxASignupForm.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport { actionCreators as ac } from \"common/Actions.jsm\";\nimport {\n  addUtmParams,\n  BASE_PARAMS,\n} from \"../../templates/FirstRun/addUtmParams\";\nimport React from \"react\";\n\nexport class FxASignupForm extends React.PureComponent {\n  constructor(props) {\n    super(props);\n    this.onSubmit = this.onSubmit.bind(this);\n    this.onInputChange = this.onInputChange.bind(this);\n    this.onInputInvalid = this.onInputInvalid.bind(this);\n    this.handleSignIn = this.handleSignIn.bind(this);\n\n    this.state = {\n      emailInput: \"\",\n    };\n  }\n\n  get email() {\n    return this.props.document\n      .getElementById(\"fxaSignupForm\")\n      .querySelector(\"input[name=email]\");\n  }\n\n  onSubmit(event) {\n    let userEvent = \"SUBMIT_EMAIL\";\n    const { email } = event.target.elements;\n    if (email.disabled) {\n      userEvent = \"SUBMIT_SIGNIN\";\n    } else if (!email.value.length) {\n      email.required = true;\n      email.checkValidity();\n      event.preventDefault();\n      return;\n    }\n\n    // Report to telemetry additional information about the form submission.\n    const value = { has_flow_params: !!this.props.flowParams.flowId.length };\n    this.props.dispatch(ac.UserEvent({ event: userEvent, value }));\n\n    global.addEventListener(\"visibilitychange\", this.props.onClose);\n  }\n\n  handleSignIn(event) {\n    // Set disabled to prevent email from appearing in url resulting in the wrong page\n    this.email.disabled = true;\n  }\n\n  componentDidMount() {\n    // Start with focus in the email input box\n    if (this.email) {\n      this.email.focus();\n    }\n  }\n\n  onInputChange(e) {\n    let error = e.target.previousSibling;\n    this.setState({ emailInput: e.target.value });\n    error.classList.remove(\"active\");\n    e.target.classList.remove(\"invalid\");\n  }\n\n  onInputInvalid(e) {\n    let error = e.target.previousSibling;\n    error.classList.add(\"active\");\n    e.target.classList.add(\"invalid\");\n    e.preventDefault(); // Override built-in form validation popup\n    e.target.focus();\n  }\n\n  render() {\n    const { content, UTMTerm } = this.props;\n    return (\n      <div\n        id=\"fxaSignupForm\"\n        role=\"group\"\n        aria-labelledby=\"joinFormHeader\"\n        aria-describedby=\"joinFormBody\"\n        className=\"fxaSignupForm\"\n      >\n        <h3 id=\"joinFormHeader\" data-l10n-id={content.form.title.string_id} />\n        <p id=\"joinFormBody\" data-l10n-id={content.form.text.string_id} />\n        <form\n          method=\"get\"\n          action={this.props.fxaEndpoint}\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n          onSubmit={this.onSubmit}\n        >\n          <input name=\"action\" type=\"hidden\" value=\"email\" />\n          <input name=\"context\" type=\"hidden\" value=\"fx_desktop_v3\" />\n          <input\n            name=\"entrypoint\"\n            type=\"hidden\"\n            value=\"activity-stream-firstrun\"\n          />\n          <input name=\"utm_source\" type=\"hidden\" value=\"activity-stream\" />\n          <input\n            name=\"utm_campaign\"\n            type=\"hidden\"\n            value={BASE_PARAMS.utm_campaign}\n          />\n          <input name=\"utm_term\" type=\"hidden\" value={UTMTerm} />\n          <input\n            name=\"device_id\"\n            type=\"hidden\"\n            value={this.props.flowParams.deviceId}\n          />\n          <input\n            name=\"flow_id\"\n            type=\"hidden\"\n            value={this.props.flowParams.flowId}\n          />\n          <input\n            name=\"flow_begin_time\"\n            type=\"hidden\"\n            value={this.props.flowParams.flowBeginTime}\n          />\n          <input name=\"style\" type=\"hidden\" value=\"trailhead\" />\n          <p\n            data-l10n-id=\"onboarding-join-form-email-error\"\n            className=\"error\"\n          />\n          <input\n            data-l10n-id={content.form.email.string_id}\n            name=\"email\"\n            type=\"email\"\n            onInvalid={this.onInputInvalid}\n            onChange={this.onInputChange}\n          />\n          <p className=\"fxa-terms\" data-l10n-id=\"onboarding-join-form-legal\">\n            <a\n              data-l10n-name=\"terms\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              href={addUtmParams(\n                \"https://accounts.firefox.com/legal/terms\",\n                UTMTerm\n              )}\n            />\n            <a\n              data-l10n-name=\"privacy\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              href={addUtmParams(\n                \"https://accounts.firefox.com/legal/privacy\",\n                UTMTerm\n              )}\n            />\n          </p>\n          <button data-l10n-id={content.form.button.string_id} type=\"submit\" />\n          {this.props.showSignInLink && (\n            <div className=\"fxa-signin\">\n              <span data-l10n-id=\"onboarding-join-form-signin-label\" />\n              <button\n                data-l10n-id=\"onboarding-join-form-signin\"\n                onClick={this.handleSignIn}\n              />\n            </div>\n          )}\n        </form>\n      </div>\n    );\n  }\n}\n\nFxASignupForm.defaultProps = { document: global.document };\n"
  },
  {
    "path": "content-src/asrouter/components/FxASignupForm/_FxASignupForm.scss",
    "content": ".fxaSignupForm {\n  min-width: 260px;\n  text-align: center;\n\n  a {\n    color: $white;\n    text-decoration: underline;\n  }\n\n  input,\n  button {\n    border-radius: 4px;\n    padding: 10px;\n  }\n\n  h3 {\n    font-size: 36px;\n    font-weight: 200;\n    line-height: 46px;\n    margin: 12px 0 4px;\n  }\n\n  p {\n    font-size: 15px;\n    line-height: 22px;\n    margin: 0 0 20px;\n  }\n\n  .fxa-terms {\n    margin: 4px 30px 20px;\n\n    a,\n    & {\n      color: $white-70;\n      font-size: 12px;\n      line-height: 20px;\n    }\n  }\n\n  .fxa-signin {\n    font-size: 16px;\n    margin-top: 19px;\n\n    span {\n      margin-inline-end: 5px;\n    }\n\n    button {\n      background-color: initial;\n      text-decoration: underline;\n      color: $white;\n      display: inline;\n      padding: 0;\n      width: auto;\n\n      &:hover,\n      &:focus,\n      &:active {\n        background-color: initial;\n      }\n    }\n  }\n\n  form {\n    position: relative;\n\n    .error.active {\n      inset-inline-start: 0;\n      z-index: 0;\n    }\n  }\n\n  button,\n  input {\n    width: 100%;\n  }\n\n  input {\n    background-color: $white;\n    border: 1px solid $grey-50;\n    box-shadow: none;\n    color: $grey-70;\n    font-size: 15px;\n    transition: border-color 150ms, box-shadow 150ms;\n\n    &:hover {\n      border-color: $grey-90;\n    }\n\n    &:focus {\n      border-color: $blue-50;\n      box-shadow: 0 0 0 3px $email-input-focus;\n    }\n\n    &.invalid {\n      border-color: $red-60;\n    }\n\n    &.invalid:focus {\n      box-shadow: 0 0 0 3px $email-input-invalid;\n    }\n  }\n\n  button {\n    background-color: $blue-60;\n    border: 0;\n    cursor: pointer;\n    display: block;\n    font-size: 15px;\n    font-weight: 400;\n    padding: 14px;\n\n    &:hover,\n    &:focus {\n      background-color: $trailhead-blue-60;\n    }\n\n    &:focus {\n      outline: dotted 1px;\n    }\n\n    &:active {\n      background-color: $trailhead-blue-70;\n    }\n  }\n}\n"
  },
  {
    "path": "content-src/asrouter/components/ImpressionsWrapper/ImpressionsWrapper.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport React from \"react\";\n\nexport const VISIBLE = \"visible\";\nexport const VISIBILITY_CHANGE_EVENT = \"visibilitychange\";\n\n/**\n * Component wrapper used to send telemetry pings on every impression.\n */\nexport class ImpressionsWrapper extends React.PureComponent {\n  // This sends an event when a user sees a set of new content. If content\n  // changes while the page is hidden (i.e. preloaded or on a hidden tab),\n  // only send the event if the page becomes visible again.\n  sendImpressionOrAddListener() {\n    if (this.props.document.visibilityState === VISIBLE) {\n      this.props.sendImpression({ id: this.props.id });\n    } else {\n      // We should only ever send the latest impression stats ping, so remove any\n      // older listeners.\n      if (this._onVisibilityChange) {\n        this.props.document.removeEventListener(\n          VISIBILITY_CHANGE_EVENT,\n          this._onVisibilityChange\n        );\n      }\n\n      // When the page becomes visible, send the impression stats ping if the section isn't collapsed.\n      this._onVisibilityChange = () => {\n        if (this.props.document.visibilityState === VISIBLE) {\n          this.props.sendImpression({ id: this.props.id });\n          this.props.document.removeEventListener(\n            VISIBILITY_CHANGE_EVENT,\n            this._onVisibilityChange\n          );\n        }\n      };\n      this.props.document.addEventListener(\n        VISIBILITY_CHANGE_EVENT,\n        this._onVisibilityChange\n      );\n    }\n  }\n\n  componentWillUnmount() {\n    if (this._onVisibilityChange) {\n      this.props.document.removeEventListener(\n        VISIBILITY_CHANGE_EVENT,\n        this._onVisibilityChange\n      );\n    }\n  }\n\n  componentDidMount() {\n    if (this.props.sendOnMount) {\n      this.sendImpressionOrAddListener();\n    }\n  }\n\n  componentDidUpdate(prevProps) {\n    if (this.props.shouldSendImpressionOnUpdate(this.props, prevProps)) {\n      this.sendImpressionOrAddListener();\n    }\n  }\n\n  render() {\n    return this.props.children;\n  }\n}\n\nImpressionsWrapper.defaultProps = {\n  document: global.document,\n  sendOnMount: true,\n};\n"
  },
  {
    "path": "content-src/asrouter/components/ModalOverlay/ModalOverlay.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport React from \"react\";\n\nexport class ModalOverlayWrapper extends React.PureComponent {\n  constructor(props) {\n    super(props);\n    this.onKeyDown = this.onKeyDown.bind(this);\n  }\n\n  // The intended behaviour is to listen for an escape key\n  // but not for a click; see Bug 1582242\n  onKeyDown(event) {\n    if (event.key === \"Escape\") {\n      this.props.onClose(event);\n    }\n  }\n\n  componentWillMount() {\n    this.props.document.addEventListener(\"keydown\", this.onKeyDown);\n    this.props.document.body.classList.add(\"modal-open\");\n    this.header = this.props.document.getElementById(\n      \"header-asrouter-container\"\n    );\n\n    if (this.header) {\n      this.header.classList.add(\"modal-scroll\");\n      this.props.document.getElementById(\"root\").classList.add(\"modal-height\");\n    }\n  }\n\n  componentWillUnmount() {\n    this.props.document.removeEventListener(\"keydown\", this.onKeyDown);\n    this.props.document.body.classList.remove(\"modal-open\");\n\n    if (this.header) {\n      this.header.classList.remove(\"modal-scroll\");\n      this.props.document\n        .getElementById(\"root\")\n        .classList.remove(\"modal-height\");\n    }\n  }\n\n  render() {\n    const { props } = this;\n    let className = props.unstyled ? \"\" : \"modalOverlayInner active\";\n    if (props.innerClassName) {\n      className += ` ${props.innerClassName}`;\n    }\n    return (\n      <React.Fragment>\n        <div\n          className=\"modalOverlayOuter active\"\n          onKeyDown={this.onKeyDown}\n          role=\"presentation\"\n        />\n        <div\n          className={className}\n          aria-labelledby={props.headerId}\n          id={props.id}\n          role=\"dialog\"\n        >\n          {props.hasDismissIcon && (\n            <button\n              className=\"icon icon-dismiss\"\n              onClick={props.onClose}\n              data-l10n-id=\"onboarding-cards-dismiss\"\n            />\n          )}\n          {props.children}\n        </div>\n      </React.Fragment>\n    );\n  }\n}\n\nModalOverlayWrapper.defaultProps = { document: global.document };\n\nexport class ModalOverlay extends React.PureComponent {\n  render() {\n    const { title, button_label } = this.props;\n    return (\n      <ModalOverlayWrapper onClose={this.props.onDismissBundle}>\n        <h2> {title} </h2>\n        {this.props.children}\n        <div className=\"footer\">\n          <button\n            className=\"button primary modalButton\"\n            onClick={this.props.onDismissBundle}\n          >\n            {\" \"}\n            {button_label}{\" \"}\n          </button>\n        </div>\n      </ModalOverlayWrapper>\n    );\n  }\n}\n"
  },
  {
    "path": "content-src/asrouter/components/ModalOverlay/_ModalOverlay.scss",
    "content": ".activity-stream {\n  &.modal-open {\n    overflow: hidden;\n  }\n}\n\n.modalOverlayOuter {\n  background: var(--newtab-overlay-color);\n  height: 100%;\n  position: fixed;\n  top: 0;\n  left: 0;\n  width: 100%;\n  display: none;\n  z-index: 1100;\n\n  &.active {\n    display: block;\n  }\n}\n\n.modal-scroll {\n  position: absolute;\n  width: 100%;\n  height: 100%;\n  overflow: auto;\n}\n\n.modal-height {\n  // \"Welcome header\" has 40px of padding and 36px font size that get neglected using position absolute\n  // causing this to visually collide with the newtab searchbar\n  padding-top: 80px;\n}\n\n.modalOverlayInner {\n  width: 960px;\n  position: fixed;\n  top: 15%;\n  left: calc(50% - 480px); // halfway across minus half the width of the modal\n  background: var(--newtab-modal-color);\n  box-shadow: 0 1px 15px 0 $black-30;\n  border-radius: 4px;\n  display: none;\n  z-index: 1101;\n\n  // modal takes over entire screen\n  @media(max-width: 960px) {\n    width: 100%;\n    height: 100%;\n    top: 0;\n    left: 0;\n    box-shadow: none;\n    border-radius: 0;\n  }\n\n  // if modal is short enough, reduce the top margin\n  @media(max-height: 730px) {\n    top: 5%;\n  }\n\n  &.active {\n    display: block;\n  }\n\n  .icon-dismiss {\n    border: 0;\n    cursor: pointer;\n    inset-inline-end: 0;\n    padding: 20px;\n    fill: $white;\n    position: absolute;\n\n    &:focus {\n      border: 1px dotted;\n    }\n  }\n\n  h2 {\n    color: $grey-60;\n    text-align: center;\n    font-weight: 200;\n    margin-top: 30px;\n    font-size: 28px;\n    line-height: 37px;\n    letter-spacing: -0.13px;\n\n    @media(max-width: 960px) {\n      margin-top: 100px;\n    }\n\n    @media(max-width: 850px) {\n      margin-top: 30px;\n    }\n  }\n\n  .footer {\n    border-top: 1px solid $grey-30;\n    border-radius: 4px;\n    height: 70px;\n    width: 100%;\n    position: absolute;\n    bottom: 0;\n    text-align: center;\n    background-color: $white;\n\n    // if modal is short enough, footer becomes sticky\n    @media(max-width: 850px) and (max-height: 730px) {\n      position: sticky;\n    }\n\n    // if modal is narrow enough, footer becomes sticky\n    @media(max-width: 650px) and (max-height: 600px) {\n      position: sticky;\n    }\n\n    .modalButton {\n      margin-top: 20px;\n      min-width: 150px;\n      height: 30px;\n      padding: 4px 30px 6px;\n      font-size: 15px;\n\n      &:focus,\n      &.active,\n      &:hover {\n        box-shadow: 0 0 0 5px $grey-30;\n        transition: box-shadow 150ms;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "content-src/asrouter/components/RichText/RichText.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport { Localized } from \"fluent-react\";\nimport React from \"react\";\nimport { RICH_TEXT_KEYS } from \"../../rich-text-strings\";\nimport { safeURI } from \"../../template-utils\";\n\n// Elements allowed in snippet content\nconst ALLOWED_TAGS = {\n  b: <b />,\n  i: <i />,\n  u: <u />,\n  strong: <strong />,\n  em: <em />,\n  br: <br />,\n};\n\n/**\n * Transform an object (tag name: {url}) into (tag name: anchor) where the url\n * is used as href, in order to render links inside a Fluent.Localized component.\n */\nexport function convertLinks(\n  links,\n  sendClick,\n  doNotAutoBlock,\n  openNewWindow = false\n) {\n  if (links) {\n    return Object.keys(links).reduce((acc, linkTag) => {\n      const { action } = links[linkTag];\n      // Setting the value to false will not include the attribute in the anchor\n      const url = action ? false : safeURI(links[linkTag].url);\n\n      acc[linkTag] = (\n        // eslint was getting a false positive caused by the dynamic injection\n        // of content.\n        // eslint-disable-next-line jsx-a11y/anchor-has-content\n        <a\n          href={url}\n          target={openNewWindow ? \"_blank\" : \"\"}\n          data-metric={links[linkTag].metric}\n          data-action={action}\n          data-args={links[linkTag].args}\n          data-do_not_autoblock={doNotAutoBlock}\n          onClick={sendClick}\n        />\n      );\n      return acc;\n    }, {});\n  }\n\n  return null;\n}\n\n/**\n * Message wrapper used to sanitize markup and render HTML.\n */\nexport function RichText(props) {\n  if (!RICH_TEXT_KEYS.includes(props.localization_id)) {\n    throw new Error(\n      `ASRouter: ${\n        props.localization_id\n      } is not a valid rich text property. If you want it to be processed, you need to add it to asrouter/rich-text-strings.js`\n    );\n  }\n  return (\n    <Localized\n      id={props.localization_id}\n      {...ALLOWED_TAGS}\n      {...props.customElements}\n      {...convertLinks(\n        props.links,\n        props.sendClick,\n        props.doNotAutoBlock,\n        props.openNewWindow\n      )}\n    >\n      <span>{props.text}</span>\n    </Localized>\n  );\n}\n"
  },
  {
    "path": "content-src/asrouter/components/SnippetBase/SnippetBase.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport React from \"react\";\nimport schema from \"../../templates/SimpleSnippet/SimpleSnippet.schema.json\";\n\nexport class SnippetBase extends React.PureComponent {\n  constructor(props) {\n    super(props);\n    this.onBlockClicked = this.onBlockClicked.bind(this);\n    this.onDismissClicked = this.onDismissClicked.bind(this);\n    this.setBlockButtonRef = this.setBlockButtonRef.bind(this);\n    this.onBlockButtonMouseEnter = this.onBlockButtonMouseEnter.bind(this);\n    this.onBlockButtonMouseLeave = this.onBlockButtonMouseLeave.bind(this);\n    this.state = { blockButtonHover: false };\n  }\n\n  componentDidMount() {\n    if (this.blockButtonRef) {\n      this.blockButtonRef.addEventListener(\n        \"mouseenter\",\n        this.onBlockButtonMouseEnter\n      );\n      this.blockButtonRef.addEventListener(\n        \"mouseleave\",\n        this.onBlockButtonMouseLeave\n      );\n    }\n  }\n\n  componentWillUnmount() {\n    if (this.blockButtonRef) {\n      this.blockButtonRef.removeEventListener(\n        \"mouseenter\",\n        this.onBlockButtonMouseEnter\n      );\n      this.blockButtonRef.removeEventListener(\n        \"mouseleave\",\n        this.onBlockButtonMouseLeave\n      );\n    }\n  }\n\n  setBlockButtonRef(element) {\n    this.blockButtonRef = element;\n  }\n\n  onBlockButtonMouseEnter() {\n    this.setState({ blockButtonHover: true });\n  }\n\n  onBlockButtonMouseLeave() {\n    this.setState({ blockButtonHover: false });\n  }\n\n  onBlockClicked() {\n    if (this.props.provider !== \"preview\") {\n      this.props.sendUserActionTelemetry({\n        event: \"BLOCK\",\n        id: this.props.UISurface,\n      });\n    }\n\n    this.props.onBlock();\n  }\n\n  onDismissClicked() {\n    if (this.props.provider !== \"preview\") {\n      this.props.sendUserActionTelemetry({\n        event: \"DISMISS\",\n        id: this.props.UISurface,\n      });\n    }\n\n    this.props.onDismiss();\n  }\n\n  renderDismissButton() {\n    if (this.props.footerDismiss) {\n      return (\n        <div className=\"footer\">\n          <div className=\"footer-content\">\n            <button\n              className=\"ASRouterButton secondary\"\n              onClick={this.onDismissClicked}\n            >\n              {this.props.content.scene2_dismiss_button_text}\n            </button>\n          </div>\n        </div>\n      );\n    }\n\n    const label =\n      this.props.content.block_button_text ||\n      schema.properties.block_button_text.default;\n    return (\n      <button\n        className=\"blockButton\"\n        title={label}\n        aria-label={label}\n        onClick={this.onBlockClicked}\n        ref={this.setBlockButtonRef}\n      />\n    );\n  }\n\n  render() {\n    const { props } = this;\n    const { blockButtonHover } = this.state;\n\n    const containerClassName = `SnippetBaseContainer${\n      props.className ? ` ${props.className}` : \"\"\n    }${blockButtonHover ? \" active\" : \"\"}`;\n\n    return (\n      <div className={containerClassName} style={this.props.textStyle}>\n        <div className=\"innerWrapper\">{props.children}</div>\n        {this.renderDismissButton()}\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "content-src/asrouter/components/SnippetBase/_SnippetBase.scss",
    "content": ".SnippetBaseContainer {\n  position: fixed;\n  z-index: 2;\n  bottom: 0;\n  left: 0;\n  right: 0;\n  background-color: var(--newtab-snippets-background-color);\n  color: var(--newtab-text-primary-color);\n  font-size: 14px;\n  line-height: 20px;\n  border-top: 1px solid var(--newtab-snippets-hairline-color);\n  box-shadow: $shadow-secondary;\n  display: flex;\n  align-items: center;\n\n  a {\n    cursor: pointer;\n    color: var(--newtab-link-primary-color);\n\n    &:hover {\n      text-decoration: underline;\n    }\n\n    [lwt-newtab-brighttext] & {\n      font-weight: bold;\n    }\n  }\n\n  input {\n    &[type='checkbox'] {\n      margin-inline-start: 0;\n    }\n  }\n\n  .innerWrapper {\n    margin: 0 auto;\n    display: flex;\n    align-items: center;\n    padding: 12px $section-horizontal-padding;\n\n    // This is to account for the block button on smaller screens\n    padding-inline-end: 36px;\n    @media (min-width: $break-point-large) {\n      padding-inline-end: $section-horizontal-padding;\n    }\n\n    max-width: $wrapper-max-width-large + ($section-horizontal-padding * 2);\n    @media (min-width: $break-point-widest) {\n      max-width: $wrapper-max-width-widest + ($section-horizontal-padding * 2);\n    }\n  }\n\n  .blockButton {\n    display: none;\n    background: none;\n    border: 0;\n    position: absolute;\n    top: 50%;\n    inset-inline-end: 12px;\n    height: 16px;\n    width: 16px;\n    background-image: url('resource://activity-stream/data/content/assets/glyph-dismiss-16.svg');\n    -moz-context-properties: fill;\n    fill: var(--newtab-icon-primary-color);\n    opacity: 0.5;\n    margin-top: -8px;\n    padding: 0;\n    cursor: pointer;\n\n    @media (min-width: 766px) {\n      inset-inline-end: 24px;\n    }\n  }\n\n  &:hover .blockButton {\n    display: block;\n  }\n\n  .icon {\n    height: 42px;\n    width: 42px;\n    margin-inline-end: 12px;\n    flex-shrink: 0;\n  }\n}\n\n.snippets-preview-banner {\n  font-size: 15px;\n  line-height: 42px;\n  color: $grey-60-70;\n  background: $grey-30-60;\n  text-align: center;\n  position: absolute;\n  top: 0;\n  width: 100%;\n\n  span {\n    vertical-align: middle;\n  }\n}\n\n// We show snippet icons for both themes and conditionally hide\n// based on which theme is currently active\nbody {\n  &:not([lwt-newtab-brighttext]) {\n    .icon-dark-theme,\n    .icon.icon-dark-theme,\n    .scene2Icon .icon-dark-theme {\n      display: none;\n    }\n  }\n\n  &[lwt-newtab-brighttext] {\n    .icon-light-theme,\n    .icon.icon-light-theme,\n    .scene2Icon .icon-light-theme {\n      display: none;\n    }\n  }\n}\n"
  },
  {
    "path": "content-src/asrouter/docs/debugging-docs.md",
    "content": "# Using ASRouter Devtools\n\n## How to enable ASRouter devtools\n- In `about:config`, set `browser.newtabpage.activity-stream.asrouter.devtoolsEnabled` to `true`\n- Visit `about:newtab#asrouter` to see the devtools.\n\n## Overview of ASRouter devtools\n\n![Devtools image](./debugging-guide.png)\n\n## How to enable/disable a provider\n\nTo enable a provider such as `snippets`, Look at the list of \"Message Providers\" at the top of the page. Make sure the checkbox is checked next to the provider you want to enable.\n\nTo disable it, uncheck the checkbox. You should see a red label indicating the provider is now disabled.\n\n## How to see all messages from a provider\n\n(Only available in Firefox 65+)\n\nIn order to see all active messages for a current provider such as `snippets`, use the drop down selector under the \"Messages\" section. Select the name of the provider you are interested in.\n\nThe messages on the page should now be filtered to include only the provider you selected.\n\n## How to test telemetry pings\n\nTo test telemetry pings, complete the the following steps:\n\n- In about:config, set:\n  - `browser.newtabpage.activity-stream.telemetry` to `true`\n  - `browser.ping-centre.log` to `true`\n- Open the Browser Toolbox devtools (Tools > Web Developer > Browser Toolbox) and switch to the console tab. Add a filter for for `activity-stream` to only display relevant pings:\n\n![Devtools telemetry ping](./telemetry-screenshot.png)\n\nYou should now see pings show up as you view/interact with ASR messages/templates.\n\n## Snippets debugging\n\n### How to view preview URLs\n\nFollow these steps to view preview URLs (e.g. `about:newtab?endpoint=https://gist.githubusercontent.com/piatra/d193ca7e0f513cc19fc6a1d396c214f7/raw/8bcaf9548212e4c613577e839198cc14e7317630/newsletter_snippet.json&theme=dark`)\n\nYou can preview in the two different themes (light and dark) by adding `&theme=dark` or `&theme=light` at the end of the url.\n\n#### IMPORTANT NOTES\n- Links to URLs starting with `about:newtab` cannot be clicked on directly. They must be copy and pasted into the address bar.\n- Previews should only be tested in `Firefox 64` and later.\n- The endpoint must be HTTPS, the host must be whitelisted (see testing instructions below)\n- Errors are surfaced in the `Console` tab of the `Browser Toolbox`\n\n#### Testing instructions\n- If your endpoint URL has a host name of `snippets-admin.mozilla.org`, you can paste the URL into the address bar view it without any further steps.\n- If your endpoint URL  starts with some other host name, it must be **whitelisted**. Open the Browser Toolbox devtools (Tools > Developer > Browser Toolbox) and paste the following code (where `gist.githubusercontent.com` is the hostname of your endpoint URL):\n```js\nServices.prefs.setStringPref(\n  \"browser.newtab.activity-stream.asrouter.whitelistHosts\",\n  \"[\\\"gist.githubusercontent.com\\\"]\"\n);\n```\n- Restart the browser\n- You should now be able to paste the URL into the address bar and view it.\n"
  },
  {
    "path": "content-src/asrouter/docs/experiment-guide.md",
    "content": "# How to run experiments with ASRouter\n\nThis guide will tell you how to run an experiment with ASRouter messages.\nNote that the actual experiment process and infrastructure is handled by\nthe experiments team (#ask-experimenter).\n\n## Why run an experiment\n\n* To measure the effect of a message on a Firefox metric (e.g. retention)\n* To test a potentially risky message on a smaller group of users\n* To compare the performance of multiple variants of messages in a controlled way\n\n## Choose cohort IDs and request an experiment\n\nFirst you should decide on a cohort ID (this can be any arbitrary unique string) for each\nindividual group you need to segment for your experiment.\n\nFor example, if I want to test two variants of an FXA Snippet, I might have two cohort IDs,\n`FXA_SNIPPET_V1` and `FXA_SNIPPET_V2`.\n\nYou will then [request](https://experimenter.services.mozilla.com/) a new \"pref-flip\" study with the Firefox Experiments team.\nThe preferences you will submit will be based on the cohort IDs you chose.\n\nFor the FXA Snippet example, your preference name would be `browser.newtabpage.activity-stream.asrouter.providers.snippets` and values would be:\n\nControl (default value)\n```json\n{\"id\":\"snippets\",\"enabled\":true,\"type\":\"remote\",\"url\":\"https://snippets.cdn.mozilla.net/%STARTPAGE_VERSION%/%NAME%/%VERSION%/%APPBUILDID%/%BUILD_TARGET%/%LOCALE%/release/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/\",\"updateCycleInMs\":14400000}\n```\n\nVariant 1:\n```json\n{\"id\":\"snippets\", \"cohort\": \"FXA_SNIPPET_V1\", \"enabled\":true,\"type\":\"remote\",\"url\":\"https://snippets.cdn.mozilla.net/%STARTPAGE_VERSION%/%NAME%/%VERSION%/%APPBUILDID%/%BUILD_TARGET%/%LOCALE%/release/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/\",\"updateCycleInMs\":14400000}\n```\n\nVariant 2:\n```json\n{\"id\":\"snippets\", \"cohort\": \"FXA_SNIPPET_V1\", \"enabled\":true,\"type\":\"remote\",\"url\":\"https://snippets.cdn.mozilla.net/%STARTPAGE_VERSION%/%NAME%/%VERSION%/%APPBUILDID%/%BUILD_TARGET%/%LOCALE%/release/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/\",\"updateCycleInMs\":14400000}\n```\n\n## Add targeting to your messages\n\nYou must now check for the cohort ID in the `targeting` expression of the messages you want to include in your experiments.\n\nFor the previous example, you wold include the following to target the first cohort:\n\n```json\n{\n  \"targeting\": \"providerCohorts.snippets == \\\"FXA_SNIPPET_V1\\\"\"\n}\n\n```\n"
  },
  {
    "path": "content-src/asrouter/docs/targeting-attributes.md",
    "content": "# Targeting attributes\n\nWhen you create ASRouter messages such as snippets, contextual feature recommendations, or onboarding cards, you may choose to include **targeting information** with those messages.\n\nTargeting information must be captured in [an expression](./targeting-guide.md) that has access to the following attributes. You may combine and compare any of these attributes as needed.\n\nPlease note that some targeting attributes require stricter controls on the telemetry than can be colleted, so when in doubt, ask for review.\n\n## Available attributes\n\n* [addonsInfo](#addonsinfo)\n* [attributionData](#attributiondata)\n* [browserSettings](#browsersettings)\n* [currentDate](#currentdate)\n* [devToolsOpenedCount](#devtoolsopenedcount)\n* [isDefaultBrowser](#isdefaultbrowser)\n* [firefoxVersion](#firefoxversion)\n* [locale](#locale)\n* [localeLanguageCode](#localelanguagecode)\n* [needsUpdate](#needsupdate)\n* [pinnedSites](#pinnedsites)\n* [previousSessionEnd](#previoussessionend)\n* [profileAgeCreated](#profileagecreated)\n* [profileAgeReset](#profileagereset)\n* [providerCohorts](#providercohorts)\n* [region](#region)\n* [searchEngines](#searchengines)\n* [sync](#sync)\n* [topFrecentSites](#topfrecentsites)\n* [totalBookmarksCount](#totalbookmarkscount)\n* [trailheadInterrupt](#trailheadinterrupt)\n* [trailheadTriplet](#trailheadtriplet)\n* [usesFirefoxSync](#usesfirefoxsync)\n* [isFxAEnabled](#isFxAEnabled)\n* [xpinstallEnabled](#xpinstallEnabled)\n* [hasPinnedTabs](#haspinnedtabs)\n* [hasAccessedFxAPanel](#hasaccessedfxapanel)\n* [isWhatsNewPanelEnabled](#iswhatsnewpanelenabled)\n* [isFxABadgeEnabled](#isfxabadgeenabled)\n* [totalBlockedCount](#totalblockedcount)\n* [recentBookmarks](#recentbookmarks)\n* [userPrefs](#userprefs)\n* [attachedFxAOAuthClients](#attachedfxaoauthclients)\n* [platformName](#platformname)\n* [scores](#scores)\n* [scoreThreshold](#scorethreshold)\n* [messageImpressions](#messageimpressions)\n* [blockedCountByType](#blockedcountbytype)\n\n## Detailed usage\n\n### `addonsInfo`\nProvides information about the add-ons the user has installed.\n\nNote that the `name`, `userDisabled`, and `installDate` is only available if `isFullData` is `true` (this is usually not the case right at start-up).\n\n**Due to an existing bug, `userDisabled` is not currently available**\n\n#### Examples\n* Has the user installed the unicorn addon?\n```java\naddonsInfo.addons[\"unicornaddon@mozilla.org\"]\n```\n\n* Has the user installed and disabled the unicorn addon?\n```java\naddonsInfo.isFullData && addonsInfo.addons[\"unicornaddon@mozilla.org\"].userDisabled\n```\n\n#### Definition\n```ts\ndeclare const addonsInfo: Promise<AddonsInfoResponse>;\ninterface AddonsInfoResponse {\n  // Does this include extra information requiring I/O?\n  isFullData: boolean;\n  // addonId should be something like activity-stream@mozilla.org\n  [addonId: string]: {\n    // Version of the add-on\n    version: string;\n    // (string) e.g. \"extension\"\n    type: AddonType;\n    // Version of the add-on\n    isSystem: boolean;\n    // Is the add-on a webextension?\n    isWebExtension: boolean;\n    // The name of the add-on\n    name: string;\n    // Is the add-on disabled?\n    // CURRENTLY UNAVAILABLE due to an outstanding bug\n    userDisabled: boolean;\n    // When was it installed? e.g. \"2018-03-10T03:41:06.000Z\"\n    installDate: string;\n  };\n}\n```\n### `attributionData`\n\nAn object containing information on exactly how Firefox was downloaded\n\n#### Examples\n* Was the browser installed via the `\"back_to_school\"` campaign?\n```java\nattributionData && attributionData.campaign == \"back_to_school\"\n```\n\n#### Definition\n```ts\ndeclare const attributionData: AttributionCode;\ninterface AttributionCode {\n  // Descriptor for where the download started from\n  campaign: string,\n  // A source, like addons.mozilla.org, or google.com\n  source: string,\n  // The medium for the download, like if this was referral\n  medium: string,\n  // Additional content, like an addonID for instance\n  content: string\n}\n```\n\n### `browserSettings`\n\nIncludes two properties:\n* `attribution`, which indicates how Firefox was downloaded - DEPRECATED - please use [attributionData](#attributiondata)\n* `update`, which has information about how Firefox updates\n\nNote that attribution can be `undefined`, so you should check that it exists first.\n\n#### Examples\n* Is updating enabled?\n```java\nbrowserSettings.update.enabled\n```\n\n#### Definition\n\n```ts\ndeclare const browserSettings: {\n  attribution: undefined | {\n    // Referring partner domain, when install happens via a known partner\n    // e.g. google.com\n    source: string;\n    // category of the source, such as \"organic\" for a search engine\n    // e.g. organic\n    medium: string;\n    // identifier of the particular campaign that led to the download of the product\n    // e.g. back_to_school\n    campaign: string;\n    // identifier to indicate the particular link within a campaign\n    // e.g. https://mozilla.org/some-page\n    content: string;\n  },\n  update: {\n    // Is auto-downloading enabled?\n    autoDownload: boolean;\n    // What release channel, e.g. \"nightly\"\n    channel: string;\n    // Is updating enabled?\n    enabled: boolean;\n  }\n}\n```\n\n### `currentDate`\n\nThe current date at the moment message targeting is checked.\n\n#### Examples\n* Is the current date after Oct 3, 2018?\n```java\ncurrentDate > \"Wed Oct 03 2018 00:00:00\"|date\n```\n\n#### Definition\n\n```ts\ndeclare const currentDate; ECMA262DateString;\n// ECMA262DateString = Date.toString()\ntype ECMA262DateString = string;\n```\n\n### `devToolsOpenedCount`\nNumber of usages of the web console.\n\n#### Examples\n* Has the user opened the web console more than 10 times?\n```java\ndevToolsOpenedCount > 10\n```\n\n#### Definition\n```ts\ndeclare const devToolsOpenedCount: number;\n```\n\n### `isDefaultBrowser`\n\nIs Firefox the user's default browser?\n\n#### Definition\n\n```ts\ndeclare const isDefaultBrowser: boolean;\n```\n\n### `firefoxVersion`\n\nThe major Firefox version of the browser\n\n#### Examples\n* Is the version of the browser greater than 63?\n```java\nfirefoxVersion > 63\n```\n\n#### Definition\n\n```ts\ndeclare const firefoxVersion: number;\n```\n\n### `locale`\nThe current locale of the browser including country code, e.g. `en-US`.\n\n#### Examples\n* Is the locale of the browser either English (US) or German (Germany)?\n```java\nlocale in [\"en-US\", \"de-DE\"]\n```\n\n#### Definition\n```ts\ndeclare const locale: string;\n```\n\n### `localeLanguageCode`\nThe current locale of the browser NOT including country code, e.g. `en`.\nThis is useful for matching all countries of a particular language.\n\n#### Examples\n* Is the locale of the browser any English locale?\n```java\nlocaleLanguageCode == \"en\"\n```\n\n#### Definition\n```ts\ndeclare const localeLanguageCode: string;\n```\n\n### `needsUpdate`\n\nDoes the client have the latest available version installed\n\n```ts\ndeclare const needsUpdate: boolean;\n```\n\n### `pinnedSites`\nThe sites (including search shortcuts) that are pinned on a user's new tab page.\n\n#### Examples\n* Has the user pinned any site on `foo.com`?\n```java\n\"foo.com\" in pinnedSites|mapToProperty(\"host\")\n```\n\n* Does the user have a pinned `duckduckgo.com` search shortcut?\n```java\n\"duckduckgo.com\" in pinnedSites[.searchTopSite == true]|mapToProperty(\"host\")\n```\n\n#### Definition\n```ts\ninterface PinnedSite {\n  // e.g. https://foo.mozilla.com/foo/bar\n  url: string;\n  // e.g. foo.mozilla.com\n  host: string;\n  // is the pin a search shortcut?\n  searchTopSite: boolean;\n}\ndeclare const pinnedSites: Array<PinnedSite>\n```\n\n### `previousSessionEnd`\n\nTimestamp of the previously closed session.\n\n#### Definition\n```ts\ndeclare const previousSessionEnd: UnixEpochNumber;\n// UnixEpochNumber is UNIX Epoch timestamp, e.g. 1522843725924\ntype UnixEpochNumber = number;\n```\n\n### `profileAgeCreated`\n\nThe date the profile was created as a UNIX Epoch timestamp.\n\n#### Definition\n\n```ts\ndeclare const profileAgeCreated: UnixEpochNumber;\n// UnixEpochNumber is UNIX Epoch timestamp, e.g. 1522843725924\ntype UnixEpochNumber = number;\n```\n\n### `profileAgeReset`\n\nThe date the profile was reset as a UNIX Epoch timestamp (if it was reset).\n\n#### Examples\n* Was the profile never reset?\n```java\n!profileAgeReset\n```\n\n#### Definition\n```ts\n// profileAgeReset can be undefined if the profile was never reset\n// UnixEpochNumber is number, e.g. 1522843725924\ndeclare const profileAgeReset: undefined | UnixEpochNumber;\n// UnixEpochNumber is UNIX Epoch timestamp, e.g. 1522843725924\ntype UnixEpochNumber = number;\n```\n\n### `providerCohorts`\n\nInformation about cohort settings (from prefs, including shield studies) for each provider.\n\n#### Examples\n* Is the user in the \"foo_test\" cohort for snippets?\n```java\nproviderCohorts.snippets == \"foo_test\"\n```\n\n#### Definition\n\n```ts\ndeclare const providerCohorts: {\n  [providerId: string]: string;\n}\n```\n\n### `region`\n\nCountry code retrieved from `location.services.mozilla.com`. Can be `\"\"` if request did not finish or encountered an error.\n\n#### Examples\n* Is the user in Canada?\n```java\nregion == \"CA\"\n```\n\n#### Definition\n\n```ts\ndeclare const region: string;\n```\n\n### `searchEngines`\n\nInformation about the current and available search engines.\n\n#### Examples\n* Is the current default search engine set to google?\n```java\nsearchEngines.current == \"google\"\n```\n\n#### Definition\n\n```ts\ndeclare const searchEngines: Promise<SearchEnginesResponse>;\ninterface SearchEnginesResponse: {\n  current: SearchEngineId;\n  installed: Array<SearchEngineId>;\n}\n// This is an identifier for a search engine such as \"google\" or \"amazondotcom\"\ntype SearchEngineId = string;\n```\n\n### `sync`\n\nInformation about synced devices.\n\n#### Examples\n* Is at least 1 mobile device synced to this profile?\n```java\nsync.mobileDevices > 0\n```\n\n#### Definition\n\n```ts\ndeclare const sync: {\n  desktopDevices: number;\n  mobileDevices: number;\n  totalDevices: number;\n}\n```\n\n### `topFrecentSites`\n\nInformation about the browser's top 25 frecent sites.\n\n**Please note this is a restricted targeting property that influences what telemetry is allowed to be collected may not be used without review**\n\n\n#### Examples\n* Is mozilla.com in the user's top frecent sites with a frececy greater than 400?\n```java\n\"mozilla.com\" in topFrecentSites[.frecency >= 400]|mapToProperty(\"host\")\n```\n\n#### Definition\n```ts\ndeclare const topFrecentSites: Promise<Array<TopSite>>\ninterface TopSite {\n  // e.g. https://foo.mozilla.com/foo/bar\n  url: string;\n  // e.g. foo.mozilla.com\n  host: string;\n  frecency: number;\n  lastVisitDate: UnixEpochNumber;\n}\n// UnixEpochNumber is UNIX Epoch timestamp, e.g. 1522843725924\ntype UnixEpochNumber = number;\n```\n\n### `totalBookmarksCount`\n\nTotal number of bookmarks.\n\n#### Definition\n\n```ts\ndeclare const totalBookmarksCount: number;\n```\n\n### `trailheadInterrupt`\n\n(67.05+ only) Experiment branch for \"interrupt\" study\n\n### `trailheadTriplet`\n\n(67.05+ only) Experiment branch for \"triplet\" study\n\n### `usesFirefoxSync`\n\nDoes the user use Firefox sync?\n\n#### Definition\n\n```ts\ndeclare const usesFirefoxSync: boolean;\n```\n\n### `isFxAEnabled`\n\nDoes the user have Firefox sync enabled? The service could potentially be turned off [for enterprise builds](https://searchfox.org/mozilla-central/rev/b59a99943de4dd314bae4e44ab43ce7687ccbbec/browser/components/enterprisepolicies/Policies.jsm#327).\n\n#### Definition\n\n```ts\ndeclare const isFxAEnabled: boolean;\n```\n\n### `xpinstallEnabled`\n\nPref used by system administrators to disallow add-ons from installed altogether.\n\n#### Definition\n\n```ts\ndeclare const xpinstallEnabled: boolean;\n```\n\n### `hasPinnedTabs`\n\nDoes the user have any pinned tabs in any windows.\n\n#### Definition\n\n```ts\ndeclare const hasPinnedTabs: boolean;\n```\n\n### `hasAccessedFxAPanel`\n\nBoolean pref that gets set the first time the user opens the FxA toolbar panel\n\n#### Definition\n\n```ts\ndeclare const hasAccessedFxAPanel: boolean;\n```\n\n### `isWhatsNewPanelEnabled`\n\nBoolean pref that controls if the What's New panel feature is enabled\n\n#### Definition\n\n```ts\ndeclare const isWhatsNewPanelEnabled: boolean;\n```\n\n### `isFxABadgeEnabled`\n\nBoolean pref that controls if the FxA toolbar button is badged by Messaging System.\n\n#### Definition\n\n```ts\ndeclare const isFxABadgeEnabled: boolean;\n```\n\n### `totalBlockedCount`\n\nTotal number of events from the content blocking database\n\n#### Definition\n\n```ts\ndeclare const totalBlockedCount: number;\n```\n\n### `recentBookmarks`\n\nAn array of GUIDs of recent bookmarks as provided by [`NewTabUtils.getRecentBookmarks`](https://searchfox.org/mozilla-central/rev/e0b0c38ee83f99d3cf868bad525ace4a395039f1/toolkit/modules/NewTabUtils.jsm#1087)\n\n#### Definition\n\n```ts\ninterface Bookmark {\n  bookmarkGuid: string;\n  url: string;\n  title: string;\n  ...\n}\ndeclare const recentBookmarks: Array<Bookmark>\n```\n\n### `userPrefs`\n\nInformation about user facing prefs configurable from `about:preferences`.\n\n#### Examples\n```java\nuserPrefs.cfrFeatures == false\n```\n\n#### Definition\n\n```ts\ndeclare const userPrefs: {\n  cfrFeatures: boolean;\n  cfrAddons: boolean;\n  snippets: boolean;\n}\n```\n\n### `attachedFxAOAuthClients`\n\nInformation about connected services associated with the FxA Account.\nReturn an empty array if no account is found or an error occurs.\n\n#### Definition\n\n```\ninterface OAuthClient {\n  // OAuth client_id of the service\n  // https://docs.telemetry.mozilla.org/datasets/fxa_metrics/attribution.html#service-attribution\n  id: string;\n  lastAccessedDaysAgo: number;\n}\n\ndeclare const attachedFxAOAuthClients: Promise<OAuthClient[]>\n```\n\n#### Examples\n```javascript\n{\n  id: \"7377719276ad44ee\",\n  name: \"Pocket\",\n  lastAccessTime: 1513599164000\n}\n```\n\n### `platformName`\n\n[Platform information](https://searchfox.org/mozilla-central/rev/05a22d864814cb1e4352faa4004e1f975c7d2eb9/toolkit/modules/AppConstants.jsm#156).\n\n#### Definition\n\n```\ndeclare const platformName = \"linux\" | \"win\" | \"macosx\" | \"android\" | \"other\";\n```\n\n### `scores`\n\n#### Definition\n\nSee more in [CFR Machine Learning Experiment](https://bugzilla.mozilla.org/show_bug.cgi?id=1594422).\n\n```\ndeclare const scores = { [cfrId: string]: number (integer); }\n```\n\n### `scoreThreshold`\n\n#### Definition\n\nSee more in [CFR Machine Learning Experiment](https://bugzilla.mozilla.org/show_bug.cgi?id=1594422).\n\n```\ndeclare const scoreThreshold = integer;\n```\n\n### `messageImpressions`\n\nDictionary that maps message ids to impression timestamps. Timestamps are stored in\nconsecutive order. Can be used to detect first impression of a message, number of\nimpressions. Can be used in targeting to show a message if another message has been\nseen.\nImpressions are used for frequency capping so we only store them if the message has\n`frequency` configured.\nImpressions for badges might not work as expected: we add a badge for every opened\nwindow so the number of impressions stored might be higher than expected. Additionally\nnot all badges have `frequency` cap so `messageImpressions` might not be defined.\nBadge impressions should not be used for targeting.\n\n#### Definition\n\n```\ndeclare const messageImpressions: { [key: string]: Array<UnixEpochNumber> };\n```\n\n### `blockedCountByType`\n\nReturns a breakdown by category of all blocked resources in the past 42 days.\n\n#### Definition\n\n```\ndeclare const messageImpressions: { [key: string]: number };\n```\n\n#### Examples\n\n```javascript\nObject {\n  trackerCount: 0,\n  cookieCount: 34,\n  cryptominerCount: 0,\n  fingerprinterCount: 3,\n  socialCount: 2\n}\n```\n"
  },
  {
    "path": "content-src/asrouter/docs/targeting-guide.md",
    "content": "# Guide to targeting with JEXL\n\nFor a more in-depth explanation of JEXL syntax you can read the [Normady project docs](https://mozilla.github.io/normandy/user/filters.html?highlight=jexl).\n\n### How to write JEXL targeting expressions\nA message needs to contain the `targeting` property (JEXL string) which is evaluated against the provided attributes.\nExamples:\n\n```javascript\n{\n  \"id\": \"7864\",\n  \"content\": {...},\n  // simple equality check\n  \"targeting\": \"usesFirefoxSync == true\"\n}\n\n{\n  \"id\": \"7865\",\n  \"content\": {...},\n  // using JEXL transforms and combining two attributes\n  \"targeting\": \"usesFirefoxSync == true && profileAgeCreated > '2018-01-07'|date\"\n}\n\n{\n  \"id\": \"7866\",\n  \"content\": {...},\n  // targeting addon information\n  \"targeting\": \"addonsInfo.addons['activity-stream@mozilla.org'].name == 'Activity Stream'\"\n}\n\n{\n  \"id\": \"7866\",\n  \"content\": {...},\n  // targeting based on time\n  \"targeting\": \"currentDate > '2018-08-08'|date\"\n}\n```\n"
  },
  {
    "path": "content-src/asrouter/docs/user-actions.md",
    "content": "# User Actions\n\nA subset of actions are available to messages via fields like `button_action` for snippets, or `primary_action` for CFRs.\n\n## Usage\n\nFor snippets, you should add the action type in `button_action` and any additional parameters in `button_action_args. For example:\n\n```json\n{\n  \"button_action\": \"OPEN_ABOUT_PAGE\",\n  \"button_action_args\": \"config\"\n}\n```\n\n## Available Actions\n\n### `OPEN_APPLICATIONS_MENU`\n\n* args: (none)\n\nOpens the applications menu.\n\n### `OPEN_PRIVATE_BROWSER_WINDOW`\n\n* args: (none)\n\nOpens a new private browsing window.\n\n\n### `OPEN_URL`\n\n* args: `string` (a url)\n\nOpens a given url.\n\nExample:\n\n```json\n{\n  \"button_action\": \"OPEN_URL\",\n  \"button_action_args\": \"https://foo.com\"\n}\n```\n\n### `OPEN_ABOUT_PAGE`\n\n* args: `string` (a valid about page without the `about:` prefix)\n\nOpens a given about page\n\nExample:\n\n```json\n{\n  \"button_action\": \"OPEN_ABOUT_PAGE\",\n  \"button_action_args\": \"config\"\n}\n```\n\n### `OPEN_PREFERENCES_PAGE`\n\n* args: `string` (a category accessible via a `#`)\n\nOpens `about:preferences` with an optional category accessible via a `#` in the URL (e.g. `about:preferences#home`).\n\nExample:\n\n```json\n{\n  \"button_action\": \"OPEN_PREFERENCES_PAGE\",\n  \"button_action_args\": \"home\"\n}\n```\n\n### `SHOW_FIREFOX_ACCOUNTS`\n\n* args: (none)\n\nOpens Firefox accounts sign-up page. Encodes some information that the origin was from snippets by default.\n\n### `SHOW_MIGRATION_WIZARD`\n\n* args: (none)\n\nOpens import wizard to bring in settings and data from another browser.\n\n### `PIN_CURRENT_TAB`\n\n* args: (none)\n\nPins the currently focused tab.\n\n### `ENABLE_FIREFOX_MONITOR`\n\n* args:\n```ts\n{\n  url: string;\n  flowRequestParams: {\n    entrypoint: string;\n    utm_term: string;\n    form_type: string;\n  }\n}\n```\n\nOpens an oauth flow to enable Firefox Monitor at a given `url` and adds Firefox metrics that user given a set of `flowRequestParams`.\n\n### `url`\n\nThe URL should start with `https://monitor.firefox.com/oauth/init` and add various metrics tags as search params, including:\n\n* `utm_source`\n* `utm_campaign`\n* `form_type`\n* `entrypoint`\n\nYou should verify the values of these search params with whoever is doing the data analysis (e.g. Leif Oines).\n\n### `flowRequestParams`\n\nThese params are used by Firefox to add information specific to that individual user to the final oauth URL. You should include:\n\n* `entrypoint`\n* `utm_term`\n* `form_type`\n\nThe `entrypoint` and `form_type` values should match the encoded values in your `url`.\n\nYou should verify the values with whoever is doing the data analysis (e.g. Leif Oines).\n\n### Example\n\n```json\n{\n  \"button_action\": \"ENABLE_FIREFOX_MONITOR\",\n  \"button_action_args\": {\n     \"url\": \"https://monitor.firefox.com/oauth/init?utm_source=snippets&utm_campaign=monitor-snippet-test&form_type=email&entrypoint=newtab\",\n      \"flowRequestParams\": {\n        \"entrypoint\": \"snippets\",\n        \"utm_term\": \"monitor\",\n        \"form_type\": \"email\"\n      }\n  }\n}\n```\n"
  },
  {
    "path": "content-src/asrouter/rich-text-strings.js",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport { FluentBundle } from \"fluent\";\n\n/**\n * Properties that allow rich text MUST be added to this list.\n *   key: the localization_id that should be used\n *   value: a property or array of properties on the message.content object\n */\nconst RICH_TEXT_CONFIG = {\n  text: [\"text\", \"scene1_text\"],\n  success_text: \"success_text\",\n  error_text: \"error_text\",\n  scene2_text: \"scene2_text\",\n  amo_html: \"amo_html\",\n  privacy_html: \"scene2_privacy_html\",\n  disclaimer_html: \"scene2_disclaimer_html\",\n};\n\nexport const RICH_TEXT_KEYS = Object.keys(RICH_TEXT_CONFIG);\n\n/**\n * Generates an array of messages suitable for fluent's localization provider\n * including all needed strings for rich text.\n * @param {object} content A .content object from an ASR message (i.e. message.content)\n * @returns {FluentBundle[]} A array containing the fluent message context\n */\nexport function generateBundles(content) {\n  const bundle = new FluentBundle(\"en-US\");\n\n  RICH_TEXT_KEYS.forEach(key => {\n    const attrs = RICH_TEXT_CONFIG[key];\n    const attrsToTry = Array.isArray(attrs) ? [...attrs] : [attrs];\n    let string = \"\";\n    while (!string && attrsToTry.length) {\n      const attr = attrsToTry.pop();\n      string = content[attr];\n    }\n    bundle.addMessages(`${key} = ${string}`);\n  });\n  return [bundle];\n}\n"
  },
  {
    "path": "content-src/asrouter/schemas/message-format.md",
    "content": "## Activity Stream Router message format\n\nField name | Type     | Required | Description | Example / Note\n---        | ---      | ---      | ---         | ---\n`id`       | `string` | Yes | A unique identifier for the message that should not conflict with any other previous message | `ONBOARDING_1`\n`template` | `string` | Yes | An id matching an existing Activity Stream Router template | [See example](https://github.com/mozilla/activity-stream/blob/33669c67c2269078a6d3d6d324fb48175d98f634/system-addon/content-src/message-center/templates/SimpleSnippet.jsx)\n`content` | `object` | Yes | An object containing all variables/props to be rendered in the template. Subset of allowed tags detailed below. | [See example below](#html-subset)\n`bundled` | `integer` | No | The number of messages of the same template this one should be shown with | [See example below](#a-bundled-message-example)\n`order` | `integer` | No | If bundled with other messages of the same template, which order should this one be placed in? Defaults to 0 if no order is desired | [See example below](#a-bundled-message-example)\n`campaign` | `string` | No | Campaign id that the message belongs to | `RustWebAssembly`\n`targeting` | `string` `JEXL` | No | A [JEXL expression](http://normandy.readthedocs.io/en/latest/user/filter_expressions.html#jexl-basics) with all targeting information needed in order to decide if the message is shown | Not yet implemented, [Examples](#targeting-attributes)\n`trigger` | `string` | No | An event or condition upon which the message will be immediately shown. This can be combined with `targeting`. Messages that define a trigger will not be shown during non-trigger-based passive message rotation.\n`trigger.params` | `[string]` | No | A set of hostnames passed down as parameters to the trigger condition. Used to restrict the number of domains where the trigger/message is valid. | [See example below](#trigger-params)\n`trigger.patterns` | `[string]` | No | A set of patterns that match multiple hostnames passed down as parameters to the trigger condition. Used to restrict the number of domains where the trigger/message is valid. | [See example below](#trigger-patterns)\n`frequency` | `object` | No | A definition for frequency cap information for the message\n`frequency.lifetime` | `integer` | No | The maximum number of lifetime impressions for the message.\n`frequency.custom` | `array` | No | An array of frequency cap definition objects including `period`, a time period in milliseconds, and `cap`, a max number of impressions for that period.\n\n### Message example\n```javascript\n{\n  id: \"ONBOARDING_1\",\n  template: \"simple_snippet\",\n  content: {\n    title: \"Find it faster\",\n    body: \"Access all of your favorite search engines with a click. Search the whole Web or just one website from the search box.\"\n  },\n  targeting: \"usesFirefoxSync && !addonsInfo.addons['activity-stream@mozilla.org']\",\n  frequency: {\n    lifetime: 20,\n    custom: [{period: \"daily\", cap: 5}, {period: 3600000, cap: 1}]\n  }\n}\n```\n\n### A Bundled Message example\nThe following 2 messages have a `bundled` property, indicating that they should be shown together, since they have the same template. The number `2` indicates that this message should be shown in a bundle of 2 messages of the same template. The order property defines that ONBOARDING_2 should be shown after ONBOARDING_3 in the bundle.\n```javascript\n{\n  id: \"ONBOARDING_2\",\n  template: \"onboarding\",\n  bundled: 2,\n  order: 2,\n  content: {\n    title: \"Private Browsing\",\n    body: \"Browse by yourself. Private Browsing with Tracking Protection blocks online trackers that follow you around the web.\"\n  },\n  targeting: \"\",\n  trigger: \"firstRun\"\n}\n{\n  id: \"ONBOARDING_3\",\n  template: \"onboarding\",\n  bundled: 2,\n  order: 1,\n  content: {\n    title: \"Find it faster\",\n    body: \"Access all of your favorite search engines with a click. Search the whole Web or just one website from the search box.\"\n  },\n  targeting: \"\",\n  trigger: \"firstRun\"\n}\n```\n\n### HTML subset\nThe following tags are allowed in the content of the snippet: `i, b, u, strong, em, br`.\n\nLinks cannot be rendered using regular anchor tags because [Fluent does not allow for href attributes](https://github.com/projectfluent/fluent.js/blob/a03d3aa833660f8c620738b26c80e46b1a4edb05/fluent-dom/src/overlay.js#L13). They will be wrapped in custom tags, for example `<cta>link</cta>` and the url will be provided as part of the payload:\n```\n{\n  \"id\": \"7899\",\n  \"content\": {\n    \"text\": \"Use the CMD (CTRL) + T keyboard shortcut to <cta>open a new tab quickly!</cta>\",\n    \"links\": {\n      \"cta\": {\n        \"url\": \"https://support.mozilla.org/en-US/kb/keyboard-shortcuts-perform-firefox-tasks-quickly\"\n      }\n    }\n  }\n}\n```\nIf a tag that is not on the allowed is used, the text content will be extracted and displayed.\n\nGrouping multiple allowed elements is not possible, only the first level will be used: `<u><b>text</b></u>` will be interpreted as `<u>text</u>`.\n\n### Trigger params\nA set of hostnames that need to exactly match the location of the selected tab in order for the trigger to execute.\n```\n[\"github.com\", \"wwww.github.com\"]\n```\nMore examples in the [CFRMessageProvider](https://github.com/mozilla/activity-stream/blob/e76ce12fbaaac1182aa492b84fc038f78c3acc33/lib/CFRMessageProvider.jsm#L40-L47).\n\n### Trigger patterns\nA set of patterns that can match multiple hostnames. When the location of the selected tab matches one of the patterns it can execute a trigger.\n```\n[\"*://*.github.com\"] // can match `github.com` but also match `https://gist.github.com/`\n```\nMore [MatchPattern examples](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Match_patterns#Examples).\n\n### Targeting attributes\n(This section has moved to [targeting-attributes.md](../docs/targeting-attributes.md)).\n"
  },
  {
    "path": "content-src/asrouter/schemas/panel/cfr-fxa-bookmark.schema.json",
    "content": "{\n  \"title\": \"CFRFxABookmark\",\n  \"description\": \"A message shown in the bookmark panel when user adds or edits a bookmark\",\n  \"version\": \"1.0.0\",\n  \"type\": \"object\",\n  \"definitions\": {\n    \"plainText\": {\n      \"description\": \"Plain text (no HTML allowed)\",\n      \"type\": \"string\"\n    },\n    \"richText\": {\n      \"description\": \"Text with HTML subset allowed: i, b, u, strong, em, br\",\n      \"type\": \"string\"\n    },\n    \"link_url\": {\n      \"description\": \"Target for links or buttons\",\n      \"type\": \"string\",\n      \"format\": \"uri\"\n    }\n  },\n  \"properties\": {\n    \"title\": {\n      \"description\": \"Shown at the top of the message in the largest font size.\",\n      \"oneOf\": [\n        {\n          \"allOf\": [\n            {\"$ref\": \"#/definitions/richText\"},\n            {\"description\": \"Message to be shown\"}\n          ]\n        },\n        {\n          \"type\": \"object\",\n          \"properties\": {\n            \"string_id\": {\n              \"type\": \"string\",\n              \"description\": \"Fluent id of localized string\"\n            }\n          },\n          \"required\": [\"string_id\"]\n        }\n      ]\n    },\n    \"text\": {\n      \"description\": \"Longest part of the message, below the title, provides explanation.\",\n      \"oneOf\": [\n        {\n          \"allOf\": [\n            {\"$ref\": \"#/definitions/richText\"},\n            {\"description\": \"Message to be shown\"}\n          ]\n        },\n        {\n          \"type\": \"object\",\n          \"properties\": {\n            \"string_id\": {\n              \"type\": \"string\",\n              \"description\": \"Fluent id of localized string\"\n            }\n          },\n          \"required\": [\"string_id\"]\n        }\n      ]\n    },\n    \"cta\": {\n      \"description\": \"Link shown at the bottom of the message, call to action\",\n      \"oneOf\": [\n        {\n          \"allOf\": [\n            {\"$ref\": \"#/definitions/richText\"},\n            {\"description\": \"Message to be shown\"}\n          ]\n        },\n        {\n          \"type\": \"object\",\n          \"properties\": {\n            \"string_id\": {\n              \"type\": \"string\",\n              \"description\": \"Fluent id of localized string\"\n            }\n          },\n          \"required\": [\"string_id\"]\n        }\n      ]\n    },\n    \"info_icon\": {\n      \"type\": \"object\",\n      \"description\": \"The small icon displayed in the top right corner of the panel. Not configurable, only the tooltip text.\" ,\n      \"properties\": {\n        \"tooltiptext\": {\n          \"oneOf\": [\n            {\n              \"allOf\": [\n                {\"$ref\": \"#/definitions/plainText\"},\n                {\"description\": \"Message to be shown\"}\n              ]\n            },\n            {\n              \"type\": \"object\",\n              \"properties\": {\n                \"string_id\": {\n                  \"type\": \"string\",\n                  \"description\": \"Fluent id of localized string\"\n                }\n              },\n              \"required\": [\"string_id\"]\n            }\n          ]\n        }\n      },\n      \"required\": [\"tooltiptext\"]\n    },\n    \"close_button\": {\n      \"type\": \"object\",\n      \"description\": \"The small dissmiss icon displayed in the top right corner of the message. Not configurable, only the tooltip text.\" ,\n      \"properties\": {\n        \"tooltiptext\": {\n          \"oneOf\": [\n            {\n              \"allOf\": [\n                {\"$ref\": \"#/definitions/plainText\"},\n                {\"description\": \"Message to be shown\"}\n              ]\n            },\n            {\n              \"type\": \"object\",\n              \"properties\": {\n                \"string_id\": {\n                  \"type\": \"string\",\n                  \"description\": \"Fluent id of localized string\"\n                }\n              },\n              \"required\": [\"string_id\"]\n            }\n          ]\n        }\n      },\n      \"required\": [\"tooltiptext\"]\n    },\n    \"color\": {\n      \"description\": \"Message text color\",\n      \"allOf\": [\n        {\"$ref\": \"#/definitions/plainText\"},\n        {\"description\": \"Valid CSS color\"}\n      ]\n    },\n    \"background_color_1\": {\n      \"description\": \"Configurable background color through CSS gradient\",\n      \"allOf\": [\n        {\"$ref\": \"#/definitions/plainText\"},\n        {\"description\": \"Valid CSS color\"}\n      ]\n    },\n    \"background_color_2\": {\n      \"description\": \"Configurable background color through CSS gradient\",\n      \"allOf\": [\n        {\"$ref\": \"#/definitions/plainText\"},\n        {\"description\": \"Valid CSS color\"}\n      ]\n    }\n  },\n  \"additionalProperties\": false,\n  \"required\": [\"title\", \"text\", \"cta\", \"info_icon\"]\n}\n"
  },
  {
    "path": "content-src/asrouter/schemas/provider-response.schema.json",
    "content": "{\n  \"title\": \"ProviderResponse\",\n  \"description\": \"A response object for remote providers of AS Router\",\n  \"type\": \"object\",\n  \"version\": \"6.1.0\",\n  \"properties\": {\n    \"messages\": {\n      \"type\": \"array\",\n      \"description\": \"An array of router messages\",\n      \"items\": {\n        \"title\": \"RouterMessage\",\n        \"description\": \"A definition of an individual message\",\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": {\n            \"type\": \"string\",\n            \"description\": \"A unique identifier for the message that should not conflict with any other previous message\"\n          },\n          \"template\": {\n            \"type\": \"string\",\n            \"description\": \"An id matching an existing Activity Stream Router template\",\n            \"enum\": [\"simple_snippet\"]\n          },\n          \"bundled\": {\n            \"type\": \"integer\",\n            \"description\": \"The number of messages of the same template this one should be shown with (optional)\"\n          },\n          \"order\": {\n            \"type\": \"integer\",\n            \"minimum\": 0,\n            \"description\": \"If bundled with other messages of the same template, which order should this one be placed in? (optional - defaults to 0)\"\n          },\n          \"content\": {\n            \"type\": \"object\",\n            \"description\": \"An object containing all variables/props to be rendered in the template. See individual template schemas for details.\"\n          },\n          \"targeting\": {\n            \"type\": \"string\",\n            \"description\": \"A JEXL expression representing targeting information\"\n          },\n          \"personalized\": {\n            \"type\": \"boolean\",\n            \"description\": \"Is a personalized score applied to the provider's messages?\"\n          },\n          \"personalizedModelVersion\": {\n            \"type\": \"string\",\n            \"description\": \"The version of the model use for personalization\"\n          },\n          \"trigger\": {\n            \"type\": \"object\",\n            \"description\": \"An action to trigger potentially showing the message\",\n            \"properties\": {\n              \"id\": {\n                \"type\": \"string\",\n                \"description\": \"A string identifying the trigger action\",\n                \"enum\": [\"firstRun\", \"openURL\"]\n              },\n              \"params\": {\n                \"type\": \"array\",\n                \"description\": \"An optional array of string parameters for the trigger action\",\n                \"items\": {\n                  \"type\": \"string\",\n                  \"description\": \"A parameter for the trigger action\"\n                }\n              }\n            },\n            \"required\": [\"id\"]\n          },\n          \"frequency\": {\n            \"type\": \"object\",\n            \"description\": \"An object containing frequency cap information for a message.\",\n            \"properties\": {\n              \"lifetime\": {\n                \"type\": \"integer\",\n                \"description\": \"The maximum lifetime impressions for a message.\",\n                \"minimum\": 1,\n                \"maximum\": 100\n              },\n              \"custom\": {\n                \"type\": \"array\",\n                \"description\": \"An array of custom frequency cap definitions.\",\n                \"items\": {\n                  \"description\": \"A frequency cap definition containing time and max impression information\",\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"period\": {\n                      \"oneOf\": [\n                        {\n                          \"type\": \"integer\",\n                          \"description\": \"Period of time in milliseconds (e.g. 86400000 for one day)\"\n                        },\n                        {\n                          \"type\": \"string\",\n                          \"description\": \"One of a preset list of short forms for period of time (e.g. 'daily' for one day)\",\n                          \"enum\": [\"daily\"]\n                        }\n                      ]\n\n                    },\n                    \"cap\": {\n                      \"type\": \"integer\",\n                      \"description\": \"The maximum impressions for the message within the defined period.\",\n                      \"minimum\": 1,\n                      \"maximum\": 100\n                    }\n                  },\n                  \"required\": [\"period\", \"cap\"]\n                }\n              }\n            }\n          }\n        },\n        \"required\": [\"id\", \"template\", \"content\"]\n      }\n    }\n  },\n  \"required\": [\"messages\"]\n}\n"
  },
  {
    "path": "content-src/asrouter/template-utils.js",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nexport function safeURI(url) {\n  if (!url) {\n    return \"\";\n  }\n  const { protocol } = new URL(url);\n  const isAllowed = [\n    \"http:\",\n    \"https:\",\n    \"data:\",\n    \"resource:\",\n    \"chrome:\",\n  ].includes(protocol);\n  if (!isAllowed) {\n    console.warn(`The protocol ${protocol} is not allowed for template URLs.`); // eslint-disable-line no-console\n  }\n  return isAllowed ? url : \"\";\n}\n"
  },
  {
    "path": "content-src/asrouter/templates/CFR/templates/ExtensionDoorhanger.schema.json",
    "content": "{\n  \"title\": \"ExtensionDoorhanger\",\n  \"description\": \"A template with a heading, addon icon, title and description. No markup allowed.\",\n  \"version\": \"1.0.0\",\n  \"type\": \"object\",\n  \"definitions\": {\n    \"plainText\": {\n      \"description\": \"Plain text (no HTML allowed)\",\n      \"type\": \"string\"\n    },\n    \"linkUrl\": {\n      \"description\": \"Target for links or buttons\",\n      \"type\": \"string\",\n      \"format\": \"uri\"\n    }\n  },\n  \"properties\": {\n    \"layout\": {\n      \"type\": \"string\",\n      \"description\": \"The layout style of the pop-over.\"\n    },\n    \"category\": {\n      \"type\": \"string\",\n      \"description\": \"Attribute used for different groups of messages from the same provider\"\n    },\n    \"layout\": {\n      \"type\": \"string\",\n      \"description\": \"Attribute used for different groups of messages from the same provider\",\n      \"enum\": [\"short_message\", \"message_and_animation\", \"icon_and_message\", \"addon_recommendation\"]\n    },\n    \"anchor_id\": {\n      \"type\": \"string\",\n      \"description\": \"A DOM element ID that the pop-over will be anchored.\"\n    },\n    \"bucket_id\": {\n      \"type\": \"string\",\n      \"description\": \"A bucket identifier for the addon. This is used in order to anonymize telemetry for history-sensitive targeting.\"\n    },\n    \"skip_address_bar_notifier\": {\n      \"type\": \"boolean\",\n      \"description\": \"Skip the 'Recommend' notifier and show directly.\"\n    },\n    \"notification_text\": {\n      \"description\": \"The text in the small blue chicklet that appears in the URL bar. This can be a reference to a localized string in Firefox or just a plain string.\",\n      \"oneOf\": [\n        {\n          \"type\": \"string\",\n          \"description\": \"Message shown in the location bar notification.\"\n        },\n        {\n          \"type\": \"object\",\n          \"properties\": {\n            \"string_id\": {\n              \"type\": \"string\",\n              \"description\": \"Id of localized string for the location bar notification.\"\n            }\n          },\n          \"required\": [\"string_id\"]\n        }\n      ]\n    },\n    \"info_icon\": {\n      \"type\": \"object\",\n      \"description\": \"The small icon displayed in the top right corner of the pop-over. Should be 19x19px, svg or png. Defaults to a small question mark.\" ,\n      \"properties\": {\n        \"label\": {\n          \"oneOf\": [\n            {\n              \"type\": \"object\",\n              \"properties\": {\n                \"attributes\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"tooltiptext\": {\n                      \"type\": \"string\",\n                      \"description\": \"Text for button tooltip used to provide information about the doorhanger.\"\n                    }\n                  },\n                  \"required\": [\"tooltiptext\"]\n                }\n              },\n              \"required\": [\"attributes\"]\n            },\n            {\n              \"type\": \"object\",\n              \"properties\": {\n                \"string_id\": {\n                  \"type\": \"string\",\n                  \"description\": \"Id of localized string used to provide information about the doorhanger.\"\n                }\n              },\n              \"required\": [\"string_id\"]\n            }\n          ]\n        },\n        \"sumo_path\": {\n          \"type\": \"string\",\n          \"description\": \"Last part of the path in the URL to the support page with the information about the doorhanger.\",\n          \"examples\": [\"extensionpromotions\", \"extensionrecommendations\"]\n        }\n      }\n    },\n    \"learn_more\": {\n      \"type\": \"string\",\n      \"description\": \"Last part of the path in the SUMO URL to the support page with the information about the doorhanger.\",\n      \"examples\": [\"extensionpromotions\", \"extensionrecommendations\"]\n    },\n    \"heading_text\": {\n      \"description\": \"The larger heading text displayed in the pop-over. This can be a reference to a localized string in Firefox or just a plain string.\",\n      \"oneOf\": [\n        {\n          \"type\": \"string\",\n          \"description\": \"The message displayed in the title of the extension doorhanger\"\n        },\n        {\n          \"type\": \"object\",\n          \"properties\": {\n            \"string_id\": {\n              \"type\": \"string\"\n            }\n          },\n          \"required\": [\"string_id\"],\n          \"description\": \"Id of localized string for extension doorhanger title\"\n        }\n      ]\n    },\n    \"icon\": {\n      \"description\": \"The icon displayed in the pop-over. Should be 32x32px or 64x64px and png/svg.\",\n      \"allOf\": [\n        {\"$ref\": \"#/definitions/linkUrl\"},\n        {\"description\": \"Icon associated with the message\"}\n      ]\n    },\n    \"icon_dark_theme\": {\n      \"type\": \"string\",\n      \"description\": \"Pop-over icon, dark theme variant. Should be 32x32px or 64x64px and png/svg.\"\n    },\n    \"icon_class\": {\n      \"type\": \"string\",\n      \"description\": \"CSS class of the pop-over icon.\"\n    },\n    \"addon\": {\n      \"description\": \"Addon information including AMO URL.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"id\": {\n          \"allOf\": [\n            {\"$ref\": \"#/definitions/plainText\"},\n            {\"description\": \"Unique addon ID\"}\n          ]\n        },\n        \"title\": {\n          \"allOf\": [\n            {\"$ref\": \"#/definitions/plainText\"},\n            {\"description\": \"Addon name\"}\n          ]\n        },\n        \"author\": {\n          \"allOf\": [\n            {\"$ref\": \"#/definitions/plainText\"},\n            {\"description\": \"Addon author\"}\n          ]\n        },\n        \"icon\": {\n          \"description\": \"The icon displayed in the pop-over. Should be 64x64px and png/svg.\",\n          \"allOf\": [\n            {\"$ref\": \"#/definitions/linkUrl\"},\n            {\"description\": \"Addon icon\"}\n          ]\n        },\n        \"rating\": {\n          \"type\": \"number\",\n          \"minimum\": 0,\n          \"maximum\": 5,\n          \"description\": \"Star rating\"\n        },\n        \"users\": {\n          \"type\": \"integer\",\n          \"minimum\": 0,\n          \"description\": \"Installed users\"\n        },\n        \"amo_url\": {\n          \"allOf\": [\n            {\"$ref\": \"#/definitions/linkUrl\"},\n            {\"description\": \"Link that offers more information related to the addon.\"}\n          ]\n        }\n      },\n      \"required\": [\"title\", \"author\", \"icon\", \"amo_url\"]\n    },\n    \"text\": {\n      \"description\": \"The body text displayed in the pop-over. This can be a reference to a localized string in Firefox or just a plain string.\",\n      \"oneOf\": [\n        {\n          \"type\": \"string\",\n          \"description\": \"Description message of the addon.\"\n        },\n        {\n          \"type\": \"object\",\n          \"properties\": {\n            \"string_id\": {\n              \"type\": \"string\",\n              \"description\": \"Id of string to localized addon description\"\n            }\n          },\n          \"required\": [\"string_id\"]\n        }\n      ]\n    },\n    \"descriptionDetails\": {\n      \"description\": \"Additional information and steps on how to use\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"steps\": {\n          \"description\": \"Array of messages or string_ids\",\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"object\",\n            \"properties\": {\n              \"string_id\": {\n                \"type\": \"string\",\n                \"description\": \"Id of string to localized addon description\"\n              }\n            },\n            \"required\": [\"string_id\"]\n          }\n        }\n      },\n      \"required\": [\"steps\"]\n    },\n    \"buttons\": {\n      \"description\": \"The label and functionality for the buttons in the pop-over.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"primary\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"label\": {\n              \"type\": \"object\",\n              \"oneOf\": [\n                {\n                  \"properties\": {\n                    \"value\": {\n                      \"allOf\": [\n                        {\"$ref\": \"#/definitions/plainText\"},\n                        {\"description\": \"Button label override used when a localized version is not available.\"}\n                      ]\n                    },\n                    \"attributes\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"accesskey\": {\n                          \"type\": \"string\",\n                          \"description\": \"A single character to be used as a shortcut key for the secondary button. This should be one of the characters that appears in the button label.\"\n                        }\n                      },\n                      \"required\": [\"accesskey\"],\n                      \"description\": \"Button attributes.\"\n                    }\n                  },\n                  \"required\": [\"value\", \"attributes\"]\n                },\n                {\n                  \"properties\": {\n                    \"string_id\": {\n                      \"allOf\": [\n                        {\"$ref\": \"#/definitions/plainText\"},\n                        {\"description\": \"Id of localized string for button\"}\n                      ]\n                    }\n                  },\n                  \"required\": [\"string_id\"]\n                }\n              ],\n              \"description\": \"Id of localized string or message override.\"\n            },\n            \"action\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"type\": {\n                  \"type\": \"string\",\n                  \"description\": \"Action dispatched by the button.\"\n                },\n                \"data\": {\n                  \"properties\": {\n                    \"url\": {\n                      \"type\": \"null\",\n                      \"$comment\": \"This is dynamically generated from the addon.id. See CFRPageActions.jsm\",\n                      \"description\": \"URL used in combination with the primary action dispatched.\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"secondary\": {\n            \"type\": \"object\",\n            \"properties\": {\n              \"label\": {\n                \"type\": \"object\",\n                \"oneOf\": [\n                  {\n                    \"properties\": {\n                      \"value\": {\n                        \"allOf\": [\n                          {\"$ref\": \"#/definitions/plainText\"},\n                          {\"description\": \"Button label override used when a localized version is not available.\"}\n                        ]\n                      },\n                      \"attributes\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"accesskey\": {\n                            \"type\": \"string\",\n                            \"description\": \"A single character to be used as a shortcut key for the secondary button. This should be one of the characters that appears in the button label.\"\n                          }\n                        },\n                        \"required\": [\"accesskey\"],\n                        \"description\": \"Button attributes.\"\n                      }\n                    },\n                    \"required\": [\"value\", \"attributes\"]\n                  },\n                  {\n                    \"properties\": {\n                      \"string_id\": {\n                        \"allOf\": [\n                          {\"$ref\": \"#/definitions/plainText\"},\n                          {\"description\": \"Id of localized string for button\"}\n                        ]\n                      }\n                    },\n                    \"required\": [\"string_id\"]\n                  }\n                ],\n                \"description\": \"Id of localized string or message override.\"\n              },\n              \"action\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"type\": {\n                    \"type\": \"string\",\n                    \"description\": \"Action dispatched by the button.\"\n                  },\n                  \"data\": {\n                    \"properties\": {\n                      \"url\": {\n                        \"allOf\": [\n                          {\"$ref\": \"#/definitions/linkUrl\"},\n                          {\"description\": \"URL used in combination with the primary action dispatched.\"}\n                        ]\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        }\n      }\n    }\n  },\n  \"additionalProperties\": false,\n  \"required\": [\"layout\", \"category\", \"bucket_id\", \"notification_text\", \"heading_text\", \"text\", \"buttons\"]\n}\n"
  },
  {
    "path": "content-src/asrouter/templates/EOYSnippet/EOYSnippet.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport React from \"react\";\nimport schema from \"./EOYSnippet.schema.json\";\nimport { SimpleSnippet } from \"../SimpleSnippet/SimpleSnippet\";\n\nclass EOYSnippetBase extends React.PureComponent {\n  constructor(props) {\n    super(props);\n    this.handleSubmit = this.handleSubmit.bind(this);\n  }\n\n  /**\n   * setFrequencyValue - `frequency` form parameter value should be `monthly`\n   *                     if `monthly-checkbox` is selected or `single` otherwise\n   */\n  setFrequencyValue() {\n    const frequencyCheckbox = this.refs.form.querySelector(\"#monthly-checkbox\");\n    if (frequencyCheckbox.checked) {\n      this.refs.form.querySelector(\"[name='frequency']\").value = \"monthly\";\n    }\n  }\n\n  handleSubmit(event) {\n    event.preventDefault();\n    this.setFrequencyValue();\n    this.refs.form.submit();\n    if (!this.props.content.do_not_autoblock) {\n      this.props.onBlock();\n    }\n  }\n\n  renderDonations() {\n    const fieldNames = [\"first\", \"second\", \"third\", \"fourth\"];\n    const numberFormat = new Intl.NumberFormat(\n      this.props.content.locale || navigator.language,\n      {\n        style: \"currency\",\n        currency: this.props.content.currency_code,\n        minimumFractionDigits: 0,\n      }\n    );\n    // Default to `second` button\n    const { selected_button } = this.props.content;\n    const btnStyle = {\n      color: this.props.content.button_color,\n      backgroundColor: this.props.content.button_background_color,\n    };\n    const donationURLParams = [];\n    const paramsStartIndex = this.props.content.donation_form_url.indexOf(\"?\");\n    for (const entry of new URLSearchParams(\n      this.props.content.donation_form_url.slice(paramsStartIndex)\n    ).entries()) {\n      donationURLParams.push(entry);\n    }\n\n    return (\n      <form\n        className=\"EOYSnippetForm\"\n        action={this.props.content.donation_form_url}\n        method={this.props.form_method}\n        onSubmit={this.handleSubmit}\n        ref=\"form\"\n      >\n        {donationURLParams.map(([key, value], idx) => (\n          <input type=\"hidden\" name={key} value={value} key={idx} />\n        ))}\n        {fieldNames.map((field, idx) => {\n          const button_name = `donation_amount_${field}`;\n          const amount = this.props.content[button_name];\n          return (\n            <React.Fragment key={idx}>\n              <input\n                type=\"radio\"\n                name=\"amount\"\n                value={amount}\n                id={field}\n                defaultChecked={button_name === selected_button}\n              />\n              <label htmlFor={field} className=\"donation-amount\">\n                {numberFormat.format(amount)}\n              </label>\n            </React.Fragment>\n          );\n        })}\n\n        <div className=\"monthly-checkbox-container\">\n          <input id=\"monthly-checkbox\" type=\"checkbox\" />\n          <label htmlFor=\"monthly-checkbox\">\n            {this.props.content.monthly_checkbox_label_text}\n          </label>\n        </div>\n\n        <input type=\"hidden\" name=\"frequency\" value=\"single\" />\n        <input\n          type=\"hidden\"\n          name=\"currency\"\n          value={this.props.content.currency_code}\n        />\n        <input\n          type=\"hidden\"\n          name=\"presets\"\n          value={fieldNames.map(\n            field => this.props.content[`donation_amount_${field}`]\n          )}\n        />\n        <button\n          style={btnStyle}\n          type=\"submit\"\n          className=\"ASRouterButton primary donation-form-url\"\n        >\n          {this.props.content.button_label}\n        </button>\n      </form>\n    );\n  }\n\n  render() {\n    const textStyle = {\n      color: this.props.content.text_color,\n      backgroundColor: this.props.content.background_color,\n    };\n    const customElement = (\n      <em style={{ backgroundColor: this.props.content.highlight_color }} />\n    );\n    return (\n      <SimpleSnippet\n        {...this.props}\n        className={this.props.content.test}\n        customElements={{ em: customElement }}\n        textStyle={textStyle}\n        extraContent={this.renderDonations()}\n      />\n    );\n  }\n}\n\nexport const EOYSnippet = props => {\n  const extendedContent = {\n    monthly_checkbox_label_text:\n      schema.properties.monthly_checkbox_label_text.default,\n    locale: schema.properties.locale.default,\n    currency_code: schema.properties.currency_code.default,\n    selected_button: schema.properties.selected_button.default,\n    ...props.content,\n  };\n\n  return (\n    <EOYSnippetBase {...props} content={extendedContent} form_method=\"GET\" />\n  );\n};\n"
  },
  {
    "path": "content-src/asrouter/templates/EOYSnippet/EOYSnippet.schema.json",
    "content": "{\n  \"title\": \"EOYSnippet\",\n  \"description\": \"Fundraising Snippet\",\n  \"version\": \"1.1.0\",\n  \"type\": \"object\",\n  \"definitions\": {\n    \"plainText\": {\n      \"description\": \"Plain text (no HTML allowed)\",\n      \"type\": \"string\"\n    },\n    \"richText\": {\n      \"description\": \"Text with HTML subset allowed: i, b, u, strong, em, br\",\n      \"type\": \"string\"\n    },\n    \"link_url\": {\n      \"description\": \"Target for links or buttons\",\n      \"type\": \"string\",\n      \"format\": \"uri\"\n    }\n  },\n  \"properties\": {\n    \"donation_form_url\": {\n      \"type\": \"string\",\n      \"description\": \"Url to the donation form.\"\n    },\n    \"currency_code\": {\n      \"type\": \"string\",\n      \"description\": \"The code for the currency. Examle gbp, cad, usd.\",\n      \"default\": \"usd\"\n    },\n    \"locale\": {\n      \"type\": \"string\",\n      \"description\": \"String for the locale code.\",\n      \"default\": \"en-US\"\n    },\n    \"text\": {\n      \"allOf\": [\n        {\"$ref\": \"#/definitions/richText\"},\n        {\"description\": \"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br\"}\n      ]\n    },\n    \"text_color\": {\n      \"type\": \"string\",\n      \"description\": \"Modify the text message color\"\n    },\n    \"background_color\": {\n      \"type\": \"string\",\n      \"description\": \"Snippet background color.\"\n    },\n    \"highlight_color\": {\n      \"type\": \"string\",\n      \"description\": \"Paragraph em highlight color.\"\n    },\n    \"donation_amount_first\": {\n      \"type\": \"number\",\n      \"description\": \"First button amount.\"\n    },\n    \"donation_amount_second\": {\n      \"type\": \"number\",\n      \"description\": \"Second button amount.\"\n    },\n    \"donation_amount_third\": {\n      \"type\": \"number\",\n      \"description\": \"Third button amount.\"\n    },\n    \"donation_amount_fourth\": {\n      \"type\": \"number\",\n      \"description\": \"Fourth button amount.\"\n    },\n    \"selected_button\": {\n      \"type\": \"string\",\n      \"description\": \"Default donation_amount_second. Donation amount button that's selected by default.\",\n      \"default\": \"donation_amount_second\"\n    },\n    \"icon\": {\n      \"type\": \"string\",\n      \"description\": \"Snippet icon. 64x64px. SVG or PNG preferred.\"\n    },\n    \"icon_dark_theme\": {\n      \"type\": \"string\",\n      \"description\": \"Snippet icon. Dark theme variant. 64x64px. SVG or PNG preferred.\"\n    },\n    \"icon_alt_text\": {\n      \"type\": \"string\",\n      \"description\": \"Alt text for accessibility\",\n      \"default\": \"\"\n    },\n    \"title\": {\n      \"allOf\": [\n        {\"$ref\": \"#/definitions/plainText\"},\n        {\"description\": \"Snippet title displayed before snippet text\"}\n      ]\n    },\n    \"title_icon\": {\n      \"type\": \"string\",\n      \"description\": \"Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale.\"\n    },\n    \"title_icon_dark_theme\": {\n      \"type\": \"string\",\n      \"description\": \"Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale.\"\n    },\n    \"button_label\": {\n      \"allOf\": [\n        {\"$ref\": \"#/definitions/plainText\"},\n        {\"description\": \"Text for a button next to main snippet text that links to button_url. Requires button_url.\"}\n      ]\n    },\n    \"button_color\": {\n      \"type\": \"string\",\n      \"description\": \"The text color of the button. Valid CSS color.\"\n    },\n    \"button_background_color\": {\n      \"type\": \"string\",\n      \"description\": \"The background color of the button. Valid CSS color.\"\n    },\n    \"block_button_text\": {\n      \"type\": \"string\",\n      \"description\": \"Tooltip text used for dismiss button.\"\n    },\n    \"monthly_checkbox_label_text\": {\n      \"type\": \"string\",\n      \"description\": \"Label text for monthly checkbox.\",\n      \"default\": \"Make my donation monthly\"\n    },\n    \"test\": {\n      \"type\": \"string\",\n      \"description\": \"Different styles for the snippet. Options are bold and takeover.\"\n    },\n    \"do_not_autoblock\": {\n      \"type\": \"boolean\",\n      \"description\": \"Used to prevent blocking the snippet after the CTA (link or button) has been clicked\"\n    },\n    \"links\": {\n      \"additionalProperties\": {\n        \"url\": {\n          \"allOf\": [\n            {\"$ref\": \"#/definitions/link_url\"},\n            {\"description\": \"The url where the link points to.\"}\n          ]\n        },\n        \"metric\": {\n          \"type\": \"string\",\n          \"description\": \"Custom event name sent with telemetry event.\"\n        },\n        \"args\": {\n          \"type\": \"string\",\n          \"description\": \"Additional parameters for link action, example which specific menu the button should open\"\n        }\n      }\n    }\n  },\n  \"additionalProperties\": false,\n  \"required\": [\"text\", \"donation_form_url\", \"donation_amount_first\", \"donation_amount_second\", \"donation_amount_third\", \"donation_amount_fourth\", \"button_label\", \"currency_code\"],\n  \"dependencies\": {\n    \"button_color\": [\"button_label\"],\n    \"button_background_color\": [\"button_label\"]\n  }\n}\n\n"
  },
  {
    "path": "content-src/asrouter/templates/EOYSnippet/_EOYSnippet.scss",
    "content": ".EOYSnippetForm {\n  margin: 10px 0 8px;\n  align-self: start;\n  font-size: 14px;\n  display: flex;\n  align-items: center;\n\n  .donation-amount,\n  .donation-form-url {\n    white-space: nowrap;\n    font-size: 14px;\n    padding: 8px 20px;\n    border-radius: 2px;\n  }\n\n  .donation-amount {\n    color: $grey-90;\n    margin-inline-end: 18px;\n    border: 1px solid $grey-40;\n    padding: 5px 14px;\n    background: $grey-10;\n    cursor: pointer;\n  }\n\n  input {\n    &[type='radio'] {\n      opacity: 0;\n      margin-inline-end: -18px;\n\n      &:checked + .donation-amount {\n        background: $grey-50;\n        color: $white;\n        border: 1px solid $grey-60;\n      }\n\n      // accessibility\n      &:checked:focus + .donation-amount,\n      &:not(:checked):focus + .donation-amount {\n        border: 1px dotted var(--newtab-link-primary-color);\n      }\n    }\n  }\n\n  .monthly-checkbox-container {\n    display: flex;\n    width: 100%;\n  }\n\n  .donation-form-url {\n    margin-inline-start: 18px;\n    align-self: flex-end;\n    display: flex;\n  }\n}\n"
  },
  {
    "path": "content-src/asrouter/templates/FXASignupSnippet/FXASignupSnippet.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport React from \"react\";\nimport schema from \"./FXASignupSnippet.schema.json\";\nimport { SubmitFormSnippet } from \"../SubmitFormSnippet/SubmitFormSnippet.jsx\";\n\nexport const FXASignupSnippet = props => {\n  const userAgent = window.navigator.userAgent.match(/Firefox\\/([0-9]+)\\./);\n  const firefox_version = userAgent ? parseInt(userAgent[1], 10) : 0;\n  const extendedContent = {\n    scene1_button_label: schema.properties.scene1_button_label.default,\n    retry_button_label: schema.properties.retry_button_label.default,\n    scene2_email_placeholder_text:\n      schema.properties.scene2_email_placeholder_text.default,\n    scene2_button_label: schema.properties.scene2_button_label.default,\n    scene2_dismiss_button_text:\n      schema.properties.scene2_dismiss_button_text.default,\n    ...props.content,\n    hidden_inputs: {\n      action: \"email\",\n      context: \"fx_desktop_v3\",\n      entrypoint: \"snippets\",\n      utm_source: \"snippet\",\n      utm_content: firefox_version,\n      utm_campaign: props.content.utm_campaign,\n      utm_term: props.content.utm_term,\n      ...props.content.hidden_inputs,\n    },\n  };\n\n  return (\n    <SubmitFormSnippet\n      {...props}\n      content={extendedContent}\n      form_action={\"https://accounts.firefox.com/\"}\n      form_method=\"GET\"\n    />\n  );\n};\n"
  },
  {
    "path": "content-src/asrouter/templates/FXASignupSnippet/FXASignupSnippet.schema.json",
    "content": "{\n  \"title\": \"FXASignupSnippet\",\n  \"description\": \"A snippet template for FxA sign up/sign in\",\n  \"version\": \"1.2.0\",\n  \"type\": \"object\",\n  \"definitions\": {\n    \"plainText\": {\n      \"description\": \"Plain text (no HTML allowed)\",\n      \"type\": \"string\"\n    },\n    \"richText\": {\n      \"description\": \"Text with HTML subset allowed: i, b, u, strong, em, br\",\n      \"type\": \"string\"\n    },\n    \"link_url\": {\n      \"description\": \"Target for links or buttons\",\n      \"type\": \"string\",\n      \"format\": \"uri\"\n    }\n  },\n  \"properties\": {\n    \"scene1_title\": {\n      \"allof\": [\n        {\"$ref\": \"#/definitions/plainText\"},\n        {\"description\": \"snippet title displayed before snippet text\"}\n      ]\n    },\n    \"scene1_text\": {\n      \"allOf\": [\n        {\"$ref\": \"#/definitions/richText\"},\n        {\"description\": \"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br\"}\n      ]\n    },\n    \"scene1_section_title_icon\": {\n      \"type\": \"string\",\n      \"description\": \"Section title icon for scene 1. 16x16px. SVG or PNG preferred. scene1_section_title_text must also be specified to display.\"\n    },\n    \"scene1_section_title_icon_dark_theme\": {\n      \"type\": \"string\",\n      \"description\": \"Section title icon for scene 1, dark theme variant. 16x16px. SVG or PNG preferred. scene1_section_title_text must also be specified to display.\"\n    },\n    \"scene1_section_title_text\": {\n      \"type\": \"string\",\n      \"description\": \"Section title text for scene 1. scene1_section_title_icon must also be specified to display.\"\n    },\n    \"scene1_section_title_url\": {\n      \"allOf\": [\n        {\"$ref\": \"#/definitions/link_url\"},\n        {\"description\": \"A url, scene1_section_title_text links to this\"}\n      ]\n    },\n    \"scene2_title\": {\n      \"allOf\": [\n        {\"$ref\": \"#/definitions/plainText\"},\n        {\"description\": \"Title displayed before text in scene 2. Should be plain text.\"}\n      ]\n    },\n    \"scene2_text\": {\n      \"allOf\": [\n        {\"$ref\": \"#/definitions/richText\"},\n        {\"description\": \"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br\"}\n      ]\n    },\n    \"scene1_icon\": {\n      \"type\": \"string\",\n      \"description\": \"Snippet icon. 64x64px. SVG or PNG preferred.\"\n    },\n    \"scene1_icon_dark_theme\": {\n      \"type\": \"string\",\n      \"description\": \"Snippet icon. Dark theme variant. 64x64px. SVG or PNG preferred.\"\n    },\n    \"scene1_title_icon\": {\n      \"type\": \"string\",\n      \"description\": \"Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale.\"\n    },\n    \"scene1_title_icon_dark_theme\": {\n      \"type\": \"string\",\n      \"description\": \"Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale.\"\n    },\n    \"scene2_email_placeholder_text\": {\n      \"type\": \"string\",\n      \"description\": \"Value to show while input is empty.\",\n      \"default\": \"Your email here\"\n    },\n    \"scene2_button_label\": {\n      \"type\": \"string\",\n      \"description\": \"Label for form submit button\",\n      \"default\": \"Sign me up\"\n    },\n    \"scene2_dismiss_button_text\": {\n      \"type\": \"string\",\n      \"description\": \"Label for the dismiss button when the sign-up form is expanded.\",\n      \"default\": \"Dismiss\"\n    },\n    \"hidden_inputs\": {\n      \"type\": \"object\",\n      \"description\": \"Each entry represents a hidden input, key is used as value for the name property.\",\n      \"properties\": {\n        \"action\": {\n          \"type\": \"string\",\n          \"enum\": [\"email\"]\n        },\n        \"context\": {\n          \"type\": \"string\",\n          \"enum\": [\"fx_desktop_v3\"]\n        },\n        \"entrypoint\": {\n          \"type\": \"string\",\n          \"enum\": [\"snippets\"]\n        },\n        \"utm_content\": {\n          \"type\": \"number\",\n          \"description\": \"Firefox version number\"\n        },\n        \"utm_source\": {\n          \"type\": \"string\",\n          \"enum\": [\"snippet\"]\n        },\n        \"utm_campaign\": {\n          \"type\": \"string\",\n          \"description\": \"(fxa) Value to pass through to GA as utm_campaign.\"\n        },\n        \"utm_term\": {\n          \"type\": \"string\",\n          \"description\": \"(fxa) Value to pass through to GA as utm_term.\"\n        },\n        \"additionalProperties\": false\n      }\n    },\n    \"scene1_button_label\": {\n      \"allOf\": [\n        {\"$ref\": \"#/definitions/plainText\"},\n        {\"description\": \"Text for a button next to main snippet text that links to button_url. Requires button_url.\"}\n      ],\n      \"default\": \"Learn more\"\n    },\n    \"scene1_button_color\": {\n      \"type\": \"string\",\n      \"description\": \"The text color of the button. Valid CSS color.\"\n    },\n    \"scene1_button_background_color\": {\n      \"type\": \"string\",\n      \"description\": \"The background color of the button. Valid CSS color.\"\n    },\n    \"retry_button_label\": {\n      \"allOf\": [\n        {\"$ref\": \"#/definitions/plainText\"},\n        {\"description\": \"Text for the button in the event of a submission error/failure.\"}\n      ],\n      \"default\": \"Try again\"\n    },\n    \"do_not_autoblock\": {\n      \"type\": \"boolean\",\n      \"description\": \"Used to prevent blocking the snippet after the CTA (link or button) has been clicked\",\n      \"default\": false\n    },\n    \"utm_campaign\": {\n      \"type\": \"string\",\n      \"description\": \"(fxa) Value to pass through to GA as utm_campaign.\"\n    },\n    \"utm_term\": {\n      \"type\": \"string\",\n      \"description\": \"(fxa) Value to pass through to GA as utm_term.\"\n    },\n    \"links\": {\n      \"additionalProperties\": {\n        \"url\": {\n          \"allOf\": [\n            {\"$ref\": \"#/definitions/link_url\"},\n            {\"description\": \"The url where the link points to.\"}\n          ]\n        },\n        \"metric\": {\n          \"type\": \"string\",\n          \"description\": \"Custom event name sent with telemetry event.\"\n        }\n      }\n    }\n  },\n  \"additionalProperties\": false,\n  \"required\": [\"scene1_text\", \"scene2_text\", \"scene1_button_label\"],\n  \"dependencies\": {\n    \"scene1_button_color\": [\"scene1_button_label\"],\n    \"scene1_button_background_color\": [\"scene1_button_label\"]\n  }\n}\n\n"
  },
  {
    "path": "content-src/asrouter/templates/FirstRun/FirstRun.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport React from \"react\";\nimport { Interrupt } from \"./Interrupt\";\nimport { Triplets } from \"./Triplets\";\nimport { BASE_PARAMS } from \"./addUtmParams\";\n\n// Note: should match the transition time on .trailheadCards in _Trailhead.scss\nconst TRANSITION_LENGTH = 500;\n\nexport const FLUENT_FILES = [\n  \"branding/brand.ftl\",\n  \"browser/branding/brandings.ftl\",\n  \"browser/branding/sync-brand.ftl\",\n  \"browser/newtab/onboarding.ftl\",\n];\n\nexport const helpers = {\n  selectInterruptAndTriplets(message = {}, interruptCleared) {\n    const hasInterrupt =\n      interruptCleared === true ? false : Boolean(message.content);\n    const hasTriplets = Boolean(message.bundle && message.bundle.length);\n    // Allow 1) falsy to not render a header 2) default welcome 3) custom header\n\n    const tripletsHeaderId =\n      message.tripletsHeaderId === undefined\n        ? \"onboarding-welcome-header\"\n        : message.tripletsHeaderId;\n    let UTMTerm = message.utm_term || \"\";\n\n    UTMTerm =\n      message.utm_term && message.trailheadTriplet\n        ? `${message.utm_term}-${message.trailheadTriplet}`\n        : UTMTerm;\n\n    return {\n      hasTriplets,\n      hasInterrupt,\n      interrupt: hasInterrupt ? message : null,\n      triplets: hasTriplets ? message.bundle : null,\n      tripletsHeaderId,\n      UTMTerm,\n    };\n  },\n\n  addFluent(document) {\n    FLUENT_FILES.forEach(file => {\n      const link = document.head.appendChild(document.createElement(\"link\"));\n      link.href = file;\n      link.rel = \"localization\";\n    });\n  },\n};\n\nexport class FirstRun extends React.PureComponent {\n  constructor(props) {\n    super(props);\n\n    this.didLoadFlowParams = false;\n\n    this.state = {\n      prevMessage: undefined,\n\n      hasInterrupt: false,\n      hasTriplets: false,\n\n      interrupt: undefined,\n      triplets: undefined,\n      tripletsHeaderId: \"\",\n\n      isInterruptVisible: false,\n      isTripletsContainerVisible: false,\n      isTripletsContentVisible: false,\n\n      UTMTerm: \"\",\n\n      flowParams: undefined,\n    };\n\n    this.closeInterrupt = this.closeInterrupt.bind(this);\n    this.closeTriplets = this.closeTriplets.bind(this);\n\n    helpers.addFluent(this.props.document);\n    // Update utm campaign parameters by appending channel for\n    // differentiating campaign in amplitude\n    if (this.props.appUpdateChannel) {\n      BASE_PARAMS.utm_campaign += `-${this.props.appUpdateChannel}`;\n    }\n  }\n\n  static getDerivedStateFromProps(props, state) {\n    const { message, interruptCleared } = props;\n    const cardIds =\n      message &&\n      message.bundle &&\n      message.bundle.map(card => card.id).join(\",\");\n    if (\n      interruptCleared !== state.prevInterruptCleared ||\n      (message && message.id !== state.prevMessageId) ||\n      cardIds !== state.prevCardIds\n    ) {\n      const {\n        hasTriplets,\n        hasInterrupt,\n        interrupt,\n        triplets,\n        tripletsHeaderId,\n        UTMTerm,\n      } = helpers.selectInterruptAndTriplets(message, interruptCleared);\n\n      return {\n        prevMessageId: message.id,\n        prevInterruptCleared: interruptCleared,\n        prevCardIds: cardIds,\n\n        hasInterrupt,\n        hasTriplets,\n\n        interrupt,\n        triplets,\n        tripletsHeaderId,\n\n        isInterruptVisible: hasInterrupt,\n        isTripletsContainerVisible: hasTriplets,\n        isTripletsContentVisible: !(hasInterrupt || !hasTriplets),\n\n        UTMTerm,\n      };\n    }\n    return null;\n  }\n\n  async fetchFlowParams() {\n    const { fxaEndpoint, fetchFlowParams } = this.props;\n    const { UTMTerm } = this.state;\n    if (fxaEndpoint && UTMTerm && !this.didLoadFlowParams) {\n      this.didLoadFlowParams = true;\n      const flowParams = await fetchFlowParams({\n        ...BASE_PARAMS,\n        entrypoint: \"activity-stream-firstrun\",\n        form_type: \"email\",\n        utm_term: UTMTerm,\n      });\n      this.setState({ flowParams });\n    }\n  }\n\n  removeHideMain() {\n    if (!this.state.hasInterrupt) {\n      // We need to remove hide-main since we should show it underneath everything that has rendered\n      this.props.document.body.classList.remove(\"hide-main\", \"welcome\");\n    }\n  }\n\n  componentDidMount() {\n    this.fetchFlowParams();\n    this.removeHideMain();\n  }\n\n  componentDidUpdate() {\n    // In case we didn't have FXA info immediately, try again when we receive it.\n    this.fetchFlowParams();\n    this.removeHideMain();\n  }\n\n  closeInterrupt() {\n    this.setState(prevState => ({\n      isInterruptVisible: false,\n      isTripletsContainerVisible: prevState.hasTriplets,\n      isTripletsContentVisible: prevState.hasTriplets,\n    }));\n  }\n\n  closeTriplets() {\n    this.setState({ isTripletsContainerVisible: false });\n\n    // Closing triplets should prevent any future extended triplets from showing up\n    setTimeout(() => {\n      this.props.onBlockById(\"EXTENDED_TRIPLETS_1\");\n    }, TRANSITION_LENGTH);\n  }\n\n  render() {\n    const { props } = this;\n    const {\n      sendUserActionTelemetry,\n      fxaEndpoint,\n      dispatch,\n      executeAction,\n    } = props;\n\n    const {\n      interrupt,\n      triplets,\n      tripletsHeaderId,\n      isInterruptVisible,\n      isTripletsContainerVisible,\n      isTripletsContentVisible,\n      hasTriplets,\n      UTMTerm,\n      flowParams,\n    } = this.state;\n\n    return (\n      <>\n        {isInterruptVisible ? (\n          <Interrupt\n            document={props.document}\n            cards={triplets}\n            message={interrupt}\n            onNextScene={this.closeInterrupt}\n            UTMTerm={UTMTerm}\n            sendUserActionTelemetry={sendUserActionTelemetry}\n            executeAction={executeAction}\n            dispatch={dispatch}\n            flowParams={flowParams}\n            onDismiss={this.closeInterrupt}\n            fxaEndpoint={fxaEndpoint}\n            onBlockById={props.onBlockById}\n          />\n        ) : null}\n        {hasTriplets ? (\n          <Triplets\n            document={props.document}\n            cards={triplets}\n            headerId={tripletsHeaderId}\n            showCardPanel={isTripletsContainerVisible}\n            showContent={isTripletsContentVisible}\n            hideContainer={this.closeTriplets}\n            sendUserActionTelemetry={sendUserActionTelemetry}\n            UTMTerm={`${UTMTerm}-card`}\n            flowParams={flowParams}\n            onAction={executeAction}\n            onBlockById={props.onBlockById}\n          />\n        ) : null}\n      </>\n    );\n  }\n}\n"
  },
  {
    "path": "content-src/asrouter/templates/FirstRun/Interrupt.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport React from \"react\";\nimport { Trailhead } from \"../Trailhead/Trailhead\";\nimport { ReturnToAMO } from \"../ReturnToAMO/ReturnToAMO\";\nimport { FullPageInterrupt } from \"../FullPageInterrupt/FullPageInterrupt\";\nimport { LocalizationProvider } from \"fluent-react\";\nimport { generateBundles } from \"../../rich-text-strings\";\n\nexport class Interrupt extends React.PureComponent {\n  render() {\n    const {\n      cards,\n      onDismiss,\n      onNextScene,\n      message,\n      sendUserActionTelemetry,\n      executeAction,\n      dispatch,\n      fxaEndpoint,\n      UTMTerm,\n      flowParams,\n    } = this.props;\n\n    switch (message.template) {\n      case \"return_to_amo_overlay\":\n        return (\n          <LocalizationProvider\n            bundles={generateBundles({ amo_html: message.content.text })}\n          >\n            <ReturnToAMO\n              {...message}\n              document={this.props.document}\n              UISurface=\"NEWTAB_OVERLAY\"\n              onBlock={onDismiss}\n              onAction={executeAction}\n              sendUserActionTelemetry={sendUserActionTelemetry}\n            />\n          </LocalizationProvider>\n        );\n      case \"full_page_interrupt\":\n        return (\n          <FullPageInterrupt\n            document={this.props.document}\n            cards={cards}\n            message={message}\n            onBlock={onDismiss}\n            onAction={executeAction}\n            dispatch={dispatch}\n            fxaEndpoint={fxaEndpoint}\n            sendUserActionTelemetry={sendUserActionTelemetry}\n            UTMTerm={UTMTerm}\n            flowParams={flowParams}\n            onBlockById={this.props.onBlockById}\n          />\n        );\n      case \"trailhead\":\n        return (\n          <Trailhead\n            document={this.props.document}\n            message={message}\n            onNextScene={onNextScene}\n            onAction={executeAction}\n            sendUserActionTelemetry={sendUserActionTelemetry}\n            dispatch={dispatch}\n            fxaEndpoint={fxaEndpoint}\n            UTMTerm={UTMTerm}\n            flowParams={flowParams}\n          />\n        );\n      default:\n        throw new Error(`${message.template} is not a valid FirstRun message`);\n    }\n  }\n}\n"
  },
  {
    "path": "content-src/asrouter/templates/FirstRun/Triplets.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport React from \"react\";\nimport { OnboardingCard } from \"../../templates/OnboardingMessage/OnboardingMessage\";\nimport { addUtmParams } from \"./addUtmParams\";\n\nexport class Triplets extends React.PureComponent {\n  constructor(props) {\n    super(props);\n    this.onCardAction = this.onCardAction.bind(this);\n    this.onHideContainer = this.onHideContainer.bind(this);\n  }\n\n  componentWillMount() {\n    global.document.body.classList.add(\"inline-onboarding\");\n  }\n\n  componentWillUnmount() {\n    this.props.document.body.classList.remove(\"inline-onboarding\");\n  }\n\n  onCardAction(action, message) {\n    let actionUpdates = {};\n    const { flowParams, UTMTerm } = this.props;\n\n    if (action.type === \"OPEN_URL\") {\n      let url = new URL(action.data.args);\n      addUtmParams(url, UTMTerm);\n\n      if (action.addFlowParams) {\n        url.searchParams.append(\"device_id\", flowParams.deviceId);\n        url.searchParams.append(\"flow_id\", flowParams.flowId);\n        url.searchParams.append(\"flow_begin_time\", flowParams.flowBeginTime);\n      }\n\n      actionUpdates = { data: { ...action.data, args: url.toString() } };\n    }\n\n    this.props.onAction({ ...action, ...actionUpdates });\n    // Only block if message is in dynamic triplets experiment\n    if (message.blockOnClick) {\n      this.props.onBlockById(message.id, { preloadedOnly: true });\n    }\n  }\n\n  onHideContainer() {\n    const { sendUserActionTelemetry, cards, hideContainer } = this.props;\n    hideContainer();\n    sendUserActionTelemetry({\n      event: \"DISMISS\",\n      id: \"onboarding-cards\",\n      message_id: cards.map(m => m.id).join(\",\"),\n      action: \"onboarding_user_event\",\n    });\n  }\n\n  render() {\n    const {\n      cards,\n      headerId,\n      showCardPanel,\n      showContent,\n      sendUserActionTelemetry,\n    } = this.props;\n    return (\n      <div\n        className={`trailheadCards ${showCardPanel ? \"expanded\" : \"collapsed\"}`}\n      >\n        <div className=\"trailheadCardsInner\" aria-hidden={!showContent}>\n          {headerId && <h1 data-l10n-id={headerId} />}\n          <div className={`trailheadCardGrid${showContent ? \" show\" : \"\"}`}>\n            {cards.map(card => (\n              <OnboardingCard\n                key={card.id}\n                message={card}\n                className=\"trailheadCard\"\n                sendUserActionTelemetry={sendUserActionTelemetry}\n                onAction={this.onCardAction}\n                UISurface=\"TRAILHEAD\"\n                {...card}\n              />\n            ))}\n          </div>\n          {showCardPanel && (\n            <button\n              className=\"icon icon-dismiss\"\n              onClick={this.onHideContainer}\n              data-l10n-id=\"onboarding-cards-dismiss\"\n            />\n          )}\n        </div>\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "content-src/asrouter/templates/FirstRun/addUtmParams.js",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\n/**\n * BASE_PARAMS keys/values can be modified from outside this file\n */\nexport const BASE_PARAMS = {\n  utm_source: \"activity-stream\",\n  utm_campaign: \"firstrun\",\n  utm_medium: \"referral\",\n};\n\n/**\n * Takes in a url as a string or URL object and returns a URL object with the\n * utm_* parameters added to it. If a URL object is passed in, the paraemeters\n * are added to it (the return value can be ignored in that case as it's the\n * same object).\n */\nexport function addUtmParams(url, utmTerm) {\n  let returnUrl = url;\n  if (typeof returnUrl === \"string\") {\n    returnUrl = new URL(url);\n  }\n  Object.keys(BASE_PARAMS).forEach(key => {\n    returnUrl.searchParams.append(key, BASE_PARAMS[key]);\n  });\n  returnUrl.searchParams.append(\"utm_term\", utmTerm);\n  return returnUrl;\n}\n"
  },
  {
    "path": "content-src/asrouter/templates/FullPageInterrupt/FullPageInterrupt.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport { addUtmParams } from \"../FirstRun/addUtmParams\";\nimport { FxASignupForm } from \"../../components/FxASignupForm/FxASignupForm\";\nimport { OnboardingCard } from \"../../templates/OnboardingMessage/OnboardingMessage\";\nimport React from \"react\";\n\nexport const FxAccounts = ({\n  document,\n  content,\n  dispatch,\n  fxaEndpoint,\n  flowParams,\n  removeOverlay,\n  url,\n  UTMTerm,\n}) => (\n  <React.Fragment>\n    <div\n      className=\"fullpage-left-section\"\n      aria-labelledby=\"fullpage-left-title\"\n      aria-describedby=\"fullpage-left-content\"\n    >\n      <h1\n        id=\"fullpage-left-title\"\n        className=\"fullpage-left-title\"\n        data-l10n-id=\"onboarding-welcome-body\"\n      />\n      <p\n        id=\"fullpage-left-content\"\n        className=\"fullpage-left-content\"\n        data-l10n-id=\"onboarding-benefit-products-text\"\n      />\n      <p\n        className=\"fullpage-left-content\"\n        data-l10n-id=\"onboarding-benefit-privacy-text\"\n      />\n      <a\n        className=\"fullpage-left-link\"\n        href={addUtmParams(url, UTMTerm)}\n        target=\"_blank\"\n        rel=\"noopener noreferrer\"\n        data-l10n-id=\"onboarding-welcome-learn-more\"\n      />\n      <div className=\"fullpage-icon fx-systems-icons\" />\n    </div>\n    <div className=\"fullpage-form\">\n      <FxASignupForm\n        document={document}\n        content={content}\n        dispatch={dispatch}\n        fxaEndpoint={fxaEndpoint}\n        UTMTerm={UTMTerm}\n        flowParams={flowParams}\n        onClose={removeOverlay}\n        showSignInLink={true}\n      />\n    </div>\n  </React.Fragment>\n);\n\nexport const FxCards = ({ cards, onCardAction, sendUserActionTelemetry }) => (\n  <React.Fragment>\n    {cards.map(card => (\n      <OnboardingCard\n        key={card.id}\n        message={card}\n        className=\"trailheadCard\"\n        sendUserActionTelemetry={sendUserActionTelemetry}\n        onAction={onCardAction}\n        UISurface=\"TRAILHEAD\"\n        {...card}\n      />\n    ))}\n  </React.Fragment>\n);\n\nexport class FullPageInterrupt extends React.PureComponent {\n  constructor(props) {\n    super(props);\n    this.removeOverlay = this.removeOverlay.bind(this);\n    this.onCardAction = this.onCardAction.bind(this);\n  }\n\n  componentWillMount() {\n    global.document.body.classList.add(\"trailhead-fullpage\");\n  }\n\n  componentDidMount() {\n    // Hide the page content from screen readers while the full page interrupt is open\n    this.props.document\n      .getElementById(\"root\")\n      .setAttribute(\"aria-hidden\", \"true\");\n  }\n\n  removeOverlay() {\n    window.removeEventListener(\"visibilitychange\", this.removeOverlay);\n    document.body.classList.remove(\"hide-main\", \"trailhead-fullpage\");\n    // Re-enable the document for screen readers\n    this.props.document\n      .getElementById(\"root\")\n      .setAttribute(\"aria-hidden\", \"false\");\n\n    this.props.onBlock();\n    document.body.classList.remove(\"welcome\");\n  }\n\n  onCardAction(action, message) {\n    let actionUpdates = {};\n    const { flowParams, UTMTerm } = this.props;\n\n    if (action.type === \"OPEN_URL\") {\n      let url = new URL(action.data.args);\n      addUtmParams(url, UTMTerm);\n\n      if (action.addFlowParams) {\n        url.searchParams.append(\"device_id\", flowParams.deviceId);\n        url.searchParams.append(\"flow_id\", flowParams.flowId);\n        url.searchParams.append(\"flow_begin_time\", flowParams.flowBeginTime);\n      }\n\n      actionUpdates = { data: { ...action.data, args: url.toString() } };\n    }\n\n    this.props.onAction({ ...action, ...actionUpdates });\n    // Only block if message is in dynamic triplets experiment\n    if (message.blockOnClick) {\n      this.props.onBlockById(message.id, { preloadedOnly: true });\n    }\n    this.removeOverlay();\n  }\n\n  render() {\n    const { props } = this;\n    const { content } = props.message;\n    const cards = (\n      <FxCards\n        cards={props.cards}\n        onCardAction={this.onCardAction}\n        sendUserActionTelemetry={props.sendUserActionTelemetry}\n      />\n    );\n\n    const accounts = (\n      <FxAccounts\n        document={props.document}\n        content={content}\n        dispatch={props.dispatch}\n        fxaEndpoint={props.fxaEndpoint}\n        flowParams={props.flowParams}\n        removeOverlay={this.removeOverlay}\n        url={content.learn.url}\n        UTMTerm={props.UTMTerm}\n      />\n    );\n\n    // By default we show accounts section on top and\n    // cards section in bottom half of the full page interrupt\n    const cardsFirst = content && content.className === \"fullPageCardsAtTop\";\n    const firstContainerClassName = [\n      \"container\",\n      content && content.className,\n    ].join(\" \");\n    return (\n      <div className=\"fullpage-wrapper\">\n        <div className=\"fullpage-icon brand-logo\" />\n        <h1\n          className=\"welcome-title\"\n          data-l10n-id=\"onboarding-welcome-header\"\n        />\n        <h2\n          className=\"welcome-subtitle\"\n          data-l10n-id=\"onboarding-fullpage-welcome-subheader\"\n        />\n        <div className={firstContainerClassName}>\n          {cardsFirst ? cards : accounts}\n        </div>\n        <div className=\"section-divider\" />\n        <div className=\"container\">{cardsFirst ? accounts : cards}</div>\n      </div>\n    );\n  }\n}\n\nFullPageInterrupt.defaultProps = {\n  flowParams: { deviceId: \"\", flowId: \"\", flowBeginTime: \"\" },\n};\n"
  },
  {
    "path": "content-src/asrouter/templates/FullPageInterrupt/_FullPageInterrupt.scss",
    "content": ".activity-stream {\n  &.welcome {\n    overflow: hidden;\n  }\n\n  &:not(.welcome) {\n    .fullpage-wrapper {\n      display: none;\n    }\n  }\n}\n\n.fullpage-wrapper {\n  $responsive-breakpoint: 975px;\n  $responsive-width: 300px;\n  $header-size: 36px;\n  $form-text-size: 16px;\n\n  align-content: center;\n  display: flex;\n  flex-direction: column;\n  overflow-x: auto;\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 100vw;\n  height: 100vh;\n  z-index: 21000;\n  background-color: $ghost-white;\n\n  + div {\n    opacity: 0;\n  }\n\n  .fullpage-icon {\n    background-position-x: left;\n    background-repeat: no-repeat;\n    background-size: contain;\n\n    &:dir(rtl) {\n      background-position-x: right;\n    }\n\n    @media screen and (max-width: $responsive-breakpoint) {\n      background-position: center;\n    }\n  }\n\n  .brand-logo {\n    background-image: url('chrome://branding/content/about-logo.png');\n    margin: 20px 10px 10px 20px;\n    padding-bottom: 50px;\n  }\n\n  .welcome-title,\n  .welcome-subtitle {\n    align-self: center;\n    margin: 0;\n    @media screen and (max-width: $responsive-breakpoint) {\n      text-align: center;\n    }\n  }\n\n  .welcome-title {\n    color: $trailhead-purple-80;\n    font-size: 46px;\n    font-weight: 600;\n    line-height: 62px;\n  }\n\n  .welcome-subtitle {\n    color: $trailhead-violet;\n    font-size: 20px;\n    line-height: 27px;\n  }\n\n  .container {\n    display: flex;\n    align-self: center;\n    padding: 50px 0;\n\n    @media screen and (max-width: $responsive-breakpoint) {\n      flex-direction: column;\n      width: $responsive-width;\n      text-align: center;\n    }\n  }\n\n  .fullpage-left-section {\n    position: relative;\n    width: 538px;\n    font-size: 18px;\n    line-height: 30px;\n\n    @media screen and (max-width: $responsive-breakpoint) {\n      width: $responsive-width;\n    }\n\n    .fullpage-left-content {\n      color: $grey-60;\n      display: inline;\n      margin: 0;\n      margin-inline-end: 2px;\n    }\n\n    .fullpage-left-link {\n      color: $blue-60;\n      display: block;\n      text-decoration: underline;\n      margin-bottom: 30px;\n\n      &:hover,\n      &:active,\n      &:focus {\n        color: $blue-60;\n      }\n    }\n\n    .fullpage-left-title {\n      margin: 0;\n      color: $trailhead-purple-80;\n      font-size: $header-size;\n      line-height: 48px;\n    }\n\n    .fx-systems-icons {\n      height: 33px;\n      display: block;\n      background-image: url('#{$image-path}trailhead/firefox-systems.png');\n      margin-bottom: 20px;\n    }\n  }\n\n  .fullpage-form {\n    position: relative;\n    text-align: center;\n    margin-inline-start: $header-size;\n\n    @media screen and (max-width: $responsive-breakpoint) {\n      margin-inline-start: 0;\n    }\n\n    .fxaSignupForm {\n      width: 356px;\n      padding: 25px;\n      box-shadow: 0 0 16px 0 $black-15;\n      border-radius: 6px;\n      background: $white;\n    }\n\n    .fxa-terms {\n      margin: 4px 0 20px;\n\n      a,\n      & {\n        color: $grey-60;\n        font-size: 12px;\n        line-height: $form-text-size;\n      }\n    }\n\n    .fxa-signin {\n      color: $grey-60;\n      line-height: 30px;\n      opacity: 0.77;\n\n      button {\n        color: $blue-60;\n      }\n    }\n\n    h3 {\n      color: $trailhead-purple-80;\n      font-weight: 400;\n      font-size: $header-size;\n      line-height: $header-size;\n      margin: 0;\n      padding: 8px;\n    }\n\n    h3 + p {\n      color: $grey-60;\n      font-size: $form-text-size;\n      line-height: 20px;\n      opacity: 0.77;\n    }\n\n    input {\n      background: $white;\n      border: 1px solid $grey-30;\n      border-radius: 2px;\n\n      &:hover {\n        border-color: $grey-50;\n      }\n\n      &.invalid {\n        border-color: $red-60;\n      }\n    }\n\n    button {\n      color: $white;\n      font-size: $form-text-size;\n\n      &:focus {\n        outline: dotted 1px $grey-50;\n      }\n    }\n  }\n\n  .section-divider::after {\n    content: '';\n    display: block;\n    border-bottom: 0.5px solid $grey-30;\n  }\n\n  .trailheadCard {\n    box-shadow: none;\n    background: none;\n    text-align: center;\n    width: 320px;\n    padding: 18px;\n\n    .onboardingTitle {\n      color: $grey-90;\n    }\n\n    .onboardingText {\n      font-weight: normal;\n      color: $grey-60;\n      margin-top: 4px;\n    }\n\n    .onboardingButton {\n      color: $grey-60;\n      background: $grey-90-10;\n\n      &:focus,\n      &:hover {\n        background: $grey-90-20;\n      }\n\n      &:active {\n        background: $grey-90-30;\n      }\n    }\n\n    .onboardingMessageImage {\n      height: 112px;\n      width: 154px;\n    }\n\n    @media screen and (max-width: $responsive-breakpoint) {\n      width: $responsive-width;\n    }\n  }\n}\n"
  },
  {
    "path": "content-src/asrouter/templates/NewsletterSnippet/NewsletterSnippet.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport React from \"react\";\nimport schema from \"./NewsletterSnippet.schema.json\";\nimport { SubmitFormSnippet } from \"../SubmitFormSnippet/SubmitFormSnippet.jsx\";\n\nexport const NewsletterSnippet = props => {\n  const extendedContent = {\n    scene1_button_label: schema.properties.scene1_button_label.default,\n    retry_button_label: schema.properties.retry_button_label.default,\n    scene2_email_placeholder_text:\n      schema.properties.scene2_email_placeholder_text.default,\n    scene2_button_label: schema.properties.scene2_button_label.default,\n    scene2_dismiss_button_text:\n      schema.properties.scene2_dismiss_button_text.default,\n    scene2_newsletter: schema.properties.scene2_newsletter.default,\n    ...props.content,\n    hidden_inputs: {\n      newsletters:\n        props.content.scene2_newsletter ||\n        schema.properties.scene2_newsletter.default,\n      fmt: schema.properties.hidden_inputs.properties.fmt.default,\n      lang: props.content.locale || schema.properties.locale.default,\n      source_url: `https://snippets.mozilla.com/show/${props.id}`,\n      ...props.content.hidden_inputs,\n    },\n  };\n\n  return (\n    <SubmitFormSnippet\n      {...props}\n      content={extendedContent}\n      form_action={\"https://basket.mozilla.org/subscribe.json\"}\n      form_method=\"POST\"\n    />\n  );\n};\n"
  },
  {
    "path": "content-src/asrouter/templates/NewsletterSnippet/NewsletterSnippet.schema.json",
    "content": "{\n  \"title\": \"NewsletterSnippet\",\n  \"description\": \"A snippet template for send to device mobile download\",\n  \"version\": \"1.2.0\",\n  \"type\": \"object\",\n  \"definitions\": {\n    \"plainText\": {\n      \"description\": \"Plain text (no HTML allowed)\",\n      \"type\": \"string\"\n    },\n    \"richText\": {\n      \"description\": \"Text with HTML subset allowed: i, b, u, strong, em, br\",\n      \"type\": \"string\"\n    },\n    \"link_url\": {\n      \"description\": \"Target for links or buttons\",\n      \"type\": \"string\",\n      \"format\": \"uri\"\n    }\n  },\n  \"properties\": {\n    \"locale\": {\n      \"type\": \"string\",\n      \"description\": \"Two to five character string for the locale code\",\n      \"default\": \"en-US\"\n    },\n    \"scene1_title\": {\n      \"allof\": [\n        {\"$ref\": \"#/definitions/plainText\"},\n        {\"description\": \"snippet title displayed before snippet text\"}\n      ]\n    },\n    \"scene1_text\": {\n      \"allOf\": [\n        {\"$ref\": \"#/definitions/richText\"},\n        {\"description\": \"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br\"}\n      ]\n    },\n    \"scene1_section_title_icon\": {\n      \"type\": \"string\",\n      \"description\": \"Section title icon for scene 1. 16x16px. SVG or PNG preferred. scene1_section_title_text must also be specified to display.\"\n    },\n    \"scene1_section_title_icon_dark_theme\": {\n      \"type\": \"string\",\n      \"description\": \"Section title icon for scene 1, dark theme variant. 16x16px. SVG or PNG preferred. scene1_section_title_text must also be specified to display.\"\n    },\n    \"scene1_section_title_text\": {\n      \"type\": \"string\",\n      \"description\": \"Section title text for scene 1. scene1_section_title_icon must also be specified to display.\"\n    },\n    \"scene1_section_title_url\": {\n      \"allOf\": [\n        {\"$ref\": \"#/definitions/link_url\"},\n        {\"description\": \"A url, scene1_section_title_text links to this\"}\n      ]\n    },\n    \"scene2_title\": {\n      \"allOf\": [\n        {\"$ref\": \"#/definitions/plainText\"},\n        {\"description\": \"Title displayed before text in scene 2. Should be plain text.\"}\n      ]\n    },\n    \"scene2_text\": {\n      \"allOf\": [\n        {\"$ref\": \"#/definitions/richText\"},\n        {\"description\": \"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br\"}\n      ]\n    },\n    \"scene1_icon\": {\n      \"type\": \"string\",\n      \"description\": \"Snippet icon. 64x64px. SVG or PNG preferred.\"\n    },\n    \"scene1_icon_dark_theme\": {\n      \"type\": \"string\",\n      \"description\": \"Snippet icon. Dark theme variant. 64x64px. SVG or PNG preferred.\"\n    },\n    \"scene1_title_icon\": {\n      \"type\": \"string\",\n      \"description\": \"Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale.\"\n    },\n    \"scene1_title_icon_dark_theme\": {\n      \"type\": \"string\",\n      \"description\": \"Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale.\"\n    },\n    \"scene2_email_placeholder_text\": {\n      \"type\": \"string\",\n      \"description\": \"Value to show while input is empty.\",\n      \"default\": \"Your email here\"\n    },\n    \"scene2_button_label\": {\n      \"type\": \"string\",\n      \"description\": \"Label for form submit button\",\n      \"default\": \"Sign me up\"\n    },\n    \"scene2_privacy_html\": {\n      \"type\": \"string\",\n      \"description\": \"(send to device) Html for disclaimer and link underneath input box.\"\n    },\n    \"scene2_dismiss_button_text\": {\n      \"type\": \"string\",\n      \"description\": \"Label for the dismiss button when the sign-up form is expanded.\",\n      \"default\": \"Dismiss\"\n    },\n    \"hidden_inputs\": {\n      \"type\": \"object\",\n      \"description\": \"Each entry represents a hidden input, key is used as value for the name property.\",\n      \"properties\": {\n        \"fmt\": {\n          \"type\": \"string\",\n          \"description\": \"\",\n          \"default\": \"H\"\n        }\n      }\n    },\n    \"scene1_button_label\": {\n      \"allOf\": [\n        {\"$ref\": \"#/definitions/plainText\"},\n        {\"description\": \"Text for a button next to main snippet text that links to button_url. Requires button_url.\"}\n      ],\n      \"default\": \"Learn more\"\n    },\n    \"scene1_button_color\": {\n      \"type\": \"string\",\n      \"description\": \"The text color of the button. Valid CSS color.\"\n    },\n    \"scene1_button_background_color\": {\n      \"type\": \"string\",\n      \"description\": \"The background color of the button. Valid CSS color.\"\n    },\n    \"retry_button_label\": {\n      \"allOf\": [\n        {\"$ref\": \"#/definitions/plainText\"},\n        {\"description\": \"Text for the button in the event of a submission error/failure.\"}\n      ],\n      \"default\": \"Try again\"\n    },\n    \"do_not_autoblock\": {\n      \"type\": \"boolean\",\n      \"description\": \"Used to prevent blocking the snippet after the CTA (link or button) has been clicked\",\n      \"default\": false\n    },\n    \"success_text\": {\n      \"type\": \"string\",\n      \"description\": \"Message shown on successful registration.\"\n    },\n    \"error_text\": {\n      \"type\": \"string\",\n      \"description\": \"Message shown if registration failed.\"\n    },\n    \"scene2_newsletter\": {\n      \"type\": \"string\",\n      \"description\": \"Newsletter/basket id user is subscribing to.\",\n      \"default\": \"mozilla-foundation\"\n    },\n    \"links\": {\n      \"additionalProperties\": {\n        \"url\": {\n          \"allOf\": [\n            {\"$ref\": \"#/definitions/link_url\"},\n            {\"description\": \"The url where the link points to.\"}\n          ]\n        },\n        \"metric\": {\n          \"type\": \"string\",\n          \"description\": \"Custom event name sent with telemetry event.\"\n        }\n      }\n    }\n  },\n  \"additionalProperties\": false,\n  \"required\": [\"scene1_text\", \"scene2_text\", \"scene1_button_label\"],\n  \"dependencies\": {\n    \"scene1_button_color\": [\"scene1_button_label\"],\n    \"scene1_button_background_color\": [\"scene1_button_label\"]\n  }\n}\n\n"
  },
  {
    "path": "content-src/asrouter/templates/OnboardingMessage/OnboardingMessage.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport React from \"react\";\n\nexport class OnboardingCard extends React.PureComponent {\n  constructor(props) {\n    super(props);\n    this.onClick = this.onClick.bind(this);\n  }\n\n  onClick() {\n    const { props } = this;\n    const ping = {\n      event: \"CLICK_BUTTON\",\n      message_id: props.id,\n      id: props.UISurface,\n    };\n    props.sendUserActionTelemetry(ping);\n    props.onAction(props.content.primary_button.action, props.message);\n  }\n\n  render() {\n    const { content } = this.props;\n    const className = this.props.className || \"onboardingMessage\";\n    return (\n      <div className={className}>\n        <div className={`onboardingMessageImage ${content.icon}`} />\n        <div className=\"onboardingContent\">\n          <span>\n            <h2\n              className=\"onboardingTitle\"\n              data-l10n-id={content.title.string_id}\n            />\n            <p\n              className=\"onboardingText\"\n              data-l10n-id={content.text.string_id}\n            />\n          </span>\n          <span className=\"onboardingButtonContainer\">\n            <button\n              data-l10n-id={content.primary_button.label.string_id}\n              className=\"button onboardingButton\"\n              onClick={this.onClick}\n            />\n          </span>\n        </div>\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "content-src/asrouter/templates/OnboardingMessage/OnboardingMessage.schema.json",
    "content": "{\n  \"title\": \"OnboardingMessage\",\n  \"description\": \"A template with a title, icon, button and description. No markup allowed.\",\n  \"version\": \"1.0.0\",\n  \"type\": \"object\",\n  \"properties\": {\n    \"title\": {\n      \"oneOf\": [\n        {\n          \"type\": \"string\",\n          \"description\": \"The message displayed in the title of the onboarding card\"\n        },\n        {\n          \"type\": \"object\",\n          \"properties\": {\n            \"string_id\": {\n              \"type\": \"string\"\n            }\n          },\n          \"required\": [\"string_id\"],\n          \"description\": \"Id of localized string for onboarding card title\"\n        }\n      ],\n      \"description\": \"Id of localized string or message override.\"\n    },\n    \"text\": {\n      \"oneOf\": [\n        {\n          \"type\": \"string\",\n          \"description\": \"The message displayed in the description of the onboarding card\"\n        },\n        {\n          \"type\": \"object\",\n          \"properties\": {\n            \"string_id\": {\n              \"type\": \"string\"\n            },\n            \"args\": {\n              \"type\": \"object\",\n              \"description\": \"An optional argument to pass to the localization module\"\n            }\n          },\n          \"required\": [\"string_id\"],\n          \"description\": \"Id of localized string for onboarding card description\"\n        }\n      ],\n      \"description\": \"Id of localized string or message override.\"\n    },\n    \"icon\": {\n      \"allOf\": [\n        {\n          \"type\": \"string\",\n          \"description\": \"Image associated with the onboarding card\"\n        }\n      ]\n    },\n    \"primary_button\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"label\": {\n          \"oneOf\": [\n            {\n              \"type\": \"string\",\n              \"description\": \"The label of the onboarding messages' action button\"\n            },\n            {\n              \"type\": \"object\",\n              \"properties\": {\n                \"string_id\": {\n                  \"type\": \"string\"\n                }\n              },\n              \"required\": [\"string_id\"],\n              \"description\": \"Id of localized string for onboarding messages' button\"\n            }\n          ],\n          \"description\": \"Id of localized string or message override.\"\n        },\n        \"action\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"type\": {\n              \"type\": \"string\",\n              \"description\": \"Action dispatched by the button.\"\n            },\n            \"data\": {\n              \"properties\": {\n                \"args\": {\n                  \"type\": \"string\",\n                  \"description\": \"Additional parameters for button action, for example which link the button should open.\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"secondary_buttons\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"label\": {\n          \"oneOf\": [\n            {\n              \"type\": \"string\",\n              \"description\": \"The label of the onboarding messages' (optional) secondary action button\"\n            },\n            {\n              \"type\": \"object\",\n              \"properties\": {\n                \"string_id\": {\n                  \"type\": \"string\"\n                }\n              },\n              \"required\": [\"string_id\"],\n              \"description\": \"Id of localized string for onboarding messages' button\"\n            }\n          ],\n          \"description\": \"Id of localized string or message override.\"\n        },\n        \"action\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"type\": {\n              \"type\": \"string\",\n              \"description\": \"Action dispatched by the button.\"\n            },\n            \"data\": {\n              \"properties\": {\n                \"args\": {\n                  \"type\": \"string\",\n                  \"description\": \"Additional parameters for button action, for example which link the button should open.\"\n                }\n              }\n            }\n          }\n        }\n      }\n    }\n  },\n  \"additionalProperties\": true,\n  \"required\": [\"title\", \"text\", \"icon\", \"primary_button\"]\n}\n"
  },
  {
    "path": "content-src/asrouter/templates/OnboardingMessage/ToolbarBadgeMessage.schema.json",
    "content": "{\n  \"title\": \"ToolbarBadgeMessage\",\n  \"description\": \"A template that specifies to which element in the browser toolbar to add a notification.\",\n  \"version\": \"1.1.0\",\n  \"type\": \"object\",\n  \"properties\": {\n    \"target\": {\n      \"type\": \"string\"\n    },\n    \"action\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"id\": {\n          \"type\": \"string\"\n        }\n      },\n      \"additionalProperties\": false,\n      \"required\": [\"id\"],\n      \"description\": \"Optional action to take in addition to showing the notification\"\n    },\n    \"delay\": {\n      \"type\": \"number\",\n      \"description\": \"Optional delay in ms after which to show the notification\"\n    },\n    \"badgeDescription\": {\n      \"type\": \"object\",\n      \"description\": \"This is used in combination with the badged button to offer a text based alternative to the visual badging. Example 'New Feature: What's New'\",\n      \"properties\": {\n        \"string_id\": {\n          \"type\": \"string\",\n          \"description\": \"Fluent string id\"\n        }\n      },\n      \"required\": [\"string_id\"]\n    }\n  },\n  \"additionalProperties\": false,\n  \"required\": [\"target\"]\n}\n"
  },
  {
    "path": "content-src/asrouter/templates/OnboardingMessage/UpdateAction.schema.json",
    "content": "{\n  \"title\": \"UpdateActionMessage\",\n  \"description\": \"A template for messages that execute predetermined actions.\",\n  \"version\": \"1.0.0\",\n  \"type\": \"object\",\n  \"properties\": {\n    \"action\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"id\": {\n          \"type\": \"string\"\n        },\n        \"data\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"url\": {\n              \"type\": \"string\",\n              \"description\": \"URL data to be used as argument to the action\"\n            },\n            \"expireDelta\": {\n              \"type\": \"number\",\n              \"description\": \"Expiration timestamp to be used as argument to the action\"\n            }\n          }\n        },\n        \"description\": \"Additional data provided as argument when executing the action\"\n      },\n      \"additionalProperties\": false,\n      \"description\": \"Optional action to take in addition to showing the notification\"\n    },\n    \"additionalProperties\": false,\n    \"required\": [\"id\", \"action\"]\n  },\n  \"additionalProperties\": false,\n  \"required\": [\"action\"]\n}\n"
  },
  {
    "path": "content-src/asrouter/templates/OnboardingMessage/WhatsNewMessage.schema.json",
    "content": "{\n  \"title\": \"WhatsNewMessage\",\n  \"description\": \"A template for the messages that appear in the What's New panel.\",\n  \"version\": \"1.2.0\",\n  \"type\": \"object\",\n  \"definitions\": {\n    \"localizableText\": {\n      \"oneOf\": [\n        {\n          \"type\": \"string\",\n          \"description\": \"The string to be rendered.\"\n        },\n        {\n          \"type\": \"object\",\n          \"properties\": {\n            \"string_id\": {\n              \"type\": \"string\"\n            }\n          },\n          \"required\": [\"string_id\"],\n          \"description\": \"Id of localized string to be rendered.\"\n        }\n      ]\n    }\n  },\n  \"properties\": {\n    \"layout\": {\n      \"description\": \"Different message layouts\",\n     \"enum\": [\"tracking-protections\"]\n    },\n    \"layout_title_content_variable\": {\n      \"description\": \"Select what profile specific value to show for the current layout.\",\n      \"type\": \"string\"\n    },\n    \"bucket_id\": {\n      \"type\": \"string\",\n      \"description\": \"A bucket identifier for the addon. This is used in order to anonymize telemetry for history-sensitive targeting.\"\n    },\n    \"published_date\": {\n      \"type\": \"integer\",\n      \"description\": \"The date/time (number of milliseconds elapsed since January 1, 1970 00:00:00 UTC) the message was published.\"\n    },\n    \"title\": {\n      \"allOf\": [\n        {\"$ref\": \"#/definitions/localizableText\"},\n        {\"description\": \"Id of localized string or message override of What's New message title\"}\n      ]\n    },\n    \"subtitle\": {\n      \"allOf\": [\n        {\"$ref\": \"#/definitions/localizableText\"},\n        {\"description\": \"Id of localized string or message override of What's New message subtitle\"}\n      ]\n    },\n    \"body\": {\n      \"allOf\": [\n        {\"$ref\": \"#/definitions/localizableText\"},\n        {\"description\": \"Id of localized string or message override of What's New message body\"}\n      ]\n    },\n    \"link_text\": {\n      \"allOf\": [\n        {\"$ref\": \"#/definitions/localizableText\"},\n        {\"description\": \"(optional) Id of localized string or message override of What's New message link text\"}\n      ]\n    },\n    \"cta_url\": {\n      \"description\": \"Target URL for the What's New message.\",\n      \"type\": \"string\",\n      \"format\": \"uri\"\n    },\n    \"cta_type\": {\n      \"description\": \"Type of url open action\",\n      \"enum\": [\"OPEN_URL\", \"OPEN_ABOUT_PAGE\"]\n    },\n    \"icon_url\": {\n      \"description\": \"(optional) URL for the What's New message icon.\",\n      \"type\": \"string\",\n      \"format\": \"uri\"\n    },\n    \"icon_alt\": {\n      \"description\": \"Alt text for image.\",\n      \"type\": \"string\"\n    }\n  },\n  \"additionalProperties\": false,\n  \"required\": [\"published_date\", \"title\", \"body\", \"cta_url\", \"bucket_id\"],\n  \"dependencies\": {\n    \"layout\": [\"layout_title_content_variable\"]\n  }\n}\n"
  },
  {
    "path": "content-src/asrouter/templates/OnboardingMessage/_OnboardingMessage.scss",
    "content": ".onboardingMessage {\n  height: 340px;\n  text-align: center;\n  padding: 13px;\n  font-weight: 200;\n\n  // at 850px, img floats left, content floats right next to it\n  @media(max-width: 850px) {\n    height: 170px;\n    text-align: left;\n    padding: 10px;\n    border-bottom: 1px solid $grey-30;\n    display: flex;\n    margin-bottom: 11px;\n\n    &:last-child {\n      border: 0;\n    }\n\n    .onboardingContent {\n      padding-left: 10px;\n      height: 100%;\n\n      > span > h3 {\n        margin-top: 0;\n        margin-bottom: 4px;\n        font-weight: 400;\n      }\n\n      > span > p {\n        margin-top: 0;\n        line-height: 22px;\n        font-size: 15px;\n      }\n    }\n  }\n\n  @media(max-width: 650px) {\n    height: 250px;\n  }\n\n  .onboardingContent {\n    height: 175px;\n\n    > span > h3 {\n      color: $grey-90;\n      margin-bottom: 8px;\n      font-weight: 400;\n    }\n\n    > span > p {\n      color: $grey-60;\n      margin-top: 0;\n      height: 180px;\n      margin-bottom: 12px;\n      font-size: 15px;\n      line-height: 22px;\n\n      @media(max-width: 650px) {\n        margin-bottom: 0;\n        height: 160px;\n      }\n    }\n  }\n\n  .onboardingButton {\n    background-color: $grey-90-10;\n    border: 0;\n    width: 150px;\n    height: 30px;\n    margin-bottom: 23px;\n    padding: 4px 0 6px;\n    font-size: 15px;\n\n    // at 850px, the button shimmies down and to the right\n    @media(max-width: 850px) {\n      float: right;\n      margin-top: -105px;\n      margin-inline-end: -10px;\n    }\n\n    @media(max-width: 650px) {\n      float: none;\n    }\n\n    &:focus,\n    &.active,\n    &:hover {\n      box-shadow: 0 0 0 5px $grey-30;\n      transition: box-shadow 150ms;\n    }\n  }\n\n\n  &::before {\n    content: '';\n    height: 230px;\n    width: 1px;\n    position: absolute;\n    background-color: $grey-30;\n    margin-top: 40px;\n    margin-inline-start: 215px;\n\n    // at 850px, the line goes from vertical to horizontal\n    @media(max-width: 850px) {\n      content: none;\n    }\n  }\n\n  &:last-child::before {\n    content: none;\n  }\n}\n\n// Also used for Trailhead\n.onboardingMessageImage {\n  height: 112px;\n  width: 120px;\n  background-size: auto 140px;\n  background-position: center center;\n  background-repeat: no-repeat;\n  display: inline-block;\n\n  // Cards will wrap into the next line after this breakpoint\n  @media(max-width: 865px) {\n    height: 75px;\n    min-width: 80px;\n    background-size: 140px;\n  }\n\n  @media (min-width: $break-point-widest) {\n    width: 250px;\n    background-size: auto 140px;\n  }\n\n  &.addons {\n    background-image: url('#{$image-path}illustration-addons@2x.png');\n  }\n\n  &.privatebrowsing {\n    background-image: url('#{$image-path}illustration-privatebrowsing@2x.png');\n  }\n\n  &.screenshots {\n    background-image: url('#{$image-path}illustration-screenshots@2x.png');\n  }\n\n  &.gift {\n    background-image: url('#{$image-path}illustration-gift@2x.png');\n  }\n\n  &.sync {\n    background-image: url('#{$image-path}illustration-sync@2x.png');\n  }\n\n  &.devices {\n    background-image: url('#{$image-path}trailhead/card-illo-devices.svg');\n  }\n\n  &.fbcont {\n    background-image: url('#{$image-path}trailhead/card-illo-fbcont.svg');\n  }\n\n  &.import {\n    background-image: url('#{$image-path}trailhead/card-illo-import.svg');\n  }\n\n  &.ffmonitor {\n    background-image: url('#{$image-path}trailhead/card-illo-ffmonitor.svg');\n  }\n\n  &.ffsend {\n    background-image: url('#{$image-path}trailhead/card-illo-ffsend.svg');\n  }\n\n  &.lockwise {\n    background-image: url('#{$image-path}trailhead/card-illo-lockwise.svg');\n  }\n\n  &.mobile {\n    background-image: url('#{$image-path}trailhead/card-illo-mobile.svg');\n  }\n\n  &.pledge {\n    background-image: url('#{$image-path}trailhead/card-illo-pledge.svg');\n  }\n\n  &.pocket {\n    background-image: url('#{$image-path}trailhead/card-illo-pocket.svg');\n  }\n\n  &.private {\n    background-image: url('#{$image-path}trailhead/card-illo-private.svg');\n  }\n\n  &.sendtab {\n    background-image: url('#{$image-path}trailhead/card-illo-sendtab.svg');\n  }\n\n  &.tracking {\n    background-image: url('#{$image-path}trailhead/card-illo-tracking.svg');\n  }\n}\n"
  },
  {
    "path": "content-src/asrouter/templates/ReturnToAMO/ReturnToAMO.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport React from \"react\";\nimport { RichText } from \"../../components/RichText/RichText\";\n\n// Alt text if available; in the future this should come from the server. See bug 1551711\nconst ICON_ALT_TEXT = \"\";\n\nexport class ReturnToAMO extends React.PureComponent {\n  constructor(props) {\n    super(props);\n    this.onClickAddExtension = this.onClickAddExtension.bind(this);\n    this.onBlockButton = this.onBlockButton.bind(this);\n  }\n\n  componentWillMount() {\n    global.document.body.classList.add(\"amo\");\n  }\n\n  componentDidMount() {\n    this.props.sendUserActionTelemetry({\n      event: \"IMPRESSION\",\n      id: this.props.UISurface,\n    });\n    // Hide the page content from screen readers while the modal is open\n    this.props.document\n      .getElementById(\"root\")\n      .setAttribute(\"aria-hidden\", \"true\");\n  }\n\n  onClickAddExtension() {\n    this.props.onAction(this.props.content.primary_button.action);\n    this.props.sendUserActionTelemetry({\n      event: \"INSTALL\",\n      id: this.props.UISurface,\n    });\n  }\n\n  onBlockButton() {\n    this.props.onBlock();\n    document.body.classList.remove(\"welcome\", \"hide-main\", \"amo\");\n    this.props.sendUserActionTelemetry({\n      event: \"BLOCK\",\n      id: this.props.UISurface,\n    });\n    // Re-enable the document for screen readers\n    this.props.document\n      .getElementById(\"root\")\n      .setAttribute(\"aria-hidden\", \"false\");\n  }\n\n  renderText() {\n    const customElement = (\n      <img\n        src={this.props.content.addon_icon}\n        width=\"20px\"\n        height=\"20px\"\n        alt={ICON_ALT_TEXT}\n      />\n    );\n    return (\n      <RichText\n        customElements={{ icon: customElement }}\n        amo_html={this.props.content.text}\n        localization_id=\"amo_html\"\n      />\n    );\n  }\n\n  render() {\n    const { content } = this.props;\n    return (\n      <div className=\"ReturnToAMOOverlay\">\n        <div>\n          <h2> {content.header} </h2>\n          <div className=\"ReturnToAMOContainer\">\n            <div className=\"ReturnToAMOAddonContents\">\n              <p> {content.title} </p>\n              <div className=\"ReturnToAMOText\">\n                <span> {this.renderText()} </span>\n              </div>\n              <button\n                onClick={this.onClickAddExtension}\n                className=\"puffy blue ReturnToAMOAddExtension\"\n              >\n                {\" \"}\n                <span className=\"icon icon-add\" />{\" \"}\n                {content.primary_button.label}{\" \"}\n              </button>\n            </div>\n            <div className=\"ReturnToAMOIcon\" />\n          </div>\n          <button\n            onClick={this.onBlockButton}\n            className=\"default grey ReturnToAMOGetStarted\"\n          >\n            {\" \"}\n            {content.secondary_button.label}{\" \"}\n          </button>\n        </div>\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "content-src/asrouter/templates/ReturnToAMO/_ReturnToAMO.scss",
    "content": ".ReturnToAMOOverlay,\n.amo + body.hide-main {  // sass-lint:disable-line no-qualifying-elements\n  background: $grey-10;\n  height: 100%;\n  position: fixed;\n  top: 0;\n  width: 100%;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  z-index: 2100;\n\n  .ReturnToAMOText {\n    color: $grey-90;\n    line-height: 32px;\n    font-size: 23px;\n    width: 100%;\n\n    img {\n      margin-inline-start: 6px;\n      margin-inline-end: 6px;\n    }\n  }\n\n  h2 {\n    color: $grey-60;\n    font-weight: 100;\n    margin: 0 0 36px;\n    font-size: 36px;\n    line-height: 48px;\n    letter-spacing: 1.2px;\n  }\n\n  p {\n    color: $grey-60;\n    font-size: 14px;\n    line-height: 18px;\n    margin-bottom: 16px;\n  }\n\n  .puffy {\n    border-radius: 4px;\n    height: 48px;\n    padding: 0 16px;\n    font-size: 15px;\n  }\n\n  .blue {\n    border: 0;\n    color: $white;\n    background-color: $blue-60;\n\n    &:hover {\n      box-shadow: none;\n      background-color: $blue-70;\n    }\n\n    &:active {\n      background-color: $blue-80;\n    }\n  }\n\n  .default {\n    border-radius: 2px;\n    height: 40px;\n    padding: 0 12px;\n    font-size: 15px;\n  }\n\n  .grey {\n    border: 0;\n    background-color: $grey-90-10;\n\n    &:hover {\n      box-shadow: none;\n      background-color: $grey-90-20;\n    }\n\n    &:active {\n      background-color: $grey-90-30;\n    }\n  }\n\n  .ReturnToAMOGetStarted {\n    margin-top: 40px;\n    float: right;\n\n    &:dir(rtl) {\n      float: left;\n    }\n  }\n\n  .ReturnToAMOAddExtension {\n    margin-top: 20px;\n  }\n\n  .ReturnToAMOContainer {\n    width: 960px;\n    background: $white;\n    box-shadow: 0 1px 15px 0 $black-30;\n    border-radius: 4px;\n    display: flex;\n    padding: 64px 64px 72px;\n  }\n\n  .ReturnToAMOAddonContents {\n    width: 560px;\n    margin-top: 32px;\n    margin-inline-end: 24px;\n  }\n\n  .ReturnToAMOIcon {\n    width: 292px;\n    height: 254px;\n    background-size: 292px 254px;\n    background-position: center center;\n    background-repeat: no-repeat;\n    background-image: url('resource://activity-stream/data/content/assets/gift-extension.svg');\n  }\n\n  .icon-add {\n    fill: $white;\n    vertical-align: sub;\n  }\n}\n"
  },
  {
    "path": "content-src/asrouter/templates/SendToDeviceSnippet/SendToDeviceSnippet.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport { isEmailOrPhoneNumber } from \"./isEmailOrPhoneNumber\";\nimport React from \"react\";\nimport schema from \"./SendToDeviceSnippet.schema.json\";\nimport { SubmitFormSnippet } from \"../SubmitFormSnippet/SubmitFormSnippet.jsx\";\n\nfunction validateInput(value, content) {\n  const type = isEmailOrPhoneNumber(value, content);\n  return type ? \"\" : \"Must be an email or a phone number.\";\n}\n\nfunction processFormData(input, message) {\n  const { content } = message;\n  const type = content.include_sms\n    ? isEmailOrPhoneNumber(input.value, content)\n    : \"email\";\n  const formData = new FormData();\n  let url;\n  if (type === \"phone\") {\n    url = \"https://basket.mozilla.org/news/subscribe_sms/\";\n    formData.append(\"mobile_number\", input.value);\n    formData.append(\"msg_name\", content.message_id_sms);\n    formData.append(\"country\", content.country);\n  } else if (type === \"email\") {\n    url = \"https://basket.mozilla.org/news/subscribe/\";\n    formData.append(\"email\", input.value);\n    formData.append(\"newsletters\", content.message_id_email);\n    formData.append(\n      \"source_url\",\n      encodeURIComponent(`https://snippets.mozilla.com/show/${message.id}`)\n    );\n  }\n  formData.append(\"lang\", content.locale);\n  return { formData, url };\n}\n\nfunction addDefaultValues(props) {\n  return {\n    ...props,\n    content: {\n      scene1_button_label: schema.properties.scene1_button_label.default,\n      retry_button_label: schema.properties.retry_button_label.default,\n      scene2_dismiss_button_text:\n        schema.properties.scene2_dismiss_button_text.default,\n      scene2_button_label: schema.properties.scene2_button_label.default,\n      scene2_input_placeholder:\n        schema.properties.scene2_input_placeholder.default,\n      locale: schema.properties.locale.default,\n      country: schema.properties.country.default,\n      message_id_email: \"\",\n      include_sms: schema.properties.include_sms.default,\n      ...props.content,\n    },\n  };\n}\n\nexport const SendToDeviceSnippet = props => {\n  const propsWithDefaults = addDefaultValues(props);\n\n  return (\n    <SubmitFormSnippet\n      {...propsWithDefaults}\n      form_method=\"POST\"\n      className=\"send_to_device_snippet\"\n      inputType={propsWithDefaults.content.include_sms ? \"text\" : \"email\"}\n      validateInput={\n        propsWithDefaults.content.include_sms ? validateInput : null\n      }\n      processFormData={processFormData}\n    />\n  );\n};\n"
  },
  {
    "path": "content-src/asrouter/templates/SendToDeviceSnippet/SendToDeviceSnippet.schema.json",
    "content": "{\n  \"title\": \"SendToDeviceSnippet\",\n  \"description\": \"A snippet template for send to device mobile download\",\n  \"version\": \"1.2.0\",\n  \"type\": \"object\",\n  \"definitions\": {\n    \"plainText\": {\n      \"description\": \"Plain text (no HTML allowed)\",\n      \"type\": \"string\"\n    },\n    \"richText\": {\n      \"description\": \"Text with HTML subset allowed: i, b, u, strong, em, br\",\n      \"type\": \"string\"\n    },\n    \"link_url\": {\n      \"description\": \"Target for links or buttons\",\n      \"type\": \"string\",\n      \"format\": \"uri\"\n    }\n  },\n  \"properties\": {\n    \"locale\": {\n      \"type\": \"string\",\n      \"description\": \"Two to five character string for the locale code\",\n      \"default\": \"en-US\"\n    },\n    \"country\": {\n      \"type\": \"string\",\n      \"description\": \"Two character string for the country code (used for SMS)\",\n      \"default\": \"us\"\n    },\n    \"scene1_title\": {\n      \"allof\": [\n        {\"$ref\": \"#/definitions/plainText\"},\n        {\"description\": \"snippet title displayed before snippet text\"}\n      ]\n    },\n    \"scene1_text\": {\n      \"allOf\": [\n        {\"$ref\": \"#/definitions/richText\"},\n        {\"description\": \"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br\"}\n      ]\n    },\n    \"scene1_section_title_icon\": {\n      \"type\": \"string\",\n      \"description\": \"Section title icon for scene 1. 16x16px. SVG or PNG preferred. scene1_section_title_text must also be specified to display.\"\n    },\n    \"scene1_section_title_icon_dark_theme\": {\n      \"type\": \"string\",\n      \"description\": \"Section title icon for scene 1, dark theme variant. 16x16px. SVG or PNG preferred. scene1_section_title_text must also be specified to display.\"\n    },\n    \"scene1_section_title_text\": {\n      \"type\": \"string\",\n      \"description\": \"Section title text for scene 1. scene1_section_title_icon must also be specified to display.\"\n    },\n    \"scene1_section_title_url\": {\n      \"allOf\": [\n        {\"$ref\": \"#/definitions/link_url\"},\n        {\"description\": \"A url, scene1_section_title_text links to this\"}\n      ]\n    },\n    \"scene2_title\": {\n      \"allOf\": [\n        {\"$ref\": \"#/definitions/plainText\"},\n        {\"description\": \"Title displayed before text in scene 2. Should be plain text.\"}\n      ]\n    },\n    \"scene2_text\": {\n      \"allOf\": [\n        {\"$ref\": \"#/definitions/richText\"},\n        {\"description\": \"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br\"}\n      ]\n    },\n    \"scene1_icon\": {\n      \"type\": \"string\",\n      \"description\": \"Snippet icon. 64x64px. SVG or PNG preferred.\"\n    },\n    \"scene1_icon_dark_theme\": {\n      \"type\": \"string\",\n      \"description\": \"Snippet icon. Dark theme variant. 64x64px. SVG or PNG preferred.\"\n    },\n    \"scene2_icon\": {\n      \"type\": \"string\",\n      \"description\": \"(send to device) Image to display above the form. Dark theme variant. 98x98px. SVG or PNG preferred.\"\n    },\n    \"scene2_icon_dark_theme\": {\n      \"type\": \"string\",\n      \"description\": \"(send to device) Image to display above the form. 98x98px. SVG or PNG preferred.\"\n    },\n    \"scene1_title_icon\": {\n      \"type\": \"string\",\n      \"description\": \"Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale.\"\n    },\n    \"scene1_title_icon_dark_theme\": {\n      \"type\": \"string\",\n      \"description\": \"Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale.\"\n    },\n    \"scene2_button_label\": {\n      \"type\": \"string\",\n      \"description\": \"Label for form submit button\",\n      \"default\": \"Send\"\n    },\n    \"scene2_input_placeholder\": {\n      \"type\": \"string\",\n      \"description\": \"(send to device) Value to show while input is empty.\",\n      \"default\": \"Your email here\"\n    },\n    \"scene2_disclaimer_html\": {\n      \"type\": \"string\",\n      \"description\": \"(send to device) Html for disclaimer and link underneath input box.\"\n    },\n    \"scene2_dismiss_button_text\": {\n      \"type\": \"string\",\n      \"description\": \"Label for the dismiss button when the sign-up form is expanded.\",\n      \"default\": \"Dismiss\"\n    },\n    \"hidden_inputs\": {\n      \"type\": \"object\",\n      \"description\": \"Each entry represents a hidden input, key is used as value for the name property.\",\n      \"properties\": {\n        \"action\": {\n          \"type\": \"string\",\n          \"enum\": [\"email\"]\n        },\n        \"context\": {\n          \"type\": \"string\",\n          \"enum\": [\"fx_desktop_v3\"]\n        },\n        \"entrypoint\": {\n          \"type\": \"string\",\n          \"enum\": [\"snippets\"]\n        },\n        \"utm_content\": {\n          \"type\": \"string\",\n          \"description\": \"Firefox version number\"\n        },\n        \"utm_source\": {\n          \"type\": \"string\",\n          \"enum\": [\"snippet\"]\n        },\n        \"utm_campaign\": {\n          \"type\": \"string\",\n          \"description\": \"(fxa) Value to pass through to GA as utm_campaign.\"\n        },\n        \"utm_term\": {\n          \"type\": \"string\",\n          \"description\": \"(fxa) Value to pass through to GA as utm_term.\"\n        },\n        \"additionalProperties\": false\n      }\n    },\n    \"scene1_button_label\": {\n      \"allOf\": [\n        {\"$ref\": \"#/definitions/plainText\"},\n        {\"description\": \"Text for a button next to main snippet text that links to button_url. Requires button_url.\"}\n      ],\n      \"default\": \"Learn more\"\n    },\n    \"scene1_button_color\": {\n      \"type\": \"string\",\n      \"description\": \"The text color of the button. Valid CSS color.\"\n    },\n    \"scene1_button_background_color\": {\n      \"type\": \"string\",\n      \"description\": \"The background color of the button. Valid CSS color.\"\n    },\n    \"retry_button_label\": {\n      \"allOf\": [\n        {\"$ref\": \"#/definitions/plainText\"},\n        {\"description\": \"Text for the button in the event of a submission error/failure.\"}\n      ],\n      \"default\": \"Try again\"\n    },\n    \"do_not_autoblock\": {\n      \"type\": \"boolean\",\n      \"description\": \"Used to prevent blocking the snippet after the CTA (link or button) has been clicked\",\n      \"default\": false\n    },\n    \"success_title\": {\n      \"type\": \"string\",\n      \"description\": \"(send to device) Title shown before text on successful registration.\"\n    },\n    \"success_text\": {\n      \"type\": \"string\",\n      \"description\": \"Message shown on successful registration.\"\n    },\n    \"error_text\": {\n      \"type\": \"string\",\n      \"description\": \"Message shown if registration failed.\"\n    },\n    \"include_sms\": {\n      \"type\": \"boolean\",\n      \"description\": \"(send to device) Allow users to send an SMS message with the form?\",\n      \"default\": false\n    },\n    \"message_id_sms\": {\n      \"type\": \"string\",\n      \"description\": \"(send to device) Newsletter/basket id representing the SMS message to be sent.\"\n    },\n    \"message_id_email\": {\n      \"type\": \"string\",\n      \"description\": \"(send to device) Newsletter/basket id representing the email message to be sent. Must be a value from the 'Slug' column here: https://basket.mozilla.org/news/.\"\n    },\n    \"utm_campaign\": {\n      \"type\": \"string\",\n      \"description\": \"(fxa) Value to pass through to GA as utm_campaign.\"\n    },\n    \"utm_term\": {\n      \"type\": \"string\",\n      \"description\": \"(fxa) Value to pass through to GA as utm_term.\"\n    },\n    \"links\": {\n      \"additionalProperties\": {\n        \"url\": {\n          \"allOf\": [\n            {\"$ref\": \"#/definitions/link_url\"},\n            {\"description\": \"The url where the link points to.\"}\n          ]\n        },\n        \"metric\": {\n          \"type\": \"string\",\n          \"description\": \"Custom event name sent with telemetry event.\"\n        }\n      }\n    }\n  },\n  \"additionalProperties\": false,\n  \"required\": [\"scene1_text\", \"scene2_text\", \"scene1_button_label\"],\n  \"dependencies\": {\n    \"scene1_button_color\": [\"scene1_button_label\"],\n    \"scene1_button_background_color\": [\"scene1_button_label\"]\n  }\n}\n\n"
  },
  {
    "path": "content-src/asrouter/templates/SendToDeviceSnippet/isEmailOrPhoneNumber.js",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\n/**\n * Checks if a given string is an email or phone number or neither\n * @param {string} val The user input\n * @param {ASRMessageContent} content .content property on ASR message\n * @returns {\"email\"|\"phone\"|\"\"} The type of the input\n */\nexport function isEmailOrPhoneNumber(val, content) {\n  const { locale } = content;\n  // http://emailregex.com/\n  const email_re = /^(([^<>()[\\]\\\\.,;:\\s@\"]+(\\.[^<>()[\\]\\\\.,;:\\s@\"]+)*)|(\".+\"))@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}])|(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,}))$/;\n  const check_email = email_re.test(val);\n  let check_phone; // depends on locale\n  switch (locale) {\n    case \"en-US\":\n    case \"en-CA\":\n      // allow 10-11 digits in case user wants to enter country code\n      check_phone = val.length >= 10 && val.length <= 11 && !isNaN(val);\n      break;\n    case \"de\":\n      // allow between 2 and 12 digits for german phone numbers\n      check_phone = val.length >= 2 && val.length <= 12 && !isNaN(val);\n      break;\n    // this case should never be hit, but good to have a fallback just in case\n    default:\n      check_phone = !isNaN(val);\n      break;\n  }\n  if (check_email) {\n    return \"email\";\n  } else if (check_phone) {\n    return \"phone\";\n  }\n  return \"\";\n}\n"
  },
  {
    "path": "content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport React from \"react\";\nimport { Button } from \"../../components/Button/Button\";\nimport { RichText } from \"../../components/RichText/RichText\";\nimport { safeURI } from \"../../template-utils\";\nimport { SnippetBase } from \"../../components/SnippetBase/SnippetBase\";\n\nconst DEFAULT_ICON_PATH = \"chrome://branding/content/icon64.png\";\n// Alt text placeholder in case the prop from the server isn't available\nconst ICON_ALT_TEXT = \"\";\n\nexport class SimpleBelowSearchSnippet extends React.PureComponent {\n  constructor(props) {\n    super(props);\n    this.onButtonClick = this.onButtonClick.bind(this);\n  }\n\n  renderText() {\n    const { props } = this;\n    return props.content.text ? (\n      <RichText\n        text={props.content.text}\n        customElements={this.props.customElements}\n        localization_id=\"text\"\n        links={props.content.links}\n        sendClick={props.sendClick}\n      />\n    ) : null;\n  }\n\n  renderTitle() {\n    const { title } = this.props.content;\n    return title ? (\n      <h3 className={\"title title-inline\"}>\n        {title}\n        <br />\n      </h3>\n    ) : null;\n  }\n\n  async onButtonClick() {\n    if (this.props.provider !== \"preview\") {\n      this.props.sendUserActionTelemetry({\n        event: \"CLICK_BUTTON\",\n        id: this.props.UISurface,\n      });\n    }\n    const { button_url } = this.props.content;\n    // If button_url is defined handle it as OPEN_URL action\n    const type = this.props.content.button_action || (button_url && \"OPEN_URL\");\n    await this.props.onAction({\n      type,\n      data: { args: this.props.content.button_action_args || button_url },\n    });\n    if (!this.props.content.do_not_autoblock) {\n      this.props.onBlock();\n    }\n  }\n\n  _shouldRenderButton() {\n    return (\n      this.props.content.button_action ||\n      this.props.onButtonClick ||\n      this.props.content.button_url\n    );\n  }\n\n  renderButton() {\n    const { props } = this;\n    if (!this._shouldRenderButton()) {\n      return null;\n    }\n\n    return (\n      <Button\n        onClick={props.onButtonClick || this.onButtonClick}\n        color={props.content.button_color}\n        backgroundColor={props.content.button_background_color}\n      >\n        {props.content.button_label}\n      </Button>\n    );\n  }\n\n  render() {\n    const { props } = this;\n    let className = \"SimpleBelowSearchSnippet\";\n    let containerName = \"below-search-snippet\";\n\n    if (props.className) {\n      className += ` ${props.className}`;\n    }\n    if (this._shouldRenderButton()) {\n      className += \" withButton\";\n      containerName += \" withButton\";\n    }\n\n    return (\n      <div className={containerName}>\n        <div className=\"snippet-hover-wrapper\">\n          <SnippetBase\n            {...props}\n            className={className}\n            textStyle={this.props.textStyle}\n          >\n            <img\n              src={safeURI(props.content.icon) || DEFAULT_ICON_PATH}\n              className=\"icon icon-light-theme\"\n              alt={props.content.icon_alt_text || ICON_ALT_TEXT}\n            />\n            <img\n              src={\n                safeURI(props.content.icon_dark_theme || props.content.icon) ||\n                DEFAULT_ICON_PATH\n              }\n              className=\"icon icon-dark-theme\"\n              alt={props.content.icon_alt_text || ICON_ALT_TEXT}\n            />\n            <div className=\"textContainer\">\n              {this.renderTitle()}\n              <p className=\"body\">{this.renderText()}</p>\n              {this.props.extraContent}\n            </div>\n            {<div className=\"buttonContainer\">{this.renderButton()}</div>}\n          </SnippetBase>\n        </div>\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.schema.json",
    "content": "{\n  \"title\": \"SimpleBelowSearchSnippet\",\n  \"description\": \"A simple template with an icon, rich text and an optional button. It gets inserted below the Activity Stream search box.\",\n  \"version\": \"1.2.0\",\n  \"type\": \"object\",\n  \"definitions\": {\n    \"plainText\": {\n      \"description\": \"Plain text (no HTML allowed)\",\n      \"type\": \"string\"\n    },\n    \"richText\": {\n      \"description\": \"Text with HTML subset allowed: i, b, u, strong, em, br\",\n      \"type\": \"string\"\n    },\n    \"link_url\": {\n      \"description\": \"Target for links or buttons\",\n      \"type\": \"string\",\n      \"format\": \"uri\"\n    }\n  },\n  \"properties\": {\n    \"title\": {\n      \"allOf\": [\n        {\"$ref\": \"#/definitions/plainText\"},\n        {\"description\": \"Snippet title displayed before snippet text\"}\n      ]\n    },\n    \"text\": {\n      \"allOf\": [\n        {\"$ref\": \"#/definitions/richText\"},\n        {\"description\": \"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br\"}\n      ]\n    },\n    \"icon\": {\n      \"type\": \"string\",\n      \"description\": \"Snippet icon. 64x64px. SVG or PNG preferred.\"\n    },\n    \"icon_dark_theme\": {\n      \"type\": \"string\",\n      \"description\": \"Snippet icon. Dark theme variant. 64x64px. SVG or PNG preferred.\"\n    },\n    \"icon_alt_text\": {\n      \"type\": \"string\",\n      \"description\": \"Alt text describing icon for screen readers\",\n      \"default\": \"\"\n    },\n    \"block_button_text\": {\n      \"type\": \"string\",\n      \"description\": \"Tooltip text used for dismiss button.\",\n      \"default\": \"Remove this\"\n    },\n    \"button_action\": {\n      \"type\": \"string\",\n      \"description\": \"The type of action the button should trigger.\"\n    },\n    \"button_url\": {\n      \"allOf\": [\n        {\"$ref\": \"#/definitions/link_url\"},\n        {\"description\": \"A url, button_label links to this\"}\n      ]\n    },\n    \"button_action_args\": {\n      \"description\": \"Additional parameters for button action, example which specific menu the button should open\"\n    },\n    \"button_label\": {\n      \"allOf\": [\n        {\"$ref\": \"#/definitions/plainText\"},\n        {\"description\": \"Text for a button next to main snippet text that links to button_url. Requires button_url.\"}\n      ]\n    },\n    \"button_color\": {\n      \"type\": \"string\",\n      \"description\": \"The text color of the button. Valid CSS color.\"\n    },\n    \"button_background_color\": {\n      \"type\": \"string\",\n      \"description\": \"The background color of the button. Valid CSS color.\"\n    },\n    \"do_not_autoblock\": {\n      \"type\": \"boolean\",\n      \"description\": \"Used to prevent blocking the snippet after the CTA link has been clicked\"\n    },\n    \"links\": {\n      \"additionalProperties\": {\n        \"url\": {\n          \"allOf\": [\n            {\"$ref\": \"#/definitions/link_url\"},\n            {\"description\": \"The url where the link points to.\"}\n          ]\n        },\n        \"metric\": {\n          \"type\": \"string\",\n          \"description\": \"Custom event name sent with telemetry event.\"\n        },\n        \"args\": {\n          \"type\": \"string\",\n          \"description\": \"Additional parameters for link action, example which specific menu the button should open\"\n        }\n      }\n    }\n  },\n  \"additionalProperties\": false,\n  \"required\": [\"text\"],\n  \"dependencies\": {\n    \"button_action\": [\"button_label\"],\n    \"button_url\": [\"button_label\"],\n    \"button_color\": [\"button_label\"],\n    \"button_background_color\": [\"button_label\"]\n  }\n}\n"
  },
  {
    "path": "content-src/asrouter/templates/SimpleBelowSearchSnippet/_SimpleBelowSearchSnippet.scss",
    "content": "\n.below-search-snippet {\n  margin: 0 auto 16px;\n\n  &.withButton {\n    padding: 0 25px;\n    margin: auto;\n    min-height: 60px;\n    background-color: transparent;\n\n    // Add more padding if discovery stream is enabled.\n    .ds-outer-wrapper-breakpoint-override & {\n      padding: 0 50px;\n\n      @media (max-width: 865px) {\n\n        .buttonContainer {\n          margin: auto;\n        }\n      }\n    }\n\n    .snippet-hover-wrapper {\n      min-height: 60px;\n      border-radius: 4px;\n\n      &:hover {\n        background-color: var(--newtab-element-hover-color);\n\n        .blockButton {\n          display: block;\n          opacity: 1;\n\n          // larger inset if discovery stream is enabled.\n          .ds-outer-wrapper-breakpoint-override & {\n            inset-inline-end: -8%;\n\n            @media (max-width: 865px) {\n              inset-inline-end: 2%;\n            }\n          }\n        }\n      }\n    }\n  }\n}\n\n.SimpleBelowSearchSnippet {\n  background-color: transparent;\n  border: 0;\n  box-shadow: none;\n  position: relative;\n  margin: auto;\n  z-index: auto;\n\n  @media (min-width: $break-point-large) {\n    width: 736px;\n  }\n\n  &.active {\n    background-color: var(--newtab-element-hover-color);\n    border-radius: 4px;\n  }\n\n  .innerWrapper {\n    align-items: center;\n    background-color: transparent;\n    border-radius: 4px;\n    box-shadow: var(--newtab-card-shadow);\n    flex-direction: column;\n    padding: 16px;\n    text-align: center;\n    width: 100%;\n\n    @mixin full-width-styles {\n      align-items: flex-start;\n      background-color: transparent;\n      border-radius: 4px;\n      box-shadow: none;\n      flex-direction: row;\n      padding: 0;\n      text-align: inherit;\n    }\n\n    @media (min-width: $break-point-medium) {\n      @include full-width-styles;\n    }\n\n    // There is an off-by-one gap between breakpoints; this is to prevent weirdness at exactly 1121px.\n    @media (max-width: $break-point-widest + 1px) {\n      margin: 0 60px;\n    }\n\n    @media (max-width: 865px) {\n      margin-inline-start: 0;\n    }\n\n    // There is an off-by-one gap between breakpoints; this is to prevent weirdness at exactly 610px.\n    @media (max-width: $break-point-medium - 1px) {\n      margin: auto;\n    }\n\n    // Disable breakpoints for now if discovery stream is enabled.\n    .ds-outer-wrapper-breakpoint-override & {\n      @include full-width-styles;\n      margin: auto;\n    }\n  }\n\n  .blockButton {\n    display: block;\n    inset-inline-end: 10px;\n    opacity: 1;\n    top: 50%;\n\n    &:focus {\n      box-shadow: 0 0 0 1px $blue-50 inset, 0 0 0 1px $blue-50, 0 0 0 4px $blue-50-30;\n      border-radius: 2px;\n    }\n  }\n\n  .title {\n    font-size: inherit;\n    margin: 0;\n  }\n\n  .title-inline {\n    display: inline;\n  }\n\n  .textContainer {\n    margin: 10px;\n    margin-inline-start: 0;\n    padding-inline-end: 20px;\n  }\n\n  .icon {\n    margin-top: 8px;\n    margin-inline-start: 12px;\n    height: 32px;\n    width: 32px;\n\n    @mixin full-width-styles {\n      height: 24px;\n      width: 24px;\n    }\n\n    @media (min-width: $break-point-medium) {\n      @include full-width-styles;\n    }\n\n    @media (max-width: $break-point-medium) {\n      margin: auto;\n    }\n\n    // Disable breakpoints for now if discovery stream is enabled.\n    .ds-outer-wrapper-breakpoint-override & {\n      @include full-width-styles;\n    }\n  }\n\n  &.withButton {\n    line-height: 20px;\n    margin-bottom: 10px;\n    min-height: 60px;\n    background-color: transparent;\n\n    .blockButton {\n      display: block;\n      inset-inline-end: -15%;\n      opacity: 0;\n      margin: auto;\n      top: unset;\n\n      &:focus {\n        opacity: 1;\n        box-shadow: none;\n      }\n\n      // There is an off-by-one gap between breakpoints; this is to prevent weirdness at exactly 1121px.\n      @media (max-width: $break-point-widest + 1px) {\n        inset-inline-end: 2%;\n      }\n\n      .ds-outer-wrapper-breakpoint-override & {\n        inset-inline-end: -10%;\n        margin: auto;\n\n        @media (max-width: 865px) {\n          inset-inline-end: 2%;\n        }\n      }\n    }\n\n    .icon {\n      width: 42px;\n      height: 42px;\n      flex-shrink: 0;\n      margin: auto 0;\n      margin-inline-end: 10px;\n\n      @media (max-width: $break-point-medium) {\n        margin: auto;\n      }\n    }\n\n    .buttonContainer {\n      margin: auto;\n      margin-inline-end: 0;\n\n      @media (max-width: $break-point-medium) {\n        margin: auto;\n      }\n    }\n  }\n\n  button {\n    @media (max-width: $break-point-medium) {\n      margin: auto;\n    }\n  }\n\n  .body {\n    display: inline;\n    position: sticky;\n    transform: translateY(-50%);\n    margin: 8px 0 0;\n\n    @media (min-width: $break-point-medium) {\n      margin: 12px 0;\n    }\n\n    // Disable breakpoints for now if discovery stream is enabled.\n    .ds-outer-wrapper-breakpoint-override & {\n      margin: 12px 0;\n    }\n\n    a {\n      font-weight: 600;\n    }\n  }\n}\n"
  },
  {
    "path": "content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport { Button } from \"../../components/Button/Button\";\nimport ConditionalWrapper from \"../../components/ConditionalWrapper/ConditionalWrapper\";\nimport React from \"react\";\nimport { RichText } from \"../../components/RichText/RichText\";\nimport { safeURI } from \"../../template-utils\";\nimport { SnippetBase } from \"../../components/SnippetBase/SnippetBase\";\n\nconst DEFAULT_ICON_PATH = \"chrome://branding/content/icon64.png\";\n// Alt text placeholder in case the prop from the server isn't available\nconst ICON_ALT_TEXT = \"\";\n\nexport class SimpleSnippet extends React.PureComponent {\n  constructor(props) {\n    super(props);\n    this.onButtonClick = this.onButtonClick.bind(this);\n  }\n\n  onButtonClick() {\n    if (this.props.provider !== \"preview\") {\n      this.props.sendUserActionTelemetry({\n        event: \"CLICK_BUTTON\",\n        id: this.props.UISurface,\n      });\n    }\n    const { button_url } = this.props.content;\n    // If button_url is defined handle it as OPEN_URL action\n    const type = this.props.content.button_action || (button_url && \"OPEN_URL\");\n    this.props.onAction({\n      type,\n      data: { args: this.props.content.button_action_args || button_url },\n    });\n    if (!this.props.content.do_not_autoblock) {\n      this.props.onBlock();\n    }\n  }\n\n  _shouldRenderButton() {\n    return (\n      this.props.content.button_action ||\n      this.props.onButtonClick ||\n      this.props.content.button_url\n    );\n  }\n\n  renderTitle() {\n    const { title } = this.props.content;\n    return title ? (\n      <h3\n        className={`title ${this._shouldRenderButton() ? \"title-inline\" : \"\"}`}\n      >\n        {this.renderTitleIcon()} {title}\n      </h3>\n    ) : null;\n  }\n\n  renderTitleIcon() {\n    const titleIconLight = safeURI(this.props.content.title_icon);\n    const titleIconDark = safeURI(\n      this.props.content.title_icon_dark_theme || this.props.content.title_icon\n    );\n    if (!titleIconLight) {\n      return null;\n    }\n\n    return (\n      <React.Fragment>\n        <span\n          className=\"titleIcon icon-light-theme\"\n          style={{ backgroundImage: `url(\"${titleIconLight}\")` }}\n        />\n        <span\n          className=\"titleIcon icon-dark-theme\"\n          style={{ backgroundImage: `url(\"${titleIconDark}\")` }}\n        />\n      </React.Fragment>\n    );\n  }\n\n  renderButton() {\n    const { props } = this;\n    if (!this._shouldRenderButton()) {\n      return null;\n    }\n\n    return (\n      <Button\n        onClick={props.onButtonClick || this.onButtonClick}\n        color={props.content.button_color}\n        backgroundColor={props.content.button_background_color}\n      >\n        {props.content.button_label}\n      </Button>\n    );\n  }\n\n  renderText() {\n    const { props } = this;\n    return (\n      <RichText\n        text={props.content.text}\n        customElements={this.props.customElements}\n        localization_id=\"text\"\n        links={props.content.links}\n        sendClick={props.sendClick}\n      />\n    );\n  }\n\n  wrapSectionHeader(url) {\n    return function(children) {\n      return <a href={url}>{children}</a>;\n    };\n  }\n\n  wrapSnippetContent(children) {\n    return <div className=\"innerContentWrapper\">{children}</div>;\n  }\n\n  renderSectionHeader() {\n    const { props } = this;\n\n    // an icon and text must be specified to render the section header\n    if (props.content.section_title_icon && props.content.section_title_text) {\n      const sectionTitleIconLight = safeURI(props.content.section_title_icon);\n      const sectionTitleIconDark = safeURI(\n        props.content.section_title_icon_dark_theme ||\n          props.content.section_title_icon\n      );\n      const sectionTitleURL = props.content.section_title_url;\n\n      return (\n        <div className=\"section-header\">\n          <h3 className=\"section-title\">\n            <ConditionalWrapper\n              condition={sectionTitleURL}\n              wrap={this.wrapSectionHeader(sectionTitleURL)}\n            >\n              <span\n                className=\"icon icon-small-spacer icon-light-theme\"\n                style={{ backgroundImage: `url(\"${sectionTitleIconLight}\")` }}\n              />\n              <span\n                className=\"icon icon-small-spacer icon-dark-theme\"\n                style={{ backgroundImage: `url(\"${sectionTitleIconDark}\")` }}\n              />\n              <span className=\"section-title-text\">\n                {props.content.section_title_text}\n              </span>\n            </ConditionalWrapper>\n          </h3>\n        </div>\n      );\n    }\n\n    return null;\n  }\n\n  render() {\n    const { props } = this;\n    const sectionHeader = this.renderSectionHeader();\n    let className = \"SimpleSnippet\";\n\n    if (props.className) {\n      className += ` ${props.className}`;\n    }\n    if (props.content.tall) {\n      className += \" tall\";\n    }\n    if (sectionHeader) {\n      className += \" has-section-header\";\n    }\n\n    return (\n      <div className=\"snippet-hover-wrapper\">\n        <SnippetBase\n          {...props}\n          className={className}\n          textStyle={this.props.textStyle}\n        >\n          {sectionHeader}\n          <ConditionalWrapper\n            condition={sectionHeader}\n            wrap={this.wrapSnippetContent}\n          >\n            <img\n              src={safeURI(props.content.icon) || DEFAULT_ICON_PATH}\n              className=\"icon icon-light-theme\"\n              alt={props.content.icon_alt_text || ICON_ALT_TEXT}\n            />\n            <img\n              src={\n                safeURI(props.content.icon_dark_theme || props.content.icon) ||\n                DEFAULT_ICON_PATH\n              }\n              className=\"icon icon-dark-theme\"\n              alt={props.content.icon_alt_text || ICON_ALT_TEXT}\n            />\n            <div>\n              {this.renderTitle()} <p className=\"body\">{this.renderText()}</p>\n              {this.props.extraContent}\n            </div>\n            {<div>{this.renderButton()}</div>}\n          </ConditionalWrapper>\n        </SnippetBase>\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.schema.json",
    "content": "{\n  \"title\": \"SimpleSnippet\",\n  \"description\": \"A simple template with an icon, text, and optional button.\",\n  \"version\": \"1.1.1\",\n  \"type\": \"object\",\n  \"definitions\": {\n    \"plainText\": {\n      \"description\": \"Plain text (no HTML allowed)\",\n      \"type\": \"string\"\n    },\n    \"richText\": {\n      \"description\": \"Text with HTML subset allowed: i, b, u, strong, em, br\",\n      \"type\": \"string\"\n    },\n    \"link_url\": {\n      \"description\": \"Target for links or buttons\",\n      \"type\": \"string\",\n      \"format\": \"uri\"\n    }\n  },\n  \"properties\": {\n    \"title\": {\n      \"allOf\": [\n        {\"$ref\": \"#/definitions/plainText\"},\n        {\"description\": \"Snippet title displayed before snippet text\"}\n      ]\n    },\n    \"text\": {\n      \"allOf\": [\n        {\"$ref\": \"#/definitions/richText\"},\n        {\"description\": \"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br\"}\n      ]\n    },\n    \"icon\": {\n      \"type\": \"string\",\n      \"description\": \"Snippet icon. 64x64px. SVG or PNG preferred.\"\n    },\n    \"icon_dark_theme\": {\n      \"type\": \"string\",\n      \"description\": \"Snippet icon, dark theme variant. 64x64px. SVG or PNG preferred.\"\n    },\n    \"icon_alt_text\": {\n      \"type\": \"string\",\n      \"description\": \"Alt text describing icon for screen readers\",\n      \"default\": \"\"\n    },\n    \"title_icon\": {\n      \"type\": \"string\",\n      \"description\": \"Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale.\"\n    },\n    \"title_icon_dark_theme\": {\n      \"type\": \"string\",\n      \"description\": \"Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale.\"\n    },\n    \"title_icon_alt_text\": {\n      \"type\": \"string\",\n      \"description\": \"Alt text describing title icon for screen readers\",\n      \"default\": \"\"\n    },\n    \"button_action\": {\n      \"type\": \"string\",\n      \"description\": \"The type of action the button should trigger.\"\n    },\n    \"button_url\": {\n      \"allOf\": [\n        {\"$ref\": \"#/definitions/link_url\"},\n        {\"description\": \"A url, button_label links to this\"}\n      ]\n    },\n    \"button_action_args\": {\n      \"description\": \"Additional parameters for button action, example which specific menu the button should open\"\n    },\n    \"button_label\": {\n      \"allOf\": [\n        {\"$ref\": \"#/definitions/plainText\"},\n        {\"description\": \"Text for a button next to main snippet text that links to button_url. Requires button_url.\"}\n      ]\n    },\n    \"button_color\": {\n      \"type\": \"string\",\n      \"description\": \"The text color of the button. Valid CSS color.\"\n    },\n    \"button_background_color\": {\n      \"type\": \"string\",\n      \"description\": \"The background color of the button. Valid CSS color.\"\n    },\n    \"block_button_text\": {\n      \"type\": \"string\",\n      \"description\": \"Tooltip text used for dismiss button.\",\n      \"default\": \"Remove this\"\n    },\n    \"tall\": {\n      \"type\": \"boolean\",\n      \"description\": \"To be used by fundraising only, increases height to roughly 120px. Defaults to false.\"\n    },\n    \"do_not_autoblock\": {\n      \"type\": \"boolean\",\n      \"description\": \"Used to prevent blocking the snippet after the CTA (link or button) has been clicked\"\n    },\n    \"links\": {\n      \"additionalProperties\": {\n        \"url\": {\n          \"allOf\": [\n            {\"$ref\": \"#/definitions/link_url\"},\n            {\"description\": \"The url where the link points to.\"}\n          ]\n        },\n        \"metric\": {\n          \"type\": \"string\",\n          \"description\": \"Custom event name sent with telemetry event.\"\n        },\n        \"args\": {\n          \"type\": \"string\",\n          \"description\": \"Additional parameters for link action, example which specific menu the button should open\"\n        }\n      }\n    },\n    \"section_title_icon\": {\n      \"type\": \"string\",\n      \"description\": \"Section title icon. 16x16px. SVG or PNG preferred. section_title_text must also be specified to display.\"\n    },\n    \"section_title_icon_dark_theme\": {\n      \"type\": \"string\",\n      \"description\": \"Section title icon, dark theme variant. 16x16px. SVG or PNG preferred. section_title_text must also be specified to display.\"\n    },\n    \"section_title_text\": {\n      \"type\": \"string\",\n      \"description\": \"Section title text. section_title_icon must also be specified to display.\"\n    },\n    \"section_title_url\": {\n      \"allOf\": [\n        {\"$ref\": \"#/definitions/link_url\"},\n        {\"description\": \"A url, section_title_text links to this\"}\n      ]\n    }\n  },\n  \"additionalProperties\": false,\n  \"required\": [\"text\"],\n  \"dependencies\": {\n    \"button_action\": [\"button_label\"],\n    \"button_url\": [\"button_label\"],\n    \"button_color\": [\"button_label\"],\n    \"button_background_color\": [\"button_label\"],\n    \"section_title_url\": [\"section_title_text\"]\n  }\n}\n"
  },
  {
    "path": "content-src/asrouter/templates/SimpleSnippet/_SimpleSnippet.scss",
    "content": "$section-header-height: 30px;\n$icon-width: 54px; // width of primary icon + margin\n\n.SimpleSnippet {\n  &.tall {\n    padding: 27px 0;\n  }\n\n  p em {\n    color: $grey-90;\n    font-style: normal;\n    background: $yellow-50;\n  }\n\n  &.bold,\n  &.takeover {\n    .donation-form-url,\n    .donation-amount {\n      padding-top: 8px;\n      padding-bottom: 8px;\n    }\n  }\n\n  &.bold {\n    height: 176px;\n\n    .body {\n      font-size: 14px;\n      line-height: 20px;\n      margin-bottom: 20px;\n    }\n\n    .icon {\n      width: 71px;\n      height: 71px;\n    }\n  }\n\n  &.takeover {\n    height: 344px;\n\n    .body {\n      font-size: 16px;\n      line-height: 24px;\n      margin-bottom: 35px;\n    }\n\n    .icon {\n      width: 79px;\n      height: 79px;\n    }\n  }\n\n  .title {\n    font-size: inherit;\n    margin: 0;\n  }\n\n  .title-inline {\n    display: inline;\n  }\n\n  .titleIcon {\n    background-repeat: no-repeat;\n    background-size: 14px;\n    background-position: center;\n    height: 16px;\n    width: 16px;\n    margin-top: 2px;\n    margin-inline-end: 2px;\n    display: inline-block;\n    vertical-align: top;\n  }\n\n  .body {\n    display: inline;\n    margin: 0;\n  }\n\n  &.tall .icon {\n    margin-inline-end: 20px;\n  }\n\n  &.takeover,\n  &.bold {\n    .icon {\n      margin-inline-end: 20px;\n    }\n  }\n\n  .icon {\n    align-self: flex-start;\n  }\n\n  &.has-section-header .innerWrapper {\n    // account for section header being 100% width\n    flex-wrap: wrap;\n    padding-top: 7px;\n  }\n\n  // wrapper div added if section-header is displayed that allows icon/text/button\n  // to squish instead of wrapping. this is effectively replicating layout behavior\n  // when section-header is *not* present.\n  .innerContentWrapper {\n    align-items: center;\n    display: flex;\n  }\n\n  .section-header {\n    flex: 0 0 100%;\n    margin-bottom: 10px;\n  }\n\n  .section-title {\n    // color should match that of 'Recommended by Pocket' and 'Highlights' in newtab page\n    color: var(--newtab-section-header-text-color);\n    display: inline-block;\n    font-size: 13px;\n    font-weight: bold;\n    margin: 0;\n\n    a {\n      color: var(--newtab-section-header-text-color);\n      font-weight: inherit;\n      text-decoration: none;\n    }\n\n    .icon {\n      height: 16px;\n      margin-inline-end: 6px;\n      margin-top: -2px;\n      width: 16px;\n    }\n  }\n}\n"
  },
  {
    "path": "content-src/asrouter/templates/SubmitFormSnippet/SubmitFormSnippet.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport { Button } from \"../../components/Button/Button\";\nimport React from \"react\";\nimport { RichText } from \"../../components/RichText/RichText\";\nimport { safeURI } from \"../../template-utils\";\nimport { SimpleSnippet } from \"../SimpleSnippet/SimpleSnippet\";\nimport { SnippetBase } from \"../../components/SnippetBase/SnippetBase\";\n\n// Alt text placeholder in case the prop from the server isn't available\nconst ICON_ALT_TEXT = \"\";\n\nexport class SubmitFormSnippet extends React.PureComponent {\n  constructor(props) {\n    super(props);\n    this.expandSnippet = this.expandSnippet.bind(this);\n    this.handleSubmit = this.handleSubmit.bind(this);\n    this.handleSubmitAttempt = this.handleSubmitAttempt.bind(this);\n    this.onInputChange = this.onInputChange.bind(this);\n    this.state = {\n      expanded: false,\n      submitAttempted: false,\n      signupSubmitted: false,\n      signupSuccess: false,\n      disableForm: false,\n    };\n  }\n\n  handleSubmitAttempt() {\n    if (!this.state.submitAttempted) {\n      this.setState({ submitAttempted: true });\n    }\n  }\n\n  async handleSubmit(event) {\n    let json;\n\n    if (this.state.disableForm) {\n      return;\n    }\n\n    event.preventDefault();\n    this.setState({ disableForm: true });\n    this.props.sendUserActionTelemetry({\n      event: \"CLICK_BUTTON\",\n      value: \"conversion-subscribe-activation\",\n      id: \"NEWTAB_FOOTER_BAR_CONTENT\",\n    });\n\n    if (this.props.form_method.toUpperCase() === \"GET\") {\n      this.props.onBlock({ preventDismiss: true });\n      this.refs.form.submit();\n      return;\n    }\n\n    const { url, formData } = this.props.processFormData\n      ? this.props.processFormData(this.refs.mainInput, this.props)\n      : { url: this.refs.form.action, formData: new FormData(this.refs.form) };\n\n    try {\n      const fetchRequest = new Request(url, {\n        body: formData,\n        method: \"POST\",\n        credentials: \"omit\",\n      });\n      const response = await fetch(fetchRequest); // eslint-disable-line fetch-options/no-fetch-credentials\n      json = await response.json();\n    } catch (err) {\n      console.log(err); // eslint-disable-line no-console\n    }\n\n    if (json && json.status === \"ok\") {\n      this.setState({ signupSuccess: true, signupSubmitted: true });\n      if (!this.props.content.do_not_autoblock) {\n        this.props.onBlock({ preventDismiss: true });\n      }\n      this.props.sendUserActionTelemetry({\n        event: \"CLICK_BUTTON\",\n        value: \"subscribe-success\",\n        id: \"NEWTAB_FOOTER_BAR_CONTENT\",\n      });\n    } else {\n      // eslint-disable-next-line no-console\n      console.error(\n        \"There was a problem submitting the form\",\n        json || \"[No JSON response]\"\n      );\n      this.setState({ signupSuccess: false, signupSubmitted: true });\n      this.props.sendUserActionTelemetry({\n        event: \"CLICK_BUTTON\",\n        value: \"subscribe-error\",\n        id: \"NEWTAB_FOOTER_BAR_CONTENT\",\n      });\n    }\n\n    this.setState({ disableForm: false });\n  }\n\n  expandSnippet() {\n    this.props.sendUserActionTelemetry({\n      event: \"CLICK_BUTTON\",\n      value: \"scene1-button-learn-more\",\n      id: this.props.UISurface,\n    });\n\n    this.setState({\n      expanded: true,\n      signupSuccess: false,\n      signupSubmitted: false,\n    });\n  }\n\n  renderHiddenFormInputs() {\n    const { hidden_inputs } = this.props.content;\n\n    if (!hidden_inputs) {\n      return null;\n    }\n\n    return Object.keys(hidden_inputs).map((key, idx) => (\n      <input key={idx} type=\"hidden\" name={key} value={hidden_inputs[key]} />\n    ));\n  }\n\n  renderDisclaimer() {\n    const { content } = this.props;\n    if (!content.scene2_disclaimer_html) {\n      return null;\n    }\n    return (\n      <p className=\"disclaimerText\">\n        <RichText\n          text={content.scene2_disclaimer_html}\n          localization_id=\"disclaimer_html\"\n          links={content.links}\n          doNotAutoBlock={true}\n          openNewWindow={true}\n          sendClick={this.props.sendClick}\n        />\n      </p>\n    );\n  }\n\n  renderFormPrivacyNotice() {\n    const { content } = this.props;\n    if (!content.scene2_privacy_html) {\n      return null;\n    }\n    return (\n      <p className=\"privacyNotice\">\n        <input\n          type=\"checkbox\"\n          id=\"id_privacy\"\n          name=\"privacy\"\n          required=\"required\"\n        />\n        <label htmlFor=\"id_privacy\">\n          <RichText\n            text={content.scene2_privacy_html}\n            localization_id=\"privacy_html\"\n            links={content.links}\n            doNotAutoBlock={true}\n            openNewWindow={true}\n            sendClick={this.props.sendClick}\n          />\n        </label>\n      </p>\n    );\n  }\n\n  renderSignupSubmitted() {\n    const { content } = this.props;\n    const isSuccess = this.state.signupSuccess;\n    const successTitle = isSuccess && content.success_title;\n    const bodyText = isSuccess\n      ? { success_text: content.success_text }\n      : { error_text: content.error_text };\n    const retryButtonText = content.retry_button_label;\n    return (\n      <SnippetBase {...this.props}>\n        <div className=\"submissionStatus\">\n          {successTitle ? (\n            <h2 className=\"submitStatusTitle\">{successTitle}</h2>\n          ) : null}\n          <p>\n            <RichText\n              {...bodyText}\n              localization_id={isSuccess ? \"success_text\" : \"error_text\"}\n            />\n            {isSuccess ? null : (\n              <Button onClick={this.expandSnippet}>{retryButtonText}</Button>\n            )}\n          </p>\n        </div>\n      </SnippetBase>\n    );\n  }\n\n  onInputChange(event) {\n    if (!this.props.validateInput) {\n      return;\n    }\n    const hasError = this.props.validateInput(\n      event.target.value,\n      this.props.content\n    );\n    event.target.setCustomValidity(hasError);\n  }\n\n  renderInput() {\n    const placholder =\n      this.props.content.scene2_email_placeholder_text ||\n      this.props.content.scene2_input_placeholder;\n    return (\n      <input\n        ref=\"mainInput\"\n        type={this.props.inputType || \"email\"}\n        className={`mainInput${this.state.submitAttempted ? \"\" : \" clean\"}`}\n        name=\"email\"\n        required={true}\n        placeholder={placholder}\n        onChange={this.props.validateInput ? this.onInputChange : null}\n      />\n    );\n  }\n\n  renderSignupView() {\n    const { content } = this.props;\n    const containerClass = `SubmitFormSnippet ${this.props.className}`;\n    return (\n      <SnippetBase\n        {...this.props}\n        className={containerClass}\n        footerDismiss={true}\n      >\n        {content.scene2_icon ? (\n          <div className=\"scene2Icon\">\n            <img\n              src={safeURI(content.scene2_icon)}\n              className=\"icon-light-theme\"\n              alt={content.scene2_icon_alt_text || ICON_ALT_TEXT}\n            />\n            <img\n              src={safeURI(\n                content.scene2_icon_dark_theme || content.scene2_icon\n              )}\n              className=\"icon-dark-theme\"\n              alt={content.scene2_icon_alt_text || ICON_ALT_TEXT}\n            />\n          </div>\n        ) : null}\n        <div className=\"message\">\n          <p>\n            {content.scene2_title && (\n              <h3 className=\"scene2Title\">{content.scene2_title}</h3>\n            )}{\" \"}\n            {content.scene2_text && (\n              <RichText\n                scene2_text={content.scene2_text}\n                localization_id=\"scene2_text\"\n              />\n            )}\n          </p>\n        </div>\n        <form\n          action={this.props.form_action}\n          method={this.props.form_method}\n          onSubmit={this.handleSubmit}\n          ref=\"form\"\n        >\n          {this.renderHiddenFormInputs()}\n          <div>\n            {this.renderInput()}\n            <button\n              type=\"submit\"\n              className=\"ASRouterButton primary\"\n              onClick={this.handleSubmitAttempt}\n              ref=\"formSubmitBtn\"\n            >\n              {content.scene2_button_label}\n            </button>\n          </div>\n          {this.renderFormPrivacyNotice() || this.renderDisclaimer()}\n        </form>\n      </SnippetBase>\n    );\n  }\n\n  getFirstSceneContent() {\n    return Object.keys(this.props.content)\n      .filter(key => key.includes(\"scene1\"))\n      .reduce((acc, key) => {\n        acc[key.substr(7)] = this.props.content[key];\n        return acc;\n      }, {});\n  }\n\n  render() {\n    const content = { ...this.props.content, ...this.getFirstSceneContent() };\n\n    if (this.state.signupSubmitted) {\n      return this.renderSignupSubmitted();\n    }\n    if (this.state.expanded) {\n      return this.renderSignupView();\n    }\n    return (\n      <SimpleSnippet\n        {...this.props}\n        content={content}\n        onButtonClick={this.expandSnippet}\n      />\n    );\n  }\n}\n"
  },
  {
    "path": "content-src/asrouter/templates/SubmitFormSnippet/SubmitFormSnippet.schema.json",
    "content": "{\n  \"title\": \"SubmitFormSnippet\",\n  \"description\": \"A template with two states: a SimpleSnippet and another that contains a form\",\n  \"version\": \"1.2.0\",\n  \"type\": \"object\",\n  \"definitions\": {\n    \"plainText\": {\n      \"description\": \"Plain text (no HTML allowed)\",\n      \"type\": \"string\"\n    },\n    \"richText\": {\n      \"description\": \"Text with HTML subset allowed: i, b, u, strong, em, br\",\n      \"type\": \"string\"\n    },\n    \"link_url\": {\n      \"description\": \"Target for links or buttons\",\n      \"type\": \"string\",\n      \"format\": \"uri\"\n    }\n  },\n  \"properties\": {\n    \"locale\": {\n      \"type\": \"string\",\n      \"description\": \"Two to five character string for the locale code\"\n    },\n    \"country\": {\n      \"type\": \"string\",\n      \"description\": \"Two character string for the country code (used for SMS)\"\n    },\n    \"scene1_title\": {\n      \"allof\": [\n        {\"$ref\": \"#/definitions/plainText\"},\n        {\"description\": \"snippet title displayed before snippet text\"}\n      ]\n    },\n    \"scene1_text\": {\n      \"allOf\": [\n        {\"$ref\": \"#/definitions/richText\"},\n        {\"description\": \"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br\"}\n      ]\n    },\n    \"scene1_section_title_icon\": {\n      \"type\": \"string\",\n      \"description\": \"Section title icon for scene 1. 16x16px. SVG or PNG preferred. scene1_section_title_text must also be specified to display.\"\n    },\n    \"scene1_section_title_icon_dark_theme\": {\n      \"type\": \"string\",\n      \"description\": \"Section title icon for scene 1, dark theme variant. 16x16px. SVG or PNG preferred. scene1_section_title_text must also be specified to display.\"\n    },\n    \"scene1_section_title_text\": {\n      \"type\": \"string\",\n      \"description\": \"Section title text for scene 1. scene1_section_title_icon must also be specified to display.\"\n    },\n    \"scene1_section_title_url\": {\n      \"allOf\": [\n        {\"$ref\": \"#/definitions/link_url\"},\n        {\"description\": \"A url, scene1_section_title_text links to this\"}\n      ]\n    },\n    \"scene2_title\": {\n      \"allOf\": [\n        {\"$ref\": \"#/definitions/plainText\"},\n        {\"description\": \"Title displayed before text in scene 2. Should be plain text.\"}\n      ]\n    },\n    \"scene2_text\": {\n      \"allOf\": [\n        {\"$ref\": \"#/definitions/richText\"},\n        {\"description\": \"Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br\"}\n      ]\n    },\n    \"scene1_icon\": {\n      \"type\": \"string\",\n      \"description\": \"Snippet icon. 64x64px. SVG or PNG preferred.\"\n    },\n    \"scene1_icon_dark_theme\": {\n      \"type\": \"string\",\n      \"description\": \"Snippet icon. Dark theme variant. 64x64px. SVG or PNG preferred.\"\n    },\n    \"scene1_icon_alt_text\": {\n      \"type\": \"string\",\n      \"description\": \"Alt text describing scene1 icon for screen readers\",\n      \"default\": \"\"\n    },\n    \"scene1_title_icon\": {\n      \"type\": \"string\",\n      \"description\": \"Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale.\"\n    },\n    \"scene1_title_icon_dark_theme\": {\n      \"type\": \"string\",\n      \"description\": \"Small icon that shows up before the title / text. Dark theme variant. 16x16px. SVG or PNG preferred. Grayscale.\"\n    },\n    \"scene1_title_icon_alt_text\": {\n      \"type\": \"string\",\n      \"description\": \"Alt text describing scene1 title icon for screen readers\",\n      \"default\": \"\"\n    },\n    \"form_action\": {\n      \"type\": \"string\",\n      \"description\": \"Endpoint to submit form data.\"\n    },\n    \"success_title\": {\n      \"type\": \"string\",\n      \"description\": \"(send to device) Title shown before text on successful registration.\"\n    },\n    \"success_text\": {\n      \"type\": \"string\",\n      \"description\": \"Message shown on successful registration.\"\n    },\n    \"error_text\": {\n      \"type\": \"string\",\n      \"description\": \"Message shown if registration failed.\"\n    },\n    \"scene2_email_placeholder_text\": {\n      \"type\": \"string\",\n      \"description\": \"Value to show while input is empty.\"\n    },\n    \"scene2_input_placeholder\": {\n      \"type\": \"string\",\n      \"description\": \"(send to device) Value to show while input is empty.\"\n    },\n    \"scene2_button_label\": {\n      \"type\": \"string\",\n      \"description\": \"Label for form submit button\"\n    },\n    \"scene2_privacy_html\": {\n      \"type\": \"string\",\n      \"description\": \"Information about how the form data is used.\"\n    },\n    \"scene2_disclaimer_html\": {\n      \"type\": \"string\",\n      \"description\": \"(send to device) Html for disclaimer and link underneath input box.\"\n    },\n    \"scene2_dismiss_button_text\": {\n      \"type\": \"string\",\n      \"description\": \"Label for the dismiss button when the sign-up form is expanded.\"\n    },\n    \"scene2_icon\": {\n      \"type\": \"string\",\n      \"description\": \"(send to device) Image to display above the form. 98x98px. SVG or PNG preferred.\"\n    },\n    \"scene2_icon_dark_theme\": {\n      \"type\": \"string\",\n      \"description\": \"(send to device) Image to display above the form. Dark theme variant. 98x98px. SVG or PNG preferred.\"\n    },\n    \"scene2_icon_alt_text\": {\n      \"type\": \"string\",\n      \"description\": \"Alt text describing scene2 icon for screen readers\",\n      \"default\": \"\"\n    },\n    \"scene2_newsletter\": {\n      \"type\": \"string\",\n      \"description\": \"Newsletter/basket id user is subscribing to. Must be a value from the 'Slug' column here: https://basket.mozilla.org/news/. Default 'mozilla-foundation'.\"\n    },\n    \"hidden_inputs\": {\n      \"type\": \"object\",\n      \"description\": \"Each entry represents a hidden input, key is used as value for the name property.\"\n    },\n    \"scene1_button_label\": {\n      \"allOf\": [\n        {\"$ref\": \"#/definitions/plainText\"},\n        {\"description\": \"Text for a button next to main snippet text that links to button_url. Requires button_url.\"}\n      ]\n    },\n    \"scene1_button_color\": {\n      \"type\": \"string\",\n      \"description\": \"The text color of the button. Valid CSS color.\"\n    },\n    \"scene1_button_background_color\": {\n      \"type\": \"string\",\n      \"description\": \"The background color of the button. Valid CSS color.\"\n    },\n    \"retry_button_label\": {\n      \"allOf\": [\n        {\"$ref\": \"#/definitions/plainText\"},\n        {\"description\": \"Text for the button in the event of a submission error/failure.\"}\n      ],\n      \"default\": \"Try again\"\n    },\n    \"do_not_autoblock\": {\n      \"type\": \"boolean\",\n      \"description\": \"Used to prevent blocking the snippet after the CTA (link or button) has been clicked\"\n    },\n    \"include_sms\": {\n      \"type\": \"boolean\",\n      \"description\": \"(send to device) Allow users to send an SMS message with the form?\"\n    },\n    \"message_id_sms\": {\n      \"type\": \"string\",\n      \"description\": \"(send to device) Newsletter/basket id representing the SMS message to be sent.\"\n    },\n    \"message_id_email\": {\n      \"type\": \"string\",\n      \"description\": \"(send to device) Newsletter/basket id representing the email message to be sent. Must be a value from the 'Slug' column here: https://basket.mozilla.org/news/.\"\n    },\n    \"utm_campaign\": {\n      \"type\": \"string\",\n      \"description\": \"(fxa) Value to pass through to GA as utm_campaign.\"\n    },\n    \"utm_term\": {\n      \"type\": \"string\",\n      \"description\": \"(fxa) Value to pass through to GA as utm_term.\"\n    },\n    \"links\": {\n      \"additionalProperties\": {\n        \"url\": {\n          \"allOf\": [\n            {\"$ref\": \"#/definitions/link_url\"},\n            {\"description\": \"The url where the link points to.\"}\n          ]\n        },\n        \"metric\": {\n          \"type\": \"string\",\n          \"description\": \"Custom event name sent with telemetry event.\"\n        }\n      }\n    }\n  },\n  \"additionalProperties\": false,\n  \"required\": [\"scene1_text\", \"scene2_text\", \"scene1_button_label\"],\n  \"dependencies\": {\n    \"scene1_button_color\": [\"scene1_button_label\"],\n    \"scene1_button_background_color\": [\"scene1_button_label\"]\n  }\n}\n"
  },
  {
    "path": "content-src/asrouter/templates/SubmitFormSnippet/_SubmitFormSnippet.scss",
    "content": ".SubmitFormSnippet {\n  flex-direction: column;\n  flex: 1 1 100%;\n  width: 100%;\n\n  .disclaimerText {\n    margin: 20px 0 0;\n    font-size: 12px;\n    color: var(--newtab-text-secondary-color);\n  }\n\n  p {\n    margin: 0;\n  }\n\n  &.send_to_device_snippet {\n    text-align: center;\n\n    .message {\n      font-size: 16px;\n      margin-bottom: 20px;\n    }\n\n    .scene2Title {\n      font-size: 24px;\n      display: block;\n    }\n  }\n\n  .ASRouterButton {\n    &.primary {\n      flex: 1 1 0;\n    }\n  }\n\n  .scene2Icon {\n    width: 100%;\n    margin-bottom: 20px;\n\n    img {\n      width: 98px;\n      display: inline-block;\n    }\n  }\n\n  .scene2Title {\n    font-size: inherit;\n    margin: 0 0 10px;\n    font-weight: bold;\n    display: inline;\n  }\n\n  form {\n    display: flex;\n    flex-direction: column;\n    width: 100%;\n  }\n\n  .message {\n    font-size: 14px;\n    align-self: stretch;\n    flex: 0 0 100%;\n    margin-bottom: 10px;\n  }\n\n  .privacyNotice {\n    font-size: 12px;\n    color: var(--newtab-text-secondary-color);\n    margin-top: 10px;\n    display: flex;\n    flex: 0 0 100%;\n  }\n\n  .innerWrapper {\n    max-width: 670px;\n    flex-wrap: wrap;\n    justify-items: center;\n    padding-top: 40px;\n    padding-bottom: 40px;\n  }\n\n  .footer {\n    width: 100%;\n    margin: 0 auto;\n    text-align: right;\n    background-color: var(--newtab-background-color);\n    padding: 10px 0;\n\n    .footer-content {\n      margin: 0 auto;\n      max-width: 768px;\n      width: 100%;\n      text-align: right;\n\n      [dir='rtl'] & {\n        text-align: left;\n      }\n    }\n  }\n\n  input {\n    &.mainInput {\n      border-radius: 2px;\n      background-color: var(--newtab-textbox-background-color);\n      border: $input-border;\n      padding: 0 8px;\n      height: 100%;\n      font-size: 14px;\n      width: 50%;\n\n      &.clean {\n        &:invalid,\n        &:required {\n          box-shadow: none;\n        }\n      }\n\n      &:focus {\n        border: $input-border-active;\n        box-shadow: var(--newtab-textbox-focus-boxshadow);\n      }\n    }\n  }\n}\n\n.submissionStatus {\n  text-align: center;\n  font-size: 14px;\n  padding: 20px 0;\n\n  .submitStatusTitle {\n    font-size: 20px;\n  }\n}\n"
  },
  {
    "path": "content-src/asrouter/templates/Trailhead/Trailhead.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport { actionCreators as ac } from \"common/Actions.jsm\";\nimport { ModalOverlayWrapper } from \"../../components/ModalOverlay/ModalOverlay\";\nimport { FxASignupForm } from \"../../components/FxASignupForm/FxASignupForm\";\nimport { addUtmParams } from \"../FirstRun/addUtmParams\";\nimport React from \"react\";\n\n// From resource://devtools/client/shared/focus.js\nconst FOCUSABLE_SELECTOR = [\n  \"a[href]:not([tabindex='-1'])\",\n  \"button:not([disabled]):not([tabindex='-1'])\",\n  \"iframe:not([tabindex='-1'])\",\n  \"input:not([disabled]):not([tabindex='-1'])\",\n  \"select:not([disabled]):not([tabindex='-1'])\",\n  \"textarea:not([disabled]):not([tabindex='-1'])\",\n  \"[tabindex]:not([tabindex='-1'])\",\n].join(\", \");\n\nexport class Trailhead extends React.PureComponent {\n  constructor(props) {\n    super(props);\n    this.closeModal = this.closeModal.bind(this);\n    this.onStartBlur = this.onStartBlur.bind(this);\n  }\n\n  get dialog() {\n    return this.props.document.getElementById(\"trailheadDialog\");\n  }\n\n  componentDidMount() {\n    // We need to remove hide-main since we should show it underneath everything that has rendered\n    this.props.document.body.classList.remove(\"hide-main\");\n\n    // The rest of the page is \"hidden\" to screen readers when the modal is open\n    this.props.document\n      .getElementById(\"root\")\n      .setAttribute(\"aria-hidden\", \"true\");\n  }\n\n  onStartBlur(event) {\n    // Make sure focus stays within the dialog when tabbing from the button\n    const { dialog } = this;\n    if (\n      event.relatedTarget &&\n      !(\n        dialog.compareDocumentPosition(event.relatedTarget) &\n        dialog.DOCUMENT_POSITION_CONTAINED_BY\n      )\n    ) {\n      dialog.querySelector(FOCUSABLE_SELECTOR).focus();\n    }\n  }\n\n  closeModal(ev) {\n    global.removeEventListener(\"visibilitychange\", this.closeModal);\n    this.props.document.body.classList.remove(\"welcome\");\n    this.props.document.getElementById(\"root\").removeAttribute(\"aria-hidden\");\n    this.props.onNextScene();\n\n    // If closeModal() was triggered by a visibilitychange event, the user actually\n    // submitted the email form so we don't send a SKIPPED_SIGNIN ping.\n    if (!ev || ev.type !== \"visibilitychange\") {\n      this.props.dispatch(\n        ac.UserEvent({ event: \"SKIPPED_SIGNIN\", ...this._getFormInfo() })\n      );\n    }\n\n    // Bug 1190882 - Focus in a disappearing dialog confuses screen readers\n    this.props.document.activeElement.blur();\n  }\n\n  /**\n   * Report to telemetry additional information about the form submission.\n   */\n  _getFormInfo() {\n    const value = { has_flow_params: !!this.props.flowParams.flowId.length };\n    return { value };\n  }\n\n  render() {\n    const { props } = this;\n    const { UTMTerm } = props;\n    const { content } = props.message;\n    const innerClassName = [\"trailhead\", content && content.className]\n      .filter(v => v)\n      .join(\" \");\n\n    return (\n      <ModalOverlayWrapper\n        innerClassName={innerClassName}\n        onClose={this.closeModal}\n        id=\"trailheadDialog\"\n        headerId=\"trailheadHeader\"\n        hasDismissIcon={true}\n      >\n        <div className=\"trailheadInner\">\n          <div className=\"trailheadContent\">\n            <h1 data-l10n-id={content.title.string_id} id=\"trailheadHeader\" />\n            {content.subtitle && (\n              <p data-l10n-id={content.subtitle.string_id} />\n            )}\n            <ul className=\"trailheadBenefits\">\n              {content.benefits.map(item => (\n                <li key={item.id} className={item.id}>\n                  <h2 data-l10n-id={item.title.string_id} />\n                  <p data-l10n-id={item.text.string_id} />\n                </li>\n              ))}\n            </ul>\n            <a\n              className=\"trailheadLearn\"\n              data-l10n-id={content.learn.text.string_id}\n              href={addUtmParams(content.learn.url, UTMTerm)}\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n            />\n          </div>\n          <div className=\"trailhead-join-form\">\n            <FxASignupForm\n              document={this.props.document}\n              content={content}\n              dispatch={this.props.dispatch}\n              fxaEndpoint={this.props.fxaEndpoint}\n              UTMTerm={UTMTerm}\n              flowParams={this.props.flowParams}\n              onClose={this.closeModal}\n            />\n          </div>\n        </div>\n\n        <button\n          className=\"trailheadStart\"\n          data-l10n-id={content.skipButton.string_id}\n          onBlur={this.onStartBlur}\n          onClick={this.closeModal}\n        />\n      </ModalOverlayWrapper>\n    );\n  }\n}\n\nTrailhead.defaultProps = {\n  flowParams: { deviceId: \"\", flowId: \"\", flowBeginTime: \"\" },\n};\n"
  },
  {
    "path": "content-src/asrouter/templates/Trailhead/_Trailhead.scss",
    "content": ".trailhead {\n  $benefit-icon-size: 62px;\n  $benefit-icon-spacing: $benefit-icon-size + 12px;\n  $benefit-icon-size-small: 40px;\n  $benefit-icon-spacing-small: $benefit-icon-size-small + 12px;\n  $responsive-breakpoint: 850px;\n  $logo-size: 100px;\n\n  background: url('#{$image-path}trailhead/accounts-form-bg.jpg') bottom / cover;\n  color: $white;\n  height: auto;\n\n  a {\n    color: $white;\n    text-decoration: underline;\n  }\n\n  input,\n  button {\n    border-radius: 4px;\n    padding: 10px;\n  }\n\n  .trailheadInner {\n    $content-spacing: 40px;\n\n    display: grid;\n    grid-column-gap: $content-spacing;\n    grid-template-columns: 5fr 3fr;\n    padding: $content-spacing 60px;\n  }\n\n  .trailheadContent {\n    h1 {\n      font-size: 36px;\n      font-weight: 200;\n      line-height: 46px;\n      margin: 0;\n    }\n\n    .trailheadLearn {\n      display: block;\n      margin-top: 30px;\n\n      @media (min-width: $responsive-breakpoint) {\n        margin-inline-start: $benefit-icon-spacing;\n      }\n    }\n  }\n\n  .trailhead-join-form {\n    background: url('#{$image-path}trailhead/firefox-logo.png') top center / $logo-size no-repeat;\n    color: $white;\n    min-width: 260px;\n    padding-top: $logo-size;\n  }\n\n  &.syncCohort {\n    left: calc(50% - 430px);\n    width: 860px;\n\n    @media (max-width: 860px) {\n      left: 0;\n      width: 100%;\n    }\n\n    .trailheadInner {\n      grid-template-columns: 4fr 3fr;\n    }\n\n    .trailheadContent {\n      .trailheadBenefits {\n        background: url('#{$image-path}sync-devices-trailhead.svg');\n        background-position: center center;\n        background-repeat: no-repeat;\n        background-size: contain;\n        height: 200px;\n        margin-inline-end: 60px;\n      }\n\n      .trailheadLearn {\n        margin-inline-start: 0;\n      }\n    }\n  }\n\n  .trailheadBenefits {\n    padding: 0;\n\n    li {\n      background-position: left 6px;\n      background-repeat: no-repeat;\n      background-size: $benefit-icon-size-small;\n      -moz-context-properties: fill;\n      fill: $blue-50;\n      list-style: none;\n      padding-top: 8px;\n\n\n      @media (min-width: $responsive-breakpoint) {\n        background-position-y: 4px;\n        background-size: $benefit-icon-size;\n        margin-inline-end: 60px;\n        padding-inline-start: $benefit-icon-spacing;\n      }\n\n      &:dir(rtl) {\n        background-position-x: right;\n      }\n\n      &.knowledge,\n      &.monitor {\n        background-image: url('#{$image-path}trailhead/benefit-knowledge.png');\n      }\n\n      &.lockwise,\n      &.privacy {\n        background-image: url('#{$image-path}trailhead/benefit-privacy.png');\n      }\n\n      &.products {\n        background-image: url('#{$image-path}trailhead/benefit-products.png');\n      }\n\n      &.sync {\n        background-image: url('#{$image-path}trailhead/benefit-sync.png');\n      }\n    }\n\n    h2 {\n      text-align: start;\n      line-height: inherit;\n      color: $violet-20;\n      font-size: 22px;\n      font-weight: 400;\n      margin: 0 0 4px;\n      padding-inline-start: $benefit-icon-spacing-small;\n\n      @media (min-width: $responsive-breakpoint) {\n        padding-inline-start: 0;\n      }\n    }\n\n    p {\n      color: $white;\n      font-size: 15px;\n      line-height: 22px;\n      margin: 4px 0 15px;\n    }\n  }\n\n  .trailheadStart {\n    border: 1px solid $white-50;\n    cursor: pointer;\n    display: block;\n    font-size: 15px;\n    font-weight: 400;\n    margin: 0 auto 40px;\n    min-width: 300px;\n    padding: 14px;\n\n    &:hover,\n    &:focus {\n      background-color: $trailhead-blue-60;\n      border-color: transparent;\n    }\n\n    &:focus {\n      outline: dotted 1px;\n    }\n\n    &:active {\n      background-color: $trailhead-blue-70;\n    }\n  }\n\n  .trailheadInner,\n  .trailheadStart {\n    animation: fadeIn 0.4s;\n  }\n}\n\n.trailheadCards {\n  background: var(--trailhead-cards-background-color);\n  overflow: hidden;\n  text-align: center;\n  // Note: should match TRANSITION_LENGTH in FirstRun.jsx\n  transition: max-height 0.5s $photon-easing;\n\n  // This is needed for the transition to work, but will cut off content at the smallest breakpoint\n  @media (min-width: $break-point-medium) {\n    max-height: 1000px;\n  }\n\n  &.collapsed {\n    max-height: 0;\n  }\n\n  h1 {\n    font-size: 36px;\n    font-weight: 200;\n    margin: 0 0 40px;\n    color: var(--trailhead-header-text-color);\n  }\n}\n\n.trailheadCardsInner {\n  margin: auto;\n  padding: 40px $section-horizontal-padding;\n\n  @media (min-width: $break-point-medium) {\n    width: $wrapper-max-width-medium;\n  }\n\n  @media (min-width: $break-point-large) {\n    width: $wrapper-max-width-large;\n  }\n\n  @media (min-width: $break-point-widest) {\n    width: $wrapper-max-width-widest;\n  }\n\n  .icon-dismiss {\n    border: 0;\n    cursor: pointer;\n    inset-inline-end: 15px;\n    padding: 15px;\n    opacity: 0.75;\n    position: absolute;\n    top: 15px;\n\n    &:hover,\n    &:focus {\n      background-color: var(--newtab-element-hover-color);\n    }\n  }\n}\n\n.trailheadCardGrid {\n  display: grid;\n  grid-gap: $base-gutter;\n  margin: 0;\n  opacity: 0;\n  transition: opacity 0.4s;\n  transition-delay: 0.1s;\n  grid-auto-rows: 1fr;\n\n  &.show {\n    opacity: 1;\n  }\n\n  @media (min-width: $break-point-medium) {\n    grid-template-columns: repeat(auto-fit, $card-width);\n  }\n\n  @media (min-width: $break-point-widest) {\n    grid-template-columns: repeat(auto-fit, $card-width-large);\n  }\n}\n\n.trailheadCard {\n  position: relative;\n  background: var(--newtab-card-background-color);\n  border-radius: 4px;\n  box-shadow: var(--newtab-card-shadow);\n\n  font-size: 13px;\n  padding: 20px 40px 60px;\n\n  @media (max-width: 865px) {\n    padding: 20px 40px;\n  }\n\n  @media (min-width: $break-point-widest) {\n    font-size: 15px;\n  }\n\n  .onboardingTitle {\n    font-weight: normal;\n    color: var(--newtab-text-primary-color);\n    margin: 10px 0 4px;\n    font-size: 15px;\n\n    @media (min-width: $break-point-widest) {\n      font-size: 18px;\n    }\n  }\n\n  .onboardingText {\n    margin: 0 0 60px;\n    color: var(--newtab-text-conditional-color);\n    line-height: 1.5;\n    font-weight: 200;\n  }\n\n  .onboardingButton {\n    color: var(--newtab-text-conditional-color);\n    background: var(--trailhead-card-button-background-color);\n    border: 0;\n    margin: 14px;\n    min-width: 70%;\n    padding: 6px 14px;\n    white-space: pre-wrap;\n\n    &:focus,\n    &:hover {\n      box-shadow: none;\n      background: var(--trailhead-card-button-background-hover-color);\n    }\n\n    &:focus {\n      outline: dotted 1px;\n    }\n\n    &:active {\n      background: var(--trailhead-card-button-background-active-color);\n    }\n  }\n\n  .onboardingButtonContainer {\n    position: absolute;\n    bottom: 16px;\n    left: 0;\n    width: 100%;\n    text-align: center;\n  }\n}\n\n.activity-stream.welcome {\n  overflow: hidden;\n}\n\n.inline-onboarding {\n  &.activity-stream.welcome {\n    overflow-y: hidden;\n  }\n\n  .trailhead.modalOverlayInner {\n    position: absolute;\n  }\n\n  .outer-wrapper {\n    position: relative;\n    display: block;\n\n    .prefs-button {\n      button {\n        position: absolute;\n      }\n    }\n  }\n\n  .asrouter-toggle {\n    position: absolute;\n  }\n}\n\n.error {\n  display: none;\n}\n\n.error.active {\n  display: block;\n  padding: 5px 12px;\n  animation: fade-down 450ms;\n  font-size: 12px;\n  font-weight: 500;\n  color: $white;\n  background-color: $red-60;\n  position: absolute;\n  inset-inline-start: 50px;\n  top: -28px;\n  border-radius: 2px;\n\n  &::before {\n    inset-inline-start: 12px;\n    background: $red-60;\n    bottom: -8px;\n    content: '.';\n    height: 16px;\n    position: absolute;\n    text-indent: -999px;\n    transform: rotate(45deg);\n    white-space: nowrap;\n    width: 16px;\n    z-index: -1;\n  }\n}\n\n@keyframes fade-down {\n  0% {\n    opacity: 0;\n    transform: translateY(-15px);\n  }\n\n  100% {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n"
  },
  {
    "path": "content-src/asrouter/templates/template-manifest.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport { EOYSnippet } from \"./EOYSnippet/EOYSnippet\";\nimport { FXASignupSnippet } from \"./FXASignupSnippet/FXASignupSnippet\";\nimport { NewsletterSnippet } from \"./NewsletterSnippet/NewsletterSnippet\";\nimport { SendToDeviceSnippet } from \"./SendToDeviceSnippet/SendToDeviceSnippet\";\nimport { SimpleBelowSearchSnippet } from \"./SimpleBelowSearchSnippet/SimpleBelowSearchSnippet\";\nimport { SimpleSnippet } from \"./SimpleSnippet/SimpleSnippet\";\n\n// Key names matching schema name of templates\nexport const SnippetsTemplates = {\n  simple_snippet: SimpleSnippet,\n  newsletter_snippet: NewsletterSnippet,\n  fxa_signup_snippet: FXASignupSnippet,\n  send_to_device_snippet: SendToDeviceSnippet,\n  eoy_snippet: EOYSnippet,\n  simple_below_search_snippet: SimpleBelowSearchSnippet,\n};\n"
  },
  {
    "path": "content-src/components/A11yLinkButton/A11yLinkButton.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport React from \"react\";\n\nexport function A11yLinkButton(props) {\n  // function for merging classes, if necessary\n  let className = \"a11y-link-button\";\n  if (props.className) {\n    className += ` ${props.className}`;\n  }\n  return (\n    <button type=\"button\" {...props} className={className}>\n      {props.children}\n    </button>\n  );\n}\n"
  },
  {
    "path": "content-src/components/A11yLinkButton/_A11yLinkButton.scss",
    "content": "\n.a11y-link-button {\n  border: 0;\n  padding: 0;\n  cursor: pointer;\n  text-align: unset;\n  color: var(--newtab-link-primary-color);\n\n  &:hover,\n  &:focus {\n    text-decoration: underline;\n  }\n}\n"
  },
  {
    "path": "content-src/components/ASRouterAdmin/ASRouterAdmin.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport { actionCreators as ac, actionTypes as at } from \"common/Actions.jsm\";\nimport { ASRouterUtils } from \"../../asrouter/asrouter-content\";\nimport { connect } from \"react-redux\";\nimport { ModalOverlay } from \"../../asrouter/components/ModalOverlay/ModalOverlay\";\nimport React from \"react\";\nimport { SimpleHashRouter } from \"./SimpleHashRouter\";\n\nconst Row = props => (\n  <tr className=\"message-item\" {...props}>\n    {props.children}\n  </tr>\n);\n\nfunction relativeTime(timestamp) {\n  if (!timestamp) {\n    return \"\";\n  }\n  const seconds = Math.floor((Date.now() - timestamp) / 1000);\n  const minutes = Math.floor((Date.now() - timestamp) / 60000);\n  if (seconds < 2) {\n    return \"just now\";\n  } else if (seconds < 60) {\n    return `${seconds} seconds ago`;\n  } else if (minutes === 1) {\n    return \"1 minute ago\";\n  } else if (minutes < 600) {\n    return `${minutes} minutes ago`;\n  }\n  return new Date(timestamp).toLocaleString();\n}\n\nconst LAYOUT_VARIANTS = {\n  basic: \"Basic default layout (on by default in nightly)\",\n  staging_spocs: \"A layout with all spocs shown\",\n  \"dev-test-all\":\n    \"A little bit of everything. Good layout for testing all components\",\n  \"dev-test-feeds\": \"Stress testing for slow feeds\",\n};\n\nexport class ToggleStoryButton extends React.PureComponent {\n  constructor(props) {\n    super(props);\n    this.handleClick = this.handleClick.bind(this);\n  }\n\n  handleClick() {\n    this.props.onClick(this.props.story);\n  }\n\n  render() {\n    return <button onClick={this.handleClick}>collapse/open</button>;\n  }\n}\n\nexport class TogglePrefCheckbox extends React.PureComponent {\n  constructor(props) {\n    super(props);\n    this.onChange = this.onChange.bind(this);\n  }\n\n  onChange(event) {\n    this.props.onChange(this.props.pref, event.target.checked);\n  }\n\n  render() {\n    return (\n      <>\n        <input\n          type=\"checkbox\"\n          checked={this.props.checked}\n          onChange={this.onChange}\n        />{\" \"}\n        {this.props.pref}{\" \"}\n      </>\n    );\n  }\n}\n\nexport class DiscoveryStreamAdmin extends React.PureComponent {\n  constructor(props) {\n    super(props);\n    this.restorePrefDefaults = this.restorePrefDefaults.bind(this);\n    this.setConfigValue = this.setConfigValue.bind(this);\n    this.expireCache = this.expireCache.bind(this);\n    this.changeEndpointVariant = this.changeEndpointVariant.bind(this);\n    this.onStoryToggle = this.onStoryToggle.bind(this);\n    this.state = {\n      toggledStories: {},\n    };\n  }\n\n  setConfigValue(name, value) {\n    this.props.dispatch(\n      ac.OnlyToMain({\n        type: at.DISCOVERY_STREAM_CONFIG_SET_VALUE,\n        data: { name, value },\n      })\n    );\n  }\n\n  restorePrefDefaults(event) {\n    this.props.dispatch(\n      ac.OnlyToMain({\n        type: at.DISCOVERY_STREAM_CONFIG_RESET_DEFAULTS,\n      })\n    );\n  }\n\n  expireCache() {\n    const { config } = this.props.state;\n    this.props.dispatch(\n      ac.OnlyToMain({\n        type: at.DISCOVERY_STREAM_CONFIG_CHANGE,\n        data: config,\n      })\n    );\n  }\n\n  changeEndpointVariant(event) {\n    const endpoint = this.props.state.config.layout_endpoint;\n    if (endpoint) {\n      this.setConfigValue(\n        \"layout_endpoint\",\n        endpoint.replace(\n          /layout_variant=.+/,\n          `layout_variant=${event.target.value}`\n        )\n      );\n    }\n  }\n\n  renderComponent(width, component) {\n    return (\n      <table>\n        <tbody>\n          <Row>\n            <td className=\"min\">Type</td>\n            <td>{component.type}</td>\n          </Row>\n          <Row>\n            <td className=\"min\">Width</td>\n            <td>{width}</td>\n          </Row>\n          {component.feed && this.renderFeed(component.feed)}\n        </tbody>\n      </table>\n    );\n  }\n\n  isCurrentVariant(id) {\n    const endpoint = this.props.state.config.layout_endpoint;\n    const isMatch = endpoint && !!endpoint.match(`layout_variant=${id}`);\n    return isMatch;\n  }\n\n  renderFeedData(url) {\n    const { feeds } = this.props.state;\n    const feed = feeds.data[url].data;\n    return (\n      <React.Fragment>\n        <h4>Feed url: {url}</h4>\n        <table>\n          <tbody>\n            {feed.recommendations.map(story => this.renderStoryData(story))}\n          </tbody>\n        </table>\n      </React.Fragment>\n    );\n  }\n\n  renderFeedsData() {\n    const { feeds } = this.props.state;\n    return (\n      <React.Fragment>\n        {Object.keys(feeds.data).map(url => this.renderFeedData(url))}\n      </React.Fragment>\n    );\n  }\n\n  renderSpocs() {\n    const { spocs } = this.props.state;\n    let spocsData = [];\n    if (spocs.data && spocs.data.spocs && spocs.data.spocs.length) {\n      spocsData = spocs.data.spocs;\n    }\n\n    return (\n      <React.Fragment>\n        <table>\n          <tbody>\n            <Row>\n              <td className=\"min\">spocs_endpoint</td>\n              <td>{spocs.spocs_endpoint}</td>\n            </Row>\n            <Row>\n              <td className=\"min\">Data last fetched</td>\n              <td>{relativeTime(spocs.lastUpdated)}</td>\n            </Row>\n          </tbody>\n        </table>\n        <h4>Spoc data</h4>\n        <table>\n          <tbody>{spocsData.map(spoc => this.renderStoryData(spoc))}</tbody>\n        </table>\n        <h4>Spoc frequency caps</h4>\n        <table>\n          <tbody>\n            {spocs.frequency_caps.map(spoc => this.renderStoryData(spoc))}\n          </tbody>\n        </table>\n      </React.Fragment>\n    );\n  }\n\n  onStoryToggle(story) {\n    const { toggledStories } = this.state;\n    this.setState({\n      toggledStories: {\n        ...toggledStories,\n        [story.id]: !toggledStories[story.id],\n      },\n    });\n  }\n\n  renderStoryData(story) {\n    let storyData = \"\";\n    if (this.state.toggledStories[story.id]) {\n      storyData = JSON.stringify(story, null, 2);\n    }\n    return (\n      <tr className=\"message-item\" key={story.id}>\n        <td className=\"message-id\">\n          <span>\n            {story.id} <br />\n          </span>\n          <ToggleStoryButton story={story} onClick={this.onStoryToggle} />\n        </td>\n        <td className=\"message-summary\">\n          <pre>{storyData}</pre>\n        </td>\n      </tr>\n    );\n  }\n\n  renderFeed(feed) {\n    const { feeds } = this.props.state;\n    if (!feed.url) {\n      return null;\n    }\n    return (\n      <React.Fragment>\n        <Row>\n          <td className=\"min\">Feed url</td>\n          <td>{feed.url}</td>\n        </Row>\n        <Row>\n          <td className=\"min\">Data last fetched</td>\n          <td>\n            {relativeTime(\n              feeds.data[feed.url] ? feeds.data[feed.url].lastUpdated : null\n            ) || \"(no data)\"}\n          </td>\n        </Row>\n      </React.Fragment>\n    );\n  }\n\n  render() {\n    const prefToggles = \"enabled hardcoded_layout show_spocs personalized collapsible\".split(\n      \" \"\n    );\n    const { config, lastUpdated, layout } = this.props.state;\n    return (\n      <div>\n        <button className=\"button\" onClick={this.restorePrefDefaults}>\n          Restore Pref Defaults\n        </button>{\" \"}\n        <button className=\"button\" onClick={this.expireCache}>\n          Expire Cache\n        </button>\n        <table>\n          <tbody>\n            {prefToggles.map(pref => (\n              <Row key={pref}>\n                <td>\n                  <TogglePrefCheckbox\n                    checked={config[pref]}\n                    pref={pref}\n                    onChange={this.setConfigValue}\n                  />\n                </td>\n              </Row>\n            ))}\n          </tbody>\n        </table>\n        <h3>Endpoint variant</h3>\n        <p>\n          You can also change this manually by changing this pref:{\" \"}\n          <code>browser.newtabpage.activity-stream.discoverystream.config</code>\n        </p>\n        <table\n          style={\n            config.enabled && !config.hardcoded_layout ? null : { opacity: 0.5 }\n          }\n        >\n          <tbody>\n            {Object.keys(LAYOUT_VARIANTS).map(id => (\n              <Row key={id}>\n                <td className=\"min\">\n                  <input\n                    type=\"radio\"\n                    value={id}\n                    checked={this.isCurrentVariant(id)}\n                    onChange={this.changeEndpointVariant}\n                  />\n                </td>\n                <td className=\"min\">{id}</td>\n                <td>{LAYOUT_VARIANTS[id]}</td>\n              </Row>\n            ))}\n          </tbody>\n        </table>\n        <h3>Caching info</h3>\n        <table style={config.enabled ? null : { opacity: 0.5 }}>\n          <tbody>\n            <Row>\n              <td className=\"min\">Data last fetched</td>\n              <td>{relativeTime(lastUpdated) || \"(no data)\"}</td>\n            </Row>\n          </tbody>\n        </table>\n        <h3>Layout</h3>\n        {layout.map((row, rowIndex) => (\n          <div key={`row-${rowIndex}`}>\n            {row.components.map((component, componentIndex) => (\n              <div key={`component-${componentIndex}`} className=\"ds-component\">\n                {this.renderComponent(row.width, component)}\n              </div>\n            ))}\n          </div>\n        ))}\n        <h3>Feeds Data</h3>\n        {this.renderFeedsData()}\n        <h3>Spocs</h3>\n        {this.renderSpocs()}\n      </div>\n    );\n  }\n}\n\nexport class ASRouterAdminInner extends React.PureComponent {\n  constructor(props) {\n    super(props);\n    this.onMessage = this.onMessage.bind(this);\n    this.handleEnabledToggle = this.handleEnabledToggle.bind(this);\n    this.handleUserPrefToggle = this.handleUserPrefToggle.bind(this);\n    this.onChangeMessageFilter = this.onChangeMessageFilter.bind(this);\n    this.findOtherBundledMessagesOfSameTemplate = this.findOtherBundledMessagesOfSameTemplate.bind(\n      this\n    );\n    this.handleExpressionEval = this.handleExpressionEval.bind(this);\n    this.onChangeTargetingParameters = this.onChangeTargetingParameters.bind(\n      this\n    );\n    this.onChangeAttributionParameters = this.onChangeAttributionParameters.bind(\n      this\n    );\n    this.setAttribution = this.setAttribution.bind(this);\n    this.onCopyTargetingParams = this.onCopyTargetingParams.bind(this);\n    this.onPasteTargetingParams = this.onPasteTargetingParams.bind(this);\n    this.onNewTargetingParams = this.onNewTargetingParams.bind(this);\n    this.state = {\n      messageFilter: \"all\",\n      evaluationStatus: {},\n      trailhead: {},\n      stringTargetingParameters: null,\n      newStringTargetingParameters: null,\n      copiedToClipboard: false,\n      pasteFromClipboard: false,\n      attributionParameters: {\n        source: \"addons.mozilla.org\",\n        campaign: \"non-fx-button\",\n        content: \"iridium@particlecore.github.io\",\n      },\n    };\n  }\n\n  onMessage({ data: action }) {\n    if (action.type === \"ADMIN_SET_STATE\") {\n      this.setState(action.data);\n      if (!this.state.stringTargetingParameters) {\n        const stringTargetingParameters = {};\n        for (const param of Object.keys(action.data.targetingParameters)) {\n          stringTargetingParameters[param] = JSON.stringify(\n            action.data.targetingParameters[param],\n            null,\n            2\n          );\n        }\n        this.setState({ stringTargetingParameters });\n      }\n    }\n  }\n\n  componentWillMount() {\n    const endpoint = ASRouterUtils.getPreviewEndpoint();\n    ASRouterUtils.sendMessage({\n      type: \"ADMIN_CONNECT_STATE\",\n      data: { endpoint },\n    });\n    ASRouterUtils.addListener(this.onMessage);\n  }\n\n  componentWillUnmount() {\n    ASRouterUtils.removeListener(this.onMessage);\n  }\n\n  findOtherBundledMessagesOfSameTemplate(template) {\n    return this.state.messages.filter(\n      msg => msg.template === template && msg.bundled\n    );\n  }\n\n  handleBlock(msg) {\n    if (msg.bundled) {\n      // If we are blocking a message that belongs to a bundle, block all other messages that are bundled of that same template\n      let bundle = this.findOtherBundledMessagesOfSameTemplate(msg.template);\n      return () => ASRouterUtils.blockBundle(bundle);\n    }\n    return () => ASRouterUtils.blockById(msg.id);\n  }\n\n  handleUnblock(msg) {\n    if (msg.bundled) {\n      // If we are unblocking a message that belongs to a bundle, unblock all other messages that are bundled of that same template\n      let bundle = this.findOtherBundledMessagesOfSameTemplate(msg.template);\n      return () => ASRouterUtils.unblockBundle(bundle);\n    }\n    return () => ASRouterUtils.unblockById(msg.id);\n  }\n\n  handleOverride(id) {\n    return () => ASRouterUtils.overrideMessage(id);\n  }\n\n  expireCache() {\n    ASRouterUtils.sendMessage({ type: \"EXPIRE_QUERY_CACHE\" });\n  }\n\n  resetPref() {\n    ASRouterUtils.sendMessage({ type: \"RESET_PROVIDER_PREF\" });\n  }\n\n  handleExpressionEval() {\n    const context = {};\n    for (const param of Object.keys(this.state.stringTargetingParameters)) {\n      const value = this.state.stringTargetingParameters[param];\n      context[param] = value ? JSON.parse(value) : null;\n    }\n    ASRouterUtils.sendMessage({\n      type: \"EVALUATE_JEXL_EXPRESSION\",\n      data: {\n        expression: this.refs.expressionInput.value,\n        context,\n      },\n    });\n  }\n\n  onChangeTargetingParameters(event) {\n    const { name } = event.target;\n    const { value } = event.target;\n\n    this.setState(({ stringTargetingParameters }) => {\n      let targetingParametersError = null;\n      const updatedParameters = { ...stringTargetingParameters };\n      updatedParameters[name] = value;\n      try {\n        JSON.parse(value);\n      } catch (e) {\n        console.log(`Error parsing value of parameter ${name}`); // eslint-disable-line no-console\n        targetingParametersError = { id: name };\n      }\n\n      return {\n        copiedToClipboard: false,\n        evaluationStatus: {},\n        stringTargetingParameters: updatedParameters,\n        targetingParametersError,\n      };\n    });\n  }\n\n  handleEnabledToggle(event) {\n    const provider = this.state.providerPrefs.find(\n      p => p.id === event.target.dataset.provider\n    );\n    const userPrefInfo = this.state.userPrefs;\n\n    const isUserEnabled =\n      provider.id in userPrefInfo ? userPrefInfo[provider.id] : true;\n    const isSystemEnabled = provider.enabled;\n    const isEnabling = event.target.checked;\n\n    if (isEnabling) {\n      if (!isUserEnabled) {\n        ASRouterUtils.sendMessage({\n          type: \"SET_PROVIDER_USER_PREF\",\n          data: { id: provider.id, value: true },\n        });\n      }\n      if (!isSystemEnabled) {\n        ASRouterUtils.sendMessage({\n          type: \"ENABLE_PROVIDER\",\n          data: provider.id,\n        });\n      }\n    } else {\n      ASRouterUtils.sendMessage({\n        type: \"DISABLE_PROVIDER\",\n        data: provider.id,\n      });\n    }\n\n    this.setState({ messageFilter: \"all\" });\n  }\n\n  handleUserPrefToggle(event) {\n    const action = {\n      type: \"SET_PROVIDER_USER_PREF\",\n      data: { id: event.target.dataset.provider, value: event.target.checked },\n    };\n    ASRouterUtils.sendMessage(action);\n    this.setState({ messageFilter: \"all\" });\n  }\n\n  onChangeMessageFilter(event) {\n    this.setState({ messageFilter: event.target.value });\n  }\n\n  // Simulate a copy event that sets to clipboard all targeting paramters and values\n  onCopyTargetingParams(event) {\n    const stringTargetingParameters = {\n      ...this.state.stringTargetingParameters,\n    };\n    for (const key of Object.keys(stringTargetingParameters)) {\n      // If the value is not set the parameter will be lost when we stringify\n      if (stringTargetingParameters[key] === undefined) {\n        stringTargetingParameters[key] = null;\n      }\n    }\n    const setClipboardData = e => {\n      e.preventDefault();\n      e.clipboardData.setData(\n        \"text\",\n        JSON.stringify(stringTargetingParameters, null, 2)\n      );\n      document.removeEventListener(\"copy\", setClipboardData);\n      this.setState({ copiedToClipboard: true });\n    };\n\n    document.addEventListener(\"copy\", setClipboardData);\n\n    document.execCommand(\"copy\");\n  }\n\n  // Copy all clipboard data to targeting parameters\n  onPasteTargetingParams(event) {\n    this.setState(({ pasteFromClipboard }) => ({\n      pasteFromClipboard: !pasteFromClipboard,\n      newStringTargetingParameters: \"\",\n    }));\n  }\n\n  onNewTargetingParams(event) {\n    this.setState({ newStringTargetingParameters: event.target.value });\n    event.target.classList.remove(\"errorState\");\n    this.refs.targetingParamsEval.innerText = \"\";\n\n    try {\n      const stringTargetingParameters = JSON.parse(event.target.value);\n      this.setState({ stringTargetingParameters });\n    } catch (e) {\n      event.target.classList.add(\"errorState\");\n      this.refs.targetingParamsEval.innerText = e.message;\n    }\n  }\n\n  renderMessageItem(msg) {\n    const isBlocked =\n      this.state.messageBlockList.includes(msg.id) ||\n      this.state.messageBlockList.includes(msg.campaign);\n    const impressions = this.state.messageImpressions[msg.id]\n      ? this.state.messageImpressions[msg.id].length\n      : 0;\n\n    let itemClassName = \"message-item\";\n    if (isBlocked) {\n      itemClassName += \" blocked\";\n    }\n\n    return (\n      <tr className={itemClassName} key={`${msg.id}-${msg.provider}`}>\n        <td className=\"message-id\">\n          <span>\n            {msg.id} <br />\n          </span>\n        </td>\n        <td>\n          <button\n            className={`button ${isBlocked ? \"\" : \" primary\"}`}\n            onClick={\n              isBlocked ? this.handleUnblock(msg) : this.handleBlock(msg)\n            }\n          >\n            {isBlocked ? \"Unblock\" : \"Block\"}\n          </button>\n          {isBlocked ? null : (\n            <button className=\"button\" onClick={this.handleOverride(msg.id)}>\n              Show\n            </button>\n          )}\n          <br />({impressions} impressions)\n        </td>\n        <td className=\"message-summary\">\n          <pre>{JSON.stringify(msg, null, 2)}</pre>\n        </td>\n      </tr>\n    );\n  }\n\n  renderMessages() {\n    if (!this.state.messages) {\n      return null;\n    }\n    const messagesToShow =\n      this.state.messageFilter === \"all\"\n        ? this.state.messages\n        : this.state.messages.filter(\n            message => message.provider === this.state.messageFilter\n          );\n    return (\n      <table>\n        <tbody>{messagesToShow.map(msg => this.renderMessageItem(msg))}</tbody>\n      </table>\n    );\n  }\n\n  renderMessageFilter() {\n    if (!this.state.providers) {\n      return null;\n    }\n    return (\n      <p>\n        {/* eslint-disable-next-line prettier/prettier */}\n      Show messages from{\" \"}\n      {/* eslint-disable-next-line jsx-a11y/no-onchange */}\n        <select\n          value={this.state.messageFilter}\n          onChange={this.onChangeMessageFilter}\n        >\n          <option value=\"all\">all providers</option>\n          {this.state.providers.map(provider => (\n            <option key={provider.id} value={provider.id}>\n              {provider.id}\n            </option>\n          ))}\n        </select>\n      </p>\n    );\n  }\n\n  renderTableHead() {\n    return (\n      <thead>\n        <tr className=\"message-item\">\n          <td className=\"min\" />\n          <td className=\"min\">Provider ID</td>\n          <td>Source</td>\n          <td className=\"min\">Cohort</td>\n          <td className=\"min\">Last Updated</td>\n        </tr>\n      </thead>\n    );\n  }\n\n  renderProviders() {\n    const providersConfig = this.state.providerPrefs;\n    const providerInfo = this.state.providers;\n    const userPrefInfo = this.state.userPrefs;\n\n    return (\n      <table>\n        {this.renderTableHead()}\n        <tbody>\n          {providersConfig.map((provider, i) => {\n            const isTestProvider = provider.id.includes(\"_local_testing\");\n            const info = providerInfo.find(p => p.id === provider.id) || {};\n            const isUserEnabled =\n              provider.id in userPrefInfo ? userPrefInfo[provider.id] : true;\n            const isSystemEnabled = isTestProvider || provider.enabled;\n\n            let label = \"local\";\n            if (provider.type === \"remote\") {\n              label = (\n                <span>\n                  endpoint (\n                  <a\n                    className=\"providerUrl\"\n                    target=\"_blank\"\n                    href={info.url}\n                    rel=\"noopener noreferrer\"\n                  >\n                    {info.url}\n                  </a>\n                  )\n                </span>\n              );\n            } else if (provider.type === \"remote-settings\") {\n              label = `remote settings (${provider.bucket})`;\n            }\n\n            let reasonsDisabled = [];\n            if (!isSystemEnabled) {\n              reasonsDisabled.push(\"system pref\");\n            }\n            if (!isUserEnabled) {\n              reasonsDisabled.push(\"user pref\");\n            }\n            if (reasonsDisabled.length) {\n              label = `disabled via ${reasonsDisabled.join(\", \")}`;\n            }\n\n            return (\n              <tr className=\"message-item\" key={i}>\n                <td>\n                  {isTestProvider ? (\n                    <input\n                      type=\"checkbox\"\n                      disabled={true}\n                      readOnly={true}\n                      checked={true}\n                    />\n                  ) : (\n                    <input\n                      type=\"checkbox\"\n                      data-provider={provider.id}\n                      checked={isUserEnabled && isSystemEnabled}\n                      onChange={this.handleEnabledToggle}\n                    />\n                  )}\n                </td>\n                <td>{provider.id}</td>\n                <td>\n                  <span\n                    className={`sourceLabel${\n                      isUserEnabled && isSystemEnabled ? \"\" : \" isDisabled\"\n                    }`}\n                  >\n                    {label}\n                  </span>\n                </td>\n                <td>{provider.cohort}</td>\n                <td style={{ whiteSpace: \"nowrap\" }}>\n                  {info.lastUpdated\n                    ? new Date(info.lastUpdated).toLocaleString()\n                    : \"\"}\n                </td>\n              </tr>\n            );\n          })}\n        </tbody>\n      </table>\n    );\n  }\n\n  renderPasteModal() {\n    if (!this.state.pasteFromClipboard) {\n      return null;\n    }\n    const errors =\n      this.refs.targetingParamsEval &&\n      this.refs.targetingParamsEval.innerText.length;\n    return (\n      <ModalOverlay\n        innerStyle=\"pasteModal\"\n        title=\"New targeting parameters\"\n        button_label={errors ? \"Cancel\" : \"Done\"}\n        onDismissBundle={this.onPasteTargetingParams}\n      >\n        <div className=\"onboardingMessage\">\n          <p>\n            <textarea\n              onChange={this.onNewTargetingParams}\n              value={this.state.newStringTargetingParameters}\n              rows=\"20\"\n              cols=\"60\"\n            />\n          </p>\n          <p ref=\"targetingParamsEval\" />\n        </div>\n      </ModalOverlay>\n    );\n  }\n\n  renderTargetingParameters() {\n    // There was no error and the result is truthy\n    const success =\n      this.state.evaluationStatus.success &&\n      !!this.state.evaluationStatus.result;\n    const result =\n      JSON.stringify(this.state.evaluationStatus.result, null, 2) ||\n      \"(Empty result)\";\n\n    return (\n      <table>\n        <tbody>\n          <tr>\n            <td>\n              <h2>Evaluate JEXL expression</h2>\n            </td>\n          </tr>\n          <tr>\n            <td>\n              <p>\n                <textarea\n                  ref=\"expressionInput\"\n                  rows=\"10\"\n                  cols=\"60\"\n                  placeholder=\"Evaluate JEXL expressions and mock parameters by changing their values below\"\n                />\n              </p>\n              <p>\n                Status:{\" \"}\n                <span ref=\"evaluationStatus\">\n                  {success ? \"✅\" : \"❌\"}, Result: {result}\n                </span>\n              </p>\n            </td>\n            <td>\n              <button\n                className=\"ASRouterButton secondary\"\n                onClick={this.handleExpressionEval}\n              >\n                Evaluate\n              </button>\n            </td>\n          </tr>\n          <tr>\n            <td>\n              <h2>Modify targeting parameters</h2>\n            </td>\n          </tr>\n          <tr>\n            <td>\n              <button\n                className=\"ASRouterButton secondary\"\n                onClick={this.onCopyTargetingParams}\n                disabled={this.state.copiedToClipboard}\n              >\n                {this.state.copiedToClipboard\n                  ? \"Parameters copied!\"\n                  : \"Copy parameters\"}\n              </button>\n              <button\n                className=\"ASRouterButton secondary\"\n                onClick={this.onPasteTargetingParams}\n                disabled={this.state.pasteFromClipboard}\n              >\n                Paste parameters\n              </button>\n            </td>\n          </tr>\n          {this.state.stringTargetingParameters &&\n            Object.keys(this.state.stringTargetingParameters).map(\n              (param, i) => {\n                const value = this.state.stringTargetingParameters[param];\n                const errorState =\n                  this.state.targetingParametersError &&\n                  this.state.targetingParametersError.id === param;\n                const className = errorState ? \"errorState\" : \"\";\n                const inputComp =\n                  (value && value.length) > 30 ? (\n                    <textarea\n                      name={param}\n                      className={className}\n                      value={value}\n                      rows=\"10\"\n                      cols=\"60\"\n                      onChange={this.onChangeTargetingParameters}\n                    />\n                  ) : (\n                    <input\n                      name={param}\n                      className={className}\n                      value={value}\n                      onChange={this.onChangeTargetingParameters}\n                    />\n                  );\n\n                return (\n                  <tr key={i}>\n                    <td>{param}</td>\n                    <td>{inputComp}</td>\n                  </tr>\n                );\n              }\n            )}\n        </tbody>\n      </table>\n    );\n  }\n\n  onChangeAttributionParameters(event) {\n    const { name, value } = event.target;\n\n    this.setState(({ attributionParameters }) => {\n      const updatedParameters = { ...attributionParameters };\n      updatedParameters[name] = value;\n\n      return { attributionParameters: updatedParameters };\n    });\n  }\n\n  setAttribution(e) {\n    ASRouterUtils.sendMessage({\n      type: \"FORCE_ATTRIBUTION\",\n      data: this.state.attributionParameters,\n    });\n  }\n\n  renderPocketStory(story) {\n    return (\n      <tr className=\"message-item\" key={story.guid}>\n        <td className=\"message-id\">\n          <span>\n            {story.guid} <br />\n          </span>\n        </td>\n        <td className=\"message-summary\">\n          <pre>{JSON.stringify(story, null, 2)}</pre>\n        </td>\n      </tr>\n    );\n  }\n\n  renderPocketStories() {\n    const { rows } =\n      this.props.Sections.find(Section => Section.id === \"topstories\") || {};\n\n    return (\n      <table>\n        <tbody>\n          {rows && rows.map(story => this.renderPocketStory(story))}\n        </tbody>\n      </table>\n    );\n  }\n\n  renderDiscoveryStream() {\n    const { config } = this.props.DiscoveryStream;\n\n    return (\n      <div>\n        <table>\n          <tbody>\n            <tr className=\"message-item\">\n              <td className=\"min\">Enabled</td>\n              <td>{config.enabled ? \"yes\" : \"no\"}</td>\n            </tr>\n            <tr className=\"message-item\">\n              <td className=\"min\">Endpoint</td>\n              <td>{config.endpoint || \"(empty)\"}</td>\n            </tr>\n          </tbody>\n        </table>\n      </div>\n    );\n  }\n\n  renderAttributionParamers() {\n    return (\n      <div>\n        <h2> Attribution Parameters </h2>\n        <p>\n          {\" \"}\n          This forces the browser to set some attribution parameters, useful for\n          testing the Return To AMO feature. Clicking on 'Force Attribution',\n          with the default values in each field, will demo the Return To AMO\n          flow with the addon called 'Iridium for Youtube'. If you wish to try\n          different attribution parameters, enter them in the text boxes. If you\n          wish to try a different addon with the Return To AMO flow, make sure\n          the 'content' text box has the addon GUID, then click 'Force\n          Attribution'.\n        </p>\n        <table>\n          <tr>\n            <td>\n              <b> Source </b>\n            </td>\n            <td>\n              {\" \"}\n              <input\n                type=\"text\"\n                name=\"source\"\n                placeholder=\"addons.mozilla.org\"\n                value={this.state.attributionParameters.source}\n                onChange={this.onChangeAttributionParameters}\n              />{\" \"}\n            </td>\n          </tr>\n          <tr>\n            <td>\n              <b> Campaign </b>\n            </td>\n            <td>\n              {\" \"}\n              <input\n                type=\"text\"\n                name=\"campaign\"\n                placeholder=\"non-fx-button\"\n                value={this.state.attributionParameters.campaign}\n                onChange={this.onChangeAttributionParameters}\n              />{\" \"}\n            </td>\n          </tr>\n          <tr>\n            <td>\n              <b> Content </b>\n            </td>\n            <td>\n              {\" \"}\n              <input\n                type=\"text\"\n                name=\"content\"\n                placeholder=\"iridium@particlecore.github.io\"\n                value={this.state.attributionParameters.content}\n                onChange={this.onChangeAttributionParameters}\n              />{\" \"}\n            </td>\n          </tr>\n          <tr>\n            <td>\n              {\" \"}\n              <button\n                className=\"ASRouterButton primary button\"\n                onClick={this.setAttribution}\n              >\n                {\" \"}\n                Force Attribution{\" \"}\n              </button>{\" \"}\n            </td>\n          </tr>\n        </table>\n      </div>\n    );\n  }\n\n  renderErrorMessage({ id, errors }) {\n    const providerId = <td rowSpan={errors.length}>{id}</td>;\n    // .reverse() so that the last error (most recent) is first\n    return errors\n      .map(({ error, timestamp }, cellKey) => (\n        <tr key={cellKey}>\n          {cellKey === errors.length - 1 ? providerId : null}\n          <td>{error.message}</td>\n          <td>{relativeTime(timestamp)}</td>\n        </tr>\n      ))\n      .reverse();\n  }\n\n  renderErrors() {\n    const providersWithErrors =\n      this.state.providers &&\n      this.state.providers.filter(p => p.errors && p.errors.length);\n\n    if (providersWithErrors && providersWithErrors.length) {\n      return (\n        <table className=\"errorReporting\">\n          <thead>\n            <tr>\n              <th>Provider ID</th>\n              <th>Message</th>\n              <th>Timestamp</th>\n            </tr>\n          </thead>\n          <tbody>{providersWithErrors.map(this.renderErrorMessage)}</tbody>\n        </table>\n      );\n    }\n\n    return <p>No errors</p>;\n  }\n\n  renderTrailheadInfo() {\n    const { trailheadInterrupt, trailheadTriplet } = this.state.trailhead;\n    return (\n      <table className=\"minimal-table\">\n        <tbody>\n          <tr>\n            <td>Interrupt branch</td>\n            <td>{trailheadInterrupt}</td>\n          </tr>\n          <tr>\n            <td>Triplet branch</td>\n            <td>{trailheadTriplet}</td>\n          </tr>\n        </tbody>\n      </table>\n    );\n  }\n\n  getSection() {\n    const [section] = this.props.location.routes;\n    switch (section) {\n      case \"targeting\":\n        return (\n          <React.Fragment>\n            <h2>Targeting Utilities</h2>\n            <button className=\"button\" onClick={this.expireCache}>\n              Expire Cache\n            </button>{\" \"}\n            (This expires the cache in ASR Targeting for bookmarks and top\n            sites)\n            {this.renderTargetingParameters()}\n            {this.renderAttributionParamers()}\n          </React.Fragment>\n        );\n      case \"pocket\":\n        return (\n          <React.Fragment>\n            <h2>Pocket</h2>\n            {this.renderPocketStories()}\n          </React.Fragment>\n        );\n      case \"ds\":\n        return (\n          <React.Fragment>\n            <h2>Discovery Stream</h2>\n            <DiscoveryStreamAdmin\n              state={this.props.DiscoveryStream}\n              otherPrefs={this.props.Prefs.values}\n              dispatch={this.props.dispatch}\n            />\n          </React.Fragment>\n        );\n      case \"errors\":\n        return (\n          <React.Fragment>\n            <h2>ASRouter Errors</h2>\n            {this.renderErrors()}\n          </React.Fragment>\n        );\n      default:\n        return (\n          <React.Fragment>\n            <h2>\n              Message Providers{\" \"}\n              <button\n                title=\"Restore all provider settings that ship with Firefox\"\n                className=\"button\"\n                onClick={this.resetPref}\n              >\n                Restore default prefs\n              </button>\n            </h2>\n            {this.state.providers ? this.renderProviders() : null}\n            <h2>Trailhead</h2>\n            {this.renderTrailheadInfo()}\n            <h2>Messages</h2>\n            {this.renderMessageFilter()}\n            {this.renderMessages()}\n            {this.renderPasteModal()}\n          </React.Fragment>\n        );\n    }\n  }\n\n  render() {\n    return (\n      <div\n        className={`asrouter-admin ${\n          this.props.collapsed ? \"collapsed\" : \"expanded\"\n        }`}\n      >\n        <aside className=\"sidebar\">\n          <ul>\n            <li>\n              <a href=\"#devtools\">General</a>\n            </li>\n            <li>\n              <a href=\"#devtools-targeting\">Targeting</a>\n            </li>\n            <li>\n              <a href=\"#devtools-pocket\">Pocket</a>\n            </li>\n            <li>\n              <a href=\"#devtools-ds\">Discovery Stream</a>\n            </li>\n            <li>\n              <a href=\"#devtools-errors\">Errors</a>\n            </li>\n          </ul>\n        </aside>\n        <main className=\"main-panel\">\n          <h1>AS Router Admin</h1>\n\n          <p className=\"helpLink\">\n            <span className=\"icon icon-small-spacer icon-info\" />{\" \"}\n            <span>\n              Need help using these tools? Check out our{\" \"}\n              <a\n                target=\"blank\"\n                href=\"https://github.com/mozilla/activity-stream/blob/master/content-src/asrouter/docs/debugging-docs.md\"\n              >\n                documentation\n              </a>\n            </span>\n          </p>\n\n          {this.getSection()}\n        </main>\n      </div>\n    );\n  }\n}\n\nexport class CollapseToggle extends React.PureComponent {\n  constructor(props) {\n    super(props);\n    this.onCollapseToggle = this.onCollapseToggle.bind(this);\n    this.state = { collapsed: false };\n  }\n\n  get renderAdmin() {\n    const { props } = this;\n    return (\n      props.location.hash &&\n      (props.location.hash.startsWith(\"#asrouter\") ||\n        props.location.hash.startsWith(\"#devtools\"))\n    );\n  }\n\n  onCollapseToggle(e) {\n    e.preventDefault();\n    this.setState(state => ({ collapsed: !state.collapsed }));\n  }\n\n  setBodyClass() {\n    if (this.renderAdmin && !this.state.collapsed) {\n      global.document.body.classList.add(\"no-scroll\");\n    } else {\n      global.document.body.classList.remove(\"no-scroll\");\n    }\n  }\n\n  componentDidMount() {\n    this.setBodyClass();\n  }\n\n  componentDidUpdate() {\n    this.setBodyClass();\n  }\n\n  componentWillUnmount() {\n    global.document.body.classList.remove(\"no-scroll\");\n  }\n\n  render() {\n    const { props } = this;\n    const { renderAdmin } = this;\n    const isCollapsed = this.state.collapsed || !renderAdmin;\n    const label = `${isCollapsed ? \"Expand\" : \"Collapse\"} devtools`;\n    return (\n      <React.Fragment>\n        <a\n          href=\"#devtools\"\n          title={label}\n          aria-label={label}\n          className={`asrouter-toggle ${\n            isCollapsed ? \"collapsed\" : \"expanded\"\n          }`}\n          onClick={this.renderAdmin ? this.onCollapseToggle : null}\n        >\n          <span className=\"icon icon-devtools\" />\n        </a>\n        {renderAdmin ? (\n          <ASRouterAdminInner {...props} collapsed={this.state.collapsed} />\n        ) : null}\n      </React.Fragment>\n    );\n  }\n}\n\nconst _ASRouterAdmin = props => (\n  <SimpleHashRouter>\n    <CollapseToggle {...props} />\n  </SimpleHashRouter>\n);\n\nexport const ASRouterAdmin = connect(state => ({\n  Sections: state.Sections,\n  DiscoveryStream: state.DiscoveryStream,\n  Prefs: state.Prefs,\n}))(_ASRouterAdmin);\n"
  },
  {
    "path": "content-src/components/ASRouterAdmin/ASRouterAdmin.scss",
    "content": "\n.asrouter-toggle {\n  position: fixed;\n  top: 15px;\n  right: 48px;\n  border: 0;\n  background: none;\n  z-index: 1;\n  border-radius: 2px;\n\n  .icon-devtools {\n    background-image: url('chrome://browser/skin/developer.svg');\n    padding: 15px;\n  }\n\n  &:hover {\n    background: var(--newtab-element-hover-color);\n  }\n\n  &.expanded {\n    background: $black-20;\n  }\n}\n\n.asrouter-admin {\n  position: fixed;\n  top: 0;\n  left: 0;\n  width: 100%;\n  background: var(--newtab-background-color);\n  height: 100%;\n  overflow-y: scroll;\n  $border-color: var(--newtab-border-secondary-color);\n  $monospace: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Mono', 'Droid Sans Mono', 'Source Code Pro', monospace;\n  $sidebar-width: 240px;\n  margin: 0 auto;\n  font-size: 14px;\n  padding-left: $sidebar-width;\n  color: var(--newtab-text-primary-color);\n\n  &.collapsed {\n    display: none;\n  }\n\n  .sidebar {\n    inset-inline-start: 0;\n    position: fixed;\n    width: $sidebar-width;\n    padding: 30px 20px;\n\n    ul {\n      margin: 0;\n      padding: 0;\n      list-style: none;\n    }\n\n    li a {\n      padding: 10px 34px;\n      display: block;\n      color: var(--lwt-sidebar-text-color);\n\n      &:hover {\n        background: var(--newtab-textbox-background-color);\n      }\n    }\n  }\n\n\n  h1 {\n    font-weight: 200;\n    font-size: 32px;\n  }\n\n  h2 .button {\n    font-size: 14px;\n    padding: 6px 12px;\n    margin-inline-start: 5px;\n    margin-bottom: 0;\n  }\n\n  table {\n    border-collapse: collapse;\n    width: 100%;\n\n    &.minimal-table {\n      border-collapse: collapse;\n\n      td {\n        padding: 8px;\n        border: 1px solid $border-color;\n      }\n\n      td:first-child {\n        width: 1%;\n        white-space: nowrap;\n      }\n\n      td:not(:first-child) {\n        font-family: $monospace;\n      }\n    }\n\n    &.errorReporting {\n      tr {\n        border: 1px solid var(--newtab-textbox-background-color);\n      }\n\n      td {\n        padding: 4px;\n\n        &[rowspan] {\n          border: 1px solid var(--newtab-textbox-background-color);\n        }\n      }\n    }\n  }\n\n  .sourceLabel {\n    background: var(--newtab-textbox-background-color);\n    padding: 2px 5px;\n    border-radius: 3px;\n\n    &.isDisabled {\n      background: $email-input-invalid;\n      color: $red-60;\n    }\n  }\n\n  .message-item {\n    &:first-child td {\n      border-top: 1px solid $border-color;\n    }\n\n    td {\n      vertical-align: top;\n      border-bottom: 1px solid $border-color;\n      padding: 8px;\n\n\n\n      &.min {\n        width: 1%;\n        white-space: nowrap;\n      }\n\n      &:first-child {\n        border-left: 1px solid $border-color;\n      }\n\n      &:last-child {\n        border-right: 1px solid $border-color;\n      }\n    }\n\n    &.blocked {\n      .message-id,\n      .message-summary {\n        opacity: 0.5;\n      }\n\n      .message-id {\n        opacity: 0.5;\n      }\n    }\n\n    .message-id {\n      font-family: $monospace;\n      font-size: 12px;\n    }\n  }\n\n  .providerUrl {\n    font-size: 12px;\n  }\n\n  pre {\n    background: var(--newtab-textbox-background-color);\n    margin: 0;\n    padding: 8px;\n    font-size: 12px;\n    max-width: 750px;\n    overflow: auto;\n    font-family: $monospace;\n  }\n\n  .errorState {\n    border: 1px solid $red-60;\n  }\n\n  .helpLink {\n    padding: 10px;\n    display: flex;\n    background: $black-10;\n    border-radius: 3px;\n\n    a {\n      text-decoration: underline;\n    }\n  }\n\n  .ds-component {\n    margin-bottom: 20px;\n  }\n\n  .modalOverlayInner {\n    height: 80%;\n  }\n}\n"
  },
  {
    "path": "content-src/components/ASRouterAdmin/SimpleHashRouter.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport React from \"react\";\n\nexport class SimpleHashRouter extends React.PureComponent {\n  constructor(props) {\n    super(props);\n    this.onHashChange = this.onHashChange.bind(this);\n    this.state = { hash: global.location.hash };\n  }\n\n  onHashChange() {\n    this.setState({ hash: global.location.hash });\n  }\n\n  componentWillMount() {\n    global.addEventListener(\"hashchange\", this.onHashChange);\n  }\n\n  componentWillUnmount() {\n    global.removeEventListener(\"hashchange\", this.onHashChange);\n  }\n\n  render() {\n    const [, ...routes] = this.state.hash.split(\"-\");\n    return React.cloneElement(this.props.children, {\n      location: {\n        hash: this.state.hash,\n        routes,\n      },\n    });\n  }\n}\n"
  },
  {
    "path": "content-src/components/Base/Base.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport { actionCreators as ac, actionTypes as at } from \"common/Actions.jsm\";\nimport { ASRouterAdmin } from \"content-src/components/ASRouterAdmin/ASRouterAdmin\";\nimport { ASRouterUISurface } from \"../../asrouter/asrouter-content\";\nimport { ConfirmDialog } from \"content-src/components/ConfirmDialog/ConfirmDialog\";\nimport { connect } from \"react-redux\";\nimport { DiscoveryStreamBase } from \"content-src/components/DiscoveryStreamBase/DiscoveryStreamBase\";\nimport { ErrorBoundary } from \"content-src/components/ErrorBoundary/ErrorBoundary\";\nimport React from \"react\";\nimport { Search } from \"content-src/components/Search/Search\";\nimport { Sections } from \"content-src/components/Sections/Sections\";\n\nconst PrefsButton = props => (\n  <div className=\"prefs-button\">\n    <button\n      className=\"icon icon-settings\"\n      onClick={props.onClick}\n      data-l10n-id=\"newtab-settings-button\"\n    />\n  </div>\n);\n\n// Returns a function will not be continuously triggered when called. The\n// function will be triggered if called again after `wait` milliseconds.\nfunction debounce(func, wait) {\n  let timer;\n  return (...args) => {\n    if (timer) {\n      return;\n    }\n\n    let wakeUp = () => {\n      timer = null;\n    };\n\n    timer = setTimeout(wakeUp, wait);\n    func.apply(this, args);\n  };\n}\n\nexport class _Base extends React.PureComponent {\n  componentWillMount() {\n    if (this.props.isFirstrun) {\n      global.document.body.classList.add(\"welcome\", \"hide-main\");\n    }\n  }\n\n  componentWillUnmount() {\n    this.updateTheme();\n  }\n\n  componentWillUpdate() {\n    this.updateTheme();\n  }\n\n  updateTheme() {\n    const bodyClassName = [\n      \"activity-stream\",\n      // If we skipped the about:welcome overlay and removed the CSS classes\n      // we don't want to add them back to the Activity Stream view\n      document.body.classList.contains(\"welcome\") ? \"welcome\" : \"\",\n      document.body.classList.contains(\"hide-main\") ? \"hide-main\" : \"\",\n      document.body.classList.contains(\"inline-onboarding\")\n        ? \"inline-onboarding\"\n        : \"\",\n    ]\n      .filter(v => v)\n      .join(\" \");\n    global.document.body.className = bodyClassName;\n  }\n\n  render() {\n    const { props } = this;\n    const { App } = props;\n    const isDevtoolsEnabled = props.Prefs.values[\"asrouter.devtoolsEnabled\"];\n\n    if (!App.initialized) {\n      return null;\n    }\n\n    return (\n      <ErrorBoundary className=\"base-content-fallback\">\n        <React.Fragment>\n          <BaseContent {...this.props} />\n          {isDevtoolsEnabled ? <ASRouterAdmin /> : null}\n        </React.Fragment>\n      </ErrorBoundary>\n    );\n  }\n}\n\nexport class BaseContent extends React.PureComponent {\n  constructor(props) {\n    super(props);\n    this.openPreferences = this.openPreferences.bind(this);\n    this.onWindowScroll = debounce(this.onWindowScroll.bind(this), 5);\n    this.state = { fixedSearch: false };\n  }\n\n  componentDidMount() {\n    global.addEventListener(\"scroll\", this.onWindowScroll);\n  }\n\n  componentWillUnmount() {\n    global.removeEventListener(\"scroll\", this.onWindowScroll);\n  }\n\n  onWindowScroll() {\n    const SCROLL_THRESHOLD = 34;\n    if (global.scrollY > SCROLL_THRESHOLD && !this.state.fixedSearch) {\n      this.setState({ fixedSearch: true });\n    } else if (global.scrollY <= SCROLL_THRESHOLD && this.state.fixedSearch) {\n      this.setState({ fixedSearch: false });\n    }\n  }\n\n  openPreferences() {\n    this.props.dispatch(ac.OnlyToMain({ type: at.SETTINGS_OPEN }));\n    this.props.dispatch(ac.UserEvent({ event: \"OPEN_NEWTAB_PREFS\" }));\n  }\n\n  render() {\n    const { props } = this;\n    const { App } = props;\n    const { initialized } = App;\n    const prefs = props.Prefs.values;\n\n    const isDiscoveryStream =\n      props.DiscoveryStream.config && props.DiscoveryStream.config.enabled;\n    let filteredSections = props.Sections;\n\n    // Filter out highlights for DS\n    if (isDiscoveryStream) {\n      filteredSections = filteredSections.filter(\n        section => section.id !== \"highlights\"\n      );\n    }\n    const noSectionsEnabled =\n      !prefs[\"feeds.topsites\"] &&\n      filteredSections.filter(section => section.enabled).length === 0;\n    const searchHandoffEnabled = prefs[\"improvesearch.handoffToAwesomebar\"];\n\n    const outerClassName = [\n      \"outer-wrapper\",\n      isDiscoveryStream && \"ds-outer-wrapper-search-alignment\",\n      isDiscoveryStream && \"ds-outer-wrapper-breakpoint-override\",\n      prefs.showSearch &&\n        this.state.fixedSearch &&\n        !noSectionsEnabled &&\n        \"fixed-search\",\n      prefs.showSearch && noSectionsEnabled && \"only-search\",\n    ]\n      .filter(v => v)\n      .join(\" \");\n\n    return (\n      <div>\n        <div className={outerClassName}>\n          <main>\n            {prefs.showSearch && (\n              <div className=\"non-collapsible-section\">\n                <ErrorBoundary>\n                  <Search\n                    showLogo={noSectionsEnabled}\n                    handoffEnabled={searchHandoffEnabled}\n                    {...props.Search}\n                  />\n                </ErrorBoundary>\n              </div>\n            )}\n            <ASRouterUISurface\n              appUpdateChannel={this.props.Prefs.values.appUpdateChannel}\n              fxaEndpoint={this.props.Prefs.values.fxa_endpoint}\n              dispatch={this.props.dispatch}\n            />\n            <div className={`body-wrapper${initialized ? \" on\" : \"\"}`}>\n              {isDiscoveryStream ? (\n                <ErrorBoundary className=\"borderless-error\">\n                  <DiscoveryStreamBase />\n                </ErrorBoundary>\n              ) : (\n                <Sections />\n              )}\n              <PrefsButton onClick={this.openPreferences} />\n            </div>\n            <ConfirmDialog />\n          </main>\n        </div>\n      </div>\n    );\n  }\n}\n\nexport const Base = connect(state => ({\n  App: state.App,\n  Prefs: state.Prefs,\n  Sections: state.Sections,\n  DiscoveryStream: state.DiscoveryStream,\n  Search: state.Search,\n}))(_Base);\n"
  },
  {
    "path": "content-src/components/Base/_Base.scss",
    "content": ".outer-wrapper {\n  color: var(--newtab-text-primary-color);\n  display: flex;\n  flex-grow: 1;\n  min-height: 100vh;\n  padding: ($section-spacing + $section-vertical-padding) $base-gutter $base-gutter;\n\n  &.only-search {\n    display: block;\n    padding-top: 134px;\n  }\n\n  a {\n    color: var(--newtab-link-primary-color);\n  }\n}\n\nmain {\n  margin: auto;\n  width: $wrapper-default-width;\n  // Offset the snippets container so things at the bottom of the page are still\n  // visible when snippets are visible. Adjust for other spacing.\n  padding-bottom: $snippets-container-height - $section-spacing - $base-gutter;\n\n  section {\n    margin-bottom: $section-spacing;\n    position: relative;\n  }\n\n  .hide-main & {\n    visibility: hidden;\n  }\n\n  @media (min-width: $break-point-medium) {\n    width: $wrapper-max-width-medium;\n  }\n\n  @media (min-width: $break-point-large) {\n    width: $wrapper-max-width-large;\n  }\n\n  @media (min-width: $break-point-widest) {\n    width: $wrapper-max-width-widest;\n  }\n\n}\n\n.below-search-snippet.withButton {\n  margin: auto;\n  width: 100%;\n}\n\n.ds-outer-wrapper-search-alignment {\n  main {\n    // This override is to ensure while Discovery Stream loads,\n    // the search bar does not jump around. (it sticks to the top)\n    margin: 0 auto;\n  }\n}\n\n.ds-outer-wrapper-breakpoint-override {\n  main {\n    // Override Activity Stream breakpoints for Discovery Stream.\n    // Right now Discovery Stream doesn't have any breakpoints,\n    // and Activity Stream breakpoints do some wonky things.\n    width: 1042px;\n  }\n\n  &:not(.fixed-search) {\n    .search-wrapper .search-inner-wrapper {\n      width: $searchbar-width-large;\n    }\n  }\n}\n\n.base-content-fallback {\n  // Make the error message be centered against the viewport\n  height: 100vh;\n}\n\n.body-wrapper {\n  // Hide certain elements so the page structure is fixed, e.g., placeholders,\n  // while avoiding flashes of changing content, e.g., icons and text\n  $selectors-to-hide: '\n    .section-title,\n    .sections-list .section:last-of-type,\n    .topics\n  ';\n\n  #{$selectors-to-hide} {\n    opacity: 0;\n  }\n\n  &.on {\n    #{$selectors-to-hide} {\n      opacity: 1;\n    }\n  }\n}\n\n.non-collapsible-section {\n  padding: 0 $section-horizontal-padding;\n}\n\n.prefs-button {\n  button {\n    background-color: transparent;\n    border: 0;\n    cursor: pointer;\n    fill: var(--newtab-icon-primary-color);\n    inset-inline-end: 15px;\n    padding: 15px;\n    position: fixed;\n    top: 15px;\n    z-index: 1000;\n\n    &:hover,\n    &:focus {\n      background-color: var(--newtab-element-hover-color);\n    }\n\n    &:active {\n      background-color: var(--newtab-element-active-color);\n    }\n  }\n}\n"
  },
  {
    "path": "content-src/components/Card/Card.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport { actionCreators as ac, actionTypes as at } from \"common/Actions.jsm\";\nimport { cardContextTypes } from \"./types\";\nimport { connect } from \"react-redux\";\nimport { ContextMenuButton } from \"content-src/components/ContextMenu/ContextMenuButton\";\nimport { LinkMenu } from \"content-src/components/LinkMenu/LinkMenu\";\nimport React from \"react\";\nimport { ScreenshotUtils } from \"content-src/lib/screenshot-utils\";\n\n// Keep track of pending image loads to only request once\nconst gImageLoading = new Map();\n\n/**\n * Card component.\n * Cards are found within a Section component and contain information about a link such\n * as preview image, page title, page description, and some context about if the page\n * was visited, bookmarked, trending etc...\n * Each Section can make an unordered list of Cards which will create one instane of\n * this class. Each card will then get a context menu which reflects the actions that\n * can be done on this Card.\n */\nexport class _Card extends React.PureComponent {\n  constructor(props) {\n    super(props);\n    this.state = {\n      activeCard: null,\n      imageLoaded: false,\n      cardImage: null,\n    };\n    this.onMenuButtonUpdate = this.onMenuButtonUpdate.bind(this);\n    this.onLinkClick = this.onLinkClick.bind(this);\n  }\n\n  /**\n   * Helper to conditionally load an image and update state when it loads.\n   */\n  async maybeLoadImage() {\n    // No need to load if it's already loaded or no image\n    const { cardImage } = this.state;\n    if (!cardImage) {\n      return;\n    }\n\n    const imageUrl = cardImage.url;\n    if (!this.state.imageLoaded) {\n      // Initialize a promise to share a load across multiple card updates\n      if (!gImageLoading.has(imageUrl)) {\n        const loaderPromise = new Promise((resolve, reject) => {\n          const loader = new Image();\n          loader.addEventListener(\"load\", resolve);\n          loader.addEventListener(\"error\", reject);\n          loader.src = imageUrl;\n        });\n\n        // Save and remove the promise only while it's pending\n        gImageLoading.set(imageUrl, loaderPromise);\n        loaderPromise\n          .catch(ex => ex)\n          .then(() => gImageLoading.delete(imageUrl))\n          .catch();\n      }\n\n      // Wait for the image whether just started loading or reused promise\n      await gImageLoading.get(imageUrl);\n\n      // Only update state if we're still waiting to load the original image\n      if (\n        ScreenshotUtils.isRemoteImageLocal(\n          this.state.cardImage,\n          this.props.link.image\n        ) &&\n        !this.state.imageLoaded\n      ) {\n        this.setState({ imageLoaded: true });\n      }\n    }\n  }\n\n  /**\n   * Helper to obtain the next state based on nextProps and prevState.\n   *\n   * NOTE: Rename this method to getDerivedStateFromProps when we update React\n   *       to >= 16.3. We will need to update tests as well. We cannot rename this\n   *       method to getDerivedStateFromProps now because there is a mismatch in\n   *       the React version that we are using for both testing and production.\n   *       (i.e. react-test-render => \"16.3.2\", react => \"16.2.0\").\n   *\n   * See https://github.com/airbnb/enzyme/blob/master/packages/enzyme-adapter-react-16/package.json#L43.\n   */\n  static getNextStateFromProps(nextProps, prevState) {\n    const { image } = nextProps.link;\n    const imageInState = ScreenshotUtils.isRemoteImageLocal(\n      prevState.cardImage,\n      image\n    );\n    let nextState = null;\n\n    // Image is updating.\n    if (!imageInState && nextProps.link) {\n      nextState = { imageLoaded: false };\n    }\n\n    if (imageInState) {\n      return nextState;\n    }\n\n    // Since image was updated, attempt to revoke old image blob URL, if it exists.\n    ScreenshotUtils.maybeRevokeBlobObjectURL(prevState.cardImage);\n\n    nextState = nextState || {};\n    nextState.cardImage = ScreenshotUtils.createLocalImageObject(image);\n\n    return nextState;\n  }\n\n  onMenuButtonUpdate(isOpen) {\n    if (isOpen) {\n      this.setState({ activeCard: this.props.index });\n    } else {\n      this.setState({ activeCard: null });\n    }\n  }\n\n  /**\n   * Report to telemetry additional information about the item.\n   */\n  _getTelemetryInfo() {\n    // Filter out \"history\" type for being the default\n    if (this.props.link.type !== \"history\") {\n      return { value: { card_type: this.props.link.type } };\n    }\n\n    return null;\n  }\n\n  onLinkClick(event) {\n    event.preventDefault();\n    if (this.props.link.type === \"download\") {\n      this.props.dispatch(\n        ac.OnlyToMain({\n          type: at.SHOW_DOWNLOAD_FILE,\n          data: this.props.link,\n        })\n      );\n    } else {\n      const { altKey, button, ctrlKey, metaKey, shiftKey } = event;\n      this.props.dispatch(\n        ac.OnlyToMain({\n          type: at.OPEN_LINK,\n          data: Object.assign(this.props.link, {\n            event: { altKey, button, ctrlKey, metaKey, shiftKey },\n          }),\n        })\n      );\n    }\n    if (this.props.isWebExtension) {\n      this.props.dispatch(\n        ac.WebExtEvent(at.WEBEXT_CLICK, {\n          source: this.props.eventSource,\n          url: this.props.link.url,\n          action_position: this.props.index,\n        })\n      );\n    } else {\n      this.props.dispatch(\n        ac.UserEvent(\n          Object.assign(\n            {\n              event: \"CLICK\",\n              source: this.props.eventSource,\n              action_position: this.props.index,\n            },\n            this._getTelemetryInfo()\n          )\n        )\n      );\n\n      if (this.props.shouldSendImpressionStats) {\n        this.props.dispatch(\n          ac.ImpressionStats({\n            source: this.props.eventSource,\n            click: 0,\n            tiles: [{ id: this.props.link.guid, pos: this.props.index }],\n          })\n        );\n      }\n    }\n  }\n\n  componentDidMount() {\n    this.maybeLoadImage();\n  }\n\n  componentDidUpdate() {\n    this.maybeLoadImage();\n  }\n\n  // NOTE: Remove this function when we update React to >= 16.3 since React will\n  //       call getDerivedStateFromProps automatically. We will also need to\n  //       rename getNextStateFromProps to getDerivedStateFromProps.\n  componentWillMount() {\n    const nextState = _Card.getNextStateFromProps(this.props, this.state);\n    if (nextState) {\n      this.setState(nextState);\n    }\n  }\n\n  // NOTE: Remove this function when we update React to >= 16.3 since React will\n  //       call getDerivedStateFromProps automatically. We will also need to\n  //       rename getNextStateFromProps to getDerivedStateFromProps.\n  componentWillReceiveProps(nextProps) {\n    const nextState = _Card.getNextStateFromProps(nextProps, this.state);\n    if (nextState) {\n      this.setState(nextState);\n    }\n  }\n\n  componentWillUnmount() {\n    ScreenshotUtils.maybeRevokeBlobObjectURL(this.state.cardImage);\n  }\n\n  render() {\n    const {\n      index,\n      className,\n      link,\n      dispatch,\n      contextMenuOptions,\n      eventSource,\n      shouldSendImpressionStats,\n    } = this.props;\n    const { props } = this;\n    const title = link.title || link.hostname;\n    const isContextMenuOpen = this.state.activeCard === index;\n    // Display \"now\" as \"trending\" until we have new strings #3402\n    const { icon, fluentID } =\n      cardContextTypes[link.type === \"now\" ? \"trending\" : link.type] || {};\n    const hasImage = this.state.cardImage || link.hasImage;\n    const imageStyle = {\n      backgroundImage: this.state.cardImage\n        ? `url(${this.state.cardImage.url})`\n        : \"none\",\n    };\n    const outerClassName = [\n      \"card-outer\",\n      className,\n      isContextMenuOpen && \"active\",\n      props.placeholder && \"placeholder\",\n    ]\n      .filter(v => v)\n      .join(\" \");\n\n    return (\n      <li className={outerClassName}>\n        <a\n          href={link.type === \"pocket\" ? link.open_url : link.url}\n          onClick={!props.placeholder ? this.onLinkClick : undefined}\n        >\n          <div className=\"card\">\n            <div className=\"card-preview-image-outer\">\n              {hasImage && (\n                <div\n                  className={`card-preview-image${\n                    this.state.imageLoaded ? \" loaded\" : \"\"\n                  }`}\n                  style={imageStyle}\n                />\n              )}\n            </div>\n            <div className=\"card-details\">\n              {link.type === \"download\" && (\n                <div\n                  className=\"card-host-name alternate\"\n                  data-l10n-id=\"newtab-menu-show-file\"\n                />\n              )}\n              {link.hostname && (\n                <div className=\"card-host-name\">\n                  {link.hostname.slice(0, 100)}\n                  {link.type === \"download\" && `  \\u2014 ${link.description}`}\n                </div>\n              )}\n              <div\n                className={[\n                  \"card-text\",\n                  icon ? \"\" : \"no-context\",\n                  link.description ? \"\" : \"no-description\",\n                  link.hostname ? \"\" : \"no-host-name\",\n                ].join(\" \")}\n              >\n                <h4 className=\"card-title\" dir=\"auto\">\n                  {link.title}\n                </h4>\n                <p className=\"card-description\" dir=\"auto\">\n                  {link.description}\n                </p>\n              </div>\n              <div className=\"card-context\">\n                {icon && !link.context && (\n                  <span\n                    aria-haspopup=\"true\"\n                    className={`card-context-icon icon icon-${icon}`}\n                  />\n                )}\n                {link.icon && link.context && (\n                  <span\n                    aria-haspopup=\"true\"\n                    className=\"card-context-icon icon\"\n                    style={{ backgroundImage: `url('${link.icon}')` }}\n                  />\n                )}\n                {fluentID && !link.context && (\n                  <div className=\"card-context-label\" data-l10n-id={fluentID} />\n                )}\n                {link.context && (\n                  <div className=\"card-context-label\">{link.context}</div>\n                )}\n              </div>\n            </div>\n          </div>\n        </a>\n        {!props.placeholder && (\n          <ContextMenuButton\n            tooltip=\"newtab-menu-content-tooltip\"\n            tooltipArgs={{ title }}\n            onUpdate={this.onMenuButtonUpdate}\n          >\n            <LinkMenu\n              dispatch={dispatch}\n              index={index}\n              source={eventSource}\n              options={link.contextMenuOptions || contextMenuOptions}\n              site={link}\n              siteInfo={this._getTelemetryInfo()}\n              shouldSendImpressionStats={shouldSendImpressionStats}\n            />\n          </ContextMenuButton>\n        )}\n      </li>\n    );\n  }\n}\n_Card.defaultProps = { link: {} };\nexport const Card = connect(state => ({\n  platform: state.Prefs.values.platform,\n}))(_Card);\nexport const PlaceholderCard = props => (\n  <Card placeholder={true} className={props.className} />\n);\n"
  },
  {
    "path": "content-src/components/Card/_Card.scss",
    "content": ".card-outer {\n  @include context-menu-button;\n  background: var(--newtab-card-background-color);\n  border-radius: $border-radius;\n  display: inline-block;\n  height: $card-height;\n  margin-inline-end: $base-gutter;\n  position: relative;\n  width: 100%;\n\n  &.placeholder {\n    background: transparent;\n\n    .card {\n      box-shadow: inset $inner-box-shadow;\n    }\n\n    .card-preview-image-outer,\n    .card-context {\n      display: none;\n    }\n  }\n\n  .card {\n    border-radius: $border-radius;\n    box-shadow: var(--newtab-card-shadow);\n    height: 100%;\n  }\n\n  > a {\n    color: inherit;\n    display: block;\n    height: 100%;\n    outline: none;\n    position: absolute;\n    width: 100%;\n\n    &:-moz-any(.active, :focus) {\n      .card {\n        @include fade-in-card;\n      }\n\n      .card-title {\n        color: var(--newtab-link-primary-color);\n      }\n    }\n  }\n\n  &:-moz-any(:hover, :focus, .active):not(.placeholder) {\n    @include fade-in-card;\n    @include context-menu-button-hover;\n    outline: none;\n\n    .card-title {\n      color: var(--newtab-link-primary-color);\n    }\n\n    .alternate ~ .card-host-name {\n      display: none;\n    }\n\n    .card-host-name.alternate {\n      display: block;\n    }\n  }\n\n  .card-preview-image-outer {\n    background-color: $grey-30;\n    border-radius: $border-radius $border-radius 0 0;\n    height: $card-preview-image-height;\n    overflow: hidden;\n    position: relative;\n\n    [lwt-newtab-brighttext] & {\n      background-color: $grey-60;\n    }\n\n    &::after {\n      border-bottom: 1px solid var(--newtab-card-hairline-color);\n      bottom: 0;\n      content: '';\n      position: absolute;\n      width: 100%;\n    }\n\n    .card-preview-image {\n      background-position: center;\n      background-repeat: no-repeat;\n      background-size: cover;\n      height: 100%;\n      opacity: 0;\n      transition: opacity 1s $photon-easing;\n      width: 100%;\n\n      &.loaded {\n        opacity: 1;\n      }\n    }\n  }\n\n  .card-details {\n    padding: 15px 16px 12px;\n  }\n\n  .card-text {\n    max-height: 4 * $card-text-line-height + $card-title-margin;\n    overflow: hidden;\n\n    &.no-host-name,\n    &.no-context {\n      max-height: 5 * $card-text-line-height + $card-title-margin;\n    }\n\n    &.no-host-name.no-context {\n      max-height: 6 * $card-text-line-height + $card-title-margin;\n    }\n\n    &:not(.no-description) .card-title {\n      max-height: 3 * $card-text-line-height;\n      overflow: hidden;\n    }\n  }\n\n  .card-host-name {\n    color: var(--newtab-text-secondary-color);\n    font-size: 10px;\n    overflow: hidden;\n    padding-bottom: 4px;\n    text-overflow: ellipsis;\n    text-transform: uppercase; // sass-lint:disable-line no-disallowed-properties\n    white-space: nowrap;\n  }\n\n  .card-host-name.alternate { display: none; }\n\n  .card-title {\n    font-size: 14px;\n    font-weight: 600;\n    line-height: $card-text-line-height;\n    margin: 0 0 $card-title-margin;\n    word-wrap: break-word;\n  }\n\n  .card-description {\n    font-size: 12px;\n    line-height: $card-text-line-height;\n    margin: 0;\n    overflow: hidden;\n    word-wrap: break-word;\n  }\n\n  .card-context {\n    bottom: 0;\n    color: var(--newtab-text-secondary-color);\n    display: flex;\n    font-size: 11px;\n    inset-inline-start: 0;\n    padding: 9px 16px 9px 14px;\n    position: absolute;\n  }\n\n  .card-context-icon {\n    fill: var(--newtab-text-secondary-color);\n    height: 22px;\n    margin-inline-end: 6px;\n  }\n\n  .card-context-label {\n    flex-grow: 1;\n    line-height: 22px;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n  }\n}\n\n.normal-cards {\n  .card-outer {\n    // Wide layout styles\n    @media (min-width: $break-point-widest) {\n      $line-height: 23px;\n      height: $card-height-large;\n\n      .card-preview-image-outer {\n        height: $card-preview-image-height-large;\n      }\n\n      .card-details {\n        padding: 13px 16px 12px;\n      }\n\n      .card-text {\n        max-height: 6 * $line-height + $card-title-margin;\n      }\n\n      .card-host-name {\n        font-size: 12px;\n        padding-bottom: 5px;\n      }\n\n      .card-title {\n        font-size: 17px;\n        line-height: $line-height;\n        margin-bottom: 0;\n      }\n\n      .card-text:not(.no-description) {\n        .card-title {\n          max-height: 3 * $line-height;\n        }\n      }\n\n      .card-description {\n        font-size: 15px;\n        line-height: $line-height;\n      }\n\n      .card-context {\n        bottom: 4px;\n        font-size: 14px;\n      }\n    }\n  }\n}\n\n.compact-cards {\n  $card-detail-vertical-spacing: 12px;\n  $card-title-font-size: 12px;\n\n  .card-outer {\n    height: $card-height-compact;\n\n    .card-preview-image-outer {\n      height: $card-preview-image-height-compact;\n    }\n\n    .card-details {\n      padding: $card-detail-vertical-spacing 16px;\n    }\n\n    .card-host-name {\n      line-height: 10px;\n    }\n\n    .card-text {\n      .card-title,\n      &:not(.no-description) .card-title {\n        font-size: $card-title-font-size;\n        line-height: $card-title-font-size + 1;\n        max-height: $card-title-font-size + 5;\n        overflow: hidden;\n        padding: 0 0 4px;\n        text-overflow: ellipsis;\n        white-space: nowrap;\n      }\n    }\n\n    .card-description {\n      display: none;\n    }\n\n    .card-context {\n      $icon-size: 16px;\n      $container-size: 32px;\n      background-color: var(--newtab-card-background-color);\n      border-radius: $container-size / 2;\n      clip-path: inset(-1px -1px $container-size - ($card-height-compact - $card-preview-image-height-compact - 2 * $card-detail-vertical-spacing));\n      height: $container-size;\n      width: $container-size;\n      padding: ($container-size - $icon-size) / 2;\n      top: $card-preview-image-height-compact - $icon-size;\n      inset-inline-end: 12px;\n      inset-inline-start: auto;\n\n      &::after {\n        border: 1px solid var(--newtab-card-hairline-color);\n        border-bottom: 0;\n        border-radius: ($container-size / 2) + 1 ($container-size / 2) + 1 0 0;\n        content: '';\n        position: absolute;\n        height: ($container-size + 2) / 2;\n        width: $container-size + 2;\n        top: -1px;\n        left: -1px;\n      }\n\n      .card-context-icon {\n        margin-inline-end: 0;\n        height: $icon-size;\n        width: $icon-size;\n\n        &.icon-bookmark-added {\n          fill: $bookmark-icon-fill;\n        }\n\n        &.icon-download {\n          fill: $download-icon-fill;\n        }\n\n        &.icon-pocket {\n          fill: $pocket-icon-fill;\n        }\n      }\n\n      .card-context-label {\n        display: none;\n      }\n    }\n  }\n\n  @media not all and (min-width: $break-point-widest) {\n    .hide-for-narrow {\n      display: none;\n    }\n  }\n}\n"
  },
  {
    "path": "content-src/components/Card/types.js",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nexport const cardContextTypes = {\n  history: {\n    fluentID: \"newtab-label-visited\",\n    icon: \"history-item\",\n  },\n  removedBookmark: {\n    fluentID: \"newtab-label-removed-bookmark\",\n    icon: \"bookmark-removed\",\n  },\n  bookmark: {\n    fluentID: \"newtab-label-bookmarked\",\n    icon: \"bookmark-added\",\n  },\n  trending: {\n    fluentID: \"newtab-label-recommended\",\n    icon: \"trending\",\n  },\n  pocket: {\n    fluentID: \"newtab-label-saved\",\n    icon: \"pocket\",\n  },\n  download: {\n    fluentID: \"newtab-label-download\",\n    icon: \"download\",\n  },\n};\n"
  },
  {
    "path": "content-src/components/CollapsibleSection/CollapsibleSection.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport { actionCreators as ac } from \"common/Actions.jsm\";\nimport { ErrorBoundary } from \"content-src/components/ErrorBoundary/ErrorBoundary\";\nimport { FluentOrText } from \"content-src/components/FluentOrText/FluentOrText\";\nimport React from \"react\";\nimport { SectionMenu } from \"content-src/components/SectionMenu/SectionMenu\";\nimport { SectionMenuOptions } from \"content-src/lib/section-menu-options\";\nimport { ContextMenuButton } from \"content-src/components/ContextMenu/ContextMenuButton\";\n\nconst VISIBLE = \"visible\";\nconst VISIBILITY_CHANGE_EVENT = \"visibilitychange\";\n\nexport class CollapsibleSection extends React.PureComponent {\n  constructor(props) {\n    super(props);\n    this.onBodyMount = this.onBodyMount.bind(this);\n    this.onHeaderClick = this.onHeaderClick.bind(this);\n    this.onKeyPress = this.onKeyPress.bind(this);\n    this.onTransitionEnd = this.onTransitionEnd.bind(this);\n    this.enableOrDisableAnimation = this.enableOrDisableAnimation.bind(this);\n    this.onMenuButtonMouseEnter = this.onMenuButtonMouseEnter.bind(this);\n    this.onMenuButtonMouseLeave = this.onMenuButtonMouseLeave.bind(this);\n    this.onMenuUpdate = this.onMenuUpdate.bind(this);\n    this.state = {\n      enableAnimation: true,\n      isAnimating: false,\n      menuButtonHover: false,\n      showContextMenu: false,\n    };\n    this.setContextMenuButtonRef = this.setContextMenuButtonRef.bind(this);\n  }\n\n  componentWillMount() {\n    this.props.document.addEventListener(\n      VISIBILITY_CHANGE_EVENT,\n      this.enableOrDisableAnimation\n    );\n  }\n\n  componentWillUpdate(nextProps) {\n    // Check if we're about to go from expanded to collapsed\n    if (!this.props.collapsed && nextProps.collapsed) {\n      // This next line forces a layout flush of the section body, which has a\n      // max-height style set, so that the upcoming collapse animation can\n      // animate from that height to the collapsed height. Without this, the\n      // update is coalesced and there's no animation from no-max-height to 0.\n      this.sectionBody.scrollHeight; // eslint-disable-line no-unused-expressions\n    }\n  }\n\n  setContextMenuButtonRef(element) {\n    this.contextMenuButtonRef = element;\n  }\n\n  componentDidMount() {\n    this.contextMenuButtonRef.addEventListener(\n      \"mouseenter\",\n      this.onMenuButtonMouseEnter\n    );\n    this.contextMenuButtonRef.addEventListener(\n      \"mouseleave\",\n      this.onMenuButtonMouseLeave\n    );\n  }\n\n  componentWillUnmount() {\n    this.props.document.removeEventListener(\n      VISIBILITY_CHANGE_EVENT,\n      this.enableOrDisableAnimation\n    );\n    this.contextMenuButtonRef.removeEventListener(\n      \"mouseenter\",\n      this.onMenuButtonMouseEnter\n    );\n    this.contextMenuButtonRef.removeEventListener(\n      \"mouseleave\",\n      this.onMenuButtonMouseLeave\n    );\n  }\n\n  enableOrDisableAnimation() {\n    // Only animate the collapse/expand for visible tabs.\n    const visible = this.props.document.visibilityState === VISIBLE;\n    if (this.state.enableAnimation !== visible) {\n      this.setState({ enableAnimation: visible });\n    }\n  }\n\n  onBodyMount(node) {\n    this.sectionBody = node;\n  }\n\n  onHeaderClick() {\n    // If this.sectionBody is unset, it means that we're in some sort of error\n    // state, probably displaying the error fallback, so we won't be able to\n    // compute the height, and we don't want to persist the preference.\n    // If props.collapsed is undefined handler shouldn't do anything.\n    if (!this.sectionBody || this.props.collapsed === undefined) {\n      return;\n    }\n\n    // Get the current height of the body so max-height transitions can work\n    this.setState({\n      isAnimating: true,\n      maxHeight: `${this._getSectionBodyHeight()}px`,\n    });\n    const { action, userEvent } = SectionMenuOptions.CheckCollapsed(this.props);\n    this.props.dispatch(action);\n    this.props.dispatch(\n      ac.UserEvent({\n        event: userEvent,\n        source: this.props.source,\n      })\n    );\n  }\n\n  onKeyPress(event) {\n    if (event.key === \"Enter\" || event.key === \" \") {\n      event.preventDefault();\n      this.onHeaderClick();\n    }\n  }\n\n  _getSectionBodyHeight() {\n    const div = this.sectionBody;\n    if (div.style.display === \"none\") {\n      // If the div isn't displayed, we can't get it's height. So we display it\n      // to get the height (it doesn't show up because max-height is set to 0px\n      // in CSS). We don't undo this because we are about to expand the section.\n      div.style.display = \"block\";\n    }\n    return div.scrollHeight;\n  }\n\n  onTransitionEnd(event) {\n    // Only update the animating state for our own transition (not a child's)\n    if (event.target === event.currentTarget) {\n      this.setState({ isAnimating: false });\n    }\n  }\n\n  renderIcon() {\n    const { icon } = this.props;\n    if (icon && icon.startsWith(\"moz-extension://\")) {\n      return (\n        <span\n          className=\"icon icon-small-spacer\"\n          style={{ backgroundImage: `url('${icon}')` }}\n        />\n      );\n    }\n    return (\n      <span\n        className={`icon icon-small-spacer icon-${icon || \"webextension\"}`}\n      />\n    );\n  }\n\n  onMenuButtonMouseEnter() {\n    this.setState({ menuButtonHover: true });\n  }\n\n  onMenuButtonMouseLeave() {\n    this.setState({ menuButtonHover: false });\n  }\n\n  onMenuUpdate(showContextMenu) {\n    this.setState({ showContextMenu });\n  }\n\n  render() {\n    const isCollapsible = this.props.collapsed !== undefined;\n    const {\n      enableAnimation,\n      isAnimating,\n      maxHeight,\n      menuButtonHover,\n      showContextMenu,\n    } = this.state;\n    const {\n      id,\n      eventSource,\n      collapsed,\n      learnMore,\n      title,\n      extraMenuOptions,\n      showPrefName,\n      privacyNoticeURL,\n      dispatch,\n      isFixed,\n      isFirst,\n      isLast,\n      isWebExtension,\n    } = this.props;\n    const active = menuButtonHover || showContextMenu;\n    let bodyStyle;\n    if (isAnimating && !collapsed) {\n      bodyStyle = { maxHeight };\n    } else if (!isAnimating && collapsed) {\n      bodyStyle = { display: \"none\" };\n    }\n    return (\n      <section\n        className={`collapsible-section ${this.props.className}${\n          enableAnimation ? \" animation-enabled\" : \"\"\n        }${collapsed ? \" collapsed\" : \"\"}${active ? \" active\" : \"\"}`}\n        aria-expanded={!collapsed}\n        // Note: data-section-id is used for web extension api tests in mozilla central\n        data-section-id={id}\n      >\n        <div className=\"section-top-bar\">\n          <h3 className=\"section-title\">\n            <span className=\"click-target-container\">\n              {/* Click-targets that toggle a collapsible section should have an aria-expanded attribute; see bug 1553234 */}\n              <span\n                className=\"click-target\"\n                role=\"button\"\n                tabIndex=\"0\"\n                onKeyPress={this.onKeyPress}\n                onClick={this.onHeaderClick}\n              >\n                {this.renderIcon()}\n                <FluentOrText message={title} />\n                {isCollapsible && (\n                  <span\n                    data-l10n-id={\n                      collapsed\n                        ? \"newtab-section-expand-section-label\"\n                        : \"newtab-section-collapse-section-label\"\n                    }\n                    className={`collapsible-arrow icon ${\n                      collapsed\n                        ? \"icon-arrowhead-forward-small\"\n                        : \"icon-arrowhead-down-small\"\n                    }`}\n                  />\n                )}\n              </span>\n              <span className=\"learn-more-link-wrapper\">\n                {learnMore && (\n                  <span className=\"learn-more-link\">\n                    <FluentOrText message={learnMore.link.message}>\n                      <a href={learnMore.link.href} />\n                    </FluentOrText>\n                  </span>\n                )}\n              </span>\n            </span>\n          </h3>\n          <div>\n            <ContextMenuButton\n              tooltip=\"newtab-menu-section-tooltip\"\n              onUpdate={this.onMenuUpdate}\n              refFunction={this.setContextMenuButtonRef}\n            >\n              <SectionMenu\n                id={id}\n                extraOptions={extraMenuOptions}\n                eventSource={eventSource}\n                showPrefName={showPrefName}\n                privacyNoticeURL={privacyNoticeURL}\n                collapsed={collapsed}\n                isFixed={isFixed}\n                isFirst={isFirst}\n                isLast={isLast}\n                dispatch={dispatch}\n                isWebExtension={isWebExtension}\n              />\n            </ContextMenuButton>\n          </div>\n        </div>\n        <ErrorBoundary className=\"section-body-fallback\">\n          <div\n            className={`section-body${isAnimating ? \" animating\" : \"\"}`}\n            onTransitionEnd={this.onTransitionEnd}\n            ref={this.onBodyMount}\n            style={bodyStyle}\n          >\n            {this.props.children}\n          </div>\n        </ErrorBoundary>\n      </section>\n    );\n  }\n}\n\nCollapsibleSection.defaultProps = {\n  document: global.document || {\n    addEventListener: () => {},\n    removeEventListener: () => {},\n    visibilityState: \"hidden\",\n  },\n  Prefs: { values: {} },\n};\n"
  },
  {
    "path": "content-src/components/CollapsibleSection/_CollapsibleSection.scss",
    "content": ".collapsible-section {\n  padding: $section-vertical-padding $section-horizontal-padding;\n  transition-delay: 100ms;\n  transition-duration: 100ms;\n  transition-property: background-color;\n\n  .section-title {\n    font-size: $section-title-font-size;\n    font-weight: bold;\n    margin: 0;\n\n    &.grey-title,\n    span {\n      color: var(--newtab-section-header-text-color);\n      display: inline-block;\n      fill: var(--newtab-section-header-text-color);\n      vertical-align: middle;\n    }\n\n    .click-target-container {\n      // Center \"What's Pocket?\" for \"mobile\" viewport\n      @media (max-width: $break-point-medium - 1) {\n        display: block;\n\n        .learn-more-link-wrapper {\n          display: block;\n          text-align: center;\n\n          .learn-more-link {\n            margin-inline-start: 0;\n          }\n        }\n      }\n\n      vertical-align: top;\n\n      .click-target {\n        cursor: pointer;\n        white-space: nowrap;\n      }\n    }\n\n    .collapsible-arrow {\n      margin-inline-start: 8px;\n      margin-top: -1px;\n    }\n  }\n\n  .section-top-bar {\n    min-height: 19px;\n    margin-bottom: 13px;\n    position: relative;\n\n    .context-menu-button {\n      background: url('chrome://global/skin/icons/more.svg') no-repeat right center;\n      border: 0;\n      cursor: pointer;\n      fill: var(--newtab-section-header-text-color);\n      height: 100%;\n      inset-inline-end: 0;\n      opacity: 0;\n      position: absolute;\n      top: 0;\n      transition-duration: 200ms;\n      transition-property: opacity;\n      width: $context-menu-button-size;\n\n      &:-moz-any(:active, :focus, :hover) {\n        fill: var(--newtab-section-header-text-color);\n        opacity: 1;\n      }\n    }\n\n    .context-menu {\n      top: 16px;\n    }\n\n    @media (max-width: $break-point-widest + $card-width * 1.5) {\n      @include context-menu-open-left;\n    }\n  }\n\n  &:hover,\n  &.active {\n    .section-top-bar {\n      .context-menu-button {\n        opacity: 1;\n      }\n    }\n  }\n\n  &.active {\n    background: var(--newtab-element-hover-color);\n    border-radius: 4px;\n\n    .section-top-bar {\n      .context-menu-button {\n        fill: var(--newtab-section-active-contextmenu-color);\n      }\n    }\n  }\n\n  .learn-more-link {\n    font-size: 11px;\n    margin-inline-start: 12px;\n\n    a {\n      color: var(--newtab-link-secondary-color);\n    }\n  }\n\n  .section-body-fallback {\n    height: $card-height;\n  }\n\n  .section-body {\n    // This is so the top sites favicon and card dropshadows don't get clipped during animation:\n    $horizontal-padding: 7px;\n    margin: 0 (-$horizontal-padding);\n    padding: 0 $horizontal-padding;\n\n    &.animating {\n      overflow: hidden;\n      pointer-events: none;\n    }\n  }\n\n  &.animation-enabled {\n    .section-title {\n      .collapsible-arrow {\n        transition: transform 0.5s $photon-easing;\n      }\n    }\n\n    .section-body {\n      transition: max-height 0.5s $photon-easing;\n    }\n  }\n\n  &.collapsed {\n    .section-body {\n      max-height: 0;\n      overflow: hidden;\n    }\n  }\n\n  // Hide first story card for the medium breakpoint to prevent orphaned third story\n  &[data-section-id='topstories'] .card-outer:first-child {\n    @media (min-width: $break-point-medium) and (max-width: $break-point-large - 1) {\n      display: none;\n    }\n  }\n}\n\n"
  },
  {
    "path": "content-src/components/ComponentPerfTimer/ComponentPerfTimer.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport { actionCreators as ac, actionTypes as at } from \"common/Actions.jsm\";\nimport { perfService as perfSvc } from \"common/PerfService.jsm\";\nimport React from \"react\";\n\n// Currently record only a fixed set of sections. This will prevent data\n// from custom sections from showing up or from topstories.\nconst RECORDED_SECTIONS = [\"highlights\", \"topsites\"];\n\nexport class ComponentPerfTimer extends React.Component {\n  constructor(props) {\n    super(props);\n    // Just for test dependency injection:\n    this.perfSvc = this.props.perfSvc || perfSvc;\n\n    this._sendBadStateEvent = this._sendBadStateEvent.bind(this);\n    this._sendPaintedEvent = this._sendPaintedEvent.bind(this);\n    this._reportMissingData = false;\n    this._timestampHandled = false;\n    this._recordedFirstRender = false;\n  }\n\n  componentDidMount() {\n    if (!RECORDED_SECTIONS.includes(this.props.id)) {\n      return;\n    }\n\n    this._maybeSendPaintedEvent();\n  }\n\n  componentDidUpdate() {\n    if (!RECORDED_SECTIONS.includes(this.props.id)) {\n      return;\n    }\n\n    this._maybeSendPaintedEvent();\n  }\n\n  /**\n   * Call the given callback after the upcoming frame paints.\n   *\n   * @note Both setTimeout and requestAnimationFrame are throttled when the page\n   * is hidden, so this callback may get called up to a second or so after the\n   * requestAnimationFrame \"paint\" for hidden tabs.\n   *\n   * Newtabs hidden while loading will presumably be fairly rare (other than\n   * preloaded tabs, which we will be filtering out on the server side), so such\n   * cases should get lost in the noise.\n   *\n   * If we decide that it's important to find out when something that's hidden\n   * has \"painted\", however, another option is to post a message to this window.\n   * That should happen even faster than setTimeout, and, at least as of this\n   * writing, it's not throttled in hidden windows in Firefox.\n   *\n   * @param {Function} callback\n   *\n   * @returns void\n   */\n  _afterFramePaint(callback) {\n    requestAnimationFrame(() => setTimeout(callback, 0));\n  }\n\n  _maybeSendBadStateEvent() {\n    // Follow up bugs:\n    // https://github.com/mozilla/activity-stream/issues/3691\n    if (!this.props.initialized) {\n      // Remember to report back when data is available.\n      this._reportMissingData = true;\n    } else if (this._reportMissingData) {\n      this._reportMissingData = false;\n      // Report how long it took for component to become initialized.\n      this._sendBadStateEvent();\n    }\n  }\n\n  _maybeSendPaintedEvent() {\n    // If we've already handled a timestamp, don't do it again.\n    if (this._timestampHandled || !this.props.initialized) {\n      return;\n    }\n\n    // And if we haven't, we're doing so now, so remember that. Even if\n    // something goes wrong in the callback, we can't try again, as we'd be\n    // sending back the wrong data, and we have to do it here, so that other\n    // calls to this method while waiting for the next frame won't also try to\n    // handle it.\n    this._timestampHandled = true;\n    this._afterFramePaint(this._sendPaintedEvent);\n  }\n\n  /**\n   * Triggered by call to render. Only first call goes through due to\n   * `_recordedFirstRender`.\n   */\n  _ensureFirstRenderTsRecorded() {\n    // Used as t0 for recording how long component took to initialize.\n    if (!this._recordedFirstRender) {\n      this._recordedFirstRender = true;\n      // topsites_first_render_ts, highlights_first_render_ts.\n      const key = `${this.props.id}_first_render_ts`;\n      this.perfSvc.mark(key);\n    }\n  }\n\n  /**\n   * Creates `TELEMETRY_UNDESIRED_EVENT` with timestamp in ms\n   * of how much longer the data took to be ready for display than it would\n   * have been the ideal case.\n   * https://github.com/mozilla/ping-centre/issues/98\n   */\n  _sendBadStateEvent() {\n    // highlights_data_ready_ts, topsites_data_ready_ts.\n    const dataReadyKey = `${this.props.id}_data_ready_ts`;\n    this.perfSvc.mark(dataReadyKey);\n\n    try {\n      const firstRenderKey = `${this.props.id}_first_render_ts`;\n      // value has to be Int32.\n      const value = parseInt(\n        this.perfSvc.getMostRecentAbsMarkStartByName(dataReadyKey) -\n          this.perfSvc.getMostRecentAbsMarkStartByName(firstRenderKey),\n        10\n      );\n      this.props.dispatch(\n        ac.OnlyToMain({\n          type: at.SAVE_SESSION_PERF_DATA,\n          // highlights_data_late_by_ms, topsites_data_late_by_ms.\n          data: { [`${this.props.id}_data_late_by_ms`]: value },\n        })\n      );\n    } catch (ex) {\n      // If this failed, it's likely because the `privacy.resistFingerprinting`\n      // pref is true.\n    }\n  }\n\n  _sendPaintedEvent() {\n    // Record first_painted event but only send if topsites.\n    if (this.props.id !== \"topsites\") {\n      return;\n    }\n\n    // topsites_first_painted_ts.\n    const key = `${this.props.id}_first_painted_ts`;\n    this.perfSvc.mark(key);\n\n    try {\n      const data = {};\n      data[key] = this.perfSvc.getMostRecentAbsMarkStartByName(key);\n\n      this.props.dispatch(\n        ac.OnlyToMain({\n          type: at.SAVE_SESSION_PERF_DATA,\n          data,\n        })\n      );\n    } catch (ex) {\n      // If this failed, it's likely because the `privacy.resistFingerprinting`\n      // pref is true.  We should at least not blow up, and should continue\n      // to set this._timestampHandled to avoid going through this again.\n    }\n  }\n\n  render() {\n    if (RECORDED_SECTIONS.includes(this.props.id)) {\n      this._ensureFirstRenderTsRecorded();\n      this._maybeSendBadStateEvent();\n    }\n    return this.props.children;\n  }\n}\n"
  },
  {
    "path": "content-src/components/ConfirmDialog/ConfirmDialog.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport { actionCreators as ac, actionTypes } from \"common/Actions.jsm\";\nimport { connect } from \"react-redux\";\nimport React from \"react\";\n\n/**\n * ConfirmDialog component.\n * One primary action button, one cancel button.\n *\n * Content displayed is controlled by `data` prop the component receives.\n * Example:\n * data: {\n *   // Any sort of data needed to be passed around by actions.\n *   payload: site.url,\n *   // Primary button AlsoToMain action.\n *   action: \"DELETE_HISTORY_URL\",\n *   // Primary button USerEvent action.\n *   userEvent: \"DELETE\",\n *   // Array of locale ids to display.\n *   message_body: [\"confirm_history_delete_p1\", \"confirm_history_delete_notice_p2\"],\n *   // Text for primary button.\n *   confirm_button_string_id: \"menu_action_delete\"\n * },\n */\nexport class _ConfirmDialog extends React.PureComponent {\n  constructor(props) {\n    super(props);\n    this._handleCancelBtn = this._handleCancelBtn.bind(this);\n    this._handleConfirmBtn = this._handleConfirmBtn.bind(this);\n  }\n\n  _handleCancelBtn() {\n    this.props.dispatch({ type: actionTypes.DIALOG_CANCEL });\n    this.props.dispatch(\n      ac.UserEvent({\n        event: actionTypes.DIALOG_CANCEL,\n        source: this.props.data.eventSource,\n      })\n    );\n  }\n\n  _handleConfirmBtn() {\n    this.props.data.onConfirm.forEach(this.props.dispatch);\n  }\n\n  _renderModalMessage() {\n    const message_body = this.props.data.body_string_id;\n\n    if (!message_body) {\n      return null;\n    }\n\n    return (\n      <span>\n        {message_body.map(msg => (\n          <p key={msg} data-l10n-id={msg} />\n        ))}\n      </span>\n    );\n  }\n\n  render() {\n    if (!this.props.visible) {\n      return null;\n    }\n\n    return (\n      <div className=\"confirmation-dialog\">\n        <div\n          className=\"modal-overlay\"\n          onClick={this._handleCancelBtn}\n          role=\"presentation\"\n        />\n        <div className=\"modal\">\n          <section className=\"modal-message\">\n            {this.props.data.icon && (\n              <span\n                className={`icon icon-spacer icon-${this.props.data.icon}`}\n              />\n            )}\n            {this._renderModalMessage()}\n          </section>\n          <section className=\"actions\">\n            <button\n              onClick={this._handleCancelBtn}\n              data-l10n-id={this.props.data.cancel_button_string_id}\n            />\n            <button\n              className=\"done\"\n              onClick={this._handleConfirmBtn}\n              data-l10n-id={this.props.data.confirm_button_string_id}\n            />\n          </section>\n        </div>\n      </div>\n    );\n  }\n}\n\nexport const ConfirmDialog = connect(state => state.Dialog)(_ConfirmDialog);\n"
  },
  {
    "path": "content-src/components/ConfirmDialog/_ConfirmDialog.scss",
    "content": ".confirmation-dialog {\n  .modal {\n    box-shadow: 0 2px 2px 0 $black-10;\n    left: 0;\n    margin: auto;\n    position: fixed;\n    right: 0;\n    top: 20%;\n    width: 400px;\n  }\n\n  section {\n    margin: 0;\n  }\n\n  .modal-message {\n    display: flex;\n    padding: 16px;\n    padding-bottom: 0;\n\n    p {\n      margin: 0;\n      margin-bottom: 16px;\n    }\n  }\n\n  .actions {\n    border: 0;\n    display: flex;\n    flex-wrap: nowrap;\n    padding: 0 16px;\n\n    button {\n      margin-inline-end: 16px;\n      padding-inline-end: 18px;\n      padding-inline-start: 18px;\n      white-space: normal;\n      width: 50%;\n\n      &.done {\n        margin-inline-end: 0;\n        margin-inline-start: 0;\n      }\n    }\n  }\n\n  .icon {\n    margin-inline-end: 16px;\n  }\n}\n\n.modal-overlay {\n  background: var(--newtab-overlay-color);\n  height: 100%;\n  left: 0;\n  position: fixed;\n  top: 0;\n  width: 100%;\n  z-index: 11001;\n}\n\n.modal {\n  background: var(--newtab-modal-color);\n  border: $border-secondary;\n  border-radius: 5px;\n  font-size: 15px;\n  z-index: 11002;\n}\n"
  },
  {
    "path": "content-src/components/ContextMenu/ContextMenu.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport React from \"react\";\n\nexport class ContextMenu extends React.PureComponent {\n  constructor(props) {\n    super(props);\n    this.hideContext = this.hideContext.bind(this);\n    this.onShow = this.onShow.bind(this);\n    this.onClick = this.onClick.bind(this);\n  }\n\n  hideContext() {\n    this.props.onUpdate(false);\n  }\n\n  onShow() {\n    if (this.props.onShow) {\n      this.props.onShow();\n    }\n  }\n\n  componentDidMount() {\n    this.onShow();\n    setTimeout(() => {\n      global.addEventListener(\"click\", this.hideContext);\n    }, 0);\n  }\n\n  componentWillUnmount() {\n    global.removeEventListener(\"click\", this.hideContext);\n  }\n\n  onClick(event) {\n    // Eat all clicks on the context menu so they don't bubble up to window.\n    // This prevents the context menu from closing when clicking disabled items\n    // or the separators.\n    event.stopPropagation();\n  }\n\n  render() {\n    // Disabling focus on the menu span allows the first tab to focus on the first menu item instead of the wrapper.\n    return (\n      // eslint-disable-next-line jsx-a11y/interactive-supports-focus\n      <span className=\"context-menu\">\n        <ul\n          role=\"menu\"\n          onClick={this.onClick}\n          onKeyDown={this.onClick}\n          className=\"context-menu-list\"\n        >\n          {this.props.options.map((option, i) =>\n            option.type === \"separator\" ? (\n              <li key={i} className=\"separator\" role=\"separator\" />\n            ) : (\n              option.type !== \"empty\" && (\n                <ContextMenuItem\n                  key={i}\n                  option={option}\n                  hideContext={this.hideContext}\n                  keyboardAccess={this.props.keyboardAccess}\n                />\n              )\n            )\n          )}\n        </ul>\n      </span>\n    );\n  }\n}\n\nexport class ContextMenuItem extends React.PureComponent {\n  constructor(props) {\n    super(props);\n    this.onClick = this.onClick.bind(this);\n    this.onKeyDown = this.onKeyDown.bind(this);\n    this.onKeyUp = this.onKeyUp.bind(this);\n    this.focusFirst = this.focusFirst.bind(this);\n  }\n\n  onClick() {\n    this.props.hideContext();\n    this.props.option.onClick();\n  }\n\n  // Focus the first menu item if the menu was accessed via the keyboard.\n  focusFirst(button) {\n    if (this.props.keyboardAccess && button) {\n      button.focus();\n    }\n  }\n\n  // This selects the correct node based on the key pressed\n  focusSibling(target, key) {\n    const parent = target.parentNode;\n    const closestSiblingSelector =\n      key === \"ArrowUp\" ? \"previousSibling\" : \"nextSibling\";\n    if (!parent[closestSiblingSelector]) {\n      return;\n    }\n    if (parent[closestSiblingSelector].firstElementChild) {\n      parent[closestSiblingSelector].firstElementChild.focus();\n    } else {\n      parent[closestSiblingSelector][\n        closestSiblingSelector\n      ].firstElementChild.focus();\n    }\n  }\n\n  onKeyDown(event) {\n    const { option } = this.props;\n    switch (event.key) {\n      case \"Tab\":\n        // tab goes down in context menu, shift + tab goes up in context menu\n        // if we're on the last item, one more tab will close the context menu\n        // similarly, if we're on the first item, one more shift + tab will close it\n        if (\n          (event.shiftKey && option.first) ||\n          (!event.shiftKey && option.last)\n        ) {\n          this.props.hideContext();\n        }\n        break;\n      case \"ArrowUp\":\n      case \"ArrowDown\":\n        event.preventDefault();\n        this.focusSibling(event.target, event.key);\n        break;\n      case \"Enter\":\n      case \" \":\n        event.preventDefault();\n        this.props.hideContext();\n        option.onClick();\n        break;\n      case \"Escape\":\n        this.props.hideContext();\n        break;\n    }\n  }\n\n  // Prevents the default behavior of spacebar\n  // scrolling the page & auto-triggering buttons.\n  onKeyUp(event) {\n    if (event.key === \" \") {\n      event.preventDefault();\n    }\n  }\n\n  render() {\n    const { option } = this.props;\n    return (\n      <li role=\"presentation\" className=\"context-menu-item\">\n        <button\n          className={option.disabled ? \"disabled\" : \"\"}\n          role=\"menuitem\"\n          onClick={this.onClick}\n          onKeyDown={this.onKeyDown}\n          onKeyUp={this.onKeyUp}\n          ref={option.first ? this.focusFirst : null}\n        >\n          {option.icon && (\n            <span className={`icon icon-spacer icon-${option.icon}`} />\n          )}\n          <span data-l10n-id={option.string_id || option.id} />\n        </button>\n      </li>\n    );\n  }\n}\n"
  },
  {
    "path": "content-src/components/ContextMenu/ContextMenuButton.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport React from \"react\";\n\nexport class ContextMenuButton extends React.PureComponent {\n  constructor(props) {\n    super(props);\n    this.state = {\n      showContextMenu: false,\n      contextMenuKeyboard: false,\n    };\n    this.onClick = this.onClick.bind(this);\n    this.onKeyDown = this.onKeyDown.bind(this);\n    this.onUpdate = this.onUpdate.bind(this);\n  }\n\n  openContextMenu(isKeyBoard, event) {\n    if (this.props.onUpdate) {\n      this.props.onUpdate(true);\n    }\n    this.setState({\n      showContextMenu: true,\n      contextMenuKeyboard: isKeyBoard,\n    });\n  }\n\n  onClick(event) {\n    event.preventDefault();\n    this.openContextMenu(false, event);\n  }\n\n  onKeyDown(event) {\n    if (event.key === \"Enter\" || event.key === \" \") {\n      event.preventDefault();\n      this.openContextMenu(true, event);\n    }\n  }\n\n  onUpdate(showContextMenu) {\n    if (this.props.onUpdate) {\n      this.props.onUpdate(showContextMenu);\n    }\n    this.setState({ showContextMenu });\n  }\n\n  render() {\n    const { tooltipArgs, tooltip, children, refFunction } = this.props;\n    const { showContextMenu, contextMenuKeyboard } = this.state;\n\n    return (\n      <React.Fragment>\n        <button\n          aria-haspopup=\"true\"\n          data-l10n-id={tooltip}\n          data-l10n-args={tooltipArgs ? JSON.stringify(tooltipArgs) : null}\n          className=\"context-menu-button icon\"\n          onKeyDown={this.onKeyDown}\n          onClick={this.onClick}\n          ref={refFunction}\n        />\n        {showContextMenu\n          ? React.cloneElement(children, {\n              keyboardAccess: contextMenuKeyboard,\n              onUpdate: this.onUpdate,\n            })\n          : null}\n      </React.Fragment>\n    );\n  }\n}\n"
  },
  {
    "path": "content-src/components/ContextMenu/_ContextMenu.scss",
    "content": ".context-menu {\n  background: var(--newtab-contextmenu-background-color);\n  border-radius: $context-menu-border-radius;\n  box-shadow: $context-menu-shadow;\n  display: block;\n  font-size: $context-menu-font-size;\n  margin-inline-start: 5px;\n  inset-inline-start: 100%;\n  position: absolute;\n  top: ($context-menu-button-size / 4);\n  z-index: 8;\n\n  > ul {\n    list-style: none;\n    margin: 0;\n    padding: $context-menu-outer-padding 0;\n\n    > li {\n      margin: 0;\n      width: 100%;\n\n      &.separator {\n        border-bottom: $border-secondary;\n        margin: $context-menu-outer-padding 0;\n      }\n\n      > a,\n      > button {\n        align-items: center;\n        color: inherit;\n        cursor: pointer;\n        display: flex;\n        width: 100%;\n        line-height: 16px;\n        outline: none;\n        border: 0;\n        padding: $context-menu-item-padding;\n        white-space: nowrap;\n\n        &:-moz-any(:focus, :hover) {\n          background: var(--newtab-element-hover-color);\n        }\n\n        &:active {\n          background: var(--newtab-element-active-color);\n        }\n\n        &.disabled {\n          opacity: 0.4;\n          pointer-events: none;\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport { actionCreators as ac } from \"common/Actions.jsm\";\nimport { CardGrid } from \"content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid\";\nimport { CollapsibleSection } from \"content-src/components/CollapsibleSection/CollapsibleSection\";\nimport { connect } from \"react-redux\";\nimport { DSDismiss } from \"content-src/components/DiscoveryStreamComponents/DSDismiss/DSDismiss\";\nimport { DSMessage } from \"content-src/components/DiscoveryStreamComponents/DSMessage/DSMessage\";\nimport { DSPrivacyModal } from \"content-src/components/DiscoveryStreamComponents/DSPrivacyModal/DSPrivacyModal\";\nimport { DSTextPromo } from \"content-src/components/DiscoveryStreamComponents/DSTextPromo/DSTextPromo\";\nimport { Hero } from \"content-src/components/DiscoveryStreamComponents/Hero/Hero\";\nimport { Highlights } from \"content-src/components/DiscoveryStreamComponents/Highlights/Highlights\";\nimport { HorizontalRule } from \"content-src/components/DiscoveryStreamComponents/HorizontalRule/HorizontalRule\";\nimport { List } from \"content-src/components/DiscoveryStreamComponents/List/List\";\nimport { Navigation } from \"content-src/components/DiscoveryStreamComponents/Navigation/Navigation\";\nimport React from \"react\";\nimport { SectionTitle } from \"content-src/components/DiscoveryStreamComponents/SectionTitle/SectionTitle\";\nimport { selectLayoutRender } from \"content-src/lib/selectLayoutRender\";\nimport { TopSites } from \"content-src/components/DiscoveryStreamComponents/TopSites/TopSites\";\n\nconst ALLOWED_CSS_URL_PREFIXES = [\n  \"chrome://\",\n  \"resource://\",\n  \"https://img-getpocket.cdn.mozilla.net/\",\n];\nconst DUMMY_CSS_SELECTOR = \"DUMMY#CSS.SELECTOR\";\nlet rollCache = []; // Cache of random probability values for a spoc position\n\n/**\n * Validate a CSS declaration. The values are assumed to be normalized by CSSOM.\n */\nexport function isAllowedCSS(property, value) {\n  // Bug 1454823: INTERNAL properties, e.g., -moz-context-properties, are\n  // exposed but their values aren't resulting in getting nothing. Fortunately,\n  // we don't care about validating the values of the current set of properties.\n  if (value === undefined) {\n    return true;\n  }\n\n  // Make sure all urls are of the allowed protocols/prefixes\n  const urls = value.match(/url\\(\"[^\"]+\"\\)/g);\n  return (\n    !urls ||\n    urls.every(url =>\n      ALLOWED_CSS_URL_PREFIXES.some(prefix => url.slice(5).startsWith(prefix))\n    )\n  );\n}\n\nexport class _DiscoveryStreamBase extends React.PureComponent {\n  constructor(props) {\n    super(props);\n    this.onStyleMount = this.onStyleMount.bind(this);\n  }\n\n  onStyleMount(style) {\n    // Unmounting style gets rid of old styles, so nothing else to do\n    if (!style) {\n      return;\n    }\n\n    const { sheet } = style;\n    const styles = JSON.parse(style.dataset.styles);\n    styles.forEach((row, rowIndex) => {\n      row.forEach((component, componentIndex) => {\n        // Nothing to do without optional styles overrides\n        if (!component) {\n          return;\n        }\n\n        Object.entries(component).forEach(([selectors, declarations]) => {\n          // Start with a dummy rule to validate declarations and selectors\n          sheet.insertRule(`${DUMMY_CSS_SELECTOR} {}`);\n          const [rule] = sheet.cssRules;\n\n          // Validate declarations and remove any offenders. CSSOM silently\n          // discards invalid entries, so here we apply extra restrictions.\n          rule.style = declarations;\n          [...rule.style].forEach(property => {\n            const value = rule.style[property];\n            if (!isAllowedCSS(property, value)) {\n              console.error(`Bad CSS declaration ${property}: ${value}`); // eslint-disable-line no-console\n              rule.style.removeProperty(property);\n            }\n          });\n\n          // Set the actual desired selectors scoped to the component\n          const prefix = `.ds-layout > .ds-column:nth-child(${rowIndex +\n            1}) .ds-column-grid > :nth-child(${componentIndex + 1})`;\n          // NB: Splitting on \",\" doesn't work with strings with commas, but\n          // we're okay with not supporting those selectors\n          rule.selectorText = selectors\n            .split(\",\")\n            .map(\n              selector =>\n                prefix +\n                // Assume :pseudo-classes are for component instead of descendant\n                (selector[0] === \":\" ? \"\" : \" \") +\n                selector\n            )\n            .join(\",\");\n\n          // CSSOM silently ignores bad selectors, so we'll be noisy instead\n          if (rule.selectorText === DUMMY_CSS_SELECTOR) {\n            console.error(`Bad CSS selector ${selectors}`); // eslint-disable-line no-console\n          }\n        });\n      });\n    });\n  }\n\n  renderComponent(component, embedWidth) {\n    const ENGAGEMENT_LABEL_ENABLED = this.props.Prefs.values[\n      `discoverystream.engagementLabelEnabled`\n    ];\n\n    switch (component.type) {\n      case \"Highlights\":\n        return <Highlights />;\n      case \"TopSites\":\n        let promoAlignment;\n        if (\n          component.spocs &&\n          component.spocs.positions &&\n          component.spocs.positions.length\n        ) {\n          promoAlignment =\n            component.spocs.positions[0].index === 0 ? \"left\" : \"right\";\n        }\n        return (\n          <TopSites\n            header={component.header}\n            data={component.data}\n            promoAlignment={promoAlignment}\n          />\n        );\n      case \"TextPromo\":\n        if (\n          !component.data ||\n          !component.data.spocs ||\n          !component.data.spocs[0]\n        ) {\n          return null;\n        }\n        // Grab the first item in the array as we only have 1 spoc position.\n        const [spoc] = component.data.spocs;\n        const {\n          image_src,\n          raw_image_src,\n          alt_text,\n          title,\n          url,\n          context,\n          cta,\n          flight_id,\n          id,\n          shim,\n        } = spoc;\n\n        return (\n          <DSDismiss\n            data={{\n              url: spoc.url,\n              guid: spoc.id,\n              shim: spoc.shim,\n            }}\n            dispatch={this.props.dispatch}\n            shouldSendImpressionStats={true}\n            extraClasses={`ds-dismiss-ds-text-promo`}\n          >\n            <DSTextPromo\n              dispatch={this.props.dispatch}\n              image={image_src}\n              raw_image_src={raw_image_src}\n              alt_text={alt_text || title}\n              header={title}\n              cta_text={cta}\n              cta_url={url}\n              subtitle={context}\n              flightId={flight_id}\n              id={id}\n              pos={0}\n              shim={shim}\n              type={component.type}\n            />\n          </DSDismiss>\n        );\n      case \"Message\":\n        return (\n          <DSMessage\n            title={component.header && component.header.title}\n            subtitle={component.header && component.header.subtitle}\n            link_text={component.header && component.header.link_text}\n            link_url={component.header && component.header.link_url}\n            icon={component.header && component.header.icon}\n          />\n        );\n      case \"SectionTitle\":\n        return <SectionTitle header={component.header} />;\n      case \"Navigation\":\n        return (\n          <Navigation\n            links={component.properties.links}\n            alignment={component.properties.alignment}\n            header={component.header}\n          />\n        );\n      case \"CardGrid\":\n        return (\n          <CardGrid\n            title={component.header && component.header.title}\n            data={component.data}\n            feed={component.feed}\n            border={component.properties.border}\n            type={component.type}\n            dispatch={this.props.dispatch}\n            items={component.properties.items}\n            cta_variant={component.cta_variant}\n            display_engagement_labels={ENGAGEMENT_LABEL_ENABLED}\n          />\n        );\n      case \"Hero\":\n        return (\n          <Hero\n            subComponentType={embedWidth >= 9 ? `cards` : `list`}\n            feed={component.feed}\n            title={component.header && component.header.title}\n            data={component.data}\n            border={component.properties.border}\n            type={component.type}\n            dispatch={this.props.dispatch}\n            items={component.properties.items}\n          />\n        );\n      case \"HorizontalRule\":\n        return <HorizontalRule />;\n      case \"List\":\n        return (\n          <List\n            data={component.data}\n            feed={component.feed}\n            fullWidth={component.properties.full_width}\n            hasBorders={component.properties.border === \"border\"}\n            hasImages={component.properties.has_images}\n            hasNumbers={component.properties.has_numbers}\n            items={component.properties.items}\n            type={component.type}\n            header={component.header}\n          />\n        );\n      default:\n        return <div>{component.type}</div>;\n    }\n  }\n\n  renderStyles(styles) {\n    // Use json string as both the key and styles to render so React knows when\n    // to unmount and mount a new instance for new styles.\n    const json = JSON.stringify(styles);\n    return <style key={json} data-styles={json} ref={this.onStyleMount} />;\n  }\n\n  componentWillReceiveProps(oldProps) {\n    if (this.props.DiscoveryStream.layout !== oldProps.DiscoveryStream.layout) {\n      rollCache = [];\n    }\n  }\n\n  render() {\n    // Select layout render data by adding spocs and position to recommendations\n    const { layoutRender, spocsFill } = selectLayoutRender({\n      state: this.props.DiscoveryStream,\n      prefs: this.props.Prefs.values,\n      rollCache,\n      lang: this.props.document.documentElement.lang,\n    });\n    const { config, spocs, feeds } = this.props.DiscoveryStream;\n\n    // Send SPOCS Fill if any. Note that it should not send it again if the same\n    // page gets re-rendered by state changes.\n    if (\n      spocs.loaded &&\n      feeds.loaded &&\n      spocsFill.length &&\n      !this._spocsFillSent\n    ) {\n      this.props.dispatch(\n        ac.DiscoveryStreamSpocsFill({ spoc_fills: spocsFill })\n      );\n      this._spocsFillSent = true;\n    }\n\n    // Allow rendering without extracting special components\n    if (!config.collapsible) {\n      return this.renderLayout(layoutRender);\n    }\n\n    // Find the first component of a type and remove it from layout\n    const extractComponent = type => {\n      for (const [rowIndex, row] of Object.entries(layoutRender)) {\n        for (const [index, component] of Object.entries(row.components)) {\n          if (component.type === type) {\n            // Remove the row if it was the only component or the single item\n            if (row.components.length === 1) {\n              layoutRender.splice(rowIndex, 1);\n            } else {\n              row.components.splice(index, 1);\n            }\n            return component;\n          }\n        }\n      }\n      return null;\n    };\n\n    // Get \"topstories\" Section state for default values\n    const topStories = this.props.Sections.find(s => s.id === \"topstories\");\n\n    if (!topStories) {\n      return null;\n    }\n\n    // Extract TopSites to render before the rest and Message to use for header\n    const topSites = extractComponent(\"TopSites\");\n    const message = extractComponent(\"Message\") || {\n      header: {\n        link_text: topStories.learnMore.link.message,\n        link_url: topStories.learnMore.link.href,\n        title: topStories.title,\n      },\n    };\n\n    // Render a DS-style TopSites then the rest if any in a collapsible section\n    return (\n      <React.Fragment>\n        {this.props.DiscoveryStream.isPrivacyInfoModalVisible && (\n          <DSPrivacyModal dispatch={this.props.dispatch} />\n        )}\n        {topSites &&\n          this.renderLayout([\n            {\n              width: 12,\n              components: [topSites],\n            },\n          ])}\n        {!!layoutRender.length && (\n          <CollapsibleSection\n            className=\"ds-layout\"\n            collapsed={topStories.pref.collapsed}\n            dispatch={this.props.dispatch}\n            icon={topStories.icon}\n            id={topStories.id}\n            isFixed={true}\n            learnMore={{\n              link: {\n                href: message.header.link_url,\n                message: message.header.link_text,\n              },\n            }}\n            privacyNoticeURL={topStories.privacyNoticeURL}\n            showPrefName={topStories.pref.feed}\n            title={message.header.title}\n          >\n            {this.renderLayout(layoutRender)}\n          </CollapsibleSection>\n        )}\n        {this.renderLayout([\n          {\n            width: 12,\n            components: [{ type: \"Highlights\" }],\n          },\n        ])}\n      </React.Fragment>\n    );\n  }\n\n  renderLayout(layoutRender) {\n    const styles = [];\n    return (\n      <div className=\"discovery-stream ds-layout\">\n        {layoutRender.map((row, rowIndex) => (\n          <div\n            key={`row-${rowIndex}`}\n            className={`ds-column ds-column-${row.width}`}\n          >\n            <div className=\"ds-column-grid\">\n              {row.components.map((component, componentIndex) => {\n                if (!component) {\n                  return null;\n                }\n                styles[rowIndex] = [\n                  ...(styles[rowIndex] || []),\n                  component.styles,\n                ];\n                return (\n                  <div key={`component-${componentIndex}`}>\n                    {this.renderComponent(component, row.width)}\n                  </div>\n                );\n              })}\n            </div>\n          </div>\n        ))}\n        {this.renderStyles(styles)}\n      </div>\n    );\n  }\n}\n\nexport const DiscoveryStreamBase = connect(state => ({\n  DiscoveryStream: state.DiscoveryStream,\n  Prefs: state.Prefs,\n  Sections: state.Sections,\n  document: global.document,\n}))(_DiscoveryStreamBase);\n"
  },
  {
    "path": "content-src/components/DiscoveryStreamBase/_DiscoveryStreamBase.scss",
    "content": "$ds-width: 936px;\n\n.discovery-stream.ds-layout {\n  $columns: 12;\n  --gridColumnGap: 48px;\n  --gridRowGap: 24px;\n  display: grid;\n  grid-template-columns: repeat($columns, 1fr);\n  grid-column-gap: var(--gridColumnGap);\n  grid-row-gap: var(--gridRowGap);\n  width: $ds-width;\n  margin: 0 auto;\n\n  @while $columns > 0 {\n    .ds-column-#{$columns} {\n      grid-column-start: auto;\n      grid-column-end: span $columns;\n    }\n\n    $columns: $columns - 1;\n  }\n\n  .ds-column-grid {\n    display: grid;\n    grid-row-gap: var(--gridRowGap);\n  }\n}\n\n.ds-header {\n  margin: 8px 0;\n}\n\n.ds-header,\n.ds-layout .section-title span {\n  @include dark-theme-only {\n    color: $grey-30;\n  }\n\n  color: $grey-50;\n  font-size: 13px;\n  font-weight: 600;\n  line-height: 20px;\n\n  .icon {\n    fill: var(--newtab-text-secondary-color);\n  }\n}\n\n.collapsible-section.ds-layout {\n  margin: auto;\n  width: $ds-width + 2 * $section-horizontal-padding;\n\n  .section-top-bar {\n    .learn-more-link a {\n      color: var(--newtab-link-primary-color);\n      font-weight: normal;\n\n      &:-moz-any(:focus, :hover) {\n        text-decoration: underline;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport { DSCard, PlaceholderDSCard } from \"../DSCard/DSCard.jsx\";\nimport { DSEmptyState } from \"../DSEmptyState/DSEmptyState.jsx\";\nimport React from \"react\";\n\nexport class CardGrid extends React.PureComponent {\n  renderCards() {\n    const recs = this.props.data.recommendations.slice(0, this.props.items);\n    const cards = [];\n\n    for (let index = 0; index < this.props.items; index++) {\n      const rec = recs[index];\n      cards.push(\n        !rec || rec.placeholder ? (\n          <PlaceholderDSCard key={`dscard-${index}`} />\n        ) : (\n          <DSCard\n            key={`dscard-${rec.id}`}\n            pos={rec.pos}\n            flightId={rec.flight_id}\n            image_src={rec.image_src}\n            raw_image_src={rec.raw_image_src}\n            title={rec.title}\n            excerpt={rec.excerpt}\n            url={rec.url}\n            id={rec.id}\n            shim={rec.shim}\n            type={this.props.type}\n            context={rec.context}\n            sponsor={rec.sponsor}\n            dispatch={this.props.dispatch}\n            source={rec.domain}\n            pocket_id={rec.pocket_id}\n            context_type={rec.context_type}\n            bookmarkGuid={rec.bookmarkGuid}\n            engagement={rec.engagement}\n            display_engagement_labels={this.props.display_engagement_labels}\n            cta={rec.cta}\n            cta_variant={this.props.cta_variant}\n          />\n        )\n      );\n    }\n\n    let divisibility = ``;\n\n    if (this.props.items % 4 === 0) {\n      divisibility = `divisible-by-4`;\n    } else if (this.props.items % 3 === 0) {\n      divisibility = `divisible-by-3`;\n    }\n\n    return (\n      <div\n        className={`ds-card-grid ds-card-grid-${\n          this.props.border\n        } ds-card-grid-${divisibility}`}\n      >\n        {cards}\n      </div>\n    );\n  }\n\n  render() {\n    const { data } = this.props;\n\n    // Handle a render before feed has been fetched by displaying nothing\n    if (!data) {\n      return null;\n    }\n\n    // Handle the case where a user has dismissed all recommendations\n    const isEmpty = data.recommendations.length === 0;\n\n    return (\n      <div>\n        {this.props.title && (\n          <div className=\"ds-header\">{this.props.title}</div>\n        )}\n        {isEmpty ? (\n          <div className=\"ds-card-grid empty\">\n            <DSEmptyState\n              status={data.status}\n              dispatch={this.props.dispatch}\n              feed={this.props.feed}\n            />\n          </div>\n        ) : (\n          this.renderCards()\n        )}\n      </div>\n    );\n  }\n}\n\nCardGrid.defaultProps = {\n  border: `border`,\n  items: 4, // Number of stories to display\n};\n"
  },
  {
    "path": "content-src/components/DiscoveryStreamComponents/CardGrid/_CardGrid.scss",
    "content": "$col4-header-line-height: 20;\n$col4-header-font-size: 14;\n\n.ds-card-grid {\n  display: grid;\n  grid-gap: 24px;\n\n  .ds-card {\n    @include dark-theme-only {\n      background: none;\n    }\n\n    background: $white;\n    border-radius: 4px;\n  }\n\n  .ds-card-link:focus {\n    @include ds-fade-in;\n  }\n\n  &.ds-card-grid-border {\n    .ds-card:not(.placeholder) {\n      @include dark-theme-only {\n        box-shadow: 0 1px 4px $shadow-10;\n        background: $grey-70;\n      }\n\n      box-shadow: 0 1px 4px 0 $grey-90-10;\n\n      .img-wrapper .img img {\n        border-radius: 4px 4px 0 0;\n      }\n    }\n  }\n\n  &.ds-card-grid-no-border {\n    .ds-card {\n      background: none;\n\n      .meta {\n        padding: 12px 0;\n      }\n    }\n  }\n\n  // \"2/3 width layout\"\n  .ds-column-5 &,\n  .ds-column-6 &,\n  .ds-column-7 &,\n  .ds-column-8 & {\n    grid-template-columns: repeat(2, 1fr);\n  }\n\n  // \"Full width layout\"\n  .ds-column-9 &,\n  .ds-column-10 &,\n  .ds-column-11 &,\n  .ds-column-12 & {\n    grid-template-columns: repeat(4, 1fr);\n\n    &.ds-card-grid-divisible-by-3 {\n      grid-template-columns: repeat(3, 1fr);\n\n      .title {\n        font-size: 17px;\n        line-height: 24px;\n      }\n\n      .excerpt {\n        @include limit-visible-lines(3, 24, 15);\n      }\n    }\n\n    &.ds-card-grid-divisible-by-4 .title {\n      @include limit-visible-lines(3, 20, 15);\n    }\n  }\n\n  &.empty {\n    grid-template-columns: auto;\n  }\n}\n"
  },
  {
    "path": "content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport { actionCreators as ac } from \"common/Actions.jsm\";\nimport { DSImage } from \"../DSImage/DSImage.jsx\";\nimport { DSLinkMenu } from \"../DSLinkMenu/DSLinkMenu\";\nimport { ImpressionStats } from \"../../DiscoveryStreamImpressionStats/ImpressionStats\";\nimport React from \"react\";\nimport { SafeAnchor } from \"../SafeAnchor/SafeAnchor\";\nimport { DSContextFooter } from \"../DSContextFooter/DSContextFooter.jsx\";\n\n// Default Meta that displays CTA as link if cta_variant in layout is set as \"link\"\nexport const DefaultMeta = ({\n  display_engagement_labels,\n  source,\n  title,\n  excerpt,\n  context,\n  context_type,\n  cta,\n  engagement,\n  cta_variant,\n}) => (\n  <div className=\"meta\">\n    <div className=\"info-wrap\">\n      <p className=\"source clamp\">{source}</p>\n      <header className=\"title clamp\">{title}</header>\n      {excerpt && <p className=\"excerpt clamp\">{excerpt}</p>}\n      {cta_variant === \"link\" && cta && (\n        <div role=\"link\" className=\"cta-link icon icon-arrow\" tabIndex=\"0\">\n          {cta}\n        </div>\n      )}\n    </div>\n    <DSContextFooter\n      context_type={context_type}\n      context={context}\n      display_engagement_labels={display_engagement_labels}\n      engagement={engagement}\n    />\n  </div>\n);\n\nexport const CTAButtonMeta = ({\n  display_engagement_labels,\n  source,\n  title,\n  excerpt,\n  context,\n  context_type,\n  cta,\n  engagement,\n  sponsor,\n}) => (\n  <div className=\"meta\">\n    <div className=\"info-wrap\">\n      <p className=\"source clamp\">\n        {sponsor ? sponsor : source}\n        {context && ` · Sponsored`}\n      </p>\n      <header className=\"title clamp\">{title}</header>\n      {excerpt && <p className=\"excerpt clamp\">{excerpt}</p>}\n    </div>\n    {context && cta && <button className=\"button cta-button\">{cta}</button>}\n    {!context && (\n      <DSContextFooter\n        context_type={context_type}\n        context={context}\n        display_engagement_labels={display_engagement_labels}\n        engagement={engagement}\n      />\n    )}\n  </div>\n);\n\nexport class DSCard extends React.PureComponent {\n  constructor(props) {\n    super(props);\n\n    this.onLinkClick = this.onLinkClick.bind(this);\n    this.setPlaceholderRef = element => {\n      this.placeholderElement = element;\n    };\n\n    this.state = {\n      isSeen: false,\n    };\n  }\n\n  onLinkClick(event) {\n    if (this.props.dispatch) {\n      this.props.dispatch(\n        ac.UserEvent({\n          event: \"CLICK\",\n          source: this.props.type.toUpperCase(),\n          action_position: this.props.pos,\n          value: { card_type: this.props.flightId ? \"spoc\" : \"organic\" },\n        })\n      );\n\n      this.props.dispatch(\n        ac.ImpressionStats({\n          source: this.props.type.toUpperCase(),\n          click: 0,\n          tiles: [\n            {\n              id: this.props.id,\n              pos: this.props.pos,\n              ...(this.props.shim && this.props.shim.click\n                ? { shim: this.props.shim.click }\n                : {}),\n            },\n          ],\n        })\n      );\n    }\n  }\n\n  onSeen(entries) {\n    if (this.state) {\n      const entry = entries.find(e => e.isIntersecting);\n\n      if (entry) {\n        if (this.placeholderElement) {\n          this.observer.unobserve(this.placeholderElement);\n        }\n\n        // Stop observing since element has been seen\n        this.setState({\n          isSeen: true,\n        });\n      }\n    }\n  }\n\n  onIdleCallback() {\n    if (!this.state.isSeen) {\n      if (this.observer && this.placeholderElement) {\n        this.observer.unobserve(this.placeholderElement);\n      }\n      this.setState({\n        isSeen: true,\n      });\n    }\n  }\n\n  componentDidMount() {\n    this.idleCallbackId = this.props.windowObj.requestIdleCallback(\n      this.onIdleCallback.bind(this)\n    );\n    if (this.placeholderElement) {\n      this.observer = new IntersectionObserver(this.onSeen.bind(this));\n      this.observer.observe(this.placeholderElement);\n    }\n  }\n\n  componentWillUnmount() {\n    // Remove observer on unmount\n    if (this.observer && this.placeholderElement) {\n      this.observer.unobserve(this.placeholderElement);\n    }\n    if (this.idleCallbackId) {\n      this.props.windowObj.cancelIdleCallback(this.idleCallbackId);\n    }\n  }\n\n  render() {\n    if (this.props.placeholder || !this.state.isSeen) {\n      return (\n        <div className=\"ds-card placeholder\" ref={this.setPlaceholderRef} />\n      );\n    }\n    const isButtonCTA = this.props.cta_variant === \"button\";\n\n    return (\n      <div className=\"ds-card\">\n        <SafeAnchor\n          className=\"ds-card-link\"\n          dispatch={this.props.dispatch}\n          onLinkClick={!this.props.placeholder ? this.onLinkClick : undefined}\n          url={this.props.url}\n        >\n          <div className=\"img-wrapper\">\n            <DSImage\n              extraClassNames=\"img\"\n              source={this.props.image_src}\n              rawSource={this.props.raw_image_src}\n            />\n          </div>\n          {isButtonCTA ? (\n            <CTAButtonMeta\n              display_engagement_labels={this.props.display_engagement_labels}\n              source={this.props.source}\n              title={this.props.title}\n              excerpt={this.props.excerpt}\n              context={this.props.context}\n              context_type={this.props.context_type}\n              engagement={this.props.engagement}\n              cta={this.props.cta}\n              sponsor={this.props.sponsor}\n            />\n          ) : (\n            <DefaultMeta\n              display_engagement_labels={this.props.display_engagement_labels}\n              source={this.props.source}\n              title={this.props.title}\n              excerpt={this.props.excerpt}\n              context={this.props.context}\n              engagement={this.props.engagement}\n              context_type={this.props.context_type}\n              cta={this.props.cta}\n              cta_variant={this.props.cta_variant}\n            />\n          )}\n          <ImpressionStats\n            flightId={this.props.flightId}\n            rows={[\n              {\n                id: this.props.id,\n                pos: this.props.pos,\n                ...(this.props.shim && this.props.shim.impression\n                  ? { shim: this.props.shim.impression }\n                  : {}),\n              },\n            ]}\n            dispatch={this.props.dispatch}\n            source={this.props.type}\n          />\n        </SafeAnchor>\n        <DSLinkMenu\n          id={this.props.id}\n          index={this.props.pos}\n          dispatch={this.props.dispatch}\n          url={this.props.url}\n          title={this.props.title}\n          source={this.props.source}\n          type={this.props.type}\n          pocket_id={this.props.pocket_id}\n          shim={this.props.shim}\n          bookmarkGuid={this.props.bookmarkGuid}\n          flightId={this.props.flightId}\n        />\n      </div>\n    );\n  }\n}\n\nDSCard.defaultProps = {\n  windowObj: window, // Added to support unit tests\n};\n\nexport const PlaceholderDSCard = props => <DSCard placeholder={true} />;\n"
  },
  {
    "path": "content-src/components/DiscoveryStreamComponents/DSCard/_DSCard.scss",
    "content": "// Type sizes\n$header-font-size: 17;\n$header-line-height: 24;\n$excerpt-font-size: 14;\n$excerpt-line-height: 20;\n\n.ds-card {\n  display: flex;\n  flex-direction: column;\n  position: relative;\n\n  &.placeholder {\n    background: transparent;\n    box-shadow: inset $inner-box-shadow;\n    border-radius: 4px;\n    min-height: 300px;\n  }\n\n  .img-wrapper {\n    width: 100%;\n  }\n\n  .img {\n    height: 0;\n    padding-top: 50%; // 2:1 aspect ratio\n\n    img {\n      border-radius: 4px;\n      box-shadow: inset 0 0 0 0.5px $black-15;\n    }\n  }\n\n  .ds-card-link {\n    height: 100%;\n    display: flex;\n    flex-direction: column;\n\n    &:hover {\n      @include ds-fade-in($grey-30);\n\n      @include dark-theme-only {\n        @include ds-fade-in($grey-60);\n      }\n\n      header {\n        @include dark-theme-only {\n          color: $blue-40;\n        }\n\n        color: $blue-60;\n      }\n    }\n\n    &:focus {\n      @include ds-fade-in;\n\n      @include dark-theme-only {\n        @include ds-fade-in($blue-40-40);\n      }\n\n      header {\n        @include dark-theme-only {\n          color: $blue-40;\n        }\n\n        color: $blue-60;\n      }\n    }\n\n    &:active {\n      @include ds-fade-in($grey-30);\n\n      @include dark-theme-only {\n        @include ds-fade-in($grey-60);\n      }\n\n      header {\n        @include dark-theme-only {\n          color: $blue-50;\n        }\n\n        color: $blue-70;\n      }\n    }\n  }\n\n  .meta {\n    display: flex;\n    flex-direction: column;\n    padding: 12px 16px;\n    flex-grow: 1;\n\n    .info-wrap {\n      flex-grow: 1;\n    }\n\n    .title {\n      // show only 3 lines of copy\n      @include limit-visible-lines(3, $header-line-height, $header-font-size);\n      font-weight: 600;\n    }\n\n    .excerpt {\n      // show only 3 lines of copy\n      @include limit-visible-lines(\n        3,\n        $excerpt-line-height,\n        $excerpt-font-size\n      );\n    }\n\n    .source {\n      @include dark-theme-only {\n        color: $grey-40;\n      }\n\n      -webkit-line-clamp: 1;\n      margin-bottom: 2px;\n      font-size: 13px;\n      color: $grey-50;\n    }\n\n    .cta-button {\n      @include dark-theme-only {\n        color: $grey-10;\n        background: $grey-90-70;\n      }\n\n      width: 100%;\n      margin: 12px 0 4px;\n      box-shadow: none;\n      border-radius: 4px;\n      height: 32px;\n      font-size: 14px;\n      font-weight: 600;\n      padding: 5px 8px 7px;\n      border: 0;\n      color: $grey-90;\n      background: $grey-90-10;\n\n      &:focus {\n        @include dark-theme-only {\n          background: $grey-90-70;\n          box-shadow: 0 0 0 2px $grey-80, 0 0 0 5px $blue-50-50;\n        }\n\n        background: $grey-90-10;\n        box-shadow: 0 0 0 2px $white, 0 0 0 5px $blue-50-50;\n      }\n\n      &:hover {\n        @include dark-theme-only {\n          background: $grey-90-50;\n        }\n\n        background: $grey-90-20;\n      }\n\n      &:active {\n        @include dark-theme-only {\n          background: $grey-90-70;\n        }\n\n        background: $grey-90-30;\n      }\n    }\n\n    .cta-link {\n      @include dark-theme-only {\n        color: $blue-40;\n        fill: $blue-40;\n      }\n\n      font-size: 15px;\n      font-weight: 600;\n      line-height: 24px;\n      height: 24px;\n      width: auto;\n      background-size: auto;\n      background-position: right 1.5px;\n      padding-right: 9px;\n      color: $blue-60;\n      fill: $blue-60;\n\n      &:focus {\n        @include dark-theme-only {\n          box-shadow: 0 0 0 1px $grey-80, 0 0 0 4px $blue-50-50;\n        }\n\n        box-shadow: 0 0 0 1px $white, 0 0 0 4px $blue-50-50;\n        border-radius: 4px;\n        outline: 0;\n      }\n\n      &:active {\n        @include dark-theme-only {\n          color: $blue-50;\n          fill: $blue-50;\n          box-shadow: none;\n        }\n\n        color: $blue-70;\n        fill: $blue-70;\n        box-shadow: none;\n      }\n\n      &:hover {\n        text-decoration: underline;\n      }\n    }\n  }\n\n  header {\n    @include dark-theme-only {\n      color: $grey-10;\n    }\n\n    line-height: $header-line-height * 1px;\n    font-size: $header-font-size * 1px;\n    color: $grey-90;\n  }\n\n  p {\n    @include dark-theme-only {\n      color: $grey-10;\n    }\n\n    font-size: $excerpt-font-size * 1px;\n    line-height: $excerpt-line-height * 1px;\n    color: $grey-90;\n    margin: 0;\n  }\n}\n"
  },
  {
    "path": "content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport { cardContextTypes } from \"../../Card/types.js\";\nimport { CSSTransition, TransitionGroup } from \"react-transition-group\";\nimport React from \"react\";\n\n// Animation time is mirrored in DSContextFooter.scss\nconst ANIMATION_DURATION = 3000;\n\nexport const StatusMessage = ({ icon, fluentID }) => (\n  <div className=\"status-message\">\n    <span\n      aria-haspopup=\"true\"\n      className={`story-badge-icon icon icon-${icon}`}\n    />\n    <div className=\"story-context-label\" data-l10n-id={fluentID} />\n  </div>\n);\n\nexport class DSContextFooter extends React.PureComponent {\n  render() {\n    // display_engagement_labels is based on pref `browser.newtabpage.activity-stream.discoverystream.engagementLabelEnabled`\n    const {\n      context,\n      context_type,\n      engagement,\n      display_engagement_labels,\n    } = this.props;\n    const { icon, fluentID } = cardContextTypes[context_type] || {};\n\n    return (\n      <div className=\"story-footer\">\n        {context && <p className=\"story-sponsored-label clamp\">{context}</p>}\n        <TransitionGroup component={null}>\n          {!context &&\n            (context_type || (display_engagement_labels && engagement)) && (\n              <CSSTransition\n                key={fluentID}\n                timeout={ANIMATION_DURATION}\n                classNames=\"story-animate\"\n              >\n                {engagement && !context_type ? (\n                  <div className=\"story-view-count\">{engagement}</div>\n                ) : (\n                  <StatusMessage icon={icon} fluentID={fluentID} />\n                )}\n              </CSSTransition>\n            )}\n        </TransitionGroup>\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "content-src/components/DiscoveryStreamComponents/DSContextFooter/_DSContextFooter.scss",
    "content": "$status-green: #058B00;\n$status-dark-green: #7C6;\n\n.story-footer {\n  color: var(--newtab-text-secondary-color);\n  inset-inline-start: 0;\n  margin-top: 12px;\n  position: relative;\n\n  .story-sponsored-label,\n  .story-view-count,\n  .status-message {\n    @include dark-theme-only {\n      color: $grey-40;\n    }\n\n    -webkit-line-clamp: 1;\n    font-size: 13px;\n    line-height: 24px;\n    color: $grey-50;\n  }\n\n  .status-message {\n    display: flex;\n    align-items: center;\n    height: 24px;\n\n    .story-badge-icon {\n      @include dark-theme-only {\n        fill: $grey-40;\n      }\n\n      fill: $grey-50;\n      height: 16px;\n      margin-inline-end: 6px;\n\n      &.icon-bookmark-removed {\n        background-image: url('#{$image-path}icon-removed-bookmark.svg');\n      }\n    }\n\n    .story-context-label {\n      @include dark-theme-only {\n        color: $grey-40;\n      }\n\n      color: $grey-50;\n      flex-grow: 1;\n      font-size: 13px;\n      line-height: 24px;\n      overflow: hidden;\n      text-overflow: ellipsis;\n      white-space: nowrap;\n    }\n  }\n}\n\n.story-animate-enter {\n  opacity: 0;\n}\n\n.story-animate-enter-active {\n  opacity: 1;\n  transition: opacity 150ms ease-in 300ms;\n\n  .story-badge-icon,\n  .story-context-label {\n    @include dark-theme-only {\n      animation: dark-color 3s ease-out 0.3s;\n    }\n\n    animation: color 3s ease-out 0.3s;\n\n    @keyframes color {\n      0% {\n        color: $status-green;\n        fill: $status-green;\n      }\n\n      100% {\n        color: $grey-50;\n        fill: $grey-50;\n      }\n    }\n\n    @keyframes dark-color {\n      0% {\n        color: $status-dark-green;\n        fill: $status-dark-green;\n      }\n\n      100% {\n        color: $grey-40;\n        fill: $grey-40;\n      }\n    }\n  }\n}\n\n.story-animate-exit {\n  position: absolute;\n  top: 0;\n  opacity: 1;\n}\n\n.story-animate-exit-active {\n  opacity: 0;\n  transition: opacity 250ms ease-in;\n}\n"
  },
  {
    "path": "content-src/components/DiscoveryStreamComponents/DSDismiss/DSDismiss.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport { actionCreators as ac } from \"common/Actions.jsm\";\nimport React from \"react\";\nimport { LinkMenuOptions } from \"content-src/lib/link-menu-options\";\n\nexport class DSDismiss extends React.PureComponent {\n  constructor(props) {\n    super(props);\n    this.onDismissClick = this.onDismissClick.bind(this);\n    this.onHover = this.onHover.bind(this);\n    this.offHover = this.offHover.bind(this);\n    this.state = {\n      hovering: false,\n    };\n  }\n\n  onDismissClick() {\n    const index = 0;\n    const source = \"DISCOVERY_STREAM\";\n    const blockUrlOption = LinkMenuOptions.BlockUrl(\n      this.props.data,\n      index,\n      source\n    );\n\n    const { action, impression, userEvent } = blockUrlOption;\n\n    this.props.dispatch(action);\n    const userEventData = Object.assign(\n      {\n        event: userEvent,\n        source,\n        action_position: index,\n      },\n      this.props.data\n    );\n    this.props.dispatch(ac.UserEvent(userEventData));\n    if (impression && this.props.shouldSendImpressionStats) {\n      this.props.dispatch(impression);\n    }\n  }\n\n  onHover() {\n    this.setState({\n      hovering: true,\n    });\n  }\n\n  offHover() {\n    this.setState({\n      hovering: false,\n    });\n  }\n\n  render() {\n    let className = `ds-dismiss\n      ${this.state.hovering ? ` hovering` : ``}\n      ${this.props.extraClasses ? ` ${this.props.extraClasses}` : ``}`;\n\n    return (\n      <div className={className}>\n        {this.props.children}\n        <button\n          className=\"ds-dismiss-button\"\n          onHover={this.onHover}\n          onClick={this.onDismissClick}\n          onMouseEnter={this.onHover}\n          onMouseLeave={this.offHover}\n          aria-label=\"dismiss\"\n        >\n          <span className=\"icon icon-dismiss\" />\n        </button>\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "content-src/components/DiscoveryStreamComponents/DSDismiss/_DSDismiss.scss",
    "content": ".ds-dismiss {\n  position: relative;\n  overflow: hidden;\n  border-radius: 8px;\n  transition-delay: 100ms;\n  transition-duration: 200ms;\n  transition-property: background;\n\n  &.hovering {\n    @include dark-theme-only {\n      background: $grey-90-30;\n    }\n\n    background: $grey-90-10;\n  }\n\n  &:hover {\n    .ds-dismiss-button {\n      opacity: 1;\n    }\n  }\n\n  .ds-dismiss-button {\n    @include dark-theme-only {\n      background: $grey-90-30;\n    }\n\n    border: 0;\n    cursor: pointer;\n    height: 32px;\n    width: 32px;\n    padding: 0;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    position: absolute;\n    right: 0;\n    top: 0;\n    border-radius: 50%;\n    margin: 18px 18px 0 0;\n    background: $grey-90-10;\n\n    &:hover {\n      @include dark-theme-only {\n        background: $grey-90-50;\n      }\n\n      background: $grey-90-20;\n    }\n\n    &:active {\n      @include dark-theme-only {\n        background: $grey-90-70;\n      }\n\n      background: $grey-90-30;\n    }\n\n    &:focus {\n      @include dark-theme-only {\n        box-shadow: 0 0 0 2px $grey-80, 0 0 0 5px $blue-50-50;\n      }\n\n      box-shadow: 0 0 0 2px $white, 0 0 0 5px $blue-50-50;\n    }\n  }\n}\n"
  },
  {
    "path": "content-src/components/DiscoveryStreamComponents/DSEmptyState/DSEmptyState.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport { actionCreators as ac, actionTypes as at } from \"common/Actions.jsm\";\nimport React from \"react\";\n\nexport class DSEmptyState extends React.PureComponent {\n  constructor(props) {\n    super(props);\n    this.onReset = this.onReset.bind(this);\n    this.state = {};\n  }\n\n  componentWillUnmount() {\n    if (this.timeout) {\n      clearTimeout(this.timeout);\n    }\n  }\n\n  onReset() {\n    if (this.props.dispatch && this.props.feed) {\n      const { feed } = this.props;\n      const { url } = feed;\n      this.props.dispatch({\n        type: at.DISCOVERY_STREAM_FEED_UPDATE,\n        data: {\n          feed: {\n            ...feed,\n            data: {\n              ...feed.data,\n              status: \"waiting\",\n            },\n          },\n          url,\n        },\n      });\n\n      this.setState({ waiting: true });\n      this.timeout = setTimeout(() => {\n        this.timeout = null;\n        this.setState({\n          waiting: false,\n        });\n      }, 300);\n\n      this.props.dispatch(\n        ac.OnlyToMain({ type: at.DISCOVERY_STREAM_RETRY_FEED, data: { feed } })\n      );\n    }\n  }\n\n  renderButton() {\n    if (this.props.status === \"waiting\" || this.state.waiting) {\n      return (\n        <button\n          className=\"try-again-button waiting\"\n          data-l10n-id=\"newtab-discovery-empty-section-topstories-loading\"\n        />\n      );\n    }\n\n    return (\n      <button\n        className=\"try-again-button\"\n        onClick={this.onReset}\n        data-l10n-id=\"newtab-discovery-empty-section-topstories-try-again-button\"\n      />\n    );\n  }\n\n  renderState() {\n    if (this.props.status === \"waiting\" || this.props.status === \"failed\") {\n      return (\n        <React.Fragment>\n          <h2 data-l10n-id=\"newtab-discovery-empty-section-topstories-timed-out\" />\n          {this.renderButton()}\n        </React.Fragment>\n      );\n    }\n\n    return (\n      <React.Fragment>\n        <h2 data-l10n-id=\"newtab-discovery-empty-section-topstories-header\" />\n        <p data-l10n-id=\"newtab-discovery-empty-section-topstories-content\" />\n      </React.Fragment>\n    );\n  }\n\n  render() {\n    return (\n      <div className=\"section-empty-state\">\n        <div className=\"empty-state-message\">{this.renderState()}</div>\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "content-src/components/DiscoveryStreamComponents/DSEmptyState/_DSEmptyState.scss",
    "content": ".section-empty-state {\n  border: $border-secondary;\n  border-radius: 4px;\n  display: flex;\n  height: $card-height-compact;\n  width: 100%;\n\n  .empty-state-message {\n    color: var(--newtab-text-secondary-color);\n    font-size: 14px;\n    line-height: 20px;\n    text-align: center;\n    margin: auto;\n    max-width: 936px;\n  }\n\n  .try-again-button {\n    margin-top: 12px;\n    padding: 6px 32px;\n    border-radius: 2px;\n    border: 0;\n    background: var(--newtab-feed-button-background);\n    color: var(--newtab-feed-button-text);\n    cursor: pointer;\n    position: relative;\n    transition: background 0.2s ease, color 0.2s ease;\n\n    &:not(.waiting) {\n      &:focus {\n        @include ds-fade-in;\n\n        @include dark-theme-only {\n          @include ds-fade-in($blue-40-40);\n        }\n      }\n\n      &:hover {\n        @include ds-fade-in($grey-30);\n\n        @include dark-theme-only {\n          @include ds-fade-in($grey-60);\n        }\n      }\n    }\n\n    &::after {\n      content: '';\n      height: 20px;\n      width: 20px;\n      animation: spinner 1s linear infinite;\n      opacity: 0;\n      position: absolute;\n      top: 50%;\n      left: 50%;\n      margin: -10px 0 0 -10px;\n      mask-image: url('../data/content/assets/spinner.svg');\n      mask-size: 20px;\n      background: var(--newtab-feed-button-spinner);\n    }\n\n    &.waiting {\n      cursor: initial;\n      background: var(--newtab-feed-button-background-faded);\n      color: var(--newtab-feed-button-text-faded);\n      transition: background 0.2s ease;\n\n      &::after {\n        transition: opacity 0.2s ease;\n        opacity: 1;\n      }\n    }\n  }\n\n  h2 {\n    font-size: 15px;\n    font-weight: 600;\n    margin: 0;\n  }\n\n  p {\n    margin: 0;\n  }\n}\n\n@keyframes spinner {\n  to { transform: rotate(360deg); }\n}\n"
  },
  {
    "path": "content-src/components/DiscoveryStreamComponents/DSImage/DSImage.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport React from \"react\";\nimport ReactDOM from \"react-dom\";\n\nexport class DSImage extends React.PureComponent {\n  constructor(props) {\n    super(props);\n\n    this.onOptimizedImageError = this.onOptimizedImageError.bind(this);\n    this.onNonOptimizedImageError = this.onNonOptimizedImageError.bind(this);\n\n    this.state = {\n      isSeen: false,\n      optimizedImageFailed: false,\n      useTransition: false,\n    };\n  }\n\n  onSeen(entries) {\n    if (this.state) {\n      const entry = entries.find(e => e.isIntersecting);\n\n      if (entry) {\n        if (this.props.optimize) {\n          this.setState({\n            // Thumbor doesn't handle subpixels and just errors out, so rounding...\n            containerWidth: Math.round(entry.boundingClientRect.width),\n            containerHeight: Math.round(entry.boundingClientRect.height),\n          });\n        }\n\n        this.setState({\n          isSeen: true,\n        });\n\n        // Stop observing since element has been seen\n        this.observer.unobserve(ReactDOM.findDOMNode(this));\n      }\n    }\n  }\n\n  onIdleCallback() {\n    if (!this.state.isSeen) {\n      this.setState({\n        useTransition: true,\n      });\n    }\n  }\n\n  reformatImageURL(url, width, height) {\n    // Change the image URL to request a size tailored for the parent container width\n    // Also: force JPEG, quality 60, no upscaling, no EXIF data\n    // Uses Thumbor: https://thumbor.readthedocs.io/en/latest/usage.html\n    return `https://img-getpocket.cdn.mozilla.net/${width}x${height}/filters:format(jpeg):quality(60):no_upscale():strip_exif()/${encodeURIComponent(\n      url\n    )}`;\n  }\n\n  componentDidMount() {\n    this.idleCallbackId = this.props.windowObj.requestIdleCallback(\n      this.onIdleCallback.bind(this)\n    );\n    this.observer = new IntersectionObserver(this.onSeen.bind(this), {\n      // Assume an image will be eventually seen if it is within\n      // half the average Desktop vertical screen size:\n      // http://gs.statcounter.com/screen-resolution-stats/desktop/north-america\n      rootMargin: `540px`,\n    });\n    this.observer.observe(ReactDOM.findDOMNode(this));\n  }\n\n  componentWillUnmount() {\n    // Remove observer on unmount\n    if (this.observer) {\n      this.observer.unobserve(ReactDOM.findDOMNode(this));\n    }\n    if (this.idleCallbackId) {\n      this.props.windowObj.cancelIdleCallback(this.idleCallbackId);\n    }\n  }\n\n  render() {\n    let classNames = `ds-image\n      ${this.props.extraClassNames ? ` ${this.props.extraClassNames}` : ``}\n      ${this.state && this.state.useTransition ? ` use-transition` : ``}\n      ${this.state && this.state.isSeen ? ` loaded` : ``}\n    `;\n\n    let img;\n\n    if (this.state && this.state.isSeen) {\n      if (\n        this.props.optimize &&\n        this.props.rawSource &&\n        !this.state.optimizedImageFailed\n      ) {\n        let source;\n        let source2x;\n\n        if (this.state && this.state.containerWidth) {\n          let baseSource = this.props.rawSource;\n\n          source = this.reformatImageURL(\n            baseSource,\n            this.state.containerWidth,\n            this.state.containerHeight\n          );\n\n          source2x = this.reformatImageURL(\n            baseSource,\n            this.state.containerWidth * 2,\n            this.state.containerHeight * 2\n          );\n\n          img = (\n            <img\n              alt={this.props.alt_text}\n              crossOrigin=\"anonymous\"\n              onError={this.onOptimizedImageError}\n              src={source}\n              srcSet={`${source2x} 2x`}\n            />\n          );\n        }\n      } else if (!this.state.nonOptimizedImageFailed) {\n        img = (\n          <img\n            alt={this.props.alt_text}\n            crossOrigin=\"anonymous\"\n            onError={this.onNonOptimizedImageError}\n            src={this.props.source}\n          />\n        );\n      } else {\n        // Remove the img element if both sources fail. Render a placeholder instead.\n        img = <div className=\"broken-image\" />;\n      }\n    }\n\n    return <picture className={classNames}>{img}</picture>;\n  }\n\n  onOptimizedImageError() {\n    // This will trigger a re-render and the unoptimized 450px image will be used as a fallback\n    this.setState({\n      optimizedImageFailed: true,\n    });\n  }\n\n  onNonOptimizedImageError() {\n    this.setState({\n      nonOptimizedImageFailed: true,\n    });\n  }\n}\n\nDSImage.defaultProps = {\n  source: null, // The current source style from Pocket API (always 450px)\n  rawSource: null, // Unadulterated image URL to filter through Thumbor\n  extraClassNames: null, // Additional classnames to append to component\n  optimize: true, // Measure parent container to request exact sizes\n  alt_text: null,\n  windowObj: window, // Added to support unit tests\n};\n"
  },
  {
    "path": "content-src/components/DiscoveryStreamComponents/DSImage/_DSImage.scss",
    "content": ".ds-image {\n  display: block;\n  position: relative;\n  opacity: 0;\n\n  &.use-transition {\n    transition: opacity 0.8s;\n  }\n\n  &.loaded {\n    opacity: 1;\n  }\n\n  img,\n  .broken-image {\n    background-color: var(--newtab-card-placeholder-color);\n    position: absolute;\n    top: 0;\n    width: 100%;\n    height: 100%;\n    object-fit: cover;\n  }\n}\n"
  },
  {
    "path": "content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport { LinkMenu } from \"content-src/components/LinkMenu/LinkMenu\";\nimport { ContextMenuButton } from \"content-src/components/ContextMenu/ContextMenuButton\";\nimport React from \"react\";\n\nexport class DSLinkMenu extends React.PureComponent {\n  constructor(props) {\n    super(props);\n    this.onMenuUpdate = this.onMenuUpdate.bind(this);\n    this.onMenuShow = this.onMenuShow.bind(this);\n    this.contextMenuButtonRef = React.createRef();\n  }\n\n  onMenuUpdate(showContextMenu) {\n    if (!showContextMenu) {\n      const dsLinkMenuHostDiv = this.contextMenuButtonRef.current.parentElement;\n      dsLinkMenuHostDiv.parentElement.classList.remove(\"active\", \"last-item\");\n    }\n  }\n\n  nextAnimationFrame() {\n    return new Promise(resolve =>\n      this.props.windowObj.requestAnimationFrame(resolve)\n    );\n  }\n\n  async onMenuShow() {\n    const dsLinkMenuHostDiv = this.contextMenuButtonRef.current.parentElement;\n    // Wait for next frame before computing scrollMaxX to allow fluent menu strings to be visible\n    await this.nextAnimationFrame();\n    if (this.props.windowObj.scrollMaxX > 0) {\n      dsLinkMenuHostDiv.parentElement.classList.add(\"last-item\");\n    }\n    dsLinkMenuHostDiv.parentElement.classList.add(\"active\");\n  }\n\n  render() {\n    const { index, dispatch } = this.props;\n    const TOP_STORIES_CONTEXT_MENU_OPTIONS = [\n      \"CheckBookmarkOrArchive\",\n      \"CheckSavedToPocket\",\n      \"Separator\",\n      \"OpenInNewWindow\",\n      \"OpenInPrivateWindow\",\n      \"Separator\",\n      \"BlockUrl\",\n      ...(this.props.flightId ? [\"ShowPrivacyInfo\"] : []),\n    ];\n    const type = this.props.type || \"DISCOVERY_STREAM\";\n    const title = this.props.title || this.props.source;\n\n    return (\n      <div>\n        <ContextMenuButton\n          refFunction={this.contextMenuButtonRef}\n          tooltip={\"newtab-menu-content-tooltip\"}\n          tooltipArgs={{ title }}\n          onUpdate={this.onMenuUpdate}\n        >\n          <LinkMenu\n            dispatch={dispatch}\n            index={index}\n            source={type.toUpperCase()}\n            onShow={this.onMenuShow}\n            options={TOP_STORIES_CONTEXT_MENU_OPTIONS}\n            shouldSendImpressionStats={true}\n            site={{\n              referrer: \"https://getpocket.com/recommendations\",\n              title: this.props.title,\n              type: this.props.type,\n              url: this.props.url,\n              guid: this.props.id,\n              pocket_id: this.props.pocket_id,\n              shim: this.props.shim,\n              bookmarkGuid: this.props.bookmarkGuid,\n              flight_id: this.props.flightId,\n            }}\n          />\n        </ContextMenuButton>\n      </div>\n    );\n  }\n}\n\nDSLinkMenu.defaultProps = {\n  windowObj: window, // Added to support unit tests\n};\n"
  },
  {
    "path": "content-src/components/DiscoveryStreamComponents/DSLinkMenu/_DSLinkMenu.scss",
    "content": ".ds-hero-item,\n.ds-list-item,\n.ds-card {\n  @include context-menu-button;\n\n  .context-menu {\n    opacity: 0;\n  }\n\n  &.active {\n    .context-menu {\n      opacity: 1;\n    }\n  }\n\n  &.last-item {\n    @include context-menu-open-left;\n\n    .context-menu {\n      opacity: 1;\n    }\n  }\n\n  &:-moz-any(:hover, :focus, .active) {\n    @include context-menu-button-hover;\n    outline: none;\n\n    &.ds-card-grid-border {\n      @include fade-in-card;\n    }\n  }\n}\n"
  },
  {
    "path": "content-src/components/DiscoveryStreamComponents/DSMessage/DSMessage.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport React from \"react\";\nimport { SafeAnchor } from \"../SafeAnchor/SafeAnchor\";\nimport { FluentOrText } from \"content-src/components/FluentOrText/FluentOrText\";\n\nexport class DSMessage extends React.PureComponent {\n  render() {\n    return (\n      <div className=\"ds-message\">\n        <header className=\"title\">\n          {this.props.icon && (\n            <div\n              className=\"glyph\"\n              style={{ backgroundImage: `url(${this.props.icon})` }}\n            />\n          )}\n          {this.props.title && (\n            <span className=\"title-text\">\n              <FluentOrText message={this.props.title} />\n            </span>\n          )}\n          {this.props.link_text && this.props.link_url && (\n            <SafeAnchor className=\"link\" url={this.props.link_url}>\n              <FluentOrText message={this.props.link_text} />\n            </SafeAnchor>\n          )}\n        </header>\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "content-src/components/DiscoveryStreamComponents/DSMessage/_DSMessage.scss",
    "content": ".ds-message {\n  margin: 8px 0 0;\n\n  .title {\n    display: flex;\n    align-items: center;\n\n    .glyph {\n      @include dark-theme-only {\n        fill: $grey-30;\n      }\n\n      width: 16px;\n      height: 16px;\n      margin: 0 6px 0 0;\n      -moz-context-properties: fill;\n      fill: $grey-50;\n      background-position: center center;\n      background-size: 16px;\n      background-repeat: no-repeat;\n    }\n\n    .title-text {\n      @include dark-theme-only {\n        color: $grey-30;\n      }\n\n      line-height: 20px;\n      font-size: 13px;\n      color: $grey-50;\n      font-weight: 600;\n      padding-right: 12px;\n    }\n\n    .link {\n      line-height: 20px;\n      font-size: 13px;\n\n      &:hover,\n      &:focus {\n        text-decoration: underline;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "content-src/components/DiscoveryStreamComponents/DSPrivacyModal/DSPrivacyModal.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport React from \"react\";\nimport { actionCreators as ac } from \"common/Actions.jsm\";\nimport { ModalOverlayWrapper } from \"content-src/asrouter/components/ModalOverlay/ModalOverlay\";\n\nexport class DSPrivacyModal extends React.PureComponent {\n  constructor(props) {\n    super(props);\n    this.closeModal = this.closeModal.bind(this);\n    this.onLinkClick = this.onLinkClick.bind(this);\n  }\n\n  onLinkClick(event) {\n    this.props.dispatch(\n      ac.UserEvent({\n        event: \"CLICK_PRIVACY_INFO\",\n        source: \"DS_PRIVACY_MODAL\",\n      })\n    );\n  }\n\n  closeModal() {\n    this.props.dispatch({\n      type: `HIDE_PRIVACY_INFO`,\n      data: {},\n    });\n  }\n\n  render() {\n    return (\n      <ModalOverlayWrapper\n        onClose={this.closeModal}\n        innerClassName=\"ds-privacy-modal\"\n      >\n        <div className=\"privacy-notice\">\n          <h3 data-l10n-id=\"newtab-privacy-modal-header\" />\n          <p data-l10n-id=\"newtab-privacy-modal-paragraph\" />\n          <a\n            data-l10n-id=\"newtab-privacy-modal-link\"\n            onClick={this.onLinkClick}\n            href=\"https://www.mozilla.org/en-US/privacy/firefox/\"\n          />\n        </div>\n        <section className=\"actions\">\n          <button\n            className=\"done\"\n            type=\"submit\"\n            onClick={this.closeModal}\n            data-l10n-id=\"newtab-privacy-modal-button-done\"\n          />\n        </section>\n      </ModalOverlayWrapper>\n    );\n  }\n}\n"
  },
  {
    "path": "content-src/components/DiscoveryStreamComponents/DSPrivacyModal/_DSPrivacyModal.scss",
    "content": ".ds-privacy-modal {\n  a:hover {\n    text-decoration: underline;\n  }\n\n  .privacy-notice {\n    width: 492px;\n    padding: 40px 0;\n    margin: auto;\n  }\n}\n"
  },
  {
    "path": "content-src/components/DiscoveryStreamComponents/DSTextPromo/DSTextPromo.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport { actionCreators as ac } from \"common/Actions.jsm\";\nimport { DSImage } from \"../DSImage/DSImage.jsx\";\nimport { ImpressionStats } from \"../../DiscoveryStreamImpressionStats/ImpressionStats\";\nimport React from \"react\";\nimport { SafeAnchor } from \"../SafeAnchor/SafeAnchor\";\n\nexport class DSTextPromo extends React.PureComponent {\n  constructor(props) {\n    super(props);\n    this.onLinkClick = this.onLinkClick.bind(this);\n  }\n\n  onLinkClick() {\n    if (this.props.dispatch) {\n      this.props.dispatch(\n        ac.UserEvent({\n          event: \"CLICK\",\n          source: this.props.type.toUpperCase(),\n          action_position: this.props.pos,\n        })\n      );\n\n      this.props.dispatch(\n        ac.ImpressionStats({\n          source: this.props.type.toUpperCase(),\n          click: 0,\n          tiles: [\n            {\n              id: this.props.id,\n              pos: this.props.pos,\n              ...(this.props.shim && this.props.shim.click\n                ? { shim: this.props.shim.click }\n                : {}),\n            },\n          ],\n        })\n      );\n    }\n  }\n\n  render() {\n    return (\n      <div className=\"ds-text-promo\">\n        <DSImage\n          alt_text={this.props.alt_text}\n          source={this.props.image}\n          rawSource={this.props.raw_image_src}\n        />\n        <div className=\"text\">\n          <h3>\n            {`${this.props.header}\\u2003`}\n            <SafeAnchor\n              className=\"ds-chevron-link\"\n              dispatch={this.props.dispatch}\n              onLinkClick={this.onLinkClick}\n              url={this.props.cta_url}\n            >\n              {this.props.cta_text}\n            </SafeAnchor>\n          </h3>\n          <p className=\"subtitle\">{this.props.subtitle}</p>\n        </div>\n        <ImpressionStats\n          flightId={this.props.flightId}\n          rows={[\n            {\n              id: this.props.id,\n              pos: this.props.pos,\n              shim: this.props.shim && this.props.shim.impression,\n            },\n          ]}\n          dispatch={this.props.dispatch}\n          source={this.props.type}\n        />\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "content-src/components/DiscoveryStreamComponents/DSTextPromo/_DSTextPromo.scss",
    "content": ".ds-dismiss-ds-text-promo {\n  width: 744px;\n  margin: auto;\n}\n\n.ds-text-promo {\n  display: flex;\n  max-width: 640px;\n  margin: 18px 24px;\n\n  .ds-image {\n    width: 40px;\n    height: 40px;\n    margin: 4px 12px 0 0;\n    flex-shrink: 0;\n\n    img {\n      border-radius: 4px;\n    }\n  }\n\n  .text {\n    line-height: 24px;\n  }\n\n  h3 {\n    @include dark-theme-only {\n      color: $grey-10;\n    }\n\n    margin: 0;\n    font-weight: 600;\n    font-size: 15px;\n  }\n\n  .subtitle {\n    @include dark-theme-only {\n      color: $grey-40;\n    }\n\n    font-size: 13px;\n    margin: 0;\n    color: $grey-50;\n  }\n}\n\n.ds-chevron-link {\n  color: $blue-60;\n  display: inline-block;\n  outline: 0;\n\n  &:hover {\n    text-decoration: underline;\n  }\n\n  &:active {\n    @include dark-theme-only {\n      color: $blue-50;\n    }\n\n    color: $blue-70;\n\n    &::after {\n      @include dark-theme-only {\n        background-color: $blue-50;\n      }\n\n      background-color: $blue-70;\n    }\n  }\n\n  &:focus {\n    @include dark-theme-only {\n      box-shadow: 0 0 0 2px $grey-80, 0 0 0 5px $blue-50-50;\n    }\n\n    box-shadow: 0 0 0 2px $white, 0 0 0 5px $blue-50-50;\n    border-radius: 2px;\n  }\n\n  &::after {\n    @include dark-theme-only {\n      background-color: $blue-40;\n    }\n\n    content: ' ';\n    mask: url('#{$image-path}glyph-caret-right.svg') 0 -8px no-repeat;\n    background-color: $blue-60;\n    margin: 0 0 0 4px;\n    width: 5px;\n    height: 8px;\n    text-decoration: none;\n    display: inline-block;\n  }\n}\n"
  },
  {
    "path": "content-src/components/DiscoveryStreamComponents/Hero/Hero.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport { DSCard, PlaceholderDSCard } from \"../DSCard/DSCard.jsx\";\nimport { actionCreators as ac } from \"common/Actions.jsm\";\nimport { DSEmptyState } from \"../DSEmptyState/DSEmptyState.jsx\";\nimport { DSImage } from \"../DSImage/DSImage.jsx\";\nimport { DSLinkMenu } from \"../DSLinkMenu/DSLinkMenu\";\nimport { ImpressionStats } from \"../../DiscoveryStreamImpressionStats/ImpressionStats\";\nimport { List } from \"../List/List.jsx\";\nimport React from \"react\";\nimport { SafeAnchor } from \"../SafeAnchor/SafeAnchor\";\nimport { DSContextFooter } from \"../DSContextFooter/DSContextFooter.jsx\";\n\nexport class Hero extends React.PureComponent {\n  constructor(props) {\n    super(props);\n    this.onLinkClick = this.onLinkClick.bind(this);\n  }\n\n  onLinkClick(event) {\n    if (this.props.dispatch) {\n      this.props.dispatch(\n        ac.UserEvent({\n          event: \"CLICK\",\n          source: this.props.type.toUpperCase(),\n          action_position: this.heroRec.pos,\n        })\n      );\n\n      this.props.dispatch(\n        ac.ImpressionStats({\n          source: this.props.type.toUpperCase(),\n          click: 0,\n          tiles: [\n            {\n              id: this.heroRec.id,\n              pos: this.heroRec.pos,\n              ...(this.heroRec.shim && this.heroRec.shim.click\n                ? { shim: this.heroRec.shim.click }\n                : {}),\n            },\n          ],\n        })\n      );\n    }\n  }\n\n  renderHero() {\n    let [heroRec, ...otherRecs] = this.props.data.recommendations.slice(\n      0,\n      this.props.items\n    );\n    this.heroRec = heroRec;\n\n    const cards = [];\n    for (let index = 0; index < this.props.items - 1; index++) {\n      const rec = otherRecs[index];\n      cards.push(\n        !rec || rec.placeholder ? (\n          <PlaceholderDSCard key={`dscard-${index}`} />\n        ) : (\n          <DSCard\n            flightId={rec.flight_id}\n            key={`dscard-${rec.id}`}\n            image_src={rec.image_src}\n            raw_image_src={rec.raw_image_src}\n            title={rec.title}\n            url={rec.url}\n            id={rec.id}\n            shim={rec.shim}\n            pos={rec.pos}\n            type={this.props.type}\n            dispatch={this.props.dispatch}\n            context={rec.context}\n            context_type={rec.context_type}\n            source={rec.domain}\n            pocket_id={rec.pocket_id}\n            bookmarkGuid={rec.bookmarkGuid}\n            engagement={rec.engagement}\n          />\n        )\n      );\n    }\n\n    let heroCard = null;\n\n    if (!heroRec || heroRec.placeholder) {\n      heroCard = <PlaceholderDSCard />;\n    } else {\n      heroCard = (\n        <div className=\"ds-hero-item\" key={`dscard-${heroRec.id}`}>\n          <SafeAnchor\n            className=\"wrapper\"\n            dispatch={this.props.dispatch}\n            onLinkClick={this.onLinkClick}\n            url={heroRec.url}\n          >\n            <div className=\"img-wrapper\">\n              <DSImage\n                extraClassNames=\"img\"\n                source={heroRec.image_src}\n                rawSource={heroRec.raw_image_src}\n              />\n            </div>\n            <div className=\"meta\">\n              <div className=\"header-and-excerpt\">\n                <p className=\"source clamp\">{heroRec.domain}</p>\n                <header className=\"clamp\">{heroRec.title}</header>\n                <p className=\"excerpt clamp\">{heroRec.excerpt}</p>\n              </div>\n              <DSContextFooter\n                context={heroRec.context}\n                context_type={heroRec.context_type}\n                engagement={heroRec.engagement}\n              />\n            </div>\n            <ImpressionStats\n              flightId={heroRec.flight_id}\n              rows={[\n                {\n                  id: heroRec.id,\n                  pos: heroRec.pos,\n                  ...(heroRec.shim && heroRec.shim.impression\n                    ? { shim: heroRec.shim.impression }\n                    : {}),\n                },\n              ]}\n              dispatch={this.props.dispatch}\n              source={this.props.type}\n            />\n          </SafeAnchor>\n          <DSLinkMenu\n            id={heroRec.id}\n            index={heroRec.pos}\n            dispatch={this.props.dispatch}\n            url={heroRec.url}\n            title={heroRec.title}\n            source={heroRec.domain}\n            type={this.props.type}\n            pocket_id={heroRec.pocket_id}\n            shim={heroRec.shim}\n            bookmarkGuid={heroRec.bookmarkGuid}\n            flightId={heroRec.flight_id}\n          />\n        </div>\n      );\n    }\n\n    let list = (\n      <List\n        recStartingPoint={1}\n        data={this.props.data}\n        feed={this.props.feed}\n        hasImages={true}\n        hasBorders={this.props.border === `border`}\n        items={this.props.items - 1}\n        type={`Hero`}\n      />\n    );\n\n    return (\n      <div className={`ds-hero ds-hero-${this.props.border}`}>\n        {heroCard}\n        <div className={`${this.props.subComponentType}`}>\n          {this.props.subComponentType === `cards` ? cards : list}\n        </div>\n      </div>\n    );\n  }\n\n  render() {\n    const { data } = this.props;\n\n    // Handle a render before feed has been fetched by displaying nothing\n    if (!data || !data.recommendations) {\n      return <div />;\n    }\n\n    // Handle the case where a user has dismissed all recommendations\n    const isEmpty = data.recommendations.length === 0;\n\n    return (\n      <div>\n        <div className=\"ds-header\">{this.props.title}</div>\n        {isEmpty ? (\n          <div className=\"ds-hero empty\">\n            <DSEmptyState\n              status={data.status}\n              dispatch={this.props.dispatch}\n              feed={this.props.feed}\n            />\n          </div>\n        ) : (\n          this.renderHero()\n        )}\n      </div>\n    );\n  }\n}\n\nHero.defaultProps = {\n  data: {},\n  border: `border`,\n  items: 1, // Number of stories to display\n};\n"
  },
  {
    "path": "content-src/components/DiscoveryStreamComponents/Hero/_Hero.scss",
    "content": "$card-header-in-hero-font-size: 14;\n$card-header-in-hero-line-height: 20;\n\n.ds-hero {\n  position: relative;\n\n  header {\n    font-weight: 600;\n  }\n\n  p {\n    line-height: 1.538;\n    margin: 8px 0;\n  }\n\n  .excerpt {\n    @include limit-visible-lines(3, 24, 15);\n    @include dark-theme-only {\n      color: $grey-10;\n    }\n\n    color: $grey-90;\n    margin: 0 0 10px;\n  }\n\n  .ds-card:not(.placeholder) {\n    border: 0;\n    padding-bottom: 20px;\n\n    &:hover {\n      border: 0;\n      box-shadow: none;\n      border-radius: 0;\n    }\n\n    .meta {\n      padding: 0;\n    }\n\n    .img-wrapper {\n      margin: 0 0 12px;\n    }\n  }\n\n  .ds-card.placeholder {\n    margin-bottom: 20px;\n    padding-bottom: 20px;\n    min-height: 180px;\n  }\n\n  .img-wrapper {\n    margin: 0 0 12px;\n  }\n\n  .ds-hero-item {\n    position: relative;\n  }\n\n  // \"1/3 width layout\" (aka \"Mobile First\")\n  .wrapper {\n    @include ds-border-top;\n    @include dark-theme-only {\n      color: $grey-30;\n    }\n\n    color: $grey-50;\n    display: block;\n    margin: 12px 0 16px;\n    padding-top: 16px;\n    height: 100%;\n\n    &:focus {\n      @include ds-fade-in;\n    }\n\n    @at-root .ds-hero-no-border .ds-hero-item .wrapper {\n      border-top: 0;\n      border-bottom: 0;\n      padding: 0 0 8px;\n    }\n\n    &:hover .meta header {\n      @include dark-theme-only {\n        color: $blue-40;\n      }\n\n      color: $blue-60;\n    }\n\n    &:active .meta header {\n      @include dark-theme-only {\n        color: $blue-40;\n      }\n\n      color: $blue-70;\n    }\n\n    .img-wrapper {\n      width: 100%;\n    }\n\n    .img {\n      height: 0;\n      padding-top: 50%; // 2:1 aspect ratio\n\n      img {\n        border-radius: 4px;\n        box-shadow: inset 0 0 0 0.5px $black-15;\n      }\n    }\n\n    .meta {\n      display: block;\n      flex-direction: column;\n      justify-content: space-between;\n\n      .header-and-excerpt {\n        flex: 1;\n      }\n\n      header {\n        @include dark-theme-only {\n          color: $white;\n        }\n\n        @include limit-visible-lines(4, 28, 22);\n        color: $grey-90;\n        margin-bottom: 0;\n      }\n\n      .context,\n      .source {\n        margin: 0 0 4px;\n      }\n\n      .context {\n        @include dark-theme-only {\n          color: $teal-10;\n        }\n\n        color: $teal-70;\n      }\n\n      .source {\n        @include dark-theme-only {\n          color: $grey-40;\n        }\n\n        font-size: 13px;\n        color: $grey-50;\n        -webkit-line-clamp: 1;\n        margin-bottom: 0;\n      }\n    }\n  }\n\n  // \"2/3 width layout\"\n  .ds-column-5 &,\n  .ds-column-6 &,\n  .ds-column-7 &,\n  .ds-column-8 & {\n    .wrapper {\n      display: grid;\n      grid-template-columns: repeat(2, 1fr);\n      grid-column-gap: 24px;\n\n      .img-wrapper {\n        margin: 0;\n        grid-column: 2;\n        grid-row: 1;\n      }\n\n      .meta {\n        grid-column: 1;\n        grid-row: 1;\n        display: flex;\n      }\n\n      .img {\n        height: 0;\n        padding-top: 100%; // 1:1 aspect ratio\n      }\n    }\n\n    .cards {\n      display: grid;\n      grid-template-columns: repeat(2, 1fr);\n      grid-column-gap: 24px;\n      grid-auto-rows: min-content;\n    }\n  }\n\n  // \"Full width layout\"\n  .ds-column-9 &,\n  .ds-column-10 &,\n  .ds-column-11 &,\n  .ds-column-12 & {\n    display: grid;\n    grid-template-columns: repeat(2, 1fr);\n    grid-column-gap: 24px;\n\n    &.ds-hero-border {\n      @include ds-border-top;\n      padding: 20px 0;\n\n      .ds-card:not(.placeholder):nth-child(-n+2) {\n        @include ds-border-bottom;\n        margin-bottom: 20px;\n      }\n    }\n\n    .wrapper {\n      border-top: 0;\n      border-bottom: 0;\n      margin: 0;\n      padding: 0 0 20px;\n      display: flex;\n      flex-direction: column;\n\n      .img-wrapper {\n        margin: 0;\n      }\n\n      .img {\n        margin-bottom: 12px;\n        height: 0;\n        padding-top: 50%; // 2:1 aspect ratio\n      }\n\n      .meta {\n        flex-grow: 1;\n        display: flex;\n        padding: 0 24px 0 0;\n\n        header {\n          @include limit-visible-lines(3, 28, 22);\n        }\n\n        .source {\n          margin-bottom: 0;\n        }\n      }\n    }\n\n    .cards {\n      display: grid;\n      grid-template-columns: repeat(2, 1fr);\n      grid-column-gap: 24px;\n      grid-auto-rows: min-content;\n\n      .ds-card {\n        &:hover {\n          @include dark-theme-only {\n            background: none;\n\n            .title {\n              color: $blue-40;\n            }\n          }\n        }\n\n        &:active .title {\n          @include dark-theme-only {\n            color: $blue-50;\n          }\n        }\n\n        .title {\n          @include dark-theme-only {\n            color: $white;\n          }\n\n          @include limit-visible-lines(3, 20, 14);\n        }\n      }\n    }\n  }\n\n  &.empty {\n    grid-template-columns: auto;\n  }\n}\n"
  },
  {
    "path": "content-src/components/DiscoveryStreamComponents/Highlights/Highlights.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport { connect } from \"react-redux\";\nimport React from \"react\";\nimport { SectionIntl } from \"content-src/components/Sections/Sections\";\n\nexport class _Highlights extends React.PureComponent {\n  render() {\n    const section = this.props.Sections.find(s => s.id === \"highlights\");\n    if (!section || !section.enabled) {\n      return null;\n    }\n\n    return (\n      <div className=\"ds-highlights sections-list\">\n        <SectionIntl {...section} isFixed={true} />\n      </div>\n    );\n  }\n}\n\nexport const Highlights = connect(state => ({ Sections: state.Sections }))(\n  _Highlights\n);\n"
  },
  {
    "path": "content-src/components/DiscoveryStreamComponents/Highlights/_Highlights.scss",
    "content": ".ds-highlights {\n  .section {\n    margin: 0 (-$section-horizontal-padding);\n\n    .section-list {\n      grid-gap: var(--gridRowGap);\n      grid-template-columns: repeat(4, 1fr);\n\n      .card-outer {\n        $line-height: 20px;\n        height: 175px;\n\n        .card-host-name {\n          font-size: 13px;\n          line-height: $line-height;\n          margin-bottom: 2px;\n          padding-bottom: 0;\n          text-transform: unset; // sass-lint:disable-line no-disallowed-properties\n        }\n\n        .card-title {\n          font-size: 14px;\n          font-weight: 600;\n          line-height: $line-height;\n          max-height: $line-height;\n        }\n      }\n    }\n  }\n\n  .hide-for-narrow {\n    display: block;\n  }\n}\n"
  },
  {
    "path": "content-src/components/DiscoveryStreamComponents/HorizontalRule/HorizontalRule.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport React from \"react\";\n\nexport class HorizontalRule extends React.PureComponent {\n  render() {\n    return <hr className=\"ds-hr\" />;\n  }\n}\n"
  },
  {
    "path": "content-src/components/DiscoveryStreamComponents/HorizontalRule/_HorizontalRule.scss",
    "content": ".ds-hr {\n  @include ds-border-top {\n    border: 0;\n  };\n\n  height: 0;\n}\n"
  },
  {
    "path": "content-src/components/DiscoveryStreamComponents/List/List.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport { actionCreators as ac } from \"common/Actions.jsm\";\nimport { connect } from \"react-redux\";\nimport { DSEmptyState } from \"../DSEmptyState/DSEmptyState.jsx\";\nimport { DSImage } from \"../DSImage/DSImage.jsx\";\nimport { DSLinkMenu } from \"../DSLinkMenu/DSLinkMenu\";\nimport { ImpressionStats } from \"../../DiscoveryStreamImpressionStats/ImpressionStats\";\nimport React from \"react\";\nimport { SafeAnchor } from \"../SafeAnchor/SafeAnchor\";\nimport { DSContextFooter } from \"../DSContextFooter/DSContextFooter.jsx\";\n\n/**\n * @note exported for testing only\n */\nexport class ListItem extends React.PureComponent {\n  // TODO performance: get feeds to send appropriately sized images rather\n  // than waiting longer and scaling down on client?\n  constructor(props) {\n    super(props);\n    this.onLinkClick = this.onLinkClick.bind(this);\n  }\n\n  onLinkClick(event) {\n    if (this.props.dispatch) {\n      this.props.dispatch(\n        ac.UserEvent({\n          event: \"CLICK\",\n          source: this.props.type.toUpperCase(),\n          action_position: this.props.pos,\n          value: { card_type: this.props.flightId ? \"spoc\" : \"organic\" },\n        })\n      );\n\n      this.props.dispatch(\n        ac.ImpressionStats({\n          source: this.props.type.toUpperCase(),\n          click: 0,\n          tiles: [\n            {\n              id: this.props.id,\n              pos: this.props.pos,\n              ...(this.props.shim && this.props.shim.click\n                ? { shim: this.props.shim.click }\n                : {}),\n            },\n          ],\n        })\n      );\n    }\n  }\n\n  render() {\n    return (\n      <li\n        className={`ds-list-item${\n          this.props.placeholder ? \" placeholder\" : \"\"\n        }`}\n      >\n        <SafeAnchor\n          className=\"ds-list-item-link\"\n          dispatch={this.props.dispatch}\n          onLinkClick={!this.props.placeholder ? this.onLinkClick : undefined}\n          url={this.props.url}\n        >\n          <div className=\"ds-list-item-text\">\n            <p>\n              <span className=\"ds-list-item-info clamp\">\n                {this.props.domain}\n              </span>\n            </p>\n            <div className=\"ds-list-item-body\">\n              <div className=\"ds-list-item-title clamp\">{this.props.title}</div>\n              {this.props.excerpt && (\n                <div className=\"ds-list-item-excerpt clamp\">\n                  {this.props.excerpt}\n                </div>\n              )}\n            </div>\n            <DSContextFooter\n              context={this.props.context}\n              context_type={this.props.context_type}\n              engagement={this.props.engagement}\n            />\n          </div>\n          <DSImage\n            extraClassNames=\"ds-list-image\"\n            source={this.props.image_src}\n            rawSource={this.props.raw_image_src}\n          />\n          <ImpressionStats\n            flightId={this.props.flightId}\n            rows={[\n              {\n                id: this.props.id,\n                pos: this.props.pos,\n                ...(this.props.shim && this.props.shim.impression\n                  ? { shim: this.props.shim.impression }\n                  : {}),\n              },\n            ]}\n            dispatch={this.props.dispatch}\n            source={this.props.type}\n          />\n        </SafeAnchor>\n        {!this.props.placeholder && (\n          <DSLinkMenu\n            id={this.props.id}\n            index={this.props.pos}\n            dispatch={this.props.dispatch}\n            url={this.props.url}\n            title={this.props.title}\n            source={this.props.source}\n            type={this.props.type}\n            pocket_id={this.props.pocket_id}\n            shim={this.props.shim}\n            bookmarkGuid={this.props.bookmarkGuid}\n            flightId={this.props.flightId}\n          />\n        )}\n      </li>\n    );\n  }\n}\n\nexport const PlaceholderListItem = props => <ListItem placeholder={true} />;\n\n/**\n * @note exported for testing only\n */\nexport function _List(props) {\n  const renderList = () => {\n    const recs = props.data.recommendations.slice(\n      props.recStartingPoint,\n      props.recStartingPoint + props.items\n    );\n    const recMarkup = [];\n\n    for (let index = 0; index < props.items; index++) {\n      const rec = recs[index];\n      recMarkup.push(\n        !rec || rec.placeholder ? (\n          <PlaceholderListItem key={`ds-list-item-${index}`} />\n        ) : (\n          <ListItem\n            key={`ds-list-item-${rec.id}`}\n            dispatch={props.dispatch}\n            flightId={rec.flight_id}\n            domain={rec.domain}\n            excerpt={rec.excerpt}\n            id={rec.id}\n            shim={rec.shim}\n            image_src={rec.image_src}\n            raw_image_src={rec.raw_image_src}\n            pos={rec.pos}\n            title={rec.title}\n            context={rec.context}\n            context_type={rec.context_type}\n            type={props.type}\n            url={rec.url}\n            pocket_id={rec.pocket_id}\n            bookmarkGuid={rec.bookmarkGuid}\n            engagement={rec.engagement}\n          />\n        )\n      );\n    }\n\n    const listStyles = [\n      \"ds-list\",\n      props.fullWidth ? \"ds-list-full-width\" : \"\",\n      props.hasBorders ? \"ds-list-borders\" : \"\",\n      props.hasImages ? \"ds-list-images\" : \"\",\n      props.hasNumbers ? \"ds-list-numbers\" : \"\",\n    ];\n\n    return <ul className={listStyles.join(\" \")}>{recMarkup}</ul>;\n  };\n\n  const { data } = props;\n  if (!data || !data.recommendations) {\n    return null;\n  }\n\n  // Handle the case where a user has dismissed all recommendations\n  const isEmpty = data.recommendations.length === 0;\n\n  return (\n    <div>\n      {props.header && props.header.title ? (\n        <div className=\"ds-header\">{props.header.title}</div>\n      ) : null}\n      {isEmpty ? (\n        <div className=\"ds-list empty\">\n          <DSEmptyState\n            status={data.status}\n            dispatch={props.dispatch}\n            feed={props.feed}\n          />\n        </div>\n      ) : (\n        renderList()\n      )}\n    </div>\n  );\n}\n\n_List.defaultProps = {\n  recStartingPoint: 0, // Index of recommendations to start displaying from\n  fullWidth: false, // Display items taking up the whole column\n  hasBorders: false, // Display lines separating each item\n  hasImages: false, // Display images for each item\n  hasNumbers: false, // Display numbers for each item\n  items: 6, // Number of stories to display.  TODO: get from endpoint\n};\n\nexport const List = connect(state => ({\n  DiscoveryStream: state.DiscoveryStream,\n}))(_List);\n"
  },
  {
    "path": "content-src/components/DiscoveryStreamComponents/List/_List.scss",
    "content": "// Type sizes\n$bordered-spacing: 16px;\n$item-font-size: 14;\n$item-image-size: 80px;\n$item-line-height: 20;\n\n// XXX this is gross, and attaches the bottom-border to the item above.\n// Ideally, we'd attach the top-border to the item that needs it.\n// Unfortunately the border needs to go _above_ the row gap as currently\n// set up, which means that some refactoring will be required to do this.\n@mixin bottom-border-except-last-grid-row($columns) {\n  .ds-list-item:not(.placeholder):not(:nth-last-child(-n+#{$columns})) {\n    @include ds-border-bottom;\n    margin-bottom: -1px;  // cancel out the pixel we used for the border\n    padding-bottom: $bordered-spacing;\n  }\n}\n\n@mixin set-item-sizes($font-size, $line-height, $image-size) {\n  .ds-list-item {\n    // XXX see if we really want absolute units, maybe hoist somewhere central?\n    font-size: $font-size * 1px;\n    line-height: $line-height * 1px;\n    position: relative;\n  }\n\n  .ds-list-item-title {\n    @include limit-visible-lines(3, $line-height, $font-size);\n  }\n\n  .ds-list-image {\n    min-width: $image-size;\n    width: $image-size;\n  }\n}\n\n.ds-list {\n  display: grid;\n  grid-row-gap: 24px;\n  grid-column-gap: 24px;\n\n  // reset some stuff from <ul>.  Should maybe be hoisted when we have better\n  // regression detection?\n  padding-inline-start: 0;\n\n  &:not(.ds-list-full-width) {\n    @include set-item-sizes($item-font-size, $item-line-height, $item-image-size);\n\n    // \"2/3 width layout\"\n    .ds-column-5 &,\n    .ds-column-6 &,\n    .ds-column-7 &,\n    .ds-column-8 & {\n      grid-template-columns: repeat(2, 1fr);\n    }\n\n    // \"Full width layout\"\n    .ds-column-9 &,\n    .ds-column-10 &,\n    .ds-column-11 &,\n    .ds-column-12 & {\n      grid-template-columns: repeat(3, 1fr);\n    }\n\n    &.empty {\n      grid-template-columns: auto;\n    }\n\n    .ds-list-item-excerpt {\n      display: none;\n    }\n  }\n\n  &:not(.ds-list-images) {\n    .ds-list-image {\n      display: none;\n    }\n  }\n\n  a {\n    @include dark-theme-only {\n      color: $grey-10;\n    }\n\n    color: $grey-90;\n  }\n}\n\n.ds-list-item-link:focus {\n  @include ds-fade-in;\n}\n\n.ds-list-numbers {\n  $counter-whitespace: ($item-line-height - $item-font-size) * 1px;\n  $counter-size: 32px;\n  $counter-padded-size: $counter-size + $counter-whitespace * 1.5;\n\n  .ds-list-item {\n    counter-increment: list;\n  }\n\n  .ds-list-item:not(.placeholder) > .ds-list-item-link {\n    padding-inline-start: $counter-padded-size;\n\n    &::before {\n      @include dark-theme-only {\n        background-color: $teal-70;\n      }\n\n      background-color: $pocket-teal;\n      border-radius: $counter-size;\n      color: $white;\n      content: counter(list);\n      font-size: 17px;\n      height: $counter-size;\n      line-height: $counter-size;\n      margin-inline-start: -$counter-padded-size;\n      margin-top: $counter-whitespace / 2;\n      position: absolute;\n      text-align: center;\n      width: $counter-size;\n    }\n\n    &:hover::before {\n      @include dark-theme-only {\n        background-color: $blue-40;\n      }\n\n      background-color: $blue-40;\n    }\n\n    &:active::before {\n      @include dark-theme-only {\n        background-color: $blue-60;\n      }\n\n      background-color: $blue-70;\n    }\n  }\n}\n\n.ds-list-borders {\n  @include ds-border-top;\n  grid-row-gap: $bordered-spacing;\n  padding-top: $bordered-spacing;\n\n  &.ds-list-full-width,\n  .ds-column-1 &,\n  .ds-column-2 &,\n  .ds-column-3 &,\n  .ds-column-4 & {\n    @include bottom-border-except-last-grid-row(1);\n  }\n\n  &:not(.ds-list-full-width) {\n    // \"2/3 width layout\"\n    .ds-column-5 &,\n    .ds-column-6 &,\n    .ds-column-7 &,\n    .ds-column-8 & {\n      @include bottom-border-except-last-grid-row(2);\n    }\n\n    // \"Full width layout\"\n    .ds-column-9 &,\n    .ds-column-10 &,\n    .ds-column-11 &,\n    .ds-column-12 & {\n      @include bottom-border-except-last-grid-row(3);\n    }\n  }\n}\n\n.ds-list-full-width {\n  @include set-item-sizes(17, 24, $item-image-size * 2);\n}\n\n.ds-list-item {\n  // reset some stuff from <li>.  Should maybe be hoisted when we have better\n  // regression detection?\n  display: block;\n  text-align: start;\n\n  &.placeholder {\n    background: transparent;\n    min-height: $item-image-size;\n    box-shadow: inset $inner-box-shadow;\n    border-radius: 4px;\n\n    .ds-list-item-link {\n      cursor: default;\n    }\n\n    .ds-list-image {\n      opacity: 0;\n    }\n  }\n\n  .ds-list-item-link {\n    mix-blend-mode: normal;\n\n    display: flex;\n    justify-content: space-between;\n    height: 100%;\n  }\n\n  .ds-list-item-excerpt {\n    @include limit-visible-lines(2, $item-line-height, $item-font-size);\n    @include dark-theme-only {\n      color: $grey-10-80;\n    }\n    color: $grey-50;\n    margin: 4px 0 8px;\n  }\n\n  p {\n    font-size: $item-font-size * 1px;\n    line-height: $item-line-height * 1px;\n    margin: 0;\n  }\n\n  .ds-list-item-info {\n    @include limit-visible-lines(1, $item-line-height, $item-font-size);\n    @include dark-theme-only {\n      color: $grey-40;\n    }\n\n    color: $grey-50;\n    font-size: 13px;\n  }\n\n  .ds-list-item-title {\n    font-weight: 600;\n    margin-bottom: 4px;\n  }\n\n  .ds-list-item-body {\n    flex: 1;\n  }\n\n  .ds-list-item-text {\n    display: flex;\n    flex-direction: column;\n    justify-content: space-between;\n  }\n\n  .ds-list-image {\n    height: $item-image-size;\n    margin-inline-start: $item-font-size * 1px;\n    min-height: $item-image-size;\n\n    img {\n      border-radius: 4px;\n      box-shadow: inset 0 0 0 0.5px $black-15;\n    }\n  }\n\n  &:hover {\n    .ds-list-item-title {\n      color: $blue-40;\n    }\n  }\n\n  &:active {\n    .ds-list-item-title {\n      color: $blue-70;\n    }\n  }\n}\n"
  },
  {
    "path": "content-src/components/DiscoveryStreamComponents/Navigation/Navigation.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport React from \"react\";\nimport { SafeAnchor } from \"../SafeAnchor/SafeAnchor\";\nimport { FluentOrText } from \"content-src/components/FluentOrText/FluentOrText\";\n\nexport class Topic extends React.PureComponent {\n  render() {\n    const { url, name } = this.props;\n    return (\n      <li>\n        <SafeAnchor key={name} url={url}>\n          {name}\n        </SafeAnchor>\n      </li>\n    );\n  }\n}\n\nexport class Navigation extends React.PureComponent {\n  render() {\n    const { links } = this.props || [];\n    const { alignment } = this.props || \"centered\";\n    const header = this.props.header || {};\n    return (\n      <div className={`ds-navigation ds-navigation-${alignment}`}>\n        {header.title ? (\n          <FluentOrText message={header.title}>\n            <div className=\"ds-header\" />\n          </FluentOrText>\n        ) : null}\n        <div>\n          <ul>\n            {links &&\n              links.map(t => <Topic key={t.name} url={t.url} name={t.name} />)}\n          </ul>\n        </div>\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "content-src/components/DiscoveryStreamComponents/Navigation/_Navigation.scss",
    "content": ".ds-navigation {\n  line-height: 32px;\n  padding: 4px 0;\n  font-size: 14px;\n  font-weight: 600;\n\n  &.ds-navigation-centered {\n    text-align: center;\n  }\n\n  &.ds-navigation-right-aligned {\n    text-align: end;\n  }\n\n  ul {\n    margin: 0;\n    padding: 0;\n  }\n\n  ul li {\n    display: inline-block;\n\n    &::after {\n      content: '·';\n      padding: 8px;\n      color: $grey-50;\n    }\n\n    &:last-child::after {\n      content: none;\n    }\n\n    a {\n      &:hover {\n        // text-decoration: underline; didn't quite match comps.\n        border-bottom: 1px solid var(--newtab-link-primary-color);\n\n        &:active {\n          border-bottom: 1px solid $blue-70;\n        }\n      }\n\n      &:active {\n        color: $blue-70;\n      }\n    }\n  }\n\n  .ds-header {\n    margin-bottom: 8px;\n  }\n}\n"
  },
  {
    "path": "content-src/components/DiscoveryStreamComponents/SafeAnchor/SafeAnchor.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport { actionCreators as ac, actionTypes as at } from \"common/Actions.jsm\";\nimport React from \"react\";\n\nexport class SafeAnchor extends React.PureComponent {\n  constructor(props) {\n    super(props);\n    this.onClick = this.onClick.bind(this);\n  }\n\n  onClick(event) {\n    // Use dispatch instead of normal link click behavior to include referrer\n    if (this.props.dispatch) {\n      event.preventDefault();\n      const { altKey, button, ctrlKey, metaKey, shiftKey } = event;\n      this.props.dispatch(\n        ac.OnlyToMain({\n          type: at.OPEN_LINK,\n          data: {\n            event: { altKey, button, ctrlKey, metaKey, shiftKey },\n            referrer: \"https://getpocket.com/recommendations\",\n            // Use the anchor's url, which could have been cleaned up\n            url: event.currentTarget.href,\n          },\n        })\n      );\n    }\n\n    // Propagate event if there's a handler\n    if (this.props.onLinkClick) {\n      this.props.onLinkClick(event);\n    }\n  }\n\n  safeURI(url) {\n    let protocol = null;\n    try {\n      protocol = new URL(url).protocol;\n    } catch (e) {\n      return \"\";\n    }\n\n    const isAllowed = [\"http:\", \"https:\"].includes(protocol);\n    if (!isAllowed) {\n      console.warn(`${url} is not allowed for anchor targets.`); // eslint-disable-line no-console\n      return \"\";\n    }\n    return url;\n  }\n\n  render() {\n    const { url, className } = this.props;\n    return (\n      <a href={this.safeURI(url)} className={className} onClick={this.onClick}>\n        {this.props.children}\n      </a>\n    );\n  }\n}\n"
  },
  {
    "path": "content-src/components/DiscoveryStreamComponents/SectionTitle/SectionTitle.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport React from \"react\";\n\nexport class SectionTitle extends React.PureComponent {\n  render() {\n    const {\n      header: { title, subtitle },\n    } = this.props;\n    return (\n      <div className=\"ds-section-title\">\n        <div className=\"title\">{title}</div>\n        {subtitle ? <div className=\"subtitle\">{subtitle}</div> : null}\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "content-src/components/DiscoveryStreamComponents/SectionTitle/_SectionTitle.scss",
    "content": ".ds-section-title {\n  text-align: center;\n  margin-top: 24px;\n\n  .title {\n    @include dark-theme-only {\n      color: $white;\n    }\n\n    line-height: 48px;\n    font-size: 36px;\n    font-weight: 300;\n    color: $grey-90;\n  }\n\n  .subtitle {\n    @include dark-theme-only {\n      color: $grey-30;\n    }\n\n    line-height: 24px;\n    font-size: 14px;\n    color: $grey-50;\n    margin-top: 4px;\n  }\n}\n"
  },
  {
    "path": "content-src/components/DiscoveryStreamComponents/TopSites/TopSites.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport { connect } from \"react-redux\";\nimport { TopSites as OldTopSites } from \"content-src/components/TopSites/TopSites\";\nimport { TOP_SITES_MAX_SITES_PER_ROW } from \"common/Reducers.jsm\";\nimport React from \"react\";\n\nexport class _TopSites extends React.PureComponent {\n  // Find a SPOC that doesn't already exist in User's TopSites\n  getFirstAvailableSpoc(topSites, data) {\n    const { spocs } = data;\n    if (!spocs || spocs.length === 0) {\n      return null;\n    }\n\n    const userTopSites = new Set(\n      topSites.map(topSite => topSite && topSite.url)\n    );\n\n    // We \"clean urls\" with http in TopSiteForm.jsx\n    // Spoc domains are in the format 'sponsorname.com'\n    return spocs.find(\n      spoc =>\n        !userTopSites.has(spoc.url) &&\n        !userTopSites.has(`http://${spoc.domain}`) &&\n        !userTopSites.has(`https://${spoc.domain}`) &&\n        !userTopSites.has(`http://www.${spoc.domain}`) &&\n        !userTopSites.has(`https://www.${spoc.domain}`)\n    );\n  }\n\n  // Find the first empty or unpinned index we can place the SPOC in.\n  // Return -1 if no available index and we should push it at the end.\n  getFirstAvailableIndex(topSites, promoAlignment) {\n    if (promoAlignment === \"left\") {\n      return topSites.findIndex(topSite => !topSite || !topSite.isPinned);\n    }\n\n    // The row isn't full so we can push it to the end of the row.\n    if (topSites.length < TOP_SITES_MAX_SITES_PER_ROW) {\n      return -1;\n    }\n\n    // If the row is full, we can check the row first for unpinned topsites to replace.\n    // Else we can check after the row. This behavior is how unpinned topsites move while drag and drop.\n    let endOfRow = TOP_SITES_MAX_SITES_PER_ROW - 1;\n    for (let i = endOfRow; i >= 0; i--) {\n      if (!topSites[i] || !topSites[i].isPinned) {\n        return i;\n      }\n    }\n\n    for (let i = endOfRow + 1; i < topSites.length; i++) {\n      if (!topSites[i] || !topSites[i].isPinned) {\n        return i;\n      }\n    }\n\n    return -1;\n  }\n\n  insertSpocContent(TopSites, data, promoAlignment) {\n    if (\n      !TopSites.rows ||\n      TopSites.rows.length === 0 ||\n      !data.spocs ||\n      data.spocs.length === 0\n    ) {\n      return null;\n    }\n\n    let topSites = [...TopSites.rows];\n    const topSiteSpoc = this.getFirstAvailableSpoc(topSites, data);\n\n    if (!topSiteSpoc) {\n      return null;\n    }\n\n    const link = {\n      customScreenshotURL: topSiteSpoc.image_src,\n      type: \"SPOC\",\n      label: topSiteSpoc.sponsor,\n      title: topSiteSpoc.sponsor,\n      url: topSiteSpoc.url,\n      flightId: topSiteSpoc.flight_id,\n      id: topSiteSpoc.id,\n      guid: topSiteSpoc.id,\n      shim: topSiteSpoc.shim,\n      // For now we are assuming position based on intended position.\n      // Actual position can shift based on other content.\n      // We also hard code left and right to be 0 and 7.\n      // We send the intended postion in the ping.\n      pos: promoAlignment === \"left\" ? 0 : 7,\n    };\n\n    const firstAvailableIndex = this.getFirstAvailableIndex(\n      topSites,\n      promoAlignment\n    );\n\n    if (firstAvailableIndex === -1) {\n      topSites.push(link);\n    } else {\n      // Normal insertion will not work since pinned topsites are in their correct index already\n      // Similar logic is done to handle drag and drop with pinned topsites in TopSite.jsx\n\n      let shiftedTopSite = topSites[firstAvailableIndex];\n      let index = firstAvailableIndex + 1;\n\n      // Shift unpinned topsites to the right by finding the next unpinned topsite to replace\n      while (shiftedTopSite) {\n        if (index === topSites.length) {\n          topSites.push(shiftedTopSite);\n          shiftedTopSite = null;\n        } else if (topSites[index] && topSites[index].isPinned) {\n          index += 1;\n        } else {\n          const nextTopSite = topSites[index];\n          topSites[index] = shiftedTopSite;\n          shiftedTopSite = nextTopSite;\n          index += 1;\n        }\n      }\n\n      topSites[firstAvailableIndex] = link;\n    }\n\n    return { ...TopSites, rows: topSites };\n  }\n\n  render() {\n    const { header = {}, data, promoAlignment, TopSites } = this.props;\n\n    const TopSitesWithSpoc =\n      TopSites && data && promoAlignment\n        ? this.insertSpocContent(TopSites, data, promoAlignment)\n        : null;\n\n    return (\n      <div\n        className={`ds-top-sites ${TopSitesWithSpoc ? \"top-sites-spoc\" : \"\"}`}\n      >\n        <OldTopSites\n          isFixed={true}\n          title={header.title}\n          TopSitesWithSpoc={TopSitesWithSpoc}\n        />\n      </div>\n    );\n  }\n}\n\nexport const TopSites = connect(state => ({ TopSites: state.TopSites }))(\n  _TopSites\n);\n"
  },
  {
    "path": "content-src/components/DiscoveryStreamComponents/TopSites/_TopSites.scss",
    "content": "$top-sites-vertical-space-with-spoc: 20px;\n\n// ds topsites wraps the original topsites, with a few css changes.\n.ds-top-sites {\n\n  // This is the override layer.\n  .top-sites {\n    // Slightly different alignment with the other DS components than AS has.\n    margin: 0 (-$section-horizontal-padding);\n\n    .top-site-outer {\n      padding: 0 12px;\n\n      .top-site-inner > a:-moz-any(.active, :focus) .tile {\n        @include ds-fade-in;\n\n        @include dark-theme-only {\n          @include ds-fade-in($blue-40-40);\n        }\n      }\n\n      .top-site-inner > a:-moz-any(:hover) .tile {\n        @include ds-fade-in($grey-30);\n\n        @include dark-theme-only {\n          @include ds-fade-in($grey-60);\n        }\n      }\n    }\n\n    .top-sites-list {\n      margin: 0 -12px;\n    }\n  }\n\n  // Only show 6 cards for 2/3 and 1/3\n  // XXX hide-for-narrow is wrapping a previous functionality, can do better.\n  .hide-for-narrow {\n    display: none;\n  }\n}\n\n// Only show 8 cards for the full row.\n// XXX hide-for-narrow is wrapping a previous functionality, can do better.\n.ds-column-9,\n.ds-column-10,\n.ds-column-11,\n.ds-column-12 {\n  .ds-top-sites {\n    .hide-for-narrow {\n      display: inline-block;\n    }\n  }\n}\n\n// Size overrides for topsites in the 2/3 view.\n.ds-column-5,\n.ds-column-6,\n.ds-column-7,\n.ds-column-8 {\n  .ds-top-sites {\n\n    .top-site-outer {\n      padding: 0 10px;\n    }\n\n    .top-sites-list {\n      margin: 0 -10px;\n    }\n\n    .top-site-inner {\n      --leftPanelIconWidth: 84.67px;\n\n      .tile {\n        width: var(--leftPanelIconWidth);\n        height: var(--leftPanelIconWidth);\n      }\n\n      .title {\n        width: var(--leftPanelIconWidth);\n      }\n    }\n  }\n}\n\n// Size overrides for topsites in the 1/3 view.\n.ds-column-1,\n.ds-column-2,\n.ds-column-3,\n.ds-column-4 {\n  .ds-top-sites {\n\n    .top-site-outer {\n      padding: 0 8px;\n    }\n\n    .top-sites-list {\n      margin: 0 -8px;\n    }\n\n    .top-site-inner {\n      --rightPanelIconWidth: 82.67px;\n\n      .tile {\n        width: var(--rightPanelIconWidth);\n        height: var(--rightPanelIconWidth);\n      }\n\n      .title {\n        width: var(--rightPanelIconWidth);\n      }\n    }\n  }\n}\n\n.top-sites-spoc {\n  .top-sites-list {\n    display: flex;\n    flex-wrap: wrap;\n\n    .top-site-outer {\n      margin: 0 0 $top-sites-vertical-space-with-spoc;\n\n      .top-site-spoc-label {\n        @include dark-theme-only {\n          color: $grey-40;\n        }\n\n        color: $grey-50;\n        font-size: 11px;\n        display: flex;\n        justify-content: center;\n        margin-top: -4px;\n      }\n\n      &.dragged {\n\n        .top-site-spoc-label {\n          visibility: hidden;\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "content-src/components/DiscoveryStreamImpressionStats/ImpressionStats.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport { actionCreators as ac, actionTypes as at } from \"common/Actions.jsm\";\nimport React from \"react\";\n\nconst VISIBLE = \"visible\";\nconst VISIBILITY_CHANGE_EVENT = \"visibilitychange\";\n\n// Per analytical requirement, we set the minimal intersection ratio to\n// 0.5, and an impression is identified when the wrapped item has at least\n// 50% visibility.\n//\n// This constant is exported for unit test\nexport const INTERSECTION_RATIO = 0.5;\n\n/**\n * Impression wrapper for Discovery Stream related React components.\n *\n * It makses use of the Intersection Observer API to detect the visibility,\n * and relies on page visibility to ensure the impression is reported\n * only when the component is visible on the page.\n *\n * Note:\n *   * This wrapper used to be used either at the individual card level,\n *     or by the card container components.\n *     It is now only used for individual card level.\n *   * Each impression will be sent only once as soon as the desired\n *     visibility is detected\n *   * Batching is not yet implemented, hence it might send multiple\n *     impression pings separately\n */\nexport class ImpressionStats extends React.PureComponent {\n  // This checks if the given cards are the same as those in the last impression ping.\n  // If so, it should not send the same impression ping again.\n  _needsImpressionStats(cards) {\n    if (\n      !this.impressionCardGuids ||\n      this.impressionCardGuids.length !== cards.length\n    ) {\n      return true;\n    }\n\n    for (let i = 0; i < cards.length; i++) {\n      if (cards[i].id !== this.impressionCardGuids[i]) {\n        return true;\n      }\n    }\n\n    return false;\n  }\n\n  _dispatchImpressionStats() {\n    const { props } = this;\n    const cards = props.rows;\n\n    if (this.props.flightId) {\n      this.props.dispatch(\n        ac.OnlyToMain({\n          type: at.DISCOVERY_STREAM_SPOC_IMPRESSION,\n          data: { flightId: this.props.flightId },\n        })\n      );\n    }\n\n    if (this._needsImpressionStats(cards)) {\n      props.dispatch(\n        ac.DiscoveryStreamImpressionStats({\n          source: props.source.toUpperCase(),\n          tiles: cards.map(link => ({\n            id: link.id,\n            pos: link.pos,\n            ...(link.shim ? { shim: link.shim } : {}),\n          })),\n        })\n      );\n      this.impressionCardGuids = cards.map(link => link.id);\n    }\n  }\n\n  // This checks if the given cards are the same as those in the last loaded content ping.\n  // If so, it should not send the same loaded content ping again.\n  _needsLoadedContent(cards) {\n    if (\n      !this.loadedContentGuids ||\n      this.loadedContentGuids.length !== cards.length\n    ) {\n      return true;\n    }\n\n    for (let i = 0; i < cards.length; i++) {\n      if (cards[i].id !== this.loadedContentGuids[i]) {\n        return true;\n      }\n    }\n\n    return false;\n  }\n\n  _dispatchLoadedContent() {\n    const { props } = this;\n    const cards = props.rows;\n\n    if (this._needsLoadedContent(cards)) {\n      props.dispatch(\n        ac.DiscoveryStreamLoadedContent({\n          source: props.source.toUpperCase(),\n          tiles: cards.map(link => ({ id: link.id, pos: link.pos })),\n        })\n      );\n      this.loadedContentGuids = cards.map(link => link.id);\n    }\n  }\n\n  setImpressionObserverOrAddListener() {\n    const { props } = this;\n\n    if (!props.dispatch) {\n      return;\n    }\n\n    if (props.document.visibilityState === VISIBLE) {\n      // Send the loaded content ping once the page is visible.\n      this._dispatchLoadedContent();\n      this.setImpressionObserver();\n    } else {\n      // We should only ever send the latest impression stats ping, so remove any\n      // older listeners.\n      if (this._onVisibilityChange) {\n        props.document.removeEventListener(\n          VISIBILITY_CHANGE_EVENT,\n          this._onVisibilityChange\n        );\n      }\n\n      this._onVisibilityChange = () => {\n        if (props.document.visibilityState === VISIBLE) {\n          // Send the loaded content ping once the page is visible.\n          this._dispatchLoadedContent();\n          this.setImpressionObserver();\n          props.document.removeEventListener(\n            VISIBILITY_CHANGE_EVENT,\n            this._onVisibilityChange\n          );\n        }\n      };\n      props.document.addEventListener(\n        VISIBILITY_CHANGE_EVENT,\n        this._onVisibilityChange\n      );\n    }\n  }\n\n  /**\n   * Set an impression observer for the wrapped component. It makes use of\n   * the Intersection Observer API to detect if the wrapped component is\n   * visible with a desired ratio, and only sends impression if that's the case.\n   *\n   * See more details about Intersection Observer API at:\n   * https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API\n   */\n  setImpressionObserver() {\n    const { props } = this;\n\n    if (!props.rows.length) {\n      return;\n    }\n\n    this._handleIntersect = entries => {\n      if (\n        entries.some(\n          entry =>\n            entry.isIntersecting &&\n            entry.intersectionRatio >= INTERSECTION_RATIO\n        )\n      ) {\n        this._dispatchImpressionStats();\n        this.impressionObserver.unobserve(this.refs.impression);\n      }\n    };\n\n    const options = { threshold: INTERSECTION_RATIO };\n    this.impressionObserver = new props.IntersectionObserver(\n      this._handleIntersect,\n      options\n    );\n    this.impressionObserver.observe(this.refs.impression);\n  }\n\n  componentDidMount() {\n    if (this.props.rows.length) {\n      this.setImpressionObserverOrAddListener();\n    }\n  }\n\n  componentWillUnmount() {\n    if (this._handleIntersect && this.impressionObserver) {\n      this.impressionObserver.unobserve(this.refs.impression);\n    }\n    if (this._onVisibilityChange) {\n      this.props.document.removeEventListener(\n        VISIBILITY_CHANGE_EVENT,\n        this._onVisibilityChange\n      );\n    }\n  }\n\n  render() {\n    return (\n      <div ref={\"impression\"} className=\"impression-observer\">\n        {this.props.children}\n      </div>\n    );\n  }\n}\n\nImpressionStats.defaultProps = {\n  IntersectionObserver: global.IntersectionObserver,\n  document: global.document,\n  rows: [],\n  source: \"\",\n};\n"
  },
  {
    "path": "content-src/components/DiscoveryStreamImpressionStats/_ImpressionStats.scss",
    "content": ".impression-observer {\n  position: absolute;\n  top: 0;\n  width: 100%;\n  height: 100%;\n  pointer-events: none;\n}\n"
  },
  {
    "path": "content-src/components/ErrorBoundary/ErrorBoundary.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport { A11yLinkButton } from \"content-src/components/A11yLinkButton/A11yLinkButton\";\nimport React from \"react\";\n\nexport class ErrorBoundaryFallback extends React.PureComponent {\n  constructor(props) {\n    super(props);\n    this.windowObj = this.props.windowObj || window;\n    this.onClick = this.onClick.bind(this);\n  }\n\n  /**\n   * Since we only get here if part of the page has crashed, do a\n   * forced reload to give us the best chance at recovering.\n   */\n  onClick() {\n    this.windowObj.location.reload(true);\n  }\n\n  render() {\n    const defaultClass = \"as-error-fallback\";\n    let className;\n    if (\"className\" in this.props) {\n      className = `${this.props.className} ${defaultClass}`;\n    } else {\n      className = defaultClass;\n    }\n\n    // \"A11yLinkButton\" to force normal link styling stuff (eg cursor on hover)\n    return (\n      <div className={className}>\n        <div data-l10n-id=\"newtab-error-fallback-info\" />\n        <span>\n          <A11yLinkButton\n            className=\"reload-button\"\n            onClick={this.onClick}\n            data-l10n-id=\"newtab-error-fallback-refresh-link\"\n          />\n        </span>\n      </div>\n    );\n  }\n}\nErrorBoundaryFallback.defaultProps = { className: \"as-error-fallback\" };\n\nexport class ErrorBoundary extends React.PureComponent {\n  constructor(props) {\n    super(props);\n    this.state = { hasError: false };\n  }\n\n  componentDidCatch(error, info) {\n    this.setState({ hasError: true });\n  }\n\n  render() {\n    if (!this.state.hasError) {\n      return this.props.children;\n    }\n\n    return <this.props.FallbackComponent className={this.props.className} />;\n  }\n}\n\nErrorBoundary.defaultProps = { FallbackComponent: ErrorBoundaryFallback };\n"
  },
  {
    "path": "content-src/components/ErrorBoundary/_ErrorBoundary.scss",
    "content": ".as-error-fallback {\n  align-items: center;\n  border-radius: $border-radius;\n  box-shadow: inset $inner-box-shadow;\n  color: var(--newtab-text-conditional-color);\n  display: flex;\n  flex-direction: column;\n  font-size: $error-fallback-font-size;\n  justify-content: center;\n  justify-items: center;\n  line-height: $error-fallback-line-height;\n\n  &.borderless-error {\n    box-shadow: none;\n  }\n\n  a {\n    color: var(--newtab-text-conditional-color);\n    text-decoration: underline;\n  }\n}\n"
  },
  {
    "path": "content-src/components/FluentOrText/FluentOrText.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport React from \"react\";\n\n/**\n * Set text on a child element/component depending on if the message is already\n * translated plain text or a fluent id with optional args.\n */\nexport class FluentOrText extends React.PureComponent {\n  render() {\n    // Ensure we have a single child to attach attributes\n    const { children, message } = this.props;\n    const child = children ? React.Children.only(children) : <span />;\n\n    // For a string message, just use it as the child's text\n    let grandChildren = message;\n    let extraProps;\n\n    // Convert a message object to set desired fluent-dom attributes\n    if (typeof message === \"object\") {\n      const args = message.args || message.values;\n      extraProps = {\n        \"data-l10n-args\": args && JSON.stringify(args),\n        \"data-l10n-id\": message.id || message.string_id,\n      };\n\n      // Use original children potentially with data-l10n-name attributes\n      grandChildren = child.props.children;\n    }\n\n    // Add the message to the child via fluent attributes or text node\n    return React.cloneElement(child, extraProps, grandChildren);\n  }\n}\n"
  },
  {
    "path": "content-src/components/LinkMenu/LinkMenu.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport { actionCreators as ac } from \"common/Actions.jsm\";\nimport { connect } from \"react-redux\";\nimport { ContextMenu } from \"content-src/components/ContextMenu/ContextMenu\";\nimport { LinkMenuOptions } from \"content-src/lib/link-menu-options\";\nimport React from \"react\";\n\nconst DEFAULT_SITE_MENU_OPTIONS = [\n  \"CheckPinTopSite\",\n  \"EditTopSite\",\n  \"Separator\",\n  \"OpenInNewWindow\",\n  \"OpenInPrivateWindow\",\n  \"Separator\",\n  \"BlockUrl\",\n];\n\nexport class _LinkMenu extends React.PureComponent {\n  getOptions() {\n    const { props } = this;\n    const {\n      site,\n      index,\n      source,\n      isPrivateBrowsingEnabled,\n      siteInfo,\n      platform,\n    } = props;\n\n    // Handle special case of default site\n    const propOptions =\n      !site.isDefault || site.searchTopSite\n        ? props.options\n        : DEFAULT_SITE_MENU_OPTIONS;\n\n    const options = propOptions\n      .map(o =>\n        LinkMenuOptions[o](\n          site,\n          index,\n          source,\n          isPrivateBrowsingEnabled,\n          siteInfo,\n          platform\n        )\n      )\n      .map(option => {\n        const { action, impression, id, type, userEvent } = option;\n        if (!type && id) {\n          option.onClick = () => {\n            props.dispatch(action);\n            if (userEvent) {\n              const userEventData = Object.assign(\n                {\n                  event: userEvent,\n                  source,\n                  action_position: index,\n                },\n                siteInfo\n              );\n              props.dispatch(ac.UserEvent(userEventData));\n            }\n            if (impression && props.shouldSendImpressionStats) {\n              props.dispatch(impression);\n            }\n          };\n        }\n        return option;\n      });\n\n    // This is for accessibility to support making each item tabbable.\n    // We want to know which item is the first and which item\n    // is the last, so we can close the context menu accordingly.\n    options[0].first = true;\n    options[options.length - 1].last = true;\n    return options;\n  }\n\n  render() {\n    return (\n      <ContextMenu\n        onUpdate={this.props.onUpdate}\n        onShow={this.props.onShow}\n        options={this.getOptions()}\n        keyboardAccess={this.props.keyboardAccess}\n      />\n    );\n  }\n}\n\nconst getState = state => ({\n  isPrivateBrowsingEnabled: state.Prefs.values.isPrivateBrowsingEnabled,\n  platform: state.Prefs.values.platform,\n});\nexport const LinkMenu = connect(getState)(_LinkMenu);\n"
  },
  {
    "path": "content-src/components/MoreRecommendations/MoreRecommendations.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport React from \"react\";\n\nexport class MoreRecommendations extends React.PureComponent {\n  render() {\n    const { read_more_endpoint } = this.props;\n    if (read_more_endpoint) {\n      return (\n        <a\n          className=\"more-recommendations\"\n          href={read_more_endpoint}\n          data-l10n-id=\"newtab-pocket-more-recommendations\"\n        />\n      );\n    }\n    return null;\n  }\n}\n"
  },
  {
    "path": "content-src/components/MoreRecommendations/_MoreRecommendations.scss",
    "content": ".more-recommendations {\n  display: flex;\n  align-items: center;\n  white-space: nowrap;\n  line-height: 1.230769231; // (16 / 13) -> 16px computed\n\n  &::after {\n    background: url('#{$image-path}topic-show-more-12.svg') no-repeat center center;\n    content: '';\n    -moz-context-properties: fill;\n    display: inline-block;\n    fill: var(--newtab-link-secondary-color);\n    height: 16px;\n    margin-inline-start: 5px;\n    vertical-align: top;\n    width: 12px;\n  }\n\n  &:dir(rtl)::after  {\n    transform: scaleX(-1);\n  }\n}\n"
  },
  {
    "path": "content-src/components/PocketLoggedInCta/PocketLoggedInCta.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport { connect } from \"react-redux\";\nimport React from \"react\";\n\nexport class _PocketLoggedInCta extends React.PureComponent {\n  render() {\n    const { pocketCta } = this.props.Pocket;\n    return (\n      <span className=\"pocket-logged-in-cta\">\n        <a\n          className=\"pocket-cta-button\"\n          href={pocketCta.ctaUrl ? pocketCta.ctaUrl : \"https://getpocket.com/\"}\n        >\n          {pocketCta.ctaButton ? (\n            pocketCta.ctaButton\n          ) : (\n            <span data-l10n-id=\"newtab-pocket-cta-button\" />\n          )}\n        </a>\n\n        <a\n          href={pocketCta.ctaUrl ? pocketCta.ctaUrl : \"https://getpocket.com/\"}\n        >\n          <span className=\"cta-text\">\n            {pocketCta.ctaText ? (\n              pocketCta.ctaText\n            ) : (\n              <span data-l10n-id=\"newtab-pocket-cta-text\" />\n            )}\n          </span>\n        </a>\n      </span>\n    );\n  }\n}\n\nexport const PocketLoggedInCta = connect(state => ({ Pocket: state.Pocket }))(\n  _PocketLoggedInCta\n);\n"
  },
  {
    "path": "content-src/components/PocketLoggedInCta/_PocketLoggedInCta.scss",
    "content": ".pocket-logged-in-cta {\n  $max-button-width: 130px;\n  $min-button-height: 18px;\n  font-size: 13px;\n  margin-inline-end: 20px;\n  display: flex;\n  align-items: flex-start;\n\n  .pocket-cta-button {\n    white-space: nowrap;\n    background: $blue-60;\n    letter-spacing: -0.34px;\n    color: $white;\n    border-radius: 4px;\n    cursor: pointer;\n    max-width: $max-button-width;\n    // The button height is 2px taller than the rest of the cta text.\n    // So I move it up by 1px to align with the rest of the cta text.\n    margin-top: -1px;\n    min-height: $min-button-height;\n    padding: 0 8px;\n    display: inline-flex;\n    justify-content: center;\n    align-items: center;\n    font-size: 11px;\n    margin-inline-end: 10px;\n  }\n\n  .cta-text {\n    font-weight: normal;\n    font-size: 13px;\n    line-height: 1.230769231; // (16 / 13) –> 16px computed\n  }\n\n  .pocket-cta-button,\n  .cta-text {\n    vertical-align: top;\n  }\n}\n"
  },
  {
    "path": "content-src/components/Search/Search.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\n/* globals ContentSearchUIController */\n\"use strict\";\n\nimport { actionCreators as ac, actionTypes as at } from \"common/Actions.jsm\";\nimport { connect } from \"react-redux\";\nimport { IS_NEWTAB } from \"content-src/lib/constants\";\nimport React from \"react\";\n\nexport class _Search extends React.PureComponent {\n  constructor(props) {\n    super(props);\n    this.onSearchClick = this.onSearchClick.bind(this);\n    this.onSearchHandoffClick = this.onSearchHandoffClick.bind(this);\n    this.onSearchHandoffPaste = this.onSearchHandoffPaste.bind(this);\n    this.onSearchHandoffDrop = this.onSearchHandoffDrop.bind(this);\n    this.onInputMount = this.onInputMount.bind(this);\n    this.onSearchHandoffButtonMount = this.onSearchHandoffButtonMount.bind(\n      this\n    );\n  }\n\n  handleEvent(event) {\n    // Also track search events with our own telemetry\n    if (event.detail.type === \"Search\") {\n      this.props.dispatch(ac.UserEvent({ event: \"SEARCH\" }));\n    }\n  }\n\n  onSearchClick(event) {\n    window.gContentSearchController.search(event);\n  }\n\n  doSearchHandoff(text) {\n    this.props.dispatch(\n      ac.OnlyToMain({ type: at.HANDOFF_SEARCH_TO_AWESOMEBAR, data: { text } })\n    );\n    this.props.dispatch({ type: at.FAKE_FOCUS_SEARCH });\n    this.props.dispatch(ac.UserEvent({ event: \"SEARCH_HANDOFF\" }));\n    if (text) {\n      this.props.dispatch({ type: at.HIDE_SEARCH });\n    }\n  }\n\n  onSearchHandoffClick(event) {\n    // When search hand-off is enabled, we render a big button that is styled to\n    // look like a search textbox. If the button is clicked, we style\n    // the button as if it was a focused search box and show a fake cursor but\n    // really focus the awesomebar without the focus styles (\"hidden focus\").\n    event.preventDefault();\n    this.doSearchHandoff();\n  }\n\n  onSearchHandoffPaste(event) {\n    event.preventDefault();\n    this.doSearchHandoff(event.clipboardData.getData(\"Text\"));\n  }\n\n  onSearchHandoffDrop(event) {\n    event.preventDefault();\n    let text = event.dataTransfer.getData(\"text\");\n    if (text) {\n      this.doSearchHandoff(text);\n    }\n  }\n\n  componentWillUnmount() {\n    delete window.gContentSearchController;\n  }\n\n  onInputMount(input) {\n    if (input) {\n      // The \"healthReportKey\" and needs to be \"newtab\" or \"abouthome\" so that\n      // BrowserUsageTelemetry.jsm knows to handle events with this name, and\n      // can add the appropriate telemetry probes for search. Without the correct\n      // name, certain tests like browser_UsageTelemetry_content.js will fail\n      // (See github ticket #2348 for more details)\n      const healthReportKey = IS_NEWTAB ? \"newtab\" : \"abouthome\";\n\n      // The \"searchSource\" needs to be \"newtab\" or \"homepage\" and is sent with\n      // the search data and acts as context for the search request (See\n      // nsISearchEngine.getSubmission). It is necessary so that search engine\n      // plugins can correctly atribute referrals. (See github ticket #3321 for\n      // more details)\n      const searchSource = IS_NEWTAB ? \"newtab\" : \"homepage\";\n\n      // gContentSearchController needs to exist as a global so that tests for\n      // the existing about:home can find it; and so it allows these tests to pass.\n      // In the future, when activity stream is default about:home, this can be renamed\n      window.gContentSearchController = new ContentSearchUIController(\n        input,\n        input.parentNode,\n        healthReportKey,\n        searchSource\n      );\n      addEventListener(\"ContentSearchClient\", this);\n    } else {\n      window.gContentSearchController = null;\n      removeEventListener(\"ContentSearchClient\", this);\n    }\n  }\n\n  onSearchHandoffButtonMount(button) {\n    // Keep a reference to the button for use during \"paste\" event handling.\n    this._searchHandoffButton = button;\n  }\n\n  /*\n   * Do not change the ID on the input field, as legacy newtab code\n   * specifically looks for the id 'newtab-search-text' on input fields\n   * in order to execute searches in various tests\n   */\n  render() {\n    const wrapperClassName = [\n      \"search-wrapper\",\n      this.props.hide && \"search-hidden\",\n      this.props.fakeFocus && \"fake-focus\",\n    ]\n      .filter(v => v)\n      .join(\" \");\n\n    return (\n      <div className={wrapperClassName}>\n        {this.props.showLogo && (\n          <div className=\"logo-and-wordmark\">\n            <div className=\"logo\" />\n            <div className=\"wordmark\" />\n          </div>\n        )}\n        {!this.props.handoffEnabled && (\n          <div className=\"search-inner-wrapper\">\n            <input\n              id=\"newtab-search-text\"\n              data-l10n-id=\"newtab-search-box-search-the-web-input\"\n              maxLength=\"256\"\n              ref={this.onInputMount}\n              type=\"search\"\n            />\n            <button\n              id=\"searchSubmit\"\n              className=\"search-button\"\n              data-l10n-id=\"newtab-search-box-search-button\"\n              onClick={this.onSearchClick}\n            />\n          </div>\n        )}\n        {this.props.handoffEnabled && (\n          <div className=\"search-inner-wrapper\">\n            <button\n              className=\"search-handoff-button\"\n              data-l10n-id=\"newtab-search-box-search-the-web-input\"\n              ref={this.onSearchHandoffButtonMount}\n              onClick={this.onSearchHandoffClick}\n              tabIndex=\"-1\"\n            >\n              <div\n                className=\"fake-textbox\"\n                data-l10n-id=\"newtab-search-box-search-the-web-text\"\n              />\n              <input\n                type=\"search\"\n                className=\"fake-editable\"\n                tabIndex=\"-1\"\n                aria-hidden=\"true\"\n                onDrop={this.onSearchHandoffDrop}\n                onPaste={this.onSearchHandoffPaste}\n              />\n              <div className=\"fake-caret\" />\n            </button>\n            {/*\n            This dummy and hidden input below is so we can load ContentSearchUIController.\n            Why? It sets --newtab-search-icon for us and it isn't trivial to port over.\n          */}\n            <input\n              type=\"search\"\n              style={{ display: \"none\" }}\n              ref={this.onInputMount}\n            />\n          </div>\n        )}\n      </div>\n    );\n  }\n}\n\nexport const Search = connect()(_Search);\n"
  },
  {
    "path": "content-src/components/Search/_Search.scss",
    "content": "$search-height: 48px;\n$search-icon-size: 24px;\n$search-icon-padding: 12px;\n$search-icon-width: 2 * $search-icon-padding + $search-icon-size -2;\n$search-button-width: 48px;\n$glyph-forward: url('chrome://browser/skin/forward.svg');\n\n.search-wrapper {\n  padding: 34px 0 64px;\n\n  .only-search & {\n    padding: 0 0 64px;\n  }\n\n  .logo-and-wordmark {\n    $logo-size: 96px;\n    $wordmark-size: 172px;\n\n    align-items: center;\n    display: flex;\n    justify-content: center;\n    margin-bottom: 49px;\n\n    .logo {\n      background: url('chrome://branding/content/icon128.png') no-repeat center center;\n      background-size: $logo-size;\n      display: inline-block;\n      height: $logo-size;\n      width: $logo-size;\n    }\n\n    .wordmark {\n      background: url('#{$image-path}firefox-wordmark.svg') no-repeat center center;\n      background-size: $wordmark-size;\n      -moz-context-properties: fill;\n      display: inline-block;\n      fill: var(--newtab-search-wordmark-color);\n      height: $logo-size;\n      margin-inline-start: 15px;\n      width: $wordmark-size;\n    }\n\n    @media (max-width: $break-point-medium - 1) {\n      $logo-size-small: 64px;\n      $wordmark-small-size: 100px;\n\n      .logo {\n        background-size: $logo-size-small;\n        height: $logo-size-small;\n        width: $logo-size-small;\n      }\n\n      .wordmark {\n        background-size: $wordmark-small-size;\n        height: $logo-size-small;\n        width: $wordmark-small-size;\n      }\n    }\n  }\n\n  .search-inner-wrapper {\n    cursor: default;\n    display: flex;\n    height: $search-height;\n    margin: 0 auto;\n    position: relative;\n    width: $searchbar-width-small;\n\n    @media (min-width: $break-point-medium) {\n      width: $searchbar-width-medium;\n    }\n\n    @media (min-width: $break-point-large) {\n      width: $searchbar-width-large;\n    }\n  }\n\n  input {\n    background: var(--newtab-textbox-background-color) var(--newtab-search-icon) $search-icon-padding center no-repeat;\n    background-size: $search-icon-size;\n    border: solid 1px var(--newtab-search-border-color);\n    box-shadow: $shadow-secondary, 0 0 0 1px $black-15;\n    font-size: 15px;\n    -moz-context-properties: fill;\n    fill: var(--newtab-search-icon-color);\n    padding: 0;\n    padding-inline-end: $search-button-width;\n    padding-inline-start: $search-icon-width;\n    width: 100%;\n\n    &:dir(rtl) {\n      background-position-x: right $search-icon-padding;\n    }\n  }\n\n  &:hover input {\n    box-shadow: $shadow-secondary, 0 0 0 1px $black-25;\n  }\n\n  .search-inner-wrapper:active input,\n  input:focus {\n    border: $input-border-active;\n    box-shadow: var(--newtab-textbox-focus-boxshadow);\n  }\n\n  .search-button {\n    background: $glyph-forward no-repeat center center;\n    background-size: 16px 16px;\n    border: 0;\n    border-radius: 0 $border-radius $border-radius 0;\n    -moz-context-properties: fill;\n    fill: var(--newtab-search-icon-color);\n    height: 100%;\n    inset-inline-end: 0;\n    position: absolute;\n    width: $search-button-width;\n\n    &:focus,\n    &:hover {\n      background-color: $grey-90-10;\n      cursor: pointer;\n    }\n\n    &:active {\n      background-color: $grey-90-20;\n    }\n\n    &:dir(rtl) {\n      transform: scaleX(-1);\n    }\n  }\n}\n\n.non-collapsible-section + .below-search-snippet-wrapper {\n  // If search is enabled, we need to invade its large bottom padding.\n  margin-top: -48px;\n}\n\n@media (max-height: 700px) {\n  .search-wrapper {\n    padding: 0 0 30px;\n  }\n\n  .non-collapsible-section + .below-search-snippet-wrapper {\n    // In shorter windows, search doesn't have such a large padding.\n    margin-top: -14px;\n  }\n\n  .below-search-snippet-wrapper {\n    min-height: 0;\n  }\n}\n\n.search-handoff-button {\n  background: var(--newtab-textbox-background-color) var(--newtab-search-icon) $search-icon-padding center no-repeat;\n  background-size: $search-icon-size;\n  border: solid 1px var(--newtab-search-border-color);\n  border-radius: 3px;\n  box-shadow: $shadow-secondary, 0 0 0 1px $black-15;\n  cursor: text;\n  font-size: 15px;\n  padding: 0;\n  padding-inline-end: 48px;\n  padding-inline-start: 46px;\n  opacity: 1;\n  transition: opacity 500ms;\n  width: 100%;\n\n  &:dir(rtl) {\n    background-position-x: right $search-icon-padding;\n  }\n\n  &:hover {\n    box-shadow: $shadow-secondary, 0 0 0 1px $black-25;\n  }\n\n  .fake-focus & {\n    border: $input-border-active;\n    box-shadow: var(--newtab-textbox-focus-boxshadow);\n\n    .fake-caret {\n      display: block;\n    }\n  }\n\n  .search-hidden & {\n    opacity: 0;\n    visibility: hidden;\n  }\n\n  .fake-editable:focus {\n    outline: none;\n    caret-color: transparent;\n  }\n\n  .fake-editable {\n    color: transparent;\n    height: 100%;\n    opacity: 0;\n    position: absolute;\n    top: 0;\n    left: 0;\n    right: 0;\n    bottom: 0;\n  }\n\n  .fake-textbox {\n    opacity: 0.54;\n    text-align: start;\n  }\n\n  .fake-caret {\n    animation: caret-animation 1.3s steps(5, start) infinite;\n    background: var(--newtab-text-primary-color);\n    display: none;\n    inset-inline-start: 47px;\n    height: 17px;\n    position: absolute;\n    top: 16px;\n    width: 1px;\n\n    @keyframes caret-animation {\n      to {\n        visibility: hidden;\n      }\n    }\n  }\n}\n\n@media (min-height: 701px) {\n  body:not(.inline-onboarding) .fixed-search {\n    main {\n      padding-top: 146px;\n    }\n\n    .search-wrapper {\n      $search-header-bar-height: 95px;\n      $search-height: 35px;\n      $search-icon-size: 16px;\n      $search-icon-padding: 16px;\n\n      background-color: var(--newtab-search-header-background-color);\n      border-bottom: solid 1px var(--newtab-border-secondary-color);\n      height: $search-header-bar-height;\n      left: 0;\n      padding: 30px 0;\n      position: fixed;\n      top: 0;\n      width: 100%;\n      z-index: 9;\n\n      .search-inner-wrapper {\n        height: $search-height;\n      }\n\n      input {\n        background-position-x: $search-icon-padding;\n        background-size: $search-icon-size;\n\n        &:dir(rtl) {\n          background-position-x: right $search-icon-padding;\n        }\n      }\n    }\n\n    .search-handoff-button {\n      background-position-x: $search-icon-padding;\n      background-size: $search-icon-size;\n\n      &:dir(rtl) {\n        background-position-x: right $search-icon-padding;\n      }\n\n      .fake-caret {\n        top: 10px;\n      }\n    }\n  }\n}\n\n@at-root {\n  // Adjust the style of the contentSearchUI-generated table\n  .contentSearchSuggestionTable {\n    background-color: var(--newtab-search-dropdown-color);\n    border: 0;\n    box-shadow: $context-menu-shadow;\n    transform: translateY($textbox-shadow-size);\n\n    .contentSearchHeader {\n      background-color: var(--newtab-search-dropdown-header-color);\n      color: var(--newtab-text-secondary-color);\n    }\n\n    .contentSearchHeader,\n    .contentSearchSettingsButton {\n      border-color: var(--newtab-border-secondary-color);\n    }\n\n    .contentSearchSuggestionsList {\n      border: 0;\n    }\n\n    .contentSearchOneOffsTable {\n      background-color: var(--newtab-search-dropdown-header-color);\n      border-top: solid 1px var(--newtab-border-secondary-color);\n    }\n\n    .contentSearchSearchWithHeaderSearchText {\n      color: var(--newtab-text-primary-color);\n    }\n\n    .contentSearchSuggestionsContainer {\n      background-color: var(--newtab-search-dropdown-color);\n    }\n\n    .contentSearchSuggestionRow {\n      &.selected {\n        background: var(--newtab-element-hover-color);\n        color: var(--newtab-text-primary-color);\n\n        &:active {\n          background: var(--newtab-element-active-color);\n        }\n\n        .historyIcon {\n          fill: var(--newtab-icon-secondary-color);\n        }\n      }\n    }\n\n    .contentSearchOneOffsTable {\n      .contentSearchSuggestionsContainer {\n        background-color: var(--newtab-search-dropdown-header-color);\n      }\n    }\n\n    .contentSearchOneOffItem {\n      // Make the border slightly shorter by offsetting from the top and bottom\n      $border-offset: 18%;\n\n      background-image: none;\n      border-image: linear-gradient(transparent $border-offset, var(--newtab-border-secondary-color) $border-offset, var(--newtab-border-secondary-color) 100% - $border-offset, transparent 100% - $border-offset) 1;\n      border-inline-end: 1px solid;\n      position: relative;\n\n      &.selected {\n        background: var(--newtab-element-hover-color);\n      }\n\n      &:active {\n        background: var(--newtab-element-active-color);\n      }\n    }\n\n    .contentSearchSettingsButton {\n      &:hover {\n        background: var(--newtab-element-hover-color);\n        color: var(--newtab-text-primary-color);\n      }\n    }\n  }\n\n  .contentSearchHeaderRow > td > img,\n  .contentSearchSuggestionRow > td > .historyIcon {\n    margin-inline-start: 7px;\n    margin-inline-end: 15px;\n  }\n}\n"
  },
  {
    "path": "content-src/components/SectionMenu/SectionMenu.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport { actionCreators as ac } from \"common/Actions.jsm\";\nimport { ContextMenu } from \"content-src/components/ContextMenu/ContextMenu\";\nimport React from \"react\";\nimport { SectionMenuOptions } from \"content-src/lib/section-menu-options\";\n\nconst DEFAULT_SECTION_MENU_OPTIONS = [\n  \"MoveUp\",\n  \"MoveDown\",\n  \"Separator\",\n  \"RemoveSection\",\n  \"CheckCollapsed\",\n  \"Separator\",\n  \"ManageSection\",\n];\nconst WEBEXT_SECTION_MENU_OPTIONS = [\n  \"MoveUp\",\n  \"MoveDown\",\n  \"Separator\",\n  \"CheckCollapsed\",\n  \"Separator\",\n  \"ManageWebExtension\",\n];\n\nexport class _SectionMenu extends React.PureComponent {\n  handleAddWhileCollapsed() {\n    const { action, userEvent } = SectionMenuOptions.ExpandSection(this.props);\n    this.props.dispatch(action);\n    if (userEvent) {\n      this.props.dispatch(\n        ac.UserEvent({\n          event: userEvent,\n          source: this.props.source,\n        })\n      );\n    }\n  }\n\n  getOptions() {\n    const { props } = this;\n\n    const propOptions = props.isWebExtension\n      ? [...WEBEXT_SECTION_MENU_OPTIONS]\n      : [...DEFAULT_SECTION_MENU_OPTIONS];\n    // Remove the move related options if the section is fixed\n    if (props.isFixed) {\n      propOptions.splice(propOptions.indexOf(\"MoveUp\"), 3);\n    }\n    // Prepend custom options and a separator\n    if (props.extraOptions) {\n      propOptions.splice(0, 0, ...props.extraOptions, \"Separator\");\n    }\n    // Insert privacy notice before the last option (\"ManageSection\")\n    if (props.privacyNoticeURL) {\n      propOptions.splice(-1, 0, \"PrivacyNotice\");\n    }\n\n    const options = propOptions\n      .map(o => SectionMenuOptions[o](props))\n      .map(option => {\n        const { action, id, type, userEvent } = option;\n        if (!type && id) {\n          option.onClick = () => {\n            const hasAddEvent =\n              userEvent === \"MENU_ADD_TOPSITE\" ||\n              userEvent === \"MENU_ADD_SEARCH\";\n\n            if (props.collapsed && hasAddEvent) {\n              this.handleAddWhileCollapsed();\n            }\n\n            props.dispatch(action);\n            if (userEvent) {\n              props.dispatch(\n                ac.UserEvent({\n                  event: userEvent,\n                  source: props.source,\n                })\n              );\n            }\n          };\n        }\n        return option;\n      });\n\n    // This is for accessibility to support making each item tabbable.\n    // We want to know which item is the first and which item\n    // is the last, so we can close the context menu accordingly.\n    options[0].first = true;\n    options[options.length - 1].last = true;\n    return options;\n  }\n\n  render() {\n    return (\n      <ContextMenu\n        onUpdate={this.props.onUpdate}\n        options={this.getOptions()}\n        keyboardAccess={this.props.keyboardAccess}\n      />\n    );\n  }\n}\n\nexport const SectionMenu = _SectionMenu;\n"
  },
  {
    "path": "content-src/components/Sections/Sections.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport { actionCreators as ac, actionTypes as at } from \"common/Actions.jsm\";\nimport { Card, PlaceholderCard } from \"content-src/components/Card/Card\";\nimport { CollapsibleSection } from \"content-src/components/CollapsibleSection/CollapsibleSection\";\nimport { ComponentPerfTimer } from \"content-src/components/ComponentPerfTimer/ComponentPerfTimer\";\nimport { FluentOrText } from \"content-src/components/FluentOrText/FluentOrText\";\nimport { connect } from \"react-redux\";\nimport { MoreRecommendations } from \"content-src/components/MoreRecommendations/MoreRecommendations\";\nimport { PocketLoggedInCta } from \"content-src/components/PocketLoggedInCta/PocketLoggedInCta\";\nimport React from \"react\";\nimport { Topics } from \"content-src/components/Topics/Topics\";\nimport { TopSites } from \"content-src/components/TopSites/TopSites\";\n\nconst VISIBLE = \"visible\";\nconst VISIBILITY_CHANGE_EVENT = \"visibilitychange\";\nconst CARDS_PER_ROW_DEFAULT = 3;\nconst CARDS_PER_ROW_COMPACT_WIDE = 4;\n\nexport class Section extends React.PureComponent {\n  get numRows() {\n    const { rowsPref, maxRows, Prefs } = this.props;\n    return rowsPref ? Prefs.values[rowsPref] : maxRows;\n  }\n\n  _dispatchImpressionStats() {\n    const { props } = this;\n    let cardsPerRow = CARDS_PER_ROW_DEFAULT;\n    if (\n      props.compactCards &&\n      global.matchMedia(`(min-width: 1072px)`).matches\n    ) {\n      // If the section has compact cards and the viewport is wide enough, we show\n      // 4 columns instead of 3.\n      // $break-point-widest = 1072px (from _variables.scss)\n      cardsPerRow = CARDS_PER_ROW_COMPACT_WIDE;\n    }\n    const maxCards = cardsPerRow * this.numRows;\n    const cards = props.rows.slice(0, maxCards);\n\n    if (this.needsImpressionStats(cards)) {\n      props.dispatch(\n        ac.ImpressionStats({\n          source: props.eventSource,\n          tiles: cards.map(link => ({ id: link.guid })),\n        })\n      );\n      this.impressionCardGuids = cards.map(link => link.guid);\n    }\n  }\n\n  // This sends an event when a user sees a set of new content. If content\n  // changes while the page is hidden (i.e. preloaded or on a hidden tab),\n  // only send the event if the page becomes visible again.\n  sendImpressionStatsOrAddListener() {\n    const { props } = this;\n\n    if (!props.shouldSendImpressionStats || !props.dispatch) {\n      return;\n    }\n\n    if (props.document.visibilityState === VISIBLE) {\n      this._dispatchImpressionStats();\n    } else {\n      // We should only ever send the latest impression stats ping, so remove any\n      // older listeners.\n      if (this._onVisibilityChange) {\n        props.document.removeEventListener(\n          VISIBILITY_CHANGE_EVENT,\n          this._onVisibilityChange\n        );\n      }\n\n      // When the page becomes visible, send the impression stats ping if the section isn't collapsed.\n      this._onVisibilityChange = () => {\n        if (props.document.visibilityState === VISIBLE) {\n          if (!this.props.pref.collapsed) {\n            this._dispatchImpressionStats();\n          }\n          props.document.removeEventListener(\n            VISIBILITY_CHANGE_EVENT,\n            this._onVisibilityChange\n          );\n        }\n      };\n      props.document.addEventListener(\n        VISIBILITY_CHANGE_EVENT,\n        this._onVisibilityChange\n      );\n    }\n  }\n\n  componentWillMount() {\n    this.sendNewTabRehydrated(this.props.initialized);\n  }\n\n  componentDidMount() {\n    if (this.props.rows.length && !this.props.pref.collapsed) {\n      this.sendImpressionStatsOrAddListener();\n    }\n  }\n\n  componentDidUpdate(prevProps) {\n    const { props } = this;\n    const isCollapsed = props.pref.collapsed;\n    const wasCollapsed = prevProps.pref.collapsed;\n    if (\n      // Don't send impression stats for the empty state\n      props.rows.length &&\n      // We only want to send impression stats if the content of the cards has changed\n      // and the section is not collapsed...\n      ((props.rows !== prevProps.rows && !isCollapsed) ||\n        // or if we are expanding a section that was collapsed.\n        (wasCollapsed && !isCollapsed))\n    ) {\n      this.sendImpressionStatsOrAddListener();\n    }\n  }\n\n  componentWillUpdate(nextProps) {\n    this.sendNewTabRehydrated(nextProps.initialized);\n  }\n\n  componentWillUnmount() {\n    if (this._onVisibilityChange) {\n      this.props.document.removeEventListener(\n        VISIBILITY_CHANGE_EVENT,\n        this._onVisibilityChange\n      );\n    }\n  }\n\n  needsImpressionStats(cards) {\n    if (\n      !this.impressionCardGuids ||\n      this.impressionCardGuids.length !== cards.length\n    ) {\n      return true;\n    }\n\n    for (let i = 0; i < cards.length; i++) {\n      if (cards[i].guid !== this.impressionCardGuids[i]) {\n        return true;\n      }\n    }\n\n    return false;\n  }\n\n  // The NEW_TAB_REHYDRATED event is used to inform feeds that their\n  // data has been consumed e.g. for counting the number of tabs that\n  // have rendered that data.\n  sendNewTabRehydrated(initialized) {\n    if (initialized && !this.renderNotified) {\n      this.props.dispatch(\n        ac.AlsoToMain({ type: at.NEW_TAB_REHYDRATED, data: {} })\n      );\n      this.renderNotified = true;\n    }\n  }\n\n  render() {\n    const {\n      id,\n      eventSource,\n      title,\n      icon,\n      rows,\n      Pocket,\n      topics,\n      emptyState,\n      dispatch,\n      compactCards,\n      read_more_endpoint,\n      contextMenuOptions,\n      initialized,\n      learnMore,\n      pref,\n      privacyNoticeURL,\n      isFirst,\n      isLast,\n    } = this.props;\n\n    const waitingForSpoc =\n      id === \"topstories\" && this.props.Pocket.waitingForSpoc;\n    const maxCardsPerRow = compactCards\n      ? CARDS_PER_ROW_COMPACT_WIDE\n      : CARDS_PER_ROW_DEFAULT;\n    const { numRows } = this;\n    const maxCards = maxCardsPerRow * numRows;\n    const maxCardsOnNarrow = CARDS_PER_ROW_DEFAULT * numRows;\n\n    const { pocketCta, isUserLoggedIn } = Pocket || {};\n    const { useCta } = pocketCta || {};\n\n    // Don't display anything until we have a definitve result from Pocket,\n    // to avoid a flash of logged out state while we render.\n    const isPocketLoggedInDefined =\n      isUserLoggedIn === true || isUserLoggedIn === false;\n\n    const hasTopics = topics && !!topics.length;\n\n    const shouldShowPocketCta =\n      id === \"topstories\" && useCta && isUserLoggedIn === false;\n\n    // Show topics only for top stories and if it has loaded with topics.\n    // The classs .top-stories-bottom-container ensures content doesn't shift as things load.\n    const shouldShowTopics =\n      id === \"topstories\" &&\n      hasTopics &&\n      ((useCta && isUserLoggedIn === true) ||\n        (!useCta && isPocketLoggedInDefined));\n\n    // We use topics to determine language support for read more.\n    const shouldShowReadMore = read_more_endpoint && hasTopics;\n\n    const realRows = rows.slice(0, maxCards);\n\n    // The empty state should only be shown after we have initialized and there is no content.\n    // Otherwise, we should show placeholders.\n    const shouldShowEmptyState = initialized && !rows.length;\n\n    const cards = [];\n    if (!shouldShowEmptyState) {\n      for (let i = 0; i < maxCards; i++) {\n        const link = realRows[i];\n        // On narrow viewports, we only show 3 cards per row. We'll mark the rest as\n        // .hide-for-narrow to hide in CSS via @media query.\n        const className = i >= maxCardsOnNarrow ? \"hide-for-narrow\" : \"\";\n        let usePlaceholder = !link;\n        // If we are in the third card and waiting for spoc,\n        // use the placeholder.\n        if (!usePlaceholder && i === 2 && waitingForSpoc) {\n          usePlaceholder = true;\n        }\n        cards.push(\n          !usePlaceholder ? (\n            <Card\n              key={i}\n              index={i}\n              className={className}\n              dispatch={dispatch}\n              link={link}\n              contextMenuOptions={contextMenuOptions}\n              eventSource={eventSource}\n              shouldSendImpressionStats={this.props.shouldSendImpressionStats}\n              isWebExtension={this.props.isWebExtension}\n            />\n          ) : (\n            <PlaceholderCard key={i} className={className} />\n          )\n        );\n      }\n    }\n\n    const sectionClassName = [\n      \"section\",\n      compactCards ? \"compact-cards\" : \"normal-cards\",\n    ].join(\" \");\n\n    // <Section> <-- React component\n    // <section> <-- HTML5 element\n    return (\n      <ComponentPerfTimer {...this.props}>\n        <CollapsibleSection\n          className={sectionClassName}\n          icon={icon}\n          title={title}\n          id={id}\n          eventSource={eventSource}\n          collapsed={this.props.pref.collapsed}\n          showPrefName={(pref && pref.feed) || id}\n          privacyNoticeURL={privacyNoticeURL}\n          Prefs={this.props.Prefs}\n          isFixed={this.props.isFixed}\n          isFirst={isFirst}\n          isLast={isLast}\n          learnMore={learnMore}\n          dispatch={this.props.dispatch}\n          isWebExtension={this.props.isWebExtension}\n        >\n          {!shouldShowEmptyState && (\n            <ul className=\"section-list\" style={{ padding: 0 }}>\n              {cards}\n            </ul>\n          )}\n          {shouldShowEmptyState && (\n            <div className=\"section-empty-state\">\n              <div className=\"empty-state\">\n                {emptyState.icon &&\n                emptyState.icon.startsWith(\"moz-extension://\") ? (\n                  <span\n                    className=\"empty-state-icon icon\"\n                    style={{ \"background-image\": `url('${emptyState.icon}')` }}\n                  />\n                ) : (\n                  <span\n                    className={`empty-state-icon icon icon-${emptyState.icon}`}\n                  />\n                )}\n                <FluentOrText message={emptyState.message}>\n                  <p className=\"empty-state-message\" />\n                </FluentOrText>\n              </div>\n            </div>\n          )}\n          {id === \"topstories\" && (\n            <div className=\"top-stories-bottom-container\">\n              {shouldShowTopics && (\n                <div className=\"wrapper-topics\">\n                  <Topics topics={this.props.topics} />\n                </div>\n              )}\n\n              {shouldShowPocketCta && (\n                <div className=\"wrapper-cta\">\n                  <PocketLoggedInCta />\n                </div>\n              )}\n\n              <div className=\"wrapper-more-recommendations\">\n                {shouldShowReadMore && (\n                  <MoreRecommendations\n                    read_more_endpoint={read_more_endpoint}\n                  />\n                )}\n              </div>\n            </div>\n          )}\n        </CollapsibleSection>\n      </ComponentPerfTimer>\n    );\n  }\n}\n\nSection.defaultProps = {\n  document: global.document,\n  rows: [],\n  emptyState: {},\n  pref: {},\n  title: \"\",\n};\n\nexport const SectionIntl = connect(state => ({\n  Prefs: state.Prefs,\n  Pocket: state.Pocket,\n}))(Section);\n\nexport class _Sections extends React.PureComponent {\n  renderSections() {\n    const sections = [];\n    const enabledSections = this.props.Sections.filter(\n      section => section.enabled\n    );\n    const {\n      sectionOrder,\n      \"feeds.topsites\": showTopSites,\n    } = this.props.Prefs.values;\n    // Enabled sections doesn't include Top Sites, so we add it if enabled.\n    const expectedCount = enabledSections.length + ~~showTopSites;\n\n    for (const sectionId of sectionOrder.split(\",\")) {\n      const commonProps = {\n        key: sectionId,\n        isFirst: sections.length === 0,\n        isLast: sections.length === expectedCount - 1,\n      };\n      if (sectionId === \"topsites\" && showTopSites) {\n        sections.push(<TopSites {...commonProps} />);\n      } else {\n        const section = enabledSections.find(s => s.id === sectionId);\n        if (section) {\n          sections.push(<SectionIntl {...section} {...commonProps} />);\n        }\n      }\n    }\n    return sections;\n  }\n\n  render() {\n    return <div className=\"sections-list\">{this.renderSections()}</div>;\n  }\n}\n\nexport const Sections = connect(state => ({\n  Sections: state.Sections,\n  Prefs: state.Prefs,\n}))(_Sections);\n"
  },
  {
    "path": "content-src/components/Sections/_Sections.scss",
    "content": ".sections-list {\n  .section-list {\n    display: grid;\n    grid-gap: $base-gutter;\n    grid-template-columns: repeat(auto-fit, $card-width);\n    margin: 0;\n\n    @media (max-width: $break-point-medium) {\n      @include context-menu-open-left;\n    }\n\n    @media (min-width: $break-point-medium) and (max-width: $break-point-large) {\n      :nth-child(2n) {\n        @include context-menu-open-left;\n      }\n    }\n\n    @media (min-width: $break-point-large) and (max-width: $break-point-large + 2 * $card-width) {\n      :nth-child(3n) {\n        @include context-menu-open-left;\n      }\n    }\n\n    @media (min-width: $break-point-widest) and (max-width: $break-point-widest + 2 * $card-width) {\n      // 3n for normal cards, 4n for compact cards\n      :nth-child(3n),\n      :nth-child(4n) {\n        @include context-menu-open-left;\n      }\n    }\n  }\n\n  .section-empty-state {\n    border: $border-secondary;\n    border-radius: $border-radius;\n    display: flex;\n    height: $card-height;\n    width: 100%;\n\n    .empty-state {\n      margin: auto;\n      max-width: 350px;\n\n      .empty-state-icon {\n        background-position: center;\n        background-repeat: no-repeat;\n        background-size: 50px 50px;\n        -moz-context-properties: fill;\n        display: block;\n        fill: var(--newtab-icon-secondary-color);\n        height: 50px;\n        margin: 0 auto;\n        width: 50px;\n      }\n\n      .empty-state-message {\n        color: var(--newtab-text-primary-color);\n        font-size: 13px;\n        margin-bottom: 0;\n        text-align: center;\n      }\n    }\n\n    @media (min-width: $break-point-widest) {\n      height: $card-height-large;\n    }\n  }\n}\n\n.top-stories-bottom-container {\n  color: var(--newtab-section-navigation-text-color);\n  font-size: 12px;\n  line-height: 1.6;\n  margin-top: $topic-margin-top;\n  display: flex;\n  justify-content: space-between;\n\n  a {\n    color: var(--newtab-link-secondary-color);\n    font-weight: bold;\n\n    &.more-recommendations {\n      font-weight: normal;\n      font-size: 13px;\n    }\n  }\n\n  .wrapper-topics,\n  .wrapper-cta + .wrapper-more-recommendations {\n    @media (max-width: $break-point-large - 1) {\n      display: none;\n    }\n  }\n\n  @media (max-width: $break-point-medium - 1) {\n    .wrapper-cta {\n      text-align: center;\n\n      .pocket-logged-in-cta {\n        display: block;\n        margin-inline-end: 0;\n\n        .pocket-cta-button {\n          max-width: none;\n          display: block;\n          margin-inline-end: 0;\n          margin: 5px 0 10px;\n        }\n      }\n    }\n\n    .wrapper-more-recommendations {\n      width: 100%;\n\n      .more-recommendations {\n        justify-content: center;\n\n        &::after {\n          display: none;\n        }\n      }\n    }\n  }\n}\n\n@media (min-width: $break-point-widest) {\n  .sections-list {\n    // Compact cards stay the same size but normal cards get bigger.\n    .normal-cards {\n      .section-list {\n        grid-template-columns: repeat(auto-fit, $card-width-large);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "content-src/components/TopSites/SearchShortcutsForm.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport { actionCreators as ac, actionTypes as at } from \"common/Actions.jsm\";\nimport React from \"react\";\nimport { TOP_SITES_SOURCE } from \"./TopSitesConstants\";\n\nexport class SelectableSearchShortcut extends React.PureComponent {\n  render() {\n    const { shortcut, selected } = this.props;\n    const imageStyle = { backgroundImage: `url(\"${shortcut.tippyTopIcon}\")` };\n    return (\n      <div className=\"top-site-outer search-shortcut\">\n        <input\n          type=\"checkbox\"\n          id={shortcut.keyword}\n          name={shortcut.keyword}\n          checked={selected}\n          onChange={this.props.onChange}\n        />\n        <label htmlFor={shortcut.keyword}>\n          <div className=\"top-site-inner\">\n            <span>\n              <div className=\"tile\">\n                <div\n                  className=\"top-site-icon rich-icon\"\n                  style={imageStyle}\n                  data-fallback=\"@\"\n                />\n                <div className=\"top-site-icon search-topsite\" />\n              </div>\n              <div className=\"title\">\n                <span dir=\"auto\">{shortcut.keyword}</span>\n              </div>\n            </span>\n          </div>\n        </label>\n      </div>\n    );\n  }\n}\n\nexport class SearchShortcutsForm extends React.PureComponent {\n  constructor(props) {\n    super(props);\n    this.handleChange = this.handleChange.bind(this);\n    this.onCancelButtonClick = this.onCancelButtonClick.bind(this);\n    this.onSaveButtonClick = this.onSaveButtonClick.bind(this);\n\n    // clone the shortcuts and add them to the state so we can add isSelected property\n    const shortcuts = [];\n    const { rows, searchShortcuts } = props.TopSites;\n    searchShortcuts.forEach(shortcut => {\n      shortcuts.push({\n        ...shortcut,\n        isSelected: !!rows.find(\n          row =>\n            row &&\n            row.isPinned &&\n            row.searchTopSite &&\n            row.label === shortcut.keyword\n        ),\n      });\n    });\n    this.state = { shortcuts };\n  }\n\n  handleChange(event) {\n    const { target } = event;\n    const { name, checked } = target;\n    this.setState(prevState => {\n      const shortcuts = prevState.shortcuts.slice();\n      let shortcut = shortcuts.find(({ keyword }) => keyword === name);\n      shortcut.isSelected = checked;\n      return { shortcuts };\n    });\n  }\n\n  onCancelButtonClick(ev) {\n    ev.preventDefault();\n    this.props.onClose();\n  }\n\n  onSaveButtonClick(ev) {\n    ev.preventDefault();\n\n    // Check if there were any changes and act accordingly\n    const { rows } = this.props.TopSites;\n    const pinQueue = [];\n    const unpinQueue = [];\n    this.state.shortcuts.forEach(shortcut => {\n      const alreadyPinned = rows.find(\n        row =>\n          row &&\n          row.isPinned &&\n          row.searchTopSite &&\n          row.label === shortcut.keyword\n      );\n      if (shortcut.isSelected && !alreadyPinned) {\n        pinQueue.push(this._searchTopSite(shortcut));\n      } else if (!shortcut.isSelected && alreadyPinned) {\n        unpinQueue.push({\n          url: alreadyPinned.url,\n          searchVendor: shortcut.shortURL,\n        });\n      }\n    });\n\n    // Tell the feed to do the work.\n    this.props.dispatch(\n      ac.OnlyToMain({\n        type: at.UPDATE_PINNED_SEARCH_SHORTCUTS,\n        data: {\n          addedShortcuts: pinQueue,\n          deletedShortcuts: unpinQueue,\n        },\n      })\n    );\n\n    // Send the Telemetry pings.\n    pinQueue.forEach(shortcut => {\n      this.props.dispatch(\n        ac.UserEvent({\n          source: TOP_SITES_SOURCE,\n          event: \"SEARCH_EDIT_ADD\",\n          value: { search_vendor: shortcut.searchVendor },\n        })\n      );\n    });\n    unpinQueue.forEach(shortcut => {\n      this.props.dispatch(\n        ac.UserEvent({\n          source: TOP_SITES_SOURCE,\n          event: \"SEARCH_EDIT_DELETE\",\n          value: { search_vendor: shortcut.searchVendor },\n        })\n      );\n    });\n\n    this.props.onClose();\n  }\n\n  _searchTopSite(shortcut) {\n    return {\n      url: shortcut.url,\n      searchTopSite: true,\n      label: shortcut.keyword,\n      searchVendor: shortcut.shortURL,\n    };\n  }\n\n  render() {\n    return (\n      <form className=\"topsite-form\">\n        <div className=\"search-shortcuts-container\">\n          <h3\n            className=\"section-title grey-title\"\n            data-l10n-id=\"newtab-topsites-add-search-engine-header\"\n          />\n          <div>\n            {this.state.shortcuts.map(shortcut => (\n              <SelectableSearchShortcut\n                key={shortcut.keyword}\n                shortcut={shortcut}\n                selected={shortcut.isSelected}\n                onChange={this.handleChange}\n              />\n            ))}\n          </div>\n        </div>\n        <section className=\"actions\">\n          <button\n            className=\"cancel\"\n            type=\"button\"\n            onClick={this.onCancelButtonClick}\n            data-l10n-id=\"newtab-topsites-cancel-button\"\n          />\n          <button\n            className=\"done\"\n            type=\"submit\"\n            onClick={this.onSaveButtonClick}\n            data-l10n-id=\"newtab-topsites-save-button\"\n          />\n        </section>\n      </form>\n    );\n  }\n}\n"
  },
  {
    "path": "content-src/components/TopSites/TopSite.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport { actionCreators as ac, actionTypes as at } from \"common/Actions.jsm\";\nimport {\n  MIN_CORNER_FAVICON_SIZE,\n  MIN_RICH_FAVICON_SIZE,\n  TOP_SITES_CONTEXT_MENU_OPTIONS,\n  TOP_SITES_SPOC_CONTEXT_MENU_OPTIONS,\n  TOP_SITES_SEARCH_SHORTCUTS_CONTEXT_MENU_OPTIONS,\n  TOP_SITES_SOURCE,\n} from \"./TopSitesConstants\";\nimport { LinkMenu } from \"content-src/components/LinkMenu/LinkMenu\";\nimport { ImpressionStats } from \"../DiscoveryStreamImpressionStats/ImpressionStats\";\nimport React from \"react\";\nimport { ScreenshotUtils } from \"content-src/lib/screenshot-utils\";\nimport { TOP_SITES_MAX_SITES_PER_ROW } from \"common/Reducers.jsm\";\nimport { ContextMenuButton } from \"content-src/components/ContextMenu/ContextMenuButton\";\nconst SPOC_TYPE = \"SPOC\";\n\nexport class TopSiteLink extends React.PureComponent {\n  constructor(props) {\n    super(props);\n    this.state = { screenshotImage: null };\n    this.onDragEvent = this.onDragEvent.bind(this);\n    this.onKeyPress = this.onKeyPress.bind(this);\n  }\n\n  /*\n   * Helper to determine whether the drop zone should allow a drop. We only allow\n   * dropping top sites for now.\n   */\n  _allowDrop(e) {\n    return e.dataTransfer.types.includes(\"text/topsite-index\");\n  }\n\n  onDragEvent(event) {\n    switch (event.type) {\n      case \"click\":\n        // Stop any link clicks if we started any dragging\n        if (this.dragged) {\n          event.preventDefault();\n        }\n        break;\n      case \"dragstart\":\n        this.dragged = true;\n        event.dataTransfer.effectAllowed = \"move\";\n        event.dataTransfer.setData(\"text/topsite-index\", this.props.index);\n        event.target.blur();\n        this.props.onDragEvent(\n          event,\n          this.props.index,\n          this.props.link,\n          this.props.title\n        );\n        break;\n      case \"dragend\":\n        this.props.onDragEvent(event);\n        break;\n      case \"dragenter\":\n      case \"dragover\":\n      case \"drop\":\n        if (this._allowDrop(event)) {\n          event.preventDefault();\n          this.props.onDragEvent(event, this.props.index);\n        }\n        break;\n      case \"mousedown\":\n        // Block the scroll wheel from appearing for middle clicks on search top sites\n        if (event.button === 1 && this.props.link.searchTopSite) {\n          event.preventDefault();\n        }\n        // Reset at the first mouse event of a potential drag\n        this.dragged = false;\n        break;\n    }\n  }\n\n  /**\n   * Helper to obtain the next state based on nextProps and prevState.\n   *\n   * NOTE: Rename this method to getDerivedStateFromProps when we update React\n   *       to >= 16.3. We will need to update tests as well. We cannot rename this\n   *       method to getDerivedStateFromProps now because there is a mismatch in\n   *       the React version that we are using for both testing and production.\n   *       (i.e. react-test-render => \"16.3.2\", react => \"16.2.0\").\n   *\n   * See https://github.com/airbnb/enzyme/blob/master/packages/enzyme-adapter-react-16/package.json#L43.\n   */\n  static getNextStateFromProps(nextProps, prevState) {\n    const { screenshot } = nextProps.link;\n    const imageInState = ScreenshotUtils.isRemoteImageLocal(\n      prevState.screenshotImage,\n      screenshot\n    );\n    if (imageInState) {\n      return null;\n    }\n\n    // Since image was updated, attempt to revoke old image blob URL, if it exists.\n    ScreenshotUtils.maybeRevokeBlobObjectURL(prevState.screenshotImage);\n\n    return {\n      screenshotImage: ScreenshotUtils.createLocalImageObject(screenshot),\n    };\n  }\n\n  // NOTE: Remove this function when we update React to >= 16.3 since React will\n  //       call getDerivedStateFromProps automatically. We will also need to\n  //       rename getNextStateFromProps to getDerivedStateFromProps.\n  componentWillMount() {\n    const nextState = TopSiteLink.getNextStateFromProps(this.props, this.state);\n    if (nextState) {\n      this.setState(nextState);\n    }\n  }\n\n  // NOTE: Remove this function when we update React to >= 16.3 since React will\n  //       call getDerivedStateFromProps automatically. We will also need to\n  //       rename getNextStateFromProps to getDerivedStateFromProps.\n  componentWillReceiveProps(nextProps) {\n    const nextState = TopSiteLink.getNextStateFromProps(nextProps, this.state);\n    if (nextState) {\n      this.setState(nextState);\n    }\n  }\n\n  componentWillUnmount() {\n    ScreenshotUtils.maybeRevokeBlobObjectURL(this.state.screenshotImage);\n  }\n\n  onKeyPress(event) {\n    // If we have tabbed to a search shortcut top site, and we click 'enter',\n    // we should execute the onClick function. This needs to be added because\n    // search top sites are anchor tags without an href. See bug 1483135\n    if (this.props.link.searchTopSite && event.key === \"Enter\") {\n      this.props.onClick(event);\n    }\n  }\n\n  render() {\n    const {\n      children,\n      className,\n      defaultStyle,\n      isDraggable,\n      link,\n      onClick,\n      title,\n    } = this.props;\n    const topSiteOuterClassName = `top-site-outer${\n      className ? ` ${className}` : \"\"\n    }${link.isDragged ? \" dragged\" : \"\"}${\n      link.searchTopSite ? \" search-shortcut\" : \"\"\n    }`;\n    const { tippyTopIcon, faviconSize } = link;\n    const [letterFallback] = title;\n    let imageClassName;\n    let imageStyle;\n    let showSmallFavicon = false;\n    let smallFaviconStyle;\n    let smallFaviconFallback;\n    let hasScreenshotImage =\n      this.state.screenshotImage && this.state.screenshotImage.url;\n    if (defaultStyle) {\n      // force no styles (letter fallback) even if the link has imagery\n      smallFaviconFallback = false;\n    } else if (link.searchTopSite) {\n      imageClassName = \"top-site-icon rich-icon\";\n      imageStyle = {\n        backgroundColor: link.backgroundColor,\n        backgroundImage: `url(${tippyTopIcon})`,\n      };\n      smallFaviconStyle = { backgroundImage: `url(${tippyTopIcon})` };\n    } else if (link.customScreenshotURL) {\n      // assume high quality custom screenshot and use rich icon styles and class names\n\n      // TopSite spoc experiment only\n      const spocImgURL =\n        link.type === SPOC_TYPE ? link.customScreenshotURL : \"\";\n\n      imageClassName = \"top-site-icon rich-icon\";\n      imageStyle = {\n        backgroundColor: link.backgroundColor,\n        backgroundImage: hasScreenshotImage\n          ? `url(${this.state.screenshotImage.url})`\n          : `url(${spocImgURL})`,\n      };\n    } else if (tippyTopIcon || faviconSize >= MIN_RICH_FAVICON_SIZE) {\n      // styles and class names for top sites with rich icons\n      imageClassName = \"top-site-icon rich-icon\";\n      imageStyle = {\n        backgroundColor: link.backgroundColor,\n        backgroundImage: `url(${tippyTopIcon || link.favicon})`,\n      };\n    } else {\n      // styles and class names for top sites with screenshot + small icon in top left corner\n      imageClassName = `screenshot${hasScreenshotImage ? \" active\" : \"\"}`;\n      imageStyle = {\n        backgroundImage: hasScreenshotImage\n          ? `url(${this.state.screenshotImage.url})`\n          : \"none\",\n      };\n\n      // only show a favicon in top left if it's greater than 16x16\n      if (faviconSize >= MIN_CORNER_FAVICON_SIZE) {\n        showSmallFavicon = true;\n        smallFaviconStyle = { backgroundImage: `url(${link.favicon})` };\n      } else if (hasScreenshotImage) {\n        // Don't show a small favicon if there is no screenshot, because that\n        // would result in two fallback icons\n        showSmallFavicon = true;\n        smallFaviconFallback = true;\n      }\n    }\n    let draggableProps = {};\n    if (isDraggable) {\n      draggableProps = {\n        onClick: this.onDragEvent,\n        onDragEnd: this.onDragEvent,\n        onDragStart: this.onDragEvent,\n        onMouseDown: this.onDragEvent,\n      };\n    }\n    return (\n      <li\n        className={topSiteOuterClassName}\n        onDrop={this.onDragEvent}\n        onDragOver={this.onDragEvent}\n        onDragEnter={this.onDragEvent}\n        onDragLeave={this.onDragEvent}\n        {...draggableProps}\n      >\n        <div className=\"top-site-inner\">\n          {/* We don't yet support an accessible drag-and-drop implementation, see Bug 1552005 */}\n          {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}\n          <a\n            className=\"top-site-button\"\n            href={link.searchTopSite ? undefined : link.url}\n            tabIndex=\"0\"\n            onKeyPress={this.onKeyPress}\n            onClick={onClick}\n            draggable={true}\n          >\n            <div\n              className=\"tile\"\n              aria-hidden={true}\n              data-fallback={letterFallback}\n            >\n              <div className={imageClassName} style={imageStyle} />\n              {link.searchTopSite && (\n                <div className=\"top-site-icon search-topsite\" />\n              )}\n              {showSmallFavicon && (\n                <div\n                  className=\"top-site-icon default-icon\"\n                  data-fallback={smallFaviconFallback && letterFallback}\n                  style={smallFaviconStyle}\n                />\n              )}\n            </div>\n            <div className={`title ${link.isPinned ? \"pinned\" : \"\"}`}>\n              {link.isPinned && <div className=\"icon icon-pin-small\" />}\n              <span dir=\"auto\">{title}</span>\n            </div>\n            {link.type === SPOC_TYPE ? (\n              <span className=\"top-site-spoc-label\">Sponsored</span>\n            ) : null}\n          </a>\n          {children}\n          {link.type === SPOC_TYPE ? (\n            <ImpressionStats\n              flightId={link.flightId}\n              rows={[\n                {\n                  id: link.id,\n                  pos: link.pos,\n                  shim: link.shim && link.shim.impression,\n                },\n              ]}\n              dispatch={this.props.dispatch}\n              source={TOP_SITES_SOURCE}\n            />\n          ) : null}\n        </div>\n      </li>\n    );\n  }\n}\nTopSiteLink.defaultProps = {\n  title: \"\",\n  link: {},\n  isDraggable: true,\n};\n\nexport class TopSite extends React.PureComponent {\n  constructor(props) {\n    super(props);\n    this.state = { showContextMenu: false };\n    this.onLinkClick = this.onLinkClick.bind(this);\n    this.onMenuUpdate = this.onMenuUpdate.bind(this);\n  }\n\n  /**\n   * Report to telemetry additional information about the item.\n   */\n  _getTelemetryInfo() {\n    const value = { icon_type: this.props.link.iconType };\n    // Filter out \"not_pinned\" type for being the default\n    if (this.props.link.isPinned) {\n      value.card_type = \"pinned\";\n    }\n    if (this.props.link.searchTopSite) {\n      // Set the card_type as \"search\" regardless of its pinning status\n      value.card_type = \"search\";\n      value.search_vendor = this.props.link.hostname;\n    }\n    if (this.props.link.type === SPOC_TYPE) {\n      value.card_type = \"spoc\";\n    }\n    return { value };\n  }\n\n  userEvent(event) {\n    this.props.dispatch(\n      ac.UserEvent(\n        Object.assign(\n          {\n            event,\n            source: TOP_SITES_SOURCE,\n            action_position: this.props.index,\n          },\n          this._getTelemetryInfo()\n        )\n      )\n    );\n  }\n\n  onLinkClick(event) {\n    this.userEvent(\"CLICK\");\n\n    // Specially handle a top site link click for \"typed\" frecency bonus as\n    // specified as a property on the link.\n    event.preventDefault();\n    const { altKey, button, ctrlKey, metaKey, shiftKey } = event;\n    if (!this.props.link.searchTopSite) {\n      this.props.dispatch(\n        ac.OnlyToMain({\n          type: at.OPEN_LINK,\n          data: Object.assign(this.props.link, {\n            event: { altKey, button, ctrlKey, metaKey, shiftKey },\n          }),\n        })\n      );\n\n      // Fire off a spoc specific impression.\n      if (this.props.link.type === SPOC_TYPE) {\n        this.props.dispatch(\n          ac.ImpressionStats({\n            source: TOP_SITES_SOURCE,\n            click: 0,\n            tiles: [\n              {\n                id: this.props.link.id,\n                pos: this.props.link.pos,\n                shim: this.props.link.shim && this.props.link.shim.click,\n              },\n            ],\n          })\n        );\n      }\n    } else {\n      this.props.dispatch(\n        ac.OnlyToMain({\n          type: at.FILL_SEARCH_TERM,\n          data: { label: this.props.link.label },\n        })\n      );\n    }\n  }\n\n  onMenuUpdate(isOpen) {\n    if (isOpen) {\n      this.props.onActivate(this.props.index);\n    } else {\n      this.props.onActivate();\n    }\n  }\n\n  render() {\n    const { props } = this;\n    const { link } = props;\n    const isContextMenuOpen = props.activeIndex === props.index;\n    const title = link.label || link.hostname;\n    const menuOptions =\n      link.type !== SPOC_TYPE\n        ? TOP_SITES_CONTEXT_MENU_OPTIONS\n        : TOP_SITES_SPOC_CONTEXT_MENU_OPTIONS;\n\n    return (\n      <TopSiteLink\n        {...props}\n        onClick={this.onLinkClick}\n        onDragEvent={this.props.onDragEvent}\n        className={`${props.className || \"\"}${\n          isContextMenuOpen ? \" active\" : \"\"\n        }`}\n        title={title}\n      >\n        <div>\n          <ContextMenuButton\n            tooltip=\"newtab-menu-content-tooltip\"\n            tooltipArgs={{ title }}\n            onUpdate={this.onMenuUpdate}\n          >\n            <LinkMenu\n              dispatch={props.dispatch}\n              index={props.index}\n              onUpdate={this.onMenuUpdate}\n              options={\n                link.searchTopSite\n                  ? TOP_SITES_SEARCH_SHORTCUTS_CONTEXT_MENU_OPTIONS\n                  : menuOptions\n              }\n              site={link}\n              shouldSendImpressionStats={link.type === SPOC_TYPE}\n              siteInfo={this._getTelemetryInfo()}\n              source={TOP_SITES_SOURCE}\n            />\n          </ContextMenuButton>\n        </div>\n      </TopSiteLink>\n    );\n  }\n}\nTopSite.defaultProps = {\n  link: {},\n  onActivate() {},\n};\n\nexport class TopSitePlaceholder extends React.PureComponent {\n  constructor(props) {\n    super(props);\n    this.onEditButtonClick = this.onEditButtonClick.bind(this);\n  }\n\n  onEditButtonClick() {\n    this.props.dispatch({\n      type: at.TOP_SITES_EDIT,\n      data: { index: this.props.index },\n    });\n  }\n\n  render() {\n    return (\n      <TopSiteLink\n        {...this.props}\n        className={`placeholder ${this.props.className || \"\"}`}\n        isDraggable={false}\n      >\n        <button\n          aria-haspopup=\"true\"\n          className=\"context-menu-button edit-button icon\"\n          data-l10n-id=\"newtab-menu-topsites-placeholder-tooltip\"\n          onClick={this.onEditButtonClick}\n        />\n      </TopSiteLink>\n    );\n  }\n}\n\nexport class TopSiteList extends React.PureComponent {\n  static get DEFAULT_STATE() {\n    return {\n      activeIndex: null,\n      draggedIndex: null,\n      draggedSite: null,\n      draggedTitle: null,\n      topSitesPreview: null,\n    };\n  }\n\n  constructor(props) {\n    super(props);\n    this.state = TopSiteList.DEFAULT_STATE;\n    this.onDragEvent = this.onDragEvent.bind(this);\n    this.onActivate = this.onActivate.bind(this);\n  }\n\n  componentWillReceiveProps(nextProps) {\n    if (this.state.draggedSite) {\n      const prevTopSites = this.props.TopSites && this.props.TopSites.rows;\n      const newTopSites = nextProps.TopSites && nextProps.TopSites.rows;\n      if (\n        prevTopSites &&\n        prevTopSites[this.state.draggedIndex] &&\n        prevTopSites[this.state.draggedIndex].url ===\n          this.state.draggedSite.url &&\n        (!newTopSites[this.state.draggedIndex] ||\n          newTopSites[this.state.draggedIndex].url !==\n            this.state.draggedSite.url)\n      ) {\n        // We got the new order from the redux store via props. We can clear state now.\n        this.setState(TopSiteList.DEFAULT_STATE);\n      }\n    }\n  }\n\n  userEvent(event, index) {\n    this.props.dispatch(\n      ac.UserEvent({\n        event,\n        source: TOP_SITES_SOURCE,\n        action_position: index,\n      })\n    );\n  }\n\n  onDragEvent(event, index, link, title) {\n    switch (event.type) {\n      case \"dragstart\":\n        this.dropped = false;\n        this.setState({\n          draggedIndex: index,\n          draggedSite: link,\n          draggedTitle: title,\n          activeIndex: null,\n        });\n        this.userEvent(\"DRAG\", index);\n        break;\n      case \"dragend\":\n        if (!this.dropped) {\n          // If there was no drop event, reset the state to the default.\n          this.setState(TopSiteList.DEFAULT_STATE);\n        }\n        break;\n      case \"dragenter\":\n        if (index === this.state.draggedIndex) {\n          this.setState({ topSitesPreview: null });\n        } else {\n          this.setState({ topSitesPreview: this._makeTopSitesPreview(index) });\n        }\n        break;\n      case \"drop\":\n        if (index !== this.state.draggedIndex) {\n          this.dropped = true;\n          this.props.dispatch(\n            ac.AlsoToMain({\n              type: at.TOP_SITES_INSERT,\n              data: {\n                site: {\n                  url: this.state.draggedSite.url,\n                  label: this.state.draggedTitle,\n                  customScreenshotURL: this.state.draggedSite\n                    .customScreenshotURL,\n                  // Only if the search topsites experiment is enabled\n                  ...(this.state.draggedSite.searchTopSite && {\n                    searchTopSite: true,\n                  }),\n                },\n                index,\n                draggedFromIndex: this.state.draggedIndex,\n              },\n            })\n          );\n          this.userEvent(\"DROP\", index);\n        }\n        break;\n    }\n  }\n\n  _getTopSites() {\n    // Make a copy of the sites to truncate or extend to desired length\n    let topSites = this.props.TopSites.rows.slice();\n    topSites.length = this.props.TopSitesRows * TOP_SITES_MAX_SITES_PER_ROW;\n    return topSites;\n  }\n\n  /**\n   * Make a preview of the topsites that will be the result of dropping the currently\n   * dragged site at the specified index.\n   */\n  _makeTopSitesPreview(index) {\n    const topSites = this._getTopSites();\n    topSites[this.state.draggedIndex] = null;\n    const pinnedOnly = topSites.map(site =>\n      site && site.isPinned ? site : null\n    );\n    const unpinned = topSites.filter(site => site && !site.isPinned);\n    const siteToInsert = Object.assign({}, this.state.draggedSite, {\n      isPinned: true,\n      isDragged: true,\n    });\n    if (!pinnedOnly[index]) {\n      pinnedOnly[index] = siteToInsert;\n    } else {\n      // Find the hole to shift the pinned site(s) towards. We shift towards the\n      // hole left by the site being dragged.\n      let holeIndex = index;\n      const indexStep = index > this.state.draggedIndex ? -1 : 1;\n      while (pinnedOnly[holeIndex]) {\n        holeIndex += indexStep;\n      }\n\n      // Shift towards the hole.\n      const shiftingStep = index > this.state.draggedIndex ? 1 : -1;\n      while (holeIndex !== index) {\n        const nextIndex = holeIndex + shiftingStep;\n        pinnedOnly[holeIndex] = pinnedOnly[nextIndex];\n        holeIndex = nextIndex;\n      }\n      pinnedOnly[index] = siteToInsert;\n    }\n\n    // Fill in the remaining holes with unpinned sites.\n    const preview = pinnedOnly;\n    for (let i = 0; i < preview.length; i++) {\n      if (!preview[i]) {\n        preview[i] = unpinned.shift() || null;\n      }\n    }\n\n    return preview;\n  }\n\n  onActivate(index) {\n    this.setState({ activeIndex: index });\n  }\n\n  render() {\n    const { props } = this;\n    const topSites = this.state.topSitesPreview || this._getTopSites();\n    const topSitesUI = [];\n    const commonProps = {\n      onDragEvent: this.onDragEvent,\n      dispatch: props.dispatch,\n    };\n    // We assign a key to each placeholder slot. We need it to be independent\n    // of the slot index (i below) so that the keys used stay the same during\n    // drag and drop reordering and the underlying DOM nodes are reused.\n    // This mostly (only?) affects linux so be sure to test on linux before changing.\n    let holeIndex = 0;\n\n    // On narrow viewports, we only show 6 sites per row. We'll mark the rest as\n    // .hide-for-narrow to hide in CSS via @media query.\n    const maxNarrowVisibleIndex = props.TopSitesRows * 6;\n\n    for (let i = 0, l = topSites.length; i < l; i++) {\n      const link =\n        topSites[i] &&\n        Object.assign({}, topSites[i], {\n          iconType: this.props.topSiteIconType(topSites[i]),\n        });\n      const slotProps = {\n        key: link ? link.url : holeIndex++,\n        index: i,\n      };\n      if (i >= maxNarrowVisibleIndex) {\n        slotProps.className = \"hide-for-narrow\";\n      }\n      topSitesUI.push(\n        !link ? (\n          <TopSitePlaceholder {...slotProps} {...commonProps} />\n        ) : (\n          <TopSite\n            link={link}\n            activeIndex={this.state.activeIndex}\n            onActivate={this.onActivate}\n            {...slotProps}\n            {...commonProps}\n          />\n        )\n      );\n    }\n    return (\n      <ul\n        className={`top-sites-list${\n          this.state.draggedSite ? \" dnd-active\" : \"\"\n        }`}\n      >\n        {topSitesUI}\n      </ul>\n    );\n  }\n}\n"
  },
  {
    "path": "content-src/components/TopSites/TopSiteForm.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport { actionCreators as ac, actionTypes as at } from \"common/Actions.jsm\";\nimport { A11yLinkButton } from \"content-src/components/A11yLinkButton/A11yLinkButton\";\nimport React from \"react\";\nimport { TOP_SITES_SOURCE } from \"./TopSitesConstants\";\nimport { TopSiteFormInput } from \"./TopSiteFormInput\";\nimport { TopSiteLink } from \"./TopSite\";\n\nexport class TopSiteForm extends React.PureComponent {\n  constructor(props) {\n    super(props);\n    const { site } = props;\n    this.state = {\n      label: site ? site.label || site.hostname : \"\",\n      url: site ? site.url : \"\",\n      validationError: false,\n      customScreenshotUrl: site ? site.customScreenshotURL : \"\",\n      showCustomScreenshotForm: site ? site.customScreenshotURL : false,\n    };\n    this.onClearScreenshotInput = this.onClearScreenshotInput.bind(this);\n    this.onLabelChange = this.onLabelChange.bind(this);\n    this.onUrlChange = this.onUrlChange.bind(this);\n    this.onCancelButtonClick = this.onCancelButtonClick.bind(this);\n    this.onClearUrlClick = this.onClearUrlClick.bind(this);\n    this.onDoneButtonClick = this.onDoneButtonClick.bind(this);\n    this.onCustomScreenshotUrlChange = this.onCustomScreenshotUrlChange.bind(\n      this\n    );\n    this.onPreviewButtonClick = this.onPreviewButtonClick.bind(this);\n    this.onEnableScreenshotUrlForm = this.onEnableScreenshotUrlForm.bind(this);\n    this.validateUrl = this.validateUrl.bind(this);\n  }\n\n  onLabelChange(event) {\n    this.setState({ label: event.target.value });\n  }\n\n  onUrlChange(event) {\n    this.setState({\n      url: event.target.value,\n      validationError: false,\n    });\n  }\n\n  onClearUrlClick() {\n    this.setState({\n      url: \"\",\n      validationError: false,\n    });\n  }\n\n  onEnableScreenshotUrlForm() {\n    this.setState({ showCustomScreenshotForm: true });\n  }\n\n  _updateCustomScreenshotInput(customScreenshotUrl) {\n    this.setState({\n      customScreenshotUrl,\n      validationError: false,\n    });\n    this.props.dispatch({ type: at.PREVIEW_REQUEST_CANCEL });\n  }\n\n  onCustomScreenshotUrlChange(event) {\n    this._updateCustomScreenshotInput(event.target.value);\n  }\n\n  onClearScreenshotInput() {\n    this._updateCustomScreenshotInput(\"\");\n  }\n\n  onCancelButtonClick(ev) {\n    ev.preventDefault();\n    this.props.onClose();\n  }\n\n  onDoneButtonClick(ev) {\n    ev.preventDefault();\n\n    if (this.validateForm()) {\n      const site = { url: this.cleanUrl(this.state.url) };\n      const { index } = this.props;\n      if (this.state.label !== \"\") {\n        site.label = this.state.label;\n      }\n\n      if (this.state.customScreenshotUrl) {\n        site.customScreenshotURL = this.cleanUrl(\n          this.state.customScreenshotUrl\n        );\n      } else if (this.props.site && this.props.site.customScreenshotURL) {\n        // Used to flag that previously cached screenshot should be removed\n        site.customScreenshotURL = null;\n      }\n      this.props.dispatch(\n        ac.AlsoToMain({\n          type: at.TOP_SITES_PIN,\n          data: { site, index },\n        })\n      );\n      this.props.dispatch(\n        ac.UserEvent({\n          source: TOP_SITES_SOURCE,\n          event: \"TOP_SITES_EDIT\",\n          action_position: index,\n        })\n      );\n\n      this.props.onClose();\n    }\n  }\n\n  onPreviewButtonClick(event) {\n    event.preventDefault();\n    if (this.validateForm()) {\n      this.props.dispatch(\n        ac.AlsoToMain({\n          type: at.PREVIEW_REQUEST,\n          data: { url: this.cleanUrl(this.state.customScreenshotUrl) },\n        })\n      );\n      this.props.dispatch(\n        ac.UserEvent({\n          source: TOP_SITES_SOURCE,\n          event: \"PREVIEW_REQUEST\",\n        })\n      );\n    }\n  }\n\n  cleanUrl(url) {\n    // If we are missing a protocol, prepend http://\n    if (!url.startsWith(\"http:\") && !url.startsWith(\"https:\")) {\n      return `http://${url}`;\n    }\n    return url;\n  }\n\n  _tryParseUrl(url) {\n    try {\n      return new URL(url);\n    } catch (e) {\n      return null;\n    }\n  }\n\n  validateUrl(url) {\n    const validProtocols = [\"http:\", \"https:\"];\n    const urlObj =\n      this._tryParseUrl(url) || this._tryParseUrl(this.cleanUrl(url));\n\n    return urlObj && validProtocols.includes(urlObj.protocol);\n  }\n\n  validateCustomScreenshotUrl() {\n    const { customScreenshotUrl } = this.state;\n    return !customScreenshotUrl || this.validateUrl(customScreenshotUrl);\n  }\n\n  validateForm() {\n    const validate =\n      this.validateUrl(this.state.url) && this.validateCustomScreenshotUrl();\n\n    if (!validate) {\n      this.setState({ validationError: true });\n    }\n\n    return validate;\n  }\n\n  _renderCustomScreenshotInput() {\n    const { customScreenshotUrl } = this.state;\n    const requestFailed = this.props.previewResponse === \"\";\n    const validationError =\n      (this.state.validationError && !this.validateCustomScreenshotUrl()) ||\n      requestFailed;\n    // Set focus on error if the url field is valid or when the input is first rendered and is empty\n    const shouldFocus =\n      (validationError && this.validateUrl(this.state.url)) ||\n      !customScreenshotUrl;\n    const isLoading =\n      this.props.previewResponse === null &&\n      customScreenshotUrl &&\n      this.props.previewUrl === this.cleanUrl(customScreenshotUrl);\n\n    if (!this.state.showCustomScreenshotForm) {\n      return (\n        <A11yLinkButton\n          onClick={this.onEnableScreenshotUrlForm}\n          className=\"enable-custom-image-input\"\n          data-l10n-id=\"newtab-topsites-use-image-link\"\n        />\n      );\n    }\n    return (\n      <div className=\"custom-image-input-container\">\n        <TopSiteFormInput\n          errorMessageId={\n            requestFailed\n              ? \"newtab-topsites-image-validation\"\n              : \"newtab-topsites-url-validation\"\n          }\n          loading={isLoading}\n          onChange={this.onCustomScreenshotUrlChange}\n          onClear={this.onClearScreenshotInput}\n          shouldFocus={shouldFocus}\n          typeUrl={true}\n          value={customScreenshotUrl}\n          validationError={validationError}\n          titleId=\"newtab-topsites-image-url-label\"\n          placeholderId=\"newtab-topsites-url-input\"\n        />\n      </div>\n    );\n  }\n\n  render() {\n    const { customScreenshotUrl } = this.state;\n    const requestFailed = this.props.previewResponse === \"\";\n    // For UI purposes, editing without an existing link is \"add\"\n    const showAsAdd = !this.props.site;\n    const previous =\n      (this.props.site && this.props.site.customScreenshotURL) || \"\";\n    const changed =\n      customScreenshotUrl && this.cleanUrl(customScreenshotUrl) !== previous;\n    // Preview mode if changes were made to the custom screenshot URL and no preview was received yet\n    // or the request failed\n    const previewMode = changed && !this.props.previewResponse;\n    const previewLink = Object.assign({}, this.props.site);\n    if (this.props.previewResponse) {\n      previewLink.screenshot = this.props.previewResponse;\n      previewLink.customScreenshotURL = this.props.previewUrl;\n    }\n    // Handles the form submit so an enter press performs the correct action\n    const onSubmit = previewMode\n      ? this.onPreviewButtonClick\n      : this.onDoneButtonClick;\n    return (\n      <form className=\"topsite-form\" onSubmit={onSubmit}>\n        <div className=\"form-input-container\">\n          <h3\n            className=\"section-title grey-title\"\n            data-l10n-id={\n              showAsAdd\n                ? \"newtab-topsites-add-topsites-header\"\n                : \"newtab-topsites-edit-topsites-header\"\n            }\n          />\n          <div className=\"fields-and-preview\">\n            <div className=\"form-wrapper\">\n              <TopSiteFormInput\n                onChange={this.onLabelChange}\n                value={this.state.label}\n                titleId=\"newtab-topsites-title-label\"\n                placeholderId=\"newtab-topsites-title-input\"\n              />\n              <TopSiteFormInput\n                onChange={this.onUrlChange}\n                shouldFocus={\n                  this.state.validationError &&\n                  !this.validateUrl(this.state.url)\n                }\n                value={this.state.url}\n                onClear={this.onClearUrlClick}\n                validationError={\n                  this.state.validationError &&\n                  !this.validateUrl(this.state.url)\n                }\n                titleId=\"newtab-topsites-url-label\"\n                typeUrl={true}\n                placeholderId=\"newtab-topsites-url-input\"\n                errorMessageId=\"newtab-topsites-url-validation\"\n              />\n              {this._renderCustomScreenshotInput()}\n            </div>\n            <TopSiteLink\n              link={previewLink}\n              defaultStyle={requestFailed}\n              title={this.state.label}\n            />\n          </div>\n        </div>\n        <section className=\"actions\">\n          <button\n            className=\"cancel\"\n            type=\"button\"\n            onClick={this.onCancelButtonClick}\n            data-l10n-id=\"newtab-topsites-cancel-button\"\n          />\n          {previewMode ? (\n            <button\n              className=\"done preview\"\n              type=\"submit\"\n              data-l10n-id=\"newtab-topsites-preview-button\"\n            />\n          ) : (\n            <button\n              className=\"done\"\n              type=\"submit\"\n              data-l10n-id={\n                showAsAdd\n                  ? \"newtab-topsites-add-button\"\n                  : \"newtab-topsites-save-button\"\n              }\n            />\n          )}\n        </section>\n      </form>\n    );\n  }\n}\n\nTopSiteForm.defaultProps = {\n  site: null,\n  index: -1,\n};\n"
  },
  {
    "path": "content-src/components/TopSites/TopSiteFormInput.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport React from \"react\";\n\nexport class TopSiteFormInput extends React.PureComponent {\n  constructor(props) {\n    super(props);\n    this.state = { validationError: this.props.validationError };\n    this.onChange = this.onChange.bind(this);\n    this.onMount = this.onMount.bind(this);\n    this.onClearIconPress = this.onClearIconPress.bind(this);\n  }\n\n  componentWillReceiveProps(nextProps) {\n    if (nextProps.shouldFocus && !this.props.shouldFocus) {\n      this.input.focus();\n    }\n    if (nextProps.validationError && !this.props.validationError) {\n      this.setState({ validationError: true });\n    }\n    // If the component is in an error state but the value was cleared by the parent\n    if (this.state.validationError && !nextProps.value) {\n      this.setState({ validationError: false });\n    }\n  }\n\n  onClearIconPress(event) {\n    // If there is input in the URL or custom image URL fields,\n    // and we hit 'enter' while tabbed over the clear icon,\n    // we should execute the function to clear the field.\n    if (event.key === \"Enter\") {\n      this.props.onClear();\n    }\n  }\n\n  onChange(ev) {\n    if (this.state.validationError) {\n      this.setState({ validationError: false });\n    }\n    this.props.onChange(ev);\n  }\n\n  onMount(input) {\n    this.input = input;\n  }\n\n  renderLoadingOrCloseButton() {\n    const showClearButton = this.props.value && this.props.onClear;\n\n    if (this.props.loading) {\n      return (\n        <div className=\"loading-container\">\n          <div className=\"loading-animation\" />\n        </div>\n      );\n    } else if (showClearButton) {\n      return (\n        <button\n          type=\"button\"\n          className=\"icon icon-clear-input icon-button-style\"\n          onClick={this.props.onClear}\n          onKeyPress={this.onClearIconPress}\n        />\n      );\n    }\n    return null;\n  }\n\n  render() {\n    const { typeUrl } = this.props;\n    const { validationError } = this.state;\n\n    return (\n      <label>\n        <span data-l10n-id={this.props.titleId} />\n        <div\n          className={`field ${typeUrl ? \"url\" : \"\"}${\n            validationError ? \" invalid\" : \"\"\n          }`}\n        >\n          <input\n            type=\"text\"\n            value={this.props.value}\n            ref={this.onMount}\n            onChange={this.onChange}\n            data-l10n-id={this.props.placeholderId}\n            // Set focus on error if the url field is valid or when the input is first rendered and is empty\n            // eslint-disable-next-line jsx-a11y/no-autofocus\n            autoFocus={this.props.shouldFocus}\n            disabled={this.props.loading}\n          />\n          {this.renderLoadingOrCloseButton()}\n          {validationError && (\n            <aside\n              className=\"error-tooltip\"\n              data-l10n-id={this.props.errorMessageId}\n            />\n          )}\n        </div>\n      </label>\n    );\n  }\n}\n\nTopSiteFormInput.defaultProps = {\n  showClearButton: false,\n  value: \"\",\n  validationError: false,\n};\n"
  },
  {
    "path": "content-src/components/TopSites/TopSites.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport { actionCreators as ac, actionTypes as at } from \"common/Actions.jsm\";\nimport {\n  MIN_CORNER_FAVICON_SIZE,\n  MIN_RICH_FAVICON_SIZE,\n  TOP_SITES_SOURCE,\n} from \"./TopSitesConstants\";\nimport { CollapsibleSection } from \"content-src/components/CollapsibleSection/CollapsibleSection\";\nimport { ComponentPerfTimer } from \"content-src/components/ComponentPerfTimer/ComponentPerfTimer\";\nimport { connect } from \"react-redux\";\nimport { ModalOverlayWrapper } from \"../../asrouter/components/ModalOverlay/ModalOverlay\";\nimport React from \"react\";\nimport { SearchShortcutsForm } from \"./SearchShortcutsForm\";\nimport { TOP_SITES_MAX_SITES_PER_ROW } from \"common/Reducers.jsm\";\nimport { TopSiteForm } from \"./TopSiteForm\";\nimport { TopSiteList } from \"./TopSite\";\n\nfunction topSiteIconType(link) {\n  if (link.customScreenshotURL) {\n    return \"custom_screenshot\";\n  }\n  if (link.tippyTopIcon || link.faviconRef === \"tippytop\") {\n    return \"tippytop\";\n  }\n  if (link.faviconSize >= MIN_RICH_FAVICON_SIZE) {\n    return \"rich_icon\";\n  }\n  if (link.screenshot && link.faviconSize >= MIN_CORNER_FAVICON_SIZE) {\n    return \"screenshot_with_icon\";\n  }\n  if (link.screenshot) {\n    return \"screenshot\";\n  }\n  return \"no_image\";\n}\n\n/**\n * Iterates through TopSites and counts types of images.\n * @param acc Accumulator for reducer.\n * @param topsite Entry in TopSites.\n */\nfunction countTopSitesIconsTypes(topSites) {\n  const countTopSitesTypes = (acc, link) => {\n    acc[topSiteIconType(link)]++;\n    return acc;\n  };\n\n  return topSites.reduce(countTopSitesTypes, {\n    custom_screenshot: 0,\n    screenshot_with_icon: 0,\n    screenshot: 0,\n    tippytop: 0,\n    rich_icon: 0,\n    no_image: 0,\n  });\n}\n\nexport class _TopSites extends React.PureComponent {\n  constructor(props) {\n    super(props);\n    this.onEditFormClose = this.onEditFormClose.bind(this);\n    this.onSearchShortcutsFormClose = this.onSearchShortcutsFormClose.bind(\n      this\n    );\n  }\n\n  /**\n   * Dispatch session statistics about the quality of TopSites icons and pinned count.\n   */\n  _dispatchTopSitesStats() {\n    const topSites = this._getVisibleTopSites().filter(\n      topSite => topSite !== null && topSite !== undefined\n    );\n    const topSitesIconsStats = countTopSitesIconsTypes(topSites);\n    const topSitesPinned = topSites.filter(site => !!site.isPinned).length;\n    const searchShortcuts = topSites.filter(site => !!site.searchTopSite)\n      .length;\n    // Dispatch telemetry event with the count of TopSites images types.\n    this.props.dispatch(\n      ac.AlsoToMain({\n        type: at.SAVE_SESSION_PERF_DATA,\n        data: {\n          topsites_icon_stats: topSitesIconsStats,\n          topsites_pinned: topSitesPinned,\n          topsites_search_shortcuts: searchShortcuts,\n        },\n      })\n    );\n  }\n\n  /**\n   * Return the TopSites that are visible based on prefs and window width.\n   */\n  _getVisibleTopSites() {\n    // We hide 2 sites per row when not in the wide layout.\n    let sitesPerRow = TOP_SITES_MAX_SITES_PER_ROW;\n    // $break-point-widest = 1072px (from _variables.scss)\n    if (!global.matchMedia(`(min-width: 1072px)`).matches) {\n      sitesPerRow -= 2;\n    }\n    return this.props.TopSites.rows.slice(\n      0,\n      this.props.TopSitesRows * sitesPerRow\n    );\n  }\n\n  componentDidUpdate() {\n    this._dispatchTopSitesStats();\n  }\n\n  componentDidMount() {\n    this._dispatchTopSitesStats();\n  }\n\n  onEditFormClose() {\n    this.props.dispatch(\n      ac.UserEvent({\n        source: TOP_SITES_SOURCE,\n        event: \"TOP_SITES_EDIT_CLOSE\",\n      })\n    );\n    this.props.dispatch({ type: at.TOP_SITES_CANCEL_EDIT });\n  }\n\n  onSearchShortcutsFormClose() {\n    this.props.dispatch(\n      ac.UserEvent({\n        source: TOP_SITES_SOURCE,\n        event: \"SEARCH_EDIT_CLOSE\",\n      })\n    );\n    this.props.dispatch({ type: at.TOP_SITES_CLOSE_SEARCH_SHORTCUTS_MODAL });\n  }\n\n  render() {\n    const { props } = this;\n    const { editForm, showSearchShortcutsForm } = props.TopSites;\n    const extraMenuOptions = [\"AddTopSite\"];\n    if (props.Prefs.values[\"improvesearch.topSiteSearchShortcuts\"]) {\n      extraMenuOptions.push(\"AddSearchShortcut\");\n    }\n\n    return (\n      <ComponentPerfTimer\n        id=\"topsites\"\n        initialized={props.TopSites.initialized}\n        dispatch={props.dispatch}\n      >\n        <CollapsibleSection\n          className=\"top-sites\"\n          icon=\"topsites\"\n          id=\"topsites\"\n          title={this.props.title || { id: \"newtab-section-header-topsites\" }}\n          extraMenuOptions={extraMenuOptions}\n          showPrefName=\"feeds.topsites\"\n          eventSource={TOP_SITES_SOURCE}\n          collapsed={\n            props.TopSites.pref ? props.TopSites.pref.collapsed : undefined\n          }\n          isFixed={props.isFixed}\n          isFirst={props.isFirst}\n          isLast={props.isLast}\n          dispatch={props.dispatch}\n        >\n          <TopSiteList\n            TopSites={props.TopSites}\n            TopSitesRows={props.TopSitesRows}\n            dispatch={props.dispatch}\n            topSiteIconType={topSiteIconType}\n          />\n          <div className=\"edit-topsites-wrapper\">\n            {editForm && (\n              <div className=\"edit-topsites\">\n                <ModalOverlayWrapper\n                  unstyled={true}\n                  onClose={this.onEditFormClose}\n                  innerClassName=\"modal\"\n                >\n                  <TopSiteForm\n                    site={props.TopSites.rows[editForm.index]}\n                    onClose={this.onEditFormClose}\n                    dispatch={this.props.dispatch}\n                    {...editForm}\n                  />\n                </ModalOverlayWrapper>\n              </div>\n            )}\n            {showSearchShortcutsForm && (\n              <div className=\"edit-search-shortcuts\">\n                <ModalOverlayWrapper\n                  unstyled={true}\n                  onClose={this.onSearchShortcutsFormClose}\n                  innerClassName=\"modal\"\n                >\n                  <SearchShortcutsForm\n                    TopSites={props.TopSites}\n                    onClose={this.onSearchShortcutsFormClose}\n                    dispatch={this.props.dispatch}\n                  />\n                </ModalOverlayWrapper>\n              </div>\n            )}\n          </div>\n        </CollapsibleSection>\n      </ComponentPerfTimer>\n    );\n  }\n}\n\nexport const TopSites = connect((state, props) => ({\n  // For SPOC Experiment only, take TopSites from DiscoveryStream TopSites that takes in SPOC Data\n  TopSites: props.TopSitesWithSpoc || state.TopSites,\n  Prefs: state.Prefs,\n  TopSitesRows: state.Prefs.values.topSitesRows,\n}))(_TopSites);\n"
  },
  {
    "path": "content-src/components/TopSites/TopSitesConstants.js",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nexport const TOP_SITES_SOURCE = \"TOP_SITES\";\nexport const TOP_SITES_CONTEXT_MENU_OPTIONS = [\n  \"CheckPinTopSite\",\n  \"EditTopSite\",\n  \"Separator\",\n  \"OpenInNewWindow\",\n  \"OpenInPrivateWindow\",\n  \"Separator\",\n  \"BlockUrl\",\n  \"DeleteUrl\",\n];\nexport const TOP_SITES_SPOC_CONTEXT_MENU_OPTIONS = [\n  \"PinSpocTopSite\",\n  \"Separator\",\n  \"OpenInNewWindow\",\n  \"OpenInPrivateWindow\",\n  \"Separator\",\n  \"BlockUrl\",\n  \"ShowPrivacyInfo\",\n];\n// the special top site for search shortcut experiment can only have the option to unpin (which removes) the topsite\nexport const TOP_SITES_SEARCH_SHORTCUTS_CONTEXT_MENU_OPTIONS = [\n  \"CheckPinTopSite\",\n  \"Separator\",\n  \"BlockUrl\",\n];\n// minimum size necessary to show a rich icon instead of a screenshot\nexport const MIN_RICH_FAVICON_SIZE = 96;\n// minimum size necessary to show any icon in the top left corner with a screenshot\nexport const MIN_CORNER_FAVICON_SIZE = 16;\n"
  },
  {
    "path": "content-src/components/TopSites/_TopSites.scss",
    "content": "$top-sites-size: $grid-unit;\n$top-sites-border-radius: 4px;\n$top-sites-title-height: 30px;\n$top-sites-vertical-space: 8px;\n$screenshot-size: cover;\n$rich-icon-size: 96px;\n$default-icon-wrapper-size: 42px;\n$default-icon-size: 32px;\n$default-icon-offset: 6px;\n$half-base-gutter: $base-gutter / 2;\n$hover-transition-duration: 150ms;\n\n.top-sites {\n  // Take back the margin from the bottom row of vertical spacing as well as the\n  // extra whitespace below the title text as it's vertically centered.\n  margin-bottom: $section-spacing - ($top-sites-vertical-space + $top-sites-title-height / 3);\n}\n\n.top-sites-list {\n  list-style: none;\n  margin: 0 (-$half-base-gutter);\n  padding: 0;\n\n  // Two columns\n  @media (max-width: $break-point-medium) {\n    :nth-child(2n+1) {\n      @include context-menu-open-middle;\n    }\n\n    :nth-child(2n) {\n      @include context-menu-open-left;\n    }\n  }\n\n  // Four columns\n  @media (min-width: $break-point-medium) and (max-width: $break-point-large) {\n    :nth-child(4n) {\n      @include context-menu-open-left;\n    }\n  }\n  @media (min-width: $break-point-medium) and (max-width: $break-point-medium + $card-width) {\n    :nth-child(4n+3) {\n      @include context-menu-open-left;\n    }\n  }\n\n  // Six columns\n  @media (min-width: $break-point-large) and (max-width: $break-point-large + 2 * $card-width) {\n    :nth-child(6n) {\n      @include context-menu-open-left;\n    }\n  }\n  @media (min-width: $break-point-large) and (max-width: $break-point-large + $card-width) {\n    :nth-child(6n+5) {\n      @include context-menu-open-left;\n    }\n  }\n\n  // Eight columns\n  @media (min-width: $break-point-widest) and (max-width: $break-point-widest + 2 * $card-width) {\n    :nth-child(8n) {\n      @include context-menu-open-left;\n    }\n  }\n  @media (min-width: $break-point-widest) and (max-width: $break-point-widest + $card-width) {\n    :nth-child(8n+7) {\n      @include context-menu-open-left;\n    }\n  }\n\n  .hide-for-narrow {\n    display: none;\n  }\n\n  @media (min-width: $break-point-medium) {\n    .hide-for-narrow {\n      display: inline-block;\n    }\n  }\n\n  @media (min-width: $break-point-large) {\n    .hide-for-narrow {\n      display: none;\n    }\n  }\n\n  @media (min-width: $break-point-widest) {\n    .hide-for-narrow {\n      display: inline-block;\n    }\n  }\n\n  li {\n    margin: 0 0 $top-sites-vertical-space;\n  }\n\n  &:not(.dnd-active) {\n    .top-site-outer:-moz-any(.active, :focus, :hover) {\n      .tile {\n        @include fade-in;\n      }\n\n      @include context-menu-button-hover;\n    }\n  }\n}\n\n// container for drop zone\n.top-site-outer {\n  padding: 0 $half-base-gutter;\n  display: inline-block;\n\n  // container for context menu\n  .top-site-inner {\n    position: relative;\n\n    > a {\n      color: inherit;\n      display: block;\n      outline: none;\n\n      &:-moz-any(.active, :focus) {\n        .tile {\n          @include fade-in;\n        }\n      }\n    }\n  }\n\n  @include context-menu-button;\n\n  .tile { // sass-lint:disable-block property-sort-order\n    border-radius: $top-sites-border-radius;\n    box-shadow: inset $inner-box-shadow, var(--newtab-card-shadow);\n    cursor: pointer;\n    height: $top-sites-size;\n    position: relative;\n    width: $top-sites-size;\n\n    // For letter fallback\n    align-items: center;\n    color: var(--newtab-text-secondary-color);\n    display: flex;\n    font-size: 32px;\n    font-weight: 200;\n    justify-content: center;\n    text-transform: uppercase; // sass-lint:disable-line no-disallowed-properties\n    transition: box-shadow $hover-transition-duration;\n\n    &::before {\n      content: attr(data-fallback);\n    }\n  }\n\n  .screenshot {\n    background-color: $white;\n    background-position: top left;\n    background-size: $screenshot-size;\n    border-radius: $top-sites-border-radius;\n    box-shadow: inset $inner-box-shadow;\n    height: 100%;\n    left: 0;\n    opacity: 0;\n    position: absolute;\n    top: 0;\n    transition: opacity 1s;\n    width: 100%;\n\n    &.active {\n      opacity: 1;\n    }\n  }\n\n  // Some common styles for all icons (rich and default) in top sites\n  .top-site-icon {\n    background-color: var(--newtab-topsites-background-color);\n    background-position: center center;\n    background-repeat: no-repeat;\n    border-radius: $top-sites-border-radius;\n    box-shadow: var(--newtab-topsites-icon-shadow);\n    position: absolute;\n  }\n\n  .rich-icon {\n    background-size: cover;\n    height: 100%;\n    inset-inline-start: 0;\n    top: 0;\n    width: 100%;\n  }\n\n  .default-icon,\n  .search-topsite {\n    background-size: $default-icon-size;\n    bottom: -$default-icon-offset;\n    height: $default-icon-wrapper-size;\n    inset-inline-end: -$default-icon-offset;\n    width: $default-icon-wrapper-size;\n\n    // for corner letter fallback\n    align-items: center;\n    display: flex;\n    font-size: 20px;\n    justify-content: center;\n\n    &[data-fallback]::before {\n      content: attr(data-fallback);\n    }\n  }\n\n  .search-topsite {\n    background-image: url('#{$image-path}glyph-search-16.svg');\n    background-size: 26px;\n    background-color: $blue-60;\n    border-radius: $default-icon-wrapper-size;\n    -moz-context-properties: fill;\n    fill: $white;\n    box-shadow: var(--newtab-card-shadow);\n    transition-duration: $hover-transition-duration;\n    transition-property: background-size, bottom, inset-inline-end, height, width;\n  }\n\n  &:hover .search-topsite {\n    $hover-icon-wrapper-size: $default-icon-wrapper-size + 4;\n    $hover-icon-offset: -$default-icon-offset - 3;\n\n    background-size: 28px;\n    border-radius: $hover-icon-wrapper-size;\n    bottom: $hover-icon-offset;\n    height: $hover-icon-wrapper-size;\n    inset-inline-end: $hover-icon-offset;\n    width: $hover-icon-wrapper-size;\n  }\n\n  // We want all search shortcuts to have a white background in case they have transparency.\n  &.search-shortcut {\n    .rich-icon {\n      background-color: $white;\n    }\n  }\n\n  .title {\n    color: var(--newtab-topsites-label-color);\n    font: message-box;\n    height: $top-sites-title-height;\n    line-height: $top-sites-title-height;\n    text-align: center;\n    width: $top-sites-size;\n    position: relative;\n\n    .icon {\n      fill: var(--newtab-icon-tertiary-color);\n      inset-inline-start: 0;\n      position: absolute;\n      top: 10px;\n    }\n\n    span {\n      height: $top-sites-title-height;\n      display: block;\n      overflow: hidden;\n      text-overflow: ellipsis;\n      white-space: nowrap;\n    }\n\n    &.pinned {\n      span {\n        padding: 0 13px;\n      }\n    }\n  }\n\n  .edit-button {\n    background-image: url('#{$image-path}glyph-edit-16.svg');\n  }\n\n  &.placeholder {\n    .tile {\n      box-shadow: inset $inner-box-shadow;\n    }\n\n    .screenshot {\n      display: none;\n    }\n  }\n\n  &.dragged {\n    .tile {\n      background: $grey-20;\n      box-shadow: none;\n\n      *,\n      &::before {\n        display: none;\n      }\n    }\n\n    .title {\n      visibility: hidden;\n    }\n  }\n}\n\n.edit-topsites-wrapper {\n  .modal {\n    box-shadow: $shadow-secondary;\n    left: 0;\n    margin: 0 auto;\n    max-height: calc(100% - 40px);\n    overflow-y: auto;\n    overflow-x: hidden;\n    position: fixed;\n    right: 0;\n    top: 40px;\n    width: $wrapper-default-width;\n\n    @media (min-width: $break-point-medium) {\n      width: $wrapper-max-width-medium;\n    }\n\n    @media (min-width: $break-point-large) {\n      width: $wrapper-max-width-large;\n    }\n  }\n}\n\n.topsite-form {\n  $form-width: 300px;\n  $form-spacing: 32px;\n\n  .section-title {\n    font-size: 16px;\n    margin: 0 0 16px;\n  }\n\n  .form-input-container {\n    max-width: $form-width + 3 * $form-spacing + $rich-icon-size;\n    margin: 0 auto;\n    padding: $form-spacing;\n\n    .top-site-outer {\n      pointer-events: none;\n    }\n  }\n\n  .search-shortcuts-container {\n    max-width: 700px;\n    margin: 0 auto;\n    padding: $form-spacing;\n\n    > div {\n      margin-inline-end: -39px;\n    }\n\n    .top-site-outer {\n      margin-inline-start: 0;\n      margin-inline-end: 39px;\n    }\n  }\n\n  .top-site-outer {\n    padding: 0;\n    margin: 24px 0 0;\n    margin-inline-start: $form-spacing;\n  }\n\n  .fields-and-preview {\n    display: flex;\n  }\n\n  label {\n    font-size: $section-title-font-size;\n  }\n\n  .form-wrapper {\n    width: 100%;\n\n    .field {\n      position: relative;\n\n      .icon-clear-input {\n        position: absolute;\n        transform: translateY(-50%);\n        top: 50%;\n        inset-inline-end: 8px;\n      }\n    }\n\n    .url {\n      input:dir(ltr) {\n        padding-right: 32px;\n      }\n\n      input:dir(rtl) {\n        padding-left: 32px;\n\n        &:not(:placeholder-shown) {\n          direction: ltr;\n          text-align: right;\n        }\n      }\n    }\n\n    .enable-custom-image-input {\n      display: inline-block;\n      font-size: 13px;\n      margin-top: 4px;\n      cursor: pointer;\n    }\n\n    .custom-image-input-container {\n      margin-top: 4px;\n\n      .loading-container {\n        width: 16px;\n        height: 16px;\n        overflow: hidden;\n        position: absolute;\n        transform: translateY(-50%);\n        top: 50%;\n        inset-inline-end: 8px;\n      }\n\n      // This animation is derived from Firefox's tab loading animation\n      // See https://searchfox.org/mozilla-central/rev/b29daa46443b30612415c35be0a3c9c13b9dc5f6/browser/themes/shared/tabs.inc.css#208-216\n      .loading-animation {\n        @keyframes tab-throbber-animation {\n          100% { transform: translateX(-960px); }\n        }\n\n        @keyframes tab-throbber-animation-rtl {\n          100% { transform: translateX(960px); }\n        }\n\n        width: 960px;\n        height: 16px;\n        -moz-context-properties: fill;\n        fill: $blue-50;\n        background-image: url('chrome://browser/skin/tabbrowser/loading.svg');\n        animation: tab-throbber-animation 1.05s steps(60) infinite;\n\n        &:dir(rtl) {\n          animation-name: tab-throbber-animation-rtl;\n        }\n      }\n    }\n\n    input {\n      &[type='text'] {\n        background-color: var(--newtab-textbox-background-color);\n        border: $input-border;\n        margin: 8px 0;\n        padding: 0 8px;\n        height: 32px;\n        width: 100%;\n        font-size: 15px;\n\n        &:focus {\n          border: $input-border-active;\n          box-shadow: var(--newtab-textbox-focus-boxshadow);\n        }\n\n        &[disabled] {\n          border: $input-border;\n          box-shadow: none;\n          opacity: 0.4;\n        }\n      }\n    }\n\n    .invalid {\n      input {\n        &[type='text'] {\n          border: $input-error-border;\n          box-shadow: $input-error-boxshadow;\n        }\n      }\n    }\n\n    .error-tooltip {\n      animation: fade-up-tt 450ms;\n      background: $red-60;\n      border-radius: 2px;\n      color: $white;\n      inset-inline-start: 3px;\n      padding: 5px 12px;\n      position: absolute;\n      top: 44px;\n      z-index: 1;\n\n      // tooltip caret\n      &::before {\n        background: $red-60;\n        bottom: -8px;\n        content: '.';\n        height: 16px;\n        inset-inline-start: 12px;\n        position: absolute;\n        text-indent: -999px;\n        top: -7px;\n        transform: rotate(45deg);\n        white-space: nowrap;\n        width: 16px;\n        z-index: -1;\n      }\n    }\n  }\n\n  .actions {\n    justify-content: flex-end;\n\n    button {\n      margin-inline-start: 10px;\n      margin-inline-end: 0;\n    }\n  }\n\n  @media (max-width: $break-point-medium) {\n    .fields-and-preview {\n      flex-direction: column;\n\n      .top-site-outer {\n        margin-inline-start: 0;\n      }\n    }\n  }\n\n  // prevent text selection of keyword label when clicking to select\n  .title {\n    -moz-user-select: none;\n  }\n\n  // CSS styled checkbox\n  [type='checkbox']:not(:checked),\n  [type='checkbox']:checked {\n    inset-inline-start: -9999px;\n    position: absolute;\n  }\n\n  [type='checkbox']:not(:checked) + label,\n  [type='checkbox']:checked + label {\n    cursor: pointer;\n    display: block;\n    position: relative;\n  }\n\n  $checkbox-offset: -8px;\n\n  [type='checkbox']:not(:checked) + label::before,\n  [type='checkbox']:checked + label::before {\n    background: var(--newtab-background-color);\n    border: $input-border;\n    border-radius: $border-radius;\n    content: '';\n    height: 21px;\n    left: $checkbox-offset;\n    position: absolute;\n    top: $checkbox-offset;\n    width: 21px;\n    z-index: 1;\n\n    [dir='rtl'] & {\n      left: auto;\n      right: $checkbox-offset;\n    }\n  }\n\n  // checkmark\n  [type='checkbox']:not(:checked) + label::after,\n  [type='checkbox']:checked + label::after {\n    background: url('chrome://global/skin/icons/check.svg') no-repeat center center; // sass-lint:disable-line no-url-domains\n    content: '';\n    height: 21px;\n    left: $checkbox-offset;\n    position: absolute;\n    top: $checkbox-offset;\n    width: 21px;\n    -moz-context-properties: fill;\n    fill: var(--newtab-link-primary-color);\n    z-index: 2;\n\n    [dir='rtl'] & {\n      left: auto;\n      right: $checkbox-offset;\n    }\n  }\n\n  // when selected, highlight the tile\n  [type='checkbox']:checked + label {\n    .tile {\n      box-shadow: 0 0 0 2px var(--newtab-link-primary-color);\n    }\n  }\n\n  // checkmark changes\n  [type='checkbox']:not(:checked) + label::after {\n    opacity: 0;\n  }\n\n  [type='checkbox']:checked + label::after {\n    opacity: 1;\n  }\n\n  // accessibility\n  [type='checkbox']:checked:focus + label::before,\n  [type='checkbox']:not(:checked):focus + label::before {\n    border: 1px dotted var(--newtab-link-primary-color);\n  }\n}\n\n//used for tooltips below form element\n@keyframes fade-up-tt {\n  0% {\n    opacity: 0;\n    transform: translateY(15px);\n  }\n\n  100% {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n"
  },
  {
    "path": "content-src/components/Topics/Topics.jsx",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport React from \"react\";\n\nexport class Topic extends React.PureComponent {\n  render() {\n    const { url, name } = this.props;\n    return (\n      <li>\n        <a key={name} href={url}>\n          {name}\n        </a>\n      </li>\n    );\n  }\n}\n\nexport class Topics extends React.PureComponent {\n  render() {\n    const { topics } = this.props;\n    return (\n      <span className=\"topics\">\n        <span data-l10n-id=\"newtab-pocket-read-more\" />\n        <ul>\n          {topics &&\n            topics.map(t => <Topic key={t.name} url={t.url} name={t.name} />)}\n        </ul>\n      </span>\n    );\n  }\n}\n"
  },
  {
    "path": "content-src/components/Topics/_Topics.scss",
    "content": ".topics {\n  ul {\n    margin: 0;\n    padding: 0;\n    @media (min-width: $break-point-large) {\n      display: inline;\n      padding-inline-start: 12px;\n    }\n  }\n\n  ul li {\n    display: inline-block;\n\n    &::after {\n      content: '•';\n      padding: 8px;\n    }\n\n    &:last-child::after {\n      content: none;\n    }\n  }\n}\n"
  },
  {
    "path": "content-src/lib/constants.js",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nexport const IS_NEWTAB =\n  global.document && global.document.documentURI === \"about:newtab\";\nexport const NEWTAB_DARK_THEME = {\n  ntp_background: {\n    r: 42,\n    g: 42,\n    b: 46,\n    a: 1,\n  },\n  ntp_text: {\n    r: 249,\n    g: 249,\n    b: 250,\n    a: 1,\n  },\n  sidebar: {\n    r: 56,\n    g: 56,\n    b: 61,\n    a: 1,\n  },\n  sidebar_text: {\n    r: 249,\n    g: 249,\n    b: 250,\n    a: 1,\n  },\n};\n"
  },
  {
    "path": "content-src/lib/detect-user-session-start.js",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport { actionCreators as ac, actionTypes as at } from \"common/Actions.jsm\";\nimport { perfService as perfSvc } from \"common/PerfService.jsm\";\n\nconst VISIBLE = \"visible\";\nconst VISIBILITY_CHANGE_EVENT = \"visibilitychange\";\n\nexport class DetectUserSessionStart {\n  constructor(store, options = {}) {\n    this._store = store;\n    // Overrides for testing\n    this.document = options.document || global.document;\n    this._perfService = options.perfService || perfSvc;\n    this._onVisibilityChange = this._onVisibilityChange.bind(this);\n  }\n\n  /**\n   * sendEventOrAddListener - Notify immediately if the page is already visible,\n   *                    or else set up a listener for when visibility changes.\n   *                    This is needed for accurate session tracking for telemetry,\n   *                    because tabs are pre-loaded.\n   */\n  sendEventOrAddListener() {\n    if (this.document.visibilityState === VISIBLE) {\n      // If the document is already visible, to the user, send a notification\n      // immediately that a session has started.\n      this._sendEvent();\n    } else {\n      // If the document is not visible, listen for when it does become visible.\n      this.document.addEventListener(\n        VISIBILITY_CHANGE_EVENT,\n        this._onVisibilityChange\n      );\n    }\n  }\n\n  /**\n   * _sendEvent - Sends a message to the main process to indicate the current\n   *              tab is now visible to the user, includes the\n   *              visibility_event_rcvd_ts time in ms from the UNIX epoch.\n   */\n  _sendEvent() {\n    this._perfService.mark(\"visibility_event_rcvd_ts\");\n\n    try {\n      let visibility_event_rcvd_ts = this._perfService.getMostRecentAbsMarkStartByName(\n        \"visibility_event_rcvd_ts\"\n      );\n\n      this._store.dispatch(\n        ac.AlsoToMain({\n          type: at.SAVE_SESSION_PERF_DATA,\n          data: { visibility_event_rcvd_ts },\n        })\n      );\n    } catch (ex) {\n      // If this failed, it's likely because the `privacy.resistFingerprinting`\n      // pref is true.  We should at least not blow up.\n    }\n  }\n\n  /**\n   * _onVisibilityChange - If the visibility has changed to visible, sends a notification\n   *                      and removes the event listener. This should only be called once per tab.\n   */\n  _onVisibilityChange() {\n    if (this.document.visibilityState === VISIBLE) {\n      this._sendEvent();\n      this.document.removeEventListener(\n        VISIBILITY_CHANGE_EVENT,\n        this._onVisibilityChange\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "content-src/lib/init-store.js",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\n/* eslint-env mozilla/frame-script */\n\nimport {\n  actionCreators as ac,\n  actionTypes as at,\n  actionUtils as au,\n} from \"common/Actions.jsm\";\nimport { applyMiddleware, combineReducers, createStore } from \"redux\";\n\nexport const MERGE_STORE_ACTION = \"NEW_TAB_INITIAL_STATE\";\nexport const OUTGOING_MESSAGE_NAME = \"ActivityStream:ContentToMain\";\nexport const INCOMING_MESSAGE_NAME = \"ActivityStream:MainToContent\";\nexport const EARLY_QUEUED_ACTIONS = [at.SAVE_SESSION_PERF_DATA];\n\n/**\n * A higher-order function which returns a reducer that, on MERGE_STORE action,\n * will return the action.data object merged into the previous state.\n *\n * For all other actions, it merely calls mainReducer.\n *\n * Because we want this to merge the entire state object, it's written as a\n * higher order function which takes the main reducer (itself often a call to\n * combineReducers) as a parameter.\n *\n * @param  {function} mainReducer reducer to call if action != MERGE_STORE_ACTION\n * @return {function}             a reducer that, on MERGE_STORE_ACTION action,\n *                                will return the action.data object merged\n *                                into the previous state, and the result\n *                                of calling mainReducer otherwise.\n */\nfunction mergeStateReducer(mainReducer) {\n  return (prevState, action) => {\n    if (action.type === MERGE_STORE_ACTION) {\n      return { ...prevState, ...action.data };\n    }\n\n    return mainReducer(prevState, action);\n  };\n}\n\n/**\n * messageMiddleware - Middleware that looks for SentToMain type actions, and sends them if necessary\n */\nconst messageMiddleware = store => next => action => {\n  const skipLocal = action.meta && action.meta.skipLocal;\n  if (au.isSendToMain(action)) {\n    RPMSendAsyncMessage(OUTGOING_MESSAGE_NAME, action);\n  }\n  if (!skipLocal) {\n    next(action);\n  }\n};\n\nexport const rehydrationMiddleware = store => next => action => {\n  if (store._didRehydrate) {\n    return next(action);\n  }\n\n  const isMergeStoreAction = action.type === MERGE_STORE_ACTION;\n  const isRehydrationRequest = action.type === at.NEW_TAB_STATE_REQUEST;\n\n  if (isRehydrationRequest) {\n    store._didRequestInitialState = true;\n    return next(action);\n  }\n\n  if (isMergeStoreAction) {\n    store._didRehydrate = true;\n    return next(action);\n  }\n\n  // If init happened after our request was made, we need to re-request\n  if (store._didRequestInitialState && action.type === at.INIT) {\n    return next(ac.AlsoToMain({ type: at.NEW_TAB_STATE_REQUEST }));\n  }\n\n  if (\n    au.isBroadcastToContent(action) ||\n    au.isSendToOneContent(action) ||\n    au.isSendToPreloaded(action)\n  ) {\n    // Note that actions received before didRehydrate will not be dispatched\n    // because this could negatively affect preloading and the the state\n    // will be replaced by rehydration anyway.\n    return null;\n  }\n\n  return next(action);\n};\n\n/**\n * This middleware queues up all the EARLY_QUEUED_ACTIONS until it receives\n * the first action from main. This is useful for those actions for main which\n * require higher reliability, i.e. the action will not be lost in the case\n * that it gets sent before the main is ready to receive it. Conversely, any\n * actions allowed early are accepted to be ignorable or re-sendable.\n */\nexport const queueEarlyMessageMiddleware = store => next => action => {\n  if (store._receivedFromMain) {\n    next(action);\n  } else if (au.isFromMain(action)) {\n    next(action);\n    store._receivedFromMain = true;\n    // Sending out all the early actions as main is ready now\n    if (store._earlyActionQueue) {\n      store._earlyActionQueue.forEach(next);\n      store._earlyActionQueue = [];\n    }\n  } else if (EARLY_QUEUED_ACTIONS.includes(action.type)) {\n    store._earlyActionQueue = store._earlyActionQueue || [];\n    store._earlyActionQueue.push(action);\n  } else {\n    // Let any other type of action go through\n    next(action);\n  }\n};\n\n/**\n * initStore - Create a store and listen for incoming actions\n *\n * @param  {object} reducers An object containing Redux reducers\n * @param  {object} intialState (optional) The initial state of the store, if desired\n * @return {object}          A redux store\n */\nexport function initStore(reducers) {\n  const store = createStore(\n    mergeStateReducer(combineReducers(reducers)),\n    global.RPMAddMessageListener &&\n      applyMiddleware(\n        rehydrationMiddleware,\n        queueEarlyMessageMiddleware,\n        messageMiddleware\n      )\n  );\n\n  store._didRehydrate = false;\n  store._didRequestInitialState = false;\n\n  if (global.RPMAddMessageListener) {\n    global.RPMAddMessageListener(INCOMING_MESSAGE_NAME, msg => {\n      try {\n        store.dispatch(msg.data);\n      } catch (ex) {\n        console.error(\"Content msg:\", msg, \"Dispatch error: \", ex); // eslint-disable-line no-console\n        dump(\n          `Content msg: ${JSON.stringify(msg)}\\nDispatch error: ${ex}\\n${\n            ex.stack\n          }`\n        );\n      }\n    });\n  }\n\n  return store;\n}\n"
  },
  {
    "path": "content-src/lib/link-menu-options.js",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport { actionCreators as ac, actionTypes as at } from \"common/Actions.jsm\";\n\nconst _OpenInPrivateWindow = site => ({\n  id: \"newtab-menu-open-new-private-window\",\n  icon: \"new-window-private\",\n  action: ac.OnlyToMain({\n    type: at.OPEN_PRIVATE_WINDOW,\n    data: { url: site.url, referrer: site.referrer },\n  }),\n  userEvent: \"OPEN_PRIVATE_WINDOW\",\n});\n\n/**\n * List of functions that return items that can be included as menu options in a\n * LinkMenu. All functions take the site as the first parameter, and optionally\n * the index of the site.\n */\nexport const LinkMenuOptions = {\n  Separator: () => ({ type: \"separator\" }),\n  EmptyItem: () => ({ type: \"empty\" }),\n  ShowPrivacyInfo: site => ({\n    id: \"newtab-menu-show-privacy-info\",\n    icon: \"info\",\n    action: {\n      type: at.SHOW_PRIVACY_INFO,\n    },\n    userEvent: \"SHOW_PRIVACY_INFO\",\n  }),\n  RemoveBookmark: site => ({\n    id: \"newtab-menu-remove-bookmark\",\n    icon: \"bookmark-added\",\n    action: ac.AlsoToMain({\n      type: at.DELETE_BOOKMARK_BY_ID,\n      data: site.bookmarkGuid,\n    }),\n    userEvent: \"BOOKMARK_DELETE\",\n  }),\n  AddBookmark: site => ({\n    id: \"newtab-menu-bookmark\",\n    icon: \"bookmark-hollow\",\n    action: ac.AlsoToMain({\n      type: at.BOOKMARK_URL,\n      data: { url: site.url, title: site.title, type: site.type },\n    }),\n    userEvent: \"BOOKMARK_ADD\",\n  }),\n  OpenInNewWindow: site => ({\n    id: \"newtab-menu-open-new-window\",\n    icon: \"new-window\",\n    action: ac.AlsoToMain({\n      type: at.OPEN_NEW_WINDOW,\n      data: {\n        referrer: site.referrer,\n        typedBonus: site.typedBonus,\n        url: site.url,\n      },\n    }),\n    userEvent: \"OPEN_NEW_WINDOW\",\n  }),\n  // This blocks the url for regular stories,\n  // but also sends a message to DiscoveryStream with flight_id.\n  // If DiscoveryStream sees this message for a flight_id\n  // it also blocks it on the flight_id.\n  BlockUrl: (site, index, eventSource) => ({\n    id: \"newtab-menu-dismiss\",\n    icon: \"dismiss\",\n    action: ac.AlsoToMain({\n      type: at.BLOCK_URL,\n      data: {\n        url: site.open_url || site.url,\n        pocket_id: site.pocket_id,\n        ...(site.flight_id ? { flight_id: site.flight_id } : {}),\n      },\n    }),\n    impression: ac.ImpressionStats({\n      source: eventSource,\n      block: 0,\n      tiles: [\n        {\n          id: site.guid,\n          pos: index,\n          ...(site.shim && site.shim.delete ? { shim: site.shim.delete } : {}),\n        },\n      ],\n    }),\n    userEvent: \"BLOCK\",\n  }),\n\n  // This is an option for web extentions which will result in remove items from\n  // memory and notify the web extenion, rather than using the built-in block list.\n  WebExtDismiss: (site, index, eventSource) => ({\n    id: \"menu_action_webext_dismiss\",\n    string_id: \"newtab-menu-dismiss\",\n    icon: \"dismiss\",\n    action: ac.WebExtEvent(at.WEBEXT_DISMISS, {\n      source: eventSource,\n      url: site.url,\n      action_position: index,\n    }),\n  }),\n  DeleteUrl: (site, index, eventSource, isEnabled, siteInfo) => ({\n    id: \"newtab-menu-delete-history\",\n    icon: \"delete\",\n    action: {\n      type: at.DIALOG_OPEN,\n      data: {\n        onConfirm: [\n          ac.AlsoToMain({\n            type: at.DELETE_HISTORY_URL,\n            data: {\n              url: site.url,\n              pocket_id: site.pocket_id,\n              forceBlock: site.bookmarkGuid,\n            },\n          }),\n          ac.UserEvent(\n            Object.assign(\n              { event: \"DELETE\", source: eventSource, action_position: index },\n              siteInfo\n            )\n          ),\n        ],\n        eventSource,\n        body_string_id: [\n          \"newtab-confirm-delete-history-p1\",\n          \"newtab-confirm-delete-history-p2\",\n        ],\n        confirm_button_string_id: \"newtab-topsites-delete-history-button\",\n        cancel_button_string_id: \"newtab-topsites-cancel-button\",\n        icon: \"modal-delete\",\n      },\n    },\n    userEvent: \"DIALOG_OPEN\",\n  }),\n  ShowFile: site => ({\n    id: \"newtab-menu-show-file\",\n    icon: \"search\",\n    action: ac.OnlyToMain({\n      type: at.SHOW_DOWNLOAD_FILE,\n      data: { url: site.url },\n    }),\n  }),\n  OpenFile: site => ({\n    id: \"newtab-menu-open-file\",\n    icon: \"open-file\",\n    action: ac.OnlyToMain({\n      type: at.OPEN_DOWNLOAD_FILE,\n      data: { url: site.url },\n    }),\n  }),\n  CopyDownloadLink: site => ({\n    id: \"newtab-menu-copy-download-link\",\n    icon: \"copy\",\n    action: ac.OnlyToMain({\n      type: at.COPY_DOWNLOAD_LINK,\n      data: { url: site.url },\n    }),\n  }),\n  GoToDownloadPage: site => ({\n    id: \"newtab-menu-go-to-download-page\",\n    icon: \"download\",\n    action: ac.OnlyToMain({\n      type: at.OPEN_LINK,\n      data: { url: site.referrer },\n    }),\n    disabled: !site.referrer,\n  }),\n  RemoveDownload: site => ({\n    id: \"newtab-menu-remove-download\",\n    icon: \"delete\",\n    action: ac.OnlyToMain({\n      type: at.REMOVE_DOWNLOAD_FILE,\n      data: { url: site.url },\n    }),\n  }),\n  PinSpocTopSite: (site, index) => ({\n    id: \"newtab-menu-pin\",\n    icon: \"pin\",\n    action: ac.AlsoToMain({\n      type: at.TOP_SITES_PIN,\n      data: {\n        site,\n        index,\n      },\n    }),\n    userEvent: \"PIN\",\n  }),\n  PinTopSite: ({ url, searchTopSite, label }, index) => ({\n    id: \"newtab-menu-pin\",\n    icon: \"pin\",\n    action: ac.AlsoToMain({\n      type: at.TOP_SITES_PIN,\n      data: {\n        site: {\n          url,\n          ...(searchTopSite && { searchTopSite, label }),\n        },\n        index,\n      },\n    }),\n    userEvent: \"PIN\",\n  }),\n  UnpinTopSite: site => ({\n    id: \"newtab-menu-unpin\",\n    icon: \"unpin\",\n    action: ac.AlsoToMain({\n      type: at.TOP_SITES_UNPIN,\n      data: { site: { url: site.url } },\n    }),\n    userEvent: \"UNPIN\",\n  }),\n  SaveToPocket: (site, index, eventSource) => ({\n    id: \"newtab-menu-save-to-pocket\",\n    icon: \"pocket-save\",\n    action: ac.AlsoToMain({\n      type: at.SAVE_TO_POCKET,\n      data: { site: { url: site.url, title: site.title } },\n    }),\n    impression: ac.ImpressionStats({\n      source: eventSource,\n      pocket: 0,\n      tiles: [\n        {\n          id: site.guid,\n          pos: index,\n          ...(site.shim && site.shim.save ? { shim: site.shim.save } : {}),\n        },\n      ],\n    }),\n    userEvent: \"SAVE_TO_POCKET\",\n  }),\n  DeleteFromPocket: site => ({\n    id: \"newtab-menu-delete-pocket\",\n    icon: \"pocket-delete\",\n    action: ac.AlsoToMain({\n      type: at.DELETE_FROM_POCKET,\n      data: { pocket_id: site.pocket_id },\n    }),\n    userEvent: \"DELETE_FROM_POCKET\",\n  }),\n  ArchiveFromPocket: site => ({\n    id: \"newtab-menu-archive-pocket\",\n    icon: \"pocket-archive\",\n    action: ac.AlsoToMain({\n      type: at.ARCHIVE_FROM_POCKET,\n      data: { pocket_id: site.pocket_id },\n    }),\n    userEvent: \"ARCHIVE_FROM_POCKET\",\n  }),\n  EditTopSite: (site, index) => ({\n    id: \"newtab-menu-edit-topsites\",\n    icon: \"edit\",\n    action: {\n      type: at.TOP_SITES_EDIT,\n      data: { index },\n    },\n  }),\n  CheckBookmark: site =>\n    site.bookmarkGuid\n      ? LinkMenuOptions.RemoveBookmark(site)\n      : LinkMenuOptions.AddBookmark(site),\n  CheckPinTopSite: (site, index) =>\n    site.isPinned\n      ? LinkMenuOptions.UnpinTopSite(site)\n      : LinkMenuOptions.PinTopSite(site, index),\n  CheckSavedToPocket: (site, index) =>\n    site.pocket_id\n      ? LinkMenuOptions.DeleteFromPocket(site)\n      : LinkMenuOptions.SaveToPocket(site, index),\n  CheckBookmarkOrArchive: site =>\n    site.pocket_id\n      ? LinkMenuOptions.ArchiveFromPocket(site)\n      : LinkMenuOptions.CheckBookmark(site),\n  OpenInPrivateWindow: (site, index, eventSource, isEnabled) =>\n    isEnabled ? _OpenInPrivateWindow(site) : LinkMenuOptions.EmptyItem(),\n};\n"
  },
  {
    "path": "content-src/lib/screenshot-utils.js",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\n/**\n * List of helper functions for screenshot-based images.\n *\n * There are two kinds of images:\n * 1. Remote Image: This is the image from the main process and it refers to\n *    the image in the React props. This can either be an object with the `data`\n *    and `path` properties, if it is a blob, or a string, if it is a normal image.\n * 2. Local Image: This is the image object in the content process and it refers\n *    to the image *object* in the React component's state. All local image\n *    objects have the `url` property, and an additional property `path`, if they\n *    are blobs.\n */\nexport const ScreenshotUtils = {\n  isBlob(isLocal, image) {\n    return !!(\n      image &&\n      image.path &&\n      ((!isLocal && image.data) || (isLocal && image.url))\n    );\n  },\n\n  // This should always be called with a remote image and not a local image.\n  createLocalImageObject(remoteImage) {\n    if (!remoteImage) {\n      return null;\n    }\n    if (this.isBlob(false, remoteImage)) {\n      return {\n        url: global.URL.createObjectURL(remoteImage.data),\n        path: remoteImage.path,\n      };\n    }\n    return { url: remoteImage };\n  },\n\n  // Revokes the object URL of the image if the local image is a blob.\n  // This should always be called with a local image and not a remote image.\n  maybeRevokeBlobObjectURL(localImage) {\n    if (this.isBlob(true, localImage)) {\n      global.URL.revokeObjectURL(localImage.url);\n    }\n  },\n\n  // Checks if remoteImage and localImage are the same.\n  isRemoteImageLocal(localImage, remoteImage) {\n    // Both remoteImage and localImage are present.\n    if (remoteImage && localImage) {\n      return this.isBlob(false, remoteImage)\n        ? localImage.path === remoteImage.path\n        : localImage.url === remoteImage;\n    }\n\n    // This will only handle the remaining three possible outcomes.\n    // (i.e. everything except when both image and localImage are present)\n    return !remoteImage && !localImage;\n  },\n};\n"
  },
  {
    "path": "content-src/lib/section-menu-options.js",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nimport { actionCreators as ac, actionTypes as at } from \"common/Actions.jsm\";\n\n/**\n * List of functions that return items that can be included as menu options in a\n * SectionMenu. All functions take the section as the only parameter.\n */\nexport const SectionMenuOptions = {\n  Separator: () => ({ type: \"separator\" }),\n  MoveUp: section => ({\n    id: \"newtab-section-menu-move-up\",\n    icon: \"arrowhead-up\",\n    action: ac.OnlyToMain({\n      type: at.SECTION_MOVE,\n      data: { id: section.id, direction: -1 },\n    }),\n    userEvent: \"MENU_MOVE_UP\",\n    disabled: !!section.isFirst,\n  }),\n  MoveDown: section => ({\n    id: \"newtab-section-menu-move-down\",\n    icon: \"arrowhead-down\",\n    action: ac.OnlyToMain({\n      type: at.SECTION_MOVE,\n      data: { id: section.id, direction: +1 },\n    }),\n    userEvent: \"MENU_MOVE_DOWN\",\n    disabled: !!section.isLast,\n  }),\n  RemoveSection: section => ({\n    id: \"newtab-section-menu-remove-section\",\n    icon: \"dismiss\",\n    action: ac.SetPref(section.showPrefName, false),\n    userEvent: \"MENU_REMOVE\",\n  }),\n  CollapseSection: section => ({\n    id: \"newtab-section-menu-collapse-section\",\n    icon: \"minimize\",\n    action: ac.OnlyToMain({\n      type: at.UPDATE_SECTION_PREFS,\n      data: { id: section.id, value: { collapsed: true } },\n    }),\n    userEvent: \"MENU_COLLAPSE\",\n  }),\n  ExpandSection: section => ({\n    id: \"newtab-section-menu-expand-section\",\n    icon: \"maximize\",\n    action: ac.OnlyToMain({\n      type: at.UPDATE_SECTION_PREFS,\n      data: { id: section.id, value: { collapsed: false } },\n    }),\n    userEvent: \"MENU_EXPAND\",\n  }),\n  ManageSection: section => ({\n    id: \"newtab-section-menu-manage-section\",\n    icon: \"settings\",\n    action: ac.OnlyToMain({ type: at.SETTINGS_OPEN }),\n    userEvent: \"MENU_MANAGE\",\n  }),\n  ManageWebExtension: section => ({\n    id: \"newtab-section-menu-manage-webext\",\n    icon: \"settings\",\n    action: ac.OnlyToMain({ type: at.OPEN_WEBEXT_SETTINGS, data: section.id }),\n  }),\n  AddTopSite: section => ({\n    id: \"newtab-section-menu-add-topsite\",\n    icon: \"add\",\n    action: { type: at.TOP_SITES_EDIT, data: { index: -1 } },\n    userEvent: \"MENU_ADD_TOPSITE\",\n  }),\n  AddSearchShortcut: section => ({\n    id: \"newtab-section-menu-add-search-engine\",\n    icon: \"search\",\n    action: { type: at.TOP_SITES_OPEN_SEARCH_SHORTCUTS_MODAL },\n    userEvent: \"MENU_ADD_SEARCH\",\n  }),\n  PrivacyNotice: section => ({\n    id: \"newtab-section-menu-privacy-notice\",\n    icon: \"info\",\n    action: ac.OnlyToMain({\n      type: at.OPEN_LINK,\n      data: { url: section.privacyNoticeURL },\n    }),\n    userEvent: \"MENU_PRIVACY_NOTICE\",\n  }),\n  CheckCollapsed: section =>\n    section.collapsed\n      ? SectionMenuOptions.ExpandSection(section)\n      : SectionMenuOptions.CollapseSection(section),\n};\n"
  },
  {
    "path": "content-src/lib/selectLayoutRender.js",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nexport const selectLayoutRender = ({\n  state = {},\n  prefs = {},\n  rollCache = [],\n  lang = \"\",\n}) => {\n  const { layout, feeds, spocs } = state;\n  let spocIndexMap = {};\n  let bufferRollCache = [];\n  // Records the chosen and unchosen spocs by the probability selection.\n  let chosenSpocs = new Set();\n  let unchosenSpocs = new Set();\n\n  function rollForSpocs(data, spocsConfig, spocsData, placementName) {\n    if (!spocIndexMap[placementName] && spocIndexMap[placementName] !== 0) {\n      spocIndexMap[placementName] = 0;\n    }\n    const results = [...data];\n    for (let position of spocsConfig.positions) {\n      const spoc = spocsData[spocIndexMap[placementName]];\n      if (!spoc) {\n        break;\n      }\n\n      // Cache random number for a position\n      let rickRoll;\n      if (!rollCache.length) {\n        rickRoll = Math.random();\n        bufferRollCache.push(rickRoll);\n      } else {\n        rickRoll = rollCache.shift();\n        bufferRollCache.push(rickRoll);\n      }\n\n      if (rickRoll <= spocsConfig.probability) {\n        spocIndexMap[placementName]++;\n        if (!spocs.blocked.includes(spoc.url)) {\n          results.splice(position.index, 0, spoc);\n          chosenSpocs.add(spoc);\n        }\n      } else {\n        unchosenSpocs.add(spoc);\n      }\n    }\n\n    return results;\n  }\n\n  const positions = {};\n  const DS_COMPONENTS = [\n    \"Message\",\n    \"TextPromo\",\n    \"SectionTitle\",\n    \"Navigation\",\n    \"CardGrid\",\n    \"Hero\",\n    \"HorizontalRule\",\n    \"List\",\n  ];\n\n  const filterArray = [];\n\n  if (!prefs[\"feeds.topsites\"]) {\n    filterArray.push(\"TopSites\");\n  }\n\n  if (!lang.startsWith(\"en-\")) {\n    filterArray.push(\"Navigation\");\n  }\n\n  if (!prefs[\"feeds.section.topstories\"]) {\n    filterArray.push(...DS_COMPONENTS);\n  }\n\n  const placeholderComponent = component => {\n    if (!component.feed) {\n      // TODO we now need a placeholder for topsites and textPromo.\n      return {\n        ...component,\n        data: {\n          spocs: [],\n        },\n      };\n    }\n    const data = {\n      recommendations: [],\n    };\n\n    let items = 0;\n    if (component.properties && component.properties.items) {\n      items = component.properties.items;\n    }\n    for (let i = 0; i < items; i++) {\n      data.recommendations.push({ placeholder: true });\n    }\n\n    return { ...component, data };\n  };\n\n  // TODO update devtools to show placements\n  const handleSpocs = (data, component) => {\n    let result = [...data];\n    // Do we ever expect to possibly have a spoc.\n    if (\n      component.spocs &&\n      component.spocs.positions &&\n      component.spocs.positions.length\n    ) {\n      const placement = component.placement || {};\n      const placementName = placement.name || \"spocs\";\n      const spocsData = spocs.data[placementName];\n      // We expect a spoc, spocs are loaded, and the server returned spocs.\n      if (spocs.loaded && spocsData && spocsData.length) {\n        result = rollForSpocs(\n          result,\n          component.spocs,\n          spocsData,\n          placementName\n        );\n      }\n    }\n    return result;\n  };\n\n  const handleComponent = component => {\n    return {\n      ...component,\n      data: {\n        spocs: handleSpocs([], component),\n      },\n    };\n  };\n\n  const handleComponentWithFeed = component => {\n    positions[component.type] = positions[component.type] || 0;\n    let data = {\n      recommendations: [],\n    };\n\n    const feed = feeds.data[component.feed.url];\n    if (feed && feed.data) {\n      data = {\n        ...feed.data,\n        recommendations: [...(feed.data.recommendations || [])],\n      };\n    }\n\n    if (component && component.properties && component.properties.offset) {\n      data = {\n        ...data,\n        recommendations: data.recommendations.slice(\n          component.properties.offset\n        ),\n      };\n    }\n\n    data = {\n      ...data,\n      recommendations: handleSpocs(data.recommendations, component),\n    };\n\n    let items = 0;\n    if (component.properties && component.properties.items) {\n      items = Math.min(component.properties.items, data.recommendations.length);\n    }\n\n    // loop through a component items\n    // Store the items position sequentially for multiple components of the same type.\n    // Example: A second card grid starts pos offset from the last card grid.\n    for (let i = 0; i < items; i++) {\n      data.recommendations[i] = {\n        ...data.recommendations[i],\n        pos: positions[component.type]++,\n      };\n    }\n\n    return { ...component, data };\n  };\n\n  const renderLayout = () => {\n    const renderedLayoutArray = [];\n    for (const row of layout.filter(\n      r => r.components.filter(c => !filterArray.includes(c.type)).length\n    )) {\n      let components = [];\n      renderedLayoutArray.push({\n        ...row,\n        components,\n      });\n      for (const component of row.components.filter(\n        c => !filterArray.includes(c.type)\n      )) {\n        const spocsConfig = component.spocs;\n        if (spocsConfig || component.feed) {\n          // TODO make sure this still works for different loading cases.\n          if (\n            (component.feed && !feeds.data[component.feed.url]) ||\n            (spocsConfig &&\n              spocsConfig.positions &&\n              spocsConfig.positions.length &&\n              !spocs.loaded)\n          ) {\n            components.push(placeholderComponent(component));\n            return renderedLayoutArray;\n          }\n          if (component.feed) {\n            components.push(handleComponentWithFeed(component));\n          } else {\n            components.push(handleComponent(component));\n          }\n        } else {\n          components.push(component);\n        }\n      }\n    }\n    return renderedLayoutArray;\n  };\n\n  const layoutRender = renderLayout();\n\n  // If empty, fill rollCache with random probability values from bufferRollCache\n  if (!rollCache.length) {\n    rollCache.push(...bufferRollCache);\n  }\n\n  // Generate the payload for the SPOCS Fill ping. Note that a SPOC could be rejected\n  // by the `probability_selection` first, then gets chosen for the next position. For\n  // all other SPOCS that never went through the probabilistic selection, its reason will\n  // be \"out_of_position\".\n  let spocsFill = [];\n  if (spocs.loaded && feeds.loaded && spocs.data.spocs) {\n    const chosenSpocsFill = [...chosenSpocs].map(spoc => ({\n      id: spoc.id,\n      reason: \"n/a\",\n      displayed: 1,\n      full_recalc: 0,\n    }));\n    const unchosenSpocsFill = [...unchosenSpocs]\n      .filter(spoc => !chosenSpocs.has(spoc))\n      .map(spoc => ({\n        id: spoc.id,\n        reason: \"probability_selection\",\n        displayed: 0,\n        full_recalc: 0,\n      }));\n    const outOfPositionSpocsFill = spocs.data.spocs\n      .slice(spocIndexMap.spocs)\n      .filter(spoc => !unchosenSpocs.has(spoc))\n      .map(spoc => ({\n        id: spoc.id,\n        reason: \"out_of_position\",\n        displayed: 0,\n        full_recalc: 0,\n      }));\n\n    spocsFill = [\n      ...chosenSpocsFill,\n      ...unchosenSpocsFill,\n      ...outOfPositionSpocsFill,\n    ];\n  }\n\n  return { spocsFill, layoutRender };\n};\n"
  },
  {
    "path": "content-src/styles/_activity-stream.scss",
    "content": "@import './normalize';\n@import './variables';\n@import './theme';\n@import './icons';\n@import './mixins';\n\nhtml {\n  height: 100%;\n}\n\nbody,\n#root { // sass-lint:disable-line no-ids\n  min-height: 100vh;\n}\n\n#root { // sass-lint:disable-line no-ids\n  position: relative;\n}\n\nbody {\n  background-color: var(--newtab-background-color);\n  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Ubuntu', 'Helvetica Neue', sans-serif;\n  font-size: 16px;\n}\n\n.no-scroll {\n  overflow: hidden;\n}\n\nh1,\nh2 {\n  font-weight: normal;\n}\n\na {\n  text-decoration: none;\n}\n\n.inner-border {\n  border: $border-secondary;\n  border-radius: $border-radius;\n  height: 100%;\n  left: 0;\n  pointer-events: none;\n  position: absolute;\n  top: 0;\n  width: 100%;\n  z-index: 100;\n}\n\n@keyframes fadeIn {\n  from {\n    opacity: 0;\n  }\n\n  to {\n    opacity: 1;\n  }\n}\n\n.show-on-init {\n  opacity: 0;\n  transition: opacity 0.2s ease-in;\n\n  &.on {\n    animation: fadeIn 0.2s;\n    opacity: 1;\n  }\n}\n\n.actions {\n  border-top: $border-secondary;\n  display: flex;\n  flex-direction: row;\n  flex-wrap: wrap;\n  justify-content: flex-start;\n  margin: 0;\n  padding: 15px 25px 0;\n}\n\n// Default button (grey)\n.button,\n.actions button {\n  background-color: var(--newtab-button-secondary-color);\n  border: $border-primary;\n  border-radius: 4px;\n  color: inherit;\n  cursor: pointer;\n  margin-bottom: 15px;\n  padding: 10px 30px;\n  white-space: nowrap;\n\n  &:hover:not(.dismiss),\n  &:focus:not(.dismiss) {\n    box-shadow: $shadow-primary;\n    transition: box-shadow 150ms;\n  }\n\n  &.dismiss {\n    background-color: transparent;\n    border: 0;\n    padding: 0;\n    text-decoration: underline;\n  }\n\n  // Blue button\n  &.primary,\n  &.done {\n    background-color: var(--newtab-button-primary-color);\n    border: solid 1px var(--newtab-button-primary-color);\n    color: $white;\n    margin-inline-start: auto;\n  }\n}\n\ninput {\n  &[type='text'],\n  &[type='search'] {\n    border-radius: $border-radius;\n  }\n}\n\n// These styles are needed for -webkit-line-clamp to work correctly, so reuse\n// this class name while separately setting a clamp value via CSS or JS.\n.clamp {\n  -webkit-box-orient: vertical;\n  display: -webkit-box;\n  overflow: hidden;\n  word-break: break-word;\n}\n\n// Components\n@import '../components/A11yLinkButton/A11yLinkButton';\n@import '../components/Base/Base';\n@import '../components/ErrorBoundary/ErrorBoundary';\n@import '../components/TopSites/TopSites';\n@import '../components/Sections/Sections';\n@import '../components/Topics/Topics';\n@import '../components/Search/Search';\n@import '../components/ContextMenu/ContextMenu';\n@import '../components/ConfirmDialog/ConfirmDialog';\n@import '../components/Card/Card';\n@import '../components/CollapsibleSection/CollapsibleSection';\n@import '../components/ASRouterAdmin/ASRouterAdmin';\n@import '../components/PocketLoggedInCta/PocketLoggedInCta';\n@import '../components/MoreRecommendations/MoreRecommendations';\n@import '../components/DiscoveryStreamBase/DiscoveryStreamBase';\n\n// Discovery Stream Components\n@import '../components/DiscoveryStreamComponents/CardGrid/CardGrid';\n@import '../components/DiscoveryStreamComponents/Hero/Hero';\n@import '../components/DiscoveryStreamComponents/Highlights/Highlights';\n@import '../components/DiscoveryStreamComponents/HorizontalRule/HorizontalRule';\n@import '../components/DiscoveryStreamComponents/List/List';\n@import '../components/DiscoveryStreamComponents/Navigation/Navigation';\n@import '../components/DiscoveryStreamComponents/SectionTitle/SectionTitle';\n@import '../components/DiscoveryStreamComponents/TopSites/TopSites';\n@import '../components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu';\n@import '../components/DiscoveryStreamComponents/DSCard/DSCard';\n@import '../components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter';\n@import '../components/DiscoveryStreamComponents/DSImage/DSImage';\n@import '../components/DiscoveryStreamComponents/DSDismiss/DSDismiss';\n@import '../components/DiscoveryStreamComponents/DSMessage/DSMessage';\n@import '../components/DiscoveryStreamImpressionStats/ImpressionStats';\n@import '../components/DiscoveryStreamComponents/DSEmptyState/DSEmptyState';\n@import '../components/DiscoveryStreamComponents/DSTextPromo/DSTextPromo';\n@import '../components/DiscoveryStreamComponents/DSPrivacyModal/DSPrivacyModal';\n\n// AS Router\n@import '../asrouter/components/Button/Button';\n@import '../asrouter/components/SnippetBase/SnippetBase';\n@import '../asrouter/components/ModalOverlay/ModalOverlay';\n@import '../asrouter/templates/ReturnToAMO/ReturnToAMO';\n@import '../asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet';\n@import '../asrouter/templates/SimpleSnippet/SimpleSnippet';\n@import '../asrouter/templates/SubmitFormSnippet/SubmitFormSnippet';\n@import '../asrouter/templates/OnboardingMessage/OnboardingMessage';\n@import '../asrouter/templates/EOYSnippet/EOYSnippet';\n@import '../asrouter/components/FxASignupForm/FxASignupForm';\n@import '../asrouter/templates/Trailhead/Trailhead';\n@import '../asrouter/templates/FullPageInterrupt/FullPageInterrupt';\n"
  },
  {
    "path": "content-src/styles/_icons.scss",
    "content": ".icon {\n  background-position: center center;\n  background-repeat: no-repeat;\n  background-size: $icon-size;\n  -moz-context-properties: fill;\n  display: inline-block;\n  fill: var(--newtab-icon-primary-color);\n  height: $icon-size;\n  vertical-align: middle;\n  width: $icon-size;\n\n  // helper classes\n  &.icon-spacer {\n    margin-inline-end: 8px;\n  }\n\n  &.icon-small-spacer {\n    margin-inline-end: 6px;\n  }\n\n  &.icon-button-style {\n    fill: var(--newtab-icon-secondary-color);\n    border: 0;\n\n    &:focus,\n    &:hover {\n      fill: var(--newtab-text-primary-color);\n    }\n  }\n\n  // icon images\n  &.icon-bookmark-added {\n    background-image: url('chrome://browser/skin/bookmark.svg');\n  }\n\n  &.icon-bookmark-hollow {\n    background-image: url('chrome://browser/skin/bookmark-hollow.svg');\n  }\n\n  &.icon-clear-input {\n    background-image: url('#{$image-path}glyph-cancel-16.svg');\n  }\n\n  &.icon-delete {\n    background-image: url('#{$image-path}glyph-delete-16.svg');\n  }\n\n  &.icon-search {\n    background-image: url('chrome://browser/skin/search-glass.svg');\n  }\n\n  &.icon-modal-delete {\n    flex-shrink: 0;\n    background-image: url('#{$image-path}glyph-modal-delete-32.svg');\n    background-size: $larger-icon-size;\n    height: $larger-icon-size;\n    width: $larger-icon-size;\n  }\n\n  &.icon-dismiss {\n    background-image: url('#{$image-path}glyph-dismiss-16.svg');\n  }\n\n  &.icon-info {\n    background-image: url('#{$image-path}glyph-info-16.svg');\n  }\n\n  &.icon-new-window {\n    @include flip-icon;\n    background-image: url('#{$image-path}glyph-newWindow-16.svg');\n  }\n\n  &.icon-new-window-private {\n    background-image: url('chrome://browser/skin/privateBrowsing.svg');\n  }\n\n  &.icon-settings {\n    background-image: url('chrome://browser/skin/settings.svg');\n  }\n\n  &.icon-pin {\n    @include flip-icon;\n    background-image: url('#{$image-path}glyph-pin-16.svg');\n  }\n\n  &.icon-unpin {\n    @include flip-icon;\n    background-image: url('#{$image-path}glyph-unpin-16.svg');\n  }\n\n  &.icon-edit {\n    background-image: url('#{$image-path}glyph-edit-16.svg');\n  }\n\n  &.icon-pocket {\n    background-image: url('#{$image-path}glyph-pocket-16.svg');\n  }\n\n  &.icon-pocket-save {\n    background-image: url('#{$image-path}glyph-pocket-save-16.svg');\n  }\n\n  &.icon-pocket-delete {\n    background-image: url('#{$image-path}glyph-pocket-delete-16.svg');\n  }\n\n  &.icon-pocket-archive {\n    background-image: url('#{$image-path}glyph-pocket-archive-16.svg');\n  }\n\n  &.icon-history-item {\n    background-image: url('chrome://browser/skin/history.svg');\n  }\n\n  &.icon-trending {\n    background-image: url('#{$image-path}glyph-trending-16.svg');\n    transform: translateY(2px); // trending bolt is visually top heavy\n  }\n\n  &.icon-now {\n    background-image: url('chrome://browser/skin/history.svg');\n  }\n\n  &.icon-topsites {\n    background-image: url('#{$image-path}glyph-topsites-16.svg');\n  }\n\n  &.icon-pin-small {\n    @include flip-icon;\n    background-image: url('#{$image-path}glyph-pin-12.svg');\n    background-size: $smaller-icon-size;\n    height: $smaller-icon-size;\n    width: $smaller-icon-size;\n  }\n\n  &.icon-check {\n    background-image: url('chrome://global/skin/icons/check.svg');\n  }\n\n  &.icon-download {\n    background-image: url('chrome://browser/skin/downloads/download-icons.svg#arrow-with-bar');\n  }\n\n  &.icon-copy {\n    background-image: url('chrome://browser/skin/edit-copy.svg');\n  }\n\n  &.icon-open-file {\n    background-image: url('#{$image-path}glyph-open-file-16.svg');\n  }\n\n  &.icon-webextension {\n    background-image: url('#{$image-path}glyph-webextension-16.svg');\n  }\n\n  &.icon-highlights {\n    background-image: url('#{$image-path}glyph-highlights-16.svg');\n  }\n\n  &.icon-arrowhead-down {\n    background-image: url('#{$image-path}glyph-arrowhead-down-16.svg');\n  }\n\n  &.icon-arrowhead-down-small {\n    background-image: url('#{$image-path}glyph-arrowhead-down-12.svg');\n    background-size: $smaller-icon-size;\n    height: $smaller-icon-size;\n    width: $smaller-icon-size;\n  }\n\n  &.icon-arrowhead-forward-small {\n    background-image: url('#{$image-path}glyph-arrowhead-down-12.svg');\n    background-size: $smaller-icon-size;\n    height: $smaller-icon-size;\n    transform: rotate(-90deg);\n    width: $smaller-icon-size;\n\n    &:dir(rtl) {\n      transform: rotate(90deg);\n    }\n  }\n\n  &.icon-arrowhead-up {\n    background-image: url('#{$image-path}glyph-arrowhead-down-16.svg');\n    transform: rotate(180deg);\n  }\n\n  &.icon-add {\n    background-image: url('#{$image-path}glyph-add-16.svg');\n  }\n\n  &.icon-minimize {\n    background-image: url('#{$image-path}glyph-minimize-16.svg');\n  }\n\n  &.icon-maximize {\n    background-image: url('#{$image-path}glyph-maximize-16.svg');\n  }\n\n  &.icon-arrow {\n    background-image: url('#{$image-path}glyph-arrow.svg');\n  }\n}\n"
  },
  {
    "path": "content-src/styles/_mixins.scss",
    "content": "// Shared styling of article images shown as background\n@mixin image-as-background {\n  background-color: var(--newtab-card-placeholder-color);\n  background-position: center;\n  background-repeat: no-repeat;\n  background-size: cover;\n  border-radius: 4px;\n  box-shadow: inset 0 0 0 0.5px $black-15;\n}\n\n// Note: lineHeight and fontSize should be unitless but can be derived from pixel values\n// Bug 1550624 to clean up / remove this mixin to avoid duplicate styles\n@mixin limit-visible-lines($line-count, $line-height, $font-size) {\n  font-size: $font-size * 1px;\n  -webkit-line-clamp: $line-count;\n  line-height: $line-height * 1px;\n}\n\n@mixin dark-theme-only {\n  [lwt-newtab-brighttext] & {\n    @content;\n  }\n}\n\n@mixin ds-border-top {\n  @content;\n\n  @include dark-theme-only {\n    border-top: 1px solid $grey-60;\n  }\n\n  border-top: 1px solid $grey-30;\n}\n\n@mixin ds-border-bottom {\n  @content;\n\n  @include dark-theme-only {\n    border-bottom: 1px solid $grey-60;\n  }\n\n  border-bottom: 1px solid $grey-30;\n}\n\n@mixin ds-fade-in($halo-color: $blue-50-30) {\n  box-shadow: 0 0 0 5px $halo-color;\n  transition: box-shadow 150ms;\n  border-radius: 4px;\n  outline: none;\n}\n"
  },
  {
    "path": "content-src/styles/_normalize.scss",
    "content": "html {\n  box-sizing: border-box;\n}\n\n*,\n*::before,\n*::after {\n  box-sizing: inherit;\n}\n\n*::-moz-focus-inner {\n  border: 0;\n}\n\nbody {\n  margin: 0;\n}\n\nbutton,\ninput {\n  background-color: inherit;\n  color: inherit;\n  font-family: inherit;\n  font-size: inherit;\n}\n\n[hidden] {\n  display: none !important; // sass-lint:disable-line no-important\n}\n"
  },
  {
    "path": "content-src/styles/_theme.scss",
    "content": "@function textbox-shadow($color) {\n  @return 0 0 0 1px $color, 0 0 0 $textbox-shadow-size rgba($color, 0.3);\n}\n\n@mixin textbox-focus($color) {\n  --newtab-textbox-focus-color: #{$color};\n  --newtab-textbox-focus-boxshadow: #{textbox-shadow($color)};\n}\n\n// scss variables related to the theme.\n$border-primary: 1px solid var(--newtab-border-primary-color);\n$border-secondary: 1px solid var(--newtab-border-secondary-color);\n$inner-box-shadow: 0 0 0 1px var(--newtab-inner-box-shadow-color);\n$input-border: 1px solid var(--newtab-textbox-border);\n$input-border-active: 1px solid var(--newtab-textbox-focus-color);\n$input-error-border: 1px solid $red-60;\n$input-error-boxshadow: textbox-shadow($red-60);\n$shadow-primary: 0 0 0 5px var(--newtab-card-active-outline-color);\n$shadow-secondary: 0 1px 4px 0 $grey-90-20;\n\n// Default theme\nbody {\n  // General styles\n  --newtab-background-color: #{$grey-10};\n  --newtab-border-primary-color: #{$grey-40};\n  --newtab-border-secondary-color: #{$grey-30};\n  --newtab-element-active-color: #{$grey-30-60};\n  --newtab-element-hover-color: #{$grey-20};\n  --newtab-icon-primary-color: #{$grey-90-80};\n  --newtab-icon-secondary-color: #{$grey-90-60};\n  --newtab-icon-tertiary-color: #{$grey-30};\n  --newtab-inner-box-shadow-color: #{$black-10};\n  --newtab-link-primary-color: #{$blue-60};\n  --newtab-link-secondary-color: #{$teal-70};\n  --newtab-text-conditional-color: #{$grey-60};\n  --newtab-text-primary-color: #{$grey-90};\n  --newtab-text-secondary-color: #{$grey-50};\n  --newtab-textbox-background-color: #{$white};\n  --newtab-textbox-border: #{$grey-90-20};\n  @include textbox-focus($blue-60); // sass-lint:disable-line mixins-before-declarations\n\n  // Buttons\n  --newtab-button-primary-color: #{$blue-60};\n  --newtab-button-secondary-color: inherit;\n  // Feed buttons\n  --newtab-feed-button-background: #{$grey-20};\n  --newtab-feed-button-text: #{$grey-90};\n  --newtab-feed-button-background-faded: #{$grey-20-60};\n  --newtab-feed-button-text-faded: #{$grey-90-00};\n  --newtab-feed-button-spinner: #{$grey-50};\n\n\n  // Context menu\n  --newtab-contextmenu-background-color: #{$grey-10};\n  --newtab-contextmenu-button-color: #{$white};\n\n  // Modal + overlay\n  --newtab-modal-color: #{$white};\n  --newtab-overlay-color: #{$grey-20-80};\n\n  // Sections\n  --newtab-section-header-text-color: #{$grey-50};\n  --newtab-section-navigation-text-color: #{$grey-50};\n  --newtab-section-active-contextmenu-color: #{$grey-90};\n\n  // Search\n  --newtab-search-border-color: transparent;\n  --newtab-search-dropdown-color: #{$white};\n  --newtab-search-dropdown-header-color: #{$grey-10};\n  --newtab-search-header-background-color: #{$grey-10-95};\n  --newtab-search-icon-color: #{$grey-90-40};\n  --newtab-search-wordmark-color: #{$firefox-wordmark-default-color};\n\n  // Top Sites\n  --newtab-topsites-background-color: #{$white};\n  --newtab-topsites-icon-shadow: inset #{$inner-box-shadow};\n  --newtab-topsites-label-color: inherit;\n\n  // Cards\n  --newtab-card-active-outline-color: #{$grey-30};\n  --newtab-card-background-color: #{$white};\n  --newtab-card-hairline-color: #{$black-10};\n  --newtab-card-placeholder-color: #{$grey-30};\n  --newtab-card-shadow: 0 1px 4px 0 #{$grey-90-10};\n\n  // Snippets\n  --newtab-snippets-background-color: #{$white};\n  --newtab-snippets-hairline-color: transparent;\n\n  // Trailhead\n  --trailhead-header-text-color: #{$trailhead-purple};\n  --trailhead-cards-background-color: #{$grey-20};\n  --trailhead-card-button-background-color: #{$grey-90-10};\n  --trailhead-card-button-background-hover-color: #{$grey-90-20};\n  --trailhead-card-button-background-active-color: #{$grey-90-30};\n\n  &[lwt-newtab-brighttext] {\n    // General styles\n    --newtab-background-color: #{$grey-80};\n    --newtab-border-primary-color: #{$grey-10-80};\n    --newtab-border-secondary-color: #{$grey-10-10};\n    --newtab-button-primary-color: #{$blue-60};\n    --newtab-button-secondary-color: #{$grey-70};\n    --newtab-element-active-color: #{$grey-10-20};\n    --newtab-element-hover-color: #{$grey-10-10};\n    --newtab-icon-primary-color: #{$grey-10-80};\n    --newtab-icon-secondary-color: #{$grey-10-40};\n    --newtab-icon-tertiary-color: #{$grey-10-40};\n    --newtab-inner-box-shadow-color: #{$grey-10-20};\n    --newtab-link-primary-color: #{$blue-40};\n    --newtab-link-secondary-color: #{$pocket-teal};\n    --newtab-text-conditional-color: #{$grey-10};\n    --newtab-text-primary-color: #{$grey-10};\n    --newtab-text-secondary-color: #{$grey-10-80};\n    --newtab-textbox-background-color: #{$grey-70};\n    --newtab-textbox-border: #{$grey-10-20};\n    @include textbox-focus($blue-40); // sass-lint:disable-line mixins-before-declarations\n\n    // Feed buttons\n    --newtab-feed-button-background: #{$grey-70};\n    --newtab-feed-button-text: #{$grey-10};\n    --newtab-feed-button-background-faded: #{$grey-70-60};\n    --newtab-feed-button-text-faded: #{$grey-10-00};\n    --newtab-feed-button-spinner: #{$grey-30};\n\n    // Context menu\n    --newtab-contextmenu-background-color: #{$grey-60};\n    --newtab-contextmenu-button-color: #{$grey-80};\n\n    // Modal + overlay\n    --newtab-modal-color: #{$grey-80};\n    --newtab-overlay-color: #{$grey-90-80};\n\n    // Sections\n    --newtab-section-header-text-color: #{$grey-10-80};\n    --newtab-section-navigation-text-color: #{$grey-10-80};\n    --newtab-section-active-contextmenu-color: #{$white};\n\n    // Search\n    --newtab-search-border-color: #{$grey-10-20};\n    --newtab-search-dropdown-color: #{$grey-70};\n    --newtab-search-dropdown-header-color: #{$grey-60};\n    --newtab-search-header-background-color: #{$grey-80-95};\n    --newtab-search-icon-color: #{$grey-10-60};\n    --newtab-search-wordmark-color: #{$firefox-wordmark-darktheme-color};\n\n    // Top Sites\n    --newtab-topsites-background-color: #{$grey-70};\n    --newtab-topsites-icon-shadow: none;\n    --newtab-topsites-label-color: #{$grey-10-80};\n\n    // Cards\n    --newtab-card-active-outline-color: #{$grey-60};\n    --newtab-card-background-color: #{$grey-70};\n    --newtab-card-hairline-color: #{$grey-10-10};\n    --newtab-card-placeholder-color: #{$grey-60};\n    --newtab-card-shadow: 0 1px 8px 0 #{$grey-90-20};\n\n    // Snippets\n    --newtab-snippets-background-color: #{$grey-70};\n    --newtab-snippets-hairline-color: #{$white-10};\n\n    // Trailhead\n    --trailhead-header-text-color: #{$white-60};\n    --trailhead-cards-background-color: #{$grey-90-10};\n    --trailhead-card-button-background-color: #{$grey-90-30};\n    --trailhead-card-button-background-hover-color: #{$grey-90-50};\n    --trailhead-card-button-background-active-color: #{$grey-90-70};\n  }\n}\n"
  },
  {
    "path": "content-src/styles/_variables.scss",
    "content": "// Photon colors from http://design.firefox.com/photon/visuals/color.html\n$blue-40: #45A1FF;\n$blue-50: #0A84FF;\n$blue-60: #0060DF;\n$blue-70: #003EAA;\n$blue-80: #002275;\n$grey-10: #F9F9FA;\n$grey-20: #EDEDF0;\n$grey-30: #D7D7DB;\n$grey-40: #B1B1B3;\n$grey-50: #737373;\n$grey-60: #4A4A4F;\n$grey-70: #38383D;\n$grey-80: #2A2A2E;\n$grey-90: #0C0C0D;\n$teal-10: #A7FFFE;\n$teal-60: #00C8D7;\n$teal-70: #008EA4;\n$teal-80: #005A71;\n$red-60: #D70022;\n$yellow-50: #FFE900;\n$violet-20: #CB9EFF;\n\n// Photon opacity from http://design.firefox.com/photon/visuals/color.html#opacity\n$grey-10-00: rgba($grey-10, 0);\n$grey-10-10: rgba($grey-10, 0.1);\n$grey-10-20: rgba($grey-10, 0.2);\n$grey-10-30: rgba($grey-10, 0.3);\n$grey-10-40: rgba($grey-10, 0.4);\n$grey-10-50: rgba($grey-10, 0.5);\n$grey-10-60: rgba($grey-10, 0.6);\n$grey-10-80: rgba($grey-10, 0.8);\n$grey-10-95: rgba($grey-10, 0.95);\n$grey-20-60: rgba($grey-20, 0.6);\n$grey-20-80: rgba($grey-20, 0.8);\n$grey-30-60: rgba($grey-30, 0.6);\n$grey-60-60: rgba($grey-60, 0.6);\n$grey-60-70: rgba($grey-60, 0.7);\n$grey-70-40: rgba($grey-70, 0.4);\n$grey-70-60: rgba($grey-70, 0.6);\n$grey-80-95: rgba($grey-80, 0.95);\n$grey-90-00: rgba($grey-90, 0);\n$grey-90-10: rgba($grey-90, 0.1);\n$grey-90-20: rgba($grey-90, 0.2);\n$grey-90-30: rgba($grey-90, 0.3);\n$grey-90-40: rgba($grey-90, 0.4);\n$grey-90-50: rgba($grey-90, 0.5);\n$grey-90-60: rgba($grey-90, 0.6);\n$grey-90-70: rgba($grey-90, 0.7);\n$grey-90-80: rgba($grey-90, 0.8);\n$grey-90-90: rgba($grey-90, 0.9);\n\n$blue-40-40: rgba($blue-40, 0.4);\n$blue-50-50: rgba($blue-50, 0.5);\n$blue-50-30: rgba($blue-50, 0.3);\n$blue-50-50: rgba($blue-50, 0.5);\n\n$black: #000;\n$black-5: rgba($black, 0.05);\n$black-10: rgba($black, 0.1);\n$black-12: rgba($black, 0.12);\n$black-15: rgba($black, 0.15);\n$black-20: rgba($black, 0.2);\n$black-25: rgba($black, 0.25);\n$black-30: rgba($black, 0.3);\n\n// Other colors\n$white: #FFF;\n$white-10: rgba($white, 0.1);\n$white-50: rgba($white, 0.5);\n$white-60: rgba($white, 0.6);\n$white-70: rgba($white, 0.7);\n$ghost-white: #FAFAFC;\n$pocket-teal: #50BCB6;\n$pocket-red: #EF4056;\n$shadow-10: rgba(12, 12, 13, 0.1);\n$bookmark-icon-fill: #0A84FF;\n$download-icon-fill: #12BC00;\n$pocket-icon-fill: #D70022;\n$email-input-focus: rgba($blue-50, 0.3);\n$email-input-invalid: rgba($red-60, 0.3);\n$aw-extra-blue-1: #004EC2;\n$aw-extra-blue-2: #0080FF;\n$aw-extra-blue-3: #00C7FF;\n$about-welcome-gradient: linear-gradient(to bottom, $blue-70 40%, $aw-extra-blue-1 60%, $blue-60 80%, $aw-extra-blue-2 90%, $aw-extra-blue-3 100%);\n$about-welcome-extra-links: #676F7E;\n$firefox-wordmark-default-color: #363959;\n$firefox-wordmark-darktheme-color: $white;\n$trailhead-violet: #7542E5;\n$trailhead-purple: #2B2156;\n$trailhead-purple-80: #36296D;\n$trailhead-blue-60: #0250BB;\n$trailhead-blue-70: #054096;\n\n// Photon transitions from http://design.firefox.com/photon/motion/duration-and-easing.html\n$photon-easing: cubic-bezier(0.07, 0.95, 0, 1);\n\n$border-radius: 3px;\n\n// Grid related styles\n$base-gutter: 32px;\n$section-horizontal-padding: 25px;\n$section-vertical-padding: 10px;\n$section-spacing: 40px - $section-vertical-padding * 2;\n$grid-unit: 96px; // 1 top site\n\n$icon-size: 16px;\n$smaller-icon-size: 12px;\n$larger-icon-size: 32px;\n\n$searchbar-width-small: $grid-unit * 2 + $base-gutter * 1;\n$searchbar-width-medium: $grid-unit * 4 + $base-gutter * 3;\n$searchbar-width-large: $grid-unit * 6 + $base-gutter * 5;\n\n$wrapper-default-width: $grid-unit * 2 + $base-gutter * 1 + $section-horizontal-padding * 2; // 2 top sites\n$wrapper-max-width-medium: $grid-unit * 4 + $base-gutter * 3 + $section-horizontal-padding * 2; // 4 top sites\n$wrapper-max-width-large: $grid-unit * 6 + $base-gutter * 5 + $section-horizontal-padding * 2; // 6 top sites\n$wrapper-max-width-widest: $grid-unit * 8 + $base-gutter * 7 + $section-horizontal-padding * 2; // 8 top sites\n// For the breakpoints, we need to add space for the scrollbar to avoid weird\n// layout issues when the scrollbar is visible. 16px is wide enough to cover all\n// OSes and keeps it simpler than a per-OS value.\n$scrollbar-width: 16px;\n\n// Breakpoints\n$break-point-medium: $wrapper-max-width-medium + $base-gutter * 2 + $scrollbar-width;\n$break-point-large: $wrapper-max-width-large + $base-gutter * 2 + $scrollbar-width;\n$break-point-widest: $wrapper-max-width-widest + $base-gutter * 2 + $scrollbar-width;\n\n$section-title-font-size: 13px;\n\n$card-width: $grid-unit * 2 + $base-gutter;\n$card-height: 266px;\n$card-preview-image-height: 122px;\n$card-title-margin: 2px;\n$card-text-line-height: 19px;\n// Larger cards for wider screens:\n$card-width-large: 309px;\n$card-height-large: 370px;\n$card-preview-image-height-large: 155px;\n// Compact cards for Highlights\n$card-height-compact: 160px;\n$card-preview-image-height-compact: 108px;\n\n$topic-margin-top: 12px;\n\n$context-menu-button-size: 27px;\n$context-menu-button-boxshadow: 0 2px $grey-90-10;\n$context-menu-shadow: 0 5px 10px $black-30, 0 0 0 1px $black-20;\n$context-menu-font-size: 14px;\n$context-menu-border-radius: 5px;\n$context-menu-outer-padding: 5px;\n$context-menu-item-padding: 3px 12px;\n\n$error-fallback-font-size: 12px;\n$error-fallback-line-height: 1.5;\n\n$image-path: '../data/content/assets/';\n\n$snippets-container-height: 120px;\n\n$textbox-shadow-size: 4px;\n\n@mixin fade-in {\n  box-shadow: inset $inner-box-shadow, $shadow-primary;\n  transition: box-shadow 150ms;\n}\n\n@mixin fade-in-card {\n  box-shadow: $shadow-primary;\n  transition: box-shadow 150ms;\n}\n\n@mixin context-menu-button {\n  .context-menu-button {\n    background-clip: padding-box;\n    background-color: var(--newtab-contextmenu-button-color);\n    background-image: url('chrome://global/skin/icons/more.svg');\n    background-position: 55%;\n    border: $border-primary;\n    border-radius: 100%;\n    box-shadow: $context-menu-button-boxshadow;\n    cursor: pointer;\n    fill: var(--newtab-icon-primary-color);\n    height: $context-menu-button-size;\n    inset-inline-end: -($context-menu-button-size / 2);\n    opacity: 0;\n    position: absolute;\n    top: -($context-menu-button-size / 2);\n    transform: scale(0.25);\n    transition-duration: 150ms;\n    transition-property: transform, opacity;\n    width: $context-menu-button-size;\n\n    &:-moz-any(:active, :focus) {\n      opacity: 1;\n      transform: scale(1);\n    }\n  }\n}\n\n@mixin context-menu-button-hover {\n  .context-menu-button {\n    opacity: 1;\n    transform: scale(1);\n    transition-delay: 333ms;\n  }\n}\n\n@mixin context-menu-open-middle {\n  .context-menu {\n    margin-inline-end: auto;\n    margin-inline-start: auto;\n    inset-inline-end: auto;\n    inset-inline-start: -$base-gutter;\n  }\n}\n\n@mixin context-menu-open-left {\n  .context-menu {\n    margin-inline-end: 5px;\n    margin-inline-start: auto;\n    inset-inline-end: 0;\n    inset-inline-start: auto;\n  }\n}\n\n@mixin flip-icon {\n  &:dir(rtl) {\n    transform: scaleX(-1);\n  }\n}\n"
  },
  {
    "path": "content-src/styles/activity-stream-linux.scss",
    "content": "// sass-lint:disable no-css-comments\n/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\n/* This is the linux variant */\n// sass-lint:enable no-css-comments\n\n$os-infopanel-arrow-height: 10px;\n$os-infopanel-arrow-offset-end: 6px;\n$os-infopanel-arrow-width: 20px;\n\n@import './activity-stream';\n"
  },
  {
    "path": "content-src/styles/activity-stream-mac.scss",
    "content": "// sass-lint:disable no-css-comments\n/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\n/* This is the mac variant */\n// sass-lint:enable no-css-comments\n\n$os-infopanel-arrow-height: 10px;\n$os-infopanel-arrow-offset-end: 7px;\n$os-infopanel-arrow-width: 18px;\n\n[lwt-newtab-brighttext] {\n  -moz-osx-font-smoothing: grayscale;\n}\n\n@import './activity-stream';\n"
  },
  {
    "path": "content-src/styles/activity-stream-windows.scss",
    "content": "// sass-lint:disable no-css-comments\n/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\n/* This is the windows variant */\n// sass-lint:enable no-css-comments\n\n$os-infopanel-arrow-height: 10px;\n$os-infopanel-arrow-offset-end: 6px;\n$os-infopanel-arrow-width: 20px;\n\n@import './activity-stream';\n"
  },
  {
    "path": "contributing.md",
    "content": "# Contributing to Activity Stream\n\nActivity Stream is an enhancement to the functionality of Firefox's about:newtab page.  We welcome new 'streamers' to contribute to the project!\n\n## Where to ask questions\n\n- Most of the core dev team can be found on the `#activity-stream` channel on `irc.mozilla.org`.\n  You can also direct message the core team (`dmose`, `emtwo`, `jkerim`, `k88hudson`, `Mardak`, `nanj`, `r1cky`, `ursula`, `andreio`)\n  or our manager (`tspurway`)\n- Slack channel (staff only): #activitystream\n- Mailing List: [activity-stream-dev](https://groups.google.com/a/mozilla.com/d/forum/activity-stream-dev)\n- File issues/questions on Github: https://github.com/mozilla/activity-stream/issues. We typically triage new issues every Monday.\n\n## Architecture ##\n\nActivity Stream is a Firefox system add-on. One of the cool things about Activity Stream is that the\n[content side of the add-on](https://developer.mozilla.org/en-US/Add-ons/SDK/Guides/Content_Scripts)\nis written using [ReactJS](https://facebook.github.io/react/).  This makes it an awesome project for React hackers to contribute to!\n\n## Finding Bugs, Filing Tickets, Earning Karma ##\n\nActivity Stream lives on [GitHub](https://github.com/mozilla/activity-stream), but you already knew that!  If you've found\na bug, or have a feature idea that you you'd like to see in Activity Stream, follow these simple guidelines:\n- Pick a thoughtful and terse title for the issue (ie. *not* Thing Doesn't Work!)\n- Make sure to mention your Firefox version, OS and basic system parameters (eg. Firefox 49.0, Windows XP, 512KB RAM)\n- If you can reproduce the bug, give a step-by-step recipe\n- Include [stack traces from the console(s)](https://developer.mozilla.org/en-US/docs/Mozilla/Debugging/Debugging_JavaScript) where appropriate\n- Screenshots welcome!\n- When in doubt, take a look at some [existing issues](https://github.com/mozilla/activity-stream/issues) and emulate\n\n## Take a Ticket, Hack some Code ##\n\nIf you are new to the repo, you might want to pay close attention to [`Good first bug`](https://github.com/mozilla/activity-stream/issues?q=is%3Aopen+is%3Aissue+label%3A%22Good+first+bug%22),\n [`Bug`](https://github.com/mozilla/activity-stream/issues?q=is%3Aopen%20is%3Aissue%20label%3ABug%20),\n [`Chore`](https://github.com/mozilla/activity-stream/issues?q=is%3Aopen+is%3Aissue+label%3AChore) and\n [`Polish`](https://github.com/mozilla/activity-stream/issues?q=is%3Aopen+is%3Aissue+label%3APolish) tags, as these are\ntypically a great way to get started.  You might see a bug that is not yet assigned to anyone, or start a conversation with\nan engineer in the ticket itself, expressing your interest in taking the bug.  If you take the bug, someone will set\nthe ticket to [`Assigned to Contributor`](https://github.com/mozilla/activity-stream/issues?utf8=%E2%9C%93&q=is%3Aopen%20is%3Aissue%20label%3A%22Assigned%20to%20contributor%22%20), which is a way we can be pro-active about helping you succeed in fixing the bug.\n\nWhen you have some code written, you can open up a [Pull Request](#pull-requests), get your code [reviewed](#code-reviews), and see your code merged into the Activity Stream codebase.\n\nIf you are thinking about contributing on a longer-term basis, check out the section on [milestones](#milestones) and [priorities](#priorities)\nto get a sense of how we plan and prioritize work.\n\n## Setting up your development environment\n\nCheck out [this guide](docs/v2-system-addon/1.GETTING_STARTED.md) on how to install dependencies, get set up, and run tests.\n\n## Pull Requests ##\n\nYou have identified the bug, written code and now want to get it into the main repo using a [Pull Request](https://help.github.com/articles/about-pull-requests/).\n\nAll code is added using a pull request against the `master` branch of our repo.  Before submitting a PR, please go through this checklist:\n- all [unit tests](docs/v2-system-addon/unit_testing_guide.md) must pass\n- if you haven't written unit tests for your patch, eyebrows will be curmudgeonly furrowed (write unit tests!)\n- if your pull request fixes a particular ticket (it does, right?), please use the `fixes #nnn` github annotation to indicate this\n- please add a `PR / Needs review` tag to your PR (if you have permission).  This starts the code review process.  If you cannot add a tag, don't worry, we will add it during triage.\n- if you can pick a module owner to be your reviewer by including `r? @username` in the comment (if not, don't worry, we will assign a reviewer)\n- make sure your PR will merge gracefully with `master` at the time you create the PR, and that your commit history is 'clean'\n\n### Setting up pre-push hooks\n\nIf you contribute often and would like to set up a pre-push hook to always run `npm lint` before you push to Github,\nyou can run the following from the root of the activity-stream directory:\n\n```\ncp hooks/pre-push .git/hooks/pre-push && chmod +x .git/hooks/pre-push\n```\n\nYour hook should now run whenever you run `git push`. To skip it, use the `--no-verify` option:\n\n```\ngit push --no-verify\n```\n\n\n## Code Reviews ##\n\nYou have created a PR and submitted it to the repo, and now are waiting patiently for you code review feedback.  One of the projects\nmodule owners will be along and will either:\n- make suggestions for some improvements\n- give you an `R+` in the comments section, indicating the review is done and the code can be merged\n\nTypically, you will iterate on the PR, making changes and pushing your changes to new commits on the PR.  When the reviewer is\n satisfied that your code is good-to-go, you will get the coveted `R+` comment, and your code can be merged.  If you have\n commit permission, you can go ahead and merge the code to `master`, otherwise, it will be done for you.\n\nOur project prides itself on it's respectful, patient and positive attitude when it comes to reviewing contributor's code, and as such,\nwe expect contributors to be respectful, patient and positive in their communications as well.  Let's be friends and learn\nfrom each other for a free and awesome web!\n\n[Mozilla Committing Rules and Responsibilities](https://developer.mozilla.org/en-US/docs/Mozilla/Developer_guide/Committing_Rules_and_Responsibilities)\n\n## Git Commit Guidelines ##\n\nWe loosely follow the [Angular commit guidelines](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md#type) of `<type>(<scope>): <subject>` where `type` must be one of:\n\n* **feat**: A new feature\n* **fix**: A bug fix\n* **docs**: Documentation only changes\n* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing\n  semi-colons, etc)\n* **refactor**: A code change that neither fixes a bug or adds a feature\n* **perf**: A code change that improves performance\n* **test**: Adding missing tests\n* **chore**: Changes to the build process or auxiliary tools and libraries such as documentation\n  generation\n\n### Scope\nThe scope could be anything specifying place of the commit change. For example `timeline`,\n`metadata`, `reporting`, `experiments` etc...\n\n### Subject\nThe subject contains succinct description of the change:\n\n* use the imperative, present tense: \"change\" not \"changed\" nor \"changes\"\n* don't capitalize first letter\n* no dot (.) at the end\n\n### Body\nIn order to maintain a reference to the context of the commit, add\n`fixes #<issue_number>` if it closes a related issue or `issue #<issue_number>`\nif it's a partial fix.\n\nYou can also write a detailed description of the commit:\nJust as in the **subject**, use the imperative, present tense: \"change\" not \"changed\" nor \"changes\"\nIt should include the motivation for the change and contrast this with previous behavior.\n\n### Footer\nThe footer should contain any information about **Breaking Changes** and is also the place to\nreference GitHub issues that this commit **Closes**.\n\n## Milestones ##\n\nAll work on Activity Stream is broken into two week iterations, which we map into a GitHub [Milestone](https://github.com/mozilla/activity-stream/milestones).  At the beginning of the iteration, we prioritize and estimate tickets\ninto the milestone, attempting to figure out how much progress we can make during the iteration.  \n\n## Priorities ##\n\nAll tickets that have been [triaged](#triage) will have a priority tag of either `P1`, `P2`, `P3`, or `P4` which are highest to lowest\npriorities of tickets in Activity Stream. We love ticket tags and you might also see `Blocked`, `Critical` or `Chemspill` tags, which\nindicate our level of anxiety about that particular ticket.  \n\n## Triage ##\n\nThe project team meets weekly (in a closed meeting, for the time being), to discuss project priorities, to triage new tickets, and to\nredistribute the work amongst team members.  Any contributors tickets or PRs are carefully considered, prioritized, and if needed,\nassigned a reviewer.  The project's GitHub [Milestone page](https://github.com/mozilla/activity-stream/milestones) is the best\nplace to look for up-to-date information on project priorities and current workload.\n\n## License\n\nMPL 2.0\n"
  },
  {
    "path": "data/content/tippytop/top_sites.json",
    "content": "[\n  {\n    \"title\": \"aliexpress\",\n    \"url\": \"https://www.aliexpress.com/\",\n    \"image_url\": \"aliexpress-com@2x.png\"\n  },\n  {\n    \"title\": \"allegro\",\n    \"url\": \"https://www.allegro.pl/\",\n    \"image_url\": \"allegro-pl@2x.png\"\n  },\n  {\n    \"title\": \"amazon\",\n    \"urls\": [\"https://www.amazon.ca/\", \"https://www.amazon.co.uk/\", \"https://www.amazon.com/\", \"https://www.amazon.de/\", \"https://www.amazon.fr/\"],\n    \"image_url\": \"amazon@2x.png\"\n  },\n  {\n    \"title\": \"avito\",\n    \"url\": \"https://www.avito.ru/\",\n    \"image_url\": \"avito-ru@2x.png\"\n  },\n  {\n    \"title\": \"baidu\",\n    \"url\": \"https://www.baidu.com/\",\n    \"image_url\": \"baidu-com@2x.png\"\n  },\n  {\n    \"title\": \"bbc\",\n    \"url\": \"https://www.bbc.co.uk/\",\n    \"image_url\": \"bbc-uk@2x.png\"\n  },\n  {\n    \"title\": \"bing\",\n    \"url\": \"https://www.bing.com/\",\n    \"image_url\": \"bing-com@2x.png\"\n  },\n  {\n    \"title\": \"duckduckgo\",\n    \"url\": \"https://www.duckduckgo.com/\",\n    \"image_url\": \"duckduckgo-com@2x.png\"\n  },\n  {\n    \"title\": \"ebay\",\n    \"urls\": [\"https://www.ebay.com\", \"https://www.ebay.co.uk/\", \"https://ebay.de\"],\n    \"image_url\": \"ebay@2x.png\"\n  },\n  {\n    \"title\": \"facebook\",\n    \"url\": \"https://www.facebook.com/\",\n    \"image_url\": \"facebook-com@2x.png\"\n  },\n  {\n    \"title\": \"google\",\n    \"url\": \"https://www.google.com/\",\n    \"image_url\": \"google-com@2x.png\"\n  },\n  {\n    \"title\": \"leboncoin\",\n    \"url\": \"http://www.leboncoin.fr/\",\n    \"image_url\": \"leboncoin-fr@2x.png\"\n  },\n  {\n    \"title\": \"ok\",\n    \"url\": \"https://www.ok.ru/\",\n    \"image_url\": \"ok-ru@2x.png\"\n  },\n  {\n    \"title\": \"olx\",\n    \"url\": \"https://www.olx.pl/\",\n    \"image_url\": \"olx-pl@2x.png\"\n  },\n  {\n    \"title\": \"reddit\",\n    \"url\": \"https://www.reddit.com/\",\n    \"image_url\": \"reddit-com@2x.png\"\n  },\n  {\n    \"title\": \"twitter\",\n    \"url\": \"https://twitter.com/\",\n    \"image_url\": \"twitter-com@2x.png\"\n  },\n  {\n    \"title\": \"vk\",\n    \"url\": \"https://vk.com/\",\n    \"image_url\": \"vk-com@2x.png\"\n  },\n  {\n    \"title\": \"wikipedia\",\n    \"url\": \"https://www.wikipedia.org/\",\n    \"image_url\": \"wikipedia-org@2x.png\"\n  },\n  {\n    \"title\": \"wykop\",\n    \"url\": \"https://www.wykop.pl/\",\n    \"image_url\": \"wykop-pl@2x.png\"\n  },\n  {\n    \"title\": \"yandex\",\n    \"url\": \"https://www.yandex.com/\",\n    \"image_url\": \"yandex-com@2x.png\"\n  },\n  {\n    \"title\": \"youtube\",\n    \"url\": \"https://www.youtube.com/\",\n    \"image_url\": \"youtube-com@2x.png\"\n  }\n]\n"
  },
  {
    "path": "docs/ISSUE_TEMPLATE.md",
    "content": "Please file new bugs in Bugzilla:\nhttps://bugzilla.mozilla.org/enter_bug.cgi?product=Firefox&component=Activity%20Streams%3A%20Newtab\n\nActivity Stream is no longer accepting new issues via GitHub, but Issues are kept open, so old issues can still be viewed.\n\nThanks for contributing!\n"
  },
  {
    "path": "docs/index.rst",
    "content": "======================\nFirefox Home (New Tab)\n======================\n\nAll files related to Firefox Home, which includes content that appears on `about:home`,\n`about:newtab`, and `about:welcome`, can we found in the `browser/components/newtab` directory.\nSome of these source files (such as `.js`, `.jsx`, and `.sass`) require an additional build step.\nWe are working on migrating this to work with `mach`, but in the meantime, please\nfollow the following steps if you need to make changes in this directory:\n\nFor .jsm files\n---------------\n\nNo build step is necessary. Use `mach` and run mochitests according to your regular Firefox workflow.\n\nFor .js, .jsx, .sass, or .css files\n-----------------------------------\n\nPrerequisites\n`````````````\n\nYou will need the following:\n\n- Node.js 8+ (On Mac, the best way to install Node.js is to use the install link on the `Node.js homepage`_)\n- npm (packaged with Node.js)\n\nTo install dependencies, run the following from the root of the mozilla-central repository\n(or cd into browser/components/newtab to omit the `--prefix` in any of these commands):\n\n.. code-block:: shell\n\n  npm install --prefix browser/components/newtab\n\n\nWhich files should you edit?\n````````````````````````````\n\nYou should not make changes to `.js` or `.css` files in `browser/components/newtab/css` or\n`browser/components/newtab/data` directory. Instead, you should edit the `.jsx`, `.js`, and `.sass` files\nin `browser/components/newtab/content-src` directory.\n\nBuilding assets and running Firefox\n```````````````````````````````````\n\nTo build assets and run Firefox, run the following from the root of the mozilla-central repository:\n\n.. code-block:: shell\n\n  npm run bundle --prefix browser/components/newtab && ./mach run\n\nRunning tests\n`````````````\nThe majority of New Tab / Messaging unit tests are written using\n`mocha <https://mochajs.org>`_, and other errors that may show up there are\n`SCSS <https://sass-lang.com/documentation/syntax>`_ issues flagged by\n`sasslint <https://github.com/sasstools/sass-lint/tree/master>`_.  These things\nare all run using `npm test` under the `newtab` slug in Treeherder/Try, so if\nthat slug turns red, these tests are what is failing.  To execute them, do this:\n\n.. code-block:: shell\n\n  npm test --prefix browser/components/newtab\n\nThese tests are not currently run by `mach test`, but there's a\n`task filed to fix that <https://bugzilla.mozilla.org/show_bug.cgi?id=1581165>`_.\n\nMochitests and xpcshell tests run normally, using `mach test`.\n\nGitHub workflow\n---------------\nThe files in this directory, including vendor dependencies, are synchronized with the https://github.com/mozilla/activity-stream repository. If you prefer a GitHub-based workflow, you can look at the documentation there to learn more.\n\n..  _Node.js homepage: https://nodejs.org/\n"
  },
  {
    "path": "docs/v2-system-addon/1.GETTING_STARTED.md",
    "content": "# Activity Stream Development Guide\n\n## Contents of this guide\n\n- Installation, set-up, and other basics (this page)\n- [Writing unit tests](./unit_testing_guide.md)\n- [Adding new Telemetry (user and performance metrics)](./telemetry.md)\n- [Reading/changing Firefox prefs](./preferences.md)\n- [Adding new sections](./sections.md)\n- [Changing geo and locale](./geo_locale.md)\n\n## How to try Activity Stream\n\nIf you just want to try out the current version of Activity Stream in Firefox, you can\ninstall [Firefox Nightly](https://www.mozilla.org/en-US/firefox/channel/desktop/#nightly)\nor any version of Firefox >= 57.0. If you still don't see activity stream, go to `about:config`,\nand make sure the `browser.newtabpage.activity-stream.enabled` pref is set to `true`.\n\n## Source code and submitting pull requests\n\nA copy of the code in the root directory of this repository\nis exported to Mozilla central on a regular basis, which can be found at [browser/components/newtab](https://searchfox.org/mozilla-central/source/browser/components/newtab).\nKeep in mind that some of these files are generated, so if you intend on editing any files, you should\ndo so in the Github version.\n\nPull requests should be sent against the master branch of https://github.com/mozilla/activity-stream,\nNOT against Mozilla central.\n\n## Prerequisites for development\n\n### Operating system and software\n\nThe Activity Stream development environment is designed to work on Mac and Linux.\nIf you need to develop on Windows, you might want to reach out on IRC (#activity-stream)\nif you run into any problems.\n\nYou will also need to install:\n\n- Node.js version 8 (To install this legacy version of Node, [use this URL](https://nodejs.org/en/download/releases/))\n- npm (packaged with Node.js)\n\n### Activity Stream Github repository\n\nYou will need to to clone Activity Stream to a local directory from the `master`\nbranch of our Github repository: https://github.com/mozilla/activity-stream\n\n```\ngit clone https://github.com/mozilla/activity-stream.git\n```\n\nAlso be sure to install the hooks for this repository so that (at least)\neslint and prettier fixing happens at commit time.\n\n```bash\ncd activity-stream\n./bin/bootstrap\n```\n\n### Mozilla Central\nYou will need a local copy of Mozilla Central in a directory named `mozilla-central`. Check the detail of how to get and build Mozilla Central in [Building Firefox](https://developer.mozilla.org/en-US/docs/Mozilla/Developer_guide/Build_Instructions/Simple_Firefox_build).\nThat directory should be a sibling of your local `activity-stream` directory (like so):\n\n```\n/\n  mozilla-central/\n  activity-stream/\n```\n\nCheck out [these docs on artifact builds](https://developer.mozilla.org/en-US/docs/Mozilla/Developer_guide/Build_Instructions/Artifact_builds)\nfor instructions about how to download and configure Mozilla Central if you have\nnever done so before.\n\n**To build Firefox way faster**, you should definitely enable Artifact builds.\nTo do that, create a `.mozconfig` (or `mozconfig`) file in the root of your\nmozilla-central directory and add the following to it:\n\n```bash\n# Automatically download and use compiled C++ components:\nac_add_options --enable-artifact-builds\n\n# Write build artifacts to:\nmk_add_options MOZ_OBJDIR=./objdir-frontend\n```\n\n## Building\n\n1. Install required dependencies by running `npm install`.\n2. To build Activity Stream, run `npm run buildmc` from the root of the\n`activity-stream` directory. This will build the js and css files and copy them\ninto the `browser/components/newtab` directory inside Mozilla Central.\n3. Build and run Firefox from the `mozilla-central` directory by running `./mach build && ./mach run`.\n\n## Continuous development / debugging\n\nRunning `npm run startmc` will start a process that watches files in `activity-stream`\nand continuously builds/copies changes to `mozilla-central`. You will\nstill need to rebuild Firefox (`./mach build`) if you change `.jsm` files.\n\n**IMPORTANT NOTE**: This task will add inline source maps to help with debugging, which changes the memory footprint.\nDo not use the `startmc` task for profiling or performance testing!\n\n## Unit Tests\n\nRun `npm run testmc` to run the unit tests with karma/mocha. The source code for these\ntests can be found in `system-addon/test/unit/`.\n\nWe have a [detailed write-up](unit_testing_guide.md) on\nActivity Stream unit testing.  This is an important read, as there are **significant** JavaScript differences when\nwriting Firefox add-on code that must be taken into consideration.\n\nOur build process will run unit tests and code coverage tools automatically.  Make that all tests pass,\nand that you are not responsible for unduly decreasing the overall code coverage percentage.\n\nIf you see any missing test coverage, you can inspect the coverage report by running `npm run testmc && npm run debugcoverage`.\n"
  },
  {
    "path": "docs/v2-system-addon/data_dictionary.md",
    "content": "# Activity Stream Pings\n\nThe Activity Stream system add-on sends various types of pings to the backend (HTTPS POST) [Onyx server](https://github.com/mozilla/onyx) :\n- a `health` ping that reports whether or not a user has a custom about:home or about:newtab page\n- a `session` ping that describes the ending of an Activity Stream session (a new tab is closed or refreshed), and\n- an `event` ping that records specific data about individual user interactions while interacting with Activity Stream\n- a `performance` ping that records specific performance related events\n- an `undesired` ping that records data about bad app states and missing data\n- an `impression_stats` ping that records data about Pocket impressions and user interactions\n\nSchema definitions/validations that can be used for tests can be found in `system-addon/test/schemas/pings.js`.\n\n## Example Activity Stream `health` log\n\n```js\n{\n  \"addon_version\": \"20180710100040\",\n  \"client_id\": \"374dc4d8-0cb2-4ac5-a3cf-c5a9bc3c602e\",\n  \"locale\": \"en-US\",\n  \"version\": \"62.0a1\",\n  \"release_channel\": \"nightly\",\n  \"event\": \"AS_ENABLED\",\n  \"value\": 10,\n\n  // These fields are generated on the server\n  \"date\": \"2016-03-07\",\n  \"ip\": \"10.192.171.13\",\n  \"ua\": \"python-requests/2.9.1\",\n  \"receive_at\": 1457396660000\n}\n```\n\n## Example Activity Stream `session` Log\n\n```js\n{\n  // These fields are sent from the client\n  \"action\": \"activity_stream_session\",\n  \"addon_version\": \"20180710100040\",\n  \"client_id\": \"374dc4d8-0cb2-4ac5-a3cf-c5a9bc3c602e\",\n  \"locale\": \"en-US\",\n  \"page\": [\"about:newtab\" | \"about:home\" | \"about:welcome\" | \"unknown\"],\n  \"session_duration\": 1635,\n  \"session_id\": \"{12dasd-213asda-213dkakj}\",\n  \"region\": \"US\",\n  \"profile_creation_date\": 14786,\n  \"user_prefs\": 7\n\n  // These fields are generated on the server\n  \"date\": \"2016-03-07\",\n  \"ip\": \"10.192.171.13\",\n  \"ua\": \"python-requests/2.9.1\",\n  \"receive_at\": 1457396660000\n}\n```\n\n## Example Activity Stream `user_event` Log\n\n```js\n{\n  \"action\": \"activity_stream_user_event\",\n  \"action_position\": \"3\",\n  \"addon_version\": \"20180710100040\",\n  \"client_id\": \"374dc4d8-0cb2-4ac5-a3cf-c5a9bc3c602e\",\n  \"event\": \"click or scroll or search or delete\",\n  \"locale\": \"en-US\",\n  \"page\": [\"about:newtab\" | \"about:home\" | \"about:welcome\" | \"unknown\"],\n  \"source\": \"top sites, or bookmarks, or...\",\n  \"session_id\": \"{12dasd-213asda-213dkakj}\",\n  \"recommender_type\": \"pocket-trending\",\n  \"metadata_source\": \"MetadataService or Local or TippyTopProvider\",\n  \"user_prefs\": 7\n\n  // These fields are generated on the server\n  \"ip\": \"10.192.171.13\",\n  \"ua\": \"python-requests/2.9.1\",\n  \"receive_at\": 1457396660000,\n  \"date\": \"2016-03-07\",\n}\n```\n\n## Example Activity Stream `performance` Log\n\n```js\n{\n  \"action\": \"activity_stream_performance_event\",\n  \"addon_version\": \"20180710100040\",\n  \"client_id\": \"374dc4d8-0cb2-4ac5-a3cf-c5a9bc3c602e\",\n  \"event\": \"previewCacheHit\",\n  \"event_id\": \"45f1912165ca4dfdb5c1c2337dbdc58f\",\n  \"locale\": \"en-US\",\n  \"page\": \"unknown\", // all session-specific perf events should be part of the session perf object\n  \"receive_at\": 1457396660000,\n  \"source\": \"TOP_FRECENT_SITES\",\n  \"value\": 1,\n  \"user_prefs\": 7,\n\n  // These fields are generated on the server\n  \"ip\": \"10.192.171.13\",\n  \"ua\": \"python-requests/2.9.1\",\n  \"receive_at\": 1457396660000,\n  \"date\": \"2016-03-07\"\n}\n```\n\n## Example Activity Stream `undesired event` Log\n\n```js\n{\n  \"action\": \"activity_stream_undesired_event\",\n  \"addon_version\": \"20180710100040\",\n  \"client_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  \"event\": \"MISSING_IMAGE\",\n  \"locale\": \"en-US\",\n  \"page\": [\"about:newtab\" | \"about:home\" | \"about:welcome\" | \"unknown\"]\n  \"source\": \"HIGHLIGHTS\",\n  \"value\": 0,\n  \"user_prefs\": 7,\n\n  // These fields are generated on the server\n  \"ip\": \"10.192.171.13\",\n  \"ua\": \"python-requests/2.9.1\",\n  \"receive_at\": 1457396660000,\n  \"date\": \"2016-03-07\"\n}\n```\n## Example Activity Stream `impression_stats` Logs\n\n```js\n{\n  \"action\": \"activity_stream_impression_stats\",\n  \"client_id\": \"n/a\",\n  \"session_id\": \"n/a\",\n  \"impression_id\": \"{005deed0-e3e4-4c02-a041-17405fd703f6}\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"source\": \"pocket\",\n  \"page\": [\"about:newtab\" | \"about:home\" | \"about:welcome\" | \"unknown\"]\n  \"tiles\": [{\"id\": 10000}, {\"id\": 10001}, {\"id\": 10002}]\n  \"user_prefs\": 7\n}\n```\n\n```js\n{\n  \"action\": \"activity_stream_impression_stats\",\n  \"client_id\": \"n/a\",\n  \"session_id\": \"n/a\",\n  \"impression_id\": \"{005deed0-e3e4-4c02-a041-17405fd703f6}\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"source\": \"pocket\",\n  \"page\": \"unknown\",\n  \"user_prefs\": 7,\n\n  // \"pos\" is the 0-based index to record the tile's position in the Pocket section.\n  // \"shim\" is a base64 encoded shim attached to spocs, unique to the impression from the Ad server.\n  \"tiles\": [{\"id\": 10000, \"pos\": 0, \"shim\": \"enuYa1j73z3RzxgTexHNxYPC/b,9JT6w5KB0CRKYEU+\"}],\n\n  // A 0-based index to record which tile in the \"tiles\" list that the user just interacted with.\n  \"click|block|pocket\": 0\n}\n```\n\n## Example Discovery Stream `SPOCS Fill` log\n\n```js\n{\n  // both \"client_id\" and \"session_id\" are set to \"n/a\" in this ping.\n  \"client_id\": \"n/a\",\n  \"session_id\": \"n/a\",\n  \"impression_id\": \"{005deed0-e3e4-4c02-a041-17405fd703f6}\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"version\": \"68\",\n  \"release_channel\": \"release\",\n  \"spoc_fills\": [\n    {\"id\": 10000, displayed: 0, reason: \"frequency_cap\", full_recalc: 1},\n    {\"id\": 10001, displayed: 0, reason: \"blocked_by_user\", full_recalc: 1},\n    {\"id\": 10002, displayed: 0, reason: \"below_min_score\", full_recalc: 1},\n    {\"id\": 10003, displayed: 0, reason: \"flight_duplicate\", full_recalc: 1},\n    {\"id\": 10004, displayed: 0, reason: \"probability_selection\", full_recalc: 0},\n    {\"id\": 10004, displayed: 0, reason: \"out_of_position\", full_recalc: 0},\n    {\"id\": 10005, displayed: 1, reason: \"n/a\", full_recalc: 0}\n  ]\n}\n```\n\n## Example Activity Stream `Router` Pings\n\n```js\n{\n  \"client_id\": \"n/a\",\n  \"action\": [\"snippets_user_event\" | \"onboarding_user_event\"],\n  \"impression_id\": \"{005deed0-e3e4-4c02-a041-17405fd703f6}\",\n  \"source\": \"pocket\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"source\": \"NEWTAB_FOOTER_BAR\",\n  \"message_id\": \"some_snippet_id\",\n  \"event\": \"IMPRESSION\",\n  \"event_context\": \"{\\\"view\\\":\\\"application_menu\\\"}\"\n}\n```\n\n| KEY | DESCRIPTION | &nbsp; |\n|-----|-------------|:-----:|\n| `action_position` | [Optional] The index of the element in the `source` that was clicked. | :one:\n| `action` | [Required] Either `activity_stream_event`, `activity_stream_session`, or `activity_stream_performance`. | :one:\n| `addon_version` | [Required] Firefox build ID, i.e. `Services.appinfo.appBuildID`. | :one:\n| `client_id` | [Required] An identifier for this client. | :one:\n| `card_type` | [Optional] (\"bookmark\", \"pocket\", \"trending\", \"pinned\", \"search\", \"spoc\", \"organic\") | :one:\n| `search_vendor` | [Optional] the vendor of the search shortcut, one of (\"google\", \"amazon\", \"wikipedia\", \"duckduckgo\", \"bing\", etc.). This field only exists when `card_type = \"search\"` | :one:\n| `date` | [Auto populated by Onyx] The date in YYYY-MM-DD format. | :three:\n| `experiment_id` | [Optional] The unique identifier for a specific experiment. | :one:\n| `event_id` | [Required] An identifier shared by multiple performance pings that describe an entire request flow. | :one:\n| `event` | [Required] The type of event. Any user defined string (\"click\", \"share\", \"delete\", \"more_items\") | :one:\n| `event_context` | [Optional] A string to record the context of an AS Router event ping. Compound context values will be stringified by JSON.stringify| :one:\n| `highlight_type` | [Optional] Either [\"bookmarks\", \"recommendation\", \"history\"]. | :one:\n| `impression_id` | [Optional] The unique impression identifier for a specific client. | :one:\n| `ip` | [Auto populated by Onyx] The IP address of the client. | :two:\n| `locale` | [Auto populated by Onyx] The browser chrome's language (eg. en-US). | :two:\n| `load_trigger_ts` | [Optional][Server Counter][Server Alert for too many omissions]  DOMHighResTimeStamp of the action perceived by the user to trigger the load of this page. | :one:\n| `load_trigger_type` | [Server Counter][Server Alert for too many omissions] Either [\"first_window_opened\", \"menu_plus_or_keyboard\", \"unexpected\"]. | :one:\n| `metadata_source` | [Optional] The source of which we computed metadata. Either (`MetadataService` or `Local` or `TippyTopProvider`). | :one:\n| `page` | [Required] One of [\"about:newtab\", \"about:home\", \"about:welcome\", \"unknown\" (which either means not-applicable or is a bug)]. | :one:\n| `recommender_type` | [Optional] The type of recommendation that is being shown, if any. | :one:\n| `session_duration` | [Optional][Server Counter][Server Alert for too many omissions] Time in (integer) milliseconds of the difference between the new tab becoming visible\nand losing focus. | :one:\n| `session_id` | [Optional] The unique identifier for a specific session. | :one:\n| `source` | [Required] Either (\"recent_links\", \"recent_bookmarks\", \"frecent_links\", \"top_sites\", \"spotlight\", \"sidebar\") and indicates what `action`. | :two:\n| `received_at` | [Auto populated by Onyx] The time in ms since epoch. | :three:\n| `total_bookmarks` | [Optional] The total number of bookmarks in the user's places db. | :one:\n| `total_history_size` | [Optional] The number of history items currently in the user's places db. | :one:\n| `ua` | [Auto populated by Onyx] The user agent string. | :two:\n| `unload_reason` | [Required] The reason the Activity Stream page lost focus. | :one:\n| `url` | [Optional] The URL of the recommendation shown in one of the highlights spots, if any. | :one:\n| `value` (performance) | [Required] An integer that represents the measured performance value. Can store counts, times in milliseconds, and should always be a positive integer.| :one:\n| `value` (event) | [Optional] An object with keys \"icon_type\" and \"card_type\" to record the extra information for event ping| :one:\n| `ver` | [Auto populated by Onyx] The version of the Onyx API the ping was sent to. | :one:\n| `highlights_size` | [Optional] The size of the Highlights set. | :one:\n| `highlights_data_late_by_ms` | [Optional] Time in ms it took for Highlights to become initialized | :one:\n| `topsites_data_late_by_ms` | [Optional] Time in ms it took for TopSites to become initialized | :one:\n| `topstories.domain.affinity.calculation.ms` | [Optional] Time in ms it took for domain affinities to be calculated | :one:\n| `topsites_first_painted_ts` | [Optional][Service Counter][Server Alert for too many omissions] Timestamp of when the Top Sites element finished painting (possibly with only placeholder screenshots) | :one:\n| `custom_screenshot` | [Optional] Number of topsites that display a custom screenshot. | :one:\n| `screenshot_with_icon` | [Optional] Number of topsites that display a screenshot and a favicon. | :one:\n| `screenshot` | [Optional] Number of topsites that display only a screenshot. | :one:\n| `tippytop` | [Optional] Number of topsites that display a tippytop icon. | :one:\n| `rich_icon` | [Optional] Number of topsites that display a high quality favicon. | :one:\n| `no_image` | [Optional] Number of topsites that have no screenshot. | :one:\n| `topsites_pinned` | [Optional] Number of topsites that are pinned. | :one:\n| `topsites_search_shortcuts` | [Optional] Number of search shortcut topsites. | :one:\n| `visibility_event_rcvd_ts` | [Optional][Server Counter][Server Alert for too many omissions] DOMHighResTimeStamp of when the page itself receives an event that document.visibilityState == visible. | :one:\n| `tiles` | [Required] A list of tile objects for the Pocket articles. Each tile object mush have a ID, optionally a \"pos\" property to indicate the tile position, and optionally a \"shim\" property unique to the impression from the Ad server | :one:\n| `click` | [Optional] An integer to record the 0-based index when user clicks on a Pocket tile. | :one:\n| `block` | [Optional] An integer to record the 0-based index when user blocks a Pocket tile. | :one:\n| `pocket` | [Optional] An integer to record the 0-based index when user saves a Pocket tile to Pocket. | :one:\n| `user_prefs` | [Required] The encoded integer of user's preferences. | :one: & :four:\n| `is_preloaded` | [Required] A boolean to signify whether the page is preloaded or not | :one:\n| `icon_type` | [Optional] (\"tippytop\", \"rich_icon\", \"screenshot_with_icon\", \"screenshot\", \"no_image\", \"custom_screenshot\") | :one:\n| `region` | [Optional] A string maps to pref \"browser.search.region\", which is essentially the two letter ISO 3166-1 country code populated by the Firefox search service. Note that: 1). it reports \"OTHER\" for those regions with smaller Firefox user base (less than 10000) so that users cannot be uniquely identified; 2). it reports \"UNSET\" if this pref is missing; 3). it reports \"EMPTY\" if the value of this pref is an empty string. | :one:\n| `profile_creation_date` | [Optional] An integer to record the age of the Firefox profile as the total number of days since the UNIX epoch. | :one:\n| `message_id` | [required] A string identifier of the message in Activity Stream Router. | :one:\n| `has_flow_params` | [required] One of [true, false]. A boolean identifier that indicates if Firefox Accounts flow parameters are set or unset. | :one:\n| `displayed` | [required] 1: a SPOC is displayed; 0: non-displayed | :one:\n| `reason` | [required] The reason if a SPOC is not displayed, \"n/a\" for the displayed, one of (\"frequency_cap\", \"blocked_by_user\", \"flight_duplicate\", \"probability_selection\", \"below_min_score\", \"out_of_position\", \"n/a\") | :one:\n| `full_recalc` | [required] Is it a full SPOCS recalculation: 0: false; 1: true. Recalculation case: 1). fetch SPOCS from Pocket endpoint. Non-recalculation cases: 1). An impression updates the SPOCS; 2). Any action that triggers the `selectLayoutRender ` | :one:\n\n**Where:**\n\n:one: Firefox data\n:two: HTTP protocol data\n:three: server augmented data\n:four: User preferences encoding table\n\n\nNote: the following session-related fields are not yet implemented in the system-addon,\nbut will likely be added in future versions:\n\n```js\n{\n  \"total_bookmarks\": 19,\n  \"total_history_size\": 9,\n  \"highlights_size\": 20\n}\n```\n\n## Encoding and decoding of `user_prefs`\n\nThis encoding mapping was defined in `system-addon/lib/TelemetryFeed.jsm`\n\n| Preference | Encoded value (binary) |\n| --- | ---: |\n| `showSearch` | 1 (00000001) |\n| `showTopSites` | 2 (00000010) |\n| `showTopStories` | 4 (00000100) |\n| `showHighlights` | 8 (00001000) |\n| `showSnippets`   | 16 (00010000) |\n| `showSponsored`  | 32 (00100000) |\n| `showCFRAddons`  | 64 (01000000) |\n| `showCFRFeatures` | 128 (10000000) |\n\nEach item above could be combined with other items through bitwise OR (`|`) operation.\n\nExamples:\n\n* Everything is on, `user_prefs = 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128 = 255`\n* Everything is off, `user_prefs = 0`\n* Only show search and Top Stories, `user_prefs = 1 | 4 = 5`\n* Everything except Highlights, `user_prefs = 1 | 2 | 4 | 16 | 32 | 64 | 128 = 247`\n\nLikewise, one can use bitwise AND (`&`) for decoding.\n\n* Check if everything is shown, `user_prefs & (1 | 2 | 4 | 8 | 16 | 32 | 64 | 128)` or `user_prefs == 255`\n* Check if everything is off, `user_prefs == 0`\n* Check if search is shown, `user_prefs & 1`\n* Check if both Top Sites and Top Stories are shown, `(user_prefs & 2) && (user_prefs & 4)`, or  `(user_prefs & (2 | 4)) == (2 | 4)`\n"
  },
  {
    "path": "docs/v2-system-addon/data_events.md",
    "content": "# Metrics we collect\n\nBy default, the about:newtab, about:welcome and about:home pages in Firefox (the pages you see when you open a new tab and when you start the browser), will send data back to Mozilla servers about usage of these pages.  The intent is to collect data in order to improve the user's experience while using Activity Stream.  Data about your specific browsing behaior or the sites you visit is **never transmitted to any Mozilla server**.  At any time, it is easy to **turn off** this data collection by [opting out of Firefox telemetry](https://support.mozilla.org/kb/share-telemetry-data-mozilla-help-improve-firefox).\n\nData is sent to our servers in the form of discreet HTTPS 'pings' or messages whenever you do some action on the Activity Stream about:home, about:newtab or about:welcome pages.  We try to minimize the amount and frequency of pings by batching them together.  Pings are sent in [JSON serialized format](http://www.json.org/).\n\nAt Mozilla, [we take your privacy very seriously](https://www.mozilla.org/privacy/).  The Activity Stream page will never send any data that could personally identify you.  We do not transmit what you are browsing, searches you perform or any private settings.  Activity Stream does not set or send cookies, and uses [Transport Layer Security](https://en.wikipedia.org/wiki/Transport_Layer_Security) to securely transmit data to Mozilla servers.\n\nData collected from Activity Stream is retained on Mozilla secured servers for a period of 30 days before being rolled up into an anonymous aggregated format.  After this period the raw data is deleted permanently.  Mozilla **never shares data with any third party**.\n\nThe following is a detailed overview of the different kinds of data we collect in the Activity Stream. See [data_dictionary.md](data_dictionary.md) for more details for each field.\n\n## Health ping\n\nThis is a heartbeat ping indicating whether Activity Stream is currently being used or not, it's submitted once upon the browser initialization.\n\n```js\n{\n  \"client_id\": \"374dc4d8-0cb2-4ac5-a3cf-c5a9bc3c602e\",\n  \"locale\": \"en-US\",\n  \"version\": \"62.0a1\",\n  \"release_channel\": \"nightly\",\n  \"event\": \"AS_ENABLED\",\n  \"value\": 10\n}\n```\nwhere the \"value\" is encoded as:\n  * Value 0: default\n  * Value 1: about:blank\n  * Value 2: web extension\n  * Value 3: other custom URL(s)\nTwo encoded integers for about:newtab and about:home are combined in a bitwise fashion. For instance, if both about:home and about:newtab were set to about:blank, then `value = 5 = (1 | (1 << 2))`, i.e `value = (bitfield of about:newtab) | (bitfield of about:newhome << 2)`.\n\n## Page takeover ping\n\nThis ping is submitted once upon Activity Stream initialization if either about:home or about:newtab are set to a custom URL. It sends the category of the custom URL. It also includes the web extension id of the extension controlling the home and/or newtab page.\n\n```js\n{\n  \"event\": \"PAGE_TAKEOVER_DATA\",\n  \"value\": {\n    \"home_url_category\": [\"search-engine\" | \"search-engine-mozilla-tag\" | \"search-engine-other-tag\" | \"news-portal\" | \"ecommerce\" | \"social-media\" | \"known-hijacker\" | \"other\"],\n    \"home_extension_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n    \"newtab_url_category\": [\"search-engine\" | \"search-engine-mozilla-tag\" | \"search-engine-other-tag\" | \"news-portal\" | \"ecommerce\" | \"social-media\" | \"known-hijacker\" | \"other\"],\n    \"newtab_extension_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  },\n\n  // Basic metadata\n  \"action\": \"activity_stream_event\",\n  \"client_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  \"session_id\": \"005deed0-e3e4-4c02-a041-17405fd703f6\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"user_prefs\": 7\n}\n```\n\n## User event pings\n\nThese pings are captured when a user **performs some kind of interaction** in the add-on.\n\n### Basic shape\n\nA user event ping includes some basic metadata (tab id, addon version, etc.) as well as variable fields which indicate the location and action of the event.\n\n```js\n{\n  // This indicates the type of interaction\n  \"event\": [\"CLICK\", \"SEARCH\", \"BLOCK\", \"DELETE\", \"OPEN_NEW_WINDOW\", \"OPEN_PRIVATE_WINDOW\", \"BOOKMARK_DELETE\", \"BOOKMARK_ADD\", \"OPEN_NEWTAB_PREFS\", \"CLOSE_NEWTAB_PREFS\", \"SEARCH_HANDOFF\"],\n\n  // Optional field indicating the UI component type\n  \"source\": \"TOP_SITES\",\n\n  // Optional field if there is more than one of a component type on a page.\n  // It is zero-indexed.\n  // For example, clicking the second Highlight would result in an action_position of 1\n  \"action_position\": 1,\n\n  // Basic metadata\n  \"page\": [\"about:newtab\" | \"about:home\" | \"about:welcome\" | \"unknown\" ],\n  \"client_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  \"session_id\": \"005deed0-e3e4-4c02-a041-17405fd703f6\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"action\": \"activity_stream_event\",\n  \"user_prefs\": 7\n}\n```\n\n### Types of user interactions\n\n#### Performing a search\n\n```js\n{\n  \"event\": \"SEARCH\",\n\n  // Basic metadata\n  \"action\": \"activity_stream_event\",\n  \"page\": [\"about:newtab\" | \"about:home\" | \"about:welcome\" | \"unknown\"],\n  \"client_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  \"session_id\": \"005deed0-e3e4-4c02-a041-17405fd703f6\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"user_prefs\": 7\n}\n```\n\n#### Performing a search handoff\n\n```js\n{\n  \"event\": \"SEARCH_HANDOFF\",\n\n  // Basic metadata\n  \"action\": \"activity_stream_event\",\n  \"page\": [\"about:newtab\" | \"about:home\" | \"about:welcome\" | \"unknown\"],\n  \"client_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  \"session_id\": \"005deed0-e3e4-4c02-a041-17405fd703f6\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"user_prefs\": 7\n}\n```\n\n#### Clicking a top site item\n\n```js\n{\n  \"event\": \"CLICK\",\n  \"source\": \"TOP_SITES\",\n  \"action_position\": 2,\n  \"value\": {\n    \"card_type\": [\"pinned\" | \"search\" | \"spoc\"],\n    \"icon_type\": [\"screenshot_with_icon\" | \"screenshot\" | \"tippytop\" | \"rich_icon\" | \"no_image\" | \"custom_screenshot\"],\n    // only exists if its card_type = \"search\"\n    \"search_vendor\": \"google\"\n  }\n\n  // Basic metadata\n  \"action\": \"activity_stream_event\",\n  \"page\": [\"about:newtab\" | \"about:home\" | \"about:welcome\" | \"unknown\"],\n  \"client_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  \"session_id\": \"005deed0-e3e4-4c02-a041-17405fd703f6\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"user_prefs\": 7\n}\n```\n\n#### Clicking a top story item\n\n```js\n{\n  \"event\": \"CLICK\",\n  \"source\": \"CARDGRID\",\n  \"action_position\": 2,\n  \"value\": {\n    // \"spoc\" for sponsored stories, \"organic\" for regular stories.\n    \"card_type\": [\"organic\" | \"spoc\"],\n  }\n\n  // Basic metadata\n  \"action\": \"activity_stream_event\",\n  \"page\": [\"about:newtab\" | \"about:home\" | \"about:welcome\" | \"unknown\"],\n  \"client_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  \"session_id\": \"005deed0-e3e4-4c02-a041-17405fd703f6\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"user_prefs\": 7\n}\n```\n\n#### Adding a search shortcut\n```js\n{\n  \"event\": \"SEARCH_EDIT_ADD\",\n  \"source\": \"TOP_SITES\",\n  \"action_position\": 2,\n  \"value\": {\n    \"search_vendor\": \"google\"\n  }\n\n  // Basic metadata\n  \"action\": \"activity_stream_event\",\n  \"page\": [\"about:newtab\" | \"about:home\" | \"about:welcome\" | \"unknown\"],\n  \"client_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  \"session_id\": \"005deed0-e3e4-4c02-a041-17405fd703f6\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"user_prefs\": 7\n}\n```\n\n#### Showing privacy information\n\n```js\n{\n  \"event\": \"SHOW_PRIVACY_INFO\",\n  \"source\": \"TOP_SITES\",\n  \"action_position\": 2,\n\n  // Basic metadata\n  \"action\": \"activity_stream_event\",\n  \"page\": [\"about:newtab\" | \"about:home\" | \"about:welcome\" | \"unknown\"],\n  \"client_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  \"session_id\": \"005deed0-e3e4-4c02-a041-17405fd703f6\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"user_prefs\": 7\n}\n```\n\n#### Clicking on privacy information link\n\n```js\n{\n  \"event\": \"CLICK_PRIVACY_INFO\",\n  \"source\": \"DS_PRIVACY_MODAL\",\n\n  // Basic metadata\n  \"action\": \"activity_stream_event\",\n  \"page\": [\"about:newtab\" | \"about:home\" | \"about:welcome\" | \"unknown\"],\n  \"client_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  \"session_id\": \"005deed0-e3e4-4c02-a041-17405fd703f6\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"user_prefs\": 7\n}\n```\n\n#### Deleting a search shortcut\n```js\n{\n  \"event\": \"SEARCH_EDIT_DELETE\",\n  \"source\": \"TOP_SITES\",\n  \"action_position\": 2,\n  \"value\": {\n    \"search_vendor\": \"google\"\n  }\n\n  // Basic metadata\n  \"action\": \"activity_stream_event\",\n  \"page\": [\"about:newtab\" | \"about:home\" | \"about:welcome\" | \"unknown\"],\n  \"client_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  \"session_id\": \"005deed0-e3e4-4c02-a041-17405fd703f6\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"user_prefs\": 7\n}\n```\n\n#### Deleting an item from history\n\n```js\n{\n  \"event\": \"DELETE\",\n  \"source\": \"TOP_SITES\",\n  \"action_position\": 2,\n  \"value\": {\n    \"card_type\": \"pinned\",\n    \"icon_type\": [\"screenshot_with_icon\" | \"screenshot\" | \"tippytop\" | \"rich_icon\" | \"no_image\" | \"custom_screenshot\"]\n  }\n\n  // Basic metadata\n  \"action\": \"activity_stream_event\",\n  \"page\": [\"about:newtab\" | \"about:home\" | \"about:welcome\" | \"unknown\"],\n  \"client_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  \"session_id\": \"005deed0-e3e4-4c02-a041-17405fd703f6\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"user_prefs\": 7\n}\n```\n\n#### Blocking a site\n\n```js\n{\n  \"event\": \"BLOCK\",\n  \"source\": \"TOP_SITES\",\n  \"action_position\": 2,\n  \"value\": {\n    \"card_type\": [\"pinned\" | \"search\" | \"spoc\"],\n    \"icon_type\": [\"screenshot_with_icon\" | \"screenshot\" | \"tippytop\" | \"rich_icon\" | \"no_image\" | \"custom_screenshot\"],\n    // only exists if its card_type = \"search\"\n    \"search_vendor\": \"google\"\n  }\n\n  // Basic metadata\n  \"action\": \"activity_stream_event\",\n  \"page\": [\"about:newtab\" | \"about:home\" | \"about:welcome\" | \"unknown\"],\n  \"client_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  \"session_id\": \"005deed0-e3e4-4c02-a041-17405fd703f6\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"user_prefs\": 7\n}\n```\n\n#### Bookmarking a link\n\n```js\n{\n  \"event\": \"BOOKMARK_ADD\",\n  \"source\": \"HIGHLIGHTS\",\n  \"action_position\": 2,\n  \"value\": {\n    \"card_type\": \"trending\"\n  }\n  \n  // Basic metadata\n  \"action\": \"activity_stream_event\",\n  \"page\": [\"about:newtab\" | \"about:home\" | \"about:welcome\"],\n  \"client_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  \"session_id\": \"005deed0-e3e4-4c02-a041-17405fd703f6\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"user_prefs\": 7\n}\n```\n\n#### Removing a bookmark from a link\n\n```js\n{\n  \"event\": \"BOOKMARK_DELETE\",\n  \"source\": \"HIGHLIGHTS\",\n  \"action_position\": 2,\n  \"value\": {\n    \"card_type\": \"bookmark\"\n  }\n\n  // Basic metadata\n  \"action\": \"activity_stream_event\",\n  \"page\": [\"about:newtab\" | \"about:home\" | \"about:welcome\" | \"unknown\"],\n  \"client_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  \"session_id\": \"005deed0-e3e4-4c02-a041-17405fd703f6\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"user_prefs\": 7\n}\n```\n\n#### Opening a link in a new window\n\n```js\n{\n  \"event\": \"OPEN_NEW_WINDOW\",\n  \"source\": \"TOP_SITES\",\n  \"action_position\": 2,\n  \"value\": {\n    \"card_type\": \"pinned\",\n    \"icon_type\": [\"screenshot_with_icon\" | \"screenshot\" | \"tippytop\" | \"rich_icon\" | \"no_image\" | \"custom_screenshot\"]\n  }\n\n  // Basic metadata\n  \"action\": \"activity_stream_event\",\n  \"page\": [\"about:newtab\" | \"about:home\" | \"about:welcome\" | \"unknown\"],\n  \"client_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  \"session_id\": \"005deed0-e3e4-4c02-a041-17405fd703f6\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"user_prefs\": 7\n}\n```\n\n#### Opening a link in a new private window\n\n```js\n{\n  \"event\": \"OPEN_PRIVATE_WINDOW\",\n  \"source\": \"TOP_SITES\",\n  \"action_position\": 2,\n  \"value\": {\n    \"card_type\": \"pinned\",\n    \"icon_type\": [\"screenshot_with_icon\" | \"screenshot\" | \"tippytop\" | \"rich_icon\" | \"no_image\" | \"custom_screenshot\"]\n  }\n\n  // Basic metadata\n  \"action\": \"activity_stream_event\",\n  \"page\": [\"about:newtab\" | \"about:home\" | \"about:welcome\" | \"unknown\"],\n  \"client_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  \"session_id\": \"005deed0-e3e4-4c02-a041-17405fd703f6\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"user_prefs\": 7\n}\n```\n\n#### Opening the new tab preferences pane\n\n```js\n{\n  \"event\": \"OPEN_NEWTAB_PREFS\",\n\n  // Basic metadata\n  \"action\": \"activity_stream_event\",\n  \"page\": [\"about:newtab\" | \"about:home\" | \"about:welcome\" | \"unknown\"],\n  \"client_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  \"session_id\": \"005deed0-e3e4-4c02-a041-17405fd703f6\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"user_prefs\": 7\n}\n```\n\n#### Closing the new tab preferences pane\n\n```js\n{\n  \"event\": \"CLOSE_NEWTAB_PREFS\",\n\n  // Basic metadata\n  \"action\": \"activity_stream_event\",\n  \"page\": [\"about:newtab\" | \"about:home\" | \"about:welcome\" | \"unknown\"],\n  \"client_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  \"session_id\": \"005deed0-e3e4-4c02-a041-17405fd703f6\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"user_prefs\": 7\n}\n```\n\n#### Pinning a tab\n\n```js\n{\n  \"event\": \"TABPINNED\",\n  \"source\": \"TAB_CONTEXT_MENU\",\n  \"value\": \"{\\\"total_pinned_tabs\\\":2}\",\n\n  // Basic metadata\n  \"action\": \"activity_stream_user_event\",\n  \"client_id\": \"aabaace5-35f4-7345-a28e-5502147dc93c\",\n  \"version\": \"67.0a1\",\n  \"addon_version\": \"20190218094427\",\n  \"locale\": \"en-US\",\n  \"user_prefs\": 59,\n  \"page\": \"n/a\",\n  \"session_id\": \"n/a\",\n}\n```\n\n#### Adding or editing a new TopSite\n\n```js\n{\n  \"event\": \"TOP_SITES_EDIT\",\n  \"source\": \"TOP_SITES_SOURCE\",\n  // \"-1\" Is used for prepending a new TopSite at the front of the list, while\n  // any other possible value is used for editing an existing TopSite slot.\n  \"action_position\": [-1 | \"0..TOP_SITES_LENGTH\"]\n}\n```\n\n#### Requesting a custom screenshot preview\n\n```js\n{\n  \"event\": \"PREVIEW_REQUEST\",\n  \"source\": \"TOP_SITES\"\n}\n```\n\n### Onboarding user events on about:welcome\n\n#### Form Submit Events\n\n```js\n{\n  \"event\": [\"SUBMIT_EMAIL\" | \"SUBMIT_SIGNIN\" | \"SKIPPED_SIGNIN\"],\n  \"value\": {\n    \"has_flow_params\": false,\n  }\n\n  // Basic metadata\n  \"action\": \"activity_stream_event\",\n  \"page\": \"about:welcome\",\n  \"client_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  \"session_id\": \"005deed0-e3e4-4c02-a041-17405fd703f6\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"user_prefs\": 7\n}\n```\n\n#### Firefox Accounts Metrics flow errors\n\n```js\n{\n  \"event\": [\"FXA_METRICS_FETCH_ERROR\" | \"FXA_METRICS_ERROR\"],\n  \"value\": 500, // Only FXA_METRICS_FETCH_ERROR provides this value, this value is any valid HTTP status code except 200.\n\n  // Basic metadata\n  \"action\": \"activity_stream_event\",\n  \"page\": \"about:welcome\",\n  \"client_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  \"session_id\": \"005deed0-e3e4-4c02-a041-17405fd703f6\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"user_prefs\": 7\n}\n```\n\n\n## Session end pings\n\nWhen a session ends, the browser will send a `\"activity_stream_session\"` ping to our metrics servers. This ping contains the length of the session, a unique reason for why the session ended, and some additional metadata.\n\n### Basic event\n\nAll `\"activity_stream_session\"` pings have the following basic shape. Some fields are variable.\n\n```js\n{\n  \"action\": \"activity_stream_session\",\n  \"client_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  \"session_id\": \"005deed0-e3e4-4c02-a041-17405fd703f6\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"page\": [\"about:newtab\" | \"about:home\" | \"about:welcome\" | \"unknown\"],\n  \"session_duration\": 4199,\n  \"region\": \"US\",\n  \"profile_creation_date\": 14786,\n  \"user_prefs\": 7\n}\n```\n\n### What causes a session end?\n\nHere are different scenarios that cause a session end event to be sent:\n\n1. After a search\n2. Clicking on something that causes navigation (top site, highlight, etc.)\n3. Closing the browser\n5. Refreshing\n6. Navigating to a new URL via the url bar or file menu\n\n\n### Session performance data\n\nThis data is held in a child object of the `activity_stream_session` event called `perf`.  All fields suffixed by `_ts` are type `DOMHighResTimeStamp` (aka a double of milliseconds, with a 5 microsecond precision) with 0 being the [timeOrigin](https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp#The_time_origin) of the browser's hidden chrome window.\n\nAn example might look like this:\n\n```javascript\nperf: {\n  // Timestamp of the action perceived by the user to trigger the load\n  // of this page.\n  //\n  // Not required at least for error cases where the\n  // observer event doesn't fire\n  \"load_trigger_ts\": 1,\n\n  // What was the perceived trigger of the load action:\n  \"load_trigger_type\": [\n    \"first_window_opened\" | // home only\n    \"menu_plus_or_keyboard\" | // newtab only\n    \"unexpected\" // sessions lacking actual start times\n  ],\n\n  // when the page itself receives an event that document.visibilityStat=visible\n  \"visibility_event_rcvd_ts\": 2,\n\n  // When did the topsites element finish painting?  Note that, at least for\n  // the first tab to be loaded, and maybe some others, this will be before\n  // topsites has yet to receive screenshots updates from the add-on code,\n  // and is therefore just showing placeholder screenshots.\n  \"topsites_first_painted_ts\": 5,\n\n  // The 6 different types of TopSites icons and how many of each kind did the\n  // user see.\n  \"topsites_icon_stats\": {\n    \"custom_screenshot\": 0,\n    \"screenshot_with_icon\": 2,\n    \"screenshot\": 1,\n    \"tippytop\": 2,\n    \"rich_icon\": 1,\n    \"no_image\": 0\n  },\n\n  // The number of Top Sites that are pinned.\n  \"topsites_pinned\": 3,\n\n  // The number of search shortcut Top Sites.\n  \"topsites_search_shortcuts\": 2,\n\n  // How much longer the data took, in milliseconds, to be ready for display\n  // than it would have been in the ideal case. The user currently sees placeholder\n  // cards instead of real cards for approximately this length of time. This is\n  // sent when the first call of the component's `render()` method happens with\n  // `this.props.initialized` set to `false`, and the value is the amount of\n  // time in ms until `render()` is called with `this.props.initialized` set to `true`.\n  \"highlights_data_late_by_ms\": 67,\n  \"topsites_data_late_by_ms\": 35,\n\n  // Whether the page is preloaded or not.\n  \"is_preloaded\": [true|false],\n}\n```\n\n## Top Story pings\n\nWhen Top Story (currently powered by Pocket) is enabled in Activity Stream, the browser will send following `activity_stream_impression_stats` to our metrics servers.\n\n### Impression stats\n\nThis reports all the Pocket recommended articles (a list of `id`s) when the user opens a newtab.\n\n```js\n{\n  \"action\": \"activity_stream_impression_stats\",\n\n  // both \"client_id\" and \"session_id\" are set to \"n/a\" in this ping.\n  \"client_id\": \"n/a\",\n  \"session_id\": \"n/a\",\n  \"impression_id\": \"{005deed0-e3e4-4c02-a041-17405fd703f6}\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"source\": \"pocket\",\n  \"page\": [\"about:newtab\" | \"about:home\" | \"about:welcome\" | \"unknown\"],\n  \"user_prefs\": 7,\n  \"tiles\": [{\"id\": 10000}, {\"id\": 10001}, {\"id\": 10002}]\n}\n```\n\n### Click/block/save_to_pocket ping\n\nThis reports the user's interaction with those Pocket tiles.\n\n```js\n{\n  \"action\": \"activity_stream_impression_stats\",\n\n  // both \"client_id\" and \"session_id\" are set to \"n/a\" in this ping.\n  \"client_id\": \"n/a\",\n  \"session_id\": \"n/a\",\n  \"impression_id\": \"{005deed0-e3e4-4c02-a041-17405fd703f6}\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"source\": \"pocket\",\n  \"page\": [\"about:newtab\" | \"about:home\" | \"about:welcome\" | \"unknown\"],\n  \"user_prefs\": 7,\n\n  // \"pos\" is the 0-based index to record the tile's position in the Pocket section.\n  // \"shim\" is a base64 encoded shim attached to spocs, unique to the impression from the Ad server.\n  \"tiles\": [{\"id\": 10000, \"pos\": 0, \"shim\": \"enuYa1j73z3RzxgTexHNxYPC/b,9JT6w5KB0CRKYEU+\"}],\n\n  // A 0-based index to record which tile in the \"tiles\" list that the user just interacted with.\n  \"click|block|pocket\": 0\n}\n```\n\n## Performance pings\n\nThese pings are captured to record performance related events i.e. how long certain operations take to execute.\n\n### Domain affinity calculation v1\n\nThis reports the duration of the domain affinity calculation in milliseconds.\n\n```js\n{\n  \"action\": \"activity_stream_performance_event\",\n  \"client_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"user_prefs\": 7,\n  \"event\": \"topstories.domain.affinity.calculation.ms\",\n  \"value\": 43\n}\n```\n\n### Domain affinity calculation v2\n\nThese report the duration of the domain affinity v2 calculations in milliseconds.\n\n#### Total calculation in ms\n\n```js\n{\n  \"action\": \"activity_stream_performance_event\",\n  \"client_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"user_prefs\": 7,\n  \"event\": \"PERSONALIZATION_V2_TOTAL_DURATION\",\n  \"value\": 43\n}\n```\n\n#### getRecipe calculation in ms\n\n```js\n{\n  \"action\": \"activity_stream_performance_event\",\n  \"client_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"user_prefs\": 7,\n  \"event\": \"PERSONALIZATION_V2_GET_RECIPE_DURATION\",\n  \"value\": 43\n}\n```\n\n#### RecipeExecutor calculation in ms\n\n```js\n{\n  \"action\": \"activity_stream_performance_event\",\n  \"client_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"user_prefs\": 7,\n  \"event\": \"PERSONALIZATION_V2_RECIPE_EXECUTOR_DURATION\",\n  \"value\": 43\n}\n```\n\n#### taggers calculation in ms\n\n```js\n{\n  \"action\": \"activity_stream_performance_event\",\n  \"client_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"user_prefs\": 7,\n  \"event\": \"PERSONALIZATION_V2_TAGGERS_DURATION\",\n  \"value\": 43\n}\n```\n\n#### createInterestVector calculation in ms\n\n```js\n{\n  \"action\": \"activity_stream_performance_event\",\n  \"client_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"user_prefs\": 7,\n  \"event\": \"PERSONALIZATION_V2_CREATE_INTEREST_VECTOR_DURATION\",\n  \"value\": 43\n}\n```\n\n#### calculateItemRelevanceScore calculation in ms\n\n```js\n{\n  \"action\": \"activity_stream_performance_event\",\n  \"client_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"user_prefs\": 7,\n  \"event\": \"PERSONALIZATION_V2_ITEM_RELEVANCE_SCORE_DURATION\",\n  \"value\": 43\n}\n```\n\n### History size used for v2 calculation\n\n```js\n{\n  \"action\": \"activity_stream_performance_event\",\n  \"client_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"user_prefs\": 7,\n  \"event\": \"PERSONALIZATION_V2_HISTORY_SIZE\",\n  \"value\": 43\n}\n```\n\n### Error events for v2 calculation\n\nThese report any failures during domain affinity v2 calculations, and where it failed.\n\n#### getRecipe error\n\n```js\n{\n  \"action\": \"activity_stream_performance_event\",\n  \"client_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"user_prefs\": 7,\n  \"event\": \"PERSONALIZATION_V2_GET_RECIPE_ERROR\"\n}\n```\n\n#### generateRecipeExecutor error\n\n```js\n{\n  \"action\": \"activity_stream_performance_event\",\n  \"client_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"user_prefs\": 7,\n  \"event\": \"PERSONALIZATION_V2_GENERATE_RECIPE_EXECUTOR_ERROR\"\n}\n```\n\n#### createInterestVector error\n\n```js\n{\n  \"action\": \"activity_stream_performance_event\",\n  \"client_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"user_prefs\": 7,\n  \"event\": \"PERSONALIZATION_V2_CREATE_INTEREST_VECTOR_ERROR\"\n}\n```\n\n### Discovery Stream loaded content\n\nThis reports all the loaded content (a list of `id`s and positions) when the user opens a newtab page and the page becomes visible. Note that this ping is a superset of the Discovery Stream impression ping, as impression pings are also subject to the individual visibility.\n\n```js\n{\n  \"action\": \"activity_stream_impression_stats\",\n\n  // Both \"client_id\" and \"session_id\" are set to \"n/a\" in this ping.\n  \"client_id\": \"n/a\",\n  \"session_id\": \"n/a\",\n  \"impression_id\": \"{005deed0-e3e4-4c02-a041-17405fd703f6}\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"source\": [\"HERO\" | \"CARDGRID\" | \"LIST\"],\n  \"page\": [\"about:newtab\" | \"about:home\" | \"about:welcome\" | \"unknown\"],\n  \"user_prefs\": 7,\n\n  // Indicating this is a `loaded content` ping (as opposed to impression) as well as the size of `tiles`\n  \"loaded\": 3,\n  \"tiles\": [{\"id\": 10000, \"pos\": 0}, {\"id\": 10001, \"pos\": 1}, {\"id\": 10002, \"pos\": 2}]\n}\n```\n\n### Discovery Stream performance pings\n\n#### Request time of layout feed in ms\n\n```js\n{\n  \"action\": \"activity_stream_performance_event\",\n  \"client_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"user_prefs\": 7,\n  \"event\": \"LAYOUT_REQUEST_TIME\",\n  \"value\": 42\n}\n```\n\n#### Request time of SPOCS feed in ms\n\n```js\n{\n  \"action\": \"activity_stream_performance_event\",\n  \"client_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"user_prefs\": 7,\n  \"event\": \"SPOCS_REQUEST_TIME\",\n  \"value\": 42\n}\n```\n\n#### Request time of component feed feed in ms\n\n```js\n{\n  \"action\": \"activity_stream_performance_event\",\n  \"client_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"user_prefs\": 7,\n  \"event\": \"COMPONENT_FEED_REQUEST_TIME\",\n  \"value\": 42\n}\n```\n\n#### Request time of total Discovery Stream feed in ms\n\n```js\n{\n  \"action\": \"activity_stream_performance_event\",\n  \"client_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"user_prefs\": 7,\n  \"event\": \"DS_FEED_TOTAL_REQUEST_TIME\",\n  \"value\": 136\n}\n```\n\n#### Cache age of Discovery Stream feed in second\n\n```js\n{\n  \"action\": \"activity_stream_performance_event\",\n  \"client_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"user_prefs\": 7,\n  \"event\": \"DS_CACHE_AGE_IN_SEC\",\n  \"value\": 1800 // 30 minutes\n}\n```\n\n### Discovery Stream SPOCS Fill ping\n\nThis reports the internal status of Pocket SPOCS (Sponsored Contents).\n\n```js\n{\n  // both \"client_id\" and \"session_id\" are set to \"n/a\" in this ping.\n  \"client_id\": \"n/a\",\n  \"session_id\": \"n/a\",\n  \"impression_id\": \"{005deed0-e3e4-4c02-a041-17405fd703f6}\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"version\": \"68\",\n  \"release_channel\": \"release\",\n  \"spoc_fills\": [\n    {\"id\": 10000, displayed: 0, reason: \"frequency_cap\", full_recalc: 1},\n    {\"id\": 10001, displayed: 0, reason: \"blocked_by_user\", full_recalc: 1},\n    {\"id\": 10002, displayed: 0, reason: \"below_min_score\", full_recalc: 1},\n    {\"id\": 10003, displayed: 0, reason: \"campaign_duplicate\", full_recalc: 1},\n    {\"id\": 10004, displayed: 0, reason: \"probability_selection\", full_recalc: 0},\n    {\"id\": 10004, displayed: 0, reason: \"out_of_position\", full_recalc: 0},\n    {\"id\": 10005, displayed: 1, reason: \"n/a\", full_recalc: 0}\n  ]\n}\n```\n\n## Undesired event pings\n\nThese pings record the undesired events happen in the addon for further investigation.\n\n### Addon initialization failure\n\nThis reports when the addon fails to initialize\n\n```js\n{\n  \"action\": \"activity_stream_undesired_event\",\n  \"client_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"user_prefs\": 7,\n  \"event\": \"ADDON_INIT_FAILED\",\n  \"value\": -1\n}\n```\n\n## Activity Stream Router pings\n\nThese pings record the impression and user interactions within Activity Stream Router.\n\n### Impression ping\n\nThis reports the impression of Activity Stream Router.\n\n#### Snippets impression\n```js\n{\n  \"client_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  \"action\": \"snippets_user_event\",\n  \"impression_id\": \"n/a\",\n  \"source\": \"SNIPPETS\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"source\": \"NEWTAB_FOOTER_BAR\",\n  \"message_id\": \"some_snippet_id\",\n  \"event\": \"IMPRESSION\"\n}\n```\n\nCFR impression ping has two forms, in which the message_id could be of different meanings.\n\n#### CFR impression for all the prerelease channels and shield experiment\n```js\n{\n  \"client_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  \"action\": \"cfr_user_event\",\n  \"impression_id\": \"n/a\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"source\": \"CFR\",\n  // message_id could be the ID of the recommendation, such as \"wikipedia_addon\"\n  \"message_id\": \"wikipedia_addon\",\n  \"event\": \"IMPRESSION\"\n}\n```\n\n#### CFR impression for the release channel\n```js\n{\n  \"client_id\": \"n/a\",\n  \"action\": \"cfr_user_event\",\n  \"impression_id\": \"{005deed0-e3e4-4c02-a041-17405fd703f6}\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"source\": \"CFR\",\n  // message_id should be a bucket ID in the release channel, we may not use the\n  // individual ID, such as addon ID, per legal's request\n  \"message_id\": \"bucket_id\",\n  \"event\": \"IMPRESSION\"\n}\n```\n\n#### Onboarding impression\n```js\n{\n  \"client_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  \"action\": \"onboarding_user_event\",\n  \"impression_id\": \"n/a\",\n  \"source\": \"FIRST_RUN\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"message_id\": \"EXTENDED_TRIPLETS_1\",\n  \"event\": \"IMPRESSION\"\n}\n```\n\n### User interaction pings\n\nThis reports the user's interaction with Activity Stream Router.\n\n#### Snippets interaction pings\n```js\n{\n  \"client_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  \"action\": \"snippets_user_event\",\n  \"addon_version\": \"20180710100040\",\n  \"impression_id\": \"n/a\",\n  \"locale\": \"en-US\",\n  \"source\": \"NEWTAB_FOOTER_BAR\",\n  \"message_id\": \"some_snippet_id\",\n  \"event\": [\"CLICK_BUTTION\" | \"BLOCK\"]\n}\n```\n\n#### Onboarding interaction pings\n```js\n{\n  \"client_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  \"action\": \"onboarding_user_event\",\n  \"addon_version\": \"20180710100040\",\n  \"impression_id\": \"n/a\",\n  \"locale\": \"en-US\",\n  \"source\": \"ONBOARDING\",\n  \"message_id\": \"onboarding_message_1\",\n  \"event\": [\"IMPRESSION\" | \"CLICK_BUTTION\" | \"INSTALL\" | \"BLOCK\"]\n}\n```\n\n#### CFR interaction pings for all the prerelease channels and shield experiment\n```js\n{\n  \"client_id\": \"26288a14-5cc4-d14f-ae0a-bb01ef45be9c\",\n  \"action\": \"cfr_user_event\",\n  \"addon_version\": \"20180710100040\",\n  \"impression_id\": \"n/a\",\n  \"locale\": \"en-US\",\n  \"source\": \"CFR\",\n  // message_id could be the ID of the recommendation, such as \"wikipedia_addon\"\n  \"message_id\": \"wikipedia_addon\",\n  \"event\": \"[IMPRESSION | INSTALL | PIN | BLOCK | DISMISS | RATIONALE | LEARN_MORE | CLICK | CLICK_DOORHANGER | MANAGE]\",\n  // \"modelVersion\" records the model identifier for the CFR machine learning experiment, see more detail in Bug 1594422.\n  // Non-experiment users will not report this field.\n  \"event_context\": \"{ \\\"modelVersion\\\": \\\"some_model_version_id\\\" }\"\n}\n```\n\n#### CFR interaction pings for release channel\n```js\n{\n  \"client_id\": \"n/a\",\n  \"action\": \"cfr_user_event\",\n  \"addon_version\": \"20180710100040\",\n  \"impression_id\": \"{005deed0-e3e4-4c02-a041-17405fd703f6}\",\n  \"locale\": \"en-US\",\n  \"source\": \"CFR\",\n  // message_id should be a bucket ID in the release channel, we may not use the\n  // individual ID, such as addon ID, per legal's request\n  \"message_id\": \"bucket_id\",\n  \"event\": \"[IMPRESSION | INSTALL | PIN | BLOCK | DISMISS | RATIONALE | LEARN_MORE | CLICK | CLICK_DOORHANGER | MANAGE]\"\n}\n```\n\n### Targeting error pings\n\nThis reports when an error has occurred when parsing/evaluating a JEXL targeting string in a message.\n\n```js\n{\n  \"client_id\": \"n/a\",\n  \"action\": \"asrouter_undesired_event\",\n  \"addon_version\": \"20180710100040\",\n  \"impression_id\": \"{005deed0-e3e4-4c02-a041-17405fd703f6}\",\n  \"locale\": \"en-US\",\n  \"message_id\": \"some_message_id\",\n  \"event\": \"TARGETING_EXPRESSION_ERROR\",\n  \"event_context\": [\"MALFORMED_EXPRESSION\" | \"OTHER_ERROR\"]\n}\n```\n\n### Remote Settings error pings\n\nThis reports a failure in the Remote Settings loader to load messages for Activity Stream Router.\n\n```js\n{\n  \"action\": \"asrouter_undesired_event\",\n  \"client_id\": \"n/a\",\n  \"addon_version\": \"20180710100040\",\n  \"locale\": \"en-US\",\n  \"user_prefs\": 7,\n  \"event\": [\"ASR_RS_NO_MESSAGES\" | \"ASR_RS_ERROR\"],\n  // The value is set to the ID of the message provider. For example: remote-cfr, remote-onboarding, etc.\n  \"event_context\": \"REMOTE_PROVIDER_ID\"\n}\n```\n\n## Trailhead experiment enrollment ping\n\nThis reports an enrollment ping when a user gets enrolled in a Trailhead experiment. Note that this ping is only collected through the Mozilla Events telemetry pipeline.\n\n```js\n{\n  \"category\": \"activity_stream\",\n  \"method\": \"enroll\",\n  \"object\": \"preference_study\"\n  \"value\": \"activity-stream-firstup-trailhead-interrupts\",\n  \"extra_keys\": {\n    \"experimentType\": \"as-firstrun\",\n    \"branch\": [\"supercharge\" | \"join\" | \"sync\" | \"privacy\" ...]\n  }\n}\n```\n\n## Feature Callouts interaction pings\n\nThis reports when a user has seen or clicked a badge/notification in the browser toolbar in a non-PBM window\n\n```\n{\n  \"locale\": \"en-US\",\n  \"client_id\": \"9da773d8-4356-f54f-b7cf-6134726bcf3d\",\n  \"version\": \"70.0a1\",\n  \"release_channel\": \"default\",\n  \"addon_version\": \"20190712095934\",\n  \"action\": \"cfr_user_event\",\n  \"source\": \"CFR\",\n  \"message_id\": \"FXA_ACCOUNTS_BADGE\",\n  \"event\": [\"CLICK\" | \"IMPRESSION\"],\n}\n```\n\n## Panel interaction pings\n\nThis reports when a user opens the panel, views messages and clicks on a message.\nFor message impressions we concatenate the ids of all messages in the panel.\n\n```\n{\n  \"locale\": \"en-US\",\n  \"client_id\": \"9da773d8-4356-f54f-b7cf-6134726bcf3d\",\n  \"version\": \"70.0a1\",\n  \"release_channel\": \"default\",\n  \"addon_version\": \"20190712095934\",\n  \"action\": \"cfr_user_event\",\n  \"source\": \"CFR\",\n  \"message_id\": \"WHATS_NEW_70\",\n  \"event\": [\"CLICK\" | \"IMPRESSION\"],\n  \"value\": { \"view\": [\"application_menu\" | \"toolbar_dropdown\"] }\n}\n```\n"
  },
  {
    "path": "docs/v2-system-addon/geo_locale.md",
    "content": "# Setting custom `geo`, `locale`, and update channels\n\nThere are instances where you may need to change your local build's locale, geo, and update channel (such as changes to the visibility of Discovery Stream on a per-geo/locale basis in `ActivityStream.jsm`).\n\n## Changing update channel\n\n- Change `app.update.channel` to desired value (eg: `release`) by editing `LOCAL_BUILD/Contents/Resources/defaults/pref/channel-prefs.js`. (**NOTE:** Changing pref `app.update.channel` from `about:config` seems to have no effect!)\n\n## Changing geo\n\n- Set `browser.search.region` to desired geo (eg `CA`)\n\n## Changing locale\n\n*Note: These prefs are only configurable on a nightly or local build.*\n\n- Toggle `extensions.langpacks.signatures.required` to `false`\n- Toggle `xpinstall.signatures.required` to `false`\n- Toggle `intl.multilingual.downloadEnabled` to `true`\n- Toggle `intl.multilingual.enabled` to `true`\n- Open the [langpack](https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central-l10n/mac/xpi/) for target locale in your local build (eg `firefox-70.0a1.en-CA.langpack.xpi` if you want an `en-CA` locale)\n- In `about:preferences` click \"Set Alternatives\" under \"Language\", move desired locale to the top position, click OK, click \"Apply And Restart\"\n"
  },
  {
    "path": "docs/v2-system-addon/mochitests.md",
    "content": "We use [mochitests](https://developer.mozilla.org/en-US/docs/Mozilla/Projects/Mochitest) to do functional (and possibly integration) testing. Mochitests are part of Firefox and allow us to test activity stream literally as you would use it.\n\nMochitests require a local checkout of the Firefox source code. This is because they are used to test a lot of Firefox, and you would usually run them inside Firefox. We are developing activity stream outside of Firefox, but still want to test it as part of Firefox, so we've borrowed the debugger.html infrastructure for using them.\n\nMochitests live in `system-addon/test/functional/mochitest`, and as of this writing, they are all the [`browser-chrome`](https://developer.mozilla.org/en-US/docs/Mozilla/Browser_chrome_tests) flavor of mochitests.  They currently only run against the bootstrapped version of the add-on in system-addon, not the test pilot version at the top level directory.\n\n## Getting Started\n\n**Requirements**\n\n* mercurial ( `brew install mercurial` )\n* autoconf213 ( `brew install autoconf@2.13 && brew unlink autoconf` )\n\nIf you haven't set up the mochitest environment yet, just run this:\n\n```bash\n./bin/prepare-mochitests-dev\n```\n\nThis will set up everything you need. You should run this *every time* you start working on mochitests, as it makes sure your local copy of Firefox is up-to-date.\n\nOn the first run, if you don't already have a mozilla-central repo as a sibling of your activity stream repo, this will download one and set up an [artifact build](https://developer.mozilla.org/en-US/docs/Mozilla/Developer_guide/Build_Instructions/Artifact_builds) (just think of a super fast Firefox build). It may take a while (10-15 minutes) to download and build Firefox.\n\nIf you do already have a mozilla-central repo, the script ask you if you're ok with losing any local changes in that repo, and, if so, it will merely update to  the latest bits and then export your current activity-stream repo to that\nmozilla-central.\n\nNow, you can run the mochitests like this:\n\n```\nnpm run buildmc\nnpm run mochitest\n```\n\nThe reason we use npm to run them is because, as of this writing, both the\nadd-on and the tests are turned off in the export to mozilla-central, so special arguments are needed to turn them both on.\n\nVisit the [mochitest](https://developer.mozilla.org/en-US/docs/Mozilla/Projects/Mochitest) and [`browser chrome`](https://developer.mozilla.org/en-US/docs/Mozilla/Browser_chrome_tests) MDN pages to learn more about mochitests. A few tips:\n\n* Doing ```npm run mochitest-debug``` will open a JavaScript debugger and allow you to debug the tests (sometimes can be fickle)\n\n### For Windows Developers\n\n*NOT YET TESTED FOR ACTIVITY STREAM*: The detailed instructions for setting up your environment to build Firefox for Windows can be found [here](https://developer.mozilla.org/en-US/docs/Mozilla/Developer_guide/Build_Instructions/Windows_Prerequisites). You need to install the latest `MozBuild` package. You can open a unix-flavor shell by starting:\n\n```\nC:\\mozilla-build\\start-shell.bat\n```\n\nIn the shell, navigate to the activity-stream project folder, and follow the Getting Started instructions as mentioned.\n\n\n## Making code changes\n\nThe mochitests are running against the compiled activity-stream bundle inside the Firefox checkout. This means that you need to update the bundle whenever you make code changes. `./bin/prepare-mochitests-dev` does this for you initially, but you can manually update it with:\n\n```\nnpm run buildmc\n```\n\n\nor have it automatically be updated whenever it changes if you leave ```npm run startmc``` running in a shell.\n\n\n## Adding New Tests\n\nIf you add new tests, make sure to list them in the `browser.ini` file. You will see the other tests there. Add a new entry with the same format as the others. You can also add new JS or HTML files by listing in under `support-files`. Make sure to start your test name with \"browser_\", so that the test suite knows the pick it up. E.g: \"browser_as_my_new_test.js\".\n\n## Writing Tests\n\nHere are a few tips for writing mochitests:\n\n* Only write mochitests for testing the interaction of multiple components on the page and to make sure that the protocol is working.\n* If you need to access the content page, use `ContentTask.spawn`:\n\n```js\nContentTask.spawn(gBrowser.selectedBrowser, null, function* () {\n  content.wrappedJSObject.foo();\n});\n```\n\nThe above calls the function `foo` that exists in the page itself. You can also access the DOM this way: `content.document.querySelector`, if you want to click a button or do other things. You can even you use assertions inside this callback to check DOM state.\n\n* If you run into problems running tests in e10s, refer to the [wiki](https://wiki.mozilla.org/Electrolysis/e10s_test_tips) for tips\n* Nobody likes to see intermittent oranges in their tests, so read the [docs on how to avoid them](https://developer.mozilla.org/en-US/docs/Mozilla/QA/Avoiding_intermittent_oranges)!\n"
  },
  {
    "path": "docs/v2-system-addon/preferences.md",
    "content": "# Preferences in Activity Stream\n\n## Preference branch\n\nThe preference branch for activity stream is `browser.newtabpage.activity-stream.`.\nAny preferences defined in the preference configuration will be relative to that\nbranch. For example, if a preference is defined with the name `foo`, the full\npreference as it is displayed in `about:config` will be `browser.newtabpage.activity-stream.foo`.\n\n## Defining new preferences\n\nAll preferences for Activity Stream should be defined in the `PREFS_CONFIG` Array\nfound in [lib/ActivityStream.jsm](../../system-addon/lib/ActivityStream.jsm).\nThe configuration object should have a `name` (the name of the pref), a `title`\nthat describes the functionality of the pref, and a `value`, the default value\nof the pref. Optionally a `getValue` function can be provided to dynamically\ngenerate a default pref value based on args, e.g., geo and locale. For\ndevelopers-specific defaults, an optional `value_local_dev` will be used instead\nof `value`. For example:\n\n```js\n{\n  name: \"telemetry.log\",\n  title: \"Log telemetry events in the console\",\n  value: false,\n  value_local_dev: true,\n  getValue: ({geo}) => geo === \"CA\"\n}\n```\n\n### IMPORTANT: Setting test-specific values for Mozilla Central\n\nIf a feed or feature behind a pref makes any network calls or would other be\ndisruptive for automated tests and that pref is on by default, make sure you\ndisable it for tests in Mozilla Central.\n\nYou should create a bug in Bugzilla and a patch that adds lines to turn off your\npref in the following files:\n- layout/tools/reftest/reftest-preferences.js\n- testing/profiles/prefs_general.js\n- testing/talos/talos/config.py\n\nYou can see an example in [this patch](https://github.com/mozilla/activity-stream/pull/2977).\n\n## Reading, setting, and observing preferences from `.jsm`s\n\nTo read/set/observe Activity Stream preferences, construct a `Prefs` instance found in [lib/ActivityStreamPrefs.jsm](../../system-addon/lib/ActivityStreamPrefs.jsm).\n\n```js\n// Import Prefs\nXPCOMUtils.defineLazyModuleGetter(this, \"Prefs\",\n  \"resource://activity-stream/lib/ActivityStreamPrefs.jsm\");\n\n// Create an instance\nconst prefs = new Prefs();\n```\n\nThe `Prefs` utility will set the Activity Stream branch for you by default, so you\ndon't need to worry about prefixing every pref with `browser.newtabpage.activity-stream.`:\n\n```js\nconst prefs = new Prefs();\n\n// This will return the value of browser.newtabpage.activity-stream.foo\nprefs.get(\"foo\");\n\n// This will set the value of browser.newtabpage.activity-stream.foo to true\nprefs.set(\"foo\", true)\n\n// This will call aCallback when browser.newtabpage.activity-stream.foo is changed\nprefs.observe(\"foo\", aCallback);\n\n// This will stop listening to browser.newtabpage.activity-stream.foo\nprefs.ignore(\"foo\", aCallback);\n```\n\nSee [toolkit/modules/Preferences.jsm](https://dxr.mozilla.org/mozilla-central/source/toolkit/modules/Preferences.jsm)\nfor more information about what methods are available.\n\n## Discovery Stream Preferences\n\nPreferences specific to the Discovery Stream are nested under the sub-branch `browser.newtabpage.activity-stream.discoverystream` (with the exception of `browser.newtabpage.blocked`).\n\n#### `browser.newtabpage.activity-stream.discoverystream.flight.blocks`\n\n- Type: `string (JSON)`\n- Default: `{}`\n- Pref Type: AS\n\nNot intended for user configuration, but is programmatically updated. Used for tracking blocked flight IDs when a user dismisses a SPOC. Keys are flight IDs. Values don't have a specific meaning.\n\n#### `browser.newtabpage.blocked`\n\n- Type: `string (JSON)`\n- Default: `null`\n- Pref Type: AS\n\nNot intended for user configuration, but is programmatically updated. Used for tracking blocked story IDs when a user dismisses one. Keys are story IDs. Values don't have a specific meaning.\n\n#### `browser.newtabpage.activity-stream.discoverystream.config`\n\n- Type `string (JSON)`\n- Default:\n  ```\n  {\n     \"api_key_pref\": \"extensions.pocket.oAuthConsumerKey\",\n     \"collapsible\": true,\n     \"enabled\": true,\n     \"show_spocs\": true,\n     \"hardcoded_layout\": true,\n     \"personalized\": true,\n     \"layout_endpoint\": \"https://getpocket.cdn.mozilla.net/v3/newtab/layout?version=1&consumer_key=$apiKey&layout_variant=basic\"\n  }\n  ```\n  - `api_key_pref` (string): The name of a variable containing the key for the Pocket API.\n  - `collapsible` (boolean): Controls whether the sections in new tab can be collapsed.\n  - `enabled` (boolean): Controls whether DS is turned on and is programmatically set based on a user's locale. DS enablement is a logical `AND` of this and the value of `browser.newtabpage.activity-stream.discoverystream.enabled`.\n  - `show_spocs` (boolean): Show sponsored content in new tab.\n  - `hardcoded_layout` (boolean): When this is true, a hardcoded layout shipped with Firefox will be used instead of a remotely fetched layout definition.\n  - `personalized` (boolean): When this is `true` personalized content based on browsing history will be displayed.\n  - `layout_endpoint` (string): The URL for a remote layout definition that will be used if `hardcoded_layout` is `false`.\n  - `unused_key` (string): This is not set by default and is unused by this codebase. It's a standardized way to differentiate configurations to prevent experiment participants from being unenrolled.\n\n#### `browser.newtabpage.activity-stream.discoverystream.enabled`\n\n- Type: `boolean`\n- Default: `true`\n- Pref Type: Firefox\n\nWhen this is set to `true` the Discovery Stream experience will show up if `enabled` is also `true` on `browser.newtabpage.activity-stream.discoverystream.config`. Otherwise the old Activity Stream experience will be shown.\n\n#### `browser.newtabpage.activity-stream.discoverystream.endpointSpocsClear`\n\n- Type: `string (URL)`\n- Default: `https://spocs.getpocket.com/user`\n- Pref Type: AS\n\nEndpoint for when a user opts-out of sponsored content to delete the corresponding data from the ad server.\n\n#### `browser.newtabpage.activity-stream.discoverystream.endpoints`\n\n- Type: `string (URLs, CSV)`\n- Default: `https://getpocket.cdn.mozilla.net/,https://spocs.getpocket.com/`\n- Pref Type: AS\n\nA whitelist of endpoints that are allowed to be used by Discovery Stream for remote content (eg: story metadata) and configuration (eg: remote layout definitions for experimentation).\n\n#### `browser.newtabpage.activity-stream.discoverystream.engagementLabelEnabled`\n\n- Type: `boolean`\n- Default: `false`\n- Pref Type: AS\n\nA flag controlling the visibility of engagement labels on cards (eg: \"Trending\" or \"Popular\").\n\n#### `browser.newtabpage.activity-stream.discoverystream.hardcoded-basic-layout`\n\n- Type: `boolean`\n- Default: `false`\n- Pref Type: Firefox\n\nIf this is `false` the default hardcoded layout is used, and if it's `true` then an alternate hardcoded layout (that currently simulates the older AS experience) is used.\n\n#### `browser.newtabpage.activity-stream.discoverystream.rec.impressions`\n\n- Type: `string (JSON)`\n- Default: `{}`\n- Pref Type: AS\n\nProgrammatically generated hash table where the keys are recommendation IDs and the values are timestamps representing the first impression.\n\n#### `browser.newtabpage.activity-stream.discoverystream.spoc.impressions`\n\n- Type: `string`\n- Default: `{}`\n- Pref Type: AS\n\nProgrammatically generated hash table where the keys are sponsored content IDs and the values are arrays of timestamps for every impression.\n\n#### `browser.newtabpage.activity-stream.discoverystream.spocs-endpoint`\n\n- Type: `string`\n- Default: `null`\n- Pref Type: Firefox\n\nOverride to specify endpoint for SPOCs. Will take precedence over remote and hardcoded layout SPOC endpoints.\n"
  },
  {
    "path": "docs/v2-system-addon/remote_cfr.md",
    "content": "# Remote CFR Messages\nStarting in Firefox 68, CFR messages will be defined using [Remote Settings](https://remote-settings.readthedocs.io/en/latest/index.html). In this document, we'll cover how how to set up a dev environment.\n\n## Using a dev server for Remote CFR\n\n**1. Setup the Remote Settings dev server with the CFR messages.**\nNote that the dev server gets wiped every 24 hours so this will be have to be done once a day. You can check if there currently are any messages [here](https://kinto.dev.mozaws.net/v1//buckets/main/collections/cfr/records).\n\n```bash\nSERVER=https://kinto.dev.mozaws.net/v1\n\n# create user\ncurl -X PUT ${SERVER}/accounts/devuser \\\n     -d '{\"data\": {\"password\": \"devpass\"}}' \\\n     -H 'Content-Type:application/json'\n\nBASIC_AUTH=devuser:devpass\n\n# create collection\nCID=cfr\ncurl -X PUT ${SERVER}/buckets/main/collections/${CID} \\\n     -H 'Content-Type:application/json' \\\n     -u ${BASIC_AUTH}\n\n# post a message\ncurl -X POST ${SERVER}/buckets/main/collections/${CID}/records \\\n     -d '{\"data\":{\"id\":\"PIN_TAB\",\"template\":\"cfr_doorhanger\",\"content\":{\"category\":\"cfrFeatures\",\"bucket_id\":\"CFR_PIN_TAB\",\"notification_text\":{\"string_id\":\"cfr-doorhanger-extension-notification\"},\"heading_text\":{\"string_id\":\"cfr-doorhanger-pintab-heading\"},\"info_icon\":{\"label\":{\"string_id\":\"cfr-doorhanger-extension-sumo-link\"},\"sumo_path\":\"extensionrecommendations\"},\"text\":{\"string_id\":\"cfr-doorhanger-pintab-description\"},\"descriptionDetails\":{\"steps\":[{\"string_id\":\"cfr-doorhanger-pintab-step1\"},{\"string_id\":\"cfr-doorhanger-pintab-step2\"},{\"string_id\":\"cfr-doorhanger-pintab-step3\"}]},\"buttons\":{\"primary\":{\"label\":{\"string_id\":\"cfr-doorhanger-pintab-ok-button\"},\"action\":{\"type\":\"PIN_CURRENT_TAB\"}},\"secondary\":[{\"label\":{\"string_id\":\"cfr-doorhanger-extension-cancel-button\"},\"action\":{\"type\":\"CANCEL\"}},{\"label\":{\"string_id\":\"cfr-doorhanger-extension-never-show-recommendation\"}},{\"label\":{\"string_id\":\"cfr-doorhanger-extension-manage-settings-button\"},\"action\":{\"type\":\"OPEN_PREFERENCES_PAGE\",\"data\":{\"category\":\"general-cfrfeatures\"}}}]}},\"targeting\":\"locale == \\\"en-US\\\" && !hasPinnedTabs && recentVisits[.timestamp > (currentDate|date - 3600 * 1000 * 1)]|length >= 3\",\"frequency\":{\"lifetime\":3},\"trigger\":{\"id\":\"frequentVisits\",\"params\":[\"docs.google.com\",\"www.docs.google.com\",\"calendar.google.com\",\"messenger.com\",\"www.messenger.com\",\"web.whatsapp.com\",\"mail.google.com\",\"outlook.live.com\",\"facebook.com\",\"www.facebook.com\",\"twitter.com\",\"www.twitter.com\",\"reddit.com\",\"www.reddit.com\",\"github.com\",\"www.github.com\",\"youtube.com\",\"www.youtube.com\",\"feedly.com\",\"www.feedly.com\",\"drive.google.com\",\"amazon.com\",\"www.amazon.com\",\"messages.android.com\"]}}}' \\\n     -H 'Content-Type:application/json' \\\n     -u ${BASIC_AUTH}\n```\n\nNow there should be a message listed: https://kinto.dev.mozaws.net/v1//buckets/main/collections/cfr/records\n\nNOTE: The collection and messages can also be created manually using the [admin interface](https://kinto.dev.mozaws.net/v1/admin/).\n\n**2. Set Remote Settings prefs to use the dev server.**\n\n```javascript\nServices.prefs.setStringPref(\"services.settings.server\", \"https://kinto.dev.mozaws.net/v1\");\n\n// Disable signature verification\nconst { RemoteSettings } = ChromeUtils.import(\"resource://services-settings/remote-settings.js\", {});\n\nRemoteSettings(\"cfr\").verifySignature = false;\n```\n\n**3. Set ASRouter CFR pref to use Remote Settings provider and enable asrouter devtools.**\n\n```javascript\nServices.prefs.setStringPref(\"browser.newtabpage.activity-stream.asrouter.providers.cfr\", JSON.stringify({\"id\":\"cfr-remote\",\"enabled\":true,\"type\":\"remote-settings\",\"bucket\":\"cfr\",\"frequency\":{\"custom\":[{\"period\":\"daily\",\"cap\":1}]},\"categories\":[\"cfrAddons\",\"cfrFeatures\"]}));\nServices.prefs.setBoolPref(\"browser.newtabpage.activity-stream.asrouter.devtoolsEnabled\", true);\n```\n\n**4. Go to `about:newtab#devtools`**\nThere should be a \"cfr-remote\" provider listed.\n\n## Using the staging server for Remote CFR\n\nIf your message is published in the staging environment the easiest way to test is using the [Remote Settings Devtools](https://github.com/mozilla/remote-settings-devtools/releases) addon. You can install this by going to `about:debugging` and using the `Load Temporary Addon` feature.\nThe devtools allow you to switch your profile between production and staging and takes care of correctly flipping all the required preferences.\n\n## Remote l10n\nBy default, all CFR messages are localized with the remote Fluent files hosted in `ms-language-packs` on Remote Settings. For local test and development, you can force ASRouter to use the local Fluent files by flipping the pref `browser.newtabpage.activity-stream.asrouter.useRemoteL10n`.\n"
  },
  {
    "path": "docs/v2-system-addon/sections.md",
    "content": "# Sections in Activity Stream\n\nEach section in Activity Stream displays data from a corresponding section feed\nin a standardised `Section` UI component. Each section feed is responsible for\nlistening to events and updating the section options such as the title, icon,\nand rows (the cards for the section to display).\n\nThe `Section` UI component displays the rows provided by the section feed. If no\nrows are available it displays an empty state consisting of an icon and a\nmessage. Optionally, the section may have a info option menu that is displayed\nwhen users hover over the info icon.\n\nOn load, `SectionsManager` and `SectionsFeed` in `SectionsManager.jsm` add the\nsections configured in the `BUILT_IN_SECTIONS` map to the state. These sections\nare initially disabled, so aren't visible. The section's feed may use the\nmethods provided by the `SectionsManager` to enable its section and update its\nproperties.\n\nThe section configuration in `BUILT_IN_SECTIONS` consists of a generator\nfunction keyed by the pref name for the section feed. The generator function\ntakes an `options` argument as the only parameter, which is passed the object\nstored as serialised JSON in the pref `{feed_pref_name}.options`, or the empty\nobject if this doesn't exist. The generator returns a section configuration\nobject which may have the following properties:\n\nProperty | Type | Description\n--- | --- | ---\nid | String | Non-optional unique id.\ntitle | Localisation object | Has property `id`, the string localisation id, and optionally a `values` object to fill in placeholders.\nicon | String | Icon id. New icons should be added in icons.scss.\nmaxRows | Integer | Maximum number of rows of cards to display. Should be >= 1.\ncontextMenuOptions | Array of strings | The menu options to provide in the card context menus.\nshouldHidePref | Boolean | If true, will the section preference in the preferences pane will not be shown.\npref | Object | Configures the section preference to show in the preferences pane. Has properties `titleString` and `descString`.\nemptyState | Object | Configures the empty state of the section. Has properties `message` and `icon`.\n\n## Section feeds\n\nEach section feed should be controlled by the pref `feeds.section.{section_id}`.\n\n### Enabling the section\n\nThe section feed must listen for the events `INIT` (dispatched when Activity\nStream is initialised) and `FEED_INIT` (dispatched when a feed is re-enabled\nhaving been turned off, with the feed id as the `data`). On these events it must\ncall `SectionsManager.enableSection(id)`. Care should be taken that this happens\nonly once `SectionsManager` has also initialised; the feed can use the method\n`SectionsManager.onceInitialized()`.\n\n### Disabling the section\n\nThe section feed must have an `uninit` method. This is called when the section\nfeed is disabled by turning the section's pref off. In `uninit` the feed must\ncall `SectionsManager.disableSection(id)`. This will remove the section's UI\ncomponent from every existing Activity Stream page.\n\n### Updating the section rows\n\nThe section feed can call `SectionsManager.updateSection(id, options)` to update\nsection options. The `rows` array property of `options` stores the cards of\nsites to display. Each card object may have the following properties:\n\n```js\n{\n  type, // One of the types in Card/types.js, e.g. \"Trending\"\n  title, // Title string\n  description, // Description string\n  image, // Image url\n  url // Site url\n}\n```\n"
  },
  {
    "path": "docs/v2-system-addon/telemetry.md",
    "content": "# Adding/Changing Telemetry Checklist\n\nAdding telemetry generally involves a few steps:\n\n1. File a \"user story\" bug about who wants what question answered. This will be used to track the client-side implementation as well as the data review request. If the server side changes are needed, ask Nan (:nanj / @ncloudio) if in doubt, bugs will be filed separately as dependencies.\n1. Implement as usual...\n1. Update `system-addon/test/schemas/pings.js` with a commented JOI schema of your changes, and add tests to system-addon/test/unit/TelemetryFeed.test.js to exercise the ping creation.\n1. Update [data_events.md](data_events.md) with an example of the data in question.\n1. Update any fields that you've added, deleted, or changed in [data_dictionary.md](data_dictionary.md).\n1. Get review from Nan on the data schema and the documentation changes.\n1. Request `data-review` of your documentation changes from a [data steward](https://wiki.mozilla.org/Firefox/Data_Collection) to ensure suitability for collection controlled by the opt-out `datareporting.healthreport.uploadEnabled` pref. Download and fill out the [data review request form](https://github.com/mozilla/data-review/blob/master/request.md) and then attach it as a text file on Bugzilla so you can r? a data steward. We've been working with Chris H-C (:chutten) for the Firefox specific telemetry, and Kenny Long (kenny@getpocket.com) for the Pocket specific telemetry, they are the best candidates for the review work as they know well about the context.\n1. After landing the implementation, check with Nan to make sure the pings are making it to the database.\n1. Once data flows in, you can build dashboard for the new telemetry on [Redash](https://sql.telemetry.mozilla.org/dashboards). If you're looking for some help about Redash or dashboard building, Nan is the guy for that.\n"
  },
  {
    "path": "docs/v2-system-addon/test-merges.md",
    "content": "## bin/test-merges.js documentation\n\nA script intended to be run from cron regularly.  It notices when a new PR has been merged to github, and then exports the code to a copy of mozilla-central and pushes it to pine, so that all the tests can be run.  It annotates the PR with the link to treeherder with the test results.\n\nSetup, needs to happen before first run:\n\nEnsure that mozilla/activity-stream has a label called pushed-to-pine.\n\n```bash\n# mkdir /home/monkey/as-pine-testing\n# cd /home/monkey/as-pine-testing\n# git clone https://github.com/mozilla/activity-stream.git\n# npm install\n```\n\nExample usage:\n\n```bash\nAS_PINE_TOKEN=01234567890 \\\nAS_PINE_TEST_DIR=/home/monkey/as-pine-testing \\\nnode bin/test-merges.js\n```\n\nAS_PINE_TOKEN is a github token for accessing mozilla/activity-stream. We use a token from the github user that has access to the mozilla/activity-stream repo (in order to label issues), and nothing else.\n\nAS_PINE_TEST_DIR should be a single directory which will contain local copies of both the activity-stream github repo and mozilla-central.  It's highly advised that AS_PINE_TEST_DIR be used for nothing else, to avoid accidentally clobbering real work.\n"
  },
  {
    "path": "docs/v2-system-addon/tippytop.md",
    "content": "# TippyTop in Activity Stream\nTippyTop, a collection of icons from the Alexa top sites, provides high quality images for the Top Sites in Activity Stream. The TippyTop manifest is hosted on S3, and then moved to [Remote Settings](https://remote-settings.readthedocs.io/en/latest/index.html) since Firefox 63. In this document, we'll cover how we produce and manage TippyTop manifest for Activity Stream.\n\n## TippyTop manifest production\nTippyTop manifest is produced by [tippy-top-sites](https://github.com/mozilla/tippy-top-sites).\n\n```sh\n# set up the environment, only needed for the first time\n$ pip install -r requirements.txt\n$ python make_manifest.py --count 2000 > icons.json  # Alexa top 2000 sites\n```\n\nBecause the manifest is hosted remotely, we use another repo [tippytop-service](https://github.com/mozilla-services/tippytop-service) for the version control and deployment. Ask :nanj or :r1cky for permission to access this private repo.\n\n## TippyTop manifest publishing\nFor each new manifest release, firstly you should tag it in the tippytop-service repo, then publish it as follows:\n\n### For Firefox 62 and below\nFile a deploy bug with the tagged version at Bugzilla as [Activity Streams: Application Servers](https://bugzilla.mozilla.org/enter_bug.cgi?product=Firefox&component=Activity%20Streams%3A%20Application%20Servers), assign it to our system engineer :jbuck, he will take care of the rest.\n\n### For Firefox 63 and beyond\nActivity Stream started using Remote Settings to manage TippyTop manifest since Firefox 63. To be able to publish new manifest, you need to be in the author&reviewer group of Remote Settings. See more details in this [mana page](https://mana.mozilla.org/wiki/pages/viewpage.action?pageId=66655528). You can also ask :nanj or :leplatram to get this set up for you.\nTo publish the manifest to Remote Settings, go to the tippytop-service repo, and run the script as follows,\n\n```sh\n# set up the remote setting, only needed for the first time\n$ python3 -m venv .venv\n$ source .venv/bin/activate\n$ pip install -r requirements.txt\n\n# publish it to prod\n$ source .venv/bin/activate\n# It will ask you for your LDAP user name and password.\n$ ./upload2remotesettings.py prod\n```\n\nAfter uploading it to Remote Setting, you can request for review in the [dashboard](https://settings-writer.prod.mozaws.net/v1/admin/). Note that you will need to log in the Mozilla LDAP VPN for both uploading and accessing Remote Setting's dashboard. Once your request gets approved by the reviewer, the new manifest will be content signed and published to production.\n\n## TippyTop Viewer\nYou can use this [viewer](https://mozilla.github.io/tippy-top-sites/manifest-viewer/) to load all the icons in the current manifest.\n"
  },
  {
    "path": "docs/v2-system-addon/unit_testing_guide.md",
    "content": "# Unit testing in Activity Stream\n\n## Overview\n\nOur unit tests in Activity Stream are written with mocha, chai, and sinon, and run\nwith karma. They include unit tests for both content code (React components, etc.)\nand `.jsm`s.\n\nYou can find unit tests in `system-addon/tests/unit`.\n\n## Running tests\n\nTo run the unit tests once, run `npm run testmc`.\n\nTo run unit tests continuously (i.e. in \"test-driven development\" mode), you can\nrun `npm run tddmc`.\n\n## Debugging tests\n\nTo debug tests, you should run them in continuous mode with `npm run tddmc`. In the\nFirefox window that is opened (it should say \"Karma... - connected\"), click the\n\"debug\" button and open your console to see test output, set breakpoints, etc.\n\nUnfortunately, source maps for tests do not currently work in Firefox. If you need\nto see line numbers, you can run the tests with Chrome by running\n`npm run tddmc -- --browsers Chrome`\n\n## Where to put new tests\n\nIf you are creating a new test, add it to a subdirectory of the `system-addon/tests/unit`\nthat corresponds to the file you are testing. Tests should end with `.test.js` or\n`.test.jsx` if the test includes any jsx.\n\nFor example, if the file you are testing is `system-addon/lib/Foo.jsm`, the test\nfile should be `system-addon/test/unit/lib/Foo.test.js`\n\n## Mocha tests\n\nAll our unit tests are written with [mocha](https://mochajs.org), which injects\nglobals like `describe`, `it`, `beforeEach`, and others. It can be used to write\nsynchronous or asynchronous tests:\n\n```js\ndescribe(\"FooModule\", () => {\n  // A synchronous test\n  it(\"should create an instance\", () => {\n    assert.instanceOf(new FooModule(), FooModule);\n  });\n  describe(\"#meaningOfLife\", () => {\n    // An asynchronous test\n    it(\"should eventually get the meaning of life\", async () => {\n      const foo = new FooModule();\n      const result = await foo.meaningOfLife();\n      assert.equal(result, 42);\n    });\n  });\n});\n```\n\n## Assertions\n\nTo write assertions, use the globally available `assert` object (this is provided\nby karma-chai, so you do not need to `require` it).\n\nFor example:\n\n```js\nassert.equal(foo, 3);\nassert.propertyVal(someObj, \"foo\", 3);\nassert.calledOnce(someStub);\n```\n\nYou can use any of the assertions from:\n\n- [`chai`](http://chaijs.com/api/assert/).\n- [`sinon-chai`](https://github.com/domenic/sinon-chai#assertions)\n\n### Custom assertions\n\nWe have some custom assertions for checking various types of actions:\n\n#### `.isUserEventAction(action)`\n\nAsserts that a given `action` is a valid User Event, i.e. that it contains only\nexpected/valid properties for User Events in Activity Stream.\n\n```js\n// This will pass\nassert.isUserEventAction(ac.UserEvent({event: \"CLICK\"}));\n\n// This will fail\nassert.isUserEventAction({type: \"FOO\"});\n\n// This will fail because BLOOP is not a valid event type\nassert.isUserEventAction(ac.UserEvent({event: \"BLOOP\"}));\n```\n\n## Overriding globals in `.jsm`s\n\nMost `.jsm`s you will be testing use `Cu.import` or `XPCOMUtils` to inject globals.\nIn order to add mocks/stubs/fakes for these globals, you should use the `GlobalOverrider`\nutility in `system-addon/test/unit/utils`:\n\n```js\nconst {GlobalOverrider} = require(\"test/unit/utils\");\ndescribe(\"MyModule\", () => {\n  let globals;\n  let sandbox;\n  beforeEach(() => {\n    globals = new GlobalOverrider();\n    sandbox = globals.sandbox; // this is a sinon sandbox\n    // This will inject a \"AboutNewTab\" global before each test\n    globals.set(\"AboutNewTab\", {override: sandbox.stub()});\n  });\n  // globals.restore() clears any globals you added as well as the sinon sandbox\n  afterEach(() => globals.restore());\n});\n```\n\n## Testing React components\n\nYou should use the [enzyme](https://github.com/airbnb/enzyme) suite of test utilities\nto test React Components for Activity Stream.\n\nWhere possible, use the [shallow rendering method](https://github.com/airbnb/enzyme/blob/master/docs/api/shallow.md) (this will avoid unnecessarily\nrendering child components):\n\n```js\nconst React = require(\"react\");\nconst {shallow} = require(\"enzyme\");\n\ndescribe(\"<Foo>\", () => {\n  it(\"should be hidden by default\", () => {\n    const wrapper = shallow(<Foo />);\n    assert.isTrue(wrapper.find(\".wrapper\").props().hidden);\n  });\n});\n```\n\nIf you need to, you can also do [Full DOM rendering](https://github.com/airbnb/enzyme/blob/master/docs/api/mount.md)\nwith enzyme's `mount` utility.\n\n```js\nconst React = require(\"react\");\nconst {mount} = require(\"enzyme\");\n...\nconst wrapper = mount(<Foo />);\n```\n"
  },
  {
    "path": "hooks/post-commit",
    "content": "#!/bin/sh\n#\n# Clean up any weirdness left around by prettier execution from pre-commit\n# hook.  Can happen for some workflows (eg `git commit .`).\n#\n# Install by executing\n#\n#   ln -s ../../hooks/post-commit .git/hooks/post-commit\n#\n# at the top-level of the activity-stream github repo.\ngit update-index -g\n"
  },
  {
    "path": "hooks/pre-commit",
    "content": "#!/bin/sh\n#\n# Recommended pre-commit git hook for activity-stream github repo\n#\n# Install by executing\n#\n#   ln -s ../../hooks/pre-commit .git/hooks/pre-commit\n#\n# at the top-level of the activity-stream github repo.\n#\n# Runs `eslint --fix` on all selected files, which, given our current\n# prettier configuration, means prettifying these files as well.  The\n# commit will be aborted if eslint exits with a failure code.\n#\n# Based on the example script in the prettier docs at\n# https://prettier.io/docs/en/precommit.html\n\nFILES=$(git diff --cached --name-only --diff-filter=ACM \"*.js\" \"*.jsx\" \"*.jsm\" | sed 's| |\\\\ |g')\n[ -z \"$FILES\" ] && exit 0\n\necho \"$FILES\" | xargs ./node_modules/.bin/eslint --cache --fix\nif [ $? -ne 0 ]\nthen\n  echo \"eslint found errors but was unable to fix them all with --fix.\"\n  echo \"Please check the output, resolve any issues, and retry.\"\n  echo \"If you want to commit anyway, pass the --no-verify flag to git commit.\"\n  exit -1\nfi\n\n# Add back the modified/prettified files to staging\necho \"$FILES\" | xargs git add\n\nexit 0\n"
  },
  {
    "path": "jar.mn",
    "content": "# This Source Code Form is subject to the terms of the Mozilla Public\n# License, v. 2.0. If a copy of the MPL was not distributed with this\n# file, You can obtain one at http://mozilla.org/MPL/2.0/.\n\nbrowser.jar:\n% resource activity-stream %res/activity-stream/ contentaccessible=yes\n  res/activity-stream/lib/ (./lib/*)\n  res/activity-stream/common/ (./common/*)\n  res/activity-stream/vendor/Redux.jsm (./vendor/Redux.jsm)\n  res/activity-stream/vendor/react.js (./vendor/react.js)\n  res/activity-stream/vendor/react-dom.js (./vendor/react-dom.js)\n#ifndef RELEASE_OR_BETA\n  res/activity-stream/vendor/react-dev.js (./vendor/react-dev.js)\n  res/activity-stream/vendor/react-dom-dev.js (./vendor/react-dom-dev.js)\n#endif\n  res/activity-stream/vendor/prop-types.js (./vendor/prop-types.js)\n  res/activity-stream/vendor/react-transition-group.js (./vendor/react-transition-group.js)\n  res/activity-stream/vendor/redux.js (./vendor/redux.js)\n  res/activity-stream/vendor/react-redux.js (./vendor/react-redux.js)\n  res/activity-stream/data/content/assets/ (./data/content/assets/*)\n  res/activity-stream/data/content/tippytop/ (./data/content/tippytop/*)\n  res/activity-stream/data/content/activity-stream.bundle.js (./data/content/activity-stream.bundle.js)\n#ifdef XP_MACOSX\n  res/activity-stream/css/activity-stream.css (./css/activity-stream-mac.css)\n#elifdef XP_WIN\n  res/activity-stream/css/activity-stream.css (./css/activity-stream-windows.css)\n#else\n  res/activity-stream/css/activity-stream.css (./css/activity-stream-linux.css)\n#endif\n  res/activity-stream/prerendered/activity-stream.html (./prerendered/activity-stream.html)\n#ifndef RELEASE_OR_BETA\n  res/activity-stream/prerendered/activity-stream-debug.html (./prerendered/activity-stream-debug.html)\n#endif\n  res/activity-stream/prerendered/activity-stream-noscripts.html (./prerendered/activity-stream-noscripts.html)\n"
  },
  {
    "path": "karma.mc.config.js",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nconst path = require(\"path\");\n\nconst PATHS = {\n  // Where is the entry point for the unit tests?\n  testEntryFile: path.resolve(__dirname, \"test/unit/unit-entry.js\"),\n\n  // A glob-style pattern matching all unit tests\n  testFilesPattern: \"test/unit/**/*.js\",\n\n  // The base directory of all source files (used for path resolution in webpack importing)\n  moduleResolveDirectory: __dirname,\n\n  // a RegEx matching all Cu.import statements of local files\n  resourcePathRegEx: /^resource:\\/\\/activity-stream\\//,\n\n  coverageReportingPath: \"logs/coverage/\",\n};\n\n// When tweaking here, be sure to review the docs about the execution ordering\n// semantics of the preprocessors array, as they are somewhat odd.\nconst preprocessors = {};\npreprocessors[PATHS.testFilesPattern] = [\n  \"webpack\", // require(\"karma-webpack\")\n  \"sourcemap\", // require(\"karma-sourcemap-loader\")\n];\n\nmodule.exports = function(config) {\n  const isTDD = config.tdd;\n  const browsers = isTDD ? [\"Firefox\"] : [\"FirefoxHeadless\"]; // require(\"karma-firefox-launcher\")\n  config.set({\n    singleRun: !isTDD,\n    browsers,\n    customLaunchers: {\n      FirefoxHeadless: {\n        base: \"Firefox\",\n        flags: [\"--headless\"],\n      },\n    },\n    frameworks: [\n      \"chai\", // require(\"chai\") require(\"karma-chai\")\n      \"mocha\", // require(\"mocha\") require(\"karma-mocha\")\n      \"sinon\", // require(\"sinon\") require(\"karma-sinon\")\n    ],\n    reporters: [\n      \"coverage-istanbul\", // require(\"karma-coverage\")\n      \"mocha\", // require(\"karma-mocha-reporter\")\n\n      // for bin/try-runner.js to parse the output easily\n      \"json\", // require(\"karma-json-reporter\")\n    ],\n    jsonReporter: {\n      // So this doesn't get interleaved with other karma output\n      stdout: false,\n      outputFile: path.join(\"logs\", \"karma-run-results.json\"),\n    },\n    coverageIstanbulReporter: {\n      reports: [\"html\", \"text-summary\"],\n      dir: PATHS.coverageReportingPath,\n      // This will make karma fail if coverage reporting is less than the minimums here\n      thresholds: !isTDD && {\n        each: {\n          statements: 100,\n          lines: 100,\n          functions: 100,\n          branches: 66,\n          overrides: {\n            \"lib/ActivityStreamStorage.jsm\": {\n              statements: 100,\n              lines: 100,\n              functions: 100,\n              branches: 83,\n            },\n            \"lib/UTEventReporting.jsm\": {\n              statements: 100,\n              lines: 100,\n              functions: 100,\n              branches: 75,\n            },\n            \"lib/*.jsm\": {\n              statements: 100,\n              lines: 100,\n              functions: 100,\n              branches: 84,\n            },\n            \"content-src/components/DiscoveryStreamComponents/**/*.jsx\": {\n              statements: 90.48,\n              lines: 90.48,\n              functions: 85.71,\n              branches: 68.75,\n            },\n            \"content-src/asrouter/**/*.jsx\": {\n              statements: 57,\n              lines: 58,\n              functions: 60,\n              branches: 50,\n            },\n            \"content-src/components/ASRouterAdmin/*.jsx\": {\n              statements: 0,\n              lines: 0,\n              functions: 0,\n              branches: 0,\n            },\n            \"content-src/components/**/*.jsx\": {\n              statements: 51.1,\n              lines: 52.38,\n              functions: 31.2,\n              branches: 31.2,\n            },\n          },\n        },\n      },\n    },\n    files: [PATHS.testEntryFile],\n    preprocessors,\n    webpack: {\n      mode: \"none\",\n      devtool: \"inline-source-map\",\n      // This loader allows us to override required files in tests\n      resolveLoader: {\n        alias: { inject: path.join(__dirname, \"loaders/inject-loader\") },\n      },\n      // This resolve config allows us to import with paths relative to the root directory, e.g. \"lib/ActivityStream.jsm\"\n      resolve: {\n        extensions: [\".js\", \".jsx\"],\n        modules: [PATHS.moduleResolveDirectory, \"node_modules\"],\n      },\n      externals: {\n        // enzyme needs these for backwards compatibility with 0.13.\n        // see https://github.com/airbnb/enzyme/blob/master/docs/guides/webpack.md#using-enzyme-with-webpack\n        \"react/addons\": true,\n        \"react/lib/ReactContext\": true,\n        \"react/lib/ExecutionEnvironment\": true,\n      },\n      module: {\n        rules: [\n          // This rule rewrites importing/exporting in .jsm files to be compatible with esmodules\n          {\n            test: /\\.jsm$/,\n            exclude: [/node_modules/],\n            use: [\n              {\n                loader: \"babel-loader\", // require(\"babel-core\")\n                options: {\n                  plugins: [\n                    // Converts .jsm files into common-js modules\n                    [\n                      \"jsm-to-commonjs\",\n                      {\n                        basePath: PATHS.resourcePathRegEx,\n                        removeOtherImports: true,\n                        replace: true,\n                      },\n                    ], // require(\"babel-plugin-jsm-to-commonjs\")\n                  ],\n                },\n              },\n            ],\n          },\n          {\n            test: /\\.js$/,\n            exclude: [/node_modules\\/(?!(fluent|fluent-react)\\/).*/, /test/],\n            loader: \"babel-loader\",\n          },\n          {\n            test: /\\.jsx$/,\n            exclude: /node_modules/,\n            loader: \"babel-loader\",\n            options: {\n              presets: [\"@babel/preset-react\"],\n            },\n          },\n          {\n            test: /\\.md$/,\n            use: \"raw-loader\",\n          },\n          {\n            enforce: \"post\",\n            test: /\\.js[mx]?$/,\n            loader: \"istanbul-instrumenter-loader\",\n            options: { esModules: true },\n            include: [\n              path.resolve(\"content-src\"),\n              path.resolve(\"lib\"),\n              path.resolve(\"common\"),\n            ],\n            exclude: [\n              path.resolve(\"test\"),\n              path.resolve(\"vendor\"),\n              path.resolve(\"lib/ASRouterTargeting.jsm\"),\n              path.resolve(\"lib/ASRouterTriggerListeners.jsm\"),\n              path.resolve(\"lib/OnboardingMessageProvider.jsm\"),\n              path.resolve(\"lib/CFRMessageProvider.jsm\"),\n              path.resolve(\"lib/CFRPageActions.jsm\"),\n            ],\n          },\n        ],\n      },\n    },\n    // Silences some overly-verbose logging of individual module builds\n    webpackMiddleware: { noInfo: true },\n  });\n};\n"
  },
  {
    "path": "lib/ASRouter.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\"use strict\";\n\nconst { Services } = ChromeUtils.import(\"resource://gre/modules/Services.jsm\");\nconst { XPCOMUtils } = ChromeUtils.import(\n  \"resource://gre/modules/XPCOMUtils.jsm\"\n);\nXPCOMUtils.defineLazyGlobalGetters(this, [\"fetch\"]);\nXPCOMUtils.defineLazyModuleGetters(this, {\n  AddonManager: \"resource://gre/modules/AddonManager.jsm\",\n  UITour: \"resource:///modules/UITour.jsm\",\n  FxAccounts: \"resource://gre/modules/FxAccounts.jsm\",\n  AppConstants: \"resource://gre/modules/AppConstants.jsm\",\n  OS: \"resource://gre/modules/osfile.jsm\",\n  BookmarkPanelHub: \"resource://activity-stream/lib/BookmarkPanelHub.jsm\",\n  SnippetsTestMessageProvider:\n    \"resource://activity-stream/lib/SnippetsTestMessageProvider.jsm\",\n  PanelTestProvider: \"resource://activity-stream/lib/PanelTestProvider.jsm\",\n  ToolbarBadgeHub: \"resource://activity-stream/lib/ToolbarBadgeHub.jsm\",\n  ToolbarPanelHub: \"resource://activity-stream/lib/ToolbarPanelHub.jsm\",\n  ASRouterTargeting: \"resource://activity-stream/lib/ASRouterTargeting.jsm\",\n  QueryCache: \"resource://activity-stream/lib/ASRouterTargeting.jsm\",\n  ASRouterPreferences: \"resource://activity-stream/lib/ASRouterPreferences.jsm\",\n  TARGETING_PREFERENCES:\n    \"resource://activity-stream/lib/ASRouterPreferences.jsm\",\n  ASRouterTriggerListeners:\n    \"resource://activity-stream/lib/ASRouterTriggerListeners.jsm\",\n  CFRMessageProvider: \"resource://activity-stream/lib/CFRMessageProvider.jsm\",\n  KintoHttpClient: \"resource://services-common/kinto-http-client.js\",\n  Downloader: \"resource://services-settings/Attachments.jsm\",\n  RemoteL10n: \"resource://activity-stream/lib/RemoteL10n.jsm\",\n  MigrationUtils: \"resource:///modules/MigrationUtils.jsm\",\n});\nXPCOMUtils.defineLazyServiceGetters(this, {\n  BrowserHandler: [\"@mozilla.org/browser/clh;1\", \"nsIBrowserHandler\"],\n});\nconst {\n  ASRouterActions: ra,\n  actionTypes: at,\n  actionCreators: ac,\n} = ChromeUtils.import(\"resource://activity-stream/common/Actions.jsm\");\n\nconst { CFRMessageProvider } = ChromeUtils.import(\n  \"resource://activity-stream/lib/CFRMessageProvider.jsm\"\n);\nconst { OnboardingMessageProvider } = ChromeUtils.import(\n  \"resource://activity-stream/lib/OnboardingMessageProvider.jsm\"\n);\nconst { RemoteSettings } = ChromeUtils.import(\n  \"resource://services-settings/remote-settings.js\"\n);\nconst { CFRPageActions } = ChromeUtils.import(\n  \"resource://activity-stream/lib/CFRPageActions.jsm\"\n);\nconst { AttributionCode } = ChromeUtils.import(\n  \"resource:///modules/AttributionCode.jsm\"\n);\n\nconst TRAILHEAD_CONFIG = {\n  DID_SEE_ABOUT_WELCOME_PREF: \"trailhead.firstrun.didSeeAboutWelcome\",\n  DYNAMIC_TRIPLET_BUNDLE_LENGTH: 3,\n};\n\nconst INCOMING_MESSAGE_NAME = \"ASRouter:child-to-parent\";\nconst OUTGOING_MESSAGE_NAME = \"ASRouter:parent-to-child\";\nconst ONE_DAY_IN_MS = 24 * 60 * 60 * 1000;\n// List of hosts for endpoints that serve router messages.\n// Key is allowed host, value is a name for the endpoint host.\nconst DEFAULT_WHITELIST_HOSTS = {\n  \"activity-stream-icons.services.mozilla.com\": \"production\",\n  \"snippets-admin.mozilla.org\": \"preview\",\n};\nconst SNIPPETS_ENDPOINT_WHITELIST =\n  \"browser.newtab.activity-stream.asrouter.whitelistHosts\";\n// Max possible impressions cap for any message\nconst MAX_MESSAGE_LIFETIME_CAP = 100;\n\nconst LOCAL_MESSAGE_PROVIDERS = {\n  OnboardingMessageProvider,\n  CFRMessageProvider,\n};\nconst STARTPAGE_VERSION = \"6\";\n\n// Remote Settings\nconst RS_SERVER_PREF = \"services.settings.server\";\nconst RS_MAIN_BUCKET = \"main\";\nconst RS_COLLECTION_L10N = \"ms-language-packs\"; // \"ms\" stands for Messaging System\nconst RS_PROVIDERS_WITH_L10N = [\"cfr\", \"cfr-fxa\", \"whats-new-panel\"];\nconst RS_FLUENT_VERSION = \"v1\";\nconst RS_FLUENT_RECORD_PREFIX = `cfr-${RS_FLUENT_VERSION}`;\nconst RS_DOWNLOAD_MAX_RETRIES = 2;\n// This is the list of providers for which we want to cache the targeting\n// expression result and reuse between calls. Cache duration is defined in\n// ASRouterTargeting where evaluation takes place.\nconst JEXL_PROVIDER_CACHE = new Set([\"snippets\"]);\n\n// To observe the app locale change notification.\nconst TOPIC_INTL_LOCALE_CHANGED = \"intl:app-locales-changed\";\n// To observe the pref that controls if ASRouter should use the remote Fluent files for l10n.\nconst USE_REMOTE_L10N_PREF =\n  \"browser.newtabpage.activity-stream.asrouter.useRemoteL10n\";\n\nconst MessageLoaderUtils = {\n  STARTPAGE_VERSION,\n  REMOTE_LOADER_CACHE_KEY: \"RemoteLoaderCache\",\n  _errors: [],\n\n  reportError(e) {\n    Cu.reportError(e);\n    this._errors.push({\n      timestamp: new Date(),\n      error: { message: e.toString(), stack: e.stack },\n    });\n  },\n\n  get errors() {\n    const errors = this._errors;\n    this._errors = [];\n    return errors;\n  },\n\n  /**\n   * _localLoader - Loads messages for a local provider (i.e. one that lives in mozilla central)\n   *\n   * @param {obj} provider An AS router provider\n   * @param {Array} provider.messages An array of messages\n   * @returns {Array} the array of messages\n   */\n  _localLoader(provider) {\n    return provider.messages;\n  },\n\n  async _remoteLoaderCache(storage) {\n    let allCached;\n    try {\n      allCached =\n        (await storage.get(MessageLoaderUtils.REMOTE_LOADER_CACHE_KEY)) || {};\n    } catch (e) {\n      // istanbul ignore next\n      MessageLoaderUtils.reportError(e);\n      // istanbul ignore next\n      allCached = {};\n    }\n    return allCached;\n  },\n\n  /**\n   * _remoteLoader - Loads messages for a remote provider\n   *\n   * @param {obj} provider An AS router provider\n   * @param {string} provider.url An endpoint that returns an array of messages as JSON\n   * @param {obj} options.storage A storage object with get() and set() methods for caching.\n   * @returns {Promise} resolves with an array of messages, or an empty array if none could be fetched\n   */\n  async _remoteLoader(provider, options) {\n    let remoteMessages = [];\n    if (provider.url) {\n      const allCached = await MessageLoaderUtils._remoteLoaderCache(\n        options.storage\n      );\n      const cached = allCached[provider.id];\n      let etag;\n\n      if (\n        cached &&\n        cached.url === provider.url &&\n        cached.version === STARTPAGE_VERSION\n      ) {\n        const { lastFetched, messages } = cached;\n        if (\n          !MessageLoaderUtils.shouldProviderUpdate({\n            ...provider,\n            lastUpdated: lastFetched,\n          })\n        ) {\n          // Cached messages haven't expired, return early.\n          return messages;\n        }\n        etag = cached.etag;\n        remoteMessages = messages;\n      }\n\n      let headers = new Headers();\n      if (etag) {\n        headers.set(\"If-None-Match\", etag);\n      }\n\n      let response;\n      try {\n        response = await fetch(provider.url, { headers, credentials: \"omit\" });\n      } catch (e) {\n        MessageLoaderUtils.reportError(e);\n      }\n      if (\n        response &&\n        response.ok &&\n        (response.status >= 200 && response.status < 400)\n      ) {\n        let jsonResponse;\n        try {\n          jsonResponse = await response.json();\n        } catch (e) {\n          MessageLoaderUtils.reportError(e);\n          return remoteMessages;\n        }\n        if (jsonResponse && jsonResponse.messages) {\n          remoteMessages = jsonResponse.messages.map(msg => ({\n            ...msg,\n            provider_url: provider.url,\n          }));\n\n          // Cache the results if this isn't a preview URL.\n          if (provider.updateCycleInMs > 0) {\n            etag = response.headers.get(\"ETag\");\n            const cacheInfo = {\n              messages: remoteMessages,\n              etag,\n              lastFetched: Date.now(),\n              version: STARTPAGE_VERSION,\n            };\n\n            options.storage.set(MessageLoaderUtils.REMOTE_LOADER_CACHE_KEY, {\n              ...allCached,\n              [provider.id]: cacheInfo,\n            });\n          }\n        } else {\n          MessageLoaderUtils.reportError(\n            `No messages returned from ${provider.url}.`\n          );\n        }\n      } else if (response) {\n        MessageLoaderUtils.reportError(\n          `Invalid response status ${response.status} from ${provider.url}.`\n        );\n      }\n    }\n    return remoteMessages;\n  },\n\n  /**\n   * _remoteSettingsLoader - Loads messages for a RemoteSettings provider\n   *\n   * Note:\n   * 1). Both \"cfr\" and \"cfr-fxa\" require the Fluent file for l10n, so there is\n   * another file downloading phase for those two providers after their messages\n   * are successfully fetched from Remote Settings. Currently, they share the same\n   * attachment of the record \"${RS_FLUENT_RECORD_PREFIX}-${locale}\" in the\n   * \"ms-language-packs\" collection. E.g. for \"en-US\" with version \"v1\",\n   * the Fluent file is attched to the record with ID \"cfr-v1-en-US\".\n   *\n   * 2). The Remote Settings downloader is able to detect the duplicate download\n   * requests for the same attachment and ignore the redundent requests automatically.\n   *\n   * @param {obj} provider An AS router provider\n   * @param {string} provider.id The id of the provider\n   * @param {string} provider.bucket The name of the Remote Settings bucket\n   * @param {func} options.dispatchToAS dispatch an action the main AS Store\n   * @returns {Promise} resolves with an array of messages, or an empty array if none could be fetched\n   */\n  async _remoteSettingsLoader(provider, options) {\n    let messages = [];\n    if (provider.bucket) {\n      try {\n        messages = await MessageLoaderUtils._getRemoteSettingsMessages(\n          provider.bucket\n        );\n        if (!messages.length) {\n          MessageLoaderUtils._handleRemoteSettingsUndesiredEvent(\n            \"ASR_RS_NO_MESSAGES\",\n            provider.id,\n            options.dispatchToAS\n          );\n        } else if (RS_PROVIDERS_WITH_L10N.includes(provider.id)) {\n          const locale = Services.locale.appLocaleAsLangTag;\n          const recordId = `${RS_FLUENT_RECORD_PREFIX}-${locale}`;\n          const kinto = new KintoHttpClient(\n            Services.prefs.getStringPref(RS_SERVER_PREF)\n          );\n          const record = await kinto\n            .bucket(RS_MAIN_BUCKET)\n            .collection(RS_COLLECTION_L10N)\n            .getRecord(recordId);\n          if (record && record.data) {\n            const downloader = new Downloader(\n              RS_MAIN_BUCKET,\n              RS_COLLECTION_L10N\n            );\n            // Await here in order to capture the exceptions for reporting.\n            await downloader.download(record.data, {\n              retries: RS_DOWNLOAD_MAX_RETRIES,\n            });\n            RemoteL10n.reloadL10n();\n          } else {\n            MessageLoaderUtils._handleRemoteSettingsUndesiredEvent(\n              \"ASR_RS_NO_MESSAGES\",\n              RS_COLLECTION_L10N,\n              options.dispatchToAS\n            );\n          }\n        }\n      } catch (e) {\n        MessageLoaderUtils._handleRemoteSettingsUndesiredEvent(\n          \"ASR_RS_ERROR\",\n          provider.id,\n          options.dispatchToAS\n        );\n        MessageLoaderUtils.reportError(e);\n      }\n    }\n    return messages;\n  },\n\n  _getRemoteSettingsMessages(bucket) {\n    return RemoteSettings(bucket).get();\n  },\n\n  _handleRemoteSettingsUndesiredEvent(event, providerId, dispatchToAS) {\n    if (dispatchToAS) {\n      dispatchToAS(\n        ac.ASRouterUserEvent({\n          action: \"asrouter_undesired_event\",\n          event,\n          message_id: \"n/a\",\n          event_context: providerId,\n        })\n      );\n    }\n  },\n\n  /**\n   * _getMessageLoader - return the right loading function given the provider's type\n   *\n   * @param {obj} provider An AS Router provider\n   * @returns {func} A loading function\n   */\n  _getMessageLoader(provider) {\n    switch (provider.type) {\n      case \"remote\":\n        return this._remoteLoader;\n      case \"remote-settings\":\n        return this._remoteSettingsLoader;\n      case \"local\":\n      default:\n        return this._localLoader;\n    }\n  },\n\n  /**\n   * shouldProviderUpdate - Given the current time, should a provider update its messages?\n   *\n   * @param {any} provider An AS Router provider\n   * @param {int} provider.updateCycleInMs The number of milliseconds we should wait between updates\n   * @param {Date} provider.lastUpdated If the provider has been updated, the time the last update occurred\n   * @param {Date} currentTime The time we should check against. (defaults to Date.now())\n   * @returns {bool} Should an update happen?\n   */\n  shouldProviderUpdate(provider, currentTime = Date.now()) {\n    return (\n      !(provider.lastUpdated >= 0) ||\n      currentTime - provider.lastUpdated > provider.updateCycleInMs\n    );\n  },\n\n  /**\n   * loadMessagesForProvider - Load messages for a provider, given the provider's type.\n   *\n   * @param {obj} provider An AS Router provider\n   * @param {string} provider.type An AS Router provider type (defaults to \"local\")\n   * @param {obj} options.storage A storage object with get() and set() methods for caching.\n   * @param {func} options.dispatchToAS dispatch an action the main AS Store\n   * @returns {obj} Returns an object with .messages (an array of messages) and .lastUpdated (the time the messages were updated)\n   */\n  async loadMessagesForProvider(provider, options) {\n    const loader = this._getMessageLoader(provider);\n    let messages = await loader(provider, options);\n    // istanbul ignore if\n    if (!messages) {\n      messages = [];\n      MessageLoaderUtils.reportError(\n        new Error(\n          `Tried to load messages for ${\n            provider.id\n          } but the result was not an Array.`\n        )\n      );\n    }\n    // Filter out messages we temporarily want to exclude\n    if (provider.exclude && provider.exclude.length) {\n      messages = messages.filter(\n        message => !provider.exclude.includes(message.id)\n      );\n    }\n    const lastUpdated = Date.now();\n    return {\n      messages: messages\n        .map(messageData => {\n          const message = {\n            weight: 100,\n            ...messageData,\n            provider: provider.id,\n          };\n\n          // This is to support a personalization experiment\n          if (provider.personalized) {\n            const score = ASRouterPreferences.personalizedCfrScores[message.id];\n            if (score) {\n              message.score = score;\n            }\n            message.personalizedModelVersion =\n              provider.personalizedModelVersion;\n          }\n\n          return message;\n        })\n        .filter(message => message.weight > 0),\n      lastUpdated,\n      errors: MessageLoaderUtils.errors,\n    };\n  },\n\n  /**\n   * _loadAddonIconInURLBar - load addons-notification icon by displaying\n   * box containing addons icon in urlbar. See Bug 1513882\n   *\n   * @param  {XULElement} Target browser element for showing addons icon\n   */\n  _loadAddonIconInURLBar(browser) {\n    if (!browser) {\n      return;\n    }\n    const chromeDoc = browser.ownerDocument;\n    let notificationPopupBox = chromeDoc.getElementById(\n      \"notification-popup-box\"\n    );\n    if (!notificationPopupBox) {\n      return;\n    }\n    if (\n      notificationPopupBox.style.display === \"none\" ||\n      notificationPopupBox.style.display === \"\"\n    ) {\n      notificationPopupBox.style.display = \"block\";\n    }\n  },\n\n  async installAddonFromURL(browser, url, telemetrySource = \"amo\") {\n    try {\n      MessageLoaderUtils._loadAddonIconInURLBar(browser);\n      const aUri = Services.io.newURI(url);\n      const systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();\n\n      // AddonManager installation source associated to the addons installed from activitystream's CFR\n      // and RTAMO (source is going to be \"amo\" if not configured explicitly in the message provider).\n      const telemetryInfo = { source: telemetrySource };\n      const install = await AddonManager.getInstallForURL(aUri.spec, {\n        telemetryInfo,\n      });\n      await AddonManager.installAddonFromWebpage(\n        \"application/x-xpinstall\",\n        browser,\n        systemPrincipal,\n        install\n      );\n    } catch (e) {\n      Cu.reportError(e);\n    }\n  },\n\n  /**\n   * cleanupCache - Removes cached data of removed providers.\n   *\n   * @param {Array} providers A list of activer AS Router providers\n   */\n  async cleanupCache(providers, storage) {\n    const ids = providers.filter(p => p.type === \"remote\").map(p => p.id);\n    const cache = await MessageLoaderUtils._remoteLoaderCache(storage);\n    let dirty = false;\n    for (let id in cache) {\n      if (!ids.includes(id)) {\n        delete cache[id];\n        dirty = true;\n      }\n    }\n    if (dirty) {\n      await storage.set(MessageLoaderUtils.REMOTE_LOADER_CACHE_KEY, cache);\n    }\n  },\n};\n\nthis.MessageLoaderUtils = MessageLoaderUtils;\n\n/**\n * @class _ASRouter - Keeps track of all messages, UI surfaces, and\n * handles blocking, rotation, etc. Inspecting ASRouter.state will\n * tell you what the current displayed message is in all UI surfaces.\n *\n * Note: This is written as a constructor rather than just a plain object\n * so that it can be more easily unit tested.\n */\nclass _ASRouter {\n  constructor(localProviders = LOCAL_MESSAGE_PROVIDERS) {\n    this.initialized = false;\n    this.messageChannel = null;\n    this.dispatchToAS = null;\n    this._storage = null;\n    this._resetInitialization();\n    this._state = {\n      providers: [],\n      messageBlockList: [],\n      providerBlockList: [],\n      messageImpressions: {},\n      providerImpressions: {},\n      trailheadInitialized: false,\n      messages: [],\n      errors: [],\n      localeInUse: Services.locale.appLocaleAsLangTag,\n    };\n    this._triggerHandler = this._triggerHandler.bind(this);\n    this._localProviders = localProviders;\n    this.blockMessageById = this.blockMessageById.bind(this);\n    this.unblockMessageById = this.unblockMessageById.bind(this);\n    this.onMessage = this.onMessage.bind(this);\n    this.handleMessageRequest = this.handleMessageRequest.bind(this);\n    this.addImpression = this.addImpression.bind(this);\n    this._handleTargetingError = this._handleTargetingError.bind(this);\n    this.onPrefChange = this.onPrefChange.bind(this);\n    this.dispatch = this.dispatch.bind(this);\n    this._onLocaleChanged = this._onLocaleChanged.bind(this);\n  }\n\n  async onPrefChange(prefName) {\n    if (TARGETING_PREFERENCES.includes(prefName)) {\n      // Notify all tabs of messages that have become invalid after pref change\n      const invalidMessages = [];\n      const context = this._getMessagesContext();\n\n      for (const msg of this._getUnblockedMessages()) {\n        if (!msg.targeting) {\n          continue;\n        }\n        const isMatch = await ASRouterTargeting.isMatch(msg.targeting, context);\n        if (!isMatch) {\n          invalidMessages.push(msg.id);\n        }\n      }\n      this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {\n        type: at.AS_ROUTER_TARGETING_UPDATE,\n        data: invalidMessages,\n      });\n    } else {\n      // Update message providers and fetch new messages on pref change\n      this._loadLocalProviders();\n      this._updateMessageProviders();\n      await this.loadMessagesFromAllProviders();\n    }\n  }\n\n  // Replace all frequency time period aliases with their millisecond values\n  // This allows us to avoid accounting for special cases later on\n  normalizeItemFrequency({ frequency }) {\n    if (frequency && frequency.custom) {\n      for (const setting of frequency.custom) {\n        if (setting.period === \"daily\") {\n          setting.period = ONE_DAY_IN_MS;\n        }\n      }\n    }\n  }\n\n  // Fetch and decode the message provider pref JSON, and update the message providers\n  _updateMessageProviders() {\n    const previousProviders = this.state.providers;\n    const providers = [\n      // If we have added a `preview` provider, hold onto it\n      ...previousProviders.filter(p => p.id === \"preview\"),\n      // The provider should be enabled and not have a user preference set to false\n      ...ASRouterPreferences.providers.filter(\n        p =>\n          p.enabled &&\n          (ASRouterPreferences.getUserPreference(p.id) !== false &&\n            // Provider is enabled or if provider has multiple categories\n            // check that at least one category is enabled\n            (!p.categories ||\n              p.categories.some(\n                c => ASRouterPreferences.getUserPreference(c) !== false\n              )))\n      ),\n    ].map(_provider => {\n      // make a copy so we don't modify the source of the pref\n      const provider = { ..._provider };\n\n      if (provider.type === \"local\" && !provider.messages) {\n        // Get the messages from the local message provider\n        const localProvider = this._localProviders[provider.localProvider];\n        provider.messages = localProvider ? localProvider.getMessages() : [];\n      }\n      if (provider.type === \"remote\" && provider.url) {\n        provider.url = provider.url.replace(\n          /%STARTPAGE_VERSION%/g,\n          STARTPAGE_VERSION\n        );\n        provider.url = Services.urlFormatter.formatURL(provider.url);\n      }\n      this.normalizeItemFrequency(provider);\n      // Reset provider update timestamp to force message refresh\n      provider.lastUpdated = undefined;\n      return provider;\n    });\n\n    const providerIDs = providers.map(p => p.id);\n\n    // Clear old messages for providers that are no longer enabled\n    for (const prevProvider of previousProviders) {\n      if (!providerIDs.includes(prevProvider.id)) {\n        this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {\n          type: \"CLEAR_PROVIDER\",\n          data: { id: prevProvider.id },\n        });\n      }\n    }\n\n    this.setState(prevState => ({\n      providers,\n      // Clear any messages from removed providers\n      messages: [\n        ...prevState.messages.filter(message =>\n          providerIDs.includes(message.provider)\n        ),\n      ],\n    }));\n  }\n\n  get state() {\n    return this._state;\n  }\n\n  set state(value) {\n    throw new Error(\n      \"Do not modify this.state directy. Instead, call this.setState(newState)\"\n    );\n  }\n\n  /**\n   * _resetInitialization - adds the following to the instance:\n   *  .initialized {bool}            Has AS Router been initialized?\n   *  .waitForInitialized {Promise}  A promise that resolves when initializion is complete\n   *  ._finishInitializing {func}    A function that, when called, resolves the .waitForInitialized\n   *                                 promise and sets .initialized to true.\n   * @memberof _ASRouter\n   */\n  _resetInitialization() {\n    this.initialized = false;\n    this.waitForInitialized = new Promise(resolve => {\n      this._finishInitializing = () => {\n        this.initialized = true;\n        resolve();\n      };\n    });\n  }\n\n  /**\n   * loadMessagesFromAllProviders - Loads messages from all providers if they require updates.\n   *                                Checks the .lastUpdated field on each provider to see if updates are needed\n   * @memberof _ASRouter\n   */\n  async loadMessagesFromAllProviders() {\n    const needsUpdate = this.state.providers.filter(provider =>\n      MessageLoaderUtils.shouldProviderUpdate(provider)\n    );\n    // Don't do extra work if we don't need any updates\n    if (needsUpdate.length) {\n      let newState = { messages: [], providers: [] };\n      for (const provider of this.state.providers) {\n        if (needsUpdate.includes(provider)) {\n          let {\n            messages,\n            lastUpdated,\n            errors,\n          } = await MessageLoaderUtils.loadMessagesForProvider(provider, {\n            storage: this._storage,\n            dispatchToAS: this.dispatchToAS,\n          });\n          messages = messages.filter(\n            ({ content }) =>\n              !content ||\n              !content.category ||\n              ASRouterPreferences.getUserPreference(content.category)\n          );\n          newState.providers.push({ ...provider, lastUpdated, errors });\n          newState.messages = [...newState.messages, ...messages];\n        } else {\n          // Skip updating this provider's messages if no update is required\n          let messages = this.state.messages.filter(\n            msg => msg.provider === provider.id\n          );\n          newState.providers.push(provider);\n          newState.messages = [...newState.messages, ...messages];\n        }\n      }\n\n      for (const message of newState.messages) {\n        this.normalizeItemFrequency(message);\n      }\n\n      // Some messages have triggers that require us to initalise trigger listeners\n      const unseenListeners = new Set(ASRouterTriggerListeners.keys());\n      for (const { trigger } of newState.messages) {\n        if (trigger && ASRouterTriggerListeners.has(trigger.id)) {\n          ASRouterTriggerListeners.get(trigger.id).init(\n            this._triggerHandler,\n            trigger.params,\n            trigger.patterns\n          );\n          unseenListeners.delete(trigger.id);\n        }\n      }\n      // We don't need these listeners, but they may have previously been\n      // initialised, so uninitialise them\n      for (const triggerID of unseenListeners) {\n        ASRouterTriggerListeners.get(triggerID).uninit();\n      }\n\n      // We don't want to cache preview endpoints, remove them after messages are fetched\n      await this.setState(this._removePreviewEndpoint(newState));\n      await this.cleanupImpressions();\n    }\n  }\n\n  async _maybeUpdateL10nAttachment() {\n    const { localeInUse } = this.state.localeInUse;\n    const newLocale = Services.locale.appLocaleAsLangTag;\n    if (newLocale !== localeInUse) {\n      const providers = [...this.state.providers];\n      let needsUpdate = false;\n      providers.forEach(provider => {\n        if (RS_PROVIDERS_WITH_L10N.includes(provider.id)) {\n          // Force to refresh the messages as well as the attachment.\n          provider.lastUpdated = undefined;\n          needsUpdate = true;\n        }\n      });\n      if (needsUpdate) {\n        await this.setState({\n          localeInUse: newLocale,\n          providers,\n        });\n        await this.loadMessagesFromAllProviders();\n      }\n    }\n  }\n\n  async _onLocaleChanged(subject, topic, data) {\n    await this._maybeUpdateL10nAttachment();\n  }\n\n  observe(aSubject, aTopic, aPrefName) {\n    switch (aPrefName) {\n      case USE_REMOTE_L10N_PREF:\n        CFRPageActions.reloadL10n();\n        break;\n    }\n  }\n\n  /**\n   * init - Initializes the MessageRouter.\n   * It is ready when it has been connected to a RemotePageManager instance.\n   *\n   * @param {RemotePageManager} channel a RemotePageManager instance\n   * @param {obj} storage an AS storage instance\n   * @param {func} dispatchToAS dispatch an action the main AS Store\n   * @memberof _ASRouter\n   */\n  async init(channel, storage, dispatchToAS) {\n    this.messageChannel = channel;\n    this.messageChannel.addMessageListener(\n      INCOMING_MESSAGE_NAME,\n      this.onMessage\n    );\n    this._storage = storage;\n    this.WHITELIST_HOSTS = this._loadSnippetsWhitelistHosts();\n    this.dispatchToAS = dispatchToAS;\n\n    ASRouterPreferences.init();\n    ASRouterPreferences.addListener(this.onPrefChange);\n    BookmarkPanelHub.init(\n      this.handleMessageRequest,\n      this.addImpression,\n      this.dispatch\n    );\n    ToolbarBadgeHub.init(this.waitForInitialized, {\n      handleMessageRequest: this.handleMessageRequest,\n      addImpression: this.addImpression,\n      blockMessageById: this.blockMessageById,\n      unblockMessageById: this.unblockMessageById,\n      dispatch: this.dispatch,\n    });\n    ToolbarPanelHub.init(this.waitForInitialized, {\n      getMessages: this.handleMessageRequest,\n      dispatch: this.dispatch,\n      handleUserAction: this.handleUserAction,\n    });\n\n    this._loadLocalProviders();\n\n    const messageBlockList =\n      (await this._storage.get(\"messageBlockList\")) || [];\n    const providerBlockList =\n      (await this._storage.get(\"providerBlockList\")) || [];\n    const messageImpressions =\n      (await this._storage.get(\"messageImpressions\")) || {};\n    const providerImpressions =\n      (await this._storage.get(\"providerImpressions\")) || {};\n    const previousSessionEnd =\n      (await this._storage.get(\"previousSessionEnd\")) || 0;\n    await this.setState({\n      messageBlockList,\n      providerBlockList,\n      messageImpressions,\n      providerImpressions,\n      previousSessionEnd,\n    });\n    this._updateMessageProviders();\n    await this.loadMessagesFromAllProviders();\n    await MessageLoaderUtils.cleanupCache(this.state.providers, storage);\n\n    // set necessary state in the rest of AS\n    this.dispatchToAS(\n      ac.BroadcastToContent({\n        type: at.AS_ROUTER_INITIALIZED,\n        data: ASRouterPreferences.specialConditions,\n      })\n    );\n\n    Services.obs.addObserver(this._onLocaleChanged, TOPIC_INTL_LOCALE_CHANGED);\n    Services.prefs.addObserver(USE_REMOTE_L10N_PREF, this);\n    // sets .initialized to true and resolves .waitForInitialized promise\n    this._finishInitializing();\n  }\n\n  uninit() {\n    this._storage.set(\"previousSessionEnd\", Date.now());\n\n    this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {\n      type: \"CLEAR_ALL\",\n    });\n    this.messageChannel.removeMessageListener(\n      INCOMING_MESSAGE_NAME,\n      this.onMessage\n    );\n    this.messageChannel = null;\n    this.dispatchToAS = null;\n\n    ASRouterPreferences.removeListener(this.onPrefChange);\n    ASRouterPreferences.uninit();\n    BookmarkPanelHub.uninit();\n    ToolbarPanelHub.uninit();\n    ToolbarBadgeHub.uninit();\n\n    // Uninitialise all trigger listeners\n    for (const listener of ASRouterTriggerListeners.values()) {\n      listener.uninit();\n    }\n    Services.obs.removeObserver(\n      this._onLocaleChanged,\n      TOPIC_INTL_LOCALE_CHANGED\n    );\n    Services.prefs.removeObserver(USE_REMOTE_L10N_PREF, this);\n    // If we added any CFR recommendations, they need to be removed\n    CFRPageActions.clearRecommendations();\n    this._resetInitialization();\n  }\n\n  setState(callbackOrObj) {\n    const newState =\n      typeof callbackOrObj === \"function\"\n        ? callbackOrObj(this.state)\n        : callbackOrObj;\n    this._state = { ...this.state, ...newState };\n    return new Promise(resolve => {\n      this._onStateChanged(this.state);\n      resolve();\n    });\n  }\n\n  getMessageById(id) {\n    return this.state.messages.find(message => message.id === id);\n  }\n\n  _onStateChanged(state) {\n    if (ASRouterPreferences.devtoolsEnabled) {\n      this._updateAdminState();\n    }\n  }\n\n  _loadLocalProviders() {\n    // If we're in ASR debug mode add the local test providers\n    if (ASRouterPreferences.devtoolsEnabled) {\n      this._localProviders = {\n        ...this._localProviders,\n        SnippetsTestMessageProvider,\n        PanelTestProvider,\n      };\n    }\n  }\n\n  /**\n   * Used by ASRouter Admin returns all ASRouterTargeting.Environment\n   * and ASRouter._getMessagesContext parameters and values\n   */\n  async getTargetingParameters(environment, localContext) {\n    const targetingParameters = {};\n    for (const param of Object.keys(environment)) {\n      targetingParameters[param] = await environment[param];\n    }\n    for (const param of Object.keys(localContext)) {\n      targetingParameters[param] = await localContext[param];\n    }\n\n    return targetingParameters;\n  }\n\n  async _updateAdminState(target) {\n    const channel = target || this.messageChannel;\n    channel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {\n      type: \"ADMIN_SET_STATE\",\n      data: {\n        ...this.state,\n        providerPrefs: ASRouterPreferences.providers,\n        userPrefs: ASRouterPreferences.getAllUserPreferences(),\n        targetingParameters: await this.getTargetingParameters(\n          ASRouterTargeting.Environment,\n          this._getMessagesContext()\n        ),\n        trailhead: ASRouterPreferences.trailhead,\n        errors: this.errors,\n      },\n    });\n  }\n\n  _handleTargetingError(type, error, message) {\n    Cu.reportError(error);\n    if (this.dispatchToAS) {\n      this.dispatchToAS(\n        ac.ASRouterUserEvent({\n          message_id: message.id,\n          action: \"asrouter_undesired_event\",\n          event: \"TARGETING_EXPRESSION_ERROR\",\n          event_context: type,\n        })\n      );\n    }\n  }\n\n  async setTrailHeadMessageSeen() {\n    if (!this.state.trailheadInitialized) {\n      Services.prefs.setBoolPref(\n        TRAILHEAD_CONFIG.DID_SEE_ABOUT_WELCOME_PREF,\n        true\n      );\n      await this.setState({\n        trailheadInitialized: true,\n      });\n    }\n  }\n\n  // Return an object containing targeting parameters used to select messages\n  _getMessagesContext() {\n    const { messageImpressions, previousSessionEnd } = this.state;\n\n    return {\n      get messageImpressions() {\n        return messageImpressions;\n      },\n      get previousSessionEnd() {\n        return previousSessionEnd;\n      },\n    };\n  }\n\n  async evaluateExpression(target, { expression, context }) {\n    const channel = target || this.messageChannel;\n    let evaluationStatus;\n    try {\n      evaluationStatus = {\n        result: await ASRouterTargeting.isMatch(expression, context),\n        success: true,\n      };\n    } catch (e) {\n      evaluationStatus = { result: e.message, success: false };\n    }\n\n    channel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {\n      type: \"ADMIN_SET_STATE\",\n      data: {\n        ...this.state,\n        evaluationStatus,\n      },\n    });\n  }\n\n  _orderBundle(bundle) {\n    return bundle.sort((a, b) => a.order - b.order);\n  }\n\n  // Work out if a message can be shown based on its and its provider's frequency caps.\n  isBelowFrequencyCaps(message) {\n    const { providers, messageImpressions, providerImpressions } = this.state;\n\n    const provider = providers.find(p => p.id === message.provider);\n    const impressionsForMessage = messageImpressions[message.id];\n    const impressionsForProvider = providerImpressions[message.provider];\n\n    return (\n      this._isBelowItemFrequencyCap(\n        message,\n        impressionsForMessage,\n        MAX_MESSAGE_LIFETIME_CAP\n      ) && this._isBelowItemFrequencyCap(provider, impressionsForProvider)\n    );\n  }\n\n  // Helper for isBelowFrecencyCaps - work out if the frequency cap for the given\n  //                                  item has been exceeded or not\n  _isBelowItemFrequencyCap(item, impressions, maxLifetimeCap = Infinity) {\n    if (item && item.frequency && impressions && impressions.length) {\n      if (\n        item.frequency.lifetime &&\n        impressions.length >= Math.min(item.frequency.lifetime, maxLifetimeCap)\n      ) {\n        return false;\n      }\n      if (item.frequency.custom) {\n        const now = Date.now();\n        for (const setting of item.frequency.custom) {\n          let { period } = setting;\n          const impressionsInPeriod = impressions.filter(t => now - t < period);\n          if (impressionsInPeriod.length >= setting.cap) {\n            return false;\n          }\n        }\n      }\n    }\n    return true;\n  }\n\n  async _getBundledMessages(originalMessage, target, trigger, force = false) {\n    let result = [];\n    let bundleLength;\n    let bundleTemplate;\n    let originalId;\n\n    if (originalMessage.includeBundle) {\n      // The original message is not part of the bundle, so don't include it\n      bundleLength = originalMessage.includeBundle.length;\n      bundleTemplate = originalMessage.includeBundle.template;\n    } else {\n      // The original message is part of the bundle\n      bundleLength = originalMessage.bundled;\n      bundleTemplate = originalMessage.template;\n      originalId = originalMessage.id;\n      // Add in a copy of the first message\n      result.push({\n        content: originalMessage.content,\n        id: originalMessage.id,\n        order: originalMessage.order || 0,\n      });\n    }\n\n    // First, find all messages of same template. These are potential matching targeting candidates\n    let bundledMessagesOfSameTemplate = this._getUnblockedMessages().filter(\n      msg =>\n        msg.bundled && msg.template === bundleTemplate && msg.id !== originalId\n    );\n\n    if (force) {\n      // Forcefully show the messages without targeting matching - this is for about:newtab#asrouter to show the messages\n      for (const message of bundledMessagesOfSameTemplate) {\n        result.push({ content: message.content, id: message.id });\n        // Stop once we have enough messages to fill a bundle\n        if (result.length === bundleLength) {\n          break;\n        }\n      }\n    } else {\n      // Find all messages that matches the targeting context\n      const allMessages = await this.handleMessageRequest({\n        messages: bundledMessagesOfSameTemplate,\n        triggerId: trigger && trigger.id,\n        triggerContext: trigger && trigger.context,\n        triggerParam: trigger && trigger.param,\n        ordered: true,\n        returnAll: true,\n      });\n\n      if (allMessages && allMessages.length) {\n        // Retrieve enough messages needed to fill a bundle\n        // Only copy the content of the message (that's what the UI cares about)\n        result = result.concat(\n          allMessages.slice(0, bundleLength).map(message => ({\n            content: message.content,\n            id: message.id,\n            order: message.order || 0,\n            // This is used to determine whether to block when action is triggered\n            // Only block for dynamic triplets experiment and when there are more messages available\n            blockOnClick:\n              ASRouterPreferences.trailhead.trailheadTriplet.startsWith(\n                \"dynamic\"\n              ) &&\n              allMessages.length >\n                TRAILHEAD_CONFIG.DYNAMIC_TRIPLET_BUNDLE_LENGTH,\n          }))\n        );\n      }\n    }\n\n    // If we did not find enough messages to fill the bundle, do not send the bundle down\n    if (result.length < bundleLength) {\n      return null;\n    }\n\n    // The bundle may have some extra attributes, like a header, or a dismiss button, so attempt to get those strings now\n    // This is a temporary solution until we can use Fluent strings in the content process, in which case the content can\n    // handle finding these strings on its own. See bug 1488973\n    const extraTemplateStrings = await this._extraTemplateStrings(\n      originalMessage\n    );\n\n    return {\n      bundle: this._orderBundle(result),\n      ...(extraTemplateStrings && { extraTemplateStrings }),\n      provider: originalMessage.provider,\n      template: originalMessage.template,\n    };\n  }\n\n  async _extraTemplateStrings(originalMessage) {\n    let extraTemplateStrings;\n    let localProvider = this._findProvider(originalMessage.provider);\n    if (localProvider && localProvider.getExtraAttributes) {\n      extraTemplateStrings = await localProvider.getExtraAttributes();\n    }\n\n    return extraTemplateStrings;\n  }\n\n  _findProvider(providerID) {\n    return this._localProviders[\n      this.state.providers.find(i => i.id === providerID).localProvider\n    ];\n  }\n\n  _getUnblockedMessages() {\n    let { state } = this;\n    return state.messages.filter(\n      item =>\n        !state.messageBlockList.includes(item.id) &&\n        (!item.campaign || !state.messageBlockList.includes(item.campaign)) &&\n        !state.providerBlockList.includes(item.provider)\n    );\n  }\n\n  /**\n   * Route messages based on template to the correct module that can display them\n   */\n  routeMessageToTarget(message, target, trigger, force = false) {\n    switch (message.template) {\n      case \"whatsnew_panel_message\":\n        if (force) {\n          ToolbarPanelHub.forceShowMessage(target, message);\n        }\n        break;\n      case \"cfr_doorhanger\":\n        if (force) {\n          CFRPageActions.forceRecommendation(target, message, this.dispatch);\n        } else {\n          CFRPageActions.addRecommendation(\n            target,\n            trigger.param && trigger.param.host,\n            message,\n            this.dispatch\n          );\n        }\n        break;\n      case \"fxa_bookmark_panel\":\n        if (force) {\n          BookmarkPanelHub._forceShowMessage(target, message);\n        }\n        break;\n      case \"toolbar_badge\":\n      case \"update_action\":\n        ToolbarBadgeHub.registerBadgeNotificationListener(message, { force });\n        break;\n      case \"milestone_message\":\n        CFRPageActions.showMilestone(target, message, this.dispatch, { force });\n        break;\n      default:\n        try {\n          target.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {\n            type: \"SET_MESSAGE\",\n            data: message,\n          });\n        } catch (e) {}\n        break;\n    }\n  }\n\n  async _sendMessageToTarget(message, target, trigger, force = false) {\n    // No message is available, so send CLEAR_ALL.\n    if (!message) {\n      try {\n        target.sendAsyncMessage(OUTGOING_MESSAGE_NAME, { type: \"CLEAR_ALL\" });\n      } catch (e) {}\n\n      // For bundled messages, look for the rest of the bundle or else send CLEAR_ALL\n    } else if (message.bundled) {\n      const bundledMessages = await this._getBundledMessages(\n        message,\n        target,\n        trigger,\n        force\n      );\n      const action = bundledMessages\n        ? { type: \"SET_BUNDLED_MESSAGES\", data: bundledMessages }\n        : { type: \"CLEAR_ALL\" };\n      try {\n        target.sendAsyncMessage(OUTGOING_MESSAGE_NAME, action);\n      } catch (e) {}\n\n      // For nested bundled messages, look for the desired bundle\n    } else if (message.includeBundle) {\n      const bundledMessages = await this._getBundledMessages(\n        message,\n        target,\n        message.includeBundle.trigger,\n        force\n      );\n      try {\n        target.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {\n          type: \"SET_MESSAGE\",\n          data: {\n            ...message,\n            trailheadTriplet:\n              ASRouterPreferences.trailhead.trailheadTriplet || \"\",\n            bundle: bundledMessages && bundledMessages.bundle,\n          },\n        });\n      } catch (e) {}\n    } else {\n      this.routeMessageToTarget(message, target, trigger, force);\n    }\n  }\n\n  async addImpression(message) {\n    const provider = this.state.providers.find(p => p.id === message.provider);\n    // We only need to store impressions for messages that have frequency, or\n    // that have providers that have frequency\n    if (message.frequency || (provider && provider.frequency)) {\n      const time = Date.now();\n      await this.setState(state => {\n        const messageImpressions = this._addImpressionForItem(\n          state,\n          message,\n          \"messageImpressions\",\n          time\n        );\n        const providerImpressions = this._addImpressionForItem(\n          state,\n          provider,\n          \"providerImpressions\",\n          time\n        );\n        return { messageImpressions, providerImpressions };\n      });\n    }\n  }\n\n  // Helper for addImpression - calculate the updated impressions object for the given\n  //                            item, then store it and return it\n  _addImpressionForItem(state, item, impressionsString, time) {\n    // The destructuring here is to avoid mutating existing objects in state as in redux\n    // (see https://redux.js.org/recipes/structuring-reducers/prerequisite-concepts#immutable-data-management)\n    const impressions = { ...state[impressionsString] };\n    if (item.frequency) {\n      impressions[item.id] = impressions[item.id]\n        ? [...impressions[item.id]]\n        : [];\n      impressions[item.id].push(time);\n      this._storage.set(impressionsString, impressions);\n    }\n    return impressions;\n  }\n\n  /**\n   * getLongestPeriod\n   *\n   * @param {obj} item Either an ASRouter message or an ASRouter provider\n   * @returns {int|null} if the item has custom frequency caps, the longest period found in the list of caps.\n                         if the item has no custom frequency caps, null\n   * @memberof _ASRouter\n   */\n  getLongestPeriod(item) {\n    if (!item.frequency || !item.frequency.custom) {\n      return null;\n    }\n    return item.frequency.custom.sort((a, b) => b.period - a.period)[0].period;\n  }\n\n  /**\n   * cleanupImpressions - this function cleans up obsolete impressions whenever\n   * messages are refreshed or fetched. It will likely need to be more sophisticated in the future,\n   * but the current behaviour for when both message impressions and provider impressions are\n   * cleared is as follows (where `item` is either `message` or `provider`):\n   *\n   * 1. If the item id for a list of item impressions no longer exists in the ASRouter state, it\n   *    will be cleared.\n   * 2. If the item has time-bound frequency caps but no lifetime cap, any item impressions older\n   *    than the longest time period will be cleared.\n   */\n  async cleanupImpressions() {\n    await this.setState(state => {\n      const messageImpressions = this._cleanupImpressionsForItems(\n        state,\n        state.messages,\n        \"messageImpressions\"\n      );\n      const providerImpressions = this._cleanupImpressionsForItems(\n        state,\n        state.providers,\n        \"providerImpressions\"\n      );\n      return { messageImpressions, providerImpressions };\n    });\n  }\n\n  // Helper for cleanupImpressions - calculate the updated impressions object for\n  //                                 the given items, then store it and return it\n  _cleanupImpressionsForItems(state, items, impressionsString) {\n    const impressions = { ...state[impressionsString] };\n    let needsUpdate = false;\n    Object.keys(impressions).forEach(id => {\n      const [item] = items.filter(x => x.id === id);\n      // Don't keep impressions for items that no longer exist\n      if (!item || !item.frequency || !Array.isArray(impressions[id])) {\n        delete impressions[id];\n        needsUpdate = true;\n        return;\n      }\n      if (!impressions[id].length) {\n        return;\n      }\n      // If we don't want to store impressions older than the longest period\n      if (item.frequency.custom && !item.frequency.lifetime) {\n        const now = Date.now();\n        impressions[id] = impressions[id].filter(\n          t => now - t < this.getLongestPeriod(item)\n        );\n        needsUpdate = true;\n      }\n    });\n    if (needsUpdate) {\n      this._storage.set(impressionsString, impressions);\n    }\n    return impressions;\n  }\n\n  handleMessageRequest({\n    messages: candidates,\n    triggerId,\n    triggerParam,\n    triggerContext,\n    template,\n    provider,\n    ordered = false,\n    returnAll = false,\n  }) {\n    const messages =\n      candidates ||\n      this._getUnblockedMessages()\n        .filter(m => {\n          if (provider && m.provider !== provider) {\n            return false;\n          }\n          if (template && m.template !== template) {\n            return false;\n          }\n          if (triggerId && !m.trigger) {\n            return false;\n          }\n          if (triggerId && m.trigger.id !== triggerId) {\n            return false;\n          }\n\n          return true;\n        })\n        .filter(m => this.isBelowFrequencyCaps(m));\n\n    const shouldCache = messages.every(m =>\n      JEXL_PROVIDER_CACHE.has(m.provider)\n    );\n    const context = this._getMessagesContext();\n\n    // Find a message that matches the targeting context as well as the trigger context (if one is provided)\n    // If no trigger is provided, we should find a message WITHOUT a trigger property defined.\n    return ASRouterTargeting.findMatchingMessage({\n      messages,\n      trigger: triggerId && {\n        id: triggerId,\n        param: triggerParam,\n        context: triggerContext,\n      },\n      context,\n      onError: this._handleTargetingError,\n      ordered,\n      shouldCache,\n      returnAll,\n    });\n  }\n\n  async setMessageById(id, target, force = true, action = {}) {\n    const newMessage = this.getMessageById(id);\n\n    await this._sendMessageToTarget(newMessage, target, action.data, force);\n  }\n\n  async blockMessageById(idOrIds) {\n    const idsToBlock = Array.isArray(idOrIds) ? idOrIds : [idOrIds];\n\n    await this.setState(state => {\n      const messageBlockList = [...state.messageBlockList];\n      const messageImpressions = { ...state.messageImpressions };\n\n      idsToBlock.forEach(id => {\n        const message = state.messages.find(m => m.id === id);\n        const idToBlock = message && message.campaign ? message.campaign : id;\n        if (!messageBlockList.includes(idToBlock)) {\n          messageBlockList.push(idToBlock);\n        }\n\n        // When a message is blocked, its impressions should be cleared as well\n        delete messageImpressions[id];\n      });\n\n      this._storage.set(\"messageBlockList\", messageBlockList);\n      this._storage.set(\"messageImpressions\", messageImpressions);\n      return { messageBlockList, messageImpressions };\n    });\n  }\n\n  unblockMessageById(id) {\n    return this.setState(state => {\n      const messageBlockList = [...state.messageBlockList];\n      const message = state.messages.find(m => m.id === id);\n      const idToUnblock = message && message.campaign ? message.campaign : id;\n      messageBlockList.splice(messageBlockList.indexOf(idToUnblock), 1);\n      this._storage.set(\"messageBlockList\", messageBlockList);\n      return { messageBlockList };\n    });\n  }\n\n  async blockProviderById(idOrIds) {\n    const idsToBlock = Array.isArray(idOrIds) ? idOrIds : [idOrIds];\n\n    await this.setState(state => {\n      const providerBlockList = [...state.providerBlockList, ...idsToBlock];\n      // When a provider is blocked, its impressions should be cleared as well\n      const providerImpressions = { ...state.providerImpressions };\n      idsToBlock.forEach(id => delete providerImpressions[id]);\n      this._storage.set(\"providerBlockList\", providerBlockList);\n      return { providerBlockList, providerImpressions };\n    });\n  }\n\n  _validPreviewEndpoint(url) {\n    try {\n      const endpoint = new URL(url);\n      if (!this.WHITELIST_HOSTS[endpoint.host]) {\n        Cu.reportError(\n          `The preview URL host ${endpoint.host} is not in the whitelist.`\n        );\n      }\n      if (endpoint.protocol !== \"https:\") {\n        Cu.reportError(\"The URL protocol is not https.\");\n      }\n      return (\n        endpoint.protocol === \"https:\" && this.WHITELIST_HOSTS[endpoint.host]\n      );\n    } catch (e) {\n      return false;\n    }\n  }\n\n  // Ensure we switch to the Onboarding message after RTAMO addon was installed\n  _updateOnboardingState() {\n    let addonInstallObs = (subject, topic) => {\n      Services.obs.removeObserver(\n        addonInstallObs,\n        \"webextension-install-notify\"\n      );\n      this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {\n        type: \"CLEAR_INTERRUPT\",\n      });\n    };\n    Services.obs.addObserver(addonInstallObs, \"webextension-install-notify\");\n  }\n\n  _loadSnippetsWhitelistHosts() {\n    let additionalHosts = [];\n    const whitelistPrefValue = Services.prefs.getStringPref(\n      SNIPPETS_ENDPOINT_WHITELIST,\n      \"\"\n    );\n    try {\n      additionalHosts = JSON.parse(whitelistPrefValue);\n    } catch (e) {\n      if (whitelistPrefValue) {\n        Cu.reportError(\n          `Pref ${SNIPPETS_ENDPOINT_WHITELIST} value is not valid JSON`\n        );\n      }\n    }\n\n    if (!additionalHosts.length) {\n      return DEFAULT_WHITELIST_HOSTS;\n    }\n\n    // If there are additional hosts we want to whitelist, add them as\n    // `preview` so that the updateCycle is 0\n    return additionalHosts.reduce(\n      (whitelist_hosts, host) => {\n        whitelist_hosts[host] = \"preview\";\n        Services.console.logStringMessage(`Adding ${host} to whitelist hosts.`);\n        return whitelist_hosts;\n      },\n      { ...DEFAULT_WHITELIST_HOSTS }\n    );\n  }\n\n  // To be passed to ASRouterTriggerListeners\n  async _triggerHandler(target, trigger) {\n    // Disable ASRouterTriggerListeners in kiosk mode.\n    if (BrowserHandler.kiosk) {\n      return;\n    }\n    await this.onMessage({\n      target,\n      data: { type: \"TRIGGER\", data: { trigger } },\n    });\n  }\n\n  _removePreviewEndpoint(state) {\n    state.providers = state.providers.filter(p => p.id !== \"preview\");\n    return state;\n  }\n\n  async _addPreviewEndpoint(url, portID) {\n    // When you view a preview snippet we want to hide all real content\n    const providers = [...this.state.providers];\n    if (\n      this._validPreviewEndpoint(url) &&\n      !providers.find(p => p.url === url)\n    ) {\n      this.dispatchToAS(\n        ac.OnlyToOneContent({ type: at.SNIPPETS_PREVIEW_MODE }, portID)\n      );\n      providers.push({\n        id: \"preview\",\n        type: \"remote\",\n        url,\n        updateCycleInMs: 0,\n      });\n      await this.setState({ providers });\n    }\n  }\n\n  // Windows specific calls to write attribution data\n  // Used by `forceAttribution` to set required targeting attributes for\n  // RTAMO messages. This should only be called from within about:newtab#asrouter\n  /* istanbul ignore next */\n  async _writeAttributionFile(data) {\n    let appDir = Services.dirsvc.get(\"LocalAppData\", Ci.nsIFile);\n    let file = appDir.clone();\n    file.append(Services.appinfo.vendor || \"mozilla\");\n    file.append(AppConstants.MOZ_APP_NAME);\n\n    await OS.File.makeDir(file.path, {\n      from: appDir.path,\n      ignoreExisting: true,\n    });\n\n    file.append(\"postSigningData\");\n    await OS.File.writeAtomic(file.path, data);\n  }\n\n  /**\n   * forceAttribution - this function should only be called from within about:newtab#asrouter.\n   * It forces the browser attribution to be set to something specified in asrouter admin\n   * tools, and reloads the providers in order to get messages that are dependant on this\n   * attribution data (see Return to AMO flow in bug 1475354 for example). Note - OSX and Windows only\n   * @param {data} Object an object containing the attribtion data that came from asrouter admin page\n   */\n  /* istanbul ignore next */\n  async forceAttribution(data) {\n    // Extract the parameters from data that will make up the referrer url\n    const { source, campaign, content } = data;\n    if (AppConstants.platform === \"win\") {\n      const attributionData = `source=${source}&campaign=${campaign}&content=${content}`;\n      this._writeAttributionFile(encodeURIComponent(attributionData));\n    } else if (AppConstants.platform === \"macosx\") {\n      let appPath = Services.dirsvc.get(\"GreD\", Ci.nsIFile).parent.parent.path;\n      let attributionSvc = Cc[\"@mozilla.org/mac-attribution;1\"].getService(\n        Ci.nsIMacAttributionService\n      );\n\n      let referrer = `https://www.mozilla.org/anything/?utm_campaign=${campaign}&utm_source=${source}&utm_content=${encodeURIComponent(\n        content\n      )}`;\n\n      // This sets the Attribution to be the referrer\n      attributionSvc.setReferrerUrl(appPath, referrer, true);\n    }\n\n    // Clear cache call is only possible in a testing environment\n    let env = Cc[\"@mozilla.org/process/environment;1\"].getService(\n      Ci.nsIEnvironment\n    );\n    env.set(\"XPCSHELL_TEST_PROFILE_DIR\", \"testing\");\n\n    // Clear and refresh Attribution, and then fetch the messages again to update\n    AttributionCode._clearCache();\n    await AttributionCode.getAttrDataAsync();\n    this._updateMessageProviders();\n    await this.loadMessagesFromAllProviders();\n  }\n\n  async handleUserAction({ data: action, target }) {\n    switch (action.type) {\n      case ra.SHOW_MIGRATION_WIZARD:\n        MigrationUtils.showMigrationWizard(target.browser.ownerGlobal, [\n          MigrationUtils.MIGRATION_ENTRYPOINT_NEWTAB,\n        ]);\n        break;\n      case ra.OPEN_PRIVATE_BROWSER_WINDOW:\n        // Forcefully open about:privatebrowsing\n        target.browser.ownerGlobal.OpenBrowserWindow({ private: true });\n        break;\n      case ra.OPEN_URL:\n        target.browser.ownerGlobal.openLinkIn(\n          action.data.args,\n          action.data.where || \"current\",\n          {\n            private: false,\n            triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal(\n              {}\n            ),\n            csp: null,\n          }\n        );\n        break;\n      case ra.OPEN_ABOUT_PAGE:\n        target.browser.ownerGlobal.openTrustedLinkIn(\n          `about:${action.data.args}`,\n          \"tab\"\n        );\n        break;\n      case ra.OPEN_PREFERENCES_PAGE:\n        target.browser.ownerGlobal.openPreferences(action.data.category);\n        break;\n      case ra.OPEN_APPLICATIONS_MENU:\n        UITour.showMenu(target.browser.ownerGlobal, action.data.args);\n        break;\n      case ra.HIGHLIGHT_FEATURE:\n        const highlight = await UITour.getTarget(\n          target.browser.ownerGlobal,\n          action.data.args\n        );\n        if (highlight) {\n          await UITour.showHighlight(\n            target.browser.ownerGlobal,\n            highlight,\n            \"none\",\n            { autohide: true }\n          );\n        }\n        break;\n      case ra.INSTALL_ADDON_FROM_URL:\n        this._updateOnboardingState();\n        await MessageLoaderUtils.installAddonFromURL(\n          target.browser,\n          action.data.url,\n          action.data.telemetrySource\n        );\n        break;\n      case ra.PIN_CURRENT_TAB:\n        let tab = target.browser.ownerGlobal.gBrowser.selectedTab;\n        target.browser.ownerGlobal.gBrowser.pinTab(tab);\n        target.browser.ownerGlobal.ConfirmationHint.show(tab, \"pinTab\", {\n          showDescription: true,\n        });\n        break;\n      case ra.SHOW_FIREFOX_ACCOUNTS:\n        const url = await FxAccounts.config.promiseConnectAccountURI(\n          \"snippets\"\n        );\n        // We want to replace the current tab.\n        target.browser.ownerGlobal.openLinkIn(url, \"current\", {\n          private: false,\n          triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal(\n            {}\n          ),\n          csp: null,\n        });\n        break;\n      case ra.OPEN_PROTECTION_PANEL:\n        let { gProtectionsHandler } = target.browser.ownerGlobal;\n        gProtectionsHandler.showProtectionsPopup({});\n        break;\n      case ra.OPEN_PROTECTION_REPORT:\n        target.browser.ownerGlobal.gProtectionsHandler.openProtections();\n        break;\n      case ra.DISABLE_STP_DOORHANGERS:\n        await this.blockMessageById([\n          \"SOCIAL_TRACKING_PROTECTION\",\n          \"FINGERPRINTERS_PROTECTION\",\n          \"CRYPTOMINERS_PROTECTION\",\n        ]);\n        break;\n    }\n  }\n\n  /**\n   * sendAsyncMessageToPreloaded - Sends an action to each preloaded browser, if any\n   *\n   * @param  {obj} action An action to be sent to content\n   */\n  sendAsyncMessageToPreloaded(action) {\n    const preloadedBrowsers = this.getPreloadedBrowser();\n    if (preloadedBrowsers) {\n      for (let preloadedBrowser of preloadedBrowsers) {\n        try {\n          preloadedBrowser.sendAsyncMessage(OUTGOING_MESSAGE_NAME, action);\n        } catch (e) {\n          // The preloaded page is no longer available, so just ignore.\n        }\n      }\n    }\n  }\n\n  /**\n   * getPreloadedBrowser - Retrieve the port of any preloaded browsers\n   *\n   * @return {Array|null} An array of ports belonging to the preloaded browsers, or null\n   *                      if there aren't any preloaded browsers\n   */\n  getPreloadedBrowser() {\n    let preloadedPorts = [];\n    for (let port of this.messageChannel.messagePorts) {\n      if (this.isPreloadedBrowser(port.browser)) {\n        preloadedPorts.push(port);\n      }\n    }\n    return preloadedPorts.length ? preloadedPorts : null;\n  }\n\n  /**\n   * isPreloadedBrowser - Returns true if the passed browser has been preloaded\n   *                      for faster rendering of new tabs.\n   *\n   * @param {<browser>} A <browser> to check.\n   * @return {boolean} True if the browser is preloaded.\n   *                   False if there aren't any preloaded browsers\n   */\n  isPreloadedBrowser(browser) {\n    return browser.getAttribute(\"preloadedState\") === \"preloaded\";\n  }\n\n  dispatch(action, target) {\n    this.onMessage({ data: action, target });\n  }\n\n  async sendNewTabMessage(target, options = {}) {\n    const { endpoint } = options;\n    let message;\n\n    // Load preview endpoint for snippets if one is sent\n    if (endpoint) {\n      await this._addPreviewEndpoint(endpoint.url, target.portID);\n    }\n\n    // Load all messages\n    await this.loadMessagesFromAllProviders();\n\n    if (endpoint) {\n      message = await this.handleMessageRequest({ provider: \"preview\" });\n\n      // We don't want to cache preview messages, remove them after we selected the message to show\n      if (message) {\n        await this.setState(state => ({\n          messages: state.messages.filter(m => m.id !== message.id),\n        }));\n      }\n    } else {\n      // On new tab, send cards if they match; othwerise send a snippet\n      message = await this.handleMessageRequest({\n        template: \"extended_triplets\",\n      });\n\n      // If no extended triplets message was returned, show snippets instead\n      if (!message) {\n        message = await this.handleMessageRequest({ provider: \"snippets\" });\n      }\n    }\n\n    await this._sendMessageToTarget(message, target);\n  }\n\n  async sendTriggerMessage(target, trigger) {\n    await this.loadMessagesFromAllProviders();\n\n    if (trigger.id === \"firstRun\") {\n      // On about welcome, set trailhead message seen on receiving firstrun trigger\n      await this.setTrailHeadMessageSeen();\n    }\n\n    const message = await this.handleMessageRequest({\n      triggerId: trigger.id,\n      triggerParam: trigger.param,\n      triggerContext: trigger.context,\n    });\n\n    await this._sendMessageToTarget(message, target, trigger);\n  }\n\n  /* eslint-disable complexity */\n  async onMessage({ data: action, target }) {\n    switch (action.type) {\n      case \"USER_ACTION\":\n        if (action.data.type in ra) {\n          await this.handleUserAction({ data: action.data, target });\n        }\n        break;\n      case \"NEWTAB_MESSAGE_REQUEST\":\n        await this.waitForInitialized;\n        await this.sendNewTabMessage(target, action.data);\n        break;\n      case \"TRIGGER\":\n        await this.waitForInitialized;\n        await this.sendTriggerMessage(\n          target,\n          action.data && action.data.trigger\n        );\n        break;\n      case \"BLOCK_MESSAGE_BY_ID\":\n        await this.blockMessageById(action.data.id);\n        // Block the message but don't dismiss it in case the action taken has\n        // another state that needs to be visible\n        if (action.data.preventDismiss) {\n          break;\n        }\n\n        const outgoingMessage = {\n          type: \"CLEAR_MESSAGE\",\n          data: { id: action.data.id },\n        };\n        if (action.data.preloadedOnly) {\n          this.sendAsyncMessageToPreloaded(outgoingMessage);\n        } else {\n          this.messageChannel.sendAsyncMessage(\n            OUTGOING_MESSAGE_NAME,\n            outgoingMessage\n          );\n        }\n        break;\n      case \"DISMISS_MESSAGE_BY_ID\":\n        this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {\n          type: \"CLEAR_MESSAGE\",\n          data: { id: action.data.id },\n        });\n        break;\n      case \"BLOCK_PROVIDER_BY_ID\":\n        await this.blockProviderById(action.data.id);\n        this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {\n          type: \"CLEAR_PROVIDER\",\n          data: { id: action.data.id },\n        });\n        break;\n      case \"BLOCK_BUNDLE\":\n        await this.blockMessageById(action.data.bundle.map(b => b.id));\n        this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {\n          type: \"CLEAR_BUNDLE\",\n        });\n        break;\n      case \"UNBLOCK_MESSAGE_BY_ID\":\n        this.unblockMessageById(action.data.id);\n        break;\n      case \"UNBLOCK_PROVIDER_BY_ID\":\n        await this.setState(state => {\n          const providerBlockList = [...state.providerBlockList];\n          providerBlockList.splice(\n            providerBlockList.indexOf(action.data.id),\n            1\n          );\n          this._storage.set(\"providerBlockList\", providerBlockList);\n          return { providerBlockList };\n        });\n        break;\n      case \"UNBLOCK_BUNDLE\":\n        await this.setState(state => {\n          const messageBlockList = [...state.messageBlockList];\n          for (let message of action.data.bundle) {\n            messageBlockList.splice(messageBlockList.indexOf(message.id), 1);\n          }\n          this._storage.set(\"messageBlockList\", messageBlockList);\n          return { messageBlockList };\n        });\n        break;\n      case \"OVERRIDE_MESSAGE\":\n        await this.setMessageById(action.data.id, target, true, action);\n        break;\n      case \"ADMIN_CONNECT_STATE\":\n        if (action.data && action.data.endpoint) {\n          this._addPreviewEndpoint(action.data.endpoint.url, target.portID);\n          await this.loadMessagesFromAllProviders();\n        } else {\n          await this._updateAdminState(target);\n        }\n        break;\n      case \"IMPRESSION\":\n        await this.addImpression(action.data);\n        break;\n      case \"DOORHANGER_TELEMETRY\":\n      case \"TOOLBAR_BADGE_TELEMETRY\":\n      case \"TOOLBAR_PANEL_TELEMETRY\":\n        if (this.dispatchToAS) {\n          this.dispatchToAS(ac.ASRouterUserEvent(action.data));\n        }\n        break;\n      case \"EXPIRE_QUERY_CACHE\":\n        QueryCache.expireAll();\n        break;\n      case \"ENABLE_PROVIDER\":\n        ASRouterPreferences.enableOrDisableProvider(action.data, true);\n        break;\n      case \"DISABLE_PROVIDER\":\n        ASRouterPreferences.enableOrDisableProvider(action.data, false);\n        break;\n      case \"RESET_PROVIDER_PREF\":\n        ASRouterPreferences.resetProviderPref();\n        break;\n      case \"SET_PROVIDER_USER_PREF\":\n        ASRouterPreferences.setUserPreference(\n          action.data.id,\n          action.data.value\n        );\n        break;\n      case \"EVALUATE_JEXL_EXPRESSION\":\n        this.evaluateExpression(target, action.data);\n        break;\n      case \"FORCE_ATTRIBUTION\":\n        this.forceAttribution(action.data);\n        break;\n      default:\n        Cu.reportError(\"Unknown message received\");\n        break;\n    }\n  }\n}\nthis._ASRouter = _ASRouter;\nthis.TRAILHEAD_CONFIG = TRAILHEAD_CONFIG;\n\n/**\n * ASRouter - singleton instance of _ASRouter that controls all messages\n * in the new tab page.\n */\nthis.ASRouter = new _ASRouter();\n\nconst EXPORTED_SYMBOLS = [\n  \"_ASRouter\",\n  \"ASRouter\",\n  \"MessageLoaderUtils\",\n  \"TRAILHEAD_CONFIG\",\n];\n"
  },
  {
    "path": "lib/ASRouterFeed.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nconst { actionTypes: at } = ChromeUtils.import(\n  \"resource://activity-stream/common/Actions.jsm\"\n);\nconst { ASRouter } = ChromeUtils.import(\n  \"resource://activity-stream/lib/ASRouter.jsm\"\n);\n\n/**\n * @class ASRouterFeed - Connects ASRouter singleton (see above) to Activity Stream's\n * store so that it can use the RemotePageManager.\n */\nclass ASRouterFeed {\n  constructor(options = {}) {\n    this.router = options.router || ASRouter;\n  }\n\n  async enable() {\n    if (!this.router.initialized) {\n      await this.router.init(\n        this.store._messageChannel.channel,\n        this.store.dbStorage.getDbTable(\"snippets\"),\n        this.store.dispatch\n      );\n    }\n  }\n\n  disable() {\n    if (this.router.initialized) {\n      this.router.uninit();\n    }\n  }\n\n  onAction(action) {\n    switch (action.type) {\n      case at.INIT:\n        this.enable();\n        break;\n      case at.UNINIT:\n        this.disable();\n        break;\n    }\n  }\n}\nthis.ASRouterFeed = ASRouterFeed;\n\nconst EXPORTED_SYMBOLS = [\"ASRouterFeed\"];\n"
  },
  {
    "path": "lib/ASRouterPreferences.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\"use strict\";\n\nconst { Services } = ChromeUtils.import(\"resource://gre/modules/Services.jsm\");\nconst { XPCOMUtils } = ChromeUtils.import(\n  \"resource://gre/modules/XPCOMUtils.jsm\"\n);\n\nconst PROVIDER_PREF_BRANCH =\n  \"browser.newtabpage.activity-stream.asrouter.providers.\";\nconst DEVTOOLS_PREF =\n  \"browser.newtabpage.activity-stream.asrouter.devtoolsEnabled\";\nconst FXA_USERNAME_PREF = \"services.sync.username\";\nconst FIRST_RUN_PREF = \"trailhead.firstrun.branches\";\nconst DEFAULT_FIRSTRUN_TRIPLET = \"supercharge\";\nconst DEFAULT_FIRSTRUN_INTERRUPT = \"join\";\n\nfunction getTrailheadConfigFromPref(value) {\n  let [interrupt, triplet] = value.split(\"-\");\n  return {\n    trailheadInterrupt: interrupt || DEFAULT_FIRSTRUN_INTERRUPT,\n    trailheadTriplet: triplet || DEFAULT_FIRSTRUN_TRIPLET,\n  };\n}\n\nXPCOMUtils.defineLazyPreferenceGetter(\n  this,\n  \"trailheadPrefs\",\n  FIRST_RUN_PREF,\n  \"\",\n  null,\n  getTrailheadConfigFromPref\n);\n\nconst DEFAULT_STATE = {\n  _initialized: false,\n  _providers: null,\n  _providerPrefBranch: PROVIDER_PREF_BRANCH,\n  _devtoolsEnabled: null,\n  _devtoolsPref: DEVTOOLS_PREF,\n};\n\nconst USER_PREFERENCES = {\n  snippets: \"browser.newtabpage.activity-stream.feeds.snippets\",\n  cfrAddons: \"browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons\",\n  cfrFeatures:\n    \"browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features\",\n};\n\n// Preferences that influence targeting attributes. When these change we need\n// to re-evaluate if the message targeting still matches\nconst TARGETING_PREFERENCES = [FXA_USERNAME_PREF];\n\nconst TEST_PROVIDERS = [\n  {\n    id: \"snippets_local_testing\",\n    type: \"local\",\n    localProvider: \"SnippetsTestMessageProvider\",\n    enabled: true,\n  },\n  {\n    id: \"panel_local_testing\",\n    type: \"local\",\n    localProvider: \"PanelTestProvider\",\n    enabled: true,\n  },\n];\n\nclass _ASRouterPreferences {\n  constructor() {\n    Object.assign(this, DEFAULT_STATE);\n    this._callbacks = new Set();\n\n    XPCOMUtils.defineLazyPreferenceGetter(\n      this,\n      \"personalizedCfrScores\",\n      \"browser.messaging-system.personalized-cfr.scores\",\n      \"{}\",\n      null,\n      this._transformPersonalizedCfrScores\n    );\n\n    XPCOMUtils.defineLazyPreferenceGetter(\n      this,\n      \"personalizedCfrThreshold\",\n      \"browser.messaging-system.personalized-cfr.score-threshold\",\n      5000\n    );\n  }\n\n  _transformPersonalizedCfrScores(value) {\n    let result = {};\n    try {\n      result = JSON.parse(value);\n    } catch (e) {\n      Cu.reportError(e);\n    }\n    return result;\n  }\n\n  _getProviderConfig() {\n    const prefList = Services.prefs.getChildList(this._providerPrefBranch);\n    return prefList.reduce((filtered, pref) => {\n      let value;\n      try {\n        value = JSON.parse(Services.prefs.getStringPref(pref, \"\"));\n      } catch (e) {\n        Cu.reportError(\n          `Could not parse ASRouter preference. Try resetting ${pref} in about:config.`\n        );\n      }\n      if (value) {\n        filtered.push(value);\n      }\n      return filtered;\n    }, []);\n  }\n\n  // istanbul ignore next\n  get trailhead() {\n    return trailheadPrefs;\n  }\n\n  get providers() {\n    if (!this._initialized || this._providers === null) {\n      const config = this._getProviderConfig();\n      const providers = config.map(provider => Object.freeze(provider));\n      if (this.devtoolsEnabled) {\n        providers.unshift(...TEST_PROVIDERS);\n      }\n      this._providers = Object.freeze(providers);\n    }\n\n    return this._providers;\n  }\n\n  enableOrDisableProvider(id, value) {\n    const providers = this._getProviderConfig();\n    const config = providers.find(p => p.id === id);\n    if (!config) {\n      Cu.reportError(\n        `Cannot set enabled state for '${id}' because the pref ${\n          this._providerPrefBranch\n        }${id} does not exist or is not correctly formatted.`\n      );\n      return;\n    }\n\n    Services.prefs.setStringPref(\n      this._providerPrefBranch + id,\n      JSON.stringify({ ...config, enabled: value })\n    );\n  }\n\n  resetProviderPref() {\n    for (const pref of Services.prefs.getChildList(this._providerPrefBranch)) {\n      Services.prefs.clearUserPref(pref);\n    }\n    for (const id of Object.keys(USER_PREFERENCES)) {\n      Services.prefs.clearUserPref(USER_PREFERENCES[id]);\n    }\n  }\n\n  get devtoolsEnabled() {\n    if (!this._initialized || this._devtoolsEnabled === null) {\n      this._devtoolsEnabled = Services.prefs.getBoolPref(\n        this._devtoolsPref,\n        false\n      );\n    }\n    return this._devtoolsEnabled;\n  }\n\n  observe(aSubject, aTopic, aPrefName) {\n    if (aPrefName && aPrefName.startsWith(this._providerPrefBranch)) {\n      this._providers = null;\n    } else if (aPrefName === this._devtoolsPref) {\n      this._providers = null;\n      this._devtoolsEnabled = null;\n    }\n    this._callbacks.forEach(cb => cb(aPrefName));\n  }\n\n  getUserPreference(providerId) {\n    if (!USER_PREFERENCES[providerId]) {\n      return null;\n    }\n    return Services.prefs.getBoolPref(USER_PREFERENCES[providerId], true);\n  }\n\n  getAllUserPreferences() {\n    const values = {};\n    for (const id of Object.keys(USER_PREFERENCES)) {\n      values[id] = this.getUserPreference(id);\n    }\n    return values;\n  }\n\n  setUserPreference(providerId, value) {\n    if (!USER_PREFERENCES[providerId]) {\n      return;\n    }\n    Services.prefs.setBoolPref(USER_PREFERENCES[providerId], value);\n  }\n\n  addListener(callback) {\n    this._callbacks.add(callback);\n  }\n\n  removeListener(callback) {\n    this._callbacks.delete(callback);\n  }\n\n  init() {\n    if (this._initialized) {\n      return;\n    }\n    Services.prefs.addObserver(this._providerPrefBranch, this);\n    Services.prefs.addObserver(this._devtoolsPref, this);\n    for (const id of Object.keys(USER_PREFERENCES)) {\n      Services.prefs.addObserver(USER_PREFERENCES[id], this);\n    }\n    for (const targetingPref of TARGETING_PREFERENCES) {\n      Services.prefs.addObserver(targetingPref, this);\n    }\n    this._initialized = true;\n  }\n\n  uninit() {\n    if (this._initialized) {\n      Services.prefs.removeObserver(this._providerPrefBranch, this);\n      Services.prefs.removeObserver(this._devtoolsPref, this);\n      for (const id of Object.keys(USER_PREFERENCES)) {\n        Services.prefs.removeObserver(USER_PREFERENCES[id], this);\n      }\n      for (const targetingPref of TARGETING_PREFERENCES) {\n        Services.prefs.removeObserver(targetingPref, this);\n      }\n    }\n    Object.assign(this, DEFAULT_STATE);\n    this._callbacks.clear();\n  }\n}\nthis._ASRouterPreferences = _ASRouterPreferences;\n\nthis.ASRouterPreferences = new _ASRouterPreferences();\nthis.TEST_PROVIDERS = TEST_PROVIDERS;\nthis.TARGETING_PREFERENCES = TARGETING_PREFERENCES;\nthis.getTrailheadConfigFromPref = getTrailheadConfigFromPref;\n\nconst EXPORTED_SYMBOLS = [\n  \"_ASRouterPreferences\",\n  \"ASRouterPreferences\",\n  \"TEST_PROVIDERS\",\n  \"TARGETING_PREFERENCES\",\n  \"getTrailheadConfigFromPref\",\n];\n"
  },
  {
    "path": "lib/ASRouterTargeting.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nconst SEARCH_REGION_PREF = \"browser.search.region\";\nconst FXA_ENABLED_PREF = \"identity.fxaccounts.enabled\";\n\nconst { Services } = ChromeUtils.import(\"resource://gre/modules/Services.jsm\");\nconst { XPCOMUtils } = ChromeUtils.import(\n  \"resource://gre/modules/XPCOMUtils.jsm\"\n);\n\nXPCOMUtils.defineLazyModuleGetters(this, {\n  ASRouterPreferences: \"resource://activity-stream/lib/ASRouterPreferences.jsm\",\n  AddonManager: \"resource://gre/modules/AddonManager.jsm\",\n  NewTabUtils: \"resource://gre/modules/NewTabUtils.jsm\",\n  ProfileAge: \"resource://gre/modules/ProfileAge.jsm\",\n  ShellService: \"resource:///modules/ShellService.jsm\",\n  TelemetryEnvironment: \"resource://gre/modules/TelemetryEnvironment.jsm\",\n  AppConstants: \"resource://gre/modules/AppConstants.jsm\",\n  AttributionCode: \"resource:///modules/AttributionCode.jsm\",\n  FilterExpressions:\n    \"resource://gre/modules/components-utils/FilterExpressions.jsm\",\n  fxAccounts: \"resource://gre/modules/FxAccounts.jsm\",\n});\n\nXPCOMUtils.defineLazyPreferenceGetter(\n  this,\n  \"cfrFeaturesUserPref\",\n  \"browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features\",\n  true\n);\nXPCOMUtils.defineLazyPreferenceGetter(\n  this,\n  \"cfrAddonsUserPref\",\n  \"browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons\",\n  true\n);\nXPCOMUtils.defineLazyPreferenceGetter(\n  this,\n  \"isWhatsNewPanelEnabled\",\n  \"browser.messaging-system.whatsNewPanel.enabled\",\n  false\n);\nXPCOMUtils.defineLazyPreferenceGetter(\n  this,\n  \"isFxABadgeEnabled\",\n  \"browser.messaging-system.fxatoolbarbadge.enabled\",\n  true\n);\nXPCOMUtils.defineLazyPreferenceGetter(\n  this,\n  \"hasAccessedFxAPanel\",\n  \"identity.fxaccounts.toolbar.accessed\",\n  false\n);\nXPCOMUtils.defineLazyPreferenceGetter(\n  this,\n  \"clientsDevicesDesktop\",\n  \"services.sync.clients.devices.desktop\",\n  0\n);\nXPCOMUtils.defineLazyPreferenceGetter(\n  this,\n  \"clientsDevicesMobile\",\n  \"services.sync.clients.devices.mobile\",\n  0\n);\nXPCOMUtils.defineLazyPreferenceGetter(\n  this,\n  \"syncNumClients\",\n  \"services.sync.numClients\",\n  0\n);\nXPCOMUtils.defineLazyPreferenceGetter(\n  this,\n  \"devtoolsSelfXSSCount\",\n  \"devtools.selfxss.count\",\n  0\n);\nXPCOMUtils.defineLazyPreferenceGetter(\n  this,\n  \"browserSearchRegion\",\n  SEARCH_REGION_PREF,\n  \"\"\n);\nXPCOMUtils.defineLazyPreferenceGetter(\n  this,\n  \"isFxAEnabled\",\n  FXA_ENABLED_PREF,\n  true\n);\nXPCOMUtils.defineLazyPreferenceGetter(\n  this,\n  \"isXPIInstallEnabled\",\n  \"xpinstall.enabled\",\n  true\n);\nXPCOMUtils.defineLazyPreferenceGetter(\n  this,\n  \"snippetsUserPref\",\n  \"browser.newtabpage.activity-stream.feeds.snippets\",\n  true\n);\nXPCOMUtils.defineLazyServiceGetter(\n  this,\n  \"TrackingDBService\",\n  \"@mozilla.org/tracking-db-service;1\",\n  \"nsITrackingDBService\"\n);\n\nconst FXA_USERNAME_PREF = \"services.sync.username\";\nconst MOZ_JEXL_FILEPATH = \"mozjexl\";\n\nconst { activityStreamProvider: asProvider } = NewTabUtils;\n\nconst FRECENT_SITES_UPDATE_INTERVAL = 6 * 60 * 60 * 1000; // Six hours\nconst FRECENT_SITES_IGNORE_BLOCKED = false;\nconst FRECENT_SITES_NUM_ITEMS = 25;\nconst FRECENT_SITES_MIN_FRECENCY = 100;\n\nconst CACHE_EXPIRATION = 60 * 1000;\nconst jexlEvaluationCache = new Map();\n\n/**\n * CachedTargetingGetter\n * @param property {string} Name of the method called on ActivityStreamProvider\n * @param options {{}?} Options object passsed to ActivityStreamProvider method\n * @param updateInterval {number?} Update interval for query. Defaults to FRECENT_SITES_UPDATE_INTERVAL\n */\nfunction CachedTargetingGetter(\n  property,\n  options = null,\n  updateInterval = FRECENT_SITES_UPDATE_INTERVAL\n) {\n  return {\n    _lastUpdated: 0,\n    _value: null,\n    // For testing\n    expire() {\n      this._lastUpdated = 0;\n      this._value = null;\n    },\n    async get() {\n      const now = Date.now();\n      if (now - this._lastUpdated >= updateInterval) {\n        this._value = await asProvider[property](options);\n        this._lastUpdated = now;\n      }\n      return this._value;\n    },\n  };\n}\n\nfunction CheckBrowserNeedsUpdate(\n  updateInterval = FRECENT_SITES_UPDATE_INTERVAL\n) {\n  const UpdateChecker = Cc[\"@mozilla.org/updates/update-checker;1\"];\n  const checker = {\n    _lastUpdated: 0,\n    _value: null,\n    // For testing. Avoid update check network call.\n    setUp(value) {\n      this._lastUpdated = Date.now();\n      this._value = value;\n    },\n    expire() {\n      this._lastUpdated = 0;\n      this._value = null;\n    },\n    get() {\n      return new Promise((resolve, reject) => {\n        const now = Date.now();\n        const updateServiceListener = {\n          onCheckComplete(request, updates) {\n            checker._value = !!updates.length;\n            resolve(checker._value);\n          },\n          onError(request, update) {\n            reject(request);\n          },\n\n          QueryInterface: ChromeUtils.generateQI([\"nsIUpdateCheckListener\"]),\n        };\n\n        if (UpdateChecker && now - this._lastUpdated >= updateInterval) {\n          const checkerInstance = UpdateChecker.createInstance(\n            Ci.nsIUpdateChecker\n          );\n          checkerInstance.checkForUpdates(updateServiceListener, true);\n          this._lastUpdated = now;\n        } else {\n          resolve(this._value);\n        }\n      });\n    },\n  };\n\n  return checker;\n}\n\nconst QueryCache = {\n  expireAll() {\n    Object.keys(this.queries).forEach(query => {\n      this.queries[query].expire();\n    });\n  },\n  queries: {\n    TopFrecentSites: new CachedTargetingGetter(\"getTopFrecentSites\", {\n      ignoreBlocked: FRECENT_SITES_IGNORE_BLOCKED,\n      numItems: FRECENT_SITES_NUM_ITEMS,\n      topsiteFrecency: FRECENT_SITES_MIN_FRECENCY,\n      onePerDomain: true,\n      includeFavicon: false,\n    }),\n    TotalBookmarksCount: new CachedTargetingGetter(\"getTotalBookmarksCount\"),\n    CheckBrowserNeedsUpdate: new CheckBrowserNeedsUpdate(),\n    RecentBookmarks: new CachedTargetingGetter(\"getRecentBookmarks\"),\n  },\n};\n\n/**\n * sortMessagesByWeightedRank\n *\n * Each message has an associated weight, which is guaranteed to be strictly\n * positive. Sort the messages so that higher weighted messages are more likely\n * to come first.\n *\n * Specifically, sort them so that the probability of message x_1 with weight\n * w_1 appearing before message x_2 with weight w_2 is (w_1 / (w_1 + w_2)).\n *\n * This is equivalent to requiring that x_1 appearing before x_2 is (w_1 / w_2)\n * \"times\" as likely as x_2 appearing before x_1.\n *\n * See Bug 1484996, Comment 2 for a justification of the method.\n *\n * @param {Array} messages - A non-empty array of messages to sort, all with\n *                           strictly positive weights\n * @returns the sorted array\n */\nfunction sortMessagesByWeightedRank(messages) {\n  return messages\n    .map(message => ({\n      message,\n      rank: Math.pow(Math.random(), 1 / message.weight),\n    }))\n    .sort((a, b) => b.rank - a.rank)\n    .map(({ message }) => message);\n}\n\n/**\n * getSortedMessages - Given an array of Messages, applies sorting and filtering rules\n *                     in expected order.\n *\n * @param {Array<Message>} messages\n * @param {{}} options\n * @param {boolean} options.ordered - Should .order be used instead of random weighted sorting?\n * @returns {Array<Message>}\n */\nfunction getSortedMessages(messages, options = {}) {\n  let { ordered } = { ordered: false, ...options };\n  let result = messages;\n  let hasScores;\n\n  if (!ordered) {\n    result = sortMessagesByWeightedRank(result);\n  }\n\n  result.sort((a, b) => {\n    // If we find at least one score, we need to apply filtering by threshold at the end.\n    if (!isNaN(a.score) || !isNaN(b.score)) {\n      hasScores = true;\n    }\n\n    // First sort by score if we're doing personalization:\n    if (a.score > b.score || (!isNaN(a.score) && isNaN(b.score))) {\n      return -1;\n    }\n    if (a.score < b.score || (isNaN(a.score) && !isNaN(b.score))) {\n      return 1;\n    }\n\n    // Next, sort by priority\n    if (a.priority > b.priority || (!isNaN(a.priority) && isNaN(b.priority))) {\n      return -1;\n    }\n    if (a.priority < b.priority || (isNaN(a.priority) && !isNaN(b.priority))) {\n      return 1;\n    }\n\n    // Sort messages with targeting expressions higher than those with none\n    if (a.targeting && !b.targeting) {\n      return -1;\n    }\n    if (!a.targeting && b.targeting) {\n      return 1;\n    }\n\n    // Next, sort by order *ascending* if ordered = true\n    if (ordered) {\n      if (a.order > b.order || (!isNaN(a.order) && isNaN(b.order))) {\n        return 1;\n      }\n      if (a.order < b.order || (isNaN(a.order) && !isNaN(b.order))) {\n        return -1;\n      }\n    }\n\n    return 0;\n  });\n\n  if (hasScores && !isNaN(ASRouterPreferences.personalizedCfrThreshold)) {\n    return result.filter(\n      message =>\n        isNaN(message.score) ||\n        message.score >= ASRouterPreferences.personalizedCfrThreshold\n    );\n  }\n\n  return result;\n}\n\nconst TargetingGetters = {\n  get locale() {\n    return Services.locale.appLocaleAsLangTag;\n  },\n  get localeLanguageCode() {\n    return (\n      Services.locale.appLocaleAsLangTag &&\n      Services.locale.appLocaleAsLangTag.substr(0, 2)\n    );\n  },\n  get browserSettings() {\n    const { settings } = TelemetryEnvironment.currentEnvironment;\n    return {\n      // This way of getting attribution is deprecated - use atttributionData instead\n      attribution: settings.attribution,\n      update: settings.update,\n    };\n  },\n  get attributionData() {\n    // Attribution is determined at startup - so we can use the cached attribution at this point\n    return AttributionCode.getCachedAttributionData();\n  },\n  get currentDate() {\n    return new Date();\n  },\n  get profileAgeCreated() {\n    return ProfileAge().then(times => times.created);\n  },\n  get profileAgeReset() {\n    return ProfileAge().then(times => times.reset);\n  },\n  get usesFirefoxSync() {\n    return Services.prefs.prefHasUserValue(FXA_USERNAME_PREF);\n  },\n  get isFxAEnabled() {\n    return isFxAEnabled;\n  },\n  get trailheadInterrupt() {\n    return ASRouterPreferences.trailhead.trailheadInterrupt;\n  },\n  get trailheadTriplet() {\n    return ASRouterPreferences.trailhead.trailheadTriplet;\n  },\n  get sync() {\n    return {\n      desktopDevices: clientsDevicesDesktop,\n      mobileDevices: clientsDevicesMobile,\n      totalDevices: syncNumClients,\n    };\n  },\n  get xpinstallEnabled() {\n    // This is needed for all add-on recommendations, to know if we allow xpi installs in the first place\n    return isXPIInstallEnabled;\n  },\n  get addonsInfo() {\n    return AddonManager.getActiveAddons([\"extension\", \"service\"]).then(\n      ({ addons, fullData }) => {\n        const info = {};\n        for (const addon of addons) {\n          info[addon.id] = {\n            version: addon.version,\n            type: addon.type,\n            isSystem: addon.isSystem,\n            isWebExtension: addon.isWebExtension,\n          };\n          if (fullData) {\n            Object.assign(info[addon.id], {\n              name: addon.name,\n              userDisabled: addon.userDisabled,\n              installDate: addon.installDate,\n            });\n          }\n        }\n        return { addons: info, isFullData: fullData };\n      }\n    );\n  },\n  get searchEngines() {\n    return new Promise(resolve => {\n      // Note: calling init ensures this code is only executed after Search has been initialized\n      Services.search\n        .getVisibleEngines()\n        .then(engines => {\n          resolve({\n            current: Services.search.defaultEngine.identifier,\n            installed: engines\n              .map(engine => engine.identifier)\n              .filter(engine => engine),\n          });\n        })\n        .catch(() => resolve({ installed: [], current: \"\" }));\n    });\n  },\n  get isDefaultBrowser() {\n    try {\n      return ShellService.isDefaultBrowser();\n    } catch (e) {}\n    return null;\n  },\n  get devToolsOpenedCount() {\n    return devtoolsSelfXSSCount;\n  },\n  get topFrecentSites() {\n    return QueryCache.queries.TopFrecentSites.get().then(sites =>\n      sites.map(site => ({\n        url: site.url,\n        host: new URL(site.url).hostname,\n        frecency: site.frecency,\n        lastVisitDate: site.lastVisitDate,\n      }))\n    );\n  },\n  get recentBookmarks() {\n    return QueryCache.queries.RecentBookmarks.get();\n  },\n  get pinnedSites() {\n    return NewTabUtils.pinnedLinks.links.map(site =>\n      site\n        ? {\n            url: site.url,\n            host: new URL(site.url).hostname,\n            searchTopSite: site.searchTopSite,\n          }\n        : {}\n    );\n  },\n  get providerCohorts() {\n    return ASRouterPreferences.providers.reduce((prev, current) => {\n      prev[current.id] = current.cohort || \"\";\n      return prev;\n    }, {});\n  },\n  get totalBookmarksCount() {\n    return QueryCache.queries.TotalBookmarksCount.get();\n  },\n  get firefoxVersion() {\n    return parseInt(AppConstants.MOZ_APP_VERSION.match(/\\d+/), 10);\n  },\n  get region() {\n    return browserSearchRegion;\n  },\n  get needsUpdate() {\n    return QueryCache.queries.CheckBrowserNeedsUpdate.get();\n  },\n  get hasPinnedTabs() {\n    for (let win of Services.wm.getEnumerator(\"navigator:browser\")) {\n      if (win.closed || !win.ownerGlobal.gBrowser) {\n        continue;\n      }\n      if (win.ownerGlobal.gBrowser.visibleTabs.filter(t => t.pinned).length) {\n        return true;\n      }\n    }\n\n    return false;\n  },\n  get hasAccessedFxAPanel() {\n    return hasAccessedFxAPanel;\n  },\n  get isWhatsNewPanelEnabled() {\n    return isWhatsNewPanelEnabled;\n  },\n  get isFxABadgeEnabled() {\n    return isFxABadgeEnabled;\n  },\n  get userPrefs() {\n    return {\n      cfrFeatures: cfrFeaturesUserPref,\n      cfrAddons: cfrAddonsUserPref,\n      snippets: snippetsUserPref,\n    };\n  },\n  get totalBlockedCount() {\n    return TrackingDBService.sumAllEvents();\n  },\n  get blockedCountByType() {\n    const idToTextMap = new Map([\n      [Ci.nsITrackingDBService.TRACKERS_ID, \"trackerCount\"],\n      [Ci.nsITrackingDBService.TRACKING_COOKIES_ID, \"cookieCount\"],\n      [Ci.nsITrackingDBService.CRYPTOMINERS_ID, \"cryptominerCount\"],\n      [Ci.nsITrackingDBService.FINGERPRINTERS_ID, \"fingerprinterCount\"],\n      [Ci.nsITrackingDBService.SOCIAL_ID, \"socialCount\"],\n    ]);\n\n    const dateTo = new Date();\n    const dateFrom = new Date(dateTo.getTime() - 42 * 24 * 60 * 60 * 1000);\n    return TrackingDBService.getEventsByDateRange(dateFrom, dateTo).then(\n      eventsByDate => {\n        let totalEvents = {};\n        for (let blockedType of idToTextMap.values()) {\n          totalEvents[blockedType] = 0;\n        }\n\n        return eventsByDate.reduce((acc, day) => {\n          const type = day.getResultByName(\"type\");\n          const count = day.getResultByName(\"count\");\n          acc[idToTextMap.get(type)] = acc[idToTextMap.get(type)] + count;\n          return acc;\n        }, totalEvents);\n      }\n    );\n  },\n  get attachedFxAOAuthClients() {\n    // Explicitly catch error objects e.g.  NO_ACCOUNT triggered when\n    // setting FXA_USERNAME_PREF from tests\n    return this.usesFirefoxSync\n      ? new Promise(resolve => {\n          fxAccounts\n            .listAttachedOAuthClients()\n            .then(clients => {\n              resolve(clients);\n            })\n            .catch(() => resolve([]));\n        })\n      : [];\n  },\n  get platformName() {\n    return AppConstants.platform;\n  },\n  get scores() {\n    return ASRouterPreferences.personalizedCfrScores;\n  },\n  get scoreThreshold() {\n    return ASRouterPreferences.personalizedCfrThreshold;\n  },\n};\n\nthis.ASRouterTargeting = {\n  Environment: TargetingGetters,\n\n  ERROR_TYPES: {\n    MALFORMED_EXPRESSION: \"MALFORMED_EXPRESSION\",\n    OTHER_ERROR: \"OTHER_ERROR\",\n  },\n\n  // Combines the getter properties of two objects without evaluating them\n  combineContexts(contextA = {}, contextB = {}) {\n    const sameProperty = Object.keys(contextA).find(p =>\n      Object.keys(contextB).includes(p)\n    );\n    if (sameProperty) {\n      Cu.reportError(\n        `Property ${sameProperty} exists in both contexts and is overwritten.`\n      );\n    }\n\n    const context = {};\n    Object.defineProperties(\n      context,\n      Object.getOwnPropertyDescriptors(contextA)\n    );\n    Object.defineProperties(\n      context,\n      Object.getOwnPropertyDescriptors(contextB)\n    );\n\n    return context;\n  },\n\n  isMatch(filterExpression, customContext) {\n    return FilterExpressions.eval(\n      filterExpression,\n      this.combineContexts(this.Environment, customContext)\n    );\n  },\n\n  isTriggerMatch(trigger = {}, candidateMessageTrigger = {}) {\n    if (trigger.id !== candidateMessageTrigger.id) {\n      return false;\n    } else if (\n      !candidateMessageTrigger.params &&\n      !candidateMessageTrigger.patterns\n    ) {\n      return true;\n    }\n\n    if (!trigger.param) {\n      return false;\n    }\n\n    return (\n      (candidateMessageTrigger.params &&\n        trigger.param.host &&\n        candidateMessageTrigger.params.includes(trigger.param.host)) ||\n      (candidateMessageTrigger.params &&\n        trigger.param.type &&\n        candidateMessageTrigger.params.filter(\n          t => (t & trigger.param.type) === t\n        ).length) ||\n      (candidateMessageTrigger.patterns &&\n        trigger.param.url &&\n        new MatchPatternSet(candidateMessageTrigger.patterns).matches(\n          trigger.param.url\n        ))\n    );\n  },\n\n  /**\n   * getCachedEvaluation - Return a cached jexl evaluation if available\n   *\n   * @param {string} targeting JEXL expression to lookup\n   * @returns {obj|null} Object with value result or null if not available\n   */\n  getCachedEvaluation(targeting) {\n    if (jexlEvaluationCache.has(targeting)) {\n      const { timestamp, value } = jexlEvaluationCache.get(targeting);\n      if (Date.now() - timestamp <= CACHE_EXPIRATION) {\n        return { value };\n      }\n      jexlEvaluationCache.delete(targeting);\n    }\n\n    return null;\n  },\n\n  /**\n   * checkMessageTargeting - Checks is a message's targeting parameters are satisfied\n   *\n   * @param {*} message An AS router message\n   * @param {obj} context A FilterExpression context\n   * @param {func} onError A function to handle errors (takes two params; error, message)\n   * @param {boolean} shouldCache Should the JEXL evaluations be cached and reused.\n   * @returns\n   */\n  async checkMessageTargeting(message, context, onError, shouldCache) {\n    // If no targeting is specified,\n    if (!message.targeting) {\n      return true;\n    }\n    let result;\n    try {\n      if (shouldCache) {\n        result = this.getCachedEvaluation(message.targeting);\n        if (result) {\n          return result.value;\n        }\n      }\n      result = await this.isMatch(message.targeting, context);\n      if (shouldCache) {\n        jexlEvaluationCache.set(message.targeting, {\n          timestamp: Date.now(),\n          value: result,\n        });\n      }\n    } catch (error) {\n      Cu.reportError(error);\n      if (onError) {\n        const type = error.fileName.includes(MOZ_JEXL_FILEPATH)\n          ? this.ERROR_TYPES.MALFORMED_EXPRESSION\n          : this.ERROR_TYPES.OTHER_ERROR;\n        onError(type, error, message);\n      }\n      result = false;\n    }\n    return result;\n  },\n\n  _getCombinedContext(trigger, context) {\n    const triggerContext = trigger ? trigger.context : {};\n    return this.combineContexts(context, triggerContext);\n  },\n\n  _isMessageMatch(message, trigger, context, onError, shouldCache = false) {\n    return (\n      message &&\n      (trigger\n        ? this.isTriggerMatch(trigger, message.trigger)\n        : !message.trigger) &&\n      // If a trigger expression was passed to this function, the message should match it.\n      // Otherwise, we should choose a message with no trigger property (i.e. a message that can show up at any time)\n      this.checkMessageTargeting(message, context, onError, shouldCache)\n    );\n  },\n\n  /**\n   * findMatchingMessage - Given an array of messages, returns one message\n   *                       whos targeting expression evaluates to true\n   *\n   * @param {Array<Message>} messages An array of AS router messages\n   * @param {trigger} string A trigger expression if a message for that trigger is desired\n   * @param {obj|null} context A FilterExpression context. Defaults to TargetingGetters above.\n   * @param {func} onError A function to handle errors (takes two params; error, message)\n   * @param {func} ordered An optional param when true sort message by order specified in message\n   * @param {boolean} shouldCache Should the JEXL evaluations be cached and reused.\n   * @param {boolean} returnAll Should we return all matching messages, not just the first one found.\n   * @returns {obj|Array<Message>} If returnAll is false, a single message. If returnAll is true, an array of messages.\n   */\n  async findMatchingMessage({\n    messages,\n    trigger,\n    context,\n    onError,\n    ordered = false,\n    shouldCache = false,\n    returnAll = false,\n  }) {\n    const sortedMessages = getSortedMessages(messages, { ordered });\n    const combinedContext = this._getCombinedContext(trigger, context);\n    const matching = returnAll ? [] : null;\n\n    const isMatch = candidate =>\n      this._isMessageMatch(\n        candidate,\n        trigger,\n        combinedContext,\n        onError,\n        shouldCache\n      );\n\n    for (const candidate of sortedMessages) {\n      if (await isMatch(candidate)) {\n        // If not returnAll, we should return the first message we find that matches.\n        if (!returnAll) {\n          return candidate;\n        }\n\n        matching.push(candidate);\n      }\n    }\n    return matching;\n  },\n};\n\n// Export for testing\nthis.getSortedMessages = getSortedMessages;\nthis.QueryCache = QueryCache;\nthis.CachedTargetingGetter = CachedTargetingGetter;\nthis.EXPORTED_SYMBOLS = [\n  \"ASRouterTargeting\",\n  \"QueryCache\",\n  \"CachedTargetingGetter\",\n  \"getSortedMessages\",\n];\n"
  },
  {
    "path": "lib/ASRouterTriggerListeners.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\"use strict\";\n\nconst { Services } = ChromeUtils.import(\"resource://gre/modules/Services.jsm\");\nconst { XPCOMUtils } = ChromeUtils.import(\n  \"resource://gre/modules/XPCOMUtils.jsm\"\n);\n\nXPCOMUtils.defineLazyModuleGetters(this, {\n  PrivateBrowsingUtils: \"resource://gre/modules/PrivateBrowsingUtils.jsm\",\n  EveryWindow: \"resource:///modules/EveryWindow.jsm\",\n});\n\nconst FEW_MINUTES = 15 * 60 * 1000; // 15 mins\nconst MATCH_PATTERN_OPTIONS = { ignorePath: true };\n\nfunction isPrivateWindow(win) {\n  return (\n    !(win instanceof Ci.nsIDOMWindow) ||\n    win.closed ||\n    PrivateBrowsingUtils.isWindowPrivate(win)\n  );\n}\n\n/**\n * Check current location against the list of whitelisted hosts\n * Additionally verify for redirects and check original request URL against\n * the whitelist.\n *\n * @returns {object} - {host, url} pair that matched the whitelist\n */\nfunction checkURLMatch(aLocationURI, { hosts, matchPatternSet }, aRequest) {\n  // If checks pass we return a match\n  let match;\n  try {\n    match = { host: aLocationURI.host, url: aLocationURI.spec };\n  } catch (e) {\n    // nsIURI.host can throw for non-nsStandardURL nsIURIs\n    return false;\n  }\n\n  // Check current location against whitelisted hosts\n  if (hosts.has(match.host)) {\n    return match;\n  }\n\n  if (matchPatternSet) {\n    if (matchPatternSet.matches(match.url)) {\n      return match;\n    }\n  }\n\n  // Nothing else to check, return early\n  if (!aRequest) {\n    return false;\n  }\n\n  // The original URL at the start of the request\n  const originalLocation = aRequest.QueryInterface(Ci.nsIChannel).originalURI;\n  // We have been redirected\n  if (originalLocation.spec !== aLocationURI.spec) {\n    return (\n      hosts.has(originalLocation.host) && {\n        host: originalLocation.host,\n        url: originalLocation.spec,\n      }\n    );\n  }\n\n  return false;\n}\n\nfunction createMatchPatternSet(patterns, flags = MATCH_PATTERN_OPTIONS) {\n  try {\n    return new MatchPatternSet(new Set(patterns), flags);\n  } catch (e) {\n    Cu.reportError(e);\n  }\n  return new MatchPatternSet([]);\n}\n\n/**\n * A Map from trigger IDs to singleton trigger listeners. Each listener must\n * have idempotent `init` and `uninit` methods.\n */\nthis.ASRouterTriggerListeners = new Map([\n  [\n    \"openArticleURL\",\n    {\n      id: \"openArticleURL\",\n      _initialized: false,\n      _triggerHandler: null,\n      _hosts: new Set(),\n      _matchPatternSet: null,\n      readerModeEvent: \"Reader:UpdateReaderButton\",\n\n      init(triggerHandler, hosts, patterns) {\n        if (!this._initialized) {\n          this.receiveMessage = this.receiveMessage.bind(this);\n          Services.mm.addMessageListener(this.readerModeEvent, this);\n          this._triggerHandler = triggerHandler;\n          this._initialized = true;\n        }\n        if (patterns) {\n          this._matchPatternSet = createMatchPatternSet([\n            ...(this._matchPatternSet ? this._matchPatternSet.patterns : []),\n            ...patterns,\n          ]);\n        }\n        if (hosts) {\n          hosts.forEach(h => this._hosts.add(h));\n        }\n      },\n\n      receiveMessage({ data, target }) {\n        if (data && data.isArticle) {\n          const match = checkURLMatch(target.currentURI, {\n            hosts: this._hosts,\n            matchPatternSet: this._matchPatternSet,\n          });\n          if (match) {\n            this._triggerHandler(target, { id: this.id, param: match });\n          }\n        }\n      },\n\n      uninit() {\n        if (this._initialized) {\n          Services.mm.removeMessageListener(this.readerModeEvent, this);\n          this._initialized = false;\n          this._triggerHandler = null;\n          this._hosts = new Set();\n          this._matchPatternSet = null;\n        }\n      },\n    },\n  ],\n  [\n    \"openBookmarkedURL\",\n    {\n      id: \"openBookmarkedURL\",\n      _initialized: false,\n      _triggerHandler: null,\n      _hosts: new Set(),\n      bookmarkEvent: \"bookmark-icon-updated\",\n\n      init(triggerHandler) {\n        if (!this._initialized) {\n          Services.obs.addObserver(this, this.bookmarkEvent);\n          this._triggerHandler = triggerHandler;\n          this._initialized = true;\n        }\n      },\n\n      observe(subject, topic, data) {\n        if (topic === this.bookmarkEvent && data === \"starred\") {\n          const browser = Services.wm.getMostRecentBrowserWindow();\n          if (browser) {\n            this._triggerHandler(browser.gBrowser.selectedBrowser, {\n              id: this.id,\n            });\n          }\n        }\n      },\n\n      uninit() {\n        if (this._initialized) {\n          Services.obs.removeObserver(this, this.bookmarkEvent);\n          this._initialized = false;\n          this._triggerHandler = null;\n          this._hosts = new Set();\n        }\n      },\n    },\n  ],\n  [\n    \"frequentVisits\",\n    {\n      id: \"frequentVisits\",\n      _initialized: false,\n      _triggerHandler: null,\n      _hosts: null,\n      _matchPatternSet: null,\n      _visits: null,\n\n      init(triggerHandler, hosts = [], patterns) {\n        if (!this._initialized) {\n          this.onTabSwitch = this.onTabSwitch.bind(this);\n          EveryWindow.registerCallback(\n            this.id,\n            win => {\n              if (!isPrivateWindow(win)) {\n                win.addEventListener(\"TabSelect\", this.onTabSwitch);\n                win.gBrowser.addTabsProgressListener(this);\n              }\n            },\n            win => {\n              if (!isPrivateWindow(win)) {\n                win.removeEventListener(\"TabSelect\", this.onTabSwitch);\n                win.gBrowser.removeTabsProgressListener(this);\n              }\n            }\n          );\n          this._visits = new Map();\n          this._initialized = true;\n        }\n        this._triggerHandler = triggerHandler;\n        if (patterns) {\n          this._matchPatternSet = createMatchPatternSet([\n            ...(this._matchPatternSet ? this._matchPatternSet.patterns : []),\n            ...patterns,\n          ]);\n        }\n        if (this._hosts) {\n          hosts.forEach(h => this._hosts.add(h));\n        } else {\n          this._hosts = new Set(hosts); // Clone the hosts to avoid unexpected behaviour\n        }\n      },\n\n      /* _updateVisits - Record visit timestamps for websites that match `this._hosts` and only\n       * if it's been more than FEW_MINUTES since the last visit.\n       * @param {string} host - Location host of current selected tab\n       * @returns {boolean} - If the new visit has been recorded\n       */\n      _updateVisits(host) {\n        const visits = this._visits.get(host);\n\n        if (visits && Date.now() - visits[0] > FEW_MINUTES) {\n          this._visits.set(host, [Date.now(), ...visits]);\n          return true;\n        }\n        if (!visits) {\n          this._visits.set(host, [Date.now()]);\n          return true;\n        }\n\n        return false;\n      },\n\n      onTabSwitch(event) {\n        if (!event.target.ownerGlobal.gBrowser) {\n          return;\n        }\n\n        const { gBrowser } = event.target.ownerGlobal;\n        const match = checkURLMatch(gBrowser.currentURI, {\n          hosts: this._hosts,\n          matchPatternSet: this._matchPatternSet,\n        });\n        if (match) {\n          this.triggerHandler(gBrowser.selectedBrowser, match);\n        }\n      },\n\n      triggerHandler(aBrowser, match) {\n        const updated = this._updateVisits(match.host);\n\n        // If the previous visit happend less than FEW_MINUTES ago\n        // no updates were made, no need to trigger the handler\n        if (!updated) {\n          return;\n        }\n\n        this._triggerHandler(aBrowser, {\n          id: this.id,\n          param: match,\n          context: {\n            // Remapped to {host, timestamp} because JEXL operators can only\n            // filter over collections (arrays of objects)\n            recentVisits: this._visits\n              .get(match.host)\n              .map(timestamp => ({ host: match.host, timestamp })),\n          },\n        });\n      },\n\n      onLocationChange(aBrowser, aWebProgress, aRequest, aLocationURI, aFlags) {\n        // Some websites trigger redirect events after they finish loading even\n        // though the location remains the same. This results in onLocationChange\n        // events to be fired twice.\n        const isSameDocument = !!(\n          aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT\n        );\n        if (aWebProgress.isTopLevel && !isSameDocument) {\n          const match = checkURLMatch(\n            aLocationURI,\n            { hosts: this._hosts, matchPatternSet: this._matchPatternSet },\n            aRequest\n          );\n          if (match) {\n            this.triggerHandler(aBrowser, match);\n          }\n        }\n      },\n\n      uninit() {\n        if (this._initialized) {\n          EveryWindow.unregisterCallback(this.id);\n\n          this._initialized = false;\n          this._triggerHandler = null;\n          this._hosts = null;\n          this._matchPatternSet = null;\n          this._visits = null;\n        }\n      },\n    },\n  ],\n\n  /**\n   * Attach listeners to every browser window to detect location changes, and\n   * notify the trigger handler whenever we navigate to a URL with a hostname\n   * we're looking for.\n   */\n  [\n    \"openURL\",\n    {\n      id: \"openURL\",\n      _initialized: false,\n      _triggerHandler: null,\n      _hosts: null,\n      _matchPatternSet: null,\n\n      /*\n       * If the listener is already initialised, `init` will replace the trigger\n       * handler and add any new hosts to `this._hosts`.\n       */\n      init(triggerHandler, hosts = [], patterns) {\n        if (!this._initialized) {\n          this.onLocationChange = this.onLocationChange.bind(this);\n          EveryWindow.registerCallback(\n            this.id,\n            win => {\n              if (!isPrivateWindow(win)) {\n                win.addEventListener(\"TabSelect\", this.onTabSwitch);\n                win.gBrowser.addTabsProgressListener(this);\n              }\n            },\n            win => {\n              if (!isPrivateWindow(win)) {\n                win.removeEventListener(\"TabSelect\", this.onTabSwitch);\n                win.gBrowser.removeTabsProgressListener(this);\n              }\n            }\n          );\n\n          this._initialized = true;\n        }\n        this._triggerHandler = triggerHandler;\n        if (patterns) {\n          this._matchPatternSet = createMatchPatternSet([\n            ...(this._matchPatternSet ? this._matchPatternSet.patterns : []),\n            ...patterns,\n          ]);\n        }\n        if (this._hosts) {\n          hosts.forEach(h => this._hosts.add(h));\n        } else {\n          this._hosts = new Set(hosts); // Clone the hosts to avoid unexpected behaviour\n        }\n      },\n\n      uninit() {\n        if (this._initialized) {\n          EveryWindow.unregisterCallback(this.id);\n\n          this._initialized = false;\n          this._triggerHandler = null;\n          this._hosts = null;\n        }\n      },\n\n      onLocationChange(aBrowser, aWebProgress, aRequest, aLocationURI, aFlags) {\n        // Some websites trigger redirect events after they finish loading even\n        // though the location remains the same. This results in onLocationChange\n        // events to be fired twice.\n        const isSameDocument = !!(\n          aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT\n        );\n        if (aWebProgress.isTopLevel && !isSameDocument) {\n          const match = checkURLMatch(\n            aLocationURI,\n            { hosts: this._hosts, matchPatternSet: this._matchPatternSet },\n            aRequest\n          );\n          if (match) {\n            this._triggerHandler(aBrowser, { id: this.id, param: match });\n          }\n        }\n      },\n    },\n  ],\n\n  /**\n   * Add an observer notification to notify the trigger handler whenever the user saves a new login\n   * via the login capture doorhanger.\n   */\n  [\n    \"newSavedLogin\",\n    {\n      _initialized: false,\n      _triggerHandler: null,\n\n      /**\n       * If the listener is already initialised, `init` will replace the trigger\n       * handler.\n       */\n      init(triggerHandler) {\n        if (!this._initialized) {\n          Services.obs.addObserver(this, \"LoginStats:NewSavedPassword\");\n          this._initialized = true;\n        }\n        this._triggerHandler = triggerHandler;\n      },\n\n      uninit() {\n        if (this._initialized) {\n          Services.obs.removeObserver(this, \"LoginStats:NewSavedPassword\");\n\n          this._initialized = false;\n          this._triggerHandler = null;\n        }\n      },\n\n      observe(aSubject, aTopic, aData) {\n        if (aSubject.currentURI.asciiHost === \"accounts.firefox.com\") {\n          // Don't notify about saved logins on the FxA login origin since this\n          // trigger is used to promote login Sync and getting a recommendation\n          // to enable Sync during the sign up process is a bad UX.\n          return;\n        }\n        this._triggerHandler(aSubject, { id: \"newSavedLogin\" });\n      },\n    },\n  ],\n\n  /**\n   * Attach listener to count location changes and notify the trigger handler\n   * on content blocked event\n   */\n  [\n    \"trackingProtection\",\n    {\n      _initialized: false,\n      _triggerHandler: null,\n      _events: [],\n      _sessionPageLoad: 0,\n      onLocationChange: null,\n\n      init(triggerHandler, params, patterns) {\n        params.forEach(p => this._events.push(p));\n\n        if (!this._initialized) {\n          Services.obs.addObserver(this, \"SiteProtection:ContentBlockingEvent\");\n          Services.obs.addObserver(\n            this,\n            \"SiteProtection:ContentBlockingMilestone\"\n          );\n          this.onLocationChange = this._onLocationChange.bind(this);\n          EveryWindow.registerCallback(\n            this.id,\n            win => {\n              if (!isPrivateWindow(win)) {\n                win.gBrowser.addTabsProgressListener(this);\n              }\n            },\n            win => {\n              if (!isPrivateWindow(win)) {\n                win.gBrowser.removeTabsProgressListener(this);\n              }\n            }\n          );\n\n          this._initialized = true;\n        }\n        this._triggerHandler = triggerHandler;\n      },\n\n      uninit() {\n        if (this._initialized) {\n          Services.obs.removeObserver(\n            this,\n            \"SiteProtection:ContentBlockingEvent\"\n          );\n          Services.obs.removeObserver(\n            this,\n            \"SiteProtection:ContentBlockingMilestone\"\n          );\n          EveryWindow.unregisterCallback(this.id);\n          this.onLocationChange = null;\n          this._initialized = false;\n        }\n        this._triggerHandler = null;\n        this._events = [];\n        this._sessionPageLoad = 0;\n      },\n\n      observe(aSubject, aTopic, aData) {\n        switch (aTopic) {\n          case \"SiteProtection:ContentBlockingEvent\":\n            const { browser, host, event } = aSubject.wrappedJSObject;\n            if (this._events.filter(e => (e & event) === e).length) {\n              this._triggerHandler(browser, {\n                id: \"trackingProtection\",\n                param: {\n                  host,\n                  type: event,\n                },\n                context: {\n                  pageLoad: this._sessionPageLoad,\n                },\n              });\n            }\n            break;\n          case \"SiteProtection:ContentBlockingMilestone\":\n            if (this._events.includes(aSubject.wrappedJSObject.event)) {\n              this._triggerHandler(\n                Services.wm.getMostRecentBrowserWindow().gBrowser\n                  .selectedBrowser,\n                {\n                  id: \"trackingProtection\",\n                  context: {\n                    pageLoad: this._sessionPageLoad,\n                  },\n                  param: {\n                    host: aSubject.wrappedJSObject.event,\n                  },\n                }\n              );\n            }\n            break;\n        }\n      },\n\n      _onLocationChange(\n        aBrowser,\n        aWebProgress,\n        aRequest,\n        aLocationURI,\n        aFlags\n      ) {\n        // Some websites trigger redirect events after they finish loading even\n        // though the location remains the same. This results in onLocationChange\n        // events to be fired twice.\n        const isSameDocument = !!(\n          aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT\n        );\n        if (\n          [\"http\", \"https\"].includes(aLocationURI.scheme) &&\n          aWebProgress.isTopLevel &&\n          !isSameDocument\n        ) {\n          this._sessionPageLoad += 1;\n        }\n      },\n    },\n  ],\n]);\n\nconst EXPORTED_SYMBOLS = [\"ASRouterTriggerListeners\"];\n"
  },
  {
    "path": "lib/AboutPreferences.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\"use strict\";\n\nconst { Services } = ChromeUtils.import(\"resource://gre/modules/Services.jsm\");\nconst { actionTypes: at } = ChromeUtils.import(\n  \"resource://activity-stream/common/Actions.jsm\"\n);\n\nconst HTML_NS = \"http://www.w3.org/1999/xhtml\";\nconst PREFERENCES_LOADED_EVENT = \"home-pane-loaded\";\n\n// These \"section\" objects are formatted in a way to be similar to the ones from\n// SectionsManager to construct the preferences view.\nconst PREFS_BEFORE_SECTIONS = [\n  {\n    id: \"search\",\n    pref: {\n      feed: \"showSearch\",\n      titleString: \"home-prefs-search-header\",\n    },\n    icon: \"chrome://browser/skin/search-glass.svg\",\n  },\n  {\n    id: \"topsites\",\n    pref: {\n      feed: \"feeds.topsites\",\n      titleString: \"home-prefs-topsites-header\",\n      descString: \"home-prefs-topsites-description\",\n    },\n    icon: \"topsites\",\n    maxRows: 4,\n    rowsPref: \"topSitesRows\",\n  },\n];\n\nconst PREFS_AFTER_SECTIONS = [\n  {\n    id: \"snippets\",\n    pref: {\n      feed: \"feeds.snippets\",\n      titleString: \"home-prefs-snippets-header\",\n      descString: \"home-prefs-snippets-description\",\n    },\n    icon: \"info\",\n  },\n];\n\n// This CSS is added to the whole about:preferences page\nconst CUSTOM_CSS = `\n#homeContentsGroup checkbox[src] .checkbox-icon {\n  -moz-context-properties: fill;\n  fill: currentColor;\n  margin-inline-end: 8px;\n  margin-inline-start: 4px;\n  width: 16px;\n}\n#homeContentsGroup [data-subcategory] {\n  margin-top: 14px;\n}\n#homeContentsGroup [data-subcategory] .section-checkbox {\n  font-weight: 600;\n}\n#homeContentsGroup [data-subcategory] > vbox menulist {\n  margin-top: 0;\n  margin-bottom: 0;\n}\n`;\n\nthis.AboutPreferences = class AboutPreferences {\n  init() {\n    Services.obs.addObserver(this, PREFERENCES_LOADED_EVENT);\n  }\n\n  uninit() {\n    Services.obs.removeObserver(this, PREFERENCES_LOADED_EVENT);\n  }\n\n  onAction(action) {\n    switch (action.type) {\n      case at.INIT:\n        this.init();\n        break;\n      case at.UNINIT:\n        this.uninit();\n        break;\n      case at.SETTINGS_OPEN:\n        action._target.browser.ownerGlobal.openPreferences(\"paneHome\");\n        break;\n      // This is used to open the web extension settings page for an extension\n      case at.OPEN_WEBEXT_SETTINGS:\n        action._target.browser.ownerGlobal.BrowserOpenAddonsMgr(\n          `addons://detail/${encodeURIComponent(action.data)}`\n        );\n        break;\n    }\n  }\n\n  handleDiscoverySettings(sections) {\n    // Deep copy object to not modify original Sections state in store\n    let sectionsCopy = JSON.parse(JSON.stringify(sections));\n    sectionsCopy.forEach(obj => {\n      if (obj.id === \"topstories\") {\n        obj.rowsPref = \"\";\n      }\n    });\n    return sectionsCopy;\n  }\n\n  observe(window) {\n    const discoveryStreamConfig = this.store.getState().DiscoveryStream.config;\n    let sections = this.store.getState().Sections;\n\n    if (discoveryStreamConfig.enabled) {\n      sections = this.handleDiscoverySettings(sections);\n    }\n\n    this.renderPreferences(window, [\n      ...PREFS_BEFORE_SECTIONS,\n      ...sections,\n      ...PREFS_AFTER_SECTIONS,\n    ]);\n  }\n\n  /**\n   * Render preferences to an about:preferences content window with the provided\n   * preferences structure.\n   */\n  renderPreferences({ document, Preferences, gHomePane }, prefStructure) {\n    // Helper to create a new element and append it\n    const createAppend = (tag, parent, options) =>\n      parent.appendChild(document.createXULElement(tag, options));\n\n    // Helper to get fluentIDs sometimes encase in an object\n    const getString = message =>\n      typeof message !== \"object\" ? message : message.id;\n\n    // Helper to link a UI element to a preference for updating\n    const linkPref = (element, name, type) => {\n      const fullPref = `browser.newtabpage.activity-stream.${name}`;\n      element.setAttribute(\"preference\", fullPref);\n      Preferences.add({ id: fullPref, type });\n\n      // Prevent changing the UI if the preference can't be changed\n      element.disabled = Preferences.get(fullPref).locked;\n    };\n\n    // Add in custom styling\n    document.insertBefore(\n      document.createProcessingInstruction(\n        \"xml-stylesheet\",\n        `href=\"data:text/css,${encodeURIComponent(CUSTOM_CSS)}\" type=\"text/css\"`\n      ),\n      document.documentElement\n    );\n\n    // Insert a new group immediately after the homepage one\n    const homeGroup = document.getElementById(\"homepageGroup\");\n    const contentsGroup = homeGroup.insertAdjacentElement(\n      \"afterend\",\n      homeGroup.cloneNode()\n    );\n    contentsGroup.id = \"homeContentsGroup\";\n    contentsGroup.setAttribute(\"data-subcategory\", \"contents\");\n    const homeHeader = createAppend(\"label\", contentsGroup).appendChild(\n      document.createElementNS(HTML_NS, \"h2\")\n    );\n    document.l10n.setAttributes(homeHeader, \"home-prefs-content-header\");\n\n    const homeDescription = createAppend(\"description\", contentsGroup);\n    document.l10n.setAttributes(\n      homeDescription,\n      \"home-prefs-content-description\"\n    );\n\n    // Add preferences for each section\n    prefStructure.forEach(sectionData => {\n      const {\n        id,\n        pref: prefData,\n        icon = \"webextension\",\n        maxRows,\n        rowsPref,\n        shouldHidePref,\n      } = sectionData;\n      const { feed: name, titleString = {}, descString, nestedPrefs = [] } =\n        prefData || {};\n\n      // Don't show any sections that we don't want to expose in preferences UI\n      if (shouldHidePref) {\n        return;\n      }\n\n      // Use full icon spec for certain protocols or fall back to packaged icon\n      const iconUrl = !icon.search(/^(chrome|moz-extension|resource):/)\n        ? icon\n        : `resource://activity-stream/data/content/assets/glyph-${icon}-16.svg`;\n\n      // Add the main preference for turning on/off a section\n      const sectionVbox = createAppend(\"vbox\", contentsGroup);\n      sectionVbox.setAttribute(\"data-subcategory\", id);\n      const checkbox = createAppend(\"checkbox\", sectionVbox);\n      checkbox.classList.add(\"section-checkbox\");\n      checkbox.setAttribute(\"src\", iconUrl);\n      document.l10n.setAttributes(\n        checkbox,\n        getString(titleString),\n        titleString.values\n      );\n\n      linkPref(checkbox, name, \"bool\");\n\n      // Specially add a link for stories\n      if (id === \"topstories\") {\n        const sponsoredHbox = createAppend(\"hbox\", sectionVbox);\n        sponsoredHbox.setAttribute(\"align\", \"center\");\n        sponsoredHbox.appendChild(checkbox);\n        checkbox.classList.add(\"tail-with-learn-more\");\n\n        const link = createAppend(\"label\", sponsoredHbox, { is: \"text-link\" });\n        link.classList.add(\"learn-sponsored\");\n        link.setAttribute(\"href\", sectionData.pref.learnMore.link.href);\n        document.l10n.setAttributes(link, sectionData.pref.learnMore.link.id);\n      }\n\n      // Add more details for the section (e.g., description, more prefs)\n      const detailVbox = createAppend(\"vbox\", sectionVbox);\n      detailVbox.classList.add(\"indent\");\n      if (descString) {\n        const label = createAppend(\"label\", detailVbox);\n        label.classList.add(\"indent\");\n        document.l10n.setAttributes(label, getString(descString));\n\n        // Add a rows dropdown if we have a pref to control and a maximum\n        if (rowsPref && maxRows) {\n          const detailHbox = createAppend(\"hbox\", detailVbox);\n          detailHbox.setAttribute(\"align\", \"center\");\n          label.setAttribute(\"flex\", 1);\n          detailHbox.appendChild(label);\n\n          // Add box so the search tooltip is positioned correctly\n          const tooltipBox = createAppend(\"hbox\", detailHbox);\n\n          // Add appropriate number of localized entries to the dropdown\n          const menulist = createAppend(\"menulist\", tooltipBox);\n          menulist.setAttribute(\"crop\", \"none\");\n          const menupopup = createAppend(\"menupopup\", menulist);\n          for (let num = 1; num <= maxRows; num++) {\n            const item = createAppend(\"menuitem\", menupopup);\n            document.l10n.setAttributes(\n              item,\n              \"home-prefs-sections-rows-option\",\n              { num }\n            );\n            item.setAttribute(\"value\", num);\n          }\n          linkPref(menulist, rowsPref, \"int\");\n        }\n      }\n\n      const subChecks = [];\n      const fullName = `browser.newtabpage.activity-stream.${\n        sectionData.pref.feed\n      }`;\n      const pref = Preferences.get(fullName);\n\n      // Add a checkbox pref for any nested preferences\n      nestedPrefs.forEach(nested => {\n        const subcheck = createAppend(\"checkbox\", detailVbox);\n        subcheck.classList.add(\"indent\");\n        document.l10n.setAttributes(subcheck, nested.titleString);\n        linkPref(subcheck, nested.name, \"bool\");\n        subChecks.push(subcheck);\n        subcheck.disabled = !pref._value;\n      });\n\n      // Disable any nested checkboxes if the parent pref is not enabled.\n      pref.on(\"change\", () => {\n        subChecks.forEach(subcheck => {\n          subcheck.disabled = !pref._value;\n        });\n      });\n    });\n\n    // Update the visibility of the Restore Defaults btn based on checked prefs\n    gHomePane.toggleRestoreDefaultsBtn();\n  }\n};\n\nthis.PREFERENCES_LOADED_EVENT = PREFERENCES_LOADED_EVENT;\nconst EXPORTED_SYMBOLS = [\"AboutPreferences\", \"PREFERENCES_LOADED_EVENT\"];\n"
  },
  {
    "path": "lib/ActivityStream.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\"use strict\";\n\nconst { Services } = ChromeUtils.import(\"resource://gre/modules/Services.jsm\");\n\nChromeUtils.defineModuleGetter(\n  this,\n  \"AppConstants\",\n  \"resource://gre/modules/AppConstants.jsm\"\n);\n\n// NB: Eagerly load modules that will be loaded/constructed/initialized in the\n// common case to avoid the overhead of wrapping and detecting lazy loading.\nconst { actionCreators: ac, actionTypes: at } = ChromeUtils.import(\n  \"resource://activity-stream/common/Actions.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"AboutPreferences\",\n  \"resource://activity-stream/lib/AboutPreferences.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"DefaultPrefs\",\n  \"resource://activity-stream/lib/ActivityStreamPrefs.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"NewTabInit\",\n  \"resource://activity-stream/lib/NewTabInit.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"SectionsFeed\",\n  \"resource://activity-stream/lib/SectionsManager.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"PlacesFeed\",\n  \"resource://activity-stream/lib/PlacesFeed.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"PrefsFeed\",\n  \"resource://activity-stream/lib/PrefsFeed.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"Store\",\n  \"resource://activity-stream/lib/Store.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"SystemTickFeed\",\n  \"resource://activity-stream/lib/SystemTickFeed.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"TelemetryFeed\",\n  \"resource://activity-stream/lib/TelemetryFeed.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"FaviconFeed\",\n  \"resource://activity-stream/lib/FaviconFeed.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"TopSitesFeed\",\n  \"resource://activity-stream/lib/TopSitesFeed.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"TopStoriesFeed\",\n  \"resource://activity-stream/lib/TopStoriesFeed.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"HighlightsFeed\",\n  \"resource://activity-stream/lib/HighlightsFeed.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"ASRouterFeed\",\n  \"resource://activity-stream/lib/ASRouterFeed.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"DiscoveryStreamFeed\",\n  \"resource://activity-stream/lib/DiscoveryStreamFeed.jsm\"\n);\n\nconst DEFAULT_SITES = new Map([\n  // This first item is the global list fallback for any unexpected geos\n  [\n    \"\",\n    \"https://www.youtube.com/,https://www.facebook.com/,https://www.wikipedia.org/,https://www.reddit.com/,https://www.amazon.com/,https://twitter.com/\",\n  ],\n  [\n    \"US\",\n    \"https://www.youtube.com/,https://www.facebook.com/,https://www.amazon.com/,https://www.reddit.com/,https://www.wikipedia.org/,https://twitter.com/\",\n  ],\n  [\n    \"CA\",\n    \"https://www.youtube.com/,https://www.facebook.com/,https://www.reddit.com/,https://www.wikipedia.org/,https://www.amazon.ca/,https://twitter.com/\",\n  ],\n  [\n    \"DE\",\n    \"https://www.youtube.com/,https://www.facebook.com/,https://www.amazon.de/,https://www.ebay.de/,https://www.wikipedia.org/,https://www.reddit.com/\",\n  ],\n  [\n    \"PL\",\n    \"https://www.youtube.com/,https://www.facebook.com/,https://allegro.pl/,https://www.wikipedia.org/,https://www.olx.pl/,https://www.wykop.pl/\",\n  ],\n  [\n    \"RU\",\n    \"https://vk.com/,https://www.youtube.com/,https://ok.ru/,https://www.avito.ru/,https://www.aliexpress.com/,https://www.wikipedia.org/\",\n  ],\n  [\n    \"GB\",\n    \"https://www.youtube.com/,https://www.facebook.com/,https://www.reddit.com/,https://www.amazon.co.uk/,https://www.bbc.co.uk/,https://www.ebay.co.uk/\",\n  ],\n  [\n    \"FR\",\n    \"https://www.youtube.com/,https://www.facebook.com/,https://www.wikipedia.org/,https://www.amazon.fr/,https://www.leboncoin.fr/,https://twitter.com/\",\n  ],\n]);\nconst GEO_PREF = \"browser.search.region\";\nconst SPOCS_GEOS = [\"US\"];\n\n// Determine if spocs should be shown for a geo/locale\nfunction showSpocs({ geo }) {\n  return SPOCS_GEOS.includes(geo);\n}\n\n// Configure default Activity Stream prefs with a plain `value` or a `getValue`\n// that computes a value. A `value_local_dev` is used for development defaults.\nconst PREFS_CONFIG = new Map([\n  [\n    \"default.sites\",\n    {\n      title:\n        \"Comma-separated list of default top sites to fill in behind visited sites\",\n      getValue: ({ geo }) =>\n        DEFAULT_SITES.get(DEFAULT_SITES.has(geo) ? geo : \"\"),\n    },\n  ],\n  [\n    \"feeds.section.topstories.options\",\n    {\n      title: \"Configuration options for top stories feed\",\n      // This is a dynamic pref as it depends on the feed being shown or not\n      getValue: args =>\n        JSON.stringify({\n          api_key_pref: \"extensions.pocket.oAuthConsumerKey\",\n          // Use the opposite value as what default value the feed would have used\n          hidden: !PREFS_CONFIG.get(\"feeds.section.topstories\").getValue(args),\n          provider_icon: \"pocket\",\n          provider_name: \"Pocket\",\n          read_more_endpoint:\n            \"https://getpocket.com/explore/trending?src=fx_new_tab\",\n          stories_endpoint: `https://getpocket.cdn.mozilla.net/v3/firefox/global-recs?version=3&consumer_key=$apiKey&locale_lang=${\n            args.locale\n          }&feed_variant=${\n            showSpocs(args) ? \"default_spocs_on\" : \"default_spocs_off\"\n          }`,\n          stories_referrer: \"https://getpocket.com/recommendations\",\n          topics_endpoint: `https://getpocket.cdn.mozilla.net/v3/firefox/trending-topics?version=2&consumer_key=$apiKey&locale_lang=${\n            args.locale\n          }`,\n          model_keys: [\n            \"nmf_model_animals\",\n            \"nmf_model_business\",\n            \"nmf_model_career\",\n            \"nmf_model_datascience\",\n            \"nmf_model_design\",\n            \"nmf_model_education\",\n            \"nmf_model_entertainment\",\n            \"nmf_model_environment\",\n            \"nmf_model_fashion\",\n            \"nmf_model_finance\",\n            \"nmf_model_food\",\n            \"nmf_model_health\",\n            \"nmf_model_home\",\n            \"nmf_model_life\",\n            \"nmf_model_marketing\",\n            \"nmf_model_politics\",\n            \"nmf_model_programming\",\n            \"nmf_model_science\",\n            \"nmf_model_shopping\",\n            \"nmf_model_sports\",\n            \"nmf_model_tech\",\n            \"nmf_model_travel\",\n            \"nb_model_animals\",\n            \"nb_model_books\",\n            \"nb_model_business\",\n            \"nb_model_career\",\n            \"nb_model_datascience\",\n            \"nb_model_design\",\n            \"nb_model_economics\",\n            \"nb_model_education\",\n            \"nb_model_entertainment\",\n            \"nb_model_environment\",\n            \"nb_model_fashion\",\n            \"nb_model_finance\",\n            \"nb_model_food\",\n            \"nb_model_game\",\n            \"nb_model_health\",\n            \"nb_model_history\",\n            \"nb_model_home\",\n            \"nb_model_life\",\n            \"nb_model_marketing\",\n            \"nb_model_military\",\n            \"nb_model_philosophy\",\n            \"nb_model_photography\",\n            \"nb_model_politics\",\n            \"nb_model_productivity\",\n            \"nb_model_programming\",\n            \"nb_model_psychology\",\n            \"nb_model_science\",\n            \"nb_model_shopping\",\n            \"nb_model_society\",\n            \"nb_model_space\",\n            \"nb_model_sports\",\n            \"nb_model_tech\",\n            \"nb_model_travel\",\n            \"nb_model_writing\",\n          ],\n          show_spocs: showSpocs(args),\n          personalized: true,\n          version: 1,\n        }),\n    },\n  ],\n  [\n    \"showSponsored\",\n    {\n      title:\n        \"Show sponsored cards in spoc experiment (show_spocs in topstories.options has to be set to true as well)\",\n      value: true,\n    },\n  ],\n  [\n    \"pocketCta\",\n    {\n      title: \"Pocket cta and button for logged out users.\",\n      value: JSON.stringify({\n        cta_button: \"\",\n        cta_text: \"\",\n        cta_url: \"\",\n        use_cta: false,\n      }),\n    },\n  ],\n  [\n    \"filterAdult\",\n    {\n      title: \"Remove adult pages from sites, highlights, etc.\",\n      value: true,\n    },\n  ],\n  [\n    \"showSearch\",\n    {\n      title: \"Show the Search bar\",\n      value: true,\n    },\n  ],\n  [\n    \"feeds.snippets\",\n    {\n      title: \"Show snippets on activity stream\",\n      value: true,\n    },\n  ],\n  [\n    \"topSitesRows\",\n    {\n      title: \"Number of rows of Top Sites to display\",\n      value: 1,\n    },\n  ],\n  [\n    \"telemetry\",\n    {\n      title: \"Enable system error and usage data collection\",\n      value: true,\n      value_local_dev: false,\n    },\n  ],\n  [\n    \"telemetry.ut.events\",\n    {\n      title: \"Enable Unified Telemetry event data collection\",\n      value: AppConstants.EARLY_BETA_OR_EARLIER,\n      value_local_dev: false,\n    },\n  ],\n  [\n    \"telemetry.structuredIngestion\",\n    {\n      title: \"Enable Structured Ingestion Telemetry data collection\",\n      value: true,\n      value_local_dev: false,\n    },\n  ],\n  [\n    \"telemetry.structuredIngestion.endpoint\",\n    {\n      title: \"Structured Ingestion telemetry server endpoint\",\n      value: \"https://incoming.telemetry.mozilla.org/submit\",\n    },\n  ],\n  [\n    \"section.highlights.includeVisited\",\n    {\n      title:\n        \"Boolean flag that decides whether or not to show visited pages in highlights.\",\n      value: true,\n    },\n  ],\n  [\n    \"section.highlights.includeBookmarks\",\n    {\n      title:\n        \"Boolean flag that decides whether or not to show bookmarks in highlights.\",\n      value: true,\n    },\n  ],\n  [\n    \"section.highlights.includePocket\",\n    {\n      title:\n        \"Boolean flag that decides whether or not to show saved Pocket stories in highlights.\",\n      value: true,\n    },\n  ],\n  [\n    \"section.highlights.includeDownloads\",\n    {\n      title:\n        \"Boolean flag that decides whether or not to show saved recent Downloads in highlights.\",\n      value: true,\n    },\n  ],\n  [\n    \"section.highlights.rows\",\n    {\n      title: \"Number of rows of Highlights to display\",\n      value: 1,\n    },\n  ],\n  [\n    \"section.topstories.rows\",\n    {\n      title: \"Number of rows of Top Stories to display\",\n      value: 1,\n    },\n  ],\n  [\n    \"sectionOrder\",\n    {\n      title: \"The rendering order for the sections\",\n      value: \"topsites,topstories,highlights\",\n    },\n  ],\n  [\n    \"improvesearch.noDefaultSearchTile\",\n    {\n      title: \"Remove tiles that are the same as the default search\",\n      value: true,\n    },\n  ],\n  [\n    \"improvesearch.topSiteSearchShortcuts.searchEngines\",\n    {\n      title:\n        \"An ordered, comma-delimited list of search shortcuts that we should try and pin\",\n      // This pref is dynamic as the shortcuts vary depending on the region\n      getValue: ({ geo }) => {\n        if (!geo) {\n          return \"\";\n        }\n        const searchShortcuts = [];\n        if (geo === \"CN\") {\n          searchShortcuts.push(\"baidu\");\n        } else if ([\"BY\", \"KZ\", \"RU\", \"TR\"].includes(geo)) {\n          searchShortcuts.push(\"yandex\");\n        } else {\n          searchShortcuts.push(\"google\");\n        }\n        if ([\"DE\", \"FR\", \"GB\", \"IT\", \"JP\", \"US\"].includes(geo)) {\n          searchShortcuts.push(\"amazon\");\n        }\n        return searchShortcuts.join(\",\");\n      },\n    },\n  ],\n  [\n    \"improvesearch.topSiteSearchShortcuts.havePinned\",\n    {\n      title:\n        \"A comma-delimited list of search shortcuts that have previously been pinned\",\n      value: \"\",\n    },\n  ],\n  [\n    \"asrouter.devtoolsEnabled\",\n    {\n      title: \"Are the asrouter devtools enabled?\",\n      value: false,\n    },\n  ],\n  [\n    \"asrouter.userprefs.cfr.addons\",\n    {\n      title: \"Does the user allow CFR addon recommendations?\",\n      value: true,\n    },\n  ],\n  [\n    \"asrouter.userprefs.cfr.features\",\n    {\n      title: \"Does the user allow CFR feature recommendations?\",\n      value: true,\n    },\n  ],\n  [\n    \"asrouter.providers.onboarding\",\n    {\n      title: \"Configuration for onboarding provider\",\n      value: JSON.stringify({\n        id: \"onboarding\",\n        type: \"local\",\n        localProvider: \"OnboardingMessageProvider\",\n        enabled: true,\n        // Block specific messages from this local provider\n        exclude: [],\n      }),\n    },\n  ],\n  [\n    \"asrouter.providers.cfr-fxa\",\n    {\n      title: \"Configuration for CFR FxA Messages provider\",\n      value: JSON.stringify({\n        id: \"cfr-fxa\",\n        enabled: true,\n        type: \"remote-settings\",\n        bucket: \"cfr-fxa\",\n        frequency: { custom: [{ period: \"daily\", cap: 1 }] },\n      }),\n    },\n  ],\n  // See browser/app/profile/firefox.js for other ASR preferences. They must be defined there to enable roll-outs.\n  [\n    \"discoverystream.flight.blocks\",\n    {\n      title: \"Track flight blocks\",\n      skipBroadcast: true,\n      value: \"{}\",\n    },\n  ],\n  [\n    \"discoverystream.config\",\n    {\n      title: \"Configuration for the new pocket new tab\",\n      getValue: ({ geo, locale }) => {\n        // PLEASE NOTE:\n        // hardcoded_layout in `lib/DiscoveryStreamFeed.jsm` only works for en-* and DE and requires refactoring for other locales\n        const dsEnablementMatrix = {\n          US: [\"en-CA\", \"en-GB\", \"en-US\"],\n          CA: [\"en-CA\", \"en-GB\", \"en-US\"],\n          DE: [\"de\", \"de-DE\", \"de-AT\", \"de-CH\"],\n        };\n\n        // Verify that the current geo & locale combination is enabled\n        const isEnabled =\n          !!dsEnablementMatrix[geo] && dsEnablementMatrix[geo].includes(locale);\n\n        return JSON.stringify({\n          api_key_pref: \"extensions.pocket.oAuthConsumerKey\",\n          collapsible: true,\n          enabled: isEnabled,\n          show_spocs: showSpocs({ geo }),\n          hardcoded_layout: true,\n          personalized: true,\n          // This is currently an exmple layout used for dev purposes.\n          layout_endpoint:\n            \"https://getpocket.cdn.mozilla.net/v3/newtab/layout?version=1&consumer_key=$apiKey&layout_variant=basic\",\n        });\n      },\n    },\n  ],\n  [\n    \"discoverystream.endpoints\",\n    {\n      title:\n        \"Endpoint prefixes (comma-separated) that are allowed to be requested\",\n      value: \"https://getpocket.cdn.mozilla.net/,https://spocs.getpocket.com/\",\n    },\n  ],\n  [\n    \"discoverystream.engagementLabelEnabled\",\n    {\n      title:\n        \"Allow the display of engagement labels for discovery stream components (eg: Trending, Popular, etc)\",\n      value: false,\n    },\n  ],\n  [\n    \"discoverystream.spoc.impressions\",\n    {\n      title: \"Track spoc impressions\",\n      skipBroadcast: true,\n      value: \"{}\",\n    },\n  ],\n  [\n    \"discoverystream.endpointSpocsClear\",\n    {\n      title:\n        \"Endpoint for when a user opts-out of sponsored content to delete the user's data from the ad server.\",\n      value: \"https://spocs.getpocket.com/user\",\n    },\n  ],\n  [\n    \"discoverystream.rec.impressions\",\n    {\n      title: \"Track rec impressions\",\n      skipBroadcast: true,\n      value: \"{}\",\n    },\n  ],\n]);\n\n// Array of each feed's FEEDS_CONFIG factory and values to add to PREFS_CONFIG\nconst FEEDS_DATA = [\n  {\n    name: \"aboutpreferences\",\n    factory: () => new AboutPreferences(),\n    title: \"about:preferences rendering\",\n    value: true,\n  },\n  {\n    name: \"newtabinit\",\n    factory: () => new NewTabInit(),\n    title: \"Sends a copy of the state to each new tab that is opened\",\n    value: true,\n  },\n  {\n    name: \"places\",\n    factory: () => new PlacesFeed(),\n    title: \"Listens for and relays various Places-related events\",\n    value: true,\n  },\n  {\n    name: \"prefs\",\n    factory: () => new PrefsFeed(PREFS_CONFIG),\n    title: \"Preferences\",\n    value: true,\n  },\n  {\n    name: \"sections\",\n    factory: () => new SectionsFeed(),\n    title: \"Manages sections\",\n    value: true,\n  },\n  {\n    name: \"section.highlights\",\n    factory: () => new HighlightsFeed(),\n    title: \"Fetches content recommendations from places db\",\n    value: true,\n  },\n  {\n    name: \"section.topstories\",\n    factory: () =>\n      new TopStoriesFeed(PREFS_CONFIG.get(\"discoverystream.config\")),\n    title:\n      \"Fetches content recommendations from a configurable content provider\",\n    // Dynamically determine if Pocket should be shown for a geo / locale\n    getValue: ({ geo, locale }) => {\n      const locales = {\n        US: [\"en-CA\", \"en-GB\", \"en-US\", \"en-ZA\"],\n        CA: [\"en-CA\", \"en-GB\", \"en-US\", \"en-ZA\"],\n        DE: [\"de\", \"de-DE\", \"de-AT\", \"de-CH\"],\n      }[geo];\n      return !!locales && locales.includes(locale);\n    },\n  },\n  {\n    name: \"systemtick\",\n    factory: () => new SystemTickFeed(),\n    title: \"Produces system tick events to periodically check for data expiry\",\n    value: true,\n  },\n  {\n    name: \"telemetry\",\n    factory: () => new TelemetryFeed(),\n    title: \"Relays telemetry-related actions to PingCentre\",\n    value: true,\n  },\n  {\n    name: \"favicon\",\n    factory: () => new FaviconFeed(),\n    title: \"Fetches tippy top manifests from remote service\",\n    value: true,\n  },\n  {\n    name: \"topsites\",\n    factory: () => new TopSitesFeed(),\n    title: \"Queries places and gets metadata for Top Sites section\",\n    value: true,\n  },\n  {\n    name: \"asrouterfeed\",\n    factory: () => new ASRouterFeed(),\n    title: \"Handles AS Router messages, such as snippets and onboaridng\",\n    value: true,\n  },\n  {\n    name: \"discoverystreamfeed\",\n    factory: () => new DiscoveryStreamFeed(),\n    title: \"Handles new pocket ui for the new tab page\",\n    value: true,\n  },\n];\n\nconst FEEDS_CONFIG = new Map();\nfor (const config of FEEDS_DATA) {\n  const pref = `feeds.${config.name}`;\n  FEEDS_CONFIG.set(pref, config.factory);\n  PREFS_CONFIG.set(pref, config);\n}\n\nthis.ActivityStream = class ActivityStream {\n  /**\n   * constructor - Initializes an instance of ActivityStream\n   */\n  constructor() {\n    this.initialized = false;\n    this.store = new Store();\n    this.feeds = FEEDS_CONFIG;\n    this._defaultPrefs = new DefaultPrefs(PREFS_CONFIG);\n  }\n\n  init() {\n    try {\n      this._updateDynamicPrefs();\n      this._defaultPrefs.init();\n\n      // Look for outdated user pref values that might have been accidentally\n      // persisted when restoring the original pref value at the end of an\n      // experiment across versions with a different default value.\n      const DS_CONFIG =\n        \"browser.newtabpage.activity-stream.discoverystream.config\";\n      if (\n        Services.prefs.prefHasUserValue(DS_CONFIG) &&\n        [\n          // Firefox 66\n          `{\"api_key_pref\":\"extensions.pocket.oAuthConsumerKey\",\"enabled\":false,\"show_spocs\":true,\"layout_endpoint\":\"https://getpocket.com/v3/newtab/layout?version=1&consumer_key=$apiKey&layout_variant=basic\"}`,\n          // Firefox 67\n          `{\"api_key_pref\":\"extensions.pocket.oAuthConsumerKey\",\"enabled\":false,\"show_spocs\":true,\"layout_endpoint\":\"https://getpocket.cdn.mozilla.net/v3/newtab/layout?version=1&consumer_key=$apiKey&layout_variant=basic\"}`,\n          // Firefox 68\n          `{\"api_key_pref\":\"extensions.pocket.oAuthConsumerKey\",\"collapsible\":true,\"enabled\":false,\"show_spocs\":true,\"hardcoded_layout\":true,\"personalized\":false,\"layout_endpoint\":\"https://getpocket.cdn.mozilla.net/v3/newtab/layout?version=1&consumer_key=$apiKey&layout_variant=basic\"}`,\n        ].includes(Services.prefs.getStringPref(DS_CONFIG))\n      ) {\n        Services.prefs.clearUserPref(DS_CONFIG);\n      }\n\n      // Hook up the store and let all feeds and pages initialize\n      this.store.init(\n        this.feeds,\n        ac.BroadcastToContent({\n          type: at.INIT,\n          data: {},\n        }),\n        { type: at.UNINIT }\n      );\n\n      this.initialized = true;\n    } catch (e) {\n      // TelemetryFeed could be unavailable if the telemetry is disabled, or\n      // the telemetry feed is not yet initialized.\n      const telemetryFeed = this.store.feeds.get(\"feeds.telemetry\");\n      if (telemetryFeed) {\n        telemetryFeed.handleUndesiredEvent({\n          data: { event: \"ADDON_INIT_FAILED\" },\n        });\n      }\n      throw e;\n    }\n  }\n\n  /**\n   * Check if an old pref has a custom value to migrate. Clears the pref so that\n   * it's the default after migrating (to avoid future need to migrate).\n   *\n   * @param oldPrefName {string} Pref to check and migrate\n   * @param cbIfNotDefault {function} Callback that gets the current pref value\n   */\n  _migratePref(oldPrefName, cbIfNotDefault) {\n    // Nothing to do if the user doesn't have a custom value\n    if (!Services.prefs.prefHasUserValue(oldPrefName)) {\n      return;\n    }\n\n    // Figure out what kind of pref getter to use\n    let prefGetter;\n    switch (Services.prefs.getPrefType(oldPrefName)) {\n      case Services.prefs.PREF_BOOL:\n        prefGetter = \"getBoolPref\";\n        break;\n      case Services.prefs.PREF_INT:\n        prefGetter = \"getIntPref\";\n        break;\n      case Services.prefs.PREF_STRING:\n        prefGetter = \"getStringPref\";\n        break;\n    }\n\n    // Give the callback the current value then clear the pref\n    cbIfNotDefault(Services.prefs[prefGetter](oldPrefName));\n    Services.prefs.clearUserPref(oldPrefName);\n  }\n\n  uninit() {\n    if (this.geo === \"\") {\n      Services.prefs.removeObserver(GEO_PREF, this);\n    }\n\n    this.store.uninit();\n    this.initialized = false;\n  }\n\n  _updateDynamicPrefs() {\n    // Save the geo pref if we have it\n    if (Services.prefs.prefHasUserValue(GEO_PREF)) {\n      this.geo = Services.prefs.getStringPref(GEO_PREF);\n    } else if (this.geo !== \"\") {\n      // Watch for geo changes and use a dummy value for now\n      Services.prefs.addObserver(GEO_PREF, this);\n      this.geo = \"\";\n    }\n\n    this.locale = Services.locale.appLocaleAsLangTag;\n\n    // Update the pref config of those with dynamic values\n    for (const pref of PREFS_CONFIG.keys()) {\n      // Only need to process dynamic prefs\n      const prefConfig = PREFS_CONFIG.get(pref);\n      if (!prefConfig.getValue) {\n        continue;\n      }\n\n      // Have the dynamic pref just reuse using existing default, e.g., those\n      // set via Autoconfig or policy\n      try {\n        const existingDefault = this._defaultPrefs.get(pref);\n        if (existingDefault !== undefined && prefConfig.value === undefined) {\n          prefConfig.getValue = () => existingDefault;\n        }\n      } catch (ex) {\n        // We get NS_ERROR_UNEXPECTED for prefs that have a user value (causing\n        // default branch to believe there's a type) but no actual default value\n      }\n\n      // Compute the dynamic value (potentially generic based on dummy geo)\n      const newValue = prefConfig.getValue({\n        geo: this.geo,\n        locale: this.locale,\n      });\n\n      // If there's an existing value and it has changed, that means we need to\n      // overwrite the default with the new value.\n      if (prefConfig.value !== undefined && prefConfig.value !== newValue) {\n        this._defaultPrefs.set(pref, newValue);\n      }\n\n      prefConfig.value = newValue;\n    }\n  }\n\n  observe(subject, topic, data) {\n    switch (topic) {\n      case \"nsPref:changed\":\n        // We should only expect one geo change, so update and stop observing\n        if (data === GEO_PREF) {\n          this._updateDynamicPrefs();\n          Services.prefs.removeObserver(GEO_PREF, this);\n        }\n        break;\n    }\n  }\n};\n\nconst EXPORTED_SYMBOLS = [\"ActivityStream\", \"PREFS_CONFIG\"];\n"
  },
  {
    "path": "lib/ActivityStreamMessageChannel.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\n\"use strict\";\n\nconst { AboutNewTab } = ChromeUtils.import(\n  \"resource:///modules/AboutNewTab.jsm\"\n);\nconst { RemotePages } = ChromeUtils.import(\n  \"resource://gre/modules/remotepagemanager/RemotePageManagerParent.jsm\"\n);\n\nconst {\n  actionCreators: ac,\n  actionTypes: at,\n  actionUtils: au,\n} = ChromeUtils.import(\"resource://activity-stream/common/Actions.jsm\");\n\nconst ABOUT_NEW_TAB_URL = \"about:newtab\";\nconst ABOUT_HOME_URL = \"about:home\";\n\nconst DEFAULT_OPTIONS = {\n  dispatch(action) {\n    throw new Error(\n      `\\nMessageChannel: Received action ${\n        action.type\n      }, but no dispatcher was defined.\\n`\n    );\n  },\n  pageURL: ABOUT_NEW_TAB_URL,\n  outgoingMessageName: \"ActivityStream:MainToContent\",\n  incomingMessageName: \"ActivityStream:ContentToMain\",\n};\n\nthis.ActivityStreamMessageChannel = class ActivityStreamMessageChannel {\n  /**\n   * ActivityStreamMessageChannel - This module connects a Redux store to a RemotePageManager in Firefox.\n   *                  Call .createChannel to start the connection, and .destroyChannel to destroy it.\n   *                  You should use the BroadcastToContent, AlsoToOneContent, and AlsoToMain action creators\n   *                  in common/Actions.jsm to help you create actions that will be automatically routed\n   *                  to the correct location.\n   *\n   * @param  {object} options\n   * @param  {function} options.dispatch The dispatch method from a Redux store\n   * @param  {string} options.pageURL The URL to which a RemotePageManager should be attached.\n   *                                  Note that if it is about:newtab, the existing RemotePageManager\n   *                                  for about:newtab will also be disabled\n   * @param  {string} options.outgoingMessageName The name of the message sent to child processes\n   * @param  {string} options.incomingMessageName The name of the message received from child processes\n   * @return {ActivityStreamMessageChannel}\n   */\n  constructor(options = {}) {\n    Object.assign(this, DEFAULT_OPTIONS, options);\n    this.channel = null;\n\n    this.middleware = this.middleware.bind(this);\n    this.onMessage = this.onMessage.bind(this);\n    this.onNewTabLoad = this.onNewTabLoad.bind(this);\n    this.onNewTabUnload = this.onNewTabUnload.bind(this);\n    this.onNewTabInit = this.onNewTabInit.bind(this);\n  }\n\n  /**\n   * middleware - Redux middleware that looks for AlsoToOneContent and BroadcastToContent type\n   *              actions, and sends them out.\n   *\n   * @param  {object} store A redux store\n   * @return {function} Redux middleware\n   */\n  middleware(store) {\n    return next => action => {\n      const skipMain = action.meta && action.meta.skipMain;\n      if (!this.channel && !skipMain) {\n        next(action);\n        return;\n      }\n      if (au.isSendToOneContent(action)) {\n        this.send(action);\n      } else if (au.isBroadcastToContent(action)) {\n        this.broadcast(action);\n      } else if (au.isSendToPreloaded(action)) {\n        this.sendToPreloaded(action);\n      }\n\n      if (!skipMain) {\n        next(action);\n      }\n    };\n  }\n\n  /**\n   * onActionFromContent - Handler for actions from a content processes\n   *\n   * @param  {object} action  A Redux action\n   * @param  {string} targetId The portID of the port that sent the message\n   */\n  onActionFromContent(action, targetId) {\n    this.dispatch(ac.AlsoToMain(action, this.validatePortID(targetId)));\n  }\n\n  /**\n   * broadcast - Sends an action to all ports\n   *\n   * @param  {object} action A Redux action\n   */\n  broadcast(action) {\n    this.channel.sendAsyncMessage(this.outgoingMessageName, action);\n  }\n\n  /**\n   * send - Sends an action to a specific port\n   *\n   * @param  {obj} action A redux action; it should contain a portID in the meta.toTarget property\n   */\n  send(action) {\n    const targetId = action.meta && action.meta.toTarget;\n    const target = this.getTargetById(targetId);\n    try {\n      target.sendAsyncMessage(this.outgoingMessageName, action);\n    } catch (e) {\n      // The target page is closed/closing by the user or test, so just ignore.\n    }\n  }\n\n  /**\n   * A valid portID is a combination of process id and port\n   * https://searchfox.org/mozilla-central/rev/196560b95f191b48ff7cba7c2ba9237bba6b5b6a/toolkit/components/remotepagemanager/RemotePageManagerChild.jsm#14\n   */\n  validatePortID(id) {\n    if (typeof id !== \"string\" || !id.includes(\":\")) {\n      Cu.reportError(\"Invalid portID\");\n    }\n\n    return id;\n  }\n\n  /**\n   * getIdByTarget - Retrieve the id of a message target, if it exists in this.targets\n   *\n   * @param  {obj} targetObj A message target\n   * @return {string|null} The unique id of the target, if it exists.\n   */\n  getTargetById(id) {\n    this.validatePortID(id);\n    for (let port of this.channel.messagePorts) {\n      if (port.portID === id) {\n        return port;\n      }\n    }\n    return null;\n  }\n\n  /**\n   * sendToPreloaded - Sends an action to each preloaded browser, if any\n   *\n   * @param  {obj} action A redux action\n   */\n  sendToPreloaded(action) {\n    const preloadedBrowsers = this.getPreloadedBrowser();\n    if (preloadedBrowsers && action.data) {\n      for (let preloadedBrowser of preloadedBrowsers) {\n        try {\n          preloadedBrowser.sendAsyncMessage(this.outgoingMessageName, action);\n        } catch (e) {\n          // The preloaded page is no longer available, so just ignore.\n        }\n      }\n    }\n  }\n\n  /**\n   * getPreloadedBrowser - Retrieve the port of any preloaded browsers\n   *\n   * @return {Array|null} An array of ports belonging to the preloaded browsers, or null\n   *                      if there aren't any preloaded browsers\n   */\n  getPreloadedBrowser() {\n    let preloadedPorts = [];\n    for (let port of this.channel.messagePorts) {\n      if (this.isPreloadedBrowser(port.browser)) {\n        preloadedPorts.push(port);\n      }\n    }\n    return preloadedPorts.length ? preloadedPorts : null;\n  }\n\n  /**\n   * isPreloadedBrowser - Returns true if the passed browser has been preloaded\n   *                      for faster rendering of new tabs.\n   *\n   * @param {<browser>} A <browser> to check.\n   * @return {bool} True if the browser is preloaded.\n   *                      if there aren't any preloaded browsers\n   */\n  isPreloadedBrowser(browser) {\n    return browser.getAttribute(\"preloadedState\") === \"preloaded\";\n  }\n\n  /**\n   * createChannel - Create RemotePages channel to establishing message passing\n   *                 between the main process and child pages\n   */\n  createChannel() {\n    //  Receive AboutNewTab's Remote Pages instance, if it exists, on override\n    const channel =\n      this.pageURL === ABOUT_NEW_TAB_URL && AboutNewTab.override(true);\n    this.channel =\n      channel || new RemotePages([ABOUT_HOME_URL, ABOUT_NEW_TAB_URL]);\n    this.channel.addMessageListener(\"RemotePage:Init\", this.onNewTabInit);\n    this.channel.addMessageListener(\"RemotePage:Load\", this.onNewTabLoad);\n    this.channel.addMessageListener(\"RemotePage:Unload\", this.onNewTabUnload);\n    this.channel.addMessageListener(this.incomingMessageName, this.onMessage);\n  }\n\n  simulateMessagesForExistingTabs() {\n    // Some pages might have already loaded, so we won't get the usual message\n    for (const target of this.channel.messagePorts) {\n      const simulatedMsg = {\n        target: Object.assign({ simulated: true }, target),\n      };\n      this.onNewTabInit(simulatedMsg);\n      if (target.loaded) {\n        this.onNewTabLoad(simulatedMsg);\n      }\n    }\n  }\n\n  /**\n   * destroyChannel - Destroys the RemotePages channel\n   */\n  destroyChannel() {\n    this.channel.removeMessageListener(\"RemotePage:Init\", this.onNewTabInit);\n    this.channel.removeMessageListener(\"RemotePage:Load\", this.onNewTabLoad);\n    this.channel.removeMessageListener(\n      \"RemotePage:Unload\",\n      this.onNewTabUnload\n    );\n    this.channel.removeMessageListener(\n      this.incomingMessageName,\n      this.onMessage\n    );\n    if (this.pageURL === ABOUT_NEW_TAB_URL) {\n      AboutNewTab.reset(this.channel);\n    } else {\n      this.channel.destroy();\n    }\n    this.channel = null;\n  }\n\n  /**\n   * onNewTabInit - Handler for special RemotePage:Init message fired\n   * by RemotePages\n   *\n   * @param  {obj} msg The messsage from a page that was just initialized\n   */\n  onNewTabInit(msg) {\n    this.onActionFromContent(\n      {\n        type: at.NEW_TAB_INIT,\n        data: msg.target,\n      },\n      msg.target.portID\n    );\n  }\n\n  /**\n   * onNewTabLoad - Handler for special RemotePage:Load message fired by RemotePages\n   *\n   * @param  {obj} msg The messsage from a page that was just loaded\n   */\n  onNewTabLoad(msg) {\n    let { browser } = msg.target;\n    if (this.isPreloadedBrowser(browser)) {\n      // As a perceived performance optimization, if this loaded Activity Stream\n      // happens to be a preloaded browser, have it render its layers to the\n      // compositor now to increase the odds that by the time we switch to\n      // the tab, the layers are already ready to present to the user.\n      browser.renderLayers = true;\n    }\n\n    this.onActionFromContent({ type: at.NEW_TAB_LOAD }, msg.target.portID);\n  }\n\n  /**\n   * onNewTabUnloadLoad - Handler for special RemotePage:Unload message fired by RemotePages\n   *\n   * @param  {obj} msg The messsage from a page that was just unloaded\n   */\n  onNewTabUnload(msg) {\n    this.onActionFromContent({ type: at.NEW_TAB_UNLOAD }, msg.target.portID);\n  }\n\n  /**\n   * onMessage - Handles custom messages from content. It expects all messages to\n   *             be formatted as Redux actions, and dispatches them to this.store\n   *\n   * @param  {obj} msg A custom message from content\n   * @param  {obj} msg.action A Redux action (e.g. {type: \"HELLO_WORLD\"})\n   * @param  {obj} msg.target A message target\n   */\n  onMessage(msg) {\n    const { portID } = msg.target;\n    if (!msg.data || !msg.data.type) {\n      Cu.reportError(\n        new Error(`Received an improperly formatted message from ${portID}`)\n      );\n      return;\n    }\n    let action = {};\n    Object.assign(action, msg.data);\n    // target is used to access a browser reference that came from the content\n    // and should only be used in feeds (not reducers)\n    action._target = msg.target;\n    this.onActionFromContent(action, portID);\n  }\n};\n\nthis.DEFAULT_OPTIONS = DEFAULT_OPTIONS;\nconst EXPORTED_SYMBOLS = [\"ActivityStreamMessageChannel\", \"DEFAULT_OPTIONS\"];\n"
  },
  {
    "path": "lib/ActivityStreamPrefs.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\"use strict\";\n\nconst { AppConstants } = ChromeUtils.import(\n  \"resource://gre/modules/AppConstants.jsm\"\n);\nconst { Preferences } = ChromeUtils.import(\n  \"resource://gre/modules/Preferences.jsm\"\n);\n\nconst ACTIVITY_STREAM_PREF_BRANCH = \"browser.newtabpage.activity-stream.\";\n\nthis.Prefs = class Prefs extends Preferences {\n  /**\n   * Prefs - A wrapper around Preferences that always sets the branch to\n   *         ACTIVITY_STREAM_PREF_BRANCH\n   */\n  constructor(branch = ACTIVITY_STREAM_PREF_BRANCH) {\n    super({ branch });\n    this._branchObservers = new Map();\n  }\n\n  ignoreBranch(listener) {\n    const observer = this._branchObservers.get(listener);\n    this._prefBranch.removeObserver(\"\", observer);\n    this._branchObservers.delete(listener);\n  }\n\n  observeBranch(listener) {\n    const observer = (subject, topic, pref) => {\n      listener.onPrefChanged(pref, this.get(pref));\n    };\n    this._prefBranch.addObserver(\"\", observer);\n    this._branchObservers.set(listener, observer);\n  }\n};\n\nthis.DefaultPrefs = class DefaultPrefs extends Preferences {\n  /**\n   * DefaultPrefs - A helper for setting and resetting default prefs for the add-on\n   *\n   * @param  {Map} config A Map with {string} key of the pref name and {object}\n   *                      value with the following pref properties:\n   *         {string} .title (optional) A description of the pref\n   *         {bool|string|number} .value The default value for the pref\n   * @param  {string} branch (optional) The pref branch (defaults to ACTIVITY_STREAM_PREF_BRANCH)\n   */\n  constructor(config, branch = ACTIVITY_STREAM_PREF_BRANCH) {\n    super({\n      branch,\n      defaultBranch: true,\n    });\n    this._config = config;\n  }\n\n  /**\n   * init - Set default prefs for all prefs in the config\n   */\n  init() {\n    // Local developer builds (with the default mozconfig) aren't OFFICIAL\n    const IS_UNOFFICIAL_BUILD = !AppConstants.MOZILLA_OFFICIAL;\n\n    for (const pref of this._config.keys()) {\n      try {\n        // Avoid replacing existing valid default pref values, e.g., those set\n        // via Autoconfig or policy\n        if (this.get(pref) !== undefined) {\n          continue;\n        }\n      } catch (ex) {\n        // We get NS_ERROR_UNEXPECTED for prefs that have a user value (causing\n        // default branch to believe there's a type) but no actual default value\n      }\n\n      const prefConfig = this._config.get(pref);\n      let value;\n      if (IS_UNOFFICIAL_BUILD && \"value_local_dev\" in prefConfig) {\n        value = prefConfig.value_local_dev;\n      } else {\n        value = prefConfig.value;\n      }\n\n      try {\n        this.set(pref, value);\n      } catch (ex) {\n        // Potentially the user somehow set an unexpected value type, so we fail\n        // to set a default of our expected type\n      }\n    }\n  }\n};\n\nconst EXPORTED_SYMBOLS = [\"DefaultPrefs\", \"Prefs\"];\n"
  },
  {
    "path": "lib/ActivityStreamStorage.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nChromeUtils.defineModuleGetter(\n  this,\n  \"IndexedDB\",\n  \"resource://gre/modules/IndexedDB.jsm\"\n);\n\nthis.ActivityStreamStorage = class ActivityStreamStorage {\n  /**\n   * @param storeNames Array of strings used to create all the required stores\n   */\n  constructor({ storeNames, telemetry }) {\n    if (!storeNames) {\n      throw new Error(\"storeNames required\");\n    }\n\n    this.dbName = \"ActivityStream\";\n    this.dbVersion = 3;\n    this.storeNames = storeNames;\n    this.telemetry = telemetry;\n  }\n\n  get db() {\n    return this._db || (this._db = this.createOrOpenDb());\n  }\n\n  /**\n   * Public method that binds the store required by the consumer and exposes\n   * the private db getters and setters.\n   *\n   * @param storeName String name of desired store\n   */\n  getDbTable(storeName) {\n    if (this.storeNames.includes(storeName)) {\n      return {\n        get: this._get.bind(this, storeName),\n        getAll: this._getAll.bind(this, storeName),\n        set: this._set.bind(this, storeName),\n      };\n    }\n\n    throw new Error(`Store name ${storeName} does not exist.`);\n  }\n\n  async _getStore(storeName) {\n    return (await this.db).objectStore(storeName, \"readwrite\");\n  }\n\n  _get(storeName, key) {\n    return this._requestWrapper(async () =>\n      (await this._getStore(storeName)).get(key)\n    );\n  }\n\n  _getAll(storeName) {\n    return this._requestWrapper(async () =>\n      (await this._getStore(storeName)).getAll()\n    );\n  }\n\n  _set(storeName, key, value) {\n    return this._requestWrapper(async () =>\n      (await this._getStore(storeName)).put(value, key)\n    );\n  }\n\n  _openDatabase() {\n    return IndexedDB.open(this.dbName, { version: this.dbVersion }, db => {\n      // If provided with array of objectStore names we need to create all the\n      // individual stores\n      this.storeNames.forEach(store => {\n        if (!db.objectStoreNames.contains(store)) {\n          this._requestWrapper(() => db.createObjectStore(store));\n        }\n      });\n    });\n  }\n\n  /**\n   * createOrOpenDb - Open a db (with this.dbName) if it exists.\n   *                  If it does not exist, create it.\n   *                  If an error occurs, deleted the db and attempt to\n   *                  re-create it.\n   * @returns Promise that resolves with a db instance\n   */\n  async createOrOpenDb() {\n    try {\n      const db = await this._openDatabase();\n      return db;\n    } catch (e) {\n      if (this.telemetry) {\n        this.telemetry.handleUndesiredEvent({\n          data: { event: \"INDEXEDDB_OPEN_FAILED\" },\n        });\n      }\n      await IndexedDB.deleteDatabase(this.dbName);\n      return this._openDatabase();\n    }\n  }\n\n  async _requestWrapper(request) {\n    let result = null;\n    try {\n      result = await request();\n    } catch (e) {\n      if (this.telemetry) {\n        this.telemetry.handleUndesiredEvent({\n          data: { event: \"TRANSACTION_FAILED\" },\n        });\n      }\n      throw e;\n    }\n\n    return result;\n  }\n};\n\nfunction getDefaultOptions(options) {\n  return { collapsed: !!options.collapsed };\n}\n\nconst EXPORTED_SYMBOLS = [\"ActivityStreamStorage\", \"getDefaultOptions\"];\n"
  },
  {
    "path": "lib/BookmarkPanelHub.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\"use strict\";\n\nChromeUtils.defineModuleGetter(\n  this,\n  \"FxAccounts\",\n  \"resource://gre/modules/FxAccounts.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"Services\",\n  \"resource://gre/modules/Services.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"PrivateBrowsingUtils\",\n  \"resource://gre/modules/PrivateBrowsingUtils.jsm\"\n);\n\nclass _BookmarkPanelHub {\n  constructor() {\n    this._id = \"BookmarkPanelHub\";\n    this._trigger = { id: \"bookmark-panel\" };\n    this._handleMessageRequest = null;\n    this._addImpression = null;\n    this._dispatch = null;\n    this._initialized = false;\n    this._response = null;\n    this._l10n = null;\n\n    this.messageRequest = this.messageRequest.bind(this);\n    this.toggleRecommendation = this.toggleRecommendation.bind(this);\n    this.sendUserEventTelemetry = this.sendUserEventTelemetry.bind(this);\n    this.collapseMessage = this.collapseMessage.bind(this);\n  }\n\n  /**\n   * @param {function} handleMessageRequest\n   * @param {function} addImpression\n   * @param {function} dispatch - Used for sending user telemetry information\n   */\n  init(handleMessageRequest, addImpression, dispatch) {\n    this._handleMessageRequest = handleMessageRequest;\n    this._addImpression = addImpression;\n    this._dispatch = dispatch;\n    this._l10n = new DOMLocalization();\n    this._initialized = true;\n  }\n\n  uninit() {\n    this._l10n = null;\n    this._initialized = false;\n    this._handleMessageRequest = null;\n    this._addImpression = null;\n    this._dispatch = null;\n    this._response = null;\n  }\n\n  /**\n   * Checks if a similar cached requests exists before forwarding the request\n   * to ASRouter. Caches only 1 request, unique identifier is `request.url`.\n   * Caching ensures we don't duplicate requests and telemetry pings.\n   * Return value is important for the caller to know if a message will be\n   * shown.\n   *\n   * @returns {obj|null} response object or null if no messages matched\n   */\n  async messageRequest(target, win) {\n    if (!this._initialized) {\n      return false;\n    }\n\n    if (\n      this._response &&\n      this._response.win === win &&\n      this._response.url === target.url &&\n      this._response.content\n    ) {\n      this.showMessage(this._response.content, target, win);\n      return true;\n    }\n\n    // If we didn't match on a previously cached request then make sure\n    // the container is empty\n    this._removeContainer(target);\n    const response = await this._handleMessageRequest({\n      triggerId: this._trigger.id,\n    });\n\n    return this.onResponse(response, target, win);\n  }\n\n  /**\n   * If the response contains a message render it and send an impression.\n   * Otherwise we remove the message from the container.\n   */\n  onResponse(response, target, win) {\n    this._response = {\n      ...response,\n      collapsed: false,\n      target,\n      win,\n      url: target.url,\n    };\n\n    if (response && response.content) {\n      // Only insert localization files if we need to show a message\n      win.MozXULElement.insertFTLIfNeeded(\"browser/newtab/asrouter.ftl\");\n      win.MozXULElement.insertFTLIfNeeded(\"browser/branding/sync-brand.ftl\");\n      this.showMessage(response.content, target, win);\n      this.sendImpression();\n      this.sendUserEventTelemetry(\"IMPRESSION\", win);\n    } else {\n      this.hideMessage(target);\n    }\n\n    target.infoButton.disabled = !response;\n\n    return !!response;\n  }\n\n  showMessage(message, target, win) {\n    if (this._response && this._response.collapsed) {\n      this.toggleRecommendation(false);\n      return;\n    }\n\n    const createElement = elem =>\n      target.document.createElementNS(\"http://www.w3.org/1999/xhtml\", elem);\n    let recommendation = target.container.querySelector(\"#cfrMessageContainer\");\n    if (!recommendation) {\n      recommendation = createElement(\"div\");\n      const headerContainer = createElement(\"div\");\n      headerContainer.classList.add(\"cfrMessageHeader\");\n      recommendation.setAttribute(\"id\", \"cfrMessageContainer\");\n      recommendation.addEventListener(\"click\", async e => {\n        target.hidePopup();\n        const url = await FxAccounts.config.promiseConnectAccountURI(\n          \"bookmark\"\n        );\n        win.ownerGlobal.openLinkIn(url, \"tabshifted\", {\n          private: false,\n          triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal(\n            {}\n          ),\n          csp: null,\n        });\n        this.sendUserEventTelemetry(\"CLICK\", win);\n      });\n      recommendation.style.color = message.color;\n      recommendation.style.background = `-moz-linear-gradient(-45deg, ${\n        message.background_color_1\n      } 0%, ${message.background_color_2} 70%)`;\n      const close = createElement(\"button\");\n      close.setAttribute(\"id\", \"cfrClose\");\n      close.setAttribute(\"aria-label\", \"close\");\n      close.style.color = message.color;\n      close.addEventListener(\"click\", e => {\n        this.sendUserEventTelemetry(\"DISMISS\", win);\n        this.collapseMessage();\n        target.close(e);\n      });\n      const title = createElement(\"h1\");\n      title.setAttribute(\"id\", \"editBookmarkPanelRecommendationTitle\");\n      const content = createElement(\"p\");\n      content.setAttribute(\"id\", \"editBookmarkPanelRecommendationContent\");\n      const cta = createElement(\"button\");\n      cta.setAttribute(\"id\", \"editBookmarkPanelRecommendationCta\");\n\n      // If `string_id` is present it means we are relying on fluent for translations\n      if (message.text.string_id) {\n        this._l10n.setAttributes(\n          close,\n          message.close_button.tooltiptext.string_id\n        );\n        this._l10n.setAttributes(title, message.title.string_id);\n        this._l10n.setAttributes(content, message.text.string_id);\n        this._l10n.setAttributes(cta, message.cta.string_id);\n      } else {\n        close.setAttribute(\"title\", message.close_button.tooltiptext);\n        title.textContent = message.title;\n        content.textContent = message.text;\n        cta.textContent = message.cta;\n      }\n\n      headerContainer.appendChild(title);\n      headerContainer.appendChild(close);\n      recommendation.appendChild(headerContainer);\n      recommendation.appendChild(content);\n      recommendation.appendChild(cta);\n      target.container.appendChild(recommendation);\n    }\n\n    this.toggleRecommendation(true);\n    this._adjustPanelHeight(win, recommendation);\n  }\n\n  /**\n   * Adjust the size of the container for locales where the message is\n   * longer than the fixed 150px set for height\n   */\n  async _adjustPanelHeight(window, messageContainer) {\n    const { document } = window;\n    // Contains the screenshot of the page we are bookmarking\n    const screenshotContainer = document.getElementById(\n      \"editBookmarkPanelImage\"\n    );\n    // Wait for strings to be added which can change element height\n    await document.l10n.translateElements([messageContainer]);\n    window.requestAnimationFrame(() => {\n      let { height } = messageContainer.getBoundingClientRect();\n      if (height > 150) {\n        messageContainer.classList.add(\"longMessagePadding\");\n        // Get the new value with the added padding\n        height = messageContainer.getBoundingClientRect().height;\n        // Needs to be adjusted to match the message height\n        screenshotContainer.style.height = `${height}px`;\n      }\n    });\n  }\n\n  /**\n   * Restore the panel back to the original size so the slide in\n   * animation can run again\n   */\n  _restorePanelHeight(window) {\n    const { document } = window;\n    // Contains the screenshot of the page we are bookmarking\n    document.getElementById(\"editBookmarkPanelImage\").style.height = \"\";\n  }\n\n  toggleRecommendation(visible) {\n    if (!this._response) {\n      return;\n    }\n\n    const { target } = this._response;\n    if (visible === undefined) {\n      // When called from the info button of the bookmark panel\n      target.infoButton.checked = !target.infoButton.checked;\n    } else {\n      target.infoButton.checked = visible;\n    }\n    if (target.infoButton.checked) {\n      // If it was ever collapsed we need to cancel the state\n      this._response.collapsed = false;\n      target.container.removeAttribute(\"disabled\");\n    } else {\n      target.container.setAttribute(\"disabled\", \"disabled\");\n    }\n  }\n\n  collapseMessage() {\n    this._response.collapsed = true;\n    this.toggleRecommendation(false);\n  }\n\n  _removeContainer(target) {\n    if (target || (this._response && this._response.target)) {\n      const container = (\n        target || this._response.target\n      ).container.querySelector(\"#cfrMessageContainer\");\n      if (container) {\n        this._restorePanelHeight(this._response.win);\n        container.remove();\n      }\n    }\n  }\n\n  hideMessage(target) {\n    this._removeContainer(target);\n    this.toggleRecommendation(false);\n    this._response = null;\n  }\n\n  _forceShowMessage(target, message) {\n    const doc = target.browser.ownerGlobal.gBrowser.ownerDocument;\n    const win = target.browser.ownerGlobal.window;\n    const panelTarget = {\n      container: doc.getElementById(\"editBookmarkPanelRecommendation\"),\n      infoButton: doc.getElementById(\"editBookmarkPanelInfoButton\"),\n      document: doc,\n      close: e => {\n        e.stopPropagation();\n        this.toggleRecommendation(false);\n      },\n    };\n    // Remove any existing message\n    this.hideMessage(panelTarget);\n    // Reset the reference to the panel elements\n    this._response = { target: panelTarget, win };\n    // Required if we want to preview messages that include fluent strings\n    win.MozXULElement.insertFTLIfNeeded(\"browser/newtab/asrouter.ftl\");\n    win.MozXULElement.insertFTLIfNeeded(\"browser/branding/sync-brand.ftl\");\n    this.showMessage(message.content, panelTarget, win);\n  }\n\n  sendImpression() {\n    this._addImpression(this._response);\n  }\n\n  sendUserEventTelemetry(event, win) {\n    // Only send pings for non private browsing windows\n    if (\n      !PrivateBrowsingUtils.isBrowserPrivate(\n        win.ownerGlobal.gBrowser.selectedBrowser\n      )\n    ) {\n      this._sendTelemetry({\n        message_id: this._response.id,\n        bucket_id: this._response.id,\n        event,\n      });\n    }\n  }\n\n  _sendTelemetry(ping) {\n    this._dispatch({\n      type: \"DOORHANGER_TELEMETRY\",\n      data: { action: \"cfr_user_event\", source: \"CFR\", ...ping },\n    });\n  }\n}\n\nthis._BookmarkPanelHub = _BookmarkPanelHub;\n\n/**\n * BookmarkPanelHub - singleton instance of _BookmarkPanelHub that can initiate\n * message requests and render messages.\n */\nthis.BookmarkPanelHub = new _BookmarkPanelHub();\n\nconst EXPORTED_SYMBOLS = [\"BookmarkPanelHub\", \"_BookmarkPanelHub\"];\n"
  },
  {
    "path": "lib/CFRMessageProvider.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\"use strict\";\nconst FACEBOOK_CONTAINER_PARAMS = {\n  existing_addons: [\n    \"@contain-facebook\",\n    \"{bb1b80be-e6b3-40a1-9b6e-9d4073343f0b}\",\n    \"{a50d61ca-d27b-437a-8b52-5fd801a0a88b}\",\n  ],\n  open_urls: [\"www.facebook.com\", \"facebook.com\"],\n  sumo_path: \"extensionrecommendations\",\n  min_frecency: 10000,\n};\nconst GOOGLE_TRANSLATE_PARAMS = {\n  existing_addons: [\n    \"jid1-93WyvpgvxzGATw@jetpack\",\n    \"{087ef4e1-4286-4be6-9aa3-8d6c420ee1db}\",\n    \"{4170faaa-ee87-4a0e-b57a-1aec49282887}\",\n    \"jid1-TMndP6cdKgxLcQ@jetpack\",\n    \"s3google@translator\",\n    \"{9c63d15c-b4d9-43bd-b223-37f0a1f22e2a}\",\n    \"translator@zoli.bod\",\n    \"{8cda9ce6-7893-4f47-ac70-a65215cec288}\",\n    \"simple-translate@sienori\",\n    \"@translatenow\",\n    \"{a79fafce-8da6-4685-923f-7ba1015b8748})\",\n    \"{8a802b5a-eeab-11e2-a41d-b0096288709b}\",\n    \"jid0-fbHwsGfb6kJyq2hj65KnbGte3yT@jetpack\",\n    \"storetranslate.plugin@gmail.com\",\n    \"jid1-r2tWDbSkq8AZK1@jetpack\",\n    \"{b384b75c-c978-4c4d-b3cf-62a82d8f8f12}\",\n    \"jid1-f7dnBeTj8ElpWQ@jetpack\",\n    \"{dac8a935-4775-4918-9205-5c0600087dc4}\",\n    \"gtranslation2@slam.com\",\n    \"{e20e0de5-1667-4df4-bd69-705720e37391}\",\n    \"{09e26ae9-e9c1-477c-80a6-99934212f2fe}\",\n    \"mgxtranslator@magemagix.com\",\n    \"gtranslatewins@mozilla.org\",\n  ],\n  open_urls: [\"translate.google.com\"],\n  sumo_path: \"extensionrecommendations\",\n  min_frecency: 10000,\n};\nconst YOUTUBE_ENHANCE_PARAMS = {\n  existing_addons: [\n    \"enhancerforyoutube@maximerf.addons.mozilla.org\",\n    \"{dc8f61ab-5e98-4027-98ef-bb2ff6060d71}\",\n    \"{7b1bf0b6-a1b9-42b0-b75d-252036438bdc}\",\n    \"jid0-UVAeBCfd34Kk5usS8A1CBiobvM8@jetpack\",\n    \"iridium@particlecore.github.io\",\n    \"jid1-ss6kLNCbNz6u0g@jetpack\",\n    \"{1cf918d2-f4ea-4b4f-b34e-455283fef19f}\",\n  ],\n  open_urls: [\"www.youtube.com\", \"youtube.com\"],\n  sumo_path: \"extensionrecommendations\",\n  min_frecency: 10000,\n};\nconst WIKIPEDIA_CONTEXT_MENU_SEARCH_PARAMS = {\n  existing_addons: [\n    \"@wikipediacontextmenusearch\",\n    \"{ebf47fc8-01d8-4dba-aa04-2118402f4b20}\",\n    \"{5737a280-b359-4e26-95b0-adec5915a854}\",\n    \"olivier.debroqueville@gmail.com\",\n    \"{3923146e-98cb-472b-9c13-f6849d34d6b8}\",\n  ],\n  open_urls: [\"www.wikipedia.org\", \"wikipedia.org\"],\n  sumo_path: \"extensionrecommendations\",\n  min_frecency: 10000,\n};\nconst REDDIT_ENHANCEMENT_PARAMS = {\n  existing_addons: [\"jid1-xUfzOsOFlzSOXg@jetpack\"],\n  open_urls: [\"www.reddit.com\", \"reddit.com\"],\n  sumo_path: \"extensionrecommendations\",\n  min_frecency: 10000,\n};\nconst PINNED_TABS_TARGET_SITES = [\n  \"docs.google.com\",\n  \"www.docs.google.com\",\n  \"calendar.google.com\",\n  \"messenger.com\",\n  \"www.messenger.com\",\n  \"web.whatsapp.com\",\n  \"mail.google.com\",\n  \"outlook.live.com\",\n  \"facebook.com\",\n  \"www.facebook.com\",\n  \"twitter.com\",\n  \"www.twitter.com\",\n  \"reddit.com\",\n  \"www.reddit.com\",\n  \"github.com\",\n  \"www.github.com\",\n  \"youtube.com\",\n  \"www.youtube.com\",\n  \"feedly.com\",\n  \"www.feedly.com\",\n  \"drive.google.com\",\n  \"amazon.com\",\n  \"www.amazon.com\",\n  \"messages.android.com\",\n  \"amazon.ca\",\n  \"www.amazon.ca\",\n  \"amazon.com.au\",\n  \"www.amazon.com.au\",\n  \"amazon.co.uk\",\n  \"www.amazon.co.uk\",\n  \"amazon.fr\",\n  \"www.amazon.fr\",\n  \"amazon.de\",\n  \"www.amazon.de\",\n];\nconst PINNED_TABS_TARGET_LOCALES = [\n  \"en-US\",\n  \"en-CA\",\n  \"en-AU\",\n  \"en-GB\",\n  \"en-ZA\",\n  \"en-NZ\",\n  \"fr\",\n  \"de\",\n];\n\nconst CFR_MESSAGES = [\n  {\n    id: \"FACEBOOK_CONTAINER_3\",\n    template: \"cfr_doorhanger\",\n    content: {\n      layout: \"addon_recommendation\",\n      category: \"cfrAddons\",\n      bucket_id: \"CFR_M1\",\n      notification_text: {\n        string_id: \"cfr-doorhanger-extension-notification2\",\n      },\n      heading_text: { string_id: \"cfr-doorhanger-extension-heading\" },\n      info_icon: {\n        label: { string_id: \"cfr-doorhanger-extension-sumo-link\" },\n        sumo_path: FACEBOOK_CONTAINER_PARAMS.sumo_path,\n      },\n      addon: {\n        id: \"954390\",\n        title: \"Facebook Container\",\n        icon:\n          \"resource://activity-stream/data/content/assets/cfr_fb_container.png\",\n        rating: 4.6,\n        users: 299019,\n        author: \"Mozilla\",\n        amo_url: \"https://addons.mozilla.org/firefox/addon/facebook-container/\",\n      },\n      text:\n        \"Stop Facebook from tracking your activity across the web. Use Facebook the way you normally do without annoying ads following you around.\",\n      buttons: {\n        primary: {\n          label: { string_id: \"cfr-doorhanger-extension-ok-button\" },\n          action: {\n            type: \"INSTALL_ADDON_FROM_URL\",\n            data: { url: null },\n          },\n        },\n        secondary: [\n          {\n            label: { string_id: \"cfr-doorhanger-extension-cancel-button\" },\n            action: { type: \"CANCEL\" },\n          },\n          {\n            label: {\n              string_id: \"cfr-doorhanger-extension-never-show-recommendation\",\n            },\n          },\n          {\n            label: {\n              string_id: \"cfr-doorhanger-extension-manage-settings-button\",\n            },\n            action: {\n              type: \"OPEN_PREFERENCES_PAGE\",\n              data: { category: \"general-cfraddons\" },\n            },\n          },\n        ],\n      },\n    },\n    frequency: { lifetime: 3 },\n    targeting: `\n      localeLanguageCode == \"en\" &&\n      (xpinstallEnabled == true) &&\n      (${JSON.stringify(\n        FACEBOOK_CONTAINER_PARAMS.existing_addons\n      )} intersect addonsInfo.addons|keys)|length == 0 &&\n      (${JSON.stringify(\n        FACEBOOK_CONTAINER_PARAMS.open_urls\n      )} intersect topFrecentSites[.frecency >= ${\n      FACEBOOK_CONTAINER_PARAMS.min_frecency\n    }]|mapToProperty('host'))|length > 0`,\n    trigger: { id: \"openURL\", params: FACEBOOK_CONTAINER_PARAMS.open_urls },\n  },\n  {\n    id: \"GOOGLE_TRANSLATE_3\",\n    template: \"cfr_doorhanger\",\n    content: {\n      layout: \"addon_recommendation\",\n      category: \"cfrAddons\",\n      bucket_id: \"CFR_M1\",\n      notification_text: {\n        string_id: \"cfr-doorhanger-extension-notification2\",\n      },\n      heading_text: { string_id: \"cfr-doorhanger-extension-heading\" },\n      info_icon: {\n        label: { string_id: \"cfr-doorhanger-extension-sumo-link\" },\n        sumo_path: GOOGLE_TRANSLATE_PARAMS.sumo_path,\n      },\n      addon: {\n        id: \"445852\",\n        title: \"To Google Translate\",\n        icon:\n          \"resource://activity-stream/data/content/assets/cfr_google_translate.png\",\n        rating: 4.1,\n        users: 313474,\n        author: \"Juan Escobar\",\n        amo_url:\n          \"https://addons.mozilla.org/firefox/addon/to-google-translate/\",\n      },\n      text:\n        \"Instantly translate any webpage text. Simply highlight the text, right-click to open the context menu, and choose a text or aural translation.\",\n      buttons: {\n        primary: {\n          label: { string_id: \"cfr-doorhanger-extension-ok-button\" },\n          action: {\n            type: \"INSTALL_ADDON_FROM_URL\",\n            data: { url: null },\n          },\n        },\n        secondary: [\n          {\n            label: { string_id: \"cfr-doorhanger-extension-cancel-button\" },\n            action: { type: \"CANCEL\" },\n          },\n          {\n            label: {\n              string_id: \"cfr-doorhanger-extension-never-show-recommendation\",\n            },\n          },\n          {\n            label: {\n              string_id: \"cfr-doorhanger-extension-manage-settings-button\",\n            },\n            action: {\n              type: \"OPEN_PREFERENCES_PAGE\",\n              data: { category: \"general-cfraddons\" },\n            },\n          },\n        ],\n      },\n    },\n    frequency: { lifetime: 3 },\n    targeting: `\n      localeLanguageCode == \"en\" &&\n      (xpinstallEnabled == true) &&\n      (${JSON.stringify(\n        GOOGLE_TRANSLATE_PARAMS.existing_addons\n      )} intersect addonsInfo.addons|keys)|length == 0 &&\n      (${JSON.stringify(\n        GOOGLE_TRANSLATE_PARAMS.open_urls\n      )} intersect topFrecentSites[.frecency >= ${\n      GOOGLE_TRANSLATE_PARAMS.min_frecency\n    }]|mapToProperty('host'))|length > 0`,\n    trigger: { id: \"openURL\", params: GOOGLE_TRANSLATE_PARAMS.open_urls },\n  },\n  {\n    id: \"YOUTUBE_ENHANCE_3\",\n    template: \"cfr_doorhanger\",\n    content: {\n      layout: \"addon_recommendation\",\n      category: \"cfrAddons\",\n      bucket_id: \"CFR_M1\",\n      notification_text: {\n        string_id: \"cfr-doorhanger-extension-notification2\",\n      },\n      heading_text: { string_id: \"cfr-doorhanger-extension-heading\" },\n      info_icon: {\n        label: { string_id: \"cfr-doorhanger-extension-sumo-link\" },\n        sumo_path: YOUTUBE_ENHANCE_PARAMS.sumo_path,\n      },\n      addon: {\n        id: \"700308\",\n        title: \"Enhancer for YouTube\\u2122\",\n        icon:\n          \"resource://activity-stream/data/content/assets/cfr_enhancer_youtube.png\",\n        rating: 4.8,\n        users: 357328,\n        author: \"Maxime RF\",\n        amo_url:\n          \"https://addons.mozilla.org/firefox/addon/enhancer-for-youtube/\",\n      },\n      text:\n        \"Take control of your YouTube experience. Automatically block annoying ads, set playback speed and volume, remove annotations, and more.\",\n      buttons: {\n        primary: {\n          label: { string_id: \"cfr-doorhanger-extension-ok-button\" },\n          action: {\n            type: \"INSTALL_ADDON_FROM_URL\",\n            data: { url: null },\n          },\n        },\n        secondary: [\n          {\n            label: { string_id: \"cfr-doorhanger-extension-cancel-button\" },\n            action: { type: \"CANCEL\" },\n          },\n          {\n            label: {\n              string_id: \"cfr-doorhanger-extension-never-show-recommendation\",\n            },\n          },\n          {\n            label: {\n              string_id: \"cfr-doorhanger-extension-manage-settings-button\",\n            },\n            action: {\n              type: \"OPEN_PREFERENCES_PAGE\",\n              data: { category: \"general-cfraddons\" },\n            },\n          },\n        ],\n      },\n    },\n    frequency: { lifetime: 3 },\n    targeting: `\n      localeLanguageCode == \"en\" &&\n      (xpinstallEnabled == true) &&\n      (${JSON.stringify(\n        YOUTUBE_ENHANCE_PARAMS.existing_addons\n      )} intersect addonsInfo.addons|keys)|length == 0 &&\n      (${JSON.stringify(\n        YOUTUBE_ENHANCE_PARAMS.open_urls\n      )} intersect topFrecentSites[.frecency >= ${\n      YOUTUBE_ENHANCE_PARAMS.min_frecency\n    }]|mapToProperty('host'))|length > 0`,\n    trigger: { id: \"openURL\", params: YOUTUBE_ENHANCE_PARAMS.open_urls },\n  },\n  {\n    id: \"WIKIPEDIA_CONTEXT_MENU_SEARCH_3\",\n    template: \"cfr_doorhanger\",\n    exclude: true,\n    content: {\n      layout: \"addon_recommendation\",\n      category: \"cfrAddons\",\n      bucket_id: \"CFR_M1\",\n      notification_text: {\n        string_id: \"cfr-doorhanger-extension-notification2\",\n      },\n      heading_text: { string_id: \"cfr-doorhanger-extension-heading\" },\n      info_icon: {\n        label: { string_id: \"cfr-doorhanger-extension-sumo-link\" },\n        sumo_path: WIKIPEDIA_CONTEXT_MENU_SEARCH_PARAMS.sumo_path,\n      },\n      addon: {\n        id: \"659026\",\n        title: \"Wikipedia Context Menu Search\",\n        icon:\n          \"resource://activity-stream/data/content/assets/cfr_wiki_search.png\",\n        rating: 4.9,\n        users: 3095,\n        author: \"Nick Diedrich\",\n        amo_url:\n          \"https://addons.mozilla.org/firefox/addon/wikipedia-context-menu-search/\",\n      },\n      text:\n        \"Get to a Wikipedia page fast, from anywhere on the web. Just highlight any webpage text and right-click to open the context menu to start a Wikipedia search.\",\n      buttons: {\n        primary: {\n          label: { string_id: \"cfr-doorhanger-extension-ok-button\" },\n          action: {\n            type: \"INSTALL_ADDON_FROM_URL\",\n            data: { url: null },\n          },\n        },\n        secondary: [\n          {\n            label: { string_id: \"cfr-doorhanger-extension-cancel-button\" },\n            action: { type: \"CANCEL\" },\n          },\n          {\n            label: {\n              string_id: \"cfr-doorhanger-extension-never-show-recommendation\",\n            },\n          },\n          {\n            label: {\n              string_id: \"cfr-doorhanger-extension-manage-settings-button\",\n            },\n            action: {\n              type: \"OPEN_PREFERENCES_PAGE\",\n              data: { category: \"general-cfraddons\" },\n            },\n          },\n        ],\n      },\n    },\n    frequency: { lifetime: 3 },\n    targeting: `\n      localeLanguageCode == \"en\" &&\n      (xpinstallEnabled == true) &&\n      (${JSON.stringify(\n        WIKIPEDIA_CONTEXT_MENU_SEARCH_PARAMS.existing_addons\n      )} intersect addonsInfo.addons|keys)|length == 0 &&\n      (${JSON.stringify(\n        WIKIPEDIA_CONTEXT_MENU_SEARCH_PARAMS.open_urls\n      )} intersect topFrecentSites[.frecency >= ${\n      WIKIPEDIA_CONTEXT_MENU_SEARCH_PARAMS.min_frecency\n    }]|mapToProperty('host'))|length > 0`,\n    trigger: {\n      id: \"openURL\",\n      params: WIKIPEDIA_CONTEXT_MENU_SEARCH_PARAMS.open_urls,\n    },\n  },\n  {\n    id: \"REDDIT_ENHANCEMENT_3\",\n    template: \"cfr_doorhanger\",\n    exclude: true,\n    content: {\n      layout: \"addon_recommendation\",\n      category: \"cfrAddons\",\n      bucket_id: \"CFR_M1\",\n      notification_text: {\n        string_id: \"cfr-doorhanger-extension-notification2\",\n      },\n      heading_text: { string_id: \"cfr-doorhanger-extension-heading\" },\n      info_icon: {\n        label: { string_id: \"cfr-doorhanger-extension-sumo-link\" },\n        sumo_path: REDDIT_ENHANCEMENT_PARAMS.sumo_path,\n      },\n      addon: {\n        id: \"387429\",\n        title: \"Reddit Enhancement Suite\",\n        icon:\n          \"resource://activity-stream/data/content/assets/cfr_reddit_enhancement.png\",\n        rating: 4.6,\n        users: 258129,\n        author: \"honestbleeps\",\n        amo_url:\n          \"https://addons.mozilla.org/firefox/addon/reddit-enhancement-suite/\",\n      },\n      text:\n        \"New features include Inline Image Viewer, Never Ending Reddit (never click 'next page' again), Keyboard Navigation, Account Switcher, and User Tagger.\",\n      buttons: {\n        primary: {\n          label: { string_id: \"cfr-doorhanger-extension-ok-button\" },\n          action: {\n            type: \"INSTALL_ADDON_FROM_URL\",\n            data: { url: null },\n          },\n        },\n        secondary: [\n          {\n            label: { string_id: \"cfr-doorhanger-extension-cancel-button\" },\n            action: { type: \"CANCEL\" },\n          },\n          {\n            label: {\n              string_id: \"cfr-doorhanger-extension-never-show-recommendation\",\n            },\n          },\n          {\n            label: {\n              string_id: \"cfr-doorhanger-extension-manage-settings-button\",\n            },\n            action: {\n              type: \"OPEN_PREFERENCES_PAGE\",\n              data: { category: \"general-cfraddons\" },\n            },\n          },\n        ],\n      },\n    },\n    frequency: { lifetime: 3 },\n    targeting: `\n      localeLanguageCode == \"en\" &&\n      (xpinstallEnabled == true) &&\n      (${JSON.stringify(\n        REDDIT_ENHANCEMENT_PARAMS.existing_addons\n      )} intersect addonsInfo.addons|keys)|length == 0 &&\n      (${JSON.stringify(\n        REDDIT_ENHANCEMENT_PARAMS.open_urls\n      )} intersect topFrecentSites[.frecency >= ${\n      REDDIT_ENHANCEMENT_PARAMS.min_frecency\n    }]|mapToProperty('host'))|length > 0`,\n    trigger: { id: \"openURL\", params: REDDIT_ENHANCEMENT_PARAMS.open_urls },\n  },\n  {\n    id: \"PIN_TAB\",\n    template: \"cfr_doorhanger\",\n    content: {\n      layout: \"message_and_animation\",\n      category: \"cfrFeatures\",\n      bucket_id: \"CFR_PIN_TAB\",\n      notification_text: { string_id: \"cfr-doorhanger-feature-notification\" },\n      heading_text: { string_id: \"cfr-doorhanger-pintab-heading\" },\n      info_icon: {\n        label: { string_id: \"cfr-doorhanger-extension-sumo-link\" },\n        sumo_path: REDDIT_ENHANCEMENT_PARAMS.sumo_path,\n      },\n      text: { string_id: \"cfr-doorhanger-pintab-description\" },\n      descriptionDetails: {\n        steps: [\n          { string_id: \"cfr-doorhanger-pintab-step1\" },\n          { string_id: \"cfr-doorhanger-pintab-step2\" },\n          { string_id: \"cfr-doorhanger-pintab-step3\" },\n        ],\n      },\n      buttons: {\n        primary: {\n          label: { string_id: \"cfr-doorhanger-pintab-ok-button\" },\n          action: {\n            type: \"PIN_CURRENT_TAB\",\n          },\n        },\n        secondary: [\n          {\n            label: { string_id: \"cfr-doorhanger-extension-cancel-button\" },\n            action: { type: \"CANCEL\" },\n          },\n          {\n            label: {\n              string_id: \"cfr-doorhanger-extension-never-show-recommendation\",\n            },\n          },\n          {\n            label: {\n              string_id: \"cfr-doorhanger-extension-manage-settings-button\",\n            },\n            action: {\n              type: \"OPEN_PREFERENCES_PAGE\",\n              data: { category: \"general-cfrfeatures\" },\n            },\n          },\n        ],\n      },\n    },\n    targeting: `locale in ${JSON.stringify(\n      PINNED_TABS_TARGET_LOCALES\n    )} && !hasPinnedTabs && recentVisits[.timestamp > (currentDate|date - 3600 * 1000 * 1)]|length >= 3`,\n    frequency: { lifetime: 3 },\n    trigger: { id: \"frequentVisits\", params: PINNED_TABS_TARGET_SITES },\n  },\n  {\n    id: \"SAVE_LOGIN\",\n    frequency: {\n      lifetime: 3,\n    },\n    targeting: \"usesFirefoxSync == false\",\n    template: \"cfr_doorhanger\",\n    last_modified: 1565907636313,\n    content: {\n      layout: \"icon_and_message\",\n      text: {\n        string_id: \"cfr-doorhanger-sync-logins-body\",\n      },\n      icon: \"chrome://browser/content/aboutlogins/icons/intro-illustration.svg\",\n      icon_class: \"cfr-doorhanger-large-icon\",\n      buttons: {\n        secondary: [\n          {\n            label: {\n              string_id: \"cfr-doorhanger-extension-cancel-button\",\n            },\n            action: {\n              type: \"CANCEL\",\n            },\n          },\n          {\n            label: {\n              string_id: \"cfr-doorhanger-extension-never-show-recommendation\",\n            },\n          },\n          {\n            label: {\n              string_id: \"cfr-doorhanger-extension-manage-settings-button\",\n            },\n            action: {\n              type: \"OPEN_PREFERENCES_PAGE\",\n              data: {\n                category: \"general-cfrfeatures\",\n              },\n            },\n          },\n        ],\n        primary: {\n          label: {\n            string_id: \"cfr-doorhanger-sync-logins-ok-button\",\n          },\n          action: {\n            type: \"OPEN_PREFERENCES_PAGE\",\n            data: {\n              category: \"sync\",\n            },\n          },\n        },\n      },\n      bucket_id: \"CFR_SAVE_LOGIN\",\n      heading_text: {\n        string_id: \"cfr-doorhanger-sync-logins-header\",\n      },\n      info_icon: {\n        label: {\n          string_id: \"cfr-doorhanger-extension-sumo-link\",\n        },\n        sumo_path: \"extensionrecommendations\",\n      },\n      notification_text: {\n        string_id: \"cfr-doorhanger-feature-notification\",\n      },\n      category: \"cfrFeatures\",\n    },\n    trigger: {\n      id: \"newSavedLogin\",\n    },\n  },\n  {\n    id: \"SOCIAL_TRACKING_PROTECTION\",\n    template: \"cfr_doorhanger\",\n    priority: 1,\n    content: {\n      layout: \"icon_and_message\",\n      category: \"cfrFeatures\",\n      anchor_id: \"tracking-protection-icon-box\",\n      skip_address_bar_notifier: true,\n      bucket_id: \"CFR_SOCIAL_TRACKING_PROTECTION\",\n      heading_text: { string_id: \"cfr-doorhanger-socialtracking-heading\" },\n      notification_text: \"\",\n      info_icon: {\n        label: {\n          string_id: \"cfr-doorhanger-extension-sumo-link\",\n        },\n        sumo_path: \"extensionrecommendations\",\n      },\n      learn_more: \"social-media-tracking-report\",\n      text: { string_id: \"cfr-doorhanger-socialtracking-description\" },\n      icon: \"chrome://browser/skin/notification-icons/block-social.svg\",\n      icon_dark_theme:\n        \"chrome://browser/skin/notification-icons/block-social-dark.svg\",\n      buttons: {\n        primary: {\n          label: { string_id: \"cfr-doorhanger-socialtracking-ok-button\" },\n          action: { type: \"OPEN_PROTECTION_PANEL\" },\n          event: \"PROTECTION\",\n        },\n        secondary: [\n          {\n            label: { string_id: \"cfr-doorhanger-socialtracking-close-button\" },\n            event: \"BLOCK\",\n          },\n          {\n            label: {\n              string_id: \"cfr-doorhanger-socialtracking-dont-show-again\",\n            },\n            action: { type: \"DISABLE_STP_DOORHANGERS\" },\n            event: \"BLOCK\",\n          },\n        ],\n      },\n    },\n    targeting: \"pageLoad >= 4 && firefoxVersion >= 71\",\n    frequency: {\n      lifetime: 2,\n      custom: [{ period: 2 * 86400 * 1000, cap: 1 }],\n    },\n    trigger: {\n      id: \"trackingProtection\",\n      params: [\n        Ci.nsIWebProgressListener.STATE_BLOCKED_SOCIALTRACKING_CONTENT,\n        Ci.nsIWebProgressListener.STATE_LOADED_SOCIALTRACKING_CONTENT |\n          Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER,\n      ],\n    },\n  },\n  {\n    id: \"FINGERPRINTERS_PROTECTION\",\n    template: \"cfr_doorhanger\",\n    priority: 2,\n    content: {\n      layout: \"icon_and_message\",\n      category: \"cfrFeatures\",\n      anchor_id: \"tracking-protection-icon-box\",\n      skip_address_bar_notifier: true,\n      bucket_id: \"CFR_SOCIAL_TRACKING_PROTECTION\",\n      heading_text: { string_id: \"cfr-doorhanger-fingerprinters-heading\" },\n      notification_text: \"\",\n      info_icon: {\n        label: {\n          string_id: \"cfr-doorhanger-extension-sumo-link\",\n        },\n        sumo_path: \"extensionrecommendations\",\n      },\n      learn_more: \"fingerprinters-report\",\n      text: { string_id: \"cfr-doorhanger-fingerprinters-description\" },\n      icon: \"chrome://browser/skin/notification-icons/block-fingerprinter.svg\",\n      icon_dark_theme:\n        \"chrome://browser/skin/notification-icons/block-fingerprinter-dark.svg\",\n      buttons: {\n        primary: {\n          label: { string_id: \"cfr-doorhanger-socialtracking-ok-button\" },\n          action: { type: \"OPEN_PROTECTION_PANEL\" },\n          event: \"PROTECTION\",\n        },\n        secondary: [\n          {\n            label: { string_id: \"cfr-doorhanger-socialtracking-close-button\" },\n            event: \"BLOCK\",\n          },\n          {\n            label: {\n              string_id: \"cfr-doorhanger-socialtracking-dont-show-again\",\n            },\n            action: { type: \"DISABLE_STP_DOORHANGERS\" },\n            event: \"BLOCK\",\n          },\n        ],\n      },\n    },\n    targeting: \"pageLoad >= 4 && firefoxVersion >= 71\",\n    frequency: {\n      lifetime: 2,\n      custom: [{ period: 2 * 86400 * 1000, cap: 1 }],\n    },\n    trigger: {\n      id: \"trackingProtection\",\n      params: [Ci.nsIWebProgressListener.STATE_BLOCKED_FINGERPRINTING_CONTENT],\n    },\n  },\n  {\n    id: \"CRYPTOMINERS_PROTECTION\",\n    template: \"cfr_doorhanger\",\n    priority: 3,\n    content: {\n      layout: \"icon_and_message\",\n      category: \"cfrFeatures\",\n      anchor_id: \"tracking-protection-icon-box\",\n      skip_address_bar_notifier: true,\n      bucket_id: \"CFR_SOCIAL_TRACKING_PROTECTION\",\n      heading_text: { string_id: \"cfr-doorhanger-cryptominers-heading\" },\n      notification_text: \"\",\n      info_icon: {\n        label: {\n          string_id: \"cfr-doorhanger-extension-sumo-link\",\n        },\n        sumo_path: \"extensionrecommendations\",\n      },\n      learn_more: \"cryptominers-report\",\n      text: { string_id: \"cfr-doorhanger-cryptominers-description\" },\n      icon: \"chrome://browser/skin/notification-icons/block-cryptominer.svg\",\n      icon_dark_theme:\n        \"chrome://browser/skin/notification-icons/block-cryptominer-dark.svg\",\n      buttons: {\n        primary: {\n          label: { string_id: \"cfr-doorhanger-socialtracking-ok-button\" },\n          action: { type: \"OPEN_PROTECTION_PANEL\" },\n          event: \"PROTECTION\",\n        },\n        secondary: [\n          {\n            label: { string_id: \"cfr-doorhanger-socialtracking-close-button\" },\n            event: \"BLOCK\",\n          },\n          {\n            label: {\n              string_id: \"cfr-doorhanger-socialtracking-dont-show-again\",\n            },\n            action: { type: \"DISABLE_STP_DOORHANGERS\" },\n            event: \"BLOCK\",\n          },\n        ],\n      },\n    },\n    targeting: \"pageLoad >= 4 && firefoxVersion >= 71\",\n    frequency: {\n      lifetime: 2,\n      custom: [{ period: 2 * 86400 * 1000, cap: 1 }],\n    },\n    trigger: {\n      id: \"trackingProtection\",\n      params: [Ci.nsIWebProgressListener.STATE_BLOCKED_CRYPTOMINING_CONTENT],\n    },\n  },\n  {\n    id: \"MILESTONE_MESSAGE\",\n    template: \"milestone_message\",\n    content: {\n      layout: \"short_message\",\n      category: \"cfrFeatures\",\n      anchor_id: \"tracking-protection-icon-box\",\n      skip_address_bar_notifier: true,\n      bucket_id: \"CFR_MILESTONE_MESSAGE\",\n      heading_text: { string_id: \"cfr-doorhanger-milestone-heading\" },\n      notification_text: \"\",\n      text: \"\",\n      buttons: {\n        primary: {\n          label: { string_id: \"cfr-doorhanger-milestone-ok-button\" },\n          action: { type: \"OPEN_PROTECTION_REPORT\" },\n          event: \"PROTECTION\",\n        },\n      },\n    },\n    targeting: \"pageLoad >= 4\",\n    frequency: {\n      lifetime: 7, // Length of privacy.trackingprotection.cfr-milestone.milestones pref\n    },\n    trigger: {\n      id: \"trackingProtection\",\n      params: [\"ContentBlockingMilestone\"],\n    },\n  },\n];\n\nconst CFRMessageProvider = {\n  getMessages() {\n    return CFR_MESSAGES.filter(msg => !msg.exclude);\n  },\n};\nthis.CFRMessageProvider = CFRMessageProvider;\n\nconst EXPORTED_SYMBOLS = [\"CFRMessageProvider\"];\n"
  },
  {
    "path": "lib/CFRPageActions.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\"use strict\";\n\nconst { XPCOMUtils } = ChromeUtils.import(\n  \"resource://gre/modules/XPCOMUtils.jsm\"\n);\nconst { Services } = ChromeUtils.import(\"resource://gre/modules/Services.jsm\");\nXPCOMUtils.defineLazyGlobalGetters(this, [\"fetch\"]);\n\nXPCOMUtils.defineLazyModuleGetters(this, {\n  PrivateBrowsingUtils: \"resource://gre/modules/PrivateBrowsingUtils.jsm\",\n  RemoteL10n: \"resource://activity-stream/lib/RemoteL10n.jsm\",\n});\n\nXPCOMUtils.defineLazyServiceGetter(\n  this,\n  \"TrackingDBService\",\n  \"@mozilla.org/tracking-db-service;1\",\n  \"nsITrackingDBService\"\n);\nXPCOMUtils.defineLazyPreferenceGetter(\n  this,\n  \"milestones\",\n  \"browser.contentblocking.cfr-milestone.milestones\",\n  \"[]\",\n  null,\n  JSON.parse\n);\n\nconst POPUP_NOTIFICATION_ID = \"contextual-feature-recommendation\";\nconst ANIMATION_BUTTON_ID = \"cfr-notification-footer-animation-button\";\nconst ANIMATION_LABEL_ID = \"cfr-notification-footer-animation-label\";\nconst SUMO_BASE_URL = Services.urlFormatter.formatURLPref(\n  \"app.support.baseURL\"\n);\nconst ADDONS_API_URL =\n  \"https://services.addons.mozilla.org/api/v3/addons/addon\";\nconst ANIMATIONS_ENABLED_PREF = \"toolkit.cosmeticAnimations.enabled\";\n\nconst DELAY_BEFORE_EXPAND_MS = 1000;\nconst CATEGORY_ICONS = {\n  cfrAddons: \"webextensions-icon\",\n  cfrFeatures: \"recommendations-icon\",\n};\n\n/**\n * A WeakMap from browsers to {host, recommendation} pairs. Recommendations are\n * defined in the ExtensionDoorhanger.schema.json.\n *\n * A recommendation is specific to a browser and host and is active until the\n * given browser is closed or the user navigates (within that browser) away from\n * the host.\n */\nlet RecommendationMap = new WeakMap();\n\n/**\n * A WeakMap from windows to their CFR PageAction.\n */\nlet PageActionMap = new WeakMap();\n\n/**\n * We need one PageAction for each window\n */\nclass PageAction {\n  constructor(win, dispatchToASRouter) {\n    this.window = win;\n    this.urlbar = win.document.getElementById(\"urlbar\");\n    // `this.urlbar` is the larger container that holds both the urlbar input\n    // and the page action buttons. The focus event will be triggered by the\n    // `urlbar-input`.\n    this.urlbarinput = win.document.getElementById(\"urlbar-input\");\n    this.container = win.document.getElementById(\n      \"contextual-feature-recommendation\"\n    );\n    this.button = win.document.getElementById(\"cfr-button\");\n    this.label = win.document.getElementById(\"cfr-label\");\n\n    // This should NOT be use directly to dispatch message-defined actions attached to buttons.\n    // Please use dispatchUserAction instead.\n    this._dispatchToASRouter = dispatchToASRouter;\n\n    this._popupStateChange = this._popupStateChange.bind(this);\n    this._collapse = this._collapse.bind(this);\n    this._showPopupOnClick = this._showPopupOnClick.bind(this);\n    this.dispatchUserAction = this.dispatchUserAction.bind(this);\n\n    // Saved timeout IDs for scheduled state changes, so they can be cancelled\n    this.stateTransitionTimeoutIDs = [];\n\n    XPCOMUtils.defineLazyGetter(this, \"isDarkTheme\", () => {\n      try {\n        return this.window.document.documentElement.hasAttribute(\n          \"lwt-toolbar-field-brighttext\"\n        );\n      } catch (e) {\n        return false;\n      }\n    });\n  }\n\n  addImpression(recommendation) {\n    this._dispatchImpression(recommendation);\n    // Only send an impression ping upon the first expansion.\n    // Note that when the user clicks on the \"show\" button on the asrouter admin\n    // page (both `bucket_id` and `id` will be set as null), we don't want to send\n    // the impression ping in that case.\n    if (!!recommendation.id && !!recommendation.content.bucket_id) {\n      this._sendTelemetry({\n        message_id: recommendation.id,\n        bucket_id: recommendation.content.bucket_id,\n        event: \"IMPRESSION\",\n        ...(recommendation.personalizedModelVersion\n          ? {\n              event_context: {\n                modelVersion: recommendation.personalizedModelVersion,\n              },\n            }\n          : {}),\n      });\n    }\n  }\n\n  reloadL10n() {\n    RemoteL10n.reloadL10n();\n  }\n\n  async showAddressBarNotifier(recommendation, shouldExpand = false) {\n    this.container.hidden = false;\n\n    let notificationText = await this.getStrings(\n      recommendation.content.notification_text\n    );\n    this.label.value = notificationText;\n    if (notificationText.attributes) {\n      this.button.setAttribute(\n        \"tooltiptext\",\n        notificationText.attributes.tooltiptext\n      );\n      // For a11y, we want the more descriptive text.\n      this.container.setAttribute(\n        \"aria-label\",\n        notificationText.attributes.tooltiptext\n      );\n    }\n    this.button.setAttribute(\n      \"data-cfr-icon\",\n      CATEGORY_ICONS[recommendation.content.category]\n    );\n\n    // Wait for layout to flush to avoid a synchronous reflow then calculate the\n    // label width. We can safely get the width even though the recommendation is\n    // collapsed; the label itself remains full width (with its overflow hidden)\n    let [{ width }] = await this.window.promiseDocumentFlushed(() =>\n      this.label.getClientRects()\n    );\n    this.urlbar.style.setProperty(\"--cfr-label-width\", `${width}px`);\n\n    this.container.addEventListener(\"click\", this._showPopupOnClick);\n    // Collapse the recommendation on url bar focus in order to free up more\n    // space to display and edit the url\n    this.urlbarinput.addEventListener(\"focus\", this._collapse);\n\n    if (shouldExpand) {\n      this._clearScheduledStateChanges();\n\n      // After one second, expand\n      this._expand(DELAY_BEFORE_EXPAND_MS);\n\n      this.addImpression(recommendation);\n    }\n\n    if (notificationText.attributes) {\n      this.window.A11yUtils.announce({\n        raw: notificationText.attributes[\"a11y-announcement\"],\n        source: this.container,\n      });\n    }\n  }\n\n  hideAddressBarNotifier() {\n    this.container.hidden = true;\n    this._clearScheduledStateChanges();\n    this.urlbar.removeAttribute(\"cfr-recommendation-state\");\n    this.container.removeEventListener(\"click\", this._showPopupOnClick);\n    this.urlbar.removeEventListener(\"focus\", this._collapse);\n    if (this.currentNotification) {\n      this.window.PopupNotifications.remove(this.currentNotification);\n      this.currentNotification = null;\n    }\n  }\n\n  _expand(delay) {\n    if (delay > 0) {\n      this.stateTransitionTimeoutIDs.push(\n        this.window.setTimeout(() => {\n          this.urlbar.setAttribute(\"cfr-recommendation-state\", \"expanded\");\n        }, delay)\n      );\n    } else {\n      // Non-delayed state change overrides any scheduled state changes\n      this._clearScheduledStateChanges();\n      this.urlbar.setAttribute(\"cfr-recommendation-state\", \"expanded\");\n    }\n  }\n\n  _collapse(delay) {\n    if (delay > 0) {\n      this.stateTransitionTimeoutIDs.push(\n        this.window.setTimeout(() => {\n          if (\n            this.urlbar.getAttribute(\"cfr-recommendation-state\") === \"expanded\"\n          ) {\n            this.urlbar.setAttribute(\"cfr-recommendation-state\", \"collapsed\");\n          }\n        }, delay)\n      );\n    } else {\n      // Non-delayed state change overrides any scheduled state changes\n      this._clearScheduledStateChanges();\n      if (this.urlbar.getAttribute(\"cfr-recommendation-state\") === \"expanded\") {\n        this.urlbar.setAttribute(\"cfr-recommendation-state\", \"collapsed\");\n      }\n    }\n\n    // TODO: FIXME: find a nicer way of cleaning this up. Maybe listening to \"popuphidden\"?\n    // Remove click listener on pause button;\n    if (this.onAnimationButtonClick) {\n      this.window.document\n        .getElementById(ANIMATION_BUTTON_ID)\n        .removeEventListener(\"click\", this.onAnimationButtonClick);\n      delete this.onAnimationButtonClick;\n    }\n  }\n\n  _clearScheduledStateChanges() {\n    while (this.stateTransitionTimeoutIDs.length) {\n      // clearTimeout is safe even with invalid/expired IDs\n      this.window.clearTimeout(this.stateTransitionTimeoutIDs.pop());\n    }\n  }\n\n  // This is called when the popup closes as a result of interaction _outside_\n  // the popup, e.g. by hitting <esc>\n  _popupStateChange(state) {\n    if (state === \"shown\") {\n      if (this._autoFocus) {\n        this.window.document.commandDispatcher.advanceFocusIntoSubtree(\n          this.currentNotification.owner.panel\n        );\n        this._autoFocus = false;\n      }\n    } else if ([\"dismissed\", \"removed\"].includes(state)) {\n      this._collapse();\n      if (this.currentNotification) {\n        this.window.PopupNotifications.remove(this.currentNotification);\n        this.currentNotification = null;\n      }\n    }\n  }\n\n  dispatchUserAction(action) {\n    this._dispatchToASRouter(\n      { type: \"USER_ACTION\", data: action },\n      { browser: this.window.gBrowser.selectedBrowser }\n    );\n  }\n\n  _dispatchImpression(message) {\n    this._dispatchToASRouter({ type: \"IMPRESSION\", data: message });\n  }\n\n  _sendTelemetry(ping) {\n    this._dispatchToASRouter({\n      type: \"DOORHANGER_TELEMETRY\",\n      data: { action: \"cfr_user_event\", source: \"CFR\", ...ping },\n    });\n  }\n\n  _blockMessage(messageID) {\n    this._dispatchToASRouter({\n      type: \"BLOCK_MESSAGE_BY_ID\",\n      data: { id: messageID },\n    });\n  }\n\n  /**\n   * getStrings - Handles getting the localized strings vs message overrides.\n   *              If string_id is not defined it assumes you passed in an override\n   *              message and it just returns it.\n   *              If subAttribute is provided, the string for it is returned.\n   * @return A string. One of 1) passed in string 2) a String object with\n   *         attributes property if there are attributes 3) the sub attribute.\n   */\n  async getStrings(string, subAttribute = \"\") {\n    if (!string.string_id) {\n      if (subAttribute) {\n        if (string.attributes) {\n          return string.attributes[subAttribute];\n        }\n\n        Cu.reportError(\n          `String ${string.value} does not contain any attributes`\n        );\n        return subAttribute;\n      }\n\n      if (typeof string.value === \"string\") {\n        const stringWithAttributes = new String(string.value); // eslint-disable-line no-new-wrappers\n        stringWithAttributes.attributes = string.attributes;\n        return stringWithAttributes;\n      }\n\n      return string;\n    }\n\n    const [localeStrings] = await RemoteL10n.l10n.formatMessages([\n      {\n        id: string.string_id,\n        args: string.args,\n      },\n    ]);\n\n    const mainString = new String(localeStrings.value); // eslint-disable-line no-new-wrappers\n    if (localeStrings.attributes) {\n      const attributes = localeStrings.attributes.reduce((acc, attribute) => {\n        acc[attribute.name] = attribute.value;\n        return acc;\n      }, {});\n      mainString.attributes = attributes;\n    }\n\n    return subAttribute ? mainString.attributes[subAttribute] : mainString;\n  }\n\n  async _setAddonAuthorAndRating(document, content) {\n    const author = this.window.document.getElementById(\n      \"cfr-notification-author\"\n    );\n    const footerFilledStars = this.window.document.getElementById(\n      \"cfr-notification-footer-filled-stars\"\n    );\n    const footerEmptyStars = this.window.document.getElementById(\n      \"cfr-notification-footer-empty-stars\"\n    );\n    const footerUsers = this.window.document.getElementById(\n      \"cfr-notification-footer-users\"\n    );\n    const footerSpacer = this.window.document.getElementById(\n      \"cfr-notification-footer-spacer\"\n    );\n\n    author.textContent = await this.getStrings({\n      string_id: \"cfr-doorhanger-extension-author\",\n      args: { name: content.addon.author },\n    });\n\n    const { rating } = content.addon;\n    if (rating) {\n      const MAX_RATING = 5;\n      const STARS_WIDTH = 17 * MAX_RATING;\n      const calcWidth = stars => `${(stars / MAX_RATING) * STARS_WIDTH}px`;\n      footerFilledStars.style.width = calcWidth(rating);\n      footerEmptyStars.style.width = calcWidth(MAX_RATING - rating);\n\n      const ratingString = await this.getStrings(\n        {\n          string_id: \"cfr-doorhanger-extension-rating\",\n          args: { total: rating },\n        },\n        \"tooltiptext\"\n      );\n      footerFilledStars.setAttribute(\"tooltiptext\", ratingString);\n      footerEmptyStars.setAttribute(\"tooltiptext\", ratingString);\n    } else {\n      footerFilledStars.style.width = \"\";\n      footerEmptyStars.style.width = \"\";\n      footerFilledStars.removeAttribute(\"tooltiptext\");\n      footerEmptyStars.removeAttribute(\"tooltiptext\");\n    }\n\n    const { users } = content.addon;\n    if (users) {\n      footerUsers.setAttribute(\n        \"value\",\n        await this.getStrings({\n          string_id: \"cfr-doorhanger-extension-total-users\",\n          args: { total: users },\n        })\n      );\n      footerUsers.removeAttribute(\"hidden\");\n    } else {\n      // Prevent whitespace around empty label from affecting other spacing\n      footerUsers.setAttribute(\"hidden\", true);\n      footerUsers.removeAttribute(\"value\");\n    }\n\n    // Spacer pushes the link to the opposite end when there's other content\n    if (rating || users) {\n      footerSpacer.removeAttribute(\"hidden\");\n    } else {\n      footerSpacer.setAttribute(\"hidden\", true);\n    }\n  }\n\n  _createElementAndAppend({ type, id }, parent) {\n    let element = this.window.document.createXULElement(type);\n    if (id) {\n      element.setAttribute(\"id\", id);\n    }\n    parent.appendChild(element);\n    return element;\n  }\n\n  async _renderPinTabAnimation() {\n    const ANIMATION_CONTAINER_ID =\n      \"cfr-notification-footer-pintab-animation-container\";\n    const footer = this.window.document.getElementById(\n      \"cfr-notification-footer\"\n    );\n    let animationContainer = this.window.document.getElementById(\n      ANIMATION_CONTAINER_ID\n    );\n    if (!animationContainer) {\n      animationContainer = this._createElementAndAppend(\n        { type: \"vbox\", id: ANIMATION_CONTAINER_ID },\n        footer\n      );\n\n      let controlsContainer = this._createElementAndAppend(\n        { type: \"hbox\", id: \"cfr-notification-footer-animation-controls\" },\n        animationContainer\n      );\n\n      // spacer\n      this._createElementAndAppend(\n        { type: \"vbox\" },\n        controlsContainer\n      ).setAttribute(\"flex\", 1);\n\n      let animationButton = this._createElementAndAppend(\n        { type: \"hbox\", id: ANIMATION_BUTTON_ID },\n        controlsContainer\n      );\n\n      // animation button label\n      this._createElementAndAppend(\n        { type: \"label\", id: ANIMATION_LABEL_ID },\n        animationButton\n      );\n    }\n\n    animationContainer.toggleAttribute(\n      \"animate\",\n      Services.prefs.getBoolPref(ANIMATIONS_ENABLED_PREF, true)\n    );\n    animationContainer.removeAttribute(\"paused\");\n\n    this.window.document.getElementById(\n      ANIMATION_LABEL_ID\n    ).textContent = await this.getStrings({\n      string_id: \"cfr-doorhanger-pintab-animation-pause\",\n    });\n\n    if (!this.onAnimationButtonClick) {\n      let animationButton = this.window.document.getElementById(\n        ANIMATION_BUTTON_ID\n      );\n      this.onAnimationButtonClick = async () => {\n        let animationLabel = this.window.document.getElementById(\n          ANIMATION_LABEL_ID\n        );\n        if (animationContainer.toggleAttribute(\"paused\")) {\n          animationLabel.textContent = await this.getStrings({\n            string_id: \"cfr-doorhanger-pintab-animation-resume\",\n          });\n        } else {\n          animationLabel.textContent = await this.getStrings({\n            string_id: \"cfr-doorhanger-pintab-animation-pause\",\n          });\n        }\n      };\n      animationButton.addEventListener(\"click\", this.onAnimationButtonClick);\n    }\n  }\n\n  async _renderMilestonePopup(message, browser) {\n    let { content } = message;\n    let { primary } = content.buttons;\n\n    let dateFormat = new Services.intl.DateTimeFormat(\n      this.window.gBrowser.ownerGlobal.navigator.language,\n      {\n        month: \"long\",\n        year: \"numeric\",\n      }\n    ).format;\n\n    let earliestDate = await TrackingDBService.getEarliestRecordedDate();\n    let monthName = dateFormat(new Date(earliestDate));\n    let panelTitle = \"\";\n    let headerLabel = this.window.document.getElementById(\n      \"cfr-notification-header-label\"\n    );\n    let reachedMilestone = null;\n    let totalSaved = await TrackingDBService.sumAllEvents();\n    for (let milestone of milestones) {\n      if (totalSaved >= milestone) {\n        reachedMilestone = milestone;\n      }\n    }\n    if (typeof message.content.heading_text === \"string\") {\n      // This is a test environment.\n      panelTitle = message.content.heading_text;\n      headerLabel.value = panelTitle;\n    } else {\n      RemoteL10n.l10n.setAttributes(\n        headerLabel,\n        content.heading_text.string_id,\n        {\n          blockedCount: reachedMilestone,\n          date: monthName,\n        }\n      );\n      await RemoteL10n.l10n.translateElements([headerLabel]);\n    }\n\n    // Use the message layout as a CSS selector to hide different parts of the\n    // notification template markup\n    this.window.document\n      .getElementById(\"contextual-feature-recommendation-notification\")\n      .setAttribute(\"data-notification-category\", content.layout);\n    this.window.document\n      .getElementById(\"contextual-feature-recommendation-notification\")\n      .setAttribute(\"data-notification-bucket\", content.bucket_id);\n    let notification = this.window.document.getElementById(\n      \"notification-popup\"\n    );\n\n    let primaryBtnString = await this.getStrings(primary.label);\n    let primaryActionCallback = () => {\n      this.dispatchUserAction(primary.action);\n      RecommendationMap.delete(browser);\n\n      // Invalidate the pref after the user interacts with the button.\n      // We don't need to show the illustration in the privacy panel.\n      Services.prefs.clearUserPref(\n        \"browser.contentblocking.cfr-milestone.milestone-shown-time\"\n      );\n    };\n    let mainAction = {\n      label: primaryBtnString,\n      accessKey: primaryBtnString.attributes.accesskey,\n      callback: primaryActionCallback,\n    };\n\n    let style = this.window.document.createElement(\"style\");\n    style.textContent = `\n      .cfr-notification-milestone .panel-arrow {\n        fill: #0250BB !important;\n      }\n    `;\n\n    let arrow;\n    let manageClass = event => {\n      if (event === \"dismissed\" || event === \"removed\") {\n        notification.shadowRoot.removeChild(style);\n        arrow.classList.remove(\"cfr-notification-milestone\");\n      } else if (event === \"showing\") {\n        notification.shadowRoot.appendChild(style);\n        arrow = notification.shadowRoot.querySelector(\".panel-arrowcontainer\");\n        arrow.classList.add(\"cfr-notification-milestone\");\n      }\n    };\n\n    // Actually show the notification\n    this.currentNotification = this.window.PopupNotifications.show(\n      browser,\n      POPUP_NOTIFICATION_ID,\n      panelTitle,\n      \"cfr\",\n      mainAction,\n      null,\n      {\n        hideClose: true,\n        eventCallback: manageClass,\n      }\n    );\n    Services.prefs.setIntPref(\n      \"browser.contentblocking.cfr-milestone.milestone-achieved\",\n      reachedMilestone\n    );\n    Services.prefs.setStringPref(\n      \"browser.contentblocking.cfr-milestone.milestone-shown-time\",\n      Date.now().toString()\n    );\n  }\n\n  // eslint-disable-next-line max-statements\n  async _renderPopup(message, browser) {\n    const { id, content, modelVersion } = message;\n\n    const headerLabel = this.window.document.getElementById(\n      \"cfr-notification-header-label\"\n    );\n    const headerLink = this.window.document.getElementById(\n      \"cfr-notification-header-link\"\n    );\n    const headerImage = this.window.document.getElementById(\n      \"cfr-notification-header-image\"\n    );\n    const footerText = this.window.document.getElementById(\n      \"cfr-notification-footer-text\"\n    );\n    const footerLink = this.window.document.getElementById(\n      \"cfr-notification-footer-learn-more-link\"\n    );\n    const { primary, secondary } = content.buttons;\n    let primaryActionCallback;\n    let options = {};\n    let panelTitle;\n\n    headerLabel.value = await this.getStrings(content.heading_text);\n    headerLink.setAttribute(\n      \"href\",\n      SUMO_BASE_URL + content.info_icon.sumo_path\n    );\n    headerImage.setAttribute(\n      \"tooltiptext\",\n      await this.getStrings(content.info_icon.label, \"tooltiptext\")\n    );\n    headerLink.onclick = () =>\n      this._sendTelemetry({\n        message_id: id,\n        bucket_id: content.bucket_id,\n        event: \"RATIONALE\",\n        ...(modelVersion ? { event_context: { modelVersion } } : {}),\n      });\n    // Use the message layout as a CSS selector to hide different parts of the\n    // notification template markup\n    this.window.document\n      .getElementById(\"contextual-feature-recommendation-notification\")\n      .setAttribute(\"data-notification-category\", content.layout);\n    this.window.document\n      .getElementById(\"contextual-feature-recommendation-notification\")\n      .setAttribute(\"data-notification-bucket\", content.bucket_id);\n\n    switch (content.layout) {\n      case \"icon_and_message\":\n        const author = this.window.document.getElementById(\n          \"cfr-notification-author\"\n        );\n        author.textContent = await this.getStrings(content.text);\n        primaryActionCallback = () => {\n          this._blockMessage(id);\n          this.dispatchUserAction(primary.action);\n          this.hideAddressBarNotifier();\n          this._sendTelemetry({\n            message_id: id,\n            bucket_id: content.bucket_id,\n            event: \"ENABLE\",\n            ...(modelVersion ? { event_context: { modelVersion } } : {}),\n          });\n          RecommendationMap.delete(browser);\n        };\n\n        let getIcon = () => {\n          if (content.icon_dark_theme && this.isDarkTheme) {\n            return content.icon_dark_theme;\n          }\n          return content.icon;\n        };\n\n        let learnMoreURL = content.learn_more\n          ? SUMO_BASE_URL + content.learn_more\n          : null;\n\n        panelTitle = await this.getStrings(content.heading_text);\n        options = {\n          popupIconURL: getIcon(),\n          popupIconClass: content.icon_class,\n          learnMoreURL,\n        };\n        break;\n      case \"message_and_animation\":\n        footerText.textContent = await this.getStrings(content.text);\n        const stepsContainerId = \"cfr-notification-feature-steps\";\n        let stepsContainer = this.window.document.getElementById(\n          stepsContainerId\n        );\n        primaryActionCallback = () => {\n          this._blockMessage(id);\n          this.dispatchUserAction(primary.action);\n          this.hideAddressBarNotifier();\n          this._sendTelemetry({\n            message_id: id,\n            bucket_id: content.bucket_id,\n            event: \"PIN\",\n            ...(modelVersion ? { event_context: { modelVersion } } : {}),\n          });\n          RecommendationMap.delete(browser);\n        };\n        panelTitle = await this.getStrings(content.heading_text);\n\n        if (content.descriptionDetails) {\n          if (stepsContainer) {\n            // If it exists we need to empty it\n            stepsContainer.remove();\n            stepsContainer = stepsContainer.cloneNode(false);\n          } else {\n            stepsContainer = this.window.document.createXULElement(\"vbox\");\n            stepsContainer.setAttribute(\"id\", stepsContainerId);\n          }\n          footerText.parentNode.appendChild(stepsContainer);\n          for (let step of content.descriptionDetails.steps) {\n            // This li is a generic xul element with custom styling\n            const li = this.window.document.createXULElement(\"li\");\n            RemoteL10n.l10n.setAttributes(li, step.string_id);\n            stepsContainer.appendChild(li);\n          }\n          await RemoteL10n.l10n.translateElements([...stepsContainer.children]);\n        }\n\n        await this._renderPinTabAnimation();\n        break;\n      default:\n        panelTitle = await this.getStrings(content.addon.title);\n        await this._setAddonAuthorAndRating(this.window.document, content);\n        // Main body content of the dropdown\n        footerText.textContent = await this.getStrings(content.text);\n        options = { popupIconURL: content.addon.icon };\n\n        footerLink.value = await this.getStrings({\n          string_id: \"cfr-doorhanger-extension-learn-more-link\",\n        });\n        footerLink.setAttribute(\"href\", content.addon.amo_url);\n        footerLink.onclick = () =>\n          this._sendTelemetry({\n            message_id: id,\n            bucket_id: content.bucket_id,\n            event: \"LEARN_MORE\",\n            ...(modelVersion ? { event_context: { modelVersion } } : {}),\n          });\n\n        primaryActionCallback = async () => {\n          // eslint-disable-next-line no-use-before-define\n          primary.action.data.url = await CFRPageActions._fetchLatestAddonVersion(\n            content.addon.id\n          );\n          this._blockMessage(id);\n          this.dispatchUserAction(primary.action);\n          this.hideAddressBarNotifier();\n          this._sendTelemetry({\n            message_id: id,\n            bucket_id: content.bucket_id,\n            event: \"INSTALL\",\n            ...(modelVersion ? { event_context: { modelVersion } } : {}),\n          });\n          RecommendationMap.delete(browser);\n        };\n    }\n\n    const primaryBtnStrings = await this.getStrings(primary.label);\n    const mainAction = {\n      label: primaryBtnStrings,\n      accessKey: primaryBtnStrings.attributes.accesskey,\n      callback: primaryActionCallback,\n    };\n\n    let _renderSecondaryButtonAction = async (event, button) => {\n      let label = await this.getStrings(button.label);\n      let { attributes } = label;\n\n      return {\n        label,\n        accessKey: attributes.accesskey,\n        callback: () => {\n          if (button.action) {\n            this.dispatchUserAction(button.action);\n          } else {\n            this._blockMessage(id);\n            this.hideAddressBarNotifier();\n            RecommendationMap.delete(browser);\n          }\n\n          this._sendTelemetry({\n            message_id: id,\n            bucket_id: content.bucket_id,\n            event,\n            ...(modelVersion ? { event_context: { modelVersion } } : {}),\n          });\n        },\n      };\n    };\n\n    // For each secondary action, define default telemetry event\n    const defaultSecondaryEvent = [\"DISMISS\", \"BLOCK\", \"MANAGE\"];\n    const secondaryActions = await Promise.all(\n      secondary.map((button, i) => {\n        return _renderSecondaryButtonAction(\n          button.event || defaultSecondaryEvent[i],\n          button\n        );\n      })\n    );\n\n    // If the recommendation button is focused, it was probably activated via\n    // the keyboard. Therefore, focus the first element in the notification when\n    // it appears.\n    // We don't use the autofocus option provided by PopupNotifications.show\n    // because it doesn't focus the first element; i.e. the user still has to\n    // press tab once. That's not good enough, especially for screen reader\n    // users. Instead, we handle this ourselves in _popupStateChange.\n    this._autoFocus = this.window.document.activeElement === this.container;\n\n    // Actually show the notification\n    this.currentNotification = this.window.PopupNotifications.show(\n      browser,\n      POPUP_NOTIFICATION_ID,\n      panelTitle,\n      \"cfr\",\n      mainAction,\n      secondaryActions,\n      {\n        ...options,\n        hideClose: true,\n        eventCallback: this._popupStateChange,\n      }\n    );\n  }\n\n  /**\n   * Respond to a user click on the recommendation by showing a doorhanger/\n   * popup notification\n   */\n  async _showPopupOnClick(event) {\n    const browser = this.window.gBrowser.selectedBrowser;\n    if (!RecommendationMap.has(browser)) {\n      // There's no recommendation for this browser, so the user shouldn't have\n      // been able to click\n      this.hideAddressBarNotifier();\n      return;\n    }\n    const message = RecommendationMap.get(browser);\n\n    // The recommendation should remain either collapsed or expanded while the\n    // doorhanger is showing\n    this._clearScheduledStateChanges(browser, message);\n\n    await this.showPopup();\n  }\n\n  async showPopup() {\n    const browser = this.window.gBrowser.selectedBrowser;\n    const message = RecommendationMap.get(browser);\n    const { id, content, modelVersion } = message;\n\n    // A hacky way of setting the popup anchor outside the usual url bar icon box\n    // See https://searchfox.org/mozilla-central/rev/847b64cc28b74b44c379f9bff4f415b97da1c6d7/toolkit/modules/PopupNotifications.jsm#42\n    browser.cfrpopupnotificationanchor =\n      this.window.document.getElementById(content.anchor_id) || this.container;\n\n    this._sendTelemetry({\n      message_id: id,\n      bucket_id: content.bucket_id,\n      event: \"CLICK_DOORHANGER\",\n      ...(modelVersion ? { event_context: { modelVersion } } : {}),\n    });\n    await this._renderPopup(message, browser);\n  }\n\n  async showMilestonePopup() {\n    const browser = this.window.gBrowser.selectedBrowser;\n    const message = RecommendationMap.get(browser);\n    const { content } = message;\n\n    // A hacky way of setting the popup anchor outside the usual url bar icon box\n    // See https://searchfox.org/mozilla-central/rev/847b64cc28b74b44c379f9bff4f415b97da1c6d7/toolkit/modules/PopupNotifications.jsm#42\n    browser.cfrpopupnotificationanchor =\n      this.window.document.getElementById(content.anchor_id) || this.container;\n\n    await this._renderMilestonePopup(message, browser);\n    return true;\n  }\n}\n\nfunction isHostMatch(browser, host) {\n  return (\n    browser.documentURI.scheme.startsWith(\"http\") &&\n    browser.documentURI.host === host\n  );\n}\n\nconst CFRPageActions = {\n  // For testing purposes\n  RecommendationMap,\n  PageActionMap,\n\n  /**\n   * To be called from browser.js on a location change, passing in the browser\n   * that's been updated\n   */\n  updatePageActions(browser) {\n    const win = browser.ownerGlobal;\n    const pageAction = PageActionMap.get(win);\n    if (!pageAction || browser !== win.gBrowser.selectedBrowser) {\n      return;\n    }\n    if (RecommendationMap.has(browser)) {\n      const recommendation = RecommendationMap.get(browser);\n      if (\n        !recommendation.content.skip_address_bar_notifier &&\n        (isHostMatch(browser, recommendation.host) ||\n          // If there is no host associated we assume we're back on a tab\n          // that had a CFR message so we should show it again\n          !recommendation.host)\n      ) {\n        // The browser has a recommendation specified with this host, so show\n        // the page action\n        pageAction.showAddressBarNotifier(recommendation);\n      } else if (recommendation.retain) {\n        // Keep the recommendation first time the user navigates away just in\n        // case they will go back to the previous page\n        pageAction.hideAddressBarNotifier();\n        recommendation.retain = false;\n      } else {\n        // The user has navigated away from the specified host in the given\n        // browser, so the recommendation is no longer valid and should be removed\n        RecommendationMap.delete(browser);\n        pageAction.hideAddressBarNotifier();\n      }\n    } else {\n      // There's no recommendation specified for this browser, so hide the page action\n      pageAction.hideAddressBarNotifier();\n    }\n  },\n\n  /**\n   * Fetch the URL to the latest add-on xpi so the recommendation can download it.\n   * @param id          The add-on ID\n   * @return            A string for the URL that was fetched\n   */\n  async _fetchLatestAddonVersion(id) {\n    let url = null;\n    try {\n      const response = await fetch(`${ADDONS_API_URL}/${id}/`, {\n        credentials: \"omit\",\n      });\n      if (response.status !== 204 && response.ok) {\n        const json = await response.json();\n        url = json.current_version.files[0].url;\n      }\n    } catch (e) {\n      Cu.reportError(\n        \"Failed to get the latest add-on version for this recommendation\"\n      );\n    }\n    return url;\n  },\n\n  /**\n   * Show Milestone notification.\n   * @param browser                 The browser for the recommendation\n   * @param recommendation          The recommendation to show\n   * @param dispatchToASRouter      A function to dispatch resulting actions to\n   * @return                        Did adding the recommendation succeed?\n   */\n  async showMilestone(browser, message, dispatchToASRouter, options = {}) {\n    let win = null;\n    const { id, content, personalizedModelVersion } = message;\n\n    // If we are forcing via the Admin page, the browser comes in a different format\n    if (options.force) {\n      win = browser.browser.ownerGlobal;\n      RecommendationMap.set(browser.browser, {\n        id,\n        content,\n        retain: true,\n        modelVersion: personalizedModelVersion,\n      });\n    } else {\n      win = browser.ownerGlobal;\n      RecommendationMap.set(browser, {\n        id,\n        content,\n        retain: true,\n        modelVersion: personalizedModelVersion,\n      });\n    }\n\n    if (!PageActionMap.has(win)) {\n      PageActionMap.set(win, new PageAction(win, dispatchToASRouter));\n    }\n\n    await PageActionMap.get(win).showMilestonePopup();\n    PageActionMap.get(win).addImpression(message);\n\n    return true;\n  },\n\n  /**\n   * Force a recommendation to be shown. Should only happen via the Admin page.\n   * @param browser                 The browser for the recommendation\n   * @param recommendation  The recommendation to show\n   * @param dispatchToASRouter      A function to dispatch resulting actions to\n   * @return                        Did adding the recommendation succeed?\n   */\n  async forceRecommendation(browser, recommendation, dispatchToASRouter) {\n    // If we are forcing via the Admin page, the browser comes in a different format\n    const win = browser.browser.ownerGlobal;\n    const { id, content, personalizedModelVersion } = recommendation;\n    RecommendationMap.set(browser.browser, {\n      id,\n      content,\n      retain: true,\n      modelVersion: personalizedModelVersion,\n    });\n    if (!PageActionMap.has(win)) {\n      PageActionMap.set(win, new PageAction(win, dispatchToASRouter));\n    }\n\n    if (content.skip_address_bar_notifier) {\n      await PageActionMap.get(win).showPopup();\n      PageActionMap.get(win).addImpression(recommendation);\n    } else {\n      await PageActionMap.get(win).showAddressBarNotifier(recommendation, true);\n    }\n    return true;\n  },\n\n  /**\n   * Add a recommendation specific to the given browser and host.\n   * @param browser                 The browser for the recommendation\n   * @param host                    The host for the recommendation\n   * @param recommendation  The recommendation to show\n   * @param dispatchToASRouter      A function to dispatch resulting actions to\n   * @return                        Did adding the recommendation succeed?\n   */\n  async addRecommendation(browser, host, recommendation, dispatchToASRouter) {\n    const win = browser.ownerGlobal;\n    if (PrivateBrowsingUtils.isWindowPrivate(win)) {\n      return false;\n    }\n    if (\n      browser !== win.gBrowser.selectedBrowser ||\n      // We can have recommendations without URL restrictions\n      (host && !isHostMatch(browser, host))\n    ) {\n      return false;\n    }\n    if (RecommendationMap.has(browser)) {\n      // Don't replace an existing message\n      return false;\n    }\n    const { id, content, personalizedModelVersion } = recommendation;\n    RecommendationMap.set(browser, {\n      id,\n      host,\n      content,\n      retain: true,\n      modelVersion: personalizedModelVersion,\n    });\n    if (!PageActionMap.has(win)) {\n      PageActionMap.set(win, new PageAction(win, dispatchToASRouter));\n    }\n\n    if (content.skip_address_bar_notifier) {\n      await PageActionMap.get(win).showPopup();\n      PageActionMap.get(win).addImpression(recommendation);\n    } else {\n      await PageActionMap.get(win).showAddressBarNotifier(recommendation, true);\n    }\n    return true;\n  },\n\n  /**\n   * Clear all recommendations and hide all PageActions\n   */\n  clearRecommendations() {\n    // WeakMaps aren't iterable so we have to test all existing windows\n    for (const win of Services.wm.getEnumerator(\"navigator:browser\")) {\n      if (win.closed || !PageActionMap.has(win)) {\n        continue;\n      }\n      PageActionMap.get(win).hideAddressBarNotifier();\n    }\n    // WeakMaps don't have a `clear` method\n    PageActionMap = new WeakMap();\n    RecommendationMap = new WeakMap();\n    this.PageActionMap = PageActionMap;\n    this.RecommendationMap = RecommendationMap;\n  },\n\n  /**\n   * Reload the l10n Fluent files for all PageActions\n   */\n  reloadL10n() {\n    for (const win of Services.wm.getEnumerator(\"navigator:browser\")) {\n      if (win.closed || !PageActionMap.has(win)) {\n        continue;\n      }\n      PageActionMap.get(win).reloadL10n();\n    }\n  },\n};\n\nthis.PageAction = PageAction;\nthis.CFRPageActions = CFRPageActions;\n\nconst EXPORTED_SYMBOLS = [\"CFRPageActions\", \"PageAction\"];\n"
  },
  {
    "path": "lib/DiscoveryStreamFeed.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\"use strict\";\n\nconst { XPCOMUtils } = ChromeUtils.import(\n  \"resource://gre/modules/XPCOMUtils.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"NewTabUtils\",\n  \"resource://gre/modules/NewTabUtils.jsm\"\n);\nconst { setTimeout, clearTimeout } = ChromeUtils.import(\n  \"resource://gre/modules/Timer.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"Services\",\n  \"resource://gre/modules/Services.jsm\"\n);\nXPCOMUtils.defineLazyGlobalGetters(this, [\"fetch\"]);\nChromeUtils.defineModuleGetter(\n  this,\n  \"perfService\",\n  \"resource://activity-stream/common/PerfService.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"UserDomainAffinityProvider\",\n  \"resource://activity-stream/lib/UserDomainAffinityProvider.jsm\"\n);\nconst { actionTypes: at, actionCreators: ac } = ChromeUtils.import(\n  \"resource://activity-stream/common/Actions.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"PersistentCache\",\n  \"resource://activity-stream/lib/PersistentCache.jsm\"\n);\nXPCOMUtils.defineLazyServiceGetters(this, {\n  gUUIDGenerator: [\"@mozilla.org/uuid-generator;1\", \"nsIUUIDGenerator\"],\n});\n\nconst CACHE_KEY = \"discovery_stream\";\nconst LAYOUT_UPDATE_TIME = 30 * 60 * 1000; // 30 minutes\nconst STARTUP_CACHE_EXPIRE_TIME = 7 * 24 * 60 * 60 * 1000; // 1 week\nconst COMPONENT_FEEDS_UPDATE_TIME = 30 * 60 * 1000; // 30 minutes\nconst SPOCS_FEEDS_UPDATE_TIME = 30 * 60 * 1000; // 30 minutes\nconst DEFAULT_RECS_EXPIRE_TIME = 60 * 60 * 1000; // 1 hour\nconst MIN_DOMAIN_AFFINITIES_UPDATE_TIME = 12 * 60 * 60 * 1000; // 12 hours\nconst MAX_LIFETIME_CAP = 500; // Guard against misconfiguration on the server\nconst DEFAULT_MAX_HISTORY_QUERY_RESULTS = 1000;\nconst FETCH_TIMEOUT = 45 * 1000;\nconst PREF_CONFIG = \"discoverystream.config\";\nconst PREF_ENDPOINTS = \"discoverystream.endpoints\";\nconst PREF_IMPRESSION_ID = \"browser.newtabpage.activity-stream.impressionId\";\nconst PREF_ENABLED = \"discoverystream.enabled\";\nconst PREF_HARDCODED_BASIC_LAYOUT = \"discoverystream.hardcoded-basic-layout\";\nconst PREF_SPOCS_ENDPOINT = \"discoverystream.spocs-endpoint\";\nconst PREF_LANG_LAYOUT_CONFIG = \"discoverystream.lang-layout-config\";\nconst PREF_TOPSTORIES = \"feeds.section.topstories\";\nconst PREF_SPOCS_CLEAR_ENDPOINT = \"discoverystream.endpointSpocsClear\";\nconst PREF_SHOW_SPONSORED = \"showSponsored\";\nconst PREF_SPOC_IMPRESSIONS = \"discoverystream.spoc.impressions\";\nconst PREF_FLIGHT_BLOCKS = \"discoverystream.flight.blocks\";\nconst PREF_REC_IMPRESSIONS = \"discoverystream.rec.impressions\";\n\nlet getHardcodedLayout;\n\nthis.DiscoveryStreamFeed = class DiscoveryStreamFeed {\n  constructor() {\n    // Internal state for checking if we've intialized all our data\n    this.loaded = false;\n\n    // Persistent cache for remote endpoint data.\n    this.cache = new PersistentCache(CACHE_KEY, true);\n    this.locale = Services.locale.appLocaleAsLangTag;\n    this._impressionId = this.getOrCreateImpressionId();\n    // Internal in-memory cache for parsing json prefs.\n    this._prefCache = {};\n  }\n\n  getOrCreateImpressionId() {\n    let impressionId = Services.prefs.getCharPref(PREF_IMPRESSION_ID, \"\");\n    if (!impressionId) {\n      impressionId = String(gUUIDGenerator.generateUUID());\n      Services.prefs.setCharPref(PREF_IMPRESSION_ID, impressionId);\n    }\n    return impressionId;\n  }\n\n  /**\n   * Send SPOCS Fill telemetry.\n   * @param {object} filteredItems An object keyed on filter reasons, and the value\n   *                 is a list of SPOCS.\n   *                 reasons: blocked_by_user, frequency_cap, below_min_score, flight_duplicate\n   * @param {boolean} fullRecalc A boolean indicating if it's a full recalculation.\n   *                  Calling `loadSpocs` will be treated as a full recalculation.\n   *                  Whereas responding the action \"DISCOVERY_STREAM_SPOC_IMPRESSION\"\n   *                  is not a full recalculation.\n   */\n  _sendSpocsFill(filteredItems, fullRecalc) {\n    const full_recalc = fullRecalc ? 1 : 0;\n    const spocsFill = [];\n    for (const [reason, items] of Object.entries(filteredItems)) {\n      items.forEach(item => {\n        // Only send SPOCS (i.e. it has a flight_id)\n        if (item.flight_id) {\n          spocsFill.push({ reason, full_recalc, id: item.id, displayed: 0 });\n        }\n      });\n    }\n\n    if (spocsFill.length) {\n      this.store.dispatch(\n        ac.DiscoveryStreamSpocsFill({ spoc_fills: spocsFill })\n      );\n    }\n  }\n\n  finalLayoutEndpoint(url, apiKey) {\n    if (url.includes(\"$apiKey\") && !apiKey) {\n      throw new Error(\n        `Layout Endpoint - An API key was specified but none configured: ${url}`\n      );\n    }\n    return url.replace(\"$apiKey\", apiKey);\n  }\n\n  get config() {\n    if (this._prefCache.config) {\n      return this._prefCache.config;\n    }\n    try {\n      this._prefCache.config = JSON.parse(\n        this.store.getState().Prefs.values[PREF_CONFIG]\n      );\n      const layoutUrl = this._prefCache.config.layout_endpoint;\n\n      const apiKeyPref = this._prefCache.config.api_key_pref;\n      if (layoutUrl && apiKeyPref) {\n        const apiKey = Services.prefs.getCharPref(apiKeyPref, \"\");\n        this._prefCache.config.layout_endpoint = this.finalLayoutEndpoint(\n          layoutUrl,\n          apiKey\n        );\n      }\n    } catch (e) {\n      // istanbul ignore next\n      this._prefCache.config = {};\n      // istanbul ignore next\n      Cu.reportError(\n        `Could not parse preference. Try resetting ${PREF_CONFIG} in about:config. ${e}`\n      );\n    }\n    this._prefCache.config.enabled =\n      this._prefCache.config.enabled &&\n      this.store.getState().Prefs.values[PREF_ENABLED];\n\n    return this._prefCache.config;\n  }\n\n  resetConfigDefauts() {\n    this.store.dispatch({\n      type: at.CLEAR_PREF,\n      data: {\n        name: PREF_CONFIG,\n      },\n    });\n  }\n\n  get showSpocs() {\n    // Combine user-set sponsored opt-out with Mozilla-set config\n    return (\n      this.store.getState().Prefs.values[PREF_SHOW_SPONSORED] &&\n      this.config.show_spocs\n    );\n  }\n\n  get personalized() {\n    return this.config.personalized;\n  }\n\n  setupPrefs() {\n    // Send the initial state of the pref on our reducer\n    this.store.dispatch(\n      ac.BroadcastToContent({\n        type: at.DISCOVERY_STREAM_CONFIG_SETUP,\n        data: this.config,\n      })\n    );\n  }\n\n  uninitPrefs() {\n    // Reset in-memory cache\n    this._prefCache = {};\n  }\n\n  async fetchFromEndpoint(rawEndpoint, options = {}) {\n    if (!rawEndpoint) {\n      Cu.reportError(\"Tried to fetch endpoint but none was configured.\");\n      return null;\n    }\n\n    const apiKeyPref = this._prefCache.config.api_key_pref;\n    const apiKey = Services.prefs.getCharPref(apiKeyPref, \"\");\n\n    // The server somtimes returns this value already replaced, but we try this for two reasons:\n    // 1. Layout endpoints are not from the server.\n    // 2. Hardcoded layouts don't have this already done for us.\n    const endpoint = rawEndpoint\n      .replace(\"$apiKey\", apiKey)\n      .replace(\"$locale\", this.locale);\n\n    try {\n      // Make sure the requested endpoint is allowed\n      const allowed = this.store\n        .getState()\n        .Prefs.values[PREF_ENDPOINTS].split(\",\");\n      if (!allowed.some(prefix => endpoint.startsWith(prefix))) {\n        throw new Error(`Not one of allowed prefixes (${allowed})`);\n      }\n\n      const controller = new AbortController();\n      const { signal } = controller;\n\n      const fetchPromise = fetch(endpoint, {\n        ...options,\n        credentials: \"omit\",\n        signal,\n      });\n      // istanbul ignore next\n      const timeoutId = setTimeout(() => {\n        controller.abort();\n      }, FETCH_TIMEOUT);\n\n      const response = await fetchPromise;\n      if (!response.ok) {\n        throw new Error(`Unexpected status (${response.status})`);\n      }\n      clearTimeout(timeoutId);\n      return response.json();\n    } catch (error) {\n      Cu.reportError(`Failed to fetch ${endpoint}: ${error.message}`);\n    }\n    return null;\n  }\n\n  /**\n   * Returns true if data in the cache for a particular key has expired or is missing.\n   * @param {object} cachedData data returned from cache.get()\n   * @param {string} key a cache key\n   * @param {string?} url for \"feed\" only, the URL of the feed.\n   * @param {boolean} is this check done at initial browser load\n   */\n  isExpired({ cachedData, key, url, isStartup }) {\n    const { layout, spocs, feeds } = cachedData;\n    const updateTimePerComponent = {\n      layout: LAYOUT_UPDATE_TIME,\n      spocs: SPOCS_FEEDS_UPDATE_TIME,\n      feed: COMPONENT_FEEDS_UPDATE_TIME,\n    };\n    const EXPIRATION_TIME = isStartup\n      ? STARTUP_CACHE_EXPIRE_TIME\n      : updateTimePerComponent[key];\n    switch (key) {\n      case \"layout\":\n        // This never needs to expire, as it's not expected to change.\n        if (this.config.hardcoded_layout) {\n          return false;\n        }\n        return !layout || !(Date.now() - layout.lastUpdated < EXPIRATION_TIME);\n      case \"spocs\":\n        return !spocs || !(Date.now() - spocs.lastUpdated < EXPIRATION_TIME);\n      case \"feed\":\n        return (\n          !feeds ||\n          !feeds[url] ||\n          !(Date.now() - feeds[url].lastUpdated < EXPIRATION_TIME)\n        );\n      default:\n        // istanbul ignore next\n        throw new Error(`${key} is not a valid key`);\n    }\n  }\n\n  async _checkExpirationPerComponent() {\n    const cachedData = (await this.cache.get()) || {};\n    const { feeds } = cachedData;\n    return {\n      layout: this.isExpired({ cachedData, key: \"layout\" }),\n      spocs: this.isExpired({ cachedData, key: \"spocs\" }),\n      feeds:\n        !feeds ||\n        Object.keys(feeds).some(url =>\n          this.isExpired({ cachedData, key: \"feed\", url })\n        ),\n    };\n  }\n\n  /**\n   * Returns true if any data for the cached endpoints has expired or is missing.\n   */\n  async checkIfAnyCacheExpired() {\n    const expirationPerComponent = await this._checkExpirationPerComponent();\n    return (\n      expirationPerComponent.layout ||\n      expirationPerComponent.spocs ||\n      expirationPerComponent.feeds\n    );\n  }\n\n  async fetchLayout(isStartup) {\n    const cachedData = (await this.cache.get()) || {};\n    let { layout } = cachedData;\n    if (this.isExpired({ cachedData, key: \"layout\", isStartup })) {\n      const start = perfService.absNow();\n      const layoutResponse = await this.fetchFromEndpoint(\n        this.config.layout_endpoint\n      );\n      if (layoutResponse && layoutResponse.layout) {\n        this.layoutRequestTime = Math.round(perfService.absNow() - start);\n        layout = {\n          lastUpdated: Date.now(),\n          spocs: layoutResponse.spocs,\n          layout: layoutResponse.layout,\n          status: \"success\",\n        };\n\n        await this.cache.set(\"layout\", layout);\n      } else {\n        Cu.reportError(\"No response for response.layout prop\");\n      }\n    }\n    return layout;\n  }\n\n  updatePlacements(sendUpdate, layout) {\n    const placements = [];\n    const placementsMap = {};\n    for (const row of layout.filter(r => r.components && r.components.length)) {\n      for (const component of row.components) {\n        if (component.placement) {\n          // Throw away any dupes for the request.\n          if (!placementsMap[component.placement.name]) {\n            placementsMap[component.placement.name] = component.placement;\n            placements.push(component.placement);\n          }\n        }\n      }\n    }\n    if (placements.length) {\n      sendUpdate({\n        type: at.DISCOVERY_STREAM_SPOCS_PLACEMENTS,\n        data: { placements },\n      });\n    }\n  }\n\n  async loadLayout(sendUpdate, isStartup) {\n    let layoutResp = {};\n    let url = \"\";\n    if (!this.config.hardcoded_layout) {\n      layoutResp = await this.fetchLayout(isStartup);\n    }\n\n    if (!layoutResp || !layoutResp.layout) {\n      const langLayoutConfig =\n        this.store.getState().Prefs.values[PREF_LANG_LAYOUT_CONFIG] || \"\";\n\n      const isBasic =\n        this.config.hardcoded_basic_layout ||\n        this.store.getState().Prefs.values[PREF_HARDCODED_BASIC_LAYOUT] ||\n        !langLayoutConfig\n          .split(\",\")\n          .find(lang => this.locale.startsWith(lang.trim()));\n\n      // Set a hardcoded layout if one is needed.\n      // Changing values in this layout in memory object is unnecessary.\n      layoutResp = getHardcodedLayout(isBasic);\n    }\n\n    sendUpdate({\n      type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,\n      data: layoutResp,\n    });\n\n    if (layoutResp.spocs) {\n      url =\n        this.store.getState().Prefs.values[PREF_SPOCS_ENDPOINT] ||\n        this.config.spocs_endpoint ||\n        layoutResp.spocs.url;\n\n      if (\n        url &&\n        url !== this.store.getState().DiscoveryStream.spocs.spocs_endpoint\n      ) {\n        sendUpdate({\n          type: at.DISCOVERY_STREAM_SPOCS_ENDPOINT,\n          data: {\n            url,\n            spocs_per_domain: layoutResp.spocs.spocs_per_domain,\n          },\n        });\n        this.updatePlacements(sendUpdate, layoutResp.layout);\n      }\n    }\n  }\n\n  /**\n   * buildFeedPromise - Adds the promise result to newFeeds and\n   *                    pushes a promise to newsFeedsPromises.\n   * @param {Object} Has both newFeedsPromises (Array) and newFeeds (Object)\n   * @param {Boolean} isStartup We have different cache handling for startup.\n   * @returns {Function} We return a function so we can contain\n   *                     the scope for isStartup and the promises object.\n   *                     Combines feed results and promises for each component with a feed.\n   */\n  buildFeedPromise({ newFeedsPromises, newFeeds }, isStartup, sendUpdate) {\n    return component => {\n      const { url } = component.feed;\n\n      if (!newFeeds[url]) {\n        // We initially stub this out so we don't fetch dupes,\n        // we then fill in with the proper object inside the promise.\n        newFeeds[url] = {};\n        const feedPromise = this.getComponentFeed(url, isStartup);\n\n        feedPromise\n          .then(feed => {\n            newFeeds[url] = this.filterRecommendations(feed);\n            sendUpdate({\n              type: at.DISCOVERY_STREAM_FEED_UPDATE,\n              data: {\n                feed: newFeeds[url],\n                url,\n              },\n            });\n\n            // We grab affinities off the first feed for the moment.\n            // Ideally this would be returned from the server on the layout,\n            // or from another endpoint.\n            if (!this.affinities) {\n              const { settings } = feed.data;\n              this.affinities = {\n                timeSegments: settings.timeSegments,\n                parameterSets: settings.domainAffinityParameterSets,\n                maxHistoryQueryResults:\n                  settings.maxHistoryQueryResults ||\n                  DEFAULT_MAX_HISTORY_QUERY_RESULTS,\n                version: settings.version,\n              };\n            }\n          })\n          .catch(\n            /* istanbul ignore next */ error => {\n              Cu.reportError(\n                `Error trying to load component feed ${url}: ${error}`\n              );\n            }\n          );\n        newFeedsPromises.push(feedPromise);\n      }\n    };\n  }\n\n  filterRecommendations(feed) {\n    if (\n      feed &&\n      feed.data &&\n      feed.data.recommendations &&\n      feed.data.recommendations.length\n    ) {\n      const { data: recommendations } = this.filterBlocked(\n        feed.data.recommendations\n      );\n      return {\n        ...feed,\n        data: {\n          ...feed.data,\n          recommendations,\n        },\n      };\n    }\n    return feed;\n  }\n\n  /**\n   * reduceFeedComponents - Filters out components with no feeds, and combines\n   *                        all feeds on this component with the feeds from other components.\n   * @param {Boolean} isStartup We have different cache handling for startup.\n   * @returns {Function} We return a function so we can contain the scope for isStartup.\n   *                     Reduces feeds into promises and feed data.\n   */\n  reduceFeedComponents(isStartup, sendUpdate) {\n    return (accumulator, row) => {\n      row.components\n        .filter(component => component && component.feed)\n        .forEach(this.buildFeedPromise(accumulator, isStartup, sendUpdate));\n      return accumulator;\n    };\n  }\n\n  /**\n   * buildFeedPromises - Filters out rows with no components,\n   *                     and gets us a promise for each unique feed.\n   * @param {Object} layout This is the Discovery Stream layout object.\n   * @param {Boolean} isStartup We have different cache handling for startup.\n   * @returns {Object} An object with newFeedsPromises (Array) and newFeeds (Object),\n   *                   we can Promise.all newFeedsPromises to get completed data in newFeeds.\n   */\n  buildFeedPromises(layout, isStartup, sendUpdate) {\n    const initialData = {\n      newFeedsPromises: [],\n      newFeeds: {},\n    };\n    return layout\n      .filter(row => row && row.components)\n      .reduce(this.reduceFeedComponents(isStartup, sendUpdate), initialData);\n  }\n\n  async loadComponentFeeds(sendUpdate, isStartup) {\n    const { DiscoveryStream } = this.store.getState();\n\n    if (!DiscoveryStream || !DiscoveryStream.layout) {\n      return;\n    }\n\n    // Reset the flag that indicates whether or not at least one API request\n    // was issued to fetch the component feed in `getComponentFeed()`.\n    this.componentFeedFetched = false;\n    const start = perfService.absNow();\n    const { newFeedsPromises, newFeeds } = this.buildFeedPromises(\n      DiscoveryStream.layout,\n      isStartup,\n      sendUpdate\n    );\n\n    // Each promise has a catch already built in, so no need to catch here.\n    await Promise.all(newFeedsPromises);\n\n    if (this.componentFeedFetched) {\n      this.cleanUpTopRecImpressionPref(newFeeds);\n      this.componentFeedRequestTime = Math.round(perfService.absNow() - start);\n    }\n    await this.cache.set(\"feeds\", newFeeds);\n    sendUpdate({\n      type: at.DISCOVERY_STREAM_FEEDS_UPDATE,\n    });\n  }\n\n  placementsForEach(callback) {\n    const { placements } = this.store.getState().DiscoveryStream.spocs;\n    // Backwards comp for before we had placements, assume just a single spocs placement.\n    if (!placements || !placements.length) {\n      [{ name: \"spocs\" }].forEach(callback);\n    } else {\n      placements.forEach(callback);\n    }\n  }\n\n  async loadSpocs(sendUpdate, isStartup) {\n    const cachedData = (await this.cache.get()) || {};\n    let spocsState;\n\n    const { placements } = this.store.getState().DiscoveryStream.spocs;\n\n    if (this.showSpocs) {\n      spocsState = cachedData.spocs;\n      if (this.isExpired({ cachedData, key: \"spocs\", isStartup })) {\n        const endpoint = this.store.getState().DiscoveryStream.spocs\n          .spocs_endpoint;\n        const start = perfService.absNow();\n\n        const headers = new Headers();\n        headers.append(\"content-type\", \"application/json\");\n\n        const apiKeyPref = this._prefCache.config.api_key_pref;\n        const apiKey = Services.prefs.getCharPref(apiKeyPref, \"\");\n\n        const spocsResponse = await this.fetchFromEndpoint(endpoint, {\n          method: \"POST\",\n          headers,\n          body: JSON.stringify({\n            pocket_id: this._impressionId,\n            version: 1,\n            consumer_key: apiKey,\n            ...(placements.length ? { placements } : {}),\n          }),\n        });\n\n        if (spocsResponse) {\n          this.spocsRequestTime = Math.round(perfService.absNow() - start);\n          spocsState = {\n            lastUpdated: Date.now(),\n            spocs: {\n              ...spocsResponse,\n            },\n          };\n\n          this.cleanUpFlightImpressionPref(spocsState.spocs);\n          await this.cache.set(\"spocs\", spocsState);\n        } else {\n          Cu.reportError(\"No response for spocs_endpoint prop\");\n        }\n      }\n    }\n\n    // Use good data if we have it, otherwise nothing.\n    // We can have no data if spocs set to off.\n    // We can have no data if request fails and there is no good cache.\n    // We want to send an update spocs or not, so client can render something.\n    spocsState =\n      spocsState && spocsState.spocs\n        ? spocsState\n        : {\n            lastUpdated: Date.now(),\n            spocs: {},\n          };\n\n    let frequencyCapped = [];\n    let blockedItems = [];\n    let belowMinScore = [];\n    let flightDupes = [];\n    this.placementsForEach(placement => {\n      const freshSpocs = spocsState.spocs[placement.name];\n\n      if (!freshSpocs || !freshSpocs.length) {\n        return;\n      }\n\n      // Migrate flight_id\n      const { data: migratedSpocs } = this.migrateFlightId(freshSpocs);\n\n      const { data: capResult, filtered: caps } = this.frequencyCapSpocs(\n        migratedSpocs\n      );\n      frequencyCapped = [...frequencyCapped, ...caps];\n\n      const { data: blockedResults, filtered: blocks } = this.filterBlocked(\n        capResult\n      );\n      blockedItems = [...blockedItems, ...blocks];\n\n      let { data: transformResult, filtered: transformFilter } = this.transform(\n        blockedResults\n      );\n      let {\n        below_min_score: minScoreFilter,\n        flight_duplicate: dupes,\n      } = transformFilter;\n      belowMinScore = [...belowMinScore, ...minScoreFilter];\n      flightDupes = [...flightDupes, ...dupes];\n\n      spocsState.spocs = {\n        ...spocsState.spocs,\n        [placement.name]: transformResult,\n      };\n    });\n\n    sendUpdate({\n      type: at.DISCOVERY_STREAM_SPOCS_UPDATE,\n      data: {\n        lastUpdated: spocsState.lastUpdated,\n        spocs: spocsState.spocs,\n      },\n    });\n    // TODO make sure this works in other places we use it.\n    // TODO make sure to also validate all of these that they still contain the right ites in the array.\n    this._sendSpocsFill(\n      {\n        frequency_cap: frequencyCapped,\n        blocked_by_user: blockedItems,\n        below_min_score: belowMinScore,\n        flight_duplicate: flightDupes,\n      },\n      true\n    );\n  }\n\n  async clearSpocs() {\n    const endpoint = this.store.getState().Prefs.values[\n      PREF_SPOCS_CLEAR_ENDPOINT\n    ];\n    if (!endpoint) {\n      return;\n    }\n    const headers = new Headers();\n    headers.append(\"content-type\", \"application/json\");\n\n    await this.fetchFromEndpoint(endpoint, {\n      method: \"DELETE\",\n      headers,\n      body: JSON.stringify({\n        pocket_id: this._impressionId,\n      }),\n    });\n  }\n\n  async loadAffinityScoresCache() {\n    const cachedData = (await this.cache.get()) || {};\n    const { affinities } = cachedData;\n    if (this.personalized && affinities && affinities.scores) {\n      this.affinityProvider = new UserDomainAffinityProvider(\n        affinities.timeSegments,\n        affinities.parameterSets,\n        affinities.maxHistoryQueryResults,\n        affinities.version,\n        affinities.scores\n      );\n\n      this.domainAffinitiesLastUpdated = affinities._timestamp;\n    }\n  }\n\n  updateDomainAffinityScores() {\n    if (\n      !this.personalized ||\n      !this.affinities ||\n      !this.affinities.parameterSets ||\n      Date.now() - this.domainAffinitiesLastUpdated <\n        MIN_DOMAIN_AFFINITIES_UPDATE_TIME\n    ) {\n      return;\n    }\n\n    this.affinityProvider = new UserDomainAffinityProvider(\n      this.affinities.timeSegments,\n      this.affinities.parameterSets,\n      this.affinities.maxHistoryQueryResults,\n      this.affinities.version,\n      undefined\n    );\n\n    const affinities = this.affinityProvider.getAffinities();\n    this.domainAffinitiesLastUpdated = Date.now();\n    affinities._timestamp = this.domainAffinitiesLastUpdated;\n    this.cache.set(\"affinities\", affinities);\n  }\n\n  observe(subject, topic, data) {\n    switch (topic) {\n      case \"idle-daily\":\n        this.updateDomainAffinityScores();\n        break;\n    }\n  }\n\n  scoreItems(items) {\n    const filtered = [];\n    const data = items\n      .map(item => this.scoreItem(item))\n      // Remove spocs that are scored too low.\n      .filter(s => {\n        if (s.score >= s.min_score) {\n          return true;\n        }\n        filtered.push(s);\n        return false;\n      })\n      // Sort by highest scores.\n      .sort((a, b) => b.score - a.score);\n    return { data, filtered };\n  }\n\n  scoreItem(item) {\n    item.score = item.item_score;\n    item.min_score = item.min_score || 0;\n    if (item.score !== 0 && !item.score) {\n      item.score = 1;\n    }\n    if (this.personalized && this.affinityProvider) {\n      const scoreResult = this.affinityProvider.calculateItemRelevanceScore(\n        item\n      );\n      if (scoreResult === 0 || scoreResult) {\n        item.score = scoreResult;\n      }\n    }\n    return item;\n  }\n\n  filterBlocked(data) {\n    const filtered = [];\n    if (data && data.length) {\n      let flights = this.readDataPref(PREF_FLIGHT_BLOCKS);\n      const filteredItems = data.filter(item => {\n        const blocked =\n          NewTabUtils.blockedLinks.isBlocked({ url: item.url }) ||\n          flights[item.flight_id];\n        if (blocked) {\n          filtered.push(item);\n        }\n        return !blocked;\n      });\n      return {\n        data: filteredItems,\n        filtered,\n      };\n    }\n    return { data, filtered };\n  }\n\n  transform(spocs) {\n    if (spocs && spocs.length) {\n      const spocsPerDomain =\n        this.store.getState().DiscoveryStream.spocs.spocs_per_domain || 1;\n      const flightMap = {};\n      const flightDuplicates = [];\n\n      // This order of operations is intended.\n      // scoreItems must be first because it creates this.score.\n      const { data: items, filtered: belowMinScoreItems } = this.scoreItems(\n        spocs\n      );\n      // This removes flight dupes.\n      // We do this only after scoring and sorting because that way\n      // we can keep the first item we see, and end up keeping the highest scored.\n      const newSpocs = items.filter(s => {\n        if (!flightMap[s.flight_id]) {\n          flightMap[s.flight_id] = 1;\n          return true;\n        } else if (flightMap[s.flight_id] < spocsPerDomain) {\n          flightMap[s.flight_id]++;\n          return true;\n        }\n        flightDuplicates.push(s);\n        return false;\n      });\n      return {\n        data: newSpocs,\n        filtered: {\n          below_min_score: belowMinScoreItems,\n          flight_duplicate: flightDuplicates,\n        },\n      };\n    }\n    return {\n      data: spocs,\n      filtered: {\n        below_min_score: [],\n        flight_duplicate: [],\n      },\n    };\n  }\n\n  // For backwards compatibility, older spoc endpoint don't have flight_id,\n  // but instead had campaign_id we can use\n  //\n  // @param {Object} data  An object that might have a SPOCS array.\n  // @returns {Object} An object with a property `data` as the result.\n  migrateFlightId(spocs) {\n    if (spocs && spocs.length) {\n      return {\n        data: spocs.map(s => {\n          return {\n            ...s,\n            ...(s.flight_id || s.campaign_id\n              ? {\n                  flight_id: s.flight_id || s.campaign_id,\n                }\n              : {}),\n            ...(s.caps\n              ? {\n                  caps: {\n                    ...s.caps,\n                    flight: s.caps.flight || s.caps.campaign,\n                  },\n                }\n              : {}),\n          };\n        }),\n      };\n    }\n    return { data: spocs };\n  }\n\n  // Filter spocs based on frequency caps\n  //\n  // @param {Object} data  An object that might have a SPOCS array.\n  // @returns {Object} An object with a property `data` as the result, and a property\n  //                   `filterItems` as the frequency capped items.\n  frequencyCapSpocs(spocs) {\n    if (spocs && spocs.length) {\n      const impressions = this.readDataPref(PREF_SPOC_IMPRESSIONS);\n      const caps = [];\n      const result = spocs.filter(s => {\n        const isBelow = this.isBelowFrequencyCap(impressions, s);\n        if (!isBelow) {\n          caps.push(s);\n        }\n        return isBelow;\n      });\n      // send caps to redux if any.\n      if (caps.length) {\n        this.store.dispatch({\n          type: at.DISCOVERY_STREAM_SPOCS_CAPS,\n          data: caps,\n        });\n      }\n      return { data: result, filtered: caps };\n    }\n    return { data: spocs, filtered: [] };\n  }\n\n  // Frequency caps are based on flight, which may include multiple spocs.\n  // We currently support two types of frequency caps:\n  // - lifetime: Indicates how many times spocs from a flight can be shown in total\n  // - period: Indicates how many times spocs from a flight can be shown within a period\n  //\n  // So, for example, the feed configuration below defines that for flight 1 no more\n  // than 5 spocs can be shown in total, and no more than 2 per hour.\n  // \"flight_id\": 1,\n  // \"caps\": {\n  //  \"lifetime\": 5,\n  //  \"flight\": {\n  //    \"count\": 2,\n  //    \"period\": 3600\n  //  }\n  // }\n  isBelowFrequencyCap(impressions, spoc) {\n    const flightImpressions = impressions[spoc.flight_id];\n    if (!flightImpressions) {\n      return true;\n    }\n\n    const lifetime = spoc.caps && spoc.caps.lifetime;\n\n    const lifeTimeCap = Math.min(\n      lifetime || MAX_LIFETIME_CAP,\n      MAX_LIFETIME_CAP\n    );\n    const lifeTimeCapExceeded = flightImpressions.length >= lifeTimeCap;\n    if (lifeTimeCapExceeded) {\n      return false;\n    }\n\n    const flightCap = spoc.caps && spoc.caps.flight;\n    if (flightCap) {\n      const flightCapExceeded =\n        flightImpressions.filter(i => Date.now() - i < flightCap.period * 1000)\n          .length >= flightCap.count;\n      return !flightCapExceeded;\n    }\n    return true;\n  }\n\n  async retryFeed(feed) {\n    const { url } = feed;\n    const result = await this.getComponentFeed(url);\n    const newFeed = this.filterRecommendations(result);\n    this.store.dispatch(\n      ac.BroadcastToContent({\n        type: at.DISCOVERY_STREAM_FEED_UPDATE,\n        data: {\n          feed: newFeed,\n          url,\n        },\n      })\n    );\n  }\n\n  async getComponentFeed(feedUrl, isStartup) {\n    const cachedData = (await this.cache.get()) || {};\n    const { feeds } = cachedData;\n\n    let feed = feeds ? feeds[feedUrl] : null;\n    if (this.isExpired({ cachedData, key: \"feed\", url: feedUrl, isStartup })) {\n      const feedResponse = await this.fetchFromEndpoint(feedUrl);\n      if (feedResponse) {\n        const { data: scoredItems } = this.scoreItems(\n          feedResponse.recommendations\n        );\n        const { recsExpireTime } = feedResponse.settings;\n        const recommendations = this.rotate(scoredItems, recsExpireTime);\n        this.componentFeedFetched = true;\n        feed = {\n          lastUpdated: Date.now(),\n          data: {\n            settings: feedResponse.settings,\n            recommendations,\n            status: \"success\",\n          },\n        };\n      } else {\n        Cu.reportError(\"No response for feed\");\n      }\n    }\n\n    // If we have no feed at this point, both fetch and cache failed for some reason.\n    return (\n      feed || {\n        data: {\n          status: \"failed\",\n        },\n      }\n    );\n  }\n\n  /**\n   * Called at startup to update cached data in the background.\n   */\n  async _maybeUpdateCachedData() {\n    const expirationPerComponent = await this._checkExpirationPerComponent();\n    // Pass in `store.dispatch` to send the updates only to main\n    if (expirationPerComponent.layout) {\n      await this.loadLayout(this.store.dispatch);\n    }\n    if (expirationPerComponent.spocs) {\n      await this.loadSpocs(this.store.dispatch);\n    }\n    if (expirationPerComponent.feeds) {\n      await this.loadComponentFeeds(this.store.dispatch);\n    }\n  }\n\n  /**\n   * @typedef {Object} RefreshAllOptions\n   * @property {boolean} updateOpenTabs - Sends updates to open tabs immediately if true,\n   *                                      updates in background if false\n   * @property {boolean} isStartup - When the function is called at browser startup\n   *\n   * Refreshes layout, component feeds, and spocs in order if caches have expired.\n   * @param {RefreshAllOptions} options\n   */\n  async refreshAll(options = {}) {\n    const { updateOpenTabs, isStartup } = options;\n    const dispatch = updateOpenTabs\n      ? action => this.store.dispatch(ac.BroadcastToContent(action))\n      : this.store.dispatch;\n\n    this.loadAffinityScoresCache();\n    await this.loadLayout(dispatch, isStartup);\n    await Promise.all([\n      this.loadSpocs(dispatch, isStartup).catch(error =>\n        Cu.reportError(`Error trying to load spocs feeds: ${error}`)\n      ),\n      this.loadComponentFeeds(dispatch, isStartup).catch(error =>\n        Cu.reportError(`Error trying to load component feeds: ${error}`)\n      ),\n    ]);\n    if (isStartup) {\n      await this._maybeUpdateCachedData();\n    }\n  }\n\n  // We have to rotate stories on the client so that\n  // active stories are at the front of the list, followed by stories that have expired\n  // impressions i.e. have been displayed for longer than recsExpireTime.\n  rotate(recommendations, recsExpireTime) {\n    const maxImpressionAge = Math.max(\n      recsExpireTime * 1000 || DEFAULT_RECS_EXPIRE_TIME,\n      DEFAULT_RECS_EXPIRE_TIME\n    );\n    const impressions = this.readDataPref(PREF_REC_IMPRESSIONS);\n    const expired = [];\n    const active = [];\n    for (const item of recommendations) {\n      if (\n        impressions[item.id] &&\n        Date.now() - impressions[item.id] >= maxImpressionAge\n      ) {\n        expired.push(item);\n      } else {\n        active.push(item);\n      }\n    }\n    return active.concat(expired);\n  }\n\n  /**\n   * Reports the cache age in second for Discovery Stream.\n   */\n  async reportCacheAge() {\n    const cachedData = (await this.cache.get()) || {};\n    const { layout, spocs, feeds } = cachedData;\n    let cacheAge = Date.now();\n    let updated = false;\n\n    if (layout && layout.lastUpdated && layout.lastUpdated < cacheAge) {\n      updated = true;\n      cacheAge = layout.lastUpdated;\n    }\n\n    if (spocs && spocs.lastUpdated && spocs.lastUpdated < cacheAge) {\n      updated = true;\n      cacheAge = spocs.lastUpdated;\n    }\n\n    if (feeds) {\n      Object.keys(feeds).forEach(url => {\n        const feed = feeds[url];\n        if (feed.lastUpdated && feed.lastUpdated < cacheAge) {\n          updated = true;\n          cacheAge = feed.lastUpdated;\n        }\n      });\n    }\n\n    if (updated) {\n      this.store.dispatch(\n        ac.PerfEvent({\n          event: \"DS_CACHE_AGE_IN_SEC\",\n          value: Math.round((Date.now() - cacheAge) / 1000),\n        })\n      );\n    }\n  }\n\n  /**\n   * Reports various time durations when the feed is requested from endpoint for\n   * the first time. This could happen on the browser start-up, or the pref changes\n   * of discovery stream.\n   *\n   * Metrics to be reported:\n   *   - Request time for layout endpoint\n   *   - Request time for feed endpoint\n   *   - Request time for spoc endpoint\n   *   - Total request time for data completeness\n   */\n  reportRequestTime() {\n    if (this.layoutRequestTime) {\n      this.store.dispatch(\n        ac.PerfEvent({\n          event: \"LAYOUT_REQUEST_TIME\",\n          value: this.layoutRequestTime,\n        })\n      );\n    }\n    if (this.spocsRequestTime) {\n      this.store.dispatch(\n        ac.PerfEvent({\n          event: \"SPOCS_REQUEST_TIME\",\n          value: this.spocsRequestTime,\n        })\n      );\n    }\n    if (this.componentFeedRequestTime) {\n      this.store.dispatch(\n        ac.PerfEvent({\n          event: \"COMPONENT_FEED_REQUEST_TIME\",\n          value: this.componentFeedRequestTime,\n        })\n      );\n    }\n    if (this.totalRequestTime) {\n      this.store.dispatch(\n        ac.PerfEvent({\n          event: \"DS_FEED_TOTAL_REQUEST_TIME\",\n          value: this.totalRequestTime,\n        })\n      );\n    }\n  }\n\n  async enable() {\n    // Note that cache age needs to be reported prior to refreshAll.\n    await this.reportCacheAge();\n    const start = perfService.absNow();\n    await this.refreshAll({ updateOpenTabs: true, isStartup: true });\n    Services.obs.addObserver(this, \"idle-daily\");\n    this.loaded = true;\n    this.totalRequestTime = Math.round(perfService.absNow() - start);\n    this.reportRequestTime();\n  }\n\n  async reset() {\n    this.resetDataPrefs();\n    await this.resetCache();\n    if (this.loaded) {\n      Services.obs.removeObserver(this, \"idle-daily\");\n    }\n    this.resetState();\n  }\n\n  async resetCache() {\n    await this.cache.set(\"layout\", {});\n    await this.cache.set(\"feeds\", {});\n    await this.cache.set(\"spocs\", {});\n    await this.cache.set(\"affinities\", {});\n  }\n\n  resetDataPrefs() {\n    this.writeDataPref(PREF_SPOC_IMPRESSIONS, {});\n    this.writeDataPref(PREF_REC_IMPRESSIONS, {});\n    this.writeDataPref(PREF_FLIGHT_BLOCKS, {});\n  }\n\n  resetState() {\n    // Reset reducer\n    this.store.dispatch(\n      ac.BroadcastToContent({ type: at.DISCOVERY_STREAM_LAYOUT_RESET })\n    );\n    this.loaded = false;\n    this.layoutRequestTime = undefined;\n    this.spocsRequestTime = undefined;\n    this.componentFeedRequestTime = undefined;\n    this.totalRequestTime = undefined;\n  }\n\n  async onPrefChange() {\n    // We always want to clear the cache/state if the pref has changed\n    await this.reset();\n    if (this.config.enabled) {\n      // Load data from all endpoints\n      await this.enable();\n    }\n  }\n\n  recordFlightImpression(flightId) {\n    let impressions = this.readDataPref(PREF_SPOC_IMPRESSIONS);\n\n    const timeStamps = impressions[flightId] || [];\n    timeStamps.push(Date.now());\n    impressions = { ...impressions, [flightId]: timeStamps };\n\n    this.writeDataPref(PREF_SPOC_IMPRESSIONS, impressions);\n  }\n\n  recordTopRecImpressions(recId) {\n    let impressions = this.readDataPref(PREF_REC_IMPRESSIONS);\n    if (!impressions[recId]) {\n      impressions = { ...impressions, [recId]: Date.now() };\n      this.writeDataPref(PREF_REC_IMPRESSIONS, impressions);\n    }\n  }\n\n  recordBlockFlightId(flightId) {\n    const flights = this.readDataPref(PREF_FLIGHT_BLOCKS);\n    if (!flights[flightId]) {\n      flights[flightId] = 1;\n      this.writeDataPref(PREF_FLIGHT_BLOCKS, flights);\n    }\n  }\n\n  cleanUpFlightImpressionPref(data) {\n    let flightIds = [];\n    this.placementsForEach(placement => {\n      const newSpocs = data[placement.name];\n      if (!newSpocs) {\n        return;\n      }\n      flightIds = [...flightIds, ...newSpocs.map(s => `${s.flight_id}`)];\n    });\n    if (flightIds && flightIds.length) {\n      this.cleanUpImpressionPref(\n        id => !flightIds.includes(id),\n        PREF_SPOC_IMPRESSIONS\n      );\n    }\n  }\n\n  // Clean up rec impression pref by removing all stories that are no\n  // longer part of the response.\n  cleanUpTopRecImpressionPref(newFeeds) {\n    // Need to build a single list of stories.\n    const activeStories = Object.keys(newFeeds)\n      .filter(currentValue => newFeeds[currentValue].data)\n      .reduce((accumulator, currentValue) => {\n        const { recommendations } = newFeeds[currentValue].data;\n        return accumulator.concat(recommendations.map(i => `${i.id}`));\n      }, []);\n    this.cleanUpImpressionPref(\n      id => !activeStories.includes(id),\n      PREF_REC_IMPRESSIONS\n    );\n  }\n\n  writeDataPref(pref, impressions) {\n    this.store.dispatch(ac.SetPref(pref, JSON.stringify(impressions)));\n  }\n\n  readDataPref(pref) {\n    const prefVal = this.store.getState().Prefs.values[pref];\n    return prefVal ? JSON.parse(prefVal) : {};\n  }\n\n  cleanUpImpressionPref(isExpired, pref) {\n    const impressions = this.readDataPref(pref);\n    let changed = false;\n\n    Object.keys(impressions).forEach(id => {\n      if (isExpired(id)) {\n        changed = true;\n        delete impressions[id];\n      }\n    });\n\n    if (changed) {\n      this.writeDataPref(pref, impressions);\n    }\n  }\n\n  async onAction(action) {\n    switch (action.type) {\n      case at.INIT:\n        // During the initialization of Firefox:\n        // 1. Set-up listeners and initialize the redux state for config;\n        this.setupPrefs();\n        // 2. If config.enabled is true, start loading data.\n        if (this.config.enabled) {\n          await this.enable();\n        }\n        break;\n      case at.SYSTEM_TICK:\n        // Only refresh if we loaded once in .enable()\n        if (\n          this.config.enabled &&\n          this.loaded &&\n          (await this.checkIfAnyCacheExpired())\n        ) {\n          await this.refreshAll({ updateOpenTabs: false });\n        }\n        break;\n      case at.DISCOVERY_STREAM_CONFIG_SET_VALUE:\n        // Use the original string pref to then set a value instead of\n        // this.config which has some modifications\n        this.store.dispatch(\n          ac.SetPref(\n            PREF_CONFIG,\n            JSON.stringify({\n              ...JSON.parse(this.store.getState().Prefs.values[PREF_CONFIG]),\n              [action.data.name]: action.data.value,\n            })\n          )\n        );\n        break;\n      case at.DISCOVERY_STREAM_CONFIG_RESET_DEFAULTS:\n        this.resetConfigDefauts();\n        break;\n      case at.DISCOVERY_STREAM_RETRY_FEED:\n        this.retryFeed(action.data.feed);\n        break;\n      case at.DISCOVERY_STREAM_CONFIG_CHANGE:\n        // When the config pref changes, load or unload data as needed.\n        await this.onPrefChange();\n        break;\n      case at.DISCOVERY_STREAM_IMPRESSION_STATS:\n        if (\n          action.data.tiles &&\n          action.data.tiles[0] &&\n          action.data.tiles[0].id\n        ) {\n          this.recordTopRecImpressions(action.data.tiles[0].id);\n        }\n        break;\n      case at.DISCOVERY_STREAM_SPOC_IMPRESSION:\n        if (this.showSpocs) {\n          this.recordFlightImpression(action.data.flightId);\n\n          // Apply frequency capping to SPOCs in the redux store, only update the\n          // store if the SPOCs are changed.\n          const spocsState = this.store.getState().DiscoveryStream.spocs;\n\n          let frequencyCapped = [];\n          this.placementsForEach(placement => {\n            const freshSpocs = spocsState.data[placement.name];\n            if (!freshSpocs) {\n              return;\n            }\n\n            const { data: newSpocs, filtered } = this.frequencyCapSpocs(\n              freshSpocs\n            );\n            frequencyCapped = [...frequencyCapped, ...filtered];\n\n            spocsState.data = {\n              ...spocsState.data,\n              [placement.name]: newSpocs,\n            };\n          });\n          if (frequencyCapped.length) {\n            this.store.dispatch(\n              ac.AlsoToPreloaded({\n                type: at.DISCOVERY_STREAM_SPOCS_UPDATE,\n                data: {\n                  lastUpdated: spocsState.lastUpdated,\n                  spocs: spocsState.data,\n                },\n              })\n            );\n            this._sendSpocsFill({ frequency_cap: frequencyCapped }, false);\n          }\n        }\n        break;\n      // This is fired from the browser, it has no concept of spocs, flight or pocket.\n      // We match the blocked url with our available spoc urls to see if there is a match.\n      // I suspect we *could* instead do this in BLOCK_URL but I'm not sure.\n      case at.PLACES_LINK_BLOCKED:\n        if (this.showSpocs) {\n          const spocsState = this.store.getState().DiscoveryStream.spocs;\n          let spocsList = [];\n          this.placementsForEach(placement => {\n            const spocs = spocsState.data[placement.name];\n            if (spocs && spocs.length) {\n              spocsList = [...spocsList, ...spocs];\n            }\n          });\n          const filtered = spocsList.filter(s => s.url === action.data.url);\n          if (filtered.length) {\n            this._sendSpocsFill({ blocked_by_user: filtered }, false);\n\n            // If we're blocking a spoc, we want a slightly different treatment for open tabs.\n            // AlsoToPreloaded updates the source data and preloaded tabs with a new spoc.\n            // BroadcastToContent updates open tabs with a non spoc instead of a new spoc.\n            this.store.dispatch(\n              ac.AlsoToPreloaded({\n                type: at.DISCOVERY_STREAM_LINK_BLOCKED,\n                data: action.data,\n              })\n            );\n            this.store.dispatch(\n              ac.BroadcastToContent({\n                type: at.DISCOVERY_STREAM_SPOC_BLOCKED,\n                data: action.data,\n              })\n            );\n            break;\n          }\n        }\n        this.store.dispatch(\n          ac.BroadcastToContent({\n            type: at.DISCOVERY_STREAM_LINK_BLOCKED,\n            data: action.data,\n          })\n        );\n        break;\n      case at.UNINIT:\n        // When this feed is shutting down:\n        this.uninitPrefs();\n        break;\n      case at.BLOCK_URL: {\n        // If we block a story that also has a flight_id\n        // we want to record that as blocked too.\n        // This is because a single flight might have slightly different urls.\n        const { flight_id } = action.data;\n        if (flight_id) {\n          this.recordBlockFlightId(flight_id);\n        }\n        break;\n      }\n      case at.PREF_CHANGED:\n        switch (action.data.name) {\n          case PREF_CONFIG:\n          case PREF_ENABLED:\n          case PREF_HARDCODED_BASIC_LAYOUT:\n          case PREF_SPOCS_ENDPOINT:\n          case PREF_LANG_LAYOUT_CONFIG:\n            // Clear the cached config and broadcast the newly computed value\n            this._prefCache.config = null;\n            this.store.dispatch(\n              ac.BroadcastToContent({\n                type: at.DISCOVERY_STREAM_CONFIG_CHANGE,\n                data: this.config,\n              })\n            );\n            break;\n          case PREF_TOPSTORIES:\n            if (!action.data.value) {\n              // Ensure we delete any remote data potentially related to spocs.\n              this.clearSpocs();\n            }\n            break;\n          // Check if spocs was disabled. Remove them if they were.\n          case PREF_SHOW_SPONSORED:\n            if (!action.data.value) {\n              // Ensure we delete any remote data potentially related to spocs.\n              this.clearSpocs();\n            }\n            await this.loadSpocs(update =>\n              this.store.dispatch(ac.BroadcastToContent(update))\n            );\n            break;\n        }\n        break;\n    }\n  }\n};\n\n// This function generates a hardcoded layout each call.\n// This is because modifying the original object would\n// persist across pref changes and system_tick updates.\ngetHardcodedLayout = basic => {\n  if (basic) {\n    // Hardcoded version of layout_variant `basic`\n    return {\n      lastUpdate: Date.now(),\n      spocs: {\n        url: \"https://spocs.getpocket.com/spocs\",\n        spocs_per_domain: 1,\n      },\n      layout: [\n        {\n          width: 12,\n          components: [\n            {\n              type: \"TopSites\",\n              header: {\n                title: {\n                  id: \"newtab-section-header-topsites\",\n                },\n              },\n              properties: {},\n            },\n            {\n              type: \"Message\",\n              header: {\n                title: {\n                  id: \"newtab-section-header-pocket\",\n                  values: { provider: \"pocket\" },\n                },\n                subtitle: \"\",\n                link_text: {\n                  id: \"newtab-pocket-whats-pocket\",\n                  values: { provider: \"pocket\" },\n                },\n                link_url: \"https://getpocket.com/firefox/new_tab_learn_more\",\n                icon:\n                  \"resource://activity-stream/data/content/assets/glyph-pocket-16.svg\",\n              },\n              properties: {},\n              styles: {\n                \".ds-message\": \"margin-bottom: -20px\",\n              },\n            },\n            {\n              type: \"CardGrid\",\n              properties: {\n                items: 3,\n              },\n              header: {\n                title: \"\",\n              },\n              feed: {\n                embed_reference: null,\n                url:\n                  \"https://getpocket.cdn.mozilla.net/v3/firefox/global-recs?version=3&consumer_key=$apiKey&locale_lang=$locale\",\n              },\n              spocs: {\n                probability: 1,\n                positions: [\n                  {\n                    index: 2,\n                  },\n                ],\n              },\n            },\n            {\n              type: \"Navigation\",\n              properties: {\n                alignment: \"left-align\",\n                links: [\n                  {\n                    name: \"Must Reads\",\n                    url:\n                      \"https://getpocket.com/explore/must-reads?src=fx_new_tab\",\n                  },\n                  {\n                    name: \"Productivity\",\n                    url:\n                      \"https://getpocket.com/explore/productivity?src=fx_new_tab\",\n                  },\n                  {\n                    name: \"Health\",\n                    url: \"https://getpocket.com/explore/health?src=fx_new_tab\",\n                  },\n                  {\n                    name: \"Finance\",\n                    url: \"https://getpocket.com/explore/finance?src=fx_new_tab\",\n                  },\n                  {\n                    name: \"Technology\",\n                    url:\n                      \"https://getpocket.com/explore/technology?src=fx_new_tab\",\n                  },\n                  {\n                    name: \"More Recommendations ›\",\n                    url:\n                      \"https://getpocket.com/explore/trending?src=fx_new_tab\",\n                  },\n                ],\n              },\n            },\n          ],\n        },\n      ],\n    };\n  }\n  // Hardcoded version of layout_variant `3-col-7-row-octr`\n  return {\n    lastUpdate: Date.now(),\n    spocs: {\n      url: \"https://spocs.getpocket.com/spocs\",\n      spocs_per_domain: 1,\n    },\n    layout: [\n      {\n        width: 12,\n        components: [\n          {\n            type: \"TopSites\",\n            header: {\n              title: {\n                id: \"newtab-section-header-topsites\",\n              },\n            },\n          },\n        ],\n      },\n      {\n        width: 12,\n        components: [\n          {\n            type: \"Message\",\n            header: {\n              title: {\n                id: \"newtab-section-header-pocket\",\n                values: { provider: \"pocket\" },\n              },\n              subtitle: \"\",\n              link_text: {\n                id: \"newtab-pocket-whats-pocket\",\n                values: { provider: \"pocket\" },\n              },\n              link_url: \"https://getpocket.com/firefox/new_tab_learn_more\",\n              icon:\n                \"resource://activity-stream/data/content/assets/glyph-pocket-16.svg\",\n            },\n            properties: {},\n            styles: {\n              \".ds-message\": \"margin-bottom: -20px\",\n            },\n          },\n        ],\n      },\n      {\n        width: 12,\n        components: [\n          {\n            type: \"CardGrid\",\n            properties: {\n              items: 21,\n            },\n            header: {\n              title: \"\",\n            },\n            feed: {\n              embed_reference: null,\n              url:\n                \"https://getpocket.cdn.mozilla.net/v3/firefox/global-recs?version=3&consumer_key=$apiKey&locale_lang=$locale&count=30\",\n            },\n            spocs: {\n              probability: 1,\n              positions: [\n                {\n                  index: 2,\n                },\n                {\n                  index: 4,\n                },\n                {\n                  index: 11,\n                },\n                {\n                  index: 20,\n                },\n              ],\n            },\n          },\n          {\n            type: \"Navigation\",\n            properties: {\n              alignment: \"left-align\",\n              links: [\n                {\n                  name: \"Must Reads\",\n                  url:\n                    \"https://getpocket.com/explore/must-reads?src=fx_new_tab\",\n                },\n                {\n                  name: \"Productivity\",\n                  url:\n                    \"https://getpocket.com/explore/productivity?src=fx_new_tab\",\n                },\n                {\n                  name: \"Health\",\n                  url: \"https://getpocket.com/explore/health?src=fx_new_tab\",\n                },\n                {\n                  name: \"Finance\",\n                  url: \"https://getpocket.com/explore/finance?src=fx_new_tab\",\n                },\n                {\n                  name: \"Technology\",\n                  url:\n                    \"https://getpocket.com/explore/technology?src=fx_new_tab\",\n                },\n                {\n                  name: \"More Recommendations ›\",\n                  url: \"https://getpocket.com/explore/trending?src=fx_new_tab\",\n                },\n              ],\n            },\n            header: {\n              title: {\n                id: \"newtab-pocket-read-more\",\n              },\n            },\n            styles: {\n              \".ds-navigation\": \"margin-top: -10px;\",\n            },\n          },\n        ],\n      },\n    ],\n  };\n};\n\nconst EXPORTED_SYMBOLS = [\"DiscoveryStreamFeed\"];\n"
  },
  {
    "path": "lib/DownloadsManager.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nconst { XPCOMUtils } = ChromeUtils.import(\n  \"resource://gre/modules/XPCOMUtils.jsm\"\n);\nXPCOMUtils.defineLazyGlobalGetters(this, [\"URL\"]);\n\nconst { actionTypes: at } = ChromeUtils.import(\n  \"resource://activity-stream/common/Actions.jsm\"\n);\n\nXPCOMUtils.defineLazyModuleGetters(this, {\n  BrowserWindowTracker: \"resource:///modules/BrowserWindowTracker.jsm\",\n  DownloadsCommon: \"resource:///modules/DownloadsCommon.jsm\",\n  DownloadsViewUI: \"resource:///modules/DownloadsViewUI.jsm\",\n  FileUtils: \"resource://gre/modules/FileUtils.jsm\",\n  NewTabUtils: \"resource://gre/modules/NewTabUtils.jsm\",\n});\n\nconst DOWNLOAD_CHANGED_DELAY_TIME = 1000; // time in ms to delay timer for downloads changed events\n\nthis.DownloadsManager = class DownloadsManager {\n  constructor(store) {\n    this._downloadData = null;\n    this._store = null;\n    this._downloadItems = new Map();\n    this._downloadTimer = null;\n  }\n\n  setTimeout(callback, delay) {\n    let timer = Cc[\"@mozilla.org/timer;1\"].createInstance(Ci.nsITimer);\n    timer.initWithCallback(callback, delay, Ci.nsITimer.TYPE_ONE_SHOT);\n    return timer;\n  }\n\n  formatDownload(download) {\n    return {\n      hostname: new URL(download.source.url).hostname,\n      url: download.source.url,\n      path: download.target.path,\n      title: DownloadsViewUI.getDisplayName(download),\n      description:\n        DownloadsViewUI.getSizeWithUnits(download) ||\n        DownloadsCommon.strings.sizeUnknown,\n      referrer: download.source.referrerInfo\n        ? download.source.referrerInfo.originalReferrer.spec\n        : null,\n      date_added: download.endTime,\n    };\n  }\n\n  init(store) {\n    this._store = store;\n    this._downloadData = DownloadsCommon.getData(\n      null /* null for non-private downloads */,\n      true,\n      false,\n      true\n    );\n    this._downloadData.addView(this);\n  }\n\n  onDownloadAdded(download) {\n    if (!this._downloadItems.has(download.source.url)) {\n      this._downloadItems.set(download.source.url, download);\n\n      // On startup, all existing downloads fire this notification, so debounce them\n      if (this._downloadTimer) {\n        this._downloadTimer.delay = DOWNLOAD_CHANGED_DELAY_TIME;\n      } else {\n        this._downloadTimer = this.setTimeout(() => {\n          this._downloadTimer = null;\n          this._store.dispatch({ type: at.DOWNLOAD_CHANGED });\n        }, DOWNLOAD_CHANGED_DELAY_TIME);\n      }\n    }\n  }\n\n  onDownloadRemoved(download) {\n    if (this._downloadItems.has(download.source.url)) {\n      this._downloadItems.delete(download.source.url);\n      this._store.dispatch({ type: at.DOWNLOAD_CHANGED });\n    }\n  }\n\n  async getDownloads(\n    threshold,\n    {\n      numItems = this._downloadItems.size,\n      onlySucceeded = false,\n      onlyExists = false,\n    }\n  ) {\n    if (!threshold) {\n      return [];\n    }\n    let results = [];\n\n    // Only get downloads within the time threshold specified and sort by recency\n    const downloadThreshold = Date.now() - threshold;\n    let downloads = [...this._downloadItems.values()]\n      .filter(download => download.endTime > downloadThreshold)\n      .sort((download1, download2) => download1.endTime < download2.endTime);\n\n    for (const download of downloads) {\n      // Ignore blocked links, but allow long (data:) uris to avoid high CPU\n      if (\n        download.source.url.length < 10000 &&\n        NewTabUtils.blockedLinks.isBlocked(download.source)\n      ) {\n        continue;\n      }\n\n      // Only include downloads where the file still exists\n      if (onlyExists) {\n        // Refresh download to ensure the 'exists' attribute is up to date\n        await download.refresh();\n        if (!download.target.exists) {\n          continue;\n        }\n      }\n      // Only include downloads that were completed successfully\n      if (onlySucceeded) {\n        if (!download.succeeded) {\n          continue;\n        }\n      }\n      const formattedDownloadForHighlights = this.formatDownload(download);\n      results.push(formattedDownloadForHighlights);\n      if (results.length === numItems) {\n        break;\n      }\n    }\n    return results;\n  }\n\n  uninit() {\n    if (this._downloadData) {\n      this._downloadData.removeView(this);\n      this._downloadData = null;\n    }\n    if (this._downloadTimer) {\n      this._downloadTimer.cancel();\n      this._downloadTimer = null;\n    }\n  }\n\n  onAction(action) {\n    let doDownloadAction = callback => {\n      let download = this._downloadItems.get(action.data.url);\n      if (download) {\n        callback(download);\n      }\n    };\n\n    switch (action.type) {\n      case at.COPY_DOWNLOAD_LINK:\n        doDownloadAction(download => {\n          DownloadsCommon.copyDownloadLink(download);\n        });\n        break;\n      case at.REMOVE_DOWNLOAD_FILE:\n        doDownloadAction(download => {\n          DownloadsCommon.deleteDownload(download).catch(Cu.reportError);\n        });\n        break;\n      case at.SHOW_DOWNLOAD_FILE:\n        doDownloadAction(download => {\n          DownloadsCommon.showDownloadedFile(\n            new FileUtils.File(download.target.path)\n          );\n        });\n        break;\n      case at.OPEN_DOWNLOAD_FILE:\n        doDownloadAction(download => {\n          DownloadsCommon.openDownloadedFile(\n            new FileUtils.File(download.target.path),\n            null,\n            BrowserWindowTracker.getTopWindow()\n          );\n        });\n        break;\n      case at.UNINIT:\n        this.uninit();\n        break;\n    }\n  }\n};\nthis.EXPORTED_SYMBOLS = [\"DownloadsManager\"];\n"
  },
  {
    "path": "lib/FaviconFeed.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\"use strict\";\n\nconst { actionTypes: at } = ChromeUtils.import(\n  \"resource://activity-stream/common/Actions.jsm\"\n);\nconst { getDomain } = ChromeUtils.import(\n  \"resource://activity-stream/lib/TippyTopProvider.jsm\"\n);\nconst { RemoteSettings } = ChromeUtils.import(\n  \"resource://services-settings/remote-settings.js\"\n);\n\nChromeUtils.defineModuleGetter(\n  this,\n  \"PlacesUtils\",\n  \"resource://gre/modules/PlacesUtils.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"Services\",\n  \"resource://gre/modules/Services.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"NewTabUtils\",\n  \"resource://gre/modules/NewTabUtils.jsm\"\n);\n\nconst MIN_FAVICON_SIZE = 96;\n\n/**\n * Get favicon info (uri and size) for a uri from Places.\n *\n * @param uri {nsIURI} Page to check for favicon data\n * @returns A promise of an object (possibly null) containing the data\n */\nfunction getFaviconInfo(uri) {\n  // Use 0 to get the biggest width available\n  const preferredWidth = 0;\n  return new Promise(resolve =>\n    PlacesUtils.favicons.getFaviconDataForPage(\n      uri,\n      // Package up the icon data in an object if we have it; otherwise null\n      (iconUri, faviconLength, favicon, mimeType, faviconSize) =>\n        resolve(iconUri ? { iconUri, faviconSize } : null),\n      preferredWidth\n    )\n  );\n}\n\n/**\n * Fetches visit paths for a given URL from its most recent visit in Places.\n *\n * Note that this includes the URL itself as well as all the following\n * permenent&temporary redirected URLs if any.\n *\n * @param {String} a URL string\n *\n * @returns {Array} Returns an array containing objects as\n *   {int}    visit_id: ID of the visit in moz_historyvisits.\n *   {String} url: URL of the redirected URL.\n */\nasync function fetchVisitPaths(url) {\n  const query = `\n    WITH RECURSIVE path(visit_id)\n    AS (\n      SELECT v.id\n      FROM moz_places h\n      JOIN moz_historyvisits v\n        ON v.place_id = h.id\n      WHERE h.url_hash = hash(:url) AND h.url = :url\n        AND v.visit_date = h.last_visit_date\n\n      UNION\n\n      SELECT id\n      FROM moz_historyvisits\n      JOIN path\n        ON visit_id = from_visit\n      WHERE visit_type IN\n        (${PlacesUtils.history.TRANSITIONS.REDIRECT_PERMANENT},\n         ${PlacesUtils.history.TRANSITIONS.REDIRECT_TEMPORARY})\n    )\n    SELECT visit_id, (\n      SELECT (\n        SELECT url\n        FROM moz_places\n        WHERE id = place_id)\n      FROM moz_historyvisits\n      WHERE id = visit_id) AS url\n    FROM path\n  `;\n\n  const visits = await NewTabUtils.activityStreamProvider.executePlacesQuery(\n    query,\n    {\n      columns: [\"visit_id\", \"url\"],\n      params: { url },\n    }\n  );\n  return visits;\n}\n\n/**\n * Fetch favicon for a url by following its redirects in Places.\n *\n * This can improve the rich icon coverage for Top Sites since Places only\n * associates the favicon to the final url if the original one gets redirected.\n * Note this is not an urgent request, hence it is dispatched to the main\n * thread idle handler to avoid any possible performance impact.\n */\nasync function fetchIconFromRedirects(url) {\n  const visitPaths = await fetchVisitPaths(url);\n  if (visitPaths.length > 1) {\n    const lastVisit = visitPaths.pop();\n    const redirectedUri = Services.io.newURI(lastVisit.url);\n    const iconInfo = await getFaviconInfo(redirectedUri);\n    if (iconInfo && iconInfo.faviconSize >= MIN_FAVICON_SIZE) {\n      PlacesUtils.favicons.setAndFetchFaviconForPage(\n        Services.io.newURI(url),\n        iconInfo.iconUri,\n        false,\n        PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,\n        null,\n        Services.scriptSecurityManager.getSystemPrincipal()\n      );\n    }\n  }\n}\n\nthis.FaviconFeed = class FaviconFeed {\n  constructor() {\n    this._queryForRedirects = new Set();\n  }\n\n  /**\n   * fetchIcon attempts to fetch a rich icon for the given url from two sources.\n   * First, it looks up the tippy top feed, if it's still missing, then it queries\n   * the places for rich icon with its most recent visit in order to deal with\n   * the redirected visit. See Bug 1421428 for more details.\n   */\n  async fetchIcon(url) {\n    // Avoid initializing and fetching icons if prefs are turned off\n    if (!this.shouldFetchIcons) {\n      return;\n    }\n\n    const site = await this.getSite(getDomain(url));\n    if (!site) {\n      if (!this._queryForRedirects.has(url)) {\n        this._queryForRedirects.add(url);\n        Services.tm.idleDispatchToMainThread(() => fetchIconFromRedirects(url));\n      }\n      return;\n    }\n\n    let iconUri = Services.io.newURI(site.image_url);\n    // The #tippytop is to be able to identify them for telemetry.\n    iconUri = iconUri\n      .mutate()\n      .setRef(\"tippytop\")\n      .finalize();\n    PlacesUtils.favicons.setAndFetchFaviconForPage(\n      Services.io.newURI(url),\n      iconUri,\n      false,\n      PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,\n      null,\n      Services.scriptSecurityManager.getSystemPrincipal()\n    );\n  }\n\n  /**\n   * Get the site tippy top data from Remote Settings.\n   */\n  async getSite(domain) {\n    const sites = await this.tippyTop.get({\n      filters: { domain },\n      syncIfEmpty: false,\n    });\n    return sites.length ? sites[0] : null;\n  }\n\n  /**\n   * Get the tippy top collection from Remote Settings.\n   */\n  get tippyTop() {\n    if (!this._tippyTop) {\n      this._tippyTop = RemoteSettings(\"tippytop\");\n    }\n    return this._tippyTop;\n  }\n\n  /**\n   * Determine if we should be fetching and saving icons.\n   */\n  get shouldFetchIcons() {\n    return Services.prefs.getBoolPref(\"browser.chrome.site_icons\");\n  }\n\n  onAction(action) {\n    switch (action.type) {\n      case at.RICH_ICON_MISSING:\n        this.fetchIcon(action.data.url);\n        break;\n    }\n  }\n};\n\nconst EXPORTED_SYMBOLS = [\"FaviconFeed\", \"fetchIconFromRedirects\"];\n"
  },
  {
    "path": "lib/FilterAdult.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\"use strict\";\n\nChromeUtils.defineModuleGetter(\n  this,\n  \"Services\",\n  \"resource://gre/modules/Services.jsm\"\n);\n\n// Keep a Set of adult base domains for lookup (initialized at end of file)\nlet gAdultSet;\n\n// Keep a hasher for repeated hashings\nlet gCryptoHash = null;\n\n/**\n * Run some text through md5 and return the base64 result.\n */\nfunction md5Hash(text) {\n  // Lazily create a reusable hasher\n  if (gCryptoHash === null) {\n    gCryptoHash = Cc[\"@mozilla.org/security/hash;1\"].createInstance(\n      Ci.nsICryptoHash\n    );\n  }\n\n  gCryptoHash.init(gCryptoHash.MD5);\n\n  // Convert the text to a byte array for hashing\n  gCryptoHash.update(text.split(\"\").map(c => c.charCodeAt(0)), text.length);\n\n  // Request the has result as ASCII base64\n  return gCryptoHash.finish(true);\n}\n\n/**\n * Filter out any link objects that have a url with an adult base domain.\n */\nfunction filterAdult(links) {\n  return links.filter(({ url }) => {\n    try {\n      const uri = Services.io.newURI(url);\n      return !gAdultSet.has(md5Hash(Services.eTLD.getBaseDomain(uri)));\n    } catch (ex) {\n      return true;\n    }\n  });\n}\n\nconst EXPORTED_SYMBOLS = [\"filterAdult\"];\n\n// These are md5 hashes of base domains to be filtered out. Originally from:\n// https://hg.mozilla.org/mozilla-central/log/default/browser/base/content/newtab/newTab.inadjacent.json\ngAdultSet = new Set([\n  \"+/UCpAhZhz368iGioEO8aQ==\",\n  \"+1e7jvUo8f2/2l0TFrQqfA==\",\n  \"+1gcqAqaRZwCj5BGiZp3CA==\",\n  \"+25t/2lo0FUEtWYK8LdQZQ==\",\n  \"+8PiQt6O7pJI/nIvQpDaAg==\",\n  \"+CLf5witKkuOvPCulTlkqw==\",\n  \"+CvLiih/gf2ugXAF+LgWqw==\",\n  \"+DWs0vvFGt6d3mzdcsdsyA==\",\n  \"+H0Rglt/HnhZwdty2hsDHg==\",\n  \"+L1FDsr5VQtuYc2Is5QGjw==\",\n  \"+LJYVZl1iPrdMU3L5+nxZw==\",\n  \"+Mp+JIyO0XC5urvMyi3wvQ==\",\n  \"+NMUaQ7XPsAi0rk7tTT9wQ==\",\n  \"+NmjwjsPhGJh9bM10SFkLw==\",\n  \"+OERSmo7OQUUjudkccSMOA==\",\n  \"+OLntmlsMBBYPREPnS6iVw==\",\n  \"+OXdvbTxHtSoLg7bZMho4w==\",\n  \"+P5q4YD1Rr5SX26Xr+tzlw==\",\n  \"+PUVXkoTqHxJHO18z4KMfw==\",\n  \"+Pl0bSMBAdXpRIA+zE02JA==\",\n  \"+QosBAnSM2h4lsKuBlqEZw==\",\n  \"+S+WXgVDSU1oGmCzGwuT3g==\",\n  \"+SclwwY8R2RPrnX54Z+A6w==\",\n  \"+VfRcTBQ80KSeJRdg0cDfw==\",\n  \"+WpF8+poKmHPUBB4UYh/ig==\",\n  \"+YVxSyViJfrme/ENe1zA7A==\",\n  \"+YrqTEJlJCv0A2RHQ8tr1A==\",\n  \"+ZozWaPWw8ws1cE5DJACeg==\",\n  \"+aF4ilbjQbLpAuFXQEYMWQ==\",\n  \"+dBv88reDrjEz6a2xX3Hzw==\",\n  \"+dIEf5FBrHpkjmwUmGS6eg==\",\n  \"+edqJYGvcy1AH2mEjJtSIg==\",\n  \"+fcjH2kZKNj8quOytUk4nQ==\",\n  \"+gO0bg8LY+py2dLM1sM7Ag==\",\n  \"+gbitI/gpxebN/rK7qj8Fw==\",\n  \"+gpHnUj2GWocP74t5XWz4w==\",\n  \"+jVN/3ASc2O44sX6ab8/cg==\",\n  \"+mJLK+6qq8xFv7O/mbILTw==\",\n  \"+n0K7OB2ItzhySZ4rhUrMg==\",\n  \"+p8pofUlwn8vV6Rp6+sz9g==\",\n  \"+tuUmnRDRWVLA+1k0dcUvg==\",\n  \"+zBkeHF4P8vLzk1iO1Zn3Q==\",\n  \"//eHwmDOQRSrv+k9C/k3ZQ==\",\n  \"/2Chaw2M9DzsadFFkCu6WQ==\",\n  \"/2c4oNniwhL3z5IOngfggg==\",\n  \"/2jGyMekNu7U136K+2N3Jg==\",\n  \"/Bwpt5fllzDHq2Ul6v86fA==\",\n  \"/DJgKE9ouibewuZ2QEnk6w==\",\n  \"/DiUApY7cVp5W9o24rkgRA==\",\n  \"/FchS2nPezycB8Bcqc2dbg==\",\n  \"/FdZzSprPnNDPwbhV1C0Cg==\",\n  \"/FsJYFNe+7UvsSkiotNJEQ==\",\n  \"/G26n5Xoviqldr5sg/Jl3w==\",\n  \"/HU2+fBqfWTEuqINc0UZSA==\",\n  \"/IarsLzJB8bf0AupJJ+/Eg==\",\n  \"/KYZdUWrkfxSsIrp46xxow==\",\n  \"/MEOgAhwb7F0nBnV4tIRZA==\",\n  \"/MeHciFhvFzQsCIw39xIZA==\",\n  \"/Ph/6l/lFNVqxAje1+PgFA==\",\n  \"/SP6pOdYFzcAl2OL05z4uQ==\",\n  \"/TSsi/AwKHtP6kQaeReI3w==\",\n  \"/VnKh/NDv7y/bfO6CWsLaQ==\",\n  \"/XC/FmMIOdhMTPqmy4DfUA==\",\n  \"/XjB6c5fxFGcKVAQ4o+OMw==\",\n  \"/YuQw7oAF08KDptxJEBS9g==\",\n  \"/a+bLXOq02sa/s8h7PhUTg==\",\n  \"/a9O7kWeXa0le45ab3+nVw==\",\n  \"/c34NtdUZAHWIwGl3JM8Tw==\",\n  \"/cJ0Nn5YbXeUpOHMfWXNHQ==\",\n  \"/cdR1i5TuQvO+u3Ov3b0KQ==\",\n  \"/gi3UZmunVOIXhZSktZ8zQ==\",\n  \"/hFhjFGJx2wRfz6hyrIpvA==\",\n  \"/jDVt9dRIn+o4IQ1DPwbsg==\",\n  \"/jH6imhTPZ/tHI4gYz2+HA==\",\n  \"/kGxvyEokQsVz0xlKzCn2A==\",\n  \"/mFp3GFkGNLhx2CiDvJv4A==\",\n  \"/mrqas0eDX+sFUNJvCQY8g==\",\n  \"/n1RLTTVpygre1dl36PDwQ==\",\n  \"/ngbFuKIAVpdSwsA3VxvNw==\",\n  \"/p/aCTIhi1bU0/liuO/a2Q==\",\n  \"/u5W2Gab4GgCMIc4KTp2mg==\",\n  \"/wIZAye9h1TUiZmDW0ZmYA==\",\n  \"/wiA2ltAuWyBhIvQAYBTQw==\",\n  \"/y/jHHEpUu5TR+R2o96kXA==\",\n  \"/zFLRvi75UL8qvg+a6zqGg==\",\n  \"00TVKawojyqrJkC7YqT41Q==\",\n  \"022B0oiRMx8Xb4Af98mTvQ==\",\n  \"02im2RooJQ/9UfUrh5LO+A==\",\n  \"0G93AxGPVwmr66ZOleM90A==\",\n  \"0HN6MIGtkdzNPsrGs611xA==\",\n  \"0K4NBxqEa3RYpnrkrD/XjQ==\",\n  \"0L0FVcH5Dlj3oL8+e9Na7g==\",\n  \"0NrvBuyjcJ2q6yaHpz/FOA==\",\n  \"0ODJyWKJSfObo+FNdRQkkA==\",\n  \"0QB0OUW5x2JLHfrtmpZQ+w==\",\n  \"0QCQORCYfLuSbq94Sbt0bQ==\",\n  \"0QbH4oI8IjZ9BRcqRyvvDQ==\",\n  \"0QxPAqRF8inBuFEEzNmLjA==\",\n  \"0SkC/4PtnX1bMYgD6r6CLA==\",\n  \"0TxcYwG72dT7Tg+eG8pP1w==\",\n  \"0UeRwDID2RBIikInqFI7uw==\",\n  \"0VsaJHR0Ms8zegsCpAKoyg==\",\n  \"0Y6iiZjCwPDwD/CwJzfioQ==\",\n  \"0ZEC3hy411LkOhKblvTcqg==\",\n  \"0ZRGz+oj2infCAkuKKuHiQ==\",\n  \"0a4SafpDIe8V4FlFWYkMHw==\",\n  \"0b/xj6fd0x+aB8EB0LC4SA==\",\n  \"0bj069wXgEJbw7dpiPr8Tg==\",\n  \"0dIeIM5Zvm5nSVWLy94LWg==\",\n  \"0e8hM3E5tnABRyy29A8yFw==\",\n  \"0egBaMnAf0CQEXf1pCIKnA==\",\n  \"0fN+eHlbRS6mVZBbH/B9FQ==\",\n  \"0fnruVOCxEczscBuv4yL9A==\",\n  \"0fpe9E6m3eLp/5j5rLrz2Q==\",\n  \"0klouNfZRHFFpdHi4ZR2hA==\",\n  \"0nOg18ZJ/NicqVUz5Jr0Hg==\",\n  \"0ofMbUCA3/v5L8lHnX4S5w==\",\n  \"0p1jMr06OyBoXQuSLYN4aQ==\",\n  \"0p8YbEMxeb73HbAfvPLQRw==\",\n  \"0q+erphtrB+6HBnnYg7O6w==\",\n  \"0rTYcuVYdilO7zEfKrxY3A==\",\n  \"0rfG4gRugAwVP0i3AGVxxg==\",\n  \"0u+0WHr7WI6IlVBBgiRi6w==\",\n  \"0yJ7TQYzcp3DXVSvwavr+w==\",\n  \"1+A9FCGP3bZhk6gU3LQtNg==\",\n  \"1+XWdu4qCqLLVjqkKz3nmA==\",\n  \"1+qmrbC8c7MJ6pxmDMcKuA==\",\n  \"1/Hxu8M9N/oNwk8bCj4FNQ==\",\n  \"1/SGIab+NnizimUmNDC4wA==\",\n  \"1/ZheMsbojazxt31j/l3iA==\",\n  \"10OltdxPXOvfatJuwPVKbQ==\",\n  \"11FE2kknwYi2Qu0JUKMn3A==\",\n  \"11U5XEwfMI7avx014LfC8g==\",\n  \"16d+fhFlgayu3ttKVV/pbg==\",\n  \"16iT/jCcPDrJEfi2bE5F+Q==\",\n  \"18RKixTv12q3xoBLz6eKiA==\",\n  \"18ndtDM9UaNfBR1cr3SHdA==\",\n  \"19yQHaBemtlgo2QkU5M6jQ==\",\n  \"1AeReq55UQotRQVKJ66pmg==\",\n  \"1ApqwW7pE+XUB2Cs2M6y7g==\",\n  \"1B5gxGQSGzVKoNd5Ol4N7g==\",\n  \"1BjsijOzgHt/0i36ZGffoQ==\",\n  \"1C50kisi9nvyVJNfq2hOEQ==\",\n  \"1E3pMgAHOnHx3ALdNoHr8Q==\",\n  \"1EI9aa955ejNo1dJepcZJw==\",\n  \"1FSrgkUXgZot2CsmbAtkPw==\",\n  \"1Gpj4TPXhdPEI4zfQFsOCg==\",\n  \"1HDgfU7xU7LWO/BXsODZAQ==\",\n  \"1I+UVx3krrD4NhzO7dgfHQ==\",\n  \"1JI9bT92UzxI8txjhst9LQ==\",\n  \"1JRgSHnfAQFQtSkFTttkqQ==\",\n  \"1LPC0BzhJbepHTSAiZ3QTw==\",\n  \"1MIn73MLroxXirrb+vyg2Q==\",\n  \"1Oykse0jQVbuR3MvW5ot4A==\",\n  \"1Pmnur6TbZ9cmemvu0+dSA==\",\n  \"1PvTn90xwZJPoVfyT5/uIQ==\",\n  \"1QGhj9NONF2rC44UdO+Izw==\",\n  \"1RQZ2pWSxT+RKyhBigtSFg==\",\n  \"1Vtrv6QUAfiYQjlLTpNovg==\",\n  \"1WIi4I62GqkjDXOYqHWJfQ==\",\n  \"1Wc8jQlDSB4Dp32wkL2odw==\",\n  \"1X14kHeKwGmLeYqpe60XEA==\",\n  \"1YO9G8qAhLIu2rShvekedw==\",\n  \"1Ym0lyBJ9aFjhJb/GdUPvQ==\",\n  \"1b2uf+CdVjufqiVpUShvHw==\",\n  \"1buQEv2YlH/ljTgH0uJEtw==\",\n  \"1cj1Fpd3+UiBAOahEhsluA==\",\n  \"1d7RPHdZ9qzAbG3Vi9BdFA==\",\n  \"1dhq3ozNCx0o4dV1syLVDA==\",\n  \"1dsKN1nG6upj7kKTKuJWsQ==\",\n  \"1eCHcz4swFH+uRhiilOinQ==\",\n  \"1eRUCdIJe3YGD5jOMbkkOg==\",\n  \"1fztTtQWNMIMSAc5Hr6jMQ==\",\n  \"1gA65t5FiBTEgMELTQFUPQ==\",\n  \"1jBaRO8Bg5l6TH7qJ8EPiw==\",\n  \"1k8tL2xmGFVYMgKUcmDcEw==\",\n  \"1lCcQWGDePPYco4vYrA5vw==\",\n  \"1m1yD4L9A7Q1Ot+wCsrxJQ==\",\n  \"1mw6LfTiirFyfjejf8QNGA==\",\n  \"1nXByug2eKq0kR3H3VjnWQ==\",\n  \"1tpM0qgdo7JDFwvT0TD78g==\",\n  \"1vqRt79ukuvdJNyIlIag8Q==\",\n  \"1wBuHqS1ciup31WTfm3NPg==\",\n  \"1xWx5V3G9murZP7srljFmA==\",\n  \"1zDfWw5LdG20ClNP1HYxgw==\",\n  \"203EqmJI9Q4tWxTJaBdSzA==\",\n  \"23C4eh3yBb5n/RNZeTyJkA==\",\n  \"23d9B9Gz5kUOi1I//EYsSQ==\",\n  \"24H9q+E8pgCEdFS7JO5kzQ==\",\n  \"25w3ZRUzCvJwAVHYCIO5uw==\",\n  \"26+yXbqI+fmIZsYl4UhUzw==\",\n  \"26Wmdp6SkKN74W0/XPcnmA==\",\n  \"29EybnMEO95Ng4l/qK4NWQ==\",\n  \"2Ct+pLXrK6Ku1f4qehjurQ==\",\n  \"2D6yhuABiaFFoXz0Lh0C+w==\",\n  \"2DNbXVgesUa7PgYQ4zX5Lw==\",\n  \"2E41e0MgM3WhFx2oasIQeA==\",\n  \"2HHqeGRMfzf3RXwVybx+ZQ==\",\n  \"2Hc5oyl0AYRy2VzcDKy+VA==\",\n  \"2QQtKtBAm2AjJ5c0WQ6BQA==\",\n  \"2QS/6OBA1T01NlIbfkTYJg==\",\n  \"2RFaMPlSbVuoEqKXgkIa5A==\",\n  \"2SI4F7Vvde2yjzMLAwxOog==\",\n  \"2SwIiUwT4vRZPrg7+vZqDA==\",\n  \"2W6lz1Z7PhkvObEAg2XKJw==\",\n  \"2Wvk/kouEEOY0evUkQLhOQ==\",\n  \"2XrR2hjDEvx8MQpHk9dnjw==\",\n  \"2aDK0tGNgMLyxT+BQPDE8Q==\",\n  \"2aIx9UdMxxZWvrfeJ+DcTw==\",\n  \"2abfl3N46tznOpr+94VONQ==\",\n  \"2bsIpvnGcFhTCSrK9EW1FQ==\",\n  \"2hEzujfG3mR5uQJXbvOPTQ==\",\n  \"2j83jrPwPfYlpJJ2clEBYQ==\",\n  \"2ksediOVrh4asSBxKcudTg==\",\n  \"2melaInV0wnhBpiI3da6/A==\",\n  \"2nSTEYzLK77h5Rgyti+ULQ==\",\n  \"2os5s7j7Tl46ZmoZJH8FjA==\",\n  \"2rOkEVl90EPqfHOF5q2FYw==\",\n  \"2rhjiY0O0Lo36wTHjmlNyw==\",\n  \"2vm7g3rk1ACJOTCXkLB3zA==\",\n  \"2wesXiib76wM9sqRZ7JYwQ==\",\n  \"2ywo4t5PPSVUCWDwUlOVwQ==\",\n  \"3++dZXzZ6AFEz7hK+i5hww==\",\n  \"3+9nURtBK3FKn0J9DQDa3g==\",\n  \"3+zsjCi7TnJhti//YXK35w==\",\n  \"3/1puZTGSrD9qNKPGaUZww==\",\n  \"300hoYyMR/mk1mfWJxS8/w==\",\n  \"301utVPZ93AnPLYbsiJggw==\",\n  \"312g8iTB9oJgk/OqcgR7Cw==\",\n  \"342VOUOxoLHUqtHANt83Hw==\",\n  \"36XDmX6j542q+Oei1/x0gw==\",\n  \"37Nkh06O979nt7xzspOFyQ==\",\n  \"3AKEYQqpkfW7CZMFQZoxOw==\",\n  \"3AVYtcIv7A5mVbVnQMaCeA==\",\n  \"3BjLFon1Il0SsjxHE2A1LQ==\",\n  \"3CJbrUdW68E3Drhe4ahUnQ==\",\n  \"3EhLkC9NqD3A6ApV6idmgg==\",\n  \"3Ejtsqw3Iep/UQd0tXnSlg==\",\n  \"3FH4D31nKV13sC9RpRZFIg==\",\n  \"3Gg9N7vjAfQEYOtQKuF/Eg==\",\n  \"3HPOzIZxoaQAmWRy9OkoSg==\",\n  \"3JhnM6G4L06NHt31lR0zXA==\",\n  \"3L3KEBHhgDwH615w4OvgZA==\",\n  \"3Leu2Sc+YOntJFlrvhaXeg==\",\n  \"3P2aJxV8Trll2GH9ptElYA==\",\n  \"3RTtSaMp1TZegJo5gFtwwA==\",\n  \"3TbRZtFtsh9ez8hqZuTDeA==\",\n  \"3TjntNWtpG7VqBt3729L6Q==\",\n  \"3UBYBMejKInSbCHRoJJ7dg==\",\n  \"3UNJ37f+gnNyYk9yLFeoYA==\",\n  \"3WVBP9fyAiBPZAq3DpMwOQ==\",\n  \"3Wfj05vCLFAB9vII5AU9tw==\",\n  \"3WwITQML938W9+MUM56a3A==\",\n  \"3XyoREdvhmSbyvAbgw2y/A==\",\n  \"3Y4w0nETru3SiSVUMcWXqw==\",\n  \"3Y6/HqS1trYc9Dh778sefg==\",\n  \"3YXp1PmMldUjBz3hC6ItbA==\",\n  \"3djRJvkZk9O2bZeUTe+7xQ==\",\n  \"3go7bJ9WqH/PPUTjNP3q/Q==\",\n  \"3hVslsq98QCDIiO40JNOuA==\",\n  \"3iC21ByW/YVL+pSyppanWw==\",\n  \"3itfXtlLPRmPCSYaSvc39Q==\",\n  \"3j0kFUZ6g+yeeEljx+WXGg==\",\n  \"3jmCreW5ytSuGfmeLv7NfQ==\",\n  \"3jqsY8/xTWELmu/az3Daug==\",\n  \"3kREs/qaMX0AwFXN0LO5ow==\",\n  \"3ltw31yJuAl4VT6MieEXXw==\",\n  \"3nthUmLZ30HxQrzr2d7xFA==\",\n  \"3oMTbWf7Bv83KRlfjNWQZA==\",\n  \"3pi3aNVq1QNJmu1j0iyL0g==\",\n  \"3rbml1D0gfXnwOs5jRZ3gA==\",\n  \"3sNJJIx1NnjYcgJhjOLJOg==\",\n  \"3v09RHCPTLUztqapThYaHg==\",\n  \"3xw8+0/WU51Yz4TWIMK8mw==\",\n  \"3y5Xk65ShGvWFbQxcZaQAQ==\",\n  \"3yDD+xT8iRfUVdxcc7RxKw==\",\n  \"3yavzOJ1mM44pOSFLLszgA==\",\n  \"4+htiqjEz9oq0YcI/ErBVg==\",\n  \"40HzgVKYnqIb6NJhpSIF0A==\",\n  \"40gCrW4YWi+2lkqMSPKBPg==\",\n  \"41WEjhYUlG6jp2UPGj11eQ==\",\n  \"444F9T6Y7J67Y9sULG81qg==\",\n  \"46FCwqh+eMkf+czjhjworw==\",\n  \"46piyANQVvvLqcoMq5G8tQ==\",\n  \"49jZr/mEW6fvnyzskyN40w==\",\n  \"49z/15Nx9Og7dN9ebVqIzg==\",\n  \"4A+RHIw+aDzw0rSRYfbc7g==\",\n  \"4BkqgraeXY7yaI1FE07Evw==\",\n  \"4CfEP8TeMKX33ktwgifGgA==\",\n  \"4DIPP/yWRgRuFqVeqIyxMQ==\",\n  \"4FBBtWPvqJ3dv4w25tRHiQ==\",\n  \"4ID0PHTzIMZz2rQqDGBVfA==\",\n  \"4KJZPCE9NKTfzFxl76GWjg==\",\n  \"4LtQrahKXVtsbXrEzYU1zQ==\",\n  \"4LvQSicqsgxQFWauqlcEjw==\",\n  \"4NHQwbb3zWq2klqbT/pG6g==\",\n  \"4NP8EFFJyPcuQKnBSxzKgQ==\",\n  \"4PBaoeEwUj79njftnYYqLg==\",\n  \"4Qinl7cWmVeLJgah8bcNkw==\",\n  \"4SdHWowXgCpCDL28jEFpAw==\",\n  \"4TQkMnRsXBobbtnBmfPKnA==\",\n  \"4VR5LiXLew6Nyn91zH9L4w==\",\n  \"4WO6eT0Rh6sokb29zSJQnQ==\",\n  \"4WRdAjiUmOQg2MahsunjAg==\",\n  \"4WcFEswYU/HHQPw77DYnyA==\",\n  \"4XNUmgwxsqDYsNmPkgNQYQ==\",\n  \"4Xh/B3C16rrjbES+FM1W8g==\",\n  \"4ZFYKa7ZgvHyZLS6WpM8gA==\",\n  \"4aPU6053cfMLHgLwAZJRNg==\",\n  \"4ekt4m38G9m599xJCmhlug==\",\n  \"4erEA42TqGA9K4iFKkxMMA==\",\n  \"4ifNsmjYf1iOn2YpMfzihg==\",\n  \"4iiCq+HhC+hPMldNQMt0NA==\",\n  \"4itEKfbRCJvqlgKnyEdIOQ==\",\n  \"4jeOFKuKpCmMXUVJSh9y0g==\",\n  \"4kXlJNuT79XXf1HuuFOlHw==\",\n  \"4kj0S8XlmhHXoUP7dQItUw==\",\n  \"4mQVNv7FHj+/O6XFqWFt/Q==\",\n  \"4mig4AMLUw+T/ect9p4CfA==\",\n  \"4qMSNAxichi3ori/pR+o0w==\",\n  \"4rrSL6N0wyucuxeRELfAmw==\",\n  \"4u3eyKc+y3uRnkASrgBVUw==\",\n  \"4wnUAbPT3AHRJrPwTTEjyw==\",\n  \"4xojeUxTFmMLGm6jiMYh/Q==\",\n  \"4yEkKp2FYZ09mAhw2IcrrA==\",\n  \"4yVqq66iHYQjiTSxGgX2oA==\",\n  \"4yrFNgqWq17zVCyffULocA==\",\n  \"50jASqzGm4VyHJbFv8qVRA==\",\n  \"50xwiYvGQytEDyVgeeOnMg==\",\n  \"51yLpfEdvqXmtB6+q27/AQ==\",\n  \"520wTzrysiRi2Td92Zq0HQ==\",\n  \"53UccFNzMi9mKmdeD82vAw==\",\n  \"54XELlPm8gBvx8D5bN3aUg==\",\n  \"59ipbMH7cKBsF9bNf4PLeQ==\",\n  \"5CMadLqS2KWwwMCpzlDmLw==\",\n  \"5DDb7fFJQEb3XTc3YyOTjg==\",\n  \"5HovoyHtul8lXh+z8ywq9A==\",\n  \"5I/heFSQG/UpWGx0uhAqGQ==\",\n  \"5KOgetfZR+O2wHQSKt41BQ==\",\n  \"5LJqHFRyIwQKA4HbtqAYQQ==\",\n  \"5LuFDNKzMd2BzpWEIYO2Ww==\",\n  \"5M3dFrAOemzQ0MAbA8bI5w==\",\n  \"5N2oi2pB69NxeNt08yPLhw==\",\n  \"5NEP7Xt7ynj6xCzWzt21hQ==\",\n  \"5Nk2Z94DhlIdfG5HNgvBbQ==\",\n  \"5PfGtbH9fmVuNnq83xIIgQ==\",\n  \"5Q/Y2V0iSVTK8HE8JerEig==\",\n  \"5S5/asYfWjOwnzYpbK6JDw==\",\n  \"5SbwLDNT6sBOy6nONtUcTg==\",\n  \"5T39s5CtSrK5awMPUcEWJg==\",\n  \"5VO1inwXMvLDBQSOahT6rg==\",\n  \"5VY++KiWgo7jXSdFJsPN3A==\",\n  \"5Wcq+6hgnWsQZ/bojERpUw==\",\n  \"5Yrj6uevT8wHRyqqgnSfeg==\",\n  \"5dUry23poD+0wxZ3hH6WmA==\",\n  \"5eHStFN7wEmIE+uuRwIlPQ==\",\n  \"5eXpiczlRdmqMYSaodOUiQ==\",\n  \"5gGoDPTc/sOIDLngmlEq4A==\",\n  \"5jHgQF4SfO/zy9xy9t+9dw==\",\n  \"5jyuDp82Fux+B0+zlx8EXw==\",\n  \"5kvyy902llnYGQdn2Py04w==\",\n  \"5l6kDfjtZjkTZPJvNNOVFw==\",\n  \"5lfLJAk1L3QzGMML3fOuSw==\",\n  \"5m1ijXEW+4RTNGZsDA/rxQ==\",\n  \"5oD/aGqoakxaezq43x0Tvw==\",\n  \"5pje7qyz8BRsa8U4a4rmoA==\",\n  \"5pqqzC/YmRIMA9tMFPi7rg==\",\n  \"5r1ZsGkrzNQEpgt/gENibw==\",\n  \"5u2PdDcIY3RQgtchSGDCGg==\",\n  \"5ugVOraop5P5z5XLlYPJyQ==\",\n  \"5w/c9WkI/FA+4lOtdPxoww==\",\n  \"5w4FbRhWACP7k2WnNitiHg==\",\n  \"6+jhreeBLfw64tJ+Nhyipw==\",\n  \"600bwlyhcy754W1E6tuyYg==\",\n  \"600mjiWke4u0CDaSQKLOOg==\",\n  \"60suecbWRfexSh7C67RENA==\",\n  \"61V74uIjaSfZM8au1dxr1A==\",\n  \"62RHCbpGU8Hb+Ubn+SCTBg==\",\n  \"63OTPaKM0xCfJOy9EDto+Q==\",\n  \"64AA4jLHXc1Dp15aMaGVcA==\",\n  \"64QzHOYX0A9++FqRzZRHlQ==\",\n  \"64YsV2qeDxk2Q6WK/h7OqA==\",\n  \"65KhGKUBFQubRRIEdh9SwQ==\",\n  \"6706ncrH1OANFnaK6DUMqQ==\",\n  \"68jPYo3znYoU4uWI7FH3/g==\",\n  \"68nqDtXOuxF7DSw6muEZvg==\",\n  \"6ACvJNfryPSjGOK39ov8Qg==\",\n  \"6CjtF1S2Y6RCbhl7hMsD+g==\",\n  \"6G2bD3Y7qbGmfPqH9TqLFA==\",\n  \"6GXHGF62/+jZ7PfIBlMxZw==\",\n  \"6HGeEPyTAu9oiKhNVLjQnA==\",\n  \"6HnWgYNKohqhoa1tnjjU3A==\",\n  \"6M6QapJ5xtMXfiD3bMaiLA==\",\n  \"6NP81geiL14BeQW6TpLnUA==\",\n  \"6PzjncEw2wHZg7SP7SQk9w==\",\n  \"6QAtjOK9enNLRhcVa2iaTg==\",\n  \"6QUGE2S8oFYx4T4nW56cCw==\",\n  \"6W79FmpUN1ByNtv5IEXY4w==\",\n  \"6WhHPWlqEUqXC52rHGRHjA==\",\n  \"6XYqR2WvDzx4fWO7BIOTjA==\",\n  \"6Z9myGCF5ylWljgIYAmhqw==\",\n  \"6ZKmm7IW7IdWuVytLr68CQ==\",\n  \"6ZMs9vCzK9lsbS6eyzZlIA==\",\n  \"6b7ue29cBDsvmj1VSa5njw==\",\n  \"6c0iuya20Ys8BsvoI4iQaQ==\",\n  \"6cTETZ9iebhWl+4W5CB+YQ==\",\n  \"6dshA8knH5qqD+KmR/kdSQ==\",\n  \"6e8boFcyc8iF0/tHVje4eQ==\",\n  \"6erpZS36qZRXeZ9RN9L+kw==\",\n  \"6fWom3YoKvW6NIg6y9o9CQ==\",\n  \"6k2cuk0McTThSMW/QRHfjA==\",\n  \"6lVSzYUQ/r0ep4W2eCzFpg==\",\n  \"6leyDVmC5jglAa98NQ3+Hg==\",\n  \"6nwR+e9Qw0qp8qIwH9S/Mg==\",\n  \"6o5g9JfKLKQ2vBPqKs6kjg==\",\n  \"6rIWazDEWU5WPZHLkqznuQ==\",\n  \"6rqK8sjLPJUIp7ohkEwfZg==\",\n  \"6sBemZt4qY/TBwqk3YcLOQ==\",\n  \"6sNP0rzCCm3w976I2q2s/w==\",\n  \"6tfM6dx3R5TiVKaqYQjnCg==\",\n  \"6txm8z4/LGCH0cpaet/Hsg==\",\n  \"6uMF5i0b/xsk55DlPumT7A==\",\n  \"6uT7LZiWjLnnqnnSEW4e/Q==\",\n  \"6v3eTZtPYBfKFSjfOo2UaA==\",\n  \"6wkfN8hyKmKU6tG3YetCmw==\",\n  \"6z8CRivao3IMyV4p4gMh7g==\",\n  \"71w3aSvuh2mBLtdqJCN3wA==\",\n  \"734u4Y1R3u7UNUnD+wWUoA==\",\n  \"74FW/QYTzr/P1k6QwVHMcw==\",\n  \"778O1hdVKHLG2q9dycUS0Q==\",\n  \"78b8sDBp28zUlYPV5UTnYw==\",\n  \"79uTykH43voFC3XhHHUzKg==\",\n  \"7E6V6/zSjbtqraG7Umj+Jw==\",\n  \"7Ephy+mklG2Y3MFdqmXqlA==\",\n  \"7Eqzyb+Kep+dIahYJWNNxQ==\",\n  \"7GgNLBppgAKcgJCDSsRqOQ==\",\n  \"7J3FoFGuTIW36q0PZkgBiw==\",\n  \"7K8l6KoP0BH82/WMLntfrg==\",\n  \"7R5rFaXCxM3moIUtoCfM2g==\",\n  \"7Tauesu7bgs5lJmQROVFiQ==\",\n  \"7VHlLw20dWck+I8tCEZilA==\",\n  \"7W9aF7dxnL+E8lbS/F7brg==\",\n  \"7XRiYvytcwscemlxd9iXIQ==\",\n  \"7Y87wVJok20UfuwkGbXxLg==\",\n  \"7b0oo4+qphu6HRvJq6qkHQ==\",\n  \"7bM/pn4G7g7Zl6Xf1r62Lg==\",\n  \"7br49X11xc2GxQLSpZWjKQ==\",\n  \"7btpMFgeGkUsiTtsmNxGQA==\",\n  \"7cnUHeaPO8txZGGWHL9tKg==\",\n  \"7dz+W494zwU5sg63v5flCg==\",\n  \"7k5rBuh8FbTTI4TP87wBPQ==\",\n  \"7l0RMKbONGS/goW/M+gnMQ==\",\n  \"7mxU5fJl/c6dXss9H3vGcQ==\",\n  \"7nr3zyWL+HHtJhRrCPhYZA==\",\n  \"7p4NpnoNSQR7ISg+w+4yFg==\",\n  \"7pkUY2UzSbGnwLvyRrbxfA==\",\n  \"7sCJ4RxbxRqVnF4MBoKfuQ==\",\n  \"7w3b73nN/fIBvuLuGZDCYQ==\",\n  \"7w4PDRJxptG8HMe/ijL6cQ==\",\n  \"7wgT9WIiMVcrj48PVAMIgw==\",\n  \"7xDIG/80SnhgxAYPL9YJtg==\",\n  \"7xTKFcog69nTmMfr5qFUTA==\",\n  \"80C9TB9/XT1gGFfQDJxRoA==\",\n  \"80PCwYh4llIKAplcDvMj4g==\",\n  \"80UE+Ivby3nwplO/HA7cPw==\",\n  \"81ZH3SO0NrOO+xoR/Ngw1g==\",\n  \"81iQLU+YwxNwq4of6e9z7A==\",\n  \"81nkjWtpBhqhvOp6K8dcWg==\",\n  \"81pAhreEPxcKse+++h1qBg==\",\n  \"82hTTe1Nr4N2g7zwgGjxkw==\",\n  \"83ERX2XJV3ST4XwvN7YWCg==\",\n  \"83WGpQGWyt6mCV+emaomog==\",\n  \"83wtvSoSP9FVBsdWaiWfpA==\",\n  \"861mBNvjIkVgkBiocCUj/Q==\",\n  \"88PNi9+yn3Bp4/upgxtWGA==\",\n  \"88tB/HgUIUnqWXEX++b5Aw==\",\n  \"897ptlztTjr7yk+pk8MT0Q==\",\n  \"8AfCSZC0uasVON9Y/0P2Pw==\",\n  \"8B12CamjOGzJDnQ+RkUf4w==\",\n  \"8BLkvEkfnOizJq0OTCYGzw==\",\n  \"8CjmgWQSAAGcXX9kz3kssw==\",\n  \"8Cm19vJW8ivhFPy0oQXVNA==\",\n  \"8DtgIyYiNFqDc5qVrpFUng==\",\n  \"8GyPup4QAiolFJ9v80/Nkw==\",\n  \"8JVHFRwAd/SCLU0CRJYofg==\",\n  \"8LNNoHe6rEQyJ0ebl151Mw==\",\n  \"8M0kSvjn5KN8bjsMdUqKZQ==\",\n  \"8N3mhHt29FZDHn1P2WH1wQ==\",\n  \"8OFxXwnPmrogpNoueZlC4Q==\",\n  \"8QK7emHS6rAcAF5QQemW/A==\",\n  \"8RtLlzkGEiisy1v9Xo0sbw==\",\n  \"8VqeoQELbCs232+Mu+HblA==\",\n  \"8WU1vLKV1GhrL7oS9PpABg==\",\n  \"8ZBiwr842ZMKphlqmNngHw==\",\n  \"8ZFPMJJYVJHsfRpU4DigSg==\",\n  \"8ZqmPJDnQSOFXvNMRQYG2Q==\",\n  \"8c+lvG5sZNimvx9NKNH3ug==\",\n  \"8cXqZub6rjgJXmh1CYJBOg==\",\n  \"8dBIsHMEAk7aoArLZKDZtg==\",\n  \"8dUcSkd2qnX5lD9B+fUe+Q==\",\n  \"8dbyfox/isKLsnVjQNsEXg==\",\n  \"8fJLQeIHaTnJ8wGqUiKU6g==\",\n  \"8g08gjG/QtvAYer32xgNAg==\",\n  \"8hsfXqi4uiuL+bV1VrHqCw==\",\n  \"8iYdEleTXGM+Wc85/7vU9w==\",\n  \"8j9GVPiFdfIRm/+ho7hpoA==\",\n  \"8nOTDhFyZ8YUA4b6M5p84w==\",\n  \"8snljTGo/uICl9q0Hxy7/A==\",\n  \"8uP4HUnSodw88yoiWXOIcw==\",\n  \"8vLA9MOdmLTo3Qg+/2GzLA==\",\n  \"8vr+ERVrM99dp+IGnCWDGQ==\",\n  \"8ylI1AS3QJpAi3I/NLMYdg==\",\n  \"9+hjTVMQUsvVKs7Tmp52tg==\",\n  \"90dtIMq0ozJXezT2r79vMQ==\",\n  \"91+Yms6Oy/rP0rVjha5z9w==\",\n  \"91LQuW6bMSxl10J/UDX23A==\",\n  \"91SdBFJEZ65M+ixGaprY/A==\",\n  \"91VcAVv7YDzkC1XtluPigw==\",\n  \"91vfsZ7Lx9x5gqWTOdM4sg==\",\n  \"96ORaz1JRHY1Gk8H74+C2g==\",\n  \"99+SBN45LwKCPfrjUKRPmw==\",\n  \"9Bet5waJF5/ZvsYaHUVEjQ==\",\n  \"9DRHdyX8ECKHUoEsGuqR4Q==\",\n  \"9DtM1vls4rFTdrSnQ7uWXw==\",\n  \"9FdpxlIFu11qIPdO7WC5nw==\",\n  \"9Gkw+hvsR/tFY1cO89topg==\",\n  \"9J53kk+InE3CKa7cPyCXMw==\",\n  \"9JKIJrlQjhNSC46H3Cstcw==\",\n  \"9L6yLO93sRN70+3qq3ObfA==\",\n  \"9MDG0WeBPpjGJLEmUJgBWg==\",\n  \"9QFYrCXsGsInUb4SClS3cQ==\",\n  \"9RGIQ2qyevNbSSEF36xk/A==\",\n  \"9RXymE9kCkDvBzWGyMgIWA==\",\n  \"9SUOfKtfKmkGICJnvbIDMg==\",\n  \"9SgfpAY0UhNC6sYGus9GgQ==\",\n  \"9T7gB0ZkdWB0VpbKIXiujQ==\",\n  \"9TalxEyFgy6hFCM73hgb7Q==\",\n  \"9UhKmKtr4vMzXTEn74BEhg==\",\n  \"9W57pTzc572EvSURqwrRhw==\",\n  \"9Y1ZmfiHJd9vCiZ6KfO1xQ==\",\n  \"9aKH1u5+4lgYhhLztQ4KWA==\",\n  \"9ajIS45NTicqRANzRhDWFA==\",\n  \"9bAWYElyRN1oJ6eJwPtCtQ==\",\n  \"9cvHJmim9e0pOaoUEtiM6A==\",\n  \"9dbn0Kzwr9adCEfBJh78uQ==\",\n  \"9iB7+VwXRbi6HLkWyh9/kg==\",\n  \"9inw7xzbqAnZDKOl/MfCqA==\",\n  \"9jxA/t3TQx8dQ+FBsn/YCg==\",\n  \"9k17UqdR1HzlF7OBAjpREA==\",\n  \"9k1u/5TgPmXrsx3/NsYUhg==\",\n  \"9lLhHcrPWI4EsA4fHIIXuw==\",\n  \"9nMltdrrBmM5ESBY2FRjGA==\",\n  \"9oQ/SVNJ4Ye9lq8AaguGAQ==\",\n  \"9oUawSwUGOmb0sDn3XS6og==\",\n  \"9onh6QKp70glZk9cX3s34A==\",\n  \"9pdeedz1UZUlv8jPfPeZ1g==\",\n  \"9pk75mBzhmcdT+koHvgDlw==\",\n  \"9qWLbRLXWIBJUXYjYhY2pg==\",\n  \"9rL8nC/VbSqrvnUtH9WsxQ==\",\n  \"9reBKZ1Rp6xcdH1pFQacjw==\",\n  \"9s3ar9q32Y5A3tla5GW/2Q==\",\n  \"9sYLg75/hudZaBA3FrzKHw==\",\n  \"9tiibT8V9VwnPOErWGNT3w==\",\n  \"9vEgJVJLEfed6wJ7hBUGgQ==\",\n  \"9viAzLFGYYudBYFu7kFamg==\",\n  \"9vmJUS7WIVOlhMqwipAknQ==\",\n  \"9wUIeSgNN36SFxy8v2unVg==\",\n  \"9xIgKpZGqq0/OU6wM5ZSHw==\",\n  \"9xmtuClkFlpz/X5E9JBWBA==\",\n  \"A+DLpIlYyCb9DaarpLN76g==\",\n  \"A2ODff+ImIkreJtDPUVrlg==\",\n  \"A3dX2ShyL9+WOi6MNJBoYQ==\",\n  \"A6TLWhipfymkjPYq8kaoDQ==\",\n  \"AChOz8avRYsvxlbWcorQ3w==\",\n  \"AEpTVUQhIEJGlXJB6rS26A==\",\n  \"AFdelaqvxRj6T3YdLgCFyg==\",\n  \"AGd0rcLnQ0n+meYyJur1Pw==\",\n  \"AGoVLd0QPcXnTedT5T95JQ==\",\n  \"ALJWKUImVE40MbEooqsrng==\",\n  \"ALlGgVDO8So71ccX0D6u2g==\",\n  \"AMfL0rH+g8c0VqOUSgNzQw==\",\n  \"ARCWkHAnVgBOIkCDQ19ZuA==\",\n  \"ARKIvf4+zRF8eCvUITWPng==\",\n  \"ATmMzriwGLl+M3ppkfcZNA==\",\n  \"AUGmvZkpkKBry5bHZn4DJA==\",\n  \"AV/YJfdoDUdRcrXVwinhQg==\",\n  \"AVjwqrTBQH1VREuBlOyUOg==\",\n  \"AX1HxQKXD12Yv5HWi39aPQ==\",\n  \"AYxGETZs477n2sa1Ulu/RQ==\",\n  \"AZs3v4KJYxdi8T1gjVjI2Q==\",\n  \"AcKwfS8FRVqb72uSkDNY/Q==\",\n  \"AcbG0e6xN8pZfYAv7QJe1Q==\",\n  \"Af9j1naGtnZf0u1LyYmK1w==\",\n  \"AfVPdxD3FyfwwNrQnVNQ7A==\",\n  \"AgDJsaW0LkpGE65Kxk5+IA==\",\n  \"Ahpi9+nl13kPTdzL+jgqMw==\",\n  \"AiMtfedwGcddA+XYNc+21g==\",\n  \"AjHz9GkRTFPjrqBokCDzFw==\",\n  \"Ak3rlzEOds6ykivfg39xmw==\",\n  \"AkAes5oErTaJiGD2I4A1Pw==\",\n  \"AklOdt9/2//3ylUhWebHRw==\",\n  \"Al8+d/dlOA5BXsUc5GL8Tg==\",\n  \"Ao1Zc0h5AdSHtYt1caWZnQ==\",\n  \"AoN/pnK4KEUaGw4V9SFjpg==\",\n  \"ApiuEPWr8UjuRyJjsYZQBw==\",\n  \"AqHVaj3JcR44hnMzUPvVYg==\",\n  \"Ar1Eb/f/LtuIjXnnVPYQlA==\",\n  \"Ar9N1VYgE7riwmcrM3bA2Q==\",\n  \"AsAHrIkMgc3RRWnklY9lJw==\",\n  \"AvdeYb9XNOUFWiiz+XGfng==\",\n  \"AwPTZpC28NJQhf5fNiJuLA==\",\n  \"AxEjImKz4tMFieSo7m60Sg==\",\n  \"AyWlT+EGzIXc395zTlEU5Q==\",\n  \"B+TsxQZf0IiQrU8X9S4dsQ==\",\n  \"B0TaUQ6dKhPfSc5V/MjLEQ==\",\n  \"B1VVUbl8pU0Phyl1RYrmBg==\",\n  \"B6reUwMkQFaCHb9BYZExpw==\",\n  \"BA18GEAOOyVXO2yZt2U35w==\",\n  \"BAJ+/jbk2HyobezZyB9LiQ==\",\n  \"BB/R8oQOcoE4j63Hrh8ifg==\",\n  \"BB9PTlwKAWkExt3kKC/Wog==\",\n  \"BDNM1u/9mefjuW1YM2DuBg==\",\n  \"BDbfe/xa9Mz1lVD82ZYRGA==\",\n  \"BH+rkZWQjTp7au6vtll/CQ==\",\n  \"BL3buzSCV78rCXNEhUhuKQ==\",\n  \"BLJk9wA88z6e0IQNrWJIVw==\",\n  \"BLbTFLSb4mkxMaq4/B2khg==\",\n  \"BMOi5JmFUg5sCkbTTffXHw==\",\n  \"BMZB1FwvAuEqyrd0rZrEzw==\",\n  \"BPT4PQxeQcsZsUQl33VGmg==\",\n  \"BTiGLT6XdZIpFBc91IJY6g==\",\n  \"BV1moliPL15M14xkL+H1zw==\",\n  \"BW0A06zoQw7S+YMGaegT7g==\",\n  \"BXGlq54wIH6R3OdYfSSDRw==\",\n  \"BYpHADmEnzBsegdYTv8B5Q==\",\n  \"BYz52gYI/Z6AbYbjWefcEA==\",\n  \"BZTzHJGhzhs3mCXHDqMjnQ==\",\n  \"BaRwTrc5ulyKbW4+QqD0dw==\",\n  \"BhKO1s1O693Fjy1LItR/Jw==\",\n  \"BjfOelfc1IBgmUxMJFjlbQ==\",\n  \"BlCgDd7EYDIqnoAiKOXX6Q==\",\n  \"BophnnMszW5o+ywgb+3Qbw==\",\n  \"Bq82MoMcDjIo/exqd/6UoA==\",\n  \"BuDVDLl0OGdomEcr+73XhQ==\",\n  \"BuENxPg7JNrWXcCxBltOPg==\",\n  \"Bv4mNIC72KppYw/nHQxfpQ==\",\n  \"Bvk8NX4l6WktLcRDRKsK/A==\",\n  \"BwRA+tMtwEvth28IwpZx+w==\",\n  \"BxFP+4o6PSlGN78eSVT1pA==\",\n  \"BxsDnI8jXr4lBwDbyHaYXw==\",\n  \"Byhi4ymFqqH8uIeoMRvPug==\",\n  \"BzkNYH03gF/mQY71RwO3VA==\",\n  \"C+Ssp+v1r+00+qiTy2d7kA==\",\n  \"C4QEzQKGxyRi2rjwioHttA==\",\n  \"C65PZm8rZxJ6tTEb6d08Eg==\",\n  \"C7UaoIEXsVRxjeA0u99Qmw==\",\n  \"CBAGa5l95f3hVzNi6MPWeQ==\",\n  \"CCK+6Dr72G3WlNCzV7nmqw==\",\n  \"CDsanJz7e3r/eQe+ZYFeVQ==\",\n  \"CF1sAlhjDQY/KWOBnSSveA==\",\n  \"CHLHizLruvCrVi9chj9sXA==\",\n  \"CHsFJfsvZkPWDXkA6ZMsDQ==\",\n  \"CJoZn5wdTXbhrWO5LkiW0g==\",\n  \"CLPzjXKGGpJ0VrkSJp7wPQ==\",\n  \"CPDs+We/1wvsGdaiqxzeCQ==\",\n  \"CQ0PPwgdG3N6Ohfwx1C8xA==\",\n  \"CQpJFrpOvcQhsTXIlJli+Q==\",\n  \"CRiL6zpjfznhGXhCIbz8pQ==\",\n  \"CRmAj3JcasAb4iZ9ZbNIbw==\",\n  \"CT3ldhWpS1SEEmPtjejR/Q==\",\n  \"CT9g8mKsIN/VeHLSTFJcNQ==\",\n  \"CUCjG2UaEBmiYWQc6+AS1Q==\",\n  \"CUEueo8QXRxkfVdfNIk/gg==\",\n  \"CWBGcRFYwZ0va6115vV/oQ==\",\n  \"CX/N/lHckmAtHKysYtGdZA==\",\n  \"CXMKIdGvm60bgfsNc+Imvg==\",\n  \"CYJB3qy5GalPLAv1KGFEZA==\",\n  \"CZNoTy26VUQirvYxSPc/5A==\",\n  \"CZbd+UoTz0Qu1kkCS3k8Xg==\",\n  \"CazLJMJjQMeHhYLwXW7YNg==\",\n  \"Ci7sS7Yi1+IwAM3VMAB4ew==\",\n  \"CiiUeJ0LeWfm7+gmEmYXtg==\",\n  \"CkDIoAFLlIRXra78bxT/ZA==\",\n  \"CkZUmKBAGu0FLpgPDrybpw==\",\n  \"Cl1u5nGyXaoGyDmNdt38Bw==\",\n  \"CmBf5qchS1V3C2mS6Rl4bw==\",\n  \"CmVD6nh8b/04/6JV9SovlA==\",\n  \"CmkmWcMK4eqPBcRbdnQvhw==\",\n  \"CnIwpRVC2URVfoiymnsdYQ==\",\n  \"CoLvjQDQGldGDqRxfQo+WQ==\",\n  \"CrJDgdfzOea2M2hVedTrIg==\",\n  \"CsPkyTZADMnKcgSuNu1qxg==\",\n  \"CtDj/h2Q/lRey20G8dzSgA==\",\n  \"CuGIxWhRLN7AalafBZLCKQ==\",\n  \"Cv079ZF55RnbsDT27MOQIA==\",\n  \"Cz1G77hsDtAjpe0WzEgQog==\",\n  \"CzP13PM/mNpJcJg8JD3s6w==\",\n  \"CzSumIcYrZlxOUwUnLR2Zw==\",\n  \"CzWhuxwYbNB/Ffj/uSCtbw==\",\n  \"D09afzGpwCEH0EgZUSmIZA==\",\n  \"D0Qt9sRlMaPnOv1xaq+XUg==\",\n  \"D0W5F7gKMljoG5rlue1jrg==\",\n  \"D175i+2bZ7aWa4quSSkQpA==\",\n  \"D2JcY4zWwqaCKebLM8lPiQ==\",\n  \"D31ZticrjGWAO45l5hFh7A==\",\n  \"D5ibbo8UJMfFZ48RffuhgQ==\",\n  \"D5jaV+HtXkSpSxJPmaBDXg==\",\n  \"D66Suu3tWBD+eurBpPXfjA==\",\n  \"D7piVoB2NJlBxK5owyo4+g==\",\n  \"D7wN7b5u5PKkMaLJBP9Ksw==\",\n  \"DA+3fjr7mgpwf6BZcExj0w==\",\n  \"DB706G73NpBSRS8TKQOVZw==\",\n  \"DBKrdpCE0awppxST4o/zzg==\",\n  \"DCjgaGV5hgSVtFY5tcwkuA==\",\n  \"DCvI9byhw0wOFwF1uP6xIQ==\",\n  \"DDitrRSvovaiXe2nfAtp4g==\",\n  \"DEaZD/8aWV6+zkiLSVN/gA==\",\n  \"DG2Qe2DqPs5MkZPOqX363Q==\",\n  \"DJ+a37tCaGF5OgUhG+T0NA==\",\n  \"DJmrmNRKARzsTCKSMLmcNA==\",\n  \"DJoy1NSZZw87oxWGlNHhfg==\",\n  \"DJscTYNFPyPmTb57g/1w+Q==\",\n  \"DKApp/alXiaPSRNm3MfSuA==\",\n  \"DLzHkTjjuH6LpWHo2ITD0Q==\",\n  \"DMHmyn2U2n+UXxkqdvKpnA==\",\n  \"DO1/jfP/xBI9N0RJNqB2Rw==\",\n  \"DQJRsUwO1fOuGlkgJavcwQ==\",\n  \"DQQB/l55iPN9XcySieNX3A==\",\n  \"DQeib845UqBMEl96sqsaSg==\",\n  \"DQlZWBgdTCoYB1tJrNS5YQ==\",\n  \"DRiFNojs7wM8sfkWcmLnhQ==\",\n  \"DWKsPfKDAtfuwgmc2dKUNg==\",\n  \"DY0IolKTYlW+jbKLPAlYjQ==\",\n  \"DYWCPUq/hpjr6puBE7KBHg==\",\n  \"DbWQI3H2tcJsVJThszfHGA==\",\n  \"DdaT4JLC7U0EkF50LzIj9w==\",\n  \"DdiNGiOSoIZxrMrGNvqkXw==\",\n  \"DinJuuBX9OKsK5fUtcaTcQ==\",\n  \"DjHszpS8Dgocv3oQkW/VZQ==\",\n  \"DjeSrUoWW2QAZOAybeLGJg==\",\n  \"Dk0L/lQizPEb3Qud6VHb1Q==\",\n  \"DmxgZsQg+Qy1GP0fPkW3VA==\",\n  \"Dmyb+a7/QFsU4d2cVQsxDw==\",\n  \"DnF6TYSJxlc+cwdfevLYng==\",\n  \"Do3aqbRKtmlQI2fXtSZfxQ==\",\n  \"DoiItHSms0B9gYmunVbRkQ==\",\n  \"DqzWt1gfyu/e7RQl5zWnuQ==\",\n  \"Dt6hvhPJu94CJpiyJ5uUkg==\",\n  \"Dt8Q5ORzTmpPR2Wdk0k+Aw==\",\n  \"DuEKxykezAvyaFO2/5ZmKQ==\",\n  \"Dulw855DfgIwiK7hr3X8vg==\",\n  \"Duz/8Ebbd0w6oHwOs0Wnwg==\",\n  \"DwOTyyCoUfaSShHZx9u6xg==\",\n  \"DwP0MQf71VsqvAbAMtC3QQ==\",\n  \"DwrNdmU5VFFf3TwCCcptPA==\",\n  \"Dz90OhYEjpaJ/pxwg1Qxhg==\",\n  \"E+02smwQGBIxv42LIF2Y4Q==\",\n  \"E1CvxFbuu9AYW604mnpGTw==\",\n  \"E2LR1aZ3DcdCBuVT7BhReA==\",\n  \"E2v8Kk60qVpQ232YzjS2ow==\",\n  \"E3jMjAgXwvwR8PA53g4+PQ==\",\n  \"E4NtzxQruLcetC23zKVIng==\",\n  \"E4ojRDwGsIiyuxBuXHsKBA==\",\n  \"E8yMPK7W0SIGTK6gIqhxiQ==\",\n  \"E9IlDyULLdeaVUzN6eky8g==\",\n  \"E9ajQQMe02gyUiW3YLjO/A==\",\n  \"E9yeifEZtpqlD0N3pomnGw==\",\n  \"EATnlYm0p3h04cLAL95JgA==\",\n  \"EC0+iUdSZvmIEzipXgj7Gg==\",\n  \"EGLOaMe6Nvzs/cmb7pNpbg==\",\n  \"EJgedRYsZPc4cT9rlwaZhg==\",\n  \"EKU3OVlT4b/8j3MTBqpMNg==\",\n  \"ENFfP93LA257G6pXQkmIdg==\",\n  \"EUXQZwLgnDG+C8qxVoBNdw==\",\n  \"EXveRXjzsjh8zbbQY2pM9g==\",\n  \"EZVQGsXTZvht1qedRLF8bQ==\",\n  \"EbGG4X18upaiVQmPfwKytg==\",\n  \"EdvIAKdRAXj7e42mMlFOGQ==\",\n  \"Ee4A3lTMLQ7iDQ7b8QP8Qg==\",\n  \"EfXDc6h69aBPE6qsB+6+Ig==\",\n  \"Egs14xVbRWjfBBX7X5Z60g==\",\n  \"Ej7W3+67kCIng3yulXGpRQ==\",\n  \"ElTNyMR4Rg8ApKrPw88WPg==\",\n  \"Epm0d/DvXkOFeM4hoPCBrg==\",\n  \"EqMlrz1to7HG4GIFTPaehQ==\",\n  \"EqYq2aVOrdX5r7hBqUJP7g==\",\n  \"Err1mbWJud80JNsDEmXcYg==\",\n  \"EuGWtIbyKToOe6DN3NkVpQ==\",\n  \"Ev/xjTi7akYBI7IeZJ4Igw==\",\n  \"EvSB+rCggob2RBeXyDQRvQ==\",\n  \"Ex3x5HeDPhgO2S9jjCFy4g==\",\n  \"EyIsYQxgFa4huyo/Lomv7g==\",\n  \"EzjbinBHx3Wr08eXpH3HXA==\",\n  \"F50iXjRo1aSTr37GQQXuJA==\",\n  \"F58ktE4O0f7C9HdsXYm+lw==\",\n  \"F5FcNti7lUa9DyF2iEpBug==\",\n  \"F5bs0GGWBx9eBwcJJpXbqg==\",\n  \"F8l+Qd9TZgzV+r8G584lKA==\",\n  \"F8tEIT5EhcvLNRU5f0zlXQ==\",\n  \"FA+nK6mpFWdD0kLFcEdhxA==\",\n  \"FAXzjjIr8l1nsQFPpgxM/g==\",\n  \"FCLQocqxxhJeleARZ6kSPg==\",\n  \"FH5Z60RXXUiDk+dSZBxD3g==\",\n  \"FHvI0IVNvih8tC7JgzvCOw==\",\n  \"FI2WhaSMb3guFLe3e9il8Q==\",\n  \"FIOCTEbzb2+KMCnEdJ7jZw==\",\n  \"FL/j3GJBuXdAo54JYiWklQ==\",\n  \"FLvED9nB9FEl9LqPn7OOrA==\",\n  \"FN7oLGBQGHXXn5dLnr/ElA==\",\n  \"FNvQqYoe0s/SogpAB7Hr1Q==\",\n  \"FUQySDFodnRhr+NUsWt0KA==\",\n  \"FV/D5uSco+Iz8L+5t7E8SA==\",\n  \"FWphIPZMumqnXr1glnbK4w==\",\n  \"FXzaxi3nAXBc8WZfFElQeA==\",\n  \"FbxScyuRacAQkdQ034ShTA==\",\n  \"FcFcn4qmPse5mJCX5yNlsA==\",\n  \"FcKjlHKfQAGoovtpf+DxWQ==\",\n  \"Fd0c8f2eykUp9GYhqOcKoA==\",\n  \"Fd2fYFs8vtjws2kx1gf6Rw==\",\n  \"FeRovookFQIsXmHXUJhGOw==\",\n  \"FhthAO5IkMyW4dFwpFS7RA==\",\n  \"Fiy3hkcGZQjNKSQP9vRqyA==\",\n  \"FltEN+7NKvzt+XAktHpfHA==\",\n  \"FnVNxl5AFH1AieYru2ZG+A==\",\n  \"FoJZ61VrU8i084pAuoWhDQ==\",\n  \"FpWDTLTDmkUhH/Sgo+g1Gg==\",\n  \"FpgdsQ2OG+bVEy3AeuLXFQ==\",\n  \"FqWLkhWl0iiD/u2cp+XK9A==\",\n  \"FrTgaF5YZCNkyfR1kVzTLQ==\",\n  \"Ft2wXUokFdUf6d2Y/lwriw==\",\n  \"FtxpWdhEmC6MT61qQv4DGA==\",\n  \"FuWspiqu5g8Eeli5Az+BkA==\",\n  \"FxnbKnuDct4OWcnFMT/a5w==\",\n  \"Fz8EI+ZpYlbcttSHs5PfpA==\",\n  \"FzqIpOcTsckSNHExrl+9jg==\",\n  \"Fzuq+Wg7clo6DTujNrxsSA==\",\n  \"G+sGF13VXPH4Ih6XgFEXxg==\",\n  \"G/PA+kt0N+jXDVKjR/054A==\",\n  \"G0LChrb0OE5YFqsfTpIL1Q==\",\n  \"G0MlFNCbRjXk4ekcPO/chQ==\",\n  \"G2UponGde3/Z+9b2m9abpQ==\",\n  \"G37U8XTFyshfCs7qzFxATg==\",\n  \"G3PmmPGHaWHpPW30xQgm3Q==\",\n  \"G4qzBI1sFP2faN+tlRL/Bw==\",\n  \"G736AX070whraDxChqUrqw==\",\n  \"G7J/za99BFbAZH+Q+/B8WA==\",\n  \"G8LFBop8u6IIng+gQuVg3w==\",\n  \"GA8k6GQ20DGduVoC+gieRA==\",\n  \"GCYI9Dn1h3gOuueKc7pdKA==\",\n  \"GDMqfhPQN0PxfJPnK1Bb9A==\",\n  \"GF0lY77rx1NQzAsZpFtXIQ==\",\n  \"GF2yvI9UWf1WY7V7HXmKPA==\",\n  \"GFRJoPcXlkKSvJRuBOAYHQ==\",\n  \"GG8a3BlwGrYIwZH9j3cnPA==\",\n  \"GHEdXgGWOeOa6RuPMF0xXg==\",\n  \"GIHKW6plyLra0BmMOurFgA==\",\n  \"GKzs8mlnQQc58CyOBTlfIg==\",\n  \"GLDNTSwygNBmuFwCIm7HtA==\",\n  \"GLmWLXURlUOJ+PMjpWEXVA==\",\n  \"GLnS9wDCje7TOMvBX9jJVA==\",\n  \"GNak/LFeoHWlTdLW1iU4eg==\",\n  \"GNrMvNXQkW7PydlyJa+f1w==\",\n  \"GQJxu1SoMBH14KPV/G/KrQ==\",\n  \"GSWncBq4nwomZCBoxCULww==\",\n  \"GT6WUDXiheKAM7tPg3he9A==\",\n  \"GTNttXfMniNhrbhn92Aykg==\",\n  \"GUiinC3vgBjbQC2ybMrMNQ==\",\n  \"GW1Uaq622QamiiF24QUA0g==\",\n  \"GWwJ32SZqD5wldrXUdNTLA==\",\n  \"GdTanUprpE3X/YjJDPpkhQ==\",\n  \"Gdf4VEDLBrKJNQ8qzDsIyw==\",\n  \"GglPoW5fvr4JSM3Zv99oiA==\",\n  \"GhpJfRSWZigLg/azTssyVA==\",\n  \"Ghuj9hAyfehmYgebBktfgA==\",\n  \"GmC+0rNDMIR+YbUudoNUXw==\",\n  \"GnJKlRzmgKN9vWyGfMq3aA==\",\n  \"GncGQgmWpI/fZyb/6zaFCg==\",\n  \"GrSbnecYAC3j5gtoKntL0A==\",\n  \"Gt4/MMrLBErhbFjGbiNqQQ==\",\n  \"GzbeM7snhe+M+J7X+gAsQw==\",\n  \"H+NHjk/GJDh/GaNzMQSzjg==\",\n  \"H+yPRiooEh5J7lAJB4RZ7Q==\",\n  \"H0UMAUfHFQH92A2AXRCBKA==\",\n  \"H1NJEI+fvOQbI51kaNQQjQ==\",\n  \"H1y2iXVaQYwP0SakN6sa+Q==\",\n  \"H1zH9I8RwfEy5DGz3z+dHw==\",\n  \"H6HPFAcdHFbQUNrYnB74dA==\",\n  \"H6j2nPbBaxHecXruxiWYkA==\",\n  \"HBRzLacCVYfwUVGzrefZYg==\",\n  \"HCbHUfsTDl6+bxPjT57lrA==\",\n  \"HCu4ZMrcLMZbPXbTlWuvvQ==\",\n  \"HDxGhvdQwGh0aLRYEGFqnw==\",\n  \"HEcOaEd9zCoOVbEmroSvJg==\",\n  \"HEghmKg3GN60K7otpeNhaA==\",\n  \"HFCQEiZf7/SNc+oNSkkwlA==\",\n  \"HFHMGgfOeO0UPrray1G+Zw==\",\n  \"HGxe+5/kkh6R9GXzEOOFHA==\",\n  \"HHxn4iIQ7m0tF1rSd+BZBg==\",\n  \"HI4ZIE5s8ez8Rb+Mv39FxA==\",\n  \"HITIVoFoWNg04NExe13dNA==\",\n  \"HJYgUxFZ66fRT8Ka73RaUg==\",\n  \"HK0yf7F97bkf1VYCrEFoWA==\",\n  \"HK9xG03FjgCy8vSR+hx8+Q==\",\n  \"HLesnV3DL+FhWF3h6RXe8g==\",\n  \"HLxROy6fx/mLXFTDSX4eLA==\",\n  \"HMQarkPWOUDIg5+5ja2dBQ==\",\n  \"HMWOlMmzocOIiJ7yG1YaDQ==\",\n  \"HOi+vsGAae4vhr+lJ5ATnQ==\",\n  \"HPvYV94ufwiNHEImu4OYvQ==\",\n  \"HRF3WL/ue3/QlYyu7NUTrA==\",\n  \"HRWYX2XOdsOqYzCcqkwIyw==\",\n  \"HYylUirJRqLm+dkp39fSOQ==\",\n  \"HaHTsLzx7V3G1SFknXpGxA==\",\n  \"HaIRV9SNPRTPDOSX9sK/bg==\",\n  \"HaSc7MZphCMysTy2JbTJkw==\",\n  \"Hb+pdSavvJ9lUXkSVZW8Og==\",\n  \"HbT6W1Ssd3W7ApKzrmsbcg==\",\n  \"HbXv8InyZqFT7i3VrllBgg==\",\n  \"HdB7Se47cWjPgpJN0pZuiA==\",\n  \"HdXg64DBy5WcL5fRRiUVOg==\",\n  \"HeQbUuBM9sqfXFXRBDISSw==\",\n  \"HfvsiCQN/3mT0FabCU5ygQ==\",\n  \"HgIFX42oUdRPu7sKAXhNWg==\",\n  \"HhBHt5lQauNl7EZXpsDHJA==\",\n  \"HiAgt86AyznvbI2pnLalVQ==\",\n  \"HjlPM2FQWdILUXHalIhQ5w==\",\n  \"HjyxyL0db2hGDq2ZjwOOhg==\",\n  \"HkbdaMuDTPBDnt3wAn5RpQ==\",\n  \"Hm6MG6BXbAGURVJKWRM6ZA==\",\n  \"HnVfyqgJ+1xSsN4deTXcIA==\",\n  \"HoaBBw2aPCyhh0f5GxF+/Q==\",\n  \"Hs3vUOOs2TWQdQZHs+FaQQ==\",\n  \"Hst3yfyTB7yBUinvVzYROQ==\",\n  \"HtDXgMuF8PJ1haWk88S0Ew==\",\n  \"HuDuxs2KiGqmeyY1s1PjpQ==\",\n  \"HwLSUie8bzH+pOJT3XQFyg==\",\n  \"HxEU37uBMeiR5y8q/pM42g==\",\n  \"Hy1nqC40l5ItxumkIC2LAA==\",\n  \"I+wVQA+jpPTJ6xEsAlYucg==\",\n  \"I07W2eDQwe6DVsm1zHKM8A==\",\n  \"I5qDndyelK4Njv4YrX7S6w==\",\n  \"I9KNZC1tijiG1T72C4cVqQ==\",\n  \"IA1jmtfpYkz/E2wD0+27WA==\",\n  \"IADk81pIu8NIL/+9Fi94pA==\",\n  \"IAMInfSYb76GxDlAr1dsTg==\",\n  \"ICPdBCdONUqPwD5BXU5lrw==\",\n  \"IEz72W2/W8xBx5aCobUFOQ==\",\n  \"IHhyR6+5sZXTH+/NrghIPg==\",\n  \"IHyIeMad23fSDisblwyfpA==\",\n  \"IKgNa2oPaFVGYnOsL+GC5Q==\",\n  \"INNBBin5ePwTyhPIyndHHg==\",\n  \"IPLD9nT5EEYG9ioaSIYuuA==\",\n  \"ITYL3tDwddEdWSD6J6ULaA==\",\n  \"ITZ3P47ALS0JguFms6/cDA==\",\n  \"IUZ5aGpkJ9rLgSg6oAmMlw==\",\n  \"IUwVHH6+8/0c+nOrjclOWA==\",\n  \"IWZnTJ3Hb9qw9HAK/M9gTw==\",\n  \"IYIP2UBRyWetVfYLRsi1SQ==\",\n  \"IYIbEaErHoFBn8sTT9ICIQ==\",\n  \"IbN736G1Px5bsYqE5gW1JQ==\",\n  \"IdadoCPmSgHDHzn1zyf8Jw==\",\n  \"IdmcpJXyVDajzeiGZixhSA==\",\n  \"IhHyHbHGyQS+VawxteLP0w==\",\n  \"IhpXs1TK7itQ3uTzZPRP5Q==\",\n  \"IindlAnepkazs5DssBCPhA==\",\n  \"IjmLaf3stWDAwvjzNbJpQA==\",\n  \"Ily2MKoFI1zr5LxBy93EmQ==\",\n  \"Iqszlv4R49UevjGxIPMhIA==\",\n  \"IrDuBrVu1HWm0BthAHyOLQ==\",\n  \"Is3uxoSNqoIo5I15z6Z2UQ==\",\n  \"IshzWega6zr3979khNVFQQ==\",\n  \"It+K/RCYMOfNrDZxo7lbcA==\",\n  \"IwLbkL33z+LdTjaFYh93kg==\",\n  \"IwfeA6d0cT4nDTCCRhK+pA==\",\n  \"J/PNYu4y6ZMWFFXsAhaoow==\",\n  \"J/eAtAPswMELIj8K2ai+Xg==\",\n  \"J0NauydfKsACUUEpMhQg8A==\",\n  \"J1nYqJ7tIQK1+a/3sMXI/Q==\",\n  \"J2NFyb8cXEpZyxWDthYQiA==\",\n  \"J4MC9He6oqjOWsYQh9nl3Q==\",\n  \"J8v2f6hWFu8oLuwhOeoQjA==\",\n  \"JATLdpQm//SQnkyCfI5x7Q==\",\n  \"JBkbaBiorCtFq9M9lSUdMg==\",\n  \"JC8Q+8yOJ52NvtVeyHo68w==\",\n  \"JFFeXsFsMA59iNtZey7LAA==\",\n  \"JFHutgSe1/SlcYKIbNNYwQ==\",\n  \"JFi6N1PlrpKaYECOnI7GFg==\",\n  \"JGEy6VP3sz3LHiyT2UwNHQ==\",\n  \"JGeqHRQpf4No74aCs+YTfA==\",\n  \"JGx8sTyvr4bLREIhSqpFkw==\",\n  \"JHBjKpCgSgrNNACZW1W+1w==\",\n  \"JIC8R48jGVqro6wmG2KXIw==\",\n  \"JJJkp1TpuDx5wrua2Wml7g==\",\n  \"JJbzQ/trOeqQomsKXKwUpQ==\",\n  \"JKg64m6mU7C/CkTwVn4ASg==\",\n  \"JKmZqz9cUnj6eTsWnFaB0A==\",\n  \"JKphO0UYjFqcbPr6EeBuqg==\",\n  \"JLq/DrW2f26NaRwfpDXIEA==\",\n  \"JPxEncA4IkfBDvpjHsQzig==\",\n  \"JQf9UmutPh3tAnu7FDk3nA==\",\n  \"JSr/lqDej81xqUvd/O2s7w==\",\n  \"JSyhTcHLTfzHsPrxJyiVrA==\",\n  \"JSyq2MIuObPnEgEUDyALjQ==\",\n  \"JVSLiwurnCelNBiG2nflpQ==\",\n  \"JXCYeWjFqcdSf6QwB54G+A==\",\n  \"JYJvOZ4CHktLrYJyAbdOnA==\",\n  \"JZRjdJLgZ+S0ieWVDj8IJg==\",\n  \"Ja3ECL7ClwDrWMTdcSQ6Ug==\",\n  \"JaYQXntiyznQzrTlEeZMIw==\",\n  \"Jbxl8Nw1vlHO9rtu0q/Fpg==\",\n  \"Jcxjli2tcIAjCe+5LyvqdQ==\",\n  \"Je1UESovkBa9T6wS0hevLw==\",\n  \"JgXSPXDqaS1G9NqmJXZG0A==\",\n  \"JgxNrUlL8wutG04ogKFPvw==\",\n  \"JipruVZx4ban3Zo5nNM37g==\",\n  \"Jit0X0srSNFnn8Ymi1EY+g==\",\n  \"Jj4IrSVpqQnhFrzNvylSzA==\",\n  \"Jm862vBTCYbv/V4T1t46+Q==\",\n  \"JnE6BK0vpWIhNkaeaYNUzw==\",\n  \"JoATsk/aJH0UcDchFMksWA==\",\n  \"JquDByOmaQEpFb47ZJ4+JA==\",\n  \"JrKGKAKdjfAaYeQH8Y2ZRQ==\",\n  \"Js7g8Dr6XsnGURA4UNF0Ug==\",\n  \"Jt4Eg6MJn8O4Ph/K2LeSUA==\",\n  \"Ju4YwtPw+MKzpbC0wJsZow==\",\n  \"JvXTdChcE3AqMbFYTT3/wg==\",\n  \"JyIDGL1m/w+pQDOyyeYupA==\",\n  \"JyUJEnU6hJu8x2NCnGrYFw==\",\n  \"JzW+yhrjXW1ivKu3mUXPXg==\",\n  \"K1CGbMfhlhIuS0YHLG30PQ==\",\n  \"K1RL+tLjICBvMupe7QppIQ==\",\n  \"K1RgR6HR5uDEQgZ32TAFgA==\",\n  \"K2gk9zWGd0lJFRMQ1AjQ/Q==\",\n  \"K3NBEG8jJTJbSrYSOC3FKw==\",\n  \"K4VS+DDkTdBblG93l2eNkA==\",\n  \"K4yZNVoqHjXNhrZzz2gTew==\",\n  \"K5lhaAIZkGeP5rH2ebSJFw==\",\n  \"K8PVQhEJCEH1ghwOdztjRw==\",\n  \"K9A87aMlJC8XB9LuFM913g==\",\n  \"KCJJfgLe00+tjSfP6EBcUg==\",\n  \"KGI/cXVz6v6CfL8H6akcUQ==\",\n  \"KI7tQFYW38zYHOzkKp9/lQ==\",\n  \"KO2XVYyNZadcQv8aCNn5JA==\",\n  \"KOm8PTa+ICgDrgK9QxCJZw==\",\n  \"KOmdvm+wJuZ/nT/o1+xOuw==\",\n  \"KPh6TwYpspne4KZA6NyMbw==\",\n  \"KQw25X4LnQ9is+qdqfxo0w==\",\n  \"KR401XBdgCrtVDSaXqPEiA==\",\n  \"KSorNz/PLR/YYkxaj1fuqw==\",\n  \"KSumhnbKxMXQDkZIpDSWmQ==\",\n  \"KTjwL+qswa+Bid8xLdjMTg==\",\n  \"KXuFON8tMBizNkCC48ICLA==\",\n  \"KXvdjZ3rRKn60djPTCENGA==\",\n  \"KYuUNrkTvjUWQovw9dNakA==\",\n  \"Kh/J1NpDBGoyDU+Mrnnxkg==\",\n  \"KhUT2buOXavGCpcDOcbOYg==\",\n  \"KhrIIHfqXl9zGE9aGrkRVg==\",\n  \"Kj1QI+s9261S3lTtPKd9eg==\",\n  \"KjfL7YyVqmCJGBGDFdJ0gw==\",\n  \"KjnL3x+56r3M2pDj1pPihA==\",\n  \"KkXlgPJPen6HLxbNn5llBw==\",\n  \"KkwQL0DeUM3nPFfHb2ej+A==\",\n  \"KlY5TGg0pR/57TVX+ik1KQ==\",\n  \"KmcGEE0pacQ/HDUgjlt7Pg==\",\n  \"KodYHHN62zESrXUye7M01g==\",\n  \"Koiog/hpN7ew5kgJbty34A==\",\n  \"Kt6BTG1zdeBZ3nlVk+BZKQ==\",\n  \"KuNY8qAJBce+yUIluW8AYw==\",\n  \"KujFdhhgB9q4oJfjYMSsLg==\",\n  \"KyLQxi5UP+qOiyZl0PoHNQ==\",\n  \"KzWdWPP2gH0DoMYV4ndJRg==\",\n  \"Kzs+/IZJO8v4uIv9mlyJ2Q==\",\n  \"L+N/6geuokiLPPSDXM9Qkg==\",\n  \"L2D7G0btrwxl9V4dP3XM5Q==\",\n  \"L2IeUnATZHqOPcrnW2APbA==\",\n  \"L2RofFWDO0fVgSz4D2mtdw==\",\n  \"L3Jt5dHQpWQk74IAuDOL8g==\",\n  \"L4+C6I7ausPl6JbIbmozAg==\",\n  \"LATQEY7f47i77M6p11wjWA==\",\n  \"LCj4hI520tA685Sscq6uLw==\",\n  \"LCvz/h9hbouXCmdWDPGWqg==\",\n  \"LDuBcL5r3PUuzKKZ9x6Kfw==\",\n  \"LEVYAE54618FrlXkDN01Kw==\",\n  \"LFcpCtnSnsCPD2gT/RA+Zg==\",\n  \"LGwcvetzQ3QqKjNh5vA8vw==\",\n  \"LHQETSI5zsejvDaPpsO29g==\",\n  \"LJeLdqmriyAQp+QjZGFkdQ==\",\n  \"LJtRcR70ug6UHiuqbT6NGw==\",\n  \"LKyOFgUKKGUU/PxpFYMILw==\",\n  \"LMCZqd3UoF/kHHwzTdj7Tw==\",\n  \"LMEtzh0+J27+4zORfcjITw==\",\n  \"LPYFDbTEp5nGtG6uO8epSw==\",\n  \"LQttmX92SI94+hDNVd8Gtw==\",\n  \"LSN9GmT6LUHlCAMFqpuPIA==\",\n  \"LUWxfy4lfgB5wUrqCOUisw==\",\n  \"LWWfRqgtph1XrpxF4N64TA==\",\n  \"LWd0+N3M94n81qd346LfJQ==\",\n  \"LZAKplVoNjeQgfaHqkyEJA==\",\n  \"La0gzdbDyXUq6YAXeKPuJA==\",\n  \"LawT9ZygiVtBk0XJ+KkQgQ==\",\n  \"LbPp1oL0t3K2BAlIN+l8DA==\",\n  \"LblwOqNiciHmt2NXjd89tg==\",\n  \"LcF0OqPWrcpHby8RwXz1Yg==\",\n  \"LcoJBEPTlSsQwfuoKQUxEw==\",\n  \"LhqRc9oewY4XaaXTcnXIHQ==\",\n  \"Lo1xTCEWSxVuIGEbBEkVxA==\",\n  \"LoUv/f2lcWpjftzpdivMww==\",\n  \"LpoayYsTO8WLFLCSh2kf2w==\",\n  \"Lqel4GdU0ZkfoJVXI5WC/Q==\",\n  \"LqgzKxbI6WTMz0AMIDJR5w==\",\n  \"LsmsPokAwWNCuC74MaqFCQ==\",\n  \"Lt/pVD4TFRoiikmgAxEWEw==\",\n  \"Lu02ic/E94s42A14m7NGCA==\",\n  \"LyPXOoOPMieqINtX8C9Zag==\",\n  \"LyYPOZKm8bBegMr5NTSBfg==\",\n  \"M/cQja3uIk1im9++brbBOA==\",\n  \"M0ESOGwJ4WZ4Ons1ljP0bQ==\",\n  \"M20iX2sUfw5SXaZLZYlTaA==\",\n  \"M2JMnViESVHTZaru6LDM6w==\",\n  \"M2suCoFHJ5fh9oKEpUG3xA==\",\n  \"M55eersiJuN9v61r8DoAjQ==\",\n  \"M98hjSxCwvZ27aBaJTGozQ==\",\n  \"M9oqlPb63e0kZE0zWOm+JQ==\",\n  \"MArbGuIAGnw4+fw6mZIxaw==\",\n  \"MBjMU/17AXBK0tqyARZP5w==\",\n  \"MFeXfNZy6Q9wBfZmPQy3xg==\",\n  \"MI+HSMRh8KTW+Afiaxd/Fw==\",\n  \"MJ1FuK8PXcmnBAG9meU84A==\",\n  \"MK7AqlJIGqK2+K5mCvMXRQ==\",\n  \"ML7ipnY/g8mA1PUIju1j8Q==\",\n  \"MLHt6Ak288G0RGhCVaOeqA==\",\n  \"MLlVniZ08FHAS5xe+ZKRaA==\",\n  \"MMaegl2Md9s/wOx5o9564w==\",\n  \"MN94B0r5CNAF9sl3Kccdbw==\",\n  \"MOrAbuJTyGKPC6MgYJlx5Q==\",\n  \"MQYM3BT77i35LG9HcqxY2Q==\",\n  \"MQvAr+OOfnYnr/Il/2Ubkg==\",\n  \"MUkRa/PjeWMhbCTq43g6Aw==\",\n  \"MVoxyIA+emaulH8Oks8Weg==\",\n  \"MWcV03ULc0vSt/pFPYPvFA==\",\n  \"MbI04HlTGCoc/6WDejwtaQ==\",\n  \"MdvhC1cuXqni/0mtQlSOCw==\",\n  \"MeKXnEfxeuQu9t3r/qWvcw==\",\n  \"MfkyURTBfkNZwB+wZKjP4g==\",\n  \"Mj87ajJ/yR41XwAbFzJbcA==\",\n  \"Ml3mi1lGS1IspHp3dYYClg==\",\n  \"MlKWxeEh8404vXenBLq4bw==\",\n  \"MlOOZOwcRGIkifaktEq0aQ==\",\n  \"MnStiFQAr3QlaRZ02SYGaQ==\",\n  \"Mofqu40zMRrlcGRLS42eBw==\",\n  \"MpAwWMt7bcs4eL7hCSLudQ==\",\n  \"MqqDg9Iyt4k3vYVW5F+LDw==\",\n  \"Mr5mCtC53+wwmwujOU/fWw==\",\n  \"MrbEUlTagbesBNg0OemHpw==\",\n  \"MrxR3cJaDHp0t3jQNThEyg==\",\n  \"MsCloSmTFoBpm7XWYb+ueQ==\",\n  \"Muf2Eafcf9G3U2ZvQ9OgtQ==\",\n  \"MvMbvZNKbXFe2XdN+HtnpQ==\",\n  \"N+K1ibXAOyMWdfYctNDSZQ==\",\n  \"N/HgDydvaXuJvTCBhG/KtA==\",\n  \"N2KovXW14hN/6+iWa1Yv3g==\",\n  \"N2X7KWekNN+fMmwyXgKD5w==\",\n  \"N3YDSkBUqSmrmNvZZx4a1Q==\",\n  \"N4/mQFyhDpPzmihjFJJn6w==\",\n  \"N65PqIWiQeS082D6qpfrAg==\",\n  \"N7fHwb397tuQHtBz1P80ZQ==\",\n  \"N8dXCawxSBX40fgRRSDqlQ==\",\n  \"N9nD7BGEM7LDwWIMDB+rEQ==\",\n  \"NBmB/cQfS+ipERd7j9+oVg==\",\n  \"ND2hYtAIQGMxBF7o7+u7nQ==\",\n  \"ND9l4JWcncRaSLATsq0LVw==\",\n  \"NDZWIhhixq7NT8baJUR4VQ==\",\n  \"NGApiVkDSwzO45GT57GDQw==\",\n  \"NKGY0ANVZ0gnUtzVx1pKSw==\",\n  \"NKRzJndo2uXNiNppVnqy1g==\",\n  \"NMbAjbnuK7EkVeY3CQI5VA==\",\n  \"NN/ymVQNa17JOTGr6ki3eQ==\",\n  \"NOmu8oZc6CcKLu+Wfz2YOQ==\",\n  \"NQVQfN3nIg9ipHiFh4BvfQ==\",\n  \"NRyFx6jqO/oo9ojvbYzsAg==\",\n  \"NSrzwNlB0bde3ph8k6ZQcQ==\",\n  \"NZtcY8fIpSKPso/KA6ZfzA==\",\n  \"Nc5kiwXCAyjpzt43G5RF1A==\",\n  \"NdULoUDGhIolzw1PyYKV0A==\",\n  \"NdVyHoTbBhX6Umz/9vbi0g==\",\n  \"Ndx5LDiVyyTz/Fh3oBTgvA==\",\n  \"Nf9fbRHm844KZ2sqUjNgkA==\",\n  \"NfxVYc3RNWZwzh2RmfXpiA==\",\n  \"Ng5v/B9Z10TTfsDFQ/XrXQ==\",\n  \"NhZbSq0CjDNOAIvBHBM9zA==\",\n  \"NiQ/m4DZXUbpca9aZdzWAw==\",\n  \"NiawWuMBDo0Q3P2xK/vnLQ==\",\n  \"NjeDgQ1nzH1XGRnLNqCmSg==\",\n  \"NmQrsmb8PVP05qnSulPe5Q==\",\n  \"NmWmDxwK5FpKlZbo0Rt8RA==\",\n  \"NoX8lkY+kd2GPuGjp+s0tQ==\",\n  \"NquRbPn8fFQhBrUCQeRRoQ==\",\n  \"Nr4zGo5VUrjXbI8Lr4YVWQ==\",\n  \"Nsd+DfRX6L54xs+iWeMjCQ==\",\n  \"NtwqUO3SKZE/9MXLbTJo/g==\",\n  \"NuBYjwlxadAH+vLWYRZ3bg==\",\n  \"NvkR0inSzAdetpI4SOXGhw==\",\n  \"NvurnIHin4O+wNP7MnrZ1w==\",\n  \"NxSdT2+MUkQN49pyNO2bJw==\",\n  \"NyF+4VRog7etp90B9FuEjA==\",\n  \"O/EizzJSuFY8MpusBRn7Tg==\",\n  \"O1ckWUwuhD44MswpaD6/rw==\",\n  \"O209ftgvu0vSr0UZywRFXA==\",\n  \"O538ibsrI4gkE5tfwjxjmg==\",\n  \"O5N2yd+QQggPBinQ+zIhtQ==\",\n  \"O7JiE0bbp583G6ZWRGBcfw==\",\n  \"O839JUrR+JS30/nOp428QA==\",\n  \"OChiB4BzcRE8Qxilu6TgJg==\",\n  \"OEJ40VmMDYzc2ESEMontRA==\",\n  \"OERGn45uzfDfglzFFn6JAg==\",\n  \"OFLn4wun6lq484I7f6yEwg==\",\n  \"OGpsXRHlaN8BvZftxh1e7A==\",\n  \"OHJBT2SEv5b5NxBpiAf7oQ==\",\n  \"OIwtfdq37eQ0qoXuB2j7Hw==\",\n  \"OMO4pqzfcbQ11YO4nkTXfg==\",\n  \"OONAvFS/kmH7+vPhAGTNSg==\",\n  \"OOS6wQCJsXH8CsWEidB35A==\",\n  \"OVHqwV8oQMC5KSMzd5VemA==\",\n  \"OaNpzwshdHUZMphQXa6i8w==\",\n  \"Oc3BqTF3ZBW3xE0QsnFn/A==\",\n  \"OlpA9HsF8MBh7b45WZSSlg==\",\n  \"OlwHO6Sg2zIwsCOCRu0HiQ==\",\n  \"Omi2ZB9kdR1HrVP2nueQkA==\",\n  \"Omr+zPWVucPCSfkgOzLmSQ==\",\n  \"OnmvXbyT2BYsSDJYZhLScA==\",\n  \"OpC/sL320wl5anx6AVEL+A==\",\n  \"OpL+vHwPasW30s2E1TYgpA==\",\n  \"OrqJKjRndcZ8OjE3cSQv7g==\",\n  \"Otz/PgYOEZ1CQDW54FWJIQ==\",\n  \"OwArFF1hpdBupCkanpwT+Q==\",\n  \"OwIGvTh8FPFqa4ijNkguAw==\",\n  \"Owg8qCpjZa+PmbhZew6/sw==\",\n  \"OzFRv+PzPqTNmOnvZGoo5g==\",\n  \"OzH7jTcyeM7RPVFtBdakpQ==\",\n  \"OzMR5D2LriC5yrVd5hchnA==\",\n  \"P0Pc8owrqt6spdf7FgBFSw==\",\n  \"P14k+fyz0TG9yIPdojp52w==\",\n  \"P3y5MoXrkRTSLhCdLlnc4A==\",\n  \"P430CeF2MDkuq11YdjvV8A==\",\n  \"P5WPQc5NOaK7WQiRtFabkw==\",\n  \"P5fucOJhtcRIoElFJS4ffg==\",\n  \"P5wS+xB8srW4a5KDp/JVkA==\",\n  \"P7eMlOz9YUcJO+pJy0Kpkw==\",\n  \"P8lUiLFoL100c9YSQWYqDA==\",\n  \"PAlx9+U+yQCAc5Fi0BOG0w==\",\n  \"PBULPuFXb6V3Di713n3Gug==\",\n  \"PCOGl7GIqbizAKj/sZmlwQ==\",\n  \"PD+yHtJxZJ2XEvjIPIJHsQ==\",\n  \"PF0lpolQQXlpc3qTLMBk8w==\",\n  \"PHwJ5ZAqqftZ4ypr8H1qiQ==\",\n  \"PKtXc4x4DEjM45dnmPWzyg==\",\n  \"PMCWKgog/G+GFZcIruSONw==\",\n  \"PMvG4NqJP76kMRAup6TSZA==\",\n  \"PPa7BDMpRdxJdBxkuWCxKA==\",\n  \"PTAm/jGkie7OlgVOvPKpaA==\",\n  \"PTW+fhZq/ErxHqpM0DZwHQ==\",\n  \"PXC6ZpdMH0ATis/jGW12iA==\",\n  \"PaROi5U16Tk35p0EKX5JpA==\",\n  \"ParhxI6RtLETBSwB0vwChQ==\",\n  \"PbDVq2Iw1eeM8c2o/XYdTA==\",\n  \"PbnxuVerGwHyshkumqAARg==\",\n  \"Pc+u0MAzp4lndTz4m6oQ5w==\",\n  \"PcdBtV8pfKU0YbDpsjPgwg==\",\n  \"PcoVtZrS1x1Q+6nfm4f80w==\",\n  \"PdBgXFq5mBqNxgCiqaRnkw==\",\n  \"PeJS+mXnAA6jQ0WxybRQ8w==\",\n  \"PfkWkSbAxIt1Iso0znW0+Q==\",\n  \"PggVPQL5YKqSU/1asihcrg==\",\n  \"PibGJQNw7VHPTgqeCzGUGA==\",\n  \"Po0lhBfiMaXhl+vYh1D8gA==\",\n  \"PolhKCedOsplEcaX4hQ0YQ==\",\n  \"Pp1ZMxJ8yajdbfKM4HAQxA==\",\n  \"PqLCd/pwc+q5GkL6MB0jTg==\",\n  \"Pt3i49uweYVgWze3OjkjJA==\",\n  \"Pu9pEf+Tek3J+3jmQNqrKw==\",\n  \"Pv9FWQEDLKnG/9K9EIz4Gw==\",\n  \"PwvPBc+4L73xK22S9kTrdA==\",\n  \"PxReytUUn/BbxYTFMu1r2Q==\",\n  \"PybPZhJErbRTuAafrrkb3g==\",\n  \"Q0TJZxpn3jk67L7N+YDaNA==\",\n  \"Q1pdQadt12anX1QRmU2Y/A==\",\n  \"Q3TpCE+wnmH/1h/EPWsBtQ==\",\n  \"Q4bfQslDSqU64MOQbBQEUw==\",\n  \"Q6vGRQiNwoyz7bDETGvi5g==\",\n  \"Q7Df6zGwvb4rC+EtIKfaSw==\",\n  \"Q7teXmTHAC5qBy+t7ugf0w==\",\n  \"Q8RVI/kRbKuXa8HAQD7zUA==\",\n  \"QAz7FA+jpz9GgLvwdoNTEQ==\",\n  \"QCpzCTReHxGm5lcLsgwPCA==\",\n  \"QGYFMpkv37CS2wmyp42ppg==\",\n  \"QH36wzyIhh6I56Vnx79hRA==\",\n  \"QH3lAwOYBAJ0Fd5pULAZqw==\",\n  \"QIKjir/ppRyS63BwUcHWmw==\",\n  \"QJEbr3+42P9yiAfrekKdRQ==\",\n  \"QTz21WkhpPjfK8YoBrpo+w==\",\n  \"QV0OG5bpjrjku4AzDvp9yw==\",\n  \"QVwuN66yPajcjiRnVk/V8g==\",\n  \"QWURrsEgxbJ8MWcaRmOWqw==\",\n  \"Qc+XYy2qyWJ5VVwd2PExbw==\",\n  \"Qf7JFJJuuacSzl6djUT2EQ==\",\n  \"Qg1ubGl+orphvT990e5ZPA==\",\n  \"QiozlNcQCbqXtwItWExqJQ==\",\n  \"QmSBVvdk0tqH9RAicXq2zA==\",\n  \"QmcURiMzmVeUNaYPSOtTTg==\",\n  \"QoUC9nyK1BAzoUVnBLV2zw==\",\n  \"QoqHzpHDHTwQD5UF30NruQ==\",\n  \"QozQL0DTtr+PXNKifv6l6g==\",\n  \"Qrh7OEHjp80IW+YzQwzlJg==\",\n  \"QsquNcCZL9wv7oZFqm64vQ==\",\n  \"QtD35QhE8sAccPrDnhtQmQ==\",\n  \"Qv6wWP4PpycDGxe7EZNSCw==\",\n  \"QvYZxsLdu+3nV/WhY1DsYg==\",\n  \"Qx6rVv9Xj8CBjqikWI9KFA==\",\n  \"QyyiJ5I/OZC50o89fa5EmQ==\",\n  \"R+beucURp/H5jLs4kW6wmg==\",\n  \"R/y6+JJP8rzz1KITJ4qWBw==\",\n  \"R1TCCfgltnXBvt5AiUnCtQ==\",\n  \"R2OOV18CV/YpWL1xzr/VQg==\",\n  \"R2Use39If2C0FVBP7KDerA==\",\n  \"R36O31Pj8jn0AWSuqI7X2Q==\",\n  \"R3ijnutzvK6IKV3AKHQZSA==\",\n  \"R5oOM58zdbVxFSDQnNWqeA==\",\n  \"R6Me6sSGP5xpNI8R0xGOWw==\",\n  \"R6cO8GzYfOGTIi773jtkXw==\",\n  \"R81DX/5a7DYKkS4CU+TL+w==\",\n  \"R8FxgXWKBpEVbnl41+tWEw==\",\n  \"R8ULpSNu9FcCwXZM0QedSg==\",\n  \"R906Kxp2VFVR3VD+o6Vxcw==\",\n  \"R97chlspND/sE9/HMScXjQ==\",\n  \"RAAw14BA1ws5Wu/rU7oegw==\",\n  \"RAECgYZmcF4WxcFcZ4A0Ww==\",\n  \"RBMv0IxXEO3o7MnV47Bzow==\",\n  \"RClzwwKh51rbB4ekl99EZA==\",\n  \"RDgGGxTtcPvRg/5KRRlz4w==\",\n  \"REnDNe9mGfqVGZt+GdsmjQ==\",\n  \"RHKCMAqrPjvUYt13BVcmvw==\",\n  \"RHToSGASrwEmvzjX6VPvNQ==\",\n  \"RIVYGO2smx9rmRoDVYMPXw==\",\n  \"RIZYDgXqsIdTf9o2Tp/S7g==\",\n  \"RJJqFMeiCZHdsqs72J17MQ==\",\n  \"RKVDdE1AkILTFndYWi9wFg==\",\n  \"RM5CpIiB94Sqxi462G7caA==\",\n  \"RNK9G1hfuz3ETY/RmA9+aA==\",\n  \"RNdyt6ZRGvwYG5Ws3QTuEA==\",\n  \"ROSt+NlEoiPFtpRqKtDUrQ==\",\n  \"RQOlmzHwQKFpafKPJj0D8w==\",\n  \"RQywrOLZEKw9+kG6qTzr3g==\",\n  \"RUmhye56tQu9xXs4SRJpOQ==\",\n  \"RVD3Ij6sRwwxTUDAxwELtA==\",\n  \"RWI0HfpP7643OSEZR8kxzw==\",\n  \"RYkDwwng6eeffPHxt8iD9A==\",\n  \"RZTpYKxOAH9JgF1QFGN+hw==\",\n  \"RfSwpO/ywQx4lfgeYlBr2w==\",\n  \"RgtwfY5pTolKrUGT+6Pp6g==\",\n  \"RhcqXY4OsZlVVF7ZlkTeRw==\",\n  \"RiahBXX2JbPzt8baPiP/8g==\",\n  \"RkQK9S1ezo+dFYHQP57qrw==\",\n  \"RlNPyhgYOIn28R4vKCVtYA==\",\n  \"RnOXOygwJFqrD+DlM3R5Ew==\",\n  \"RnxOYPSQdHS6fw4KkDJtrA==\",\n  \"RppDe/WGt1Ed6Vqg1+cCkQ==\",\n  \"RqYpA5AY7mKPaSxoQfI1CA==\",\n  \"RrE3B3X/SJi3CqCUlTYwaw==\",\n  \"Rrq0ak9YexLqqbSD4SSXlw==\",\n  \"Rs8deApkoosIJSfX7NXtAA==\",\n  \"RuLeQHP1wHsxhdmYMcgtrQ==\",\n  \"RvXWAFwM+mUAPW1MjPBaHA==\",\n  \"Rvchz/xjcY9uKiDAkRBMmA==\",\n  \"Rww3qkF3kWSd+AaMT0kfdw==\",\n  \"RxmdoO8ak8y/HzMSIm+yBQ==\",\n  \"Ry3zgZ6KHrpNyb7+Tt2Pkw==\",\n  \"RzeH+G3gvuK1z+nJGYqARQ==\",\n  \"S+b37XhKRm8cDwRb1gSsKQ==\",\n  \"S2MAIYeDQeJ1pl9vhtYtUg==\",\n  \"S3VQa6DH+BdlSrxT/g6B5g==\",\n  \"S47hklz3Ow+n5aY6+qsCoA==\",\n  \"S4RvORcJ3m6WhnAgV4YfYA==\",\n  \"S4rFuiKLFKZ+cL7ldiTwpg==\",\n  \"S7Vjy/gOWp0HozPP1RUOZw==\",\n  \"S8jlvuYuankCnvIvMVMzmg==\",\n  \"S9L29U2P5K8wNW+sWbiH7w==\",\n  \"SCO9nQncEcyVXGCtx30Jdg==\",\n  \"SChDh/Np1HyTPWfICfE1uA==\",\n  \"SDi5+FoP9bMyKYp+vVv1XA==\",\n  \"SEGu+cSbeeeZg4xWwsSErQ==\",\n  \"SEIZhyguLoyH7So0p1KY0A==\",\n  \"SESKbGF35rjO64gktmLTWA==\",\n  \"SElc2+YVi3afE1eG1MI7dQ==\",\n  \"SFn78uklZfMtKoz2N0xDaQ==\",\n  \"SIuKH/Qediq0TyvqUF93HQ==\",\n  \"SM7E98MyViSSS9G0Pwzwyw==\",\n  \"SNPYH4r/J9vpciGN2ybP5Q==\",\n  \"SOdpdrk2ayeyv0xWdNuy9g==\",\n  \"SPGpjEJrpflv1hF0qsFlPw==\",\n  \"SPHU6ES1WVm0Mu2LB+YjrA==\",\n  \"SSKhl2L3Mvy93DcZulADtA==\",\n  \"SUAwMWLMml8uGqagz5oqhQ==\",\n  \"SVFbcjXbV7HRg+7jUrzpwg==\",\n  \"SVLHWPCCH7GPVCF7QApPbw==\",\n  \"SVuEYfQ9FGyVMo1672n0Yg==\",\n  \"SbMjjI8/P8B9a9H2G0wHEQ==\",\n  \"Scto+9TWxj1eZgvNKo+a9A==\",\n  \"SfwnYZCKP1iUJyU1yq4eKg==\",\n  \"SiSlasZ+6U2IZYogqr2UPg==\",\n  \"Slu3z535ijcs5kzDnR7kfA==\",\n  \"SmRWEzqddY9ucGAP5jXjAg==\",\n  \"Sr9c0ReRpkDYGAiqSy683g==\",\n  \"Srl4HivgHMxMOUHyM3jvNw==\",\n  \"StDtLMlCI75g4XC59mESEQ==\",\n  \"StoXC7TBzyRViPzytAlzyQ==\",\n  \"StpQm/cQF8cT0LFzKUhC5w==\",\n  \"SusSOsWNoAerAIMBVWHtfA==\",\n  \"Swjn3YkWgj0uxbZ1Idtk+A==\",\n  \"SzCGM8ypE58FLaR1+1ccxQ==\",\n  \"Szko0IPE7RX2+mfsWczrMg==\",\n  \"T/6gSz2HwWJDFIVrmcm8Ug==\",\n  \"T1pMWdoNDpIsHF8nKuOn2A==\",\n  \"T6LA+daQqRI38iDKZTdg1A==\",\n  \"T7waQc3PvTFr0yWGKmFQdQ==\",\n  \"T9WoUJNwp8h4Yydixbx6nA==\",\n  \"TA9WjiLAFgJubLN4StPwLw==\",\n  \"TAD0Lk95CD86vbwrcRogaQ==\",\n  \"TBQpcKq2huNC5OmI2wzRQw==\",\n  \"TDrq23VUdzEU/8L5i8jRJQ==\",\n  \"TGB+FIzzKnouLh5bAiVOQg==\",\n  \"THfzE2G2NVKKfO+A2TjeFw==\",\n  \"THs1r8ZEPChSGrrhrNTlsA==\",\n  \"TI90EuS/bHq/CAlX32UFXg==\",\n  \"TIKadc6FAaRWSQUg5OATgg==\",\n  \"TIWSM78m0RprwgPGK/e0JA==\",\n  \"TLJbasOoVO435E5NE5JDcA==\",\n  \"TNyvLixb03aP2f8cDozzfA==\",\n  \"TSGL3iQYUgVg/O9SBKP9EA==\",\n  \"TSPFvkgw6uLsJh66Ou0H9w==\",\n  \"TVlHoi8J7sOZ2Ti7Dm92cQ==\",\n  \"TXab/hqNGWaSK+fXAoB2bg==\",\n  \"TYlnrwgyeZoRgOpBYneRAg==\",\n  \"TZ3ATPOFjNqFGSKY3vP2Hw==\",\n  \"TZT86wXfzFffjt0f95UF5w==\",\n  \"TafM7nTE5d+tBpRCsb8TjQ==\",\n  \"TahqPgS7kEg+y6Df0HBASw==\",\n  \"TcFinyBrUoAEcLzWdFymow==\",\n  \"TcGhAJHRr7eMwGeFgpFBhg==\",\n  \"TcyyXrSsQsnz0gJ36w4Dxw==\",\n  \"TeBGJCqSqbzvljIh9viAqA==\",\n  \"TfHvdbl2M4deg65QKBTPng==\",\n  \"TfNHjSTV8w6Pg6+FaGlxvA==\",\n  \"TgWe70YalDPyyUz6n88ujg==\",\n  \"Tk5MAqd1gyHpkYi8ErlbWg==\",\n  \"TlJizlASbPtShZhkPww4UA==\",\n  \"Tm4zk2Lmg8w4ITMI31NfTA==\",\n  \"Tmx0suRHzlUK4FdBivwOwA==\",\n  \"Tp52d1NndiC9w3crFqFm9g==\",\n  \"TrLmfgwaNATh24eSrOT+pw==\",\n  \"TrWS+reCJ0vbrDNT5HDR9w==\",\n  \"Tu6w6DtX2RJJ3Ym3o3QAWw==\",\n  \"TuaG3wRdM9BWKAxh2UmAsg==\",\n  \"Tud+AMyuFkWYYZ73yoJGpQ==\",\n  \"Tug3eh+28ttyf+U7jfpg5w==\",\n  \"U+bB5NjFIuQr/Y5UpXHwxA==\",\n  \"U+oTpcjhc0E+6UjP11OE/Q==\",\n  \"U0KmEI6e5zJkaI4YJyA5Ew==\",\n  \"U49SfOBeqQV9wzsNkboi8Q==\",\n  \"U6VQghxOXsydh3Naa5Nz4A==\",\n  \"U9kE50Wq5/EHO03c5hE4Ug==\",\n  \"UAqf4owQ+EmrE45hBcUMEw==\",\n  \"UEMwF4kwgIGxGT4jrBhMPQ==\",\n  \"UHpge5Bldt9oPGo2oxnYvQ==\",\n  \"UIXytIHyVODxlrg+eQoARA==\",\n  \"UK+R+hAoVeZ4xvsoZjdWpw==\",\n  \"UNRlg6+CYVOt68NwgufGNA==\",\n  \"UNdKik7Vy23LjjPzEdzNsg==\",\n  \"UNt7CNMtltJWq8giDciGyA==\",\n  \"UP7NXAE0uxHRXUAWPhto0w==\",\n  \"UP9mmAKzeQqGhod7NCqzhg==\",\n  \"UPYR575ASaBSZIR3aX1IgQ==\",\n  \"UPzS4LR3p/h0u69+7YemrQ==\",\n  \"UQTQk5rrs6lEb1a+nkLwfg==\",\n  \"USCvrMEm/Wqeu9oX6FrgcQ==\",\n  \"USq1iF90eUv41QBebs3bhw==\",\n  \"UTmTgvl+vGiCDQpLXyVgOg==\",\n  \"UVEZPoH9cysC+17MKHFraw==\",\n  \"UXUNYEOffgW3AdBs7zTMFA==\",\n  \"UZoibx+y1YJy/uRSa9Oa2w==\",\n  \"Ua6aO6HwM+rY4sPR19CNFA==\",\n  \"UbABE6ECnjB+9YvblE9CYw==\",\n  \"UbSFw5jtyLk5MealqJw++A==\",\n  \"Ugt8HVC/aUzyWpiHd0gCOQ==\",\n  \"UgvtdE2eBZBUCAJG/6c0og==\",\n  \"Uh1mvZNGehK1AaI4a1auKQ==\",\n  \"Uje3Ild84sN41JEg3PEHDg==\",\n  \"UjmDFO7uzjl4RZDPeMeNyg==\",\n  \"Um1ftRBycvb+363a90Osog==\",\n  \"Umd+5fTcxa3mzRFDL9Z8Ww==\",\n  \"Uo+FIhw1mfjF6/M8cE1c/Q==\",\n  \"Uo1ebgsOxc3eDRds1ah3ag==\",\n  \"UreSZCIdDgloih8KLeX7gg==\",\n  \"UtLYUlQJ02oKcjNR3l+ktg==\",\n  \"Uudn69Kcv2CGz2FbfJSSEA==\",\n  \"UvC1WADanMrhT+gPp/yVqA==\",\n  \"Uw6Iw+TP9ZdZGm2b/DAmkg==\",\n  \"UwqBVd4Wfias4ElOjk2BzQ==\",\n  \"Uy4QI8D2y1bq/HDNItCtAw==\",\n  \"UymZUnEEQWVnLDdRemv+Tw==\",\n  \"UzPPFSXgeV7KW4CN5GIQXA==\",\n  \"V+QzdKh5gxTPp2yPC9ZNEg==\",\n  \"V/xG5QFyx1pihimKmAo8ZA==\",\n  \"V1fvtnJ0L3sluj9nI5KzRw==\",\n  \"V2P75JFB4Se9h7TCUMfeNA==\",\n  \"V5HEaY3v9agOhsbYOAZgJA==\",\n  \"V5HKdaTHjA8IzvHNd9C51g==\",\n  \"V6CRKrKezPwsRdbm0DJ2Yg==\",\n  \"V6zyoX6MERIybGhhULnZiw==\",\n  \"V7eji28JSg3vTi30BCS7gw==\",\n  \"V8m51xgUgywRoV6BGKUrgg==\",\n  \"V8q+xz4ljszLZMrOMOngug==\",\n  \"V9G1we3DOIQGKXjjPqIppQ==\",\n  \"V9vkAanK+Pkc4FGAokJsTA==\",\n  \"VAg/aU5nl72O+cdNuPRO4g==\",\n  \"VCL3xfPVCL5RjihQM59fgg==\",\n  \"VE4sLM5bKlLdk85sslxiLQ==\",\n  \"VGRCSrgGTkBNb8sve0fYnQ==\",\n  \"VH70dN82yPCRctmAHMfCig==\",\n  \"VI8pgqBZeGWNaxkuqQVe7g==\",\n  \"VIC7inSiqzM6v9VqtXDyCw==\",\n  \"VIkS30v268x+M1GCcq/A8A==\",\n  \"VJt2kPVBLEBpGpgvuv1oUw==\",\n  \"VK95g27ws2C6J2h/7rC2qA==\",\n  \"VOB+9Bcfu8aHKGdNO0iMRw==\",\n  \"VOvrzqiZ1EHw+ZzzTWtpsw==\",\n  \"VPa7DG6v7KnzMvtJPb88LQ==\",\n  \"VPqyIomYm7HbK5biVDvlpw==\",\n  \"VQIpquUqmeyt/q6OgxzduQ==\",\n  \"VRnx+kd6VdxChwsfbo1oeQ==\",\n  \"VUDsc9RMS1fSM43c+Jo9dQ==\",\n  \"VWNDBOtjiiI4uVNntOlu/A==\",\n  \"VWb8U4jF/Ic0+wpoXi/y/g==\",\n  \"VWy9lB5t4fNCp4O/4n8S4w==\",\n  \"VX+cVXV8p9i5EBTMoiQOQQ==\",\n  \"VXu4ARjq7DS2IR/gT24Pfw==\",\n  \"VZX1FnyC8NS2k3W+RGQm4g==\",\n  \"VaJc9vtYlqJbRPGb5Tf0ow==\",\n  \"VbCoGr8apEcN7xfdaVwVXw==\",\n  \"VbHoWmtiiPdABvkbt+3XKQ==\",\n  \"Vg2E5qEDfC+QxZTZDCu9yQ==\",\n  \"VhYGC8KYe5Up+UJ2OTLKUw==\",\n  \"Vik8tGNxO0xfdV0pFmmFDw==\",\n  \"ViweSJuNWbx5Lc49ETEs/A==\",\n  \"VjclDY8HN4fSpB263jsEiQ==\",\n  \"VllbOAjeW3Dpbj5lp2OSmA==\",\n  \"VoPth5hDHhkQcrQTxHXbuw==\",\n  \"VpmBstwR7qPVqPgKYQTA3g==\",\n  \"VsXEBIaMkVftkxt1kIh7TA==\",\n  \"Vu0E+IJXBnc25x4n41kQig==\",\n  \"VzQ1NwNv9btxUzxwVqvHQg==\",\n  \"VznvTPAAwAev+yhl9oZT0w==\",\n  \"W+M4BcYNmjj7xAximDGWsA==\",\n  \"W/0s1x3Qm+wN8DhROk6FrQ==\",\n  \"W/5ThNLu43uT1O+fg0Fzwg==\",\n  \"W04GeDh+Tk/I1S85KlozRA==\",\n  \"W2x0SBzSIsTRgyWUCOZ/lg==\",\n  \"W4CfeVp9mXgk04flryL7iA==\",\n  \"W4utAK3ws0zjiba/3i91YA==\",\n  \"W5now3RWSzzMDAxsHSl++Q==\",\n  \"W8bATujVUT80v2XGJTKXDg==\",\n  \"W8y32OLHihfeV0XFw7LmOg==\",\n  \"WADmxH7R6B4LR+W6HqQQ6A==\",\n  \"WBu0gJmmjVdVbjDmQOkU6w==\",\n  \"WGKFTWJac8uehn3N59yHJw==\",\n  \"WHutPin+uUEqtrA7L8878A==\",\n  \"WKehT4nGF2T7aKuzABDMlA==\",\n  \"WLsh3UF4WXdHwgnbKEwRlQ==\",\n  \"WLwpjgr9KzevuogoHZaVUw==\",\n  \"WN7lFJfw4lSnTCcbmt5nsg==\",\n  \"WNfDNaWUOqABQ6c6kR+eyw==\",\n  \"WQMffxULFKJ+bun6NrCURA==\",\n  \"WQznrwqvMhUlM3CzmbhAOQ==\",\n  \"WRjYdKdtnd1G9e/vFXCt0g==\",\n  \"WRoJMO0BCJyn5V6qnpUi4Q==\",\n  \"WTr3q/gDkmB4Zyj7Ly20+w==\",\n  \"WVhfn2yJZ43qCTu0TVWJwA==\",\n  \"WWN44lbUnEdHmxSfMCZc6w==\",\n  \"WY7mCUGvpXrC8gkBB46euw==\",\n  \"WbAdlac/PhYUq7J2+n5f+w==\",\n  \"Wd0dOs7eIMqW5wnILTQBtg==\",\n  \"WdCWezJU4JK43EOZ9YHVdg==\",\n  \"Wf2olJCYZRGTTZxZoBePuQ==\",\n  \"WjDqf1LyFyhdd8qkwWk+MA==\",\n  \"WkSJpxBa45XJRWWZFee7hw==\",\n  \"Wn+Vj4eiWx0WPUHr3nFbyA==\",\n  \"WnHK5ZQDR6Da5cGODXeo0A==\",\n  \"WrJMOuXSLKKzgmIDALkyNw==\",\n  \"WtT0QAERZSiIt2SFDiAizg==\",\n  \"WwraoO97OTalvavjUsqhxQ==\",\n  \"Wx9jh/teM0LJHrvTScssyQ==\",\n  \"WyCFB4+6lVtlzu3ExHAGbQ==\",\n  \"WzjvUJ4jZAEK7sBqw+m07A==\",\n  \"X/Gha4Ajjm/GStp/tv+Jvw==\",\n  \"X1PaCfEDScclLtOTiF5JUw==\",\n  \"X2Tawm2Cra6H7WtXi1Z4Qw==\",\n  \"X2YfnPXgF2VHVX95ZcBaxQ==\",\n  \"X4hrgqMIcApsjA9qOWBoCw==\",\n  \"X4kdXUuhcUqMSduqhfLpxA==\",\n  \"X4o0OkTz0ec70mzgwRfltA==\",\n  \"X6Ln4si8G5aKar52ZH/FEQ==\",\n  \"X6ulLp4noBgefQTsbuIbYQ==\",\n  \"X9QAaNjgiOeAWSphrGtyVw==\",\n  \"XA2hUgq3GVPpxtRYiqnclg==\",\n  \"XAq/C+XyR6m3uzzLlMWO5Q==\",\n  \"XEwOJG24eaEtAuBWtMxhwg==\",\n  \"XF/yncdoT4ruPeXCxEhl9Q==\",\n  \"XGAXhUFjORwKmAq9gGEcRg==\",\n  \"XHHEg/8KZioW/4/wgSEkbQ==\",\n  \"XHjrTLXkm/bBY/BewmJcCQ==\",\n  \"XJihma9zSRrXLC+T+VcFDA==\",\n  \"XLq/nWX8lQqjxsK9jlCqUg==\",\n  \"XOG1PYgqoG8gVLIbVLTQgg==\",\n  \"XSb71ae0v+yDxNF5HJXGbQ==\",\n  \"XTCcsVfEvqxnjc0K5PLcyw==\",\n  \"XV13yK0QypJXmgI+dj4KYw==\",\n  \"XV5MYe0Q7YMtoBD6/iMdSw==\",\n  \"XVVy3e6dTnO3HpgD6BtwQw==\",\n  \"XXFr0WUuGsH5nXPas7hR3Q==\",\n  \"Xconi1dtldH90Wou9swggw==\",\n  \"XddlSluOH6VkR7spFIFmdQ==\",\n  \"XdkxmYYooeDKzy7PXVigBQ==\",\n  \"XePy/hhnQwHXFeXUQQ55Vg==\",\n  \"XfBOCJwi2dezYzLe316ivw==\",\n  \"XfY+QUriCAA1+3QAsswdgg==\",\n  \"XgPHx2+ULpm14IOZU2lrDg==\",\n  \"XjjrIpsmATV/lyln4tPb+g==\",\n  \"Xo8ZjXOIoXlBjFCGdlPuZw==\",\n  \"XpGXh76RDgXC4qnTCsnNHA==\",\n  \"XqFSbgvgZn0CpaZoZiRauQ==\",\n  \"XqTK/2QuGWj50tGmiDxysA==\",\n  \"XqUO7ULEYhDOuT/I2J8BOA==\",\n  \"XqW7UBTobbV4lt1yfh0LZw==\",\n  \"XrFDomoH2qFjQ2jJ2yp9lA==\",\n  \"XsF7R12agx/KkRWl0TyXRA==\",\n  \"Xv0mNYedaBc57RrcbHr9OA==\",\n  \"XwKWd03sAz8MmvJEuN08xA==\",\n  \"Y1Nm3omeWX2MXaCjDDYnWQ==\",\n  \"Y1flEyZZAYxauMo4cmtJ1w==\",\n  \"Y26jxXvl79RcffH8O8b9Ew==\",\n  \"Y5KKN7t/v9JSxG/m1GMPSA==\",\n  \"Y5XR8Igvau/h+c1pRgKayg==\",\n  \"Y5iDQySR2c3MK7RPMCgSrw==\",\n  \"Y78dviyBS3Jq9zoRD5sZtQ==\",\n  \"Y7OofF9eUvp7qlpgdrzvkg==\",\n  \"Y7XpxIwsGK3Lm/7jX/rRmg==\",\n  \"Y7iDCWYrO1coopM3RZWIPg==\",\n  \"YA+zdEC+yEgFWRIgS1Eiqw==\",\n  \"YA0kMTJ82PYuLA4pkn4rfw==\",\n  \"YHM6NNHjmodv+G0mRLK7kw==\",\n  \"YK+q7uJObkQZvOwQ9hplMg==\",\n  \"YLz+HA6qIneP+4naavq44Q==\",\n  \"YNqIHCmBp/EbCgaPKJ7phw==\",\n  \"YPgMthbpcBN2CMkugV60hQ==\",\n  \"YVlRQHQglkbj3J2nHiP/Hw==\",\n  \"YXHQ3JI9+oca8pc/jMH6mA==\",\n  \"YZ39RIXpeLAhyMgmW2vfkQ==\",\n  \"YZt6HwCvdI5DRQqndA/hBQ==\",\n  \"YaUKOTyByjUvp1XaoLiW5Q==\",\n  \"YfbfE3WyYOW7083Y8sGfwQ==\",\n  \"YgVpC5d5V6K/BpOD663yQA==\",\n  \"YhLEPsi/TNyeUJw69SPYzQ==\",\n  \"Yig+Wh18VIqdsmwtwfoUQw==\",\n  \"Yjm5tSq1ejZn3aWqqysNvA==\",\n  \"YmaksRzoU+OwlpiEaBDYaQ==\",\n  \"YmjZJyNfHN5FaTL/HAm8ww==\",\n  \"YodhkayN5wsgPZEYN7/KNA==\",\n  \"YrEP9z2WPQ8l7TY1qWncDA==\",\n  \"YtZ8CYfnIpMd2FFA5fJ+1Q==\",\n  \"Yw4ztKv6yqxK9U1L0noFXg==\",\n  \"Yy2pPhITTmkEwoudXizHqQ==\",\n  \"YzTV0esAxBFVls3e0qRsnA==\",\n  \"Z+bsbVP91KrJvxrujBLrrQ==\",\n  \"Z0sjccxzKylgEiPCFBqPSA==\",\n  \"Z2MkqmpQXdlctCTCUDPyzw==\",\n  \"Z2rwGmVEMCY6nCfHO3qOzw==\",\n  \"Z5B+uOmPZbpbFWHpI9WhPw==\",\n  \"Z8T1b9RsUWf59D06MUrXCQ==\",\n  \"Z9bDWIgcq6XwMoU2ECDR5Q==\",\n  \"ZAQHWU6RMg4IadOxuaukyw==\",\n  \"ZCdad3AwhVArttapWFwT/Q==\",\n  \"ZH5Es/4lJ+D5KEkF1BVSGg==\",\n  \"ZIZx4MehWTVXPN9cVQBmyA==\",\n  \"ZItMIn1vhGqAlpDHclg0Ig==\",\n  \"ZJY+hujfd58mTKTdsmHoQQ==\",\n  \"ZJc7GV0Yb6MrXkpDVIuc8g==\",\n  \"ZKXxq9yr7NGBOHidht34uQ==\",\n  \"ZKeTDCboOgCptrjSfgu0xw==\",\n  \"ZKvox7BaQg4/p5jIX69Umw==\",\n  \"ZNrjP1fLdQpGykFXoLBNPw==\",\n  \"ZQ0ZnTsZKWxbRj7Tilh24Q==\",\n  \"ZQSDYgpsimK+lYGdXBWE/w==\",\n  \"ZRWyfXyXqAaOEjkzWl949Q==\",\n  \"ZRnR6i+5WKMRfs3BDRBCJg==\",\n  \"ZSmN8mmI9lDEHkJqBBg0Nw==\",\n  \"ZV8mEgJweIYk0/l0BFKetA==\",\n  \"ZVnErH1Si4u51QoT0OT7pA==\",\n  \"ZWXfE3uGU91WpPMGyknmqw==\",\n  \"ZXeMG5eqQpZO/SGKC4WQkA==\",\n  \"ZYW30FfgwHmW6nAbUGmwzA==\",\n  \"ZZImGypBWwYOAW43xDRWCQ==\",\n  \"ZaPsR9X77SNt7dLjMJUh8A==\",\n  \"ZbLVNTQSVZQWTNgC4ZGfQg==\",\n  \"ZcuIvc8fDI+2uF0I0uLiVA==\",\n  \"ZfRlID+pC1Rr4IY14jolMw==\",\n  \"ZgdpqFrVGiaHkh9o3rDszg==\",\n  \"ZgjifTVKmxOieco81gnccQ==\",\n  \"ZiJ/kJ9GneF3TIEm08lfvQ==\",\n  \"ZlBNHAiYsfaEEiPQ1z+rCA==\",\n  \"ZlOAnCLV1PkR0kb3E+Nfuw==\",\n  \"ZmVpw1TUVuT13Zw/MNI5hQ==\",\n  \"ZmblZauRqO5tGysY3/0kDw==\",\n  \"ZoNSxARrRiKZF5Wvpg7bew==\",\n  \"Zqd6+81TwYuiIgLrToFOTQ==\",\n  \"ZqjnqxZE/BjOUY0CMdVl0g==\",\n  \"ZqkmoGB0p5uT5J6XBGh7Tw==\",\n  \"ZrCezGLz38xKmzAom6yCTQ==\",\n  \"ZrCnZB/U/vcqEtI1cSvnww==\",\n  \"ZtWvgitOSRDWq7LAKYYd4Q==\",\n  \"ZtmnX24AwYAXHb2ZDC6MeQ==\",\n  \"ZuayB6IpbeITokKGVi9R5w==\",\n  \"ZvvxwDd0I6MsYd7aobjLUA==\",\n  \"ZyDh3vCQWzS5DI1zSasXWA==\",\n  \"ZybIEGf1Rn/26vlHmuMxhw==\",\n  \"ZydKlOpn2ySBW0G3uAqwuw==\",\n  \"ZygAjaN62XhW5smlLkks+Q==\",\n  \"Zyo0fzewcqXiKe2mAwKx5g==\",\n  \"ZyoaR1cMiKAsElmYZqKjLA==\",\n  \"Zz/5VMbw1TqwazReplvsEg==\",\n  \"ZzT5b0dYQXkQHTXySpWEaA==\",\n  \"ZzduJxTnXLD9EPKMn1LI4Q==\",\n  \"a/Y6IAVFv0ykRs9WD+ming==\",\n  \"a1aL8zQ+ie3YPogE3hyFFg==\",\n  \"a4EYNljinYTx9vb1VvUA6A==\",\n  \"a4rPqbDWiMivVzaRxvAj7g==\",\n  \"a5gZ5uuRrXEAjgaoh7PXAg==\",\n  \"a6IszND1m+6w+W+CvseC7g==\",\n  \"a6vem8n6WmRZAalDrHNP0g==\",\n  \"a7Pv1SOWYnkhIUC22dhdDA==\",\n  \"aD4QvtMlr8Lk/zZgZ6zIMg==\",\n  \"aEnHUfn7UE/Euh6jsMuZ7g==\",\n  \"aFJuE/s+Kbge4ppn+wulkA==\",\n  \"aIPde9CtyZrhbHLK740bfw==\",\n  \"aJFbBhYtMbTyMFBFIz/dTA==\",\n  \"aK9nybtiIBUvxgs1iQFgsw==\",\n  \"aLY2pCT0WfFO5EJyinLpPg==\",\n  \"aLh1XEUrfR9W82gzusKcOg==\",\n  \"aMa1yVA71/w6Uf1Szc9rMA==\",\n  \"aMmrAzoRWLOMPHhBuxczKg==\",\n  \"aN5x46Gw1VihRalwCt1CGg==\",\n  \"aOeJZUIZM9YWjIEokFPnzQ==\",\n  \"aRpdnrOyu5mWB1P5YMbvOA==\",\n  \"aRrcmH+Ud3mF1vEXcpEm4w==\",\n  \"aTWiWjyeSDVY/q8y9xc2zg==\",\n  \"aWZRql2IUPVe9hS3dxgVfQ==\",\n  \"aXqiibI6BpW3qilV6izHaQ==\",\n  \"aXrbsro7KLV8s4I4NMi4Eg==\",\n  \"aXs9qTEXLTkN956ch3pnOA==\",\n  \"aY6B28XdPnuYnbOy9uSP8A==\",\n  \"adJAjAFyR2ne1puEgRiH+g==\",\n  \"adT+OjEB2kqpeYi4kQ6FPg==\",\n  \"afMd/Hr3rYz/l7a3CfdDjg==\",\n  \"ahAbmGJZvUOXrcK6OydNGQ==\",\n  \"alJtvTAD7dH/zss/Ek1DMQ==\",\n  \"alqHQBz8V446EdzuVfeY5Q==\",\n  \"anyANMnNkUqr3JuPJz5Qzw==\",\n  \"apWEPWUvMC24Y+2vTSLXoA==\",\n  \"aqcOby9QyEbizPsgO3g0yw==\",\n  \"ash1r2J6B0PUxJe8P0otVQ==\",\n  \"asouSfUjJa8yfMG7BBe+fA==\",\n  \"auvG6kWMnhCMi7c7e9eHrw==\",\n  \"avFTp3rS6z5zxQUZQuaBHQ==\",\n  \"avZp5K7zJvRvJvpLSldNAw==\",\n  \"aw4CzX8pYbPVMuNrGCEcWg==\",\n  \"axEl7xXt/bwlvxKhI7hx4g==\",\n  \"ayBGGPEy++biljvGcwIjXA==\",\n  \"aySnrShOW4/xRSzl/dtSKQ==\",\n  \"ays5/F7JANIgPHN0vp2dqQ==\",\n  \"b06KGv5zDYsTxyTbQ9/eyA==\",\n  \"b0vZfEyuTja2JYMa20Rtbg==\",\n  \"b16O4LF7sVqB7aLU2f3F1A==\",\n  \"b3BQG9/9qDNC/bNSTBY/sQ==\",\n  \"b3q8kjHJPj9DWrz3yNgwjQ==\",\n  \"b4BoZmzVErvuynxirLxn0w==\",\n  \"b4aFwwcWMXsSdgS1AdFOXA==\",\n  \"b53qqLnrTBthRXmmnuXWvw==\",\n  \"b6rrRA0W247O+FfvDHbVCQ==\",\n  \"b85nxzs8xiHxaqezuDVWvg==\",\n  \"b8BZV1NfBdLi70ir4vYvZg==\",\n  \"bA2kaTpeXflTElTnQRp6GQ==\",\n  \"bBEndaOStXBpAK79FrgHaw==\",\n  \"bG+P+p34t/IJ1ubRiWg6IA==\",\n  \"bGGUhiG9SqJMHQWitXTcYQ==\",\n  \"bIk7Fa6SW7X18hfDjTKowg==\",\n  \"bJ1cZW7KsXmoLw0BcoppJg==\",\n  \"bJgsuw29cO2WozqsGZxl7w==\",\n  \"bK045TkBlz+/3+6n6Qwvrg==\",\n  \"bL2FuwsPT7a7oserJQnPcw==\",\n  \"bLEntCrCHFy9pg3T3gbBzg==\",\n  \"bLd38ZNkVeuhf0joEAxnBQ==\",\n  \"bLsStF0DDebpO+xulqGNtg==\",\n  \"bMWFvjM8eVezU1ZXKmdgqw==\",\n  \"bMb1ia0rElr2ZpZVhva0Jw==\",\n  \"bNDKcFu8T5Y6OoLSV+o/Sw==\",\n  \"bNq/hj0Cjt4lkLQeVxDVdQ==\",\n  \"bO55S58bqDiRWXSAIUGJKw==\",\n  \"bPRX2zl+K1S0iWAWUn1DZw==\",\n  \"bQ7J5mebp38rfP/fuqQOsg==\",\n  \"bQKkL+/KUCsAXlwwIH0N3w==\",\n  \"bTNRjJm+FfSQVfd56nNNqQ==\",\n  \"bUF0JIfS4uKd3JZj2xotLQ==\",\n  \"bUxQBaqKyvlSHcuRL9whjg==\",\n  \"bV9r7j2kNJpDCEM5E2339Q==\",\n  \"bWwtTFlhO3xEh/pdw0uWaQ==\",\n  \"bb/U8UynPHwczew/hxLQxw==\",\n  \"bbBsi6tXMVWyq3SDVTIXUg==\",\n  \"beSrliUu0BOadCWmx+yZyA==\",\n  \"bfUD03N2PRDT+MZ+WFVtow==\",\n  \"bhVbgJ4Do4v56D9mBuR/EA==\",\n  \"birqO8GOwGEI97zYaHyAuw==\",\n  \"bjLZ7ot/X/vWSVx4EYwMCg==\",\n  \"bkRdUHAksJZGzE1gugizYQ==\",\n  \"blygTgAHZJ3NzyAT33Bfww==\",\n  \"bs2QG8yYWxPzhtyMqO6u3A==\",\n  \"bsHIShcLS134C+dTxFQHyA==\",\n  \"bvbMJZMHScwjJALxEyGIyg==\",\n  \"bvyB6OEwhwCIfJ6KRhjnRw==\",\n  \"bz294kSG4egZnH2dJ8HwEg==\",\n  \"bzVeU2qM9zHuzf7cVIsSZw==\",\n  \"bzXXzQGZs8ustv0K4leklA==\",\n  \"c1wbFbN7AdUERO/xVPJlgw==\",\n  \"c3WVxyC5ZFtzGeQlH5Gw+w==\",\n  \"c5Tc7rTFXNJqYyc0ppW+Iw==\",\n  \"c5q/8n7Oeffv3B1snHM/lA==\",\n  \"c5ymZKqx/td1MiS2ERiz9A==\",\n  \"c6Yhwy/q3j7skXq52l36Ww==\",\n  \"cBBOQn7ZjxDku0CUrxq2ng==\",\n  \"cFFE2R4GztNoftYkqalqUQ==\",\n  \"cHSj5dpQ04h/WyefjABfmQ==\",\n  \"cHkOsVd80Rgwepeweq4S1g==\",\n  \"cLR0Ry4/N5swqga1R6QDMw==\",\n  \"cMo6l1EQESx1rIo+R4Vogg==\",\n  \"cNsC9bH30eM1EZS6IdEdtQ==\",\n  \"cSHSg9xJz/3F6kc+hKXkwg==\",\n  \"cT3PwwS6ALZA/na9NjtdzA==\",\n  \"cTvDd8okNUx0RCMer6O8sw==\",\n  \"cUyqCa7Oue934riyC17F8g==\",\n  \"cVhdRFuZaW/09CYPmtNv5g==\",\n  \"cWUg7AfqhiiEmBIu+ryImA==\",\n  \"cWdlhVZD7NWHUGte24tMjg==\",\n  \"cXpfd6Io6Glj2/QzrDMCvA==\",\n  \"ca+kx+kf7JuZ3pfYKDwFlg==\",\n  \"caepyBOAFu0MxbcXrGf6TA==\",\n  \"catI+QUNk3uJ+mUBY3bY8Q==\",\n  \"cbBXgB1WQ/i8Xul0bYY2fg==\",\n  \"ccK42Lm8Tsv73YMVZRwL6A==\",\n  \"cchuqe+CWCJpoakjHLvUfA==\",\n  \"ccmy4GVuX967KaQyycmO0w==\",\n  \"ccy3Ke2k4+evIw0agHlh3w==\",\n  \"cdWUm6uLNzR/knuj2x75eA==\",\n  \"cffrYrBX3UQhfX1TbAF+GQ==\",\n  \"cfh5VZFmIqJH/bKboDvtlA==\",\n  \"cgSEbLqqvDsNUyeA3ryJ6Q==\",\n  \"chwv4+xbEAa93PHg8q9zgQ==\",\n  \"ck86G8HsbXflyrK7MBntLg==\",\n  \"ckugAisBNX18eQz+EnEjjw==\",\n  \"cl4t9FXabQg7tbh1g7a0OA==\",\n  \"coGEgMVs2b314qrXMjNumQ==\",\n  \"cszpMdGbsbe6BygqMlnC9Q==\",\n  \"ctJYJegZhG42i+vnPFWAWw==\",\n  \"cu4ZluwohhfIYLkWp72pqA==\",\n  \"cuQslgfqD2VOMhAdnApHrA==\",\n  \"cvMJ714elj/HUh89a9lzOQ==\",\n  \"cvOg7N4DmTM+ok1NBLyBiQ==\",\n  \"cvZT1pvNbIL8TWg+SoTZdA==\",\n  \"cvrGmub2LoJ+FaM5HTPt9A==\",\n  \"cw1gBLtxH/m4H7dSM7yvFg==\",\n  \"cwBNvZc0u4bGABo88YUsVQ==\",\n  \"cxpZ4bloGv734LBf4NpVhA==\",\n  \"cxqHS4UbPolcYUwMMzgoOA==\",\n  \"czBWiYsQtNFrksWwoQxlOw==\",\n  \"d+ctfXU0j07rpRRzb5/HDA==\",\n  \"d/Wd3Ma1xYyoMByPQnA9Cw==\",\n  \"d0NBFiwGlQNclKObRtGVMQ==\",\n  \"d0VAZLbLcDUgLgIfT1GmVQ==\",\n  \"d0qvm3bl38rRCpYdWqolCQ==\",\n  \"d13Rj3NJdcat0K/kxlHLFw==\",\n  \"dAq8/1JSQf1f4QPLUitp0g==\",\n  \"dCDaYYrgASXPMGFRV0RCGg==\",\n  \"dChBe9QR29ObPFu/9PusLg==\",\n  \"dFSavcNwGd8OaLUdWq3sng==\",\n  \"dFetwmFw+D6bPMAZodUMZQ==\",\n  \"dG98w8MynOoX7aWmkvt+jg==\",\n  \"dGjcKAOGBd4gIjJq7fL+qQ==\",\n  \"dGrf9SWJ13+eWS6BtmKCNw==\",\n  \"dJHKDkfMFJeoULg7U4wwDQ==\",\n  \"dK2DU3t1ns+DWDwfBvH3SQ==\",\n  \"dL6n/JsK+Iq6UTbQuo/GOw==\",\n  \"dM9up4vKQV5LeX82j//1jQ==\",\n  \"dMRx4Mf6LrN64tiJuyWmDw==\",\n  \"dNTU+/2DdZyGGTdc+3KMhQ==\",\n  \"dNq2InSVDGnYXjkxPNPRxA==\",\n  \"dOS+mVCy3rFX9FvpkTxGXA==\",\n  \"dRFCIbVu0Y8XbjG5i+UFCQ==\",\n  \"dTMoNd6DDr1Tu8tuZWLudw==\",\n  \"dUx1REyXKiDFAABooqrKEA==\",\n  \"dVh/XMTUIx1nYN4q1iH1bA==\",\n  \"dXDPnL1ggEoBqR13aaW9HA==\",\n  \"dZg5w8rFETMp9SgW7m0gfg==\",\n  \"dZgMquvZmfLqP4EcFaWCiA==\",\n  \"daBhAvmE9shDgmciDAC5eg==\",\n  \"dhTevyxTYAuKbdLWhG47Kw==\",\n  \"dihDsG7+6aocG6M9BWrCzQ==\",\n  \"dmAfbd9F0OJHRAhNMEkRsA==\",\n  \"dml2gqLPsKpbIZ93zTXwCQ==\",\n  \"dnvatwSEcl73ROwcZ4bbIQ==\",\n  \"dpSTNOCPFHN5yGoMpl1EUA==\",\n  \"dqVw2q2nhCvTcW82MT7z0g==\",\n  \"drfODfDI6GyMW7hzkmzQvA==\",\n  \"dsueq9eygFXILDC7ZpamuA==\",\n  \"dtnE401dC0zRWU0S/QOTAg==\",\n  \"duRFqmvqF93uf/vWn8aOmg==\",\n  \"dxWv00FN/2Cgmgq9U3NVDQ==\",\n  \"e/nWuo5YalCAFKsoJmFyFA==\",\n  \"e2xLFVavnZIUUtxJx+qa1g==\",\n  \"e369ZIQjxMZJtopA//G55Q==\",\n  \"e4B3HmWjW+6hQzcOLru6Xg==\",\n  \"e5KCqQ/1GAyVMRNgQpYf6g==\",\n  \"e5l9ZiNWXglpw6nVCtO8JQ==\",\n  \"e5txnNRcGs2a9+mBFcF1Qg==\",\n  \"e9GqAEnk8XI5ix6kJuieNQ==\",\n  \"eAOEgF5N80A/oDVnlZYRAw==\",\n  \"eBapvE+hdyFTsZ0y5yrahg==\",\n  \"eC/RcoCVQBlXdE9WtcgXIw==\",\n  \"eCy/T+a8kXggn1L8SQwgvA==\",\n  \"eDWsx4isnr2xPveBOGc7Hw==\",\n  \"eDcyiPaB954q5cPXcuxAQw==\",\n  \"eFimq+LuHi42byKnBeqnZQ==\",\n  \"eFkXKRd2dwu/KWI5ZFpEzw==\",\n  \"eJDUejE/Ez/7kV+S74PDYg==\",\n  \"eJFIQh/TR7JriMzYiTw4Sg==\",\n  \"eJLrGwPRa6NgWiOrw1pA7w==\",\n  \"eJlcN+gJnqAnctbWSIO9uA==\",\n  \"eKQCVzLuzoCLcB4im8147A==\",\n  \"eLYKLr4labZeLiRrDJ9mnA==\",\n  \"ePlsM/iOMme2jEUYwi15ng==\",\n  \"eQ45Mvf5in9xKrP6/qjYbg==\",\n  \"eRwaYiog2DdlGQyaltCMJg==\",\n  \"eS/vTdSlMUnpmnl1PbHjyw==\",\n  \"eTMPXa60OTGjSPmvR4IgGw==\",\n  \"eV+RwWPiGEB+76bqvw+hbA==\",\n  \"eWgLAqJOU+fdn8raHb9HCw==\",\n  \"eXFOya6x5inTdGwJx/xtUQ==\",\n  \"eYAQWuWZX2346VMCD6s7/A==\",\n  \"eYE9No9sN5kUZ5ePEyS3+Q==\",\n  \"eddhS+FkXxiUnbPoCd5JJw==\",\n  \"edlXkskLx287vOBZ9+gVYg==\",\n  \"ehfPlu6YctzzpQmFiQDxGA==\",\n  \"ehwc2vvwNUAI7MxU4MWQZw==\",\n  \"ejfikwrSPMqEHjZAk3DMkA==\",\n  \"emVLJVzha7ui5OFHPJzeRQ==\",\n  \"enj9VEzLbmeOyYugTmdGfQ==\",\n  \"epY+dsm5EMoXnZCnO4WSHw==\",\n  \"es/L9iW8wsyLeC5S4Q8t+g==\",\n  \"eshD40tvOA6bXb0Fs/cH3A==\",\n  \"etRjRvfL/IwceY/IJ1tgzQ==\",\n  \"euxzbIq4vfGYoY3s1QmLcw==\",\n  \"evaWFoxZNQcRszIRnxqB+A==\",\n  \"ewPT4dM12nDWEDoRfiZZnA==\",\n  \"ewe/P3pJLYu/kMb5tpvVog==\",\n  \"ezsm4aFd6+DO9FUxz0A8Pg==\",\n  \"f/BjtP5fmFw2dRHgocbFlg==\",\n  \"f07bdNVAe9x+cAMdF1bByQ==\",\n  \"f09F7+1LRolRL5nZTcfKGA==\",\n  \"f0H/AFSx2KLZi9kVx5BAZg==\",\n  \"f1+fHgR5rDPsCZOzqrHM7Q==\",\n  \"f1Gs++Iilgq9GHukcnBG3w==\",\n  \"f1h+Vp+xmdZsZIziHrB2+g==\",\n  \"f5Xo7F1uaiM760Qbt978iw==\",\n  \"f6Ye5F0Lkn34uLVDCzogFQ==\",\n  \"f6iLrMpxKhFxIlfRsFAuew==\",\n  \"f9ywiGXsz+PuEsLTV3zIbQ==\",\n  \"fAKFfwlCOyhtdBK6yNnsNg==\",\n  \"fDOUzPTU2ndpbH0vgkgrJQ==\",\n  \"fFvXa1dbMoOOoWZdHxPGjw==\",\n  \"fHL+fHtDxhALZFb9W/uHuw==\",\n  \"fHNpW230mNib08aB7IM3XQ==\",\n  \"fKalNdhsyxTt1w08bv9fJA==\",\n  \"fM5uYpkvJFArnYiQ3MrQnA==\",\n  \"fO0+6TsjL+45p9mSsMRiIg==\",\n  \"fOARCnIg/foF/6tm7m9+3w==\",\n  \"fQS0jnQMnHBn7+JZWkiE/g==\",\n  \"fS471/rN4K2m10mUwGFuLg==\",\n  \"fSANOaHD0Koaqg7AoieY9A==\",\n  \"fU32wmMeD44UsFSqFY0wBA==\",\n  \"fU5ZZ1bIVsV+eXxOpGWo/Q==\",\n  \"fUAy3f9bAglLvZWvkO2Lug==\",\n  \"fVCRaPsTCKEVLkoF4y3zEw==\",\n  \"fW3QZyq5UixIA1mP6eWgqQ==\",\n  \"fX4G68hFL7DmEmjbWlCBJQ==\",\n  \"fY9VATklOvceDfHZDDk57A==\",\n  \"fZrj3wGQSt8RXv0ykJROcQ==\",\n  \"fbTm027Ms0/tEzbGnKZMDA==\",\n  \"fdqt93OrpG13KAJ5cASvkg==\",\n  \"fgXfRuqFfAu8qxbTi4bmhA==\",\n  \"fgdUFvQPb5h+Rqz8pzLsmw==\",\n  \"fhcbn9xE/6zobqQ2niSBgA==\",\n  \"fiv0DJivQeqUkrzDNlluRw==\",\n  \"fmC+85h5WBuk8fDEUWPjtQ==\",\n  \"fo3JL+2kPgDWfP+CCrFlFw==\",\n  \"foPAmiABJ3IXBoed2EgQXA==\",\n  \"foXSDEUwMhfHWJSmSejsQg==\",\n  \"fpXijBOM3Ai1RkmHven5Ww==\",\n  \"fsW2DaKYTCC7gswCT+ByQQ==\",\n  \"fsoXIbq0T0nmSpW8b+bj+g==\",\n  \"fsrX00onlGvfsuiCc35pGg==\",\n  \"ftsf2qztw3NC78ep/CZXWQ==\",\n  \"fv/PW8oexJYWf5De30fdLQ==\",\n  \"fvm0IQfnbfZFETg9v3z/Fg==\",\n  \"fxg/vQq9WPpmQsqQ4RFYaA==\",\n  \"fy54Milpa7KZH/zgrDmMXQ==\",\n  \"fzkmVWKhJsxyCwiqB/ULnQ==\",\n  \"g/z9yk94XaeBRFj4hqPzdw==\",\n  \"g0GbRp2hFVIdc7ct7Ky7ag==\",\n  \"g0aTR8aJ0uVy3YvGYu5xrw==\",\n  \"g0kHTNRI7x/lAsr92EEppw==\",\n  \"g0lWrzEYMntVIahC7i0O2g==\",\n  \"g1ELwsk6hQ+RAY1BH640Pg==\",\n  \"g2nh2xENCFOpHZfdEXnoQA==\",\n  \"g5EzTJ0KA4sO3+Opss3LMg==\",\n  \"g6udffWh7qUnSIo1Ldn3eA==\",\n  \"g6zSo8BvLuKqdmBFM1ejLA==\",\n  \"g8TcogVxHpw7uhgNFt5VCQ==\",\n  \"gAoV4BZYdW1Wm712YXOhWQ==\",\n  \"gB8wkuIzvuDAIhDtNT1gyA==\",\n  \"gBgJF0PiGEfcUnXF0RO7/w==\",\n  \"gC7gUwGumN7GNlWwfIOjJQ==\",\n  \"gDLjxT7vm07arF4SRX5/Vg==\",\n  \"gDxqUdxxeXDYhJk9zcrNyA==\",\n  \"gEHGeR2F82OgBeAlnYhRSw==\",\n  \"gFEnTI8os2BfRGqx9p5x8w==\",\n  \"gGLz3Ss+amU7y6JF09jq7A==\",\n  \"gICaI06E9scnisonpvqCsA==\",\n  \"gK7dhke5ChQzlYc/bcIkcg==\",\n  \"gR0sgItXIH8hE4FVs9Q07w==\",\n  \"gR3B8usSEb0NLos51BmJQg==\",\n  \"gTB2zM3RPm27mUQRXc/YRg==\",\n  \"gTnsH3IzALFscTZ1JkA9pw==\",\n  \"gU3gu8Y5CYVPqHrZmLYHbQ==\",\n  \"gUNP5w7ANJm257qjFxSJrA==\",\n  \"gW0oKhtQQ7BxozxUWw5XvQ==\",\n  \"gXlb7bbRqHXusTE5deolGA==\",\n  \"gYGQBLo5TdMyXks0LsZhsQ==\",\n  \"gYgCu/qUpXWryubJauuPNw==\",\n  \"gYnznEt9r97haD/j2Cko7g==\",\n  \"gYvdNJCDDQmNhtJ6NKSuTA==\",\n  \"gZNJ1Qq6OcnwXqc+jXzMLQ==\",\n  \"gZWTFt5CuLqMz6OhWL+hqQ==\",\n  \"gaEtlJtD6ZjF5Ftx0IFt0A==\",\n  \"gf1Ypna/Tt+TZ08Y+GcvGg==\",\n  \"gfhkPuMvjoC3CGcnOvki3Q==\",\n  \"gfnbviaVhKvv1UvlRGznww==\",\n  \"ggIfX1J4dX3xQoHnHUI7VA==\",\n  \"gglLMohmJDPRGMY1XKndjQ==\",\n  \"ghp8sWGKWw20S/z1tbTxFg==\",\n  \"ginkFyNVMwkZLE49AbfqfA==\",\n  \"gkrg0NR0iCaL7edq0vtewA==\",\n  \"glnqaRfwm6NxivtB2nySzw==\",\n  \"gnAIpoCyl3mQytLFgBEgGA==\",\n  \"gnez1VrH+UHT8C/SB9qGdA==\",\n  \"gnkadeCgjdmLdlu/AjBZJg==\",\n  \"goSgZ8N5UbT5NMnW3PjIlQ==\",\n  \"gqehq46BhFX2YLknuMv02w==\",\n  \"gsC/mWD8KFblxB0JxNuqJw==\",\n  \"gvvyX5ATi4q9NhnwxRxC8w==\",\n  \"gwyVIrTk5o0YMKQq4lpJ+Q==\",\n  \"gxwbqZDHLbQVqXjaq42BCg==\",\n  \"h+KRDKIvyVUBmRjv1LcCyg==\",\n  \"h0MH5NGFfChgmRJ3E/R3HQ==\",\n  \"h13Xuonj+0dD1xH86IhSyQ==\",\n  \"h1NNwMy0RjQmLloSw1hvdg==\",\n  \"h2B0ty0GobQhDnFqmKOpKQ==\",\n  \"h2cnQQF2/R3Mq2hWdDdrTg==\",\n  \"h3vYYI9yhpSZV2MQMJtwFQ==\",\n  \"h5HsEsObPuPFqREfynVblw==\",\n  \"h7Fc+eT/GuC8iWI+YTD0UQ==\",\n  \"hCzsi1yDv9ja5/o7t94j9Q==\",\n  \"hDGa2yLwNvgBd/v6mxmQaQ==\",\n  \"hDILjSpTLqJpiSSSGu445A==\",\n  \"hIABph+vhtSF5kkZQtOCTA==\",\n  \"hIJA+1QGuKEj+3ijniyBSQ==\",\n  \"hIjgi20+km+Ks23NJ4VQ6Q==\",\n  \"hJ8leLNuJ6DK5V8scnDaZQ==\",\n  \"hJSP7CostefBkJrwVEjKHA==\",\n  \"hK8KhTFcR06onlIJjTji/Q==\",\n  \"hKOsXOBoFTl/K4xE+RNHDA==\",\n  \"hN9bmMHfmnVBVr+7Ibd2Ng==\",\n  \"hNHqznsrIVRSQdII6crkww==\",\n  \"hP7dSa8lLn9KTE/Z0s4GVQ==\",\n  \"hPnPQOhz4QKhZi02KD6C+A==\",\n  \"hRxbdeniAVFgKUgB9Q3Y+g==\",\n  \"hSNZWNKUtDtMo6otkXA/DA==\",\n  \"hSkY45CeB6Ilvh0Io4W6cg==\",\n  \"hUWqqG1QwYgGC5uXJpCvJw==\",\n  \"hW9DJA1YCxHmVUAF7rhSmQ==\",\n  \"hWoxz5HhE50oYBNRoPp1JQ==\",\n  \"hY82j+sUQQRpCi6CCGea5A==\",\n  \"hZlX6qOfwxW5SPfqtRqaMw==\",\n  \"hdzol5dk//Q6tCm4+OndIA==\",\n  \"hf9HFxWRNX2ucH8FLS7ytA==\",\n  \"hfcH5Az2M7rp+EjtVpPwsg==\",\n  \"hiYg+aVzdBUDCG0CXz9kCw==\",\n  \"hkOBNoHbno2iNR7t3/d4vg==\",\n  \"hlMumZ7RJFpILuKs09ABtw==\",\n  \"hlu7os0KtAkpBTBV6D2jyQ==\",\n  \"hlvtFGW8r0PkbUAYXEM+Hw==\",\n  \"hnCUnoxofUiqQvrxl73M8w==\",\n  \"hq35Fjgvrcx6I9e6egWS4w==\",\n  \"hqeSvwu8eqA072iidlJBAw==\",\n  \"htDbVu1xGhCRd8qoMlBoMg==\",\n  \"htNVAogFakQkTX6GHoCVXg==\",\n  \"hv5GrLEIjPb4bGOi8RSO0w==\",\n  \"hvsZ5JmVevK1zclFYmxHaw==\",\n  \"hy303iin+Wm7JA6MeelwiQ==\",\n  \"i2sSvrTh/RdLJX0uKhbrew==\",\n  \"i42XumprV/aDT5R0HcmfIQ==\",\n  \"i6ZYpFwsyWyMJNgqUMSV1A==\",\n  \"i6r+mZfyhZyqlYv56o0H+w==\",\n  \"i8XXN7jcrmhnrOVDV8a2Hw==\",\n  \"i9IRqAqKjBTppsxtPB7rdw==\",\n  \"iANKiuMqWzrHSk9nbPe3bQ==\",\n  \"iCF+GWw9/YGQXsOOPAnPHQ==\",\n  \"iCnm5fPmSmxsIzuRK6osrA==\",\n  \"iFtadcw8v6betKka9yaJfg==\",\n  \"iGI9uqMoBBAjPszpxjZBWQ==\",\n  \"iGuY4VxcotHvMFXuXum7KA==\",\n  \"iGykaF+h4p46HhrWqL8Ffg==\",\n  \"iIWxFdolLcnXqIjPMg+5kQ==\",\n  \"iIm8c9uDotr87Aij+4vnMw==\",\n  \"iJ2nT8w8LuK11IXYqBK+YA==\",\n  \"iK0dWKHjVVexuXvMWJV9pg==\",\n  \"iPwX3SbbG9ez9HoHsrHbKw==\",\n  \"iQ304I1hmLZktA1d1cuOJA==\",\n  \"iS9wumBV5ktCTefFzKYfkA==\",\n  \"iSeH0JFSGK73F470Rhtesw==\",\n  \"iUsUCB0mfRsE9KPEQctIzw==\",\n  \"iVDd2Zk7vwmEh97LkOONpQ==\",\n  \"iWNlSnwrtCmVF89B+DZqOQ==\",\n  \"ibsb1ncaLZXAYgGkMO7tjQ==\",\n  \"ieEAgvK9LsWh2t6DsQOpWA==\",\n  \"ifZM0gBm9g9L09YlL+vXBg==\",\n  \"ifuJCv9ZA84Vz1FYAPsyEA==\",\n  \"ilBBNK/IV69xKTShvI94fQ==\",\n  \"imZ+mwiT22sW2M9alcUFfg==\",\n  \"inrUwXyKikpOW0y2Kl1wGw==\",\n  \"ionqS0piAOY2LeSReAz4zg==\",\n  \"ipPPjxpXHS1tcykXmrHPMQ==\",\n  \"irnD9K8bsT+up/JUrxPw6A==\",\n  \"iruDC5MeywV4yA8o1tw/KQ==\",\n  \"isep9d+Q7DEUf0W7CJJYzw==\",\n  \"itPtn+JaO4i7wz2wOPOmDQ==\",\n  \"iu5csar0IQQBOTgw5OvJwQ==\",\n  \"iujlt9fXcUXEYc+T2s5UjA==\",\n  \"iwKBOGDTFzV4aXgDGfyUkw==\",\n  \"izeyFvXOumNgVyLrbKW45g==\",\n  \"j+8/VARfbQSYhHzj0KPurQ==\",\n  \"j+lDhAnWAyso+1N8cm85hQ==\",\n  \"j4FBMnNfdBwx0VsDeTvhFg==\",\n  \"j8nMH8mK/0Aae7ZkqyPgdg==\",\n  \"j8to4gtSIRYpCogv2TESuQ==\",\n  \"jCgdKXsBCgf7giUKnr6paQ==\",\n  \"jEdanvXKyZdZJG6mj/3FWw==\",\n  \"jEqP0dyHKHiUjZ9dNNGTlQ==\",\n  \"jGHMJqbj6X1NdTDyWmXYAQ==\",\n  \"jHOoSl3ldFYr9YErEBnD3w==\",\n  \"jKJn4czwUl/6wtZklcMsSg==\",\n  \"jLI3XpVfjJ6IzrwOc4g9Pw==\",\n  \"jLkmUZ6fV56GfhC0nkh4GA==\",\n  \"jMZKSMP2THqwpWqJNJRWdw==\",\n  \"jNJQ6otieHBYIXA9LjXprg==\",\n  \"jNcMS2zX1iSZN9uYnb2EIg==\",\n  \"jOPdd330tB6+7C29a9wn0Q==\",\n  \"jQVlDU+HjZ2OHSDBidxX5A==\",\n  \"jQjyjWCEo9nWFjP4O8lehw==\",\n  \"jS0JuioLGAVaHdo/96JFoQ==\",\n  \"jTg9Y6EfpON4CRFOq0QovA==\",\n  \"jTmPbq+wh30+yJ/dRXk1cA==\",\n  \"jV/D2B11NLXZRH77sG9lBw==\",\n  \"jWsC7kdp2YmIZpfXGUimiA==\",\n  \"jZMDIu95ITTjaUX0pk4V5g==\",\n  \"jd6IpPJwOJW1otHKtKZ5Gw==\",\n  \"jdRzkUJrWxrqoyNH9paHfQ==\",\n  \"jdVMQqApseHH3fd91NFhxg==\",\n  \"jfegbZSZWkDoPulFomVntA==\",\n  \"jgNijyoj2JrQNSlUv4gk4A==\",\n  \"ji+1YHlRvzevs3q5Uw1gfA==\",\n  \"ji306HRiq965zb8EZD2uig==\",\n  \"jiV+b/1EFMnHG6J0hHpzBg==\",\n  \"jjNMPXbmpFNsCpWY0cv3eg==\",\n  \"jkUpkLoIXuu7aSH8ZghIAQ==\",\n  \"joDXdLpXvRjOqkRiYaD/Sw==\",\n  \"jon1y9yMEGfiIBjsDeeJdA==\",\n  \"jp5Em/0Ml4Txr1ptTUQjpg==\",\n  \"jpNUgFnanr9Sxvj2xbBXZw==\",\n  \"jpjpNjL1IKzJdGqWujhxCw==\",\n  \"jqPQ0aOuvOJte/ghI1RVng==\",\n  \"jrRH0aTUYCOpPLZwzwPRfQ==\",\n  \"jrfRznO0nAz6tZM1mHOKIA==\",\n  \"jt9Ocr9D8EwGRgrXVz//aQ==\",\n  \"jx7rpxbm1NaUMcE2ktg5sA==\",\n  \"jz7QlwxCIzysP39Cgro8jg==\",\n  \"k+IBS52XdOe5/hLp28ufnA==\",\n  \"k/Aou2Jmyh8Bu3k8/+ndsQ==\",\n  \"k/OVIllJvW6BefaLEPq7DA==\",\n  \"k/pBSWE2BvUsvJhA9Zl5uw==\",\n  \"k0XIjxp2vFG7sTrKcfAihA==\",\n  \"k1DPiH6NkOFXP/r3N12GyA==\",\n  \"k2KP9oPMnHmFlZO6u6tgyw==\",\n  \"k6OmSlaSZ5CB0i7SD9LczQ==\",\n  \"k8eZxqwxiN/ievXdLSEL/w==\",\n  \"kBAB2PSjXwqoQOXNrv80AA==\",\n  \"kFrRjz7Cf2KvLtz9X6oD+w==\",\n  \"kGeXrHEN6o7h5qJYcThCPw==\",\n  \"kHcBZXoxnFJ+GMwBZ/xhfQ==\",\n  \"kIGxCUxSlNgsKZ45Al1lWw==\",\n  \"kJdY3XEdJS/hyHdR+IN0GA==\",\n  \"kMUdiwM7WR8KGOucLK4Brw==\",\n  \"kNGIV3+jQmJlZDTXy1pnyA==\",\n  \"kRnBEH6ILR5GNSmjHYOclw==\",\n  \"kSUectNPXpXNg+tIveTFRw==\",\n  \"kTCHqcb3Cos51o8cL+MXcg==\",\n  \"kUhyc3G8Zvx8+q5q5nVEhw==\",\n  \"kUudvRfA33uJDzHIShQd3Q==\",\n  \"kWPUUi7x9kKKa6nJ+FDR5Q==\",\n  \"kZ/mZZg9YSDmk2rCGChYAg==\",\n  \"kZ0D191c/uv4YMG15yVLDw==\",\n  \"kZkmDatUOdIqs7GzH3nI1A==\",\n  \"ka7pMp8eSiv92WgAsz2vdA==\",\n  \"kcJ1acgBv6FtUhV8KuWoow==\",\n  \"kgKWQJJQKLUuD2VYKIKvxA==\",\n  \"kggaIvN2tlbZdZRI8S5Apw==\",\n  \"kgyUtd8MFe0tuuxDEUZA9w==\",\n  \"kh51WUI5TRnKhur6ZEpRTQ==\",\n  \"kj5WqpRCjWAfjM7ULMcuPQ==\",\n  \"kjWYVC7Eok2w2YT4rrI+IA==\",\n  \"kkbX+a00dfiTgbMI+aJpMg==\",\n  \"kly/2kE4/7ffbO34WTgoGg==\",\n  \"knYKU74onR6NkGVjQLezZg==\",\n  \"kq26VyDyJTH/eM6QvS2cMw==\",\n  \"kr8tw1+3NxoPExnAtTmfxg==\",\n  \"ksOFI9C7IrDNk4OP6SpPgw==\",\n  \"kuWGANwzNRpG4XmY7KjjNg==\",\n  \"kvAaIJb+aRAfKK104dxFAA==\",\n  \"kwlAQhR2jPMmfLTAwcmoxw==\",\n  \"kydoXVaNcx1peR5g6i588g==\",\n  \"kzGNkWh3fz27cZer4BspUQ==\",\n  \"kzTl7WH/JXsX1fqgnuTOgw==\",\n  \"kzXsrxWRnWhkA82LsLRYog==\",\n  \"kzYddqiMsY3EYrpxve2/CQ==\",\n  \"l+x2QhxG8wb5AQbcRxXlmA==\",\n  \"l0E0U/CJsyCVSTsXW4Fp+w==\",\n  \"l2NppPcweAtmA1V2CNdk2Q==\",\n  \"l2ZB9TvT68rn8AAN4MdxWw==\",\n  \"l2mAbuFF3QBIUILDODiUHQ==\",\n  \"l4ddTxbTCW5UmZW+KRmx6A==\",\n  \"l5f3I6osM9oxLRAwnUnc5A==\",\n  \"l6QHU5JsJExNoOnqxBPVbw==\",\n  \"l6Ssc04/CnsqUua9ELu2iQ==\",\n  \"l8/KMItWaW3n4g1Yot/rcQ==\",\n  \"lC5EumoIcctvxYqwELqIqw==\",\n  \"lFUq6PGk9dBRtUuiEW7Cug==\",\n  \"lHN2dn2cUKJ8ocVL3vEhUQ==\",\n  \"lJFPmPWcDzDp5B2S8Ad8AA==\",\n  \"lK2xe+OuPutp4os0ZAZx5w==\",\n  \"lM/EhwTsbivA7MDecaVTPw==\",\n  \"lMaO8Yf+6YNowGyhDkPhQA==\",\n  \"lMjip5hbCjkD9JQjuhewDg==\",\n  \"lNF8PvUIN02NattcGi5u4g==\",\n  \"lON3WM0uMJ30F8poBMvAjQ==\",\n  \"lOPJhHqCtMRFZfWMX/vFZQ==\",\n  \"lTE6u9G/RzvmbuAzq2J2/Q==\",\n  \"lV70RNlE++04G1KFB3BMXA==\",\n  \"lY+tivtsfvU0LJzBQ6itYQ==\",\n  \"lacCCRiWdquNm4YRO7FoKA==\",\n  \"leDlMcM+B1mDE8k5SWtUeg==\",\n  \"lf1fwA0YoWUZaEybE+LyMQ==\",\n  \"lfOLLyZNbsWQgHRhicr4ag==\",\n  \"lffapwUUgaQOIqLz2QPbAg==\",\n  \"lhAOM81Ej6YZYBu45pQYgg==\",\n  \"lizovLQxu6L9sbafNQuShQ==\",\n  \"lkl6XkrTMUpXi46dPxTPxg==\",\n  \"lkzFdvtBx5bV6xZO0cxK7g==\",\n  \"ll2M0QQzBsj5OFi02fv3Yg==\",\n  \"llOvGOUDVfX68jKnAlvVRA==\",\n  \"llujnWE17U8MIHmx4SbrSA==\",\n  \"lqhgbgEqROAdfzEnJ17eXA==\",\n  \"lsBTMnse2BgPS6wvPbe7JA==\",\n  \"luO1R8dUM9gy1E2lojRQoA==\",\n  \"luR/kvHLwA6tSdLeTM4TzA==\",\n  \"lwYQm2ynA3ik2gE1m11IEg==\",\n  \"lyfqic/AbEJbCiw+wA01FA==\",\n  \"lz+SeifYXxamOLs1FsFmSQ==\",\n  \"lzUQ1o7JAbdJYpmEqi6KnQ==\",\n  \"m+eh+ZqS74w2q0vejBkjaw==\",\n  \"m/Lp4U75AQyk9c8cX14HJg==\",\n  \"m06wctjNc3o7iyBHDMZs2w==\",\n  \"m3XYojKO+I6PXlVRUQBC3w==\",\n  \"m416yrrAlv+YPClGvGh+qQ==\",\n  \"m5JIUETVXcRza4VL4xlJbg==\",\n  \"m6get5wjq5j1i5abnpXuZQ==\",\n  \"m6srF+pMehggHB1tdoxlPg==\",\n  \"m9iuy4UtsjmyPzy6FTTZvw==\",\n  \"mAiD16zf+rCc7Qzxjd5buA==\",\n  \"mAzsVkijuqihhmhNTTz65g==\",\n  \"mDXHuOmI4ayjy2kLSHku1Q==\",\n  \"mI0eT4Rlr7QerMIngcu/ng==\",\n  \"mMLhjdWNnZ8zts9q+a2v3g==\",\n  \"mMfn8OaKBxtetweulho+xQ==\",\n  \"mNlYGAOPc6KIMW8ITyBzIg==\",\n  \"mNv2Q67zePjk/jbQuvkAFA==\",\n  \"mPk1IsU5DmDFA/Ym5+1ojw==\",\n  \"mPwCyD0yrIDonVi+fhXyEQ==\",\n  \"mS99D+CXhwyfVt8xJ+dJZA==\",\n  \"mSJF9dJnxZ15lTC6ilbJ2A==\",\n  \"mSstwJq7IkJ0JBJ5T8xDKg==\",\n  \"mTAqtg6oi0iytHQCaSVUsA==\",\n  \"mTLBkP+yGHsdk5g7zLjVUw==\",\n  \"mU4CqbAwpwqegxJaOz9ofQ==\",\n  \"mUek9NkXm8HiVhQ6YXiyzA==\",\n  \"mVT74Eht+gAowINoMKV7IQ==\",\n  \"mW6TCje9Zg2Ep7nzmDjSYQ==\",\n  \"mXBfDUt/sBW5OUZs2sihvw==\",\n  \"mXPtbPaoNAAlGmUMmJEWBQ==\",\n  \"mXZ4JeBwT2WJQL4a/Tm4jQ==\",\n  \"mXycPfF5zOvcj1p4hnikWw==\",\n  \"mc45FSMtzdw2PTcEBwHWPw==\",\n  \"md6zNd7ZBn3qArYqQz7/fw==\",\n  \"me61ST+JrXM5k3/a11gRAA==\",\n  \"meHzY9dIF7llDpFQo1gyMg==\",\n  \"miiOqnhtef1ODjFzMHnxjA==\",\n  \"mjFBVRJ7TgnJx+Q74xllPg==\",\n  \"mjQS8CpyGnsZIDOIEdYUxg==\",\n  \"mk1CKDah7EzDJEdhL22B7w==\",\n  \"mmRob7iyTkTLDu8ObmTPow==\",\n  \"mnalaO6xJucSiZ0+99r3Cg==\",\n  \"mpOtwBvle+nyY6lUBwTemw==\",\n  \"mpWNaUH9kn4WY26DWNAh3Q==\",\n  \"mr1qjhliRfl87wPOrJbFQg==\",\n  \"mrinv7KooPQPrLCNTRWCFg==\",\n  \"mrxlFD3FBqpSZr1kuuwxGg==\",\n  \"msstzxq++XO0AqNTmA7Bmg==\",\n  \"mxug34EekabLz0JynutfBg==\",\n  \"myzvc+2MfxGD9uuvZYdnqQ==\",\n  \"n+xYzfKmMoB3lWkdZ+D3rg==\",\n  \"n1M2dgFPpmaICP+JwxHUug==\",\n  \"n1ixvP7SfwYT3L2iWpJg6A==\",\n  \"n5GA+pA9mO/f4RN9NL9lNg==\",\n  \"n6QVaozMGniCO0PCwGQZ6w==\",\n  \"n7Bns42aTungqxKkRfQ5OQ==\",\n  \"n7KL1Kv027TSxBVwzt9qeA==\",\n  \"n7h9v2N1gOcvMuBEf8uThw==\",\n  \"nDAsSla+9XfAlQSPsXtzPA==\",\n  \"nE72uQToQFVLOzcu/nMjww==\",\n  \"nFBXCPeiwxK9mLXPScXzTA==\",\n  \"nFPDZGZowr3XXLmDVpo7hg==\",\n  \"nGzPc0kI/EduVjiK7bzM6Q==\",\n  \"nHTsDl0xeQPC5zNRnoa0Rw==\",\n  \"nHUpYmfV59fe3RWaXhPs3Q==\",\n  \"nL4iEd3b5v4Y9fHWDs+Lrw==\",\n  \"nMuMtK/Zkb3Xr34oFuX/Lg==\",\n  \"nNaGqigseHw30DaAhjBU3g==\",\n  \"nOiwBFnXxCBfPCHYITgqNg==\",\n  \"nR3ACzeVF5YcLX6Gj6AGyQ==\",\n  \"nULSbtw2dXbfVjZh33pDiA==\",\n  \"nUgYO7/oVNSX8fJqP2dbdg==\",\n  \"nVDxVhaa2o38gd1XJgE3aw==\",\n  \"nW3zZshjZEoM8KVJoVfnuQ==\",\n  \"nY/H7vThZ+dDxoPRyql+Cg==\",\n  \"neQoa8pvETr07blVMN3pgA==\",\n  \"nf8x+F03kOpMhsCSUWEhVg==\",\n  \"ng1Q0A7ljho3TUWWYl46sw==\",\n  \"nhAnHuCGXcYlqzOxrrEe1g==\",\n  \"nkbLVLvh3ClKED97+nH+7Q==\",\n  \"nkedTagkmf6YE4tEY+0fKw==\",\n  \"nknBKPgb7US42v8A0fTl/w==\",\n  \"nmD7fEU4u7/4+W/pkC4/0Q==\",\n  \"nqpKfidczdgrNaAyPi7BOQ==\",\n  \"nqtQI1bSM7DCO9P1jGV97Q==\",\n  \"nsnX3tKkN1elr18E31tXDw==\",\n  \"nvLEpj6ZZF3LWH3wUB6lKg==\",\n  \"nvUKoKfC6j8fz3gEDQrc/w==\",\n  \"nvmBgp0YlUrdZ05INsEE8Q==\",\n  \"nwtCsN1xEYaHvEOPzBv+qQ==\",\n  \"nx/U4Tode5ILux4DSR+QMg==\",\n  \"nxDGRpePV3H4NChn4eLwag==\",\n  \"nyaekSYTKzfSeSfPrB114Q==\",\n  \"nykEOLL/o7h0cs0yvdeT2g==\",\n  \"o+areESiXgSO0Lby56cBeg==\",\n  \"o+nYS4TqJc6XOiuUzEpC3A==\",\n  \"o/Y4U6rWfsUCXJ72p5CUGw==\",\n  \"o1uhaQg5/zfne84BFAINUQ==\",\n  \"o1zeXHJEKevURAAbUE/Vog==\",\n  \"o5XVEpdP4OXH0NEO4Yfc/A==\",\n  \"o64LDtKq/Fulf1PkVfFcyg==\",\n  \"o7y4zQXQAryST2cak4gVbw==\",\n  \"o9tdzmIu+3J/EYU4YWyTkA==\",\n  \"oAHVGBSJ2cf4dVnb/KEYmw==\",\n  \"oDca3JEdRb4vONT9GUUsaQ==\",\n  \"oFNMOKbQXcydxnp8fUNOHw==\",\n  \"oFanDWdePmmZN0xqwpUukA==\",\n  \"oGH7SMLI2/qjd9Vnhi3s0A==\",\n  \"oIU19xAvLJwQSZzIH577aA==\",\n  \"oIWwTbkVS5DDL47mY9/1KQ==\",\n  \"oKt57TPe4PogmsGssc3Cbg==\",\n  \"oLWWIn/2AbKRHnddr2og9g==\",\n  \"oMJLQTH1wW7LvOV0KRx/dw==\",\n  \"oNOI17POQCAkDwj6lJsYOA==\",\n  \"oONlXCW4aAqGczQ/bUllBw==\",\n  \"oPcxgoismve6+jXyIKK6AQ==\",\n  \"oPlhC4ebXdkIDazeMSn1fQ==\",\n  \"oQjugfjraFziga1BcwRLRA==\",\n  \"oR8rvIZoeoaZ/ufpo0htfQ==\",\n  \"oSnrpW4UmmVXtUGWqLq+tQ==\",\n  \"oUqO4HrBvkpSL781qAC9+w==\",\n  \"oVlG+0rjrg2tdFImxIeVBA==\",\n  \"oad5SwflzN0vfNcyEyF4EA==\",\n  \"obW3kzv2KBvuckU7F+tfjA==\",\n  \"ocRh5LR1ZIN9Johnht8fhQ==\",\n  \"ocpLRASvTgqfkY20YlVFHQ==\",\n  \"ocvA1/NbyxM0hanwwY6EiA==\",\n  \"odGhKtO4bDW5R8SYiI5yCg==\",\n  \"ogcuGHUZJkmv+vCz567a2g==\",\n  \"ohK6EftXOqBzIMI+5XnESw==\",\n  \"ojZY7Gi2QJXE/fp6Wy31iA==\",\n  \"ojf6uL85EuEYgLvHoGhUrw==\",\n  \"ojugpLIfzflgU2lonfdGxA==\",\n  \"ol9xhVTG9e1wNo50JdZbOA==\",\n  \"olTSlmirL9MFhKORiOKYkQ==\",\n  \"omAjyj1l6gyQAlBGfdxJTw==\",\n  \"onFcHOO1c3pDdfCb5N4WkQ==\",\n  \"oqlkgrYe9aCOwHXddxuyag==\",\n  \"oxoZP897lgMg/KLcZAtkAg==\",\n  \"oyYtf08AkWLR52bXm5+sKw==\",\n  \"ozVqYsmUueKifb4lDyVyrg==\",\n  \"p+bx+/WQWALXEBCTnIMr4w==\",\n  \"p/48hurJ1kh2FFPpyChzJg==\",\n  \"p/7qM5+Lwzw1/lIPY91YxQ==\",\n  \"p0eNK7zJd7D/HEGaVOrtrQ==\",\n  \"p2JPOX8yDQ0agG+tUyyT/g==\",\n  \"p3V7NfveB6cNxFW7+XQNeQ==\",\n  \"p48i7AfSSAyTdJSyHvOONw==\",\n  \"p73gSu4d+4T/ZNNkIv9Nlw==\",\n  \"p8W1LgFuW6JSOKjHkx3+aA==\",\n  \"pCQmlnn3BxhsV2GwqjRhXg==\",\n  \"pFKzcRHSUBqSMtkEJvrR1Q==\",\n  \"pGQEWJ38hb/ZYy2P1+FIuw==\",\n  \"pHo1O5zrCHCiLvopP2xaWw==\",\n  \"pHozgRyMiEmyzThtJnY4MQ==\",\n  \"pKaTI+TfcV3p/sxbd2e7YQ==\",\n  \"pT1raq2fChffFSIBX3fRiA==\",\n  \"pUfWmRXo70yGkUD/x5oIvA==\",\n  \"pVG1hL96/+hQ+58rJJy6/A==\",\n  \"pVgjGg4TeTNhKimyOu3AAw==\",\n  \"pW4gDKtVLj48gNz6V17QdA==\",\n  \"pZfn6IiG+V28fN8E2hawDQ==\",\n  \"pa8nkpAAzDKUldWjIvYMYg==\",\n  \"pcoBh5ic7baSD4TZWb3BSw==\",\n  \"pdPwUHauXOowaq9hpL2yFw==\",\n  \"pdaY6kZ8+QqkMOInvvACNA==\",\n  \"peMW+rpwmXrSwplVuB/gTA==\",\n  \"pfGcaa49SM3S6yJIPk/EJQ==\",\n  \"plXHHzA8X9QGwWzlJxhLRw==\",\n  \"pnJnBzAJlO4j3IRqcfmhkQ==\",\n  \"prCOYlboBnzmLEBG/OeVrQ==\",\n  \"prOsOG0adI4o+oz50moipw==\",\n  \"pulldyBt2sw6QDvTrCh6zw==\",\n  \"pv/m2mA/RJiEQu2Qyfv9RA==\",\n  \"pvXHwJ3dwf9GDzfDD9JI3g==\",\n  \"pw1jplCdTC+b0ThX0FXOjw==\",\n  \"pxuSWn1u+bHtRjyh2Z8veA==\",\n  \"pyrUqiZ98gVXxlXQNXv5fA==\",\n  \"pzC8Y0Vj9MPBy3YXR32z6w==\",\n  \"q/siBRjx6wNu+OTvpFKDwA==\",\n  \"q4z6A4l3nhX3smTmXr+Sig==\",\n  \"q5g3c8tnQTW2EjNfb2sukw==\",\n  \"q6LG0VzO1oxiogAAU63hyg==\",\n  \"q7m/EtZySBjZNBjQ5m1hKw==\",\n  \"q8YF9G2jqydAxSqwyyys5Q==\",\n  \"qA0sTaeNPNIiQbjIe1bOgQ==\",\n  \"qCPfJTR8ecTw6u6b1yHibA==\",\n  \"qE/h/Z+6buZWf+cmPdhxog==\",\n  \"qIFpKKwUmztsBpJgMaVvSg==\",\n  \"qIUJPanWmGzTD1XxvHp+6w==\",\n  \"qNOSm15bdkIDSc/iUr+UTQ==\",\n  \"qNyy6Fc0b8oOMWqqaliZ/w==\",\n  \"qO4HlyHMK5ygX+6HbwQe8w==\",\n  \"qOEIUWtGm5vx/+fg4tuazg==\",\n  \"qP1cCE4zsKGTPhjbcpczMw==\",\n  \"qQQwJ/aF87BbnLu3okXxaw==\",\n  \"qYHdgFAXhF/XcW4lxqfvWQ==\",\n  \"qYuo5vY8V3tZx41Kh9/4Dw==\",\n  \"qZ2q5j2gH3O56xqxkNhlIA==\",\n  \"qaTdVEeZ6S8NMOxfm+wOMA==\",\n  \"qcpeZWUlPllQYZU6mHVwUw==\",\n  \"qenHZKKlTUiEFv6goKM/Mw==\",\n  \"qkvEep4vvXhc2ZJ6R449Mg==\",\n  \"qngzBJbiTB4fivrdnE5gOg==\",\n  \"qnkFUlJ8QT322JuCI3LQgg==\",\n  \"qnsBdl050y9cUaWxbCczRw==\",\n  \"qnzWszsyJhYtx8wkMN6b1g==\",\n  \"qoK2keBg3hdbn7Q24kkVXg==\",\n  \"qpFJZqzkklby+u1UT3c1iA==\",\n  \"qt5CsMts2aD4lw/4Q6bHYQ==\",\n  \"qxALQrqHoDq9d91nU0DckA==\",\n  \"qyRmvxh8p4j4f+61c10ZFQ==\",\n  \"r/b5px/UImGNjT/X5sYjuA==\",\n  \"r0QffVKB9OD9yGsOtqzlhA==\",\n  \"r0hAwlS0mPZVfCSB+2G6uQ==\",\n  \"r1VGXWeqGeGbfKjigaAS+Q==\",\n  \"r2f2MyT+ww1g9uEBzdYI1w==\",\n  \"r36kVMpF+9J+sfI3GeGqow==\",\n  \"r3lQAYOYhwlLnDWQIunKqg==\",\n  \"r95wJtP5rsTExKMS7QhHcw==\",\n  \"rBt6L/KLT7eybxKt5wtFdg==\",\n  \"rCxoo4TP/+fupXMuIM0sDA==\",\n  \"rHagXw+CkF3uEWPWDKXvog==\",\n  \"rIMXaCaozDvrdpvpWvyZOQ==\",\n  \"rJ9qVn8/2nOxexWzqIHlcQ==\",\n  \"rJCuanCy51ydVD4nInf9IQ==\",\n  \"rKAQxu80Q8g1EEhW5Wh8tg==\",\n  \"rKb3TBM4EPx/RErFOFVCnQ==\",\n  \"rLZII1R6EGus+tYCiUtm6g==\",\n  \"rM/BOovNgnvebKMxZQdk7g==\",\n  \"rMm9bHK69h0fcMkMdGgeeA==\",\n  \"rOYeIcB+Rg5V6JG2k4zS2w==\",\n  \"rSvhrHyIlnIBlfNJqemEbw==\",\n  \"rTwJggSxTbwIYdp07ly0LA==\",\n  \"rUp5Mfc57+A8Q29SPcvH/Q==\",\n  \"rWliqgfZ3/uCRBOZ9sMmdA==\",\n  \"rXGWY/Gq+ZEsmvBHUfFMmQ==\",\n  \"rXSbbRABEf4Ymtda45w8Fw==\",\n  \"rXfWkabSPN+23Ei1bdxfmQ==\",\n  \"rXtGpN17Onx8LnccJnXwJQ==\",\n  \"rZKD8oJnIj5fSNGiccfcvA==\",\n  \"raKMXnnX6PFFsbloDqyVzQ==\",\n  \"raYifKqev8pASjjuV+UTKQ==\",\n  \"rcY4Ot40678ByCfqvGOGdg==\",\n  \"rdeftHE7gwAT67wwhCmkYQ==\",\n  \"rfPTskbnoh3hRJH6ZAzQRg==\",\n  \"rgcXxjx3pDLotH7TTfAoZw==\",\n  \"rh7bzsTQ1UZjG7amysr0Gg==\",\n  \"rhgtLQh0F9bRA6IllM7AGw==\",\n  \"ri4AOITPdB1YHyXV+5S51g==\",\n  \"rkeLYwMZ1/pW2EmIibALfA==\",\n  \"rlXt6zKE7DswUl0oWGOQUQ==\",\n  \"rqHKB91H3qVuQAm+Ym5cUA==\",\n  \"rqucO37p86LpzehR/asCSQ==\",\n  \"rs2QrN4qzAHCHhkcrAvIfA==\",\n  \"rtJdfki8fG6CB36CADp0QA==\",\n  \"rtd6mqFgGe98mqO0pFGbSw==\",\n  \"rueNryrchijjmWaA3kljYg==\",\n  \"rvE64KQGkVkbl07y7JwBqw==\",\n  \"rwplpbNJz0ADUHTmzAj15Q==\",\n  \"rwtF86ZAbWyKI6kLn4+KBw==\",\n  \"rxfACPLtKXbYua18l3WlUw==\",\n  \"rzj6mjHCcMEouL66083BAg==\",\n  \"s+eHg5K9zZ2Jozu5Oya9ZQ==\",\n  \"s/BZAhh1cTV3JCDUQsV8mA==\",\n  \"s2AKVTwrY65/SWqQxDGJQg==\",\n  \"s5+78jS4hQYrFtxqTW3g1Q==\",\n  \"s5RUHVRNAoKMuPR/Jkfc2Q==\",\n  \"s7iW1M6gkAMp+D/3jHY58w==\",\n  \"s8NpalwgPdHPla7Zi9FJ3w==\",\n  \"sBpytpE38xz0zYeT+0qc2A==\",\n  \"sC11Rf/mau3FG5SnON4+vQ==\",\n  \"sCLMrLjEUQ6P1L8tz90Kxg==\",\n  \"sEeblUmISi1HK4omrWuPTA==\",\n  \"sGLPmr568+SalaQr8SE/PA==\",\n  \"sLJrshdEANp0qk2xOUtTnQ==\",\n  \"sLdxIKap0ZfC3GpUk3gjog==\",\n  \"sNmW2b2Ud7dZi3qOF8O8EQ==\",\n  \"sQAxqWXeiu/Su0pnnXgI9A==\",\n  \"sQskMBELEq86o1SJGQqfzg==\",\n  \"sQzCwNDlRsSH7iB9cTbBcg==\",\n  \"sS6QcitMPdvUBLiMXkWQkw==\",\n  \"sWLcS+m4aWk31BiBF+vfJQ==\",\n  \"sXlFMSTBFnq0STHj6cS/8w==\",\n  \"sa2DECaqYH1z1/AFhpHi+g==\",\n  \"saEpnDGBSZWqeXSJm34eOA==\",\n  \"scCQPl0em2Zmv/RQYar60g==\",\n  \"sfIClgTMtZo9CM9MHaoqhQ==\",\n  \"sfowXUMdN2mCoBVrUzulZg==\",\n  \"sfte/o9vVNyida/yLvqADA==\",\n  \"siHwJx6EgeB1gBT9z/vTyw==\",\n  \"skrQRB9xbOsiSA19YgAdIQ==\",\n  \"snGTzo540cCqgBjxrfNpKw==\",\n  \"soBA65OmZdfBGJkBmY/4Iw==\",\n  \"spHVvA/pc7nF9Q4ON020+w==\",\n  \"spJI3xFUlpCDqzg0XCxopA==\",\n  \"sr3UXbMg5zzkRduFx/as7g==\",\n  \"sw+bmpzqsM4gEQtnqocQLQ==\",\n  \"swJhrPwllq5JORWiP5EkDA==\",\n  \"swsVVsPi/5aPFBGP+jmPIw==\",\n  \"syeBfQBUmkXNWCZ1GV8xSA==\",\n  \"t+bYn9UqrzKiuxAYGF7RLA==\",\n  \"t0WN8TwMLgi8UVEImoFXKg==\",\n  \"t2EkpUsLOEOsrnep0nZSmA==\",\n  \"t2vWMIh2BvfDSQaz5T1TZw==\",\n  \"t3Txxjq43e/CtQmfQTKwWg==\",\n  \"t5U+VMsTtlWAAWSW+00SfQ==\",\n  \"t5wh9JGSkQO78QoQoEqvXA==\",\n  \"t7HaNlXL16fVwjgSXmeOAQ==\",\n  \"t8pjhdyNJirkvYgWIO/eKg==\",\n  \"tBQDfy48FnIOZI04rxfdcA==\",\n  \"tFMJRXfWE9g78O1uBUxeqQ==\",\n  \"tFmWYH82I3zb+ymk5dhepA==\",\n  \"tG+rpfJBXlyGXxTmkceiKA==\",\n  \"tHDbi43e6k6uBgO0hA+Uiw==\",\n  \"tIqwBotg052wGBL65DZ+yA==\",\n  \"tJt6VDdAPEemBUvnoc4viA==\",\n  \"tOdlnsE3L3XCBDJRmb/OqA==\",\n  \"tOkYq1BZY152/7IJ6ZYKUg==\",\n  \"tU31r8zla146sqczdKXufg==\",\n  \"tVhXk9Ff3wAg56FbdNtcFg==\",\n  \"tVvWdA+JqH0HR2OlNVRoag==\",\n  \"tVw8U1AsslIFmQs4H1xshg==\",\n  \"tX8X8KoxUQ8atFSCxgwE1Q==\",\n  \"tXVb5f90k9l3e1oK2NGXog==\",\n  \"tXuu7YpZOuMLTv87NjKerA==\",\n  \"tY916jrSySzrL+YTcVmYKQ==\",\n  \"tYeIZjIm0tVEsYxH1iIiUQ==\",\n  \"tb5+2dmYALJibez1W4zXgA==\",\n  \"td7nDgTDmKPSODRusMcupw==\",\n  \"tdgI9v7cqJsgCAeW1Fii1A==\",\n  \"tdiTXKrkqxstDasT0D5BPA==\",\n  \"tejpAZp7y32SO2+o4OGvwQ==\",\n  \"tfgO55QqUyayjDfQh+Zo1Q==\",\n  \"tj2rWvF2Fl+XIccctj8Mhw==\",\n  \"tnUtJ/DQX9WaVJyTgemsUA==\",\n  \"tq5xUJt8GtjDIh1b48SthQ==\",\n  \"tr+U/vt+MIGXPRQYYWJfRg==\",\n  \"trjM81KANPZrg9iSThWx6Q==\",\n  \"tsiqwelcBAMU/HpLGBtMGw==\",\n  \"twPn6wTGqI0aR//0wP3xtA==\",\n  \"twjiDKJM7528oIu/el4Zbg==\",\n  \"tzV7ixFH37ze4zuLILTlfA==\",\n  \"u/QxrP1NOM/bOJlJlsi/jQ==\",\n  \"u2WQlcMxOACy6VbJXK4FwA==\",\n  \"u5cUPxM6/spLIV8VidPrAA==\",\n  \"uC2lzm7HaMAoczJO6Z/IhQ==\",\n  \"uChFnF0oCwARhAOz/d47eA==\",\n  \"uESeJe/nYrHCq4RQbrNpGA==\",\n  \"uExgqZkkJnZj252l5dKAGg==\",\n  \"uIkVijg7RPi/1j7c18G1qA==\",\n  \"uJZGw3IY2nCcdVeWW1geNQ==\",\n  \"uMq8cDVWFD+tpn8aeP8Pqg==\",\n  \"uNWFZlP7DA96sf+LWiAhtQ==\",\n  \"uNzpptKjihEfKRo5A1nWmw==\",\n  \"uO+uK1DntCxVRr1KttfUIw==\",\n  \"uOHrw37yF9oLLVd16nUpeg==\",\n  \"uOkMpYy/7DYYoethJdixfQ==\",\n  \"uPdjKJIGzN7pbGZDZdCGaA==\",\n  \"uPi8TsGY3vQsMVo/nsbgVQ==\",\n  \"uPm+cF4Jq08S5pQhYFjU8A==\",\n  \"uPnL9tboMZo0Kl2fe24CmA==\",\n  \"uQs79rbD/wEakMUxqMI48A==\",\n  \"uSIiF1r9F18avZczmlEuMQ==\",\n  \"uT6WRh5UpVdeABssoP2VTg==\",\n  \"uTA0XbiH3fTeVV7u5z0b3w==\",\n  \"uTHBqApdKOAgdwX3cjrCYQ==\",\n  \"uU1TX5DoDg6EcFKgFcn0GA==\",\n  \"uXuPA/2KJbb7ZX+NymN3dw==\",\n  \"uXvr6vi5kazZ9BCg2PWPJA==\",\n  \"uZ2gUA74/7Q33tI2TcGQlg==\",\n  \"ucLMWnNDSqE4NOCGWvcGWw==\",\n  \"udU65VtsvJspYmamiOsgXw==\",\n  \"ueODvMv/f9ZD8O0aIHn4sg==\",\n  \"ugY8rTtJkN4CXWMVcRZiZw==\",\n  \"uhT12XY79CtbwhcSfAmAXQ==\",\n  \"ulLuTZqhEDkX0EJ3xwRP9A==\",\n  \"ulpDxLeQnIRPnq6oaah2AA==\",\n  \"up2MVDi9ve+s83/nwNtZ7Q==\",\n  \"uqe3rFveJ2JIkcZQ3ZMXHQ==\",\n  \"uqp92lAqjec8UQYfyjaEZw==\",\n  \"ur9JDCVNwzSH4q4ngDlHNQ==\",\n  \"uu+ncs63SdQIvG6z4r7Q3Q==\",\n  \"uuiJ+yB7JLDh2ulthM0mjg==\",\n  \"uvKYnKE01D5r7kR9UQyo5A==\",\n  \"uvzmRcvgepW6mZbMfYgcNw==\",\n  \"uwA6N5LptSXqIBkTO0Jd7Q==\",\n  \"uwGivY3/C9WK+dirRPJZ4A==\",\n  \"uzEgwx1iAXAvWPKSVwYSeQ==\",\n  \"uzkNhmo2d08tv5AmnyqkoQ==\",\n  \"v/PshI6JjkL9nojLlMNfhg==\",\n  \"v0Bvws1WYVoEgDt8xmVKew==\",\n  \"v1AWe5qb5y3vSKFb7ADeEw==\",\n  \"v4xIYrfPGILEbD/LwVDDzA==\",\n  \"v6jZicMNM3ysm3U5xu0HoQ==\",\n  \"v7BrkRmK0FfWSHunTRHQFQ==\",\n  \"vCekQ2nOQKiN/q8Be/qwZg==\",\n  \"vFFzkWgGyw6OPADONtEojQ==\",\n  \"vFox1d3llOeBeCUZGvTy0A==\",\n  \"vFtC0B2oe1gck28JOM1dyg==\",\n  \"vGKknndb4j6VTV8DxeT4fQ==\",\n  \"vHGjRRSlZHJIliCwIkCAmQ==\",\n  \"vHVXsAMQqc0qp7HA5Q+YkA==\",\n  \"vHmQUl4WHXs1E/Shh+TeyA==\",\n  \"vIORTYSHFIXk5E2NyIvWcQ==\",\n  \"vMuaLvAntJB5o7lmt/kVXA==\",\n  \"vOJ55zFdgPPauPyFYBf01w==\",\n  \"vRgkZZGVN7YZrlml0vxrKA==\",\n  \"vSKsa0JhLCe9QFZKkcj58Q==\",\n  \"vTAmgfq3GxL4+ubXpzwk5w==\",\n  \"vUC0HlTTHj6qNHwfviDtAw==\",\n  \"vUE8Iw3NyWXURpXyoNJdaw==\",\n  \"vWn9OPnrJgfPavg4D6T/HQ==\",\n  \"vX7RIhatQeXAMr1+OjzhZw==\",\n  \"vZtL0yWpSIA+9v8i23bZSg==\",\n  \"vb6Agwzk4JG0Nn7qRPPFMQ==\",\n  \"vbyiKeDCQ4q9dDRI1Q0Ong==\",\n  \"vg3jozLXEmAnmJwdfcEN0g==\",\n  \"vhdFtKVH4bVatb4n8KzeXw==\",\n  \"vjrSYGUpeKOtJ2cNgLFg2g==\",\n  \"vljJciS+uuIvL7XXm5688g==\",\n  \"vmqfGJE6r4yDahtU/HLrxw==\",\n  \"vnOJ3e9Zd4wPx8PX7QgZzQ==\",\n  \"voO3krg4sdy4Iu+MZEr8+g==\",\n  \"vqYHQ3MnHrAIAr1QHwfIag==\",\n  \"vsRNZx4thFFFPneubKq1Fw==\",\n  \"vvEH5A39TTe1AOC11rRCLA==\",\n  \"vvh9vAIrXjIwLVkuJb5oDQ==\",\n  \"vwno3vugCvt6ooT3CD4qIQ==\",\n  \"w+jzM0I5DRzoUiLS/9QIMQ==\",\n  \"w0PKdssv+Zc5J/BbphoxpA==\",\n  \"w1zN28mSrI/gqHsgs4ME3A==\",\n  \"w3G+qXXqqKi8F5s+qvkBUg==\",\n  \"w5N/aHbtOIKzcvG3GlMjGA==\",\n  \"wDiGoFEfIVEDyyc4VpwhWQ==\",\n  \"wEJDulZafLuXCvcqBYioFQ==\",\n  \"wHA+D5cObfV3kGORCdEknw==\",\n  \"wI7JrSPQwYHpv2lRsQu9nQ==\",\n  \"wIfvvLKC61gOpsddUFjVog==\",\n  \"wJ4uCrl4DPg70ltw1dZO3w==\",\n  \"wJKFMqh6MGctWfasjHrPEg==\",\n  \"wJpepvmtQQ3sz3tVFDnFqw==\",\n  \"wK6Srd83eLigZ11Q20XGrg==\",\n  \"wM8tnXO4PDlLVHspZFcjYw==\",\n  \"wMOE/pEKVIklE75xjt6b6w==\",\n  \"wMum67lfk5E1ohUObJgrOg==\",\n  \"wMyJLQJdmrC2TSeFkIuSvQ==\",\n  \"wOc4TbwQGUwOC1B3BEZ4OQ==\",\n  \"wOhbpTzmFla8R0kI9OiHaA==\",\n  \"wPhJcp7U7IVX83szbIOOxQ==\",\n  \"wQKL8Ga6JQkpZ7yymDkC3w==\",\n  \"wR2Gxb07nkaPcZHlEjr8iA==\",\n  \"wRqaDZVHHurp5whOQ1kDbQ==\",\n  \"wTO49YX/ePHMWtcoxUAHpw==\",\n  \"wUYhs4j3W9nIywu1HIv2JA==\",\n  \"wVfSZYjMjbTsD2gaSbwuqQ==\",\n  \"wX2URK6eDDHeEOF3cgPgHA==\",\n  \"wX70jKLKJApHnhyK0r6t3A==\",\n  \"wajwXfWz2J+O+NVaj6j2UQ==\",\n  \"wc+8ohFWgOF4VlSYiZIGwQ==\",\n  \"wdRyYjaM11VmqkkxV/5bsA==\",\n  \"wfwuxn+Vja1DNwiDwL2pcQ==\",\n  \"wgH1GlUxWi6/yLLFzE76uQ==\",\n  \"who8uUamlHWHXnBf7dwy4A==\",\n  \"wlWxtQDJ+siGhN2fJn3qtw==\",\n  \"wnfYUctNK+UPwefX5y4/Rw==\",\n  \"wpZqFkKafFpLcykN2IISqg==\",\n  \"wqUJ1Gq1Yz2cXFkbcCmzHQ==\",\n  \"wqWqe0KRjZlUIrGgEOG9Mg==\",\n  \"wrewZ0hoHODf7qmoGcOd7g==\",\n  \"wsp+vmW8sEqXYVURd/gjHA==\",\n  \"wt+qDLU38kzNU75ZYi3Hbw==\",\n  \"wtyAZIfhomcHe9dLbYoSvA==\",\n  \"wux5Y8AipBnc5tJapTzgEQ==\",\n  \"wv4NC9CIpwuGf/nOQYe/oA==\",\n  \"wxkb8evGEaGf/rg/1XUWiA==\",\n  \"wy/Z8505o4sVovk4UuBp1A==\",\n  \"wyqmQGB6vgRVrYtmB2vB7w==\",\n  \"wyx5mnUMgP5wjykjAfTO7w==\",\n  \"x+8rwkqKCv0juoT5m1A4eg==\",\n  \"x/BIDm6TKMhqu/gtb3kGyw==\",\n  \"x/MpsQvziUpW40nNUHDS5Q==\",\n  \"x0eIHCvQLd2jdDaXwSWTYQ==\",\n  \"x1A74vg/hwwjAx6GrkU8zw==\",\n  \"x2NpqNnqRihktNzpxmepkQ==\",\n  \"x2nSgcTjA3oGgI8mMgiqjw==\",\n  \"x5lyMArsv1MuJmEFlWCnNw==\",\n  \"x5zMDuW66467ofgL3spLUQ==\",\n  \"x6M66krXSi0EhppwmDmsxA==\",\n  \"x6lNRGgJcRxgKTlzhc1WPg==\",\n  \"x8kRVzohTdhkryvYeMvkMw==\",\n  \"x9TIZ9Ua++3BX+MpjgTuWA==\",\n  \"x9VwDdFPp/rJ+SF16ooWYg==\",\n  \"xAAipGfHTGTjp9Qk1MR8RQ==\",\n  \"xJi0T+psHOXMivSOVpMWeQ==\",\n  \"xLm/bJBonpTs0PwsF0DvRg==\",\n  \"xMIHeno2qj3V8q9H1xezeg==\",\n  \"xNilc7UOu1kyP0+nK5MrLw==\",\n  \"xPe76nHyHmald6kmMQsKdg==\",\n  \"xQpYjaAmrQudWgsdu24J0A==\",\n  \"xTizUioizbMQxD0T6fy/EQ==\",\n  \"xUXEE7OBBCudsQnuj5ycOA==\",\n  \"xWYecfzAtXT9WyQ8NYY/hw==\",\n  \"xX6atcCApI08oVLjjLteLg==\",\n  \"xYD8jrCDmuQna+p1ebnKDQ==\",\n  \"xbBxUP9JyY0wDgHDipBHeg==\",\n  \"xdCCdP8SNBOK3IsX6PiPQA==\",\n  \"xdmY+qyoxxuRZa9kuNpDEg==\",\n  \"xfYZ6qhWNBqqJ0PdWRjOwA==\",\n  \"xfjBQk3CrNjhufdPIhr91A==\",\n  \"xiFlcSfa/gnPiO+LwbixcQ==\",\n  \"xiyRfVG0EfBA+rCk+tgWRQ==\",\n  \"xjA21QjNdThLW3VV7SCnrg==\",\n  \"xjTMO2mvtpvwQrounD4e8g==\",\n  \"xktOghh1S9nIX6fXWnT+Ug==\",\n  \"xmGgK3W5y+oCd0K2u8XjZQ==\",\n  \"xmsYnsJq78/f9xuKuQ2pBQ==\",\n  \"xoPSM86Se+1hHX0y3hhdkw==\",\n  \"xs8J3cesq7lDhP/dNltqOw==\",\n  \"xsCZVhCk2qJmOqvUjK3Y8Q==\",\n  \"xsf0m31Am0W9eLhopAkfnA==\",\n  \"xukOAM0QVsA72qEy0yku9A==\",\n  \"xvipmmwKdYt4eoKvvRnjEg==\",\n  \"xweGAZf+Yb3TtwR/sGmGIA==\",\n  \"xzGzN5Hhbh0m/KezjNvXbQ==\",\n  \"y+1I05LDAYJ09tKMs3zW6g==\",\n  \"y+cl1/Knb9MZPz8nBB0M+w==\",\n  \"y/e3HSdg7T19FanRpJ7+7Q==\",\n  \"y1J+o6DC2sETFsySgpDZyA==\",\n  \"y2JOIoIiT9cV1VxplZPraQ==\",\n  \"y2Tn2gmhKs5WKc01ce74rg==\",\n  \"y4/HohCJxtt+cT7nLJB08w==\",\n  \"y4Y4mSSTw/WrIdRpktc5Hw==\",\n  \"y4iBxAMn/KzMmaWShdYiIw==\",\n  \"y4mfEDerrhaqApDdhP5vjA==\",\n  \"y7yS9x3yshVhMpDbQtfYOQ==\",\n  \"yCu+DVU/ceMTOZ5h/7wQTg==\",\n  \"yD3Dd4ToRrl53k/2NSCJiw==\",\n  \"yDrAd1ot38soBk7zKdnT8A==\",\n  \"yKLLiqzxfrCsr6+Rm6kx1Q==\",\n  \"yKrsKX4/1B1C0TyvciNz5w==\",\n  \"yL1DwlIIREPuyuCFULi0uw==\",\n  \"yLAhLNezvqVHmN1SfMRrPw==\",\n  \"yOE90OHQdyOfrAgwDvn2gA==\",\n  \"yPIeWcW8+3HjDagegrN8bw==\",\n  \"yQCLV9IoPyXEOaj3IdFMWw==\",\n  \"yQmNZnp/JZywbBiZs3gecA==\",\n  \"yS/yMnJDHW0iaOsbj4oPTg==\",\n  \"yTVJKBn72RjakMBXDoBKHg==\",\n  \"yTgN5xFIdz1MzFS6xMl5uQ==\",\n  \"yU3N0HMSP5etuHPNrVkZtg==\",\n  \"yV3IbbTWAbHMhMGVvgb/ZQ==\",\n  \"yYBIS9PZbKo7Gram7IXWPA==\",\n  \"yYVW07lOZHdgtX42xJONIA==\",\n  \"yYmnM/WOgi+48Rw7foGyXA==\",\n  \"yYp4iuI5f/y/l1AEJxYolQ==\",\n  \"ybpTgPr3SjJ12Rj5lC/IMA==\",\n  \"ycjv4XkS5O7zcF3sqq9MwQ==\",\n  \"yctId8ltkl3+xqi9bj+RqA==\",\n  \"ydVj2odhergi+2zGUwK4/A==\",\n  \"yf06Slv9l3IZEjVqvxP2aA==\",\n  \"yfAaL0MMtSXPQ37pBdmHxQ==\",\n  \"yhI5jHlfFJxu4eV5VJO2zQ==\",\n  \"yhRi5M9Etuu9HSu4d24i3w==\",\n  \"yhexr/OFKfZl0o3lS70e4w==\",\n  \"ylA6sU7Kaf9fMNIx1+sIlw==\",\n  \"ymtA8EMPMgmMcimWZZ0A1Q==\",\n  \"ynaj4XjU27b7XbqPyxI8Ig==\",\n  \"yqQPU4jT9XvRABZgNQXjgg==\",\n  \"yqtj8GfLaUHYv/BsdjxIVw==\",\n  \"ysRQ+7Aq7eVLOp88KnFVMA==\",\n  \"ytDXLDBqWiU1w3sTurYmaw==\",\n  \"yteeQr3ub2lDXgLziZV+DQ==\",\n  \"yxCyBXqGWA735JEyljDP7Q==\",\n  \"z+1oDVy8GJ5u/UDF+bIQdA==\",\n  \"z/e5M2lE9qh3bzB97jZCKA==\",\n  \"z0BU//aSjYHAkGGk3ZSGNg==\",\n  \"z20AAnvj7WsfJeOu3vemlA==\",\n  \"z3L2BNjQOMOfTVBUxcpnRA==\",\n  \"z4Bft++f72QeDh4PWGr/sw==\",\n  \"z4oKy2wKH+sbNSgGjbdHGw==\",\n  \"z5DveTu377UW8IHnsiUGZg==\",\n  \"z920R8eahJPiTsifrPYdxA==\",\n  \"z9cd+Qj+ueX34Zf3997MNQ==\",\n  \"zCRZgVsHbQZcVMHd9pGD3A==\",\n  \"zCpibjrZOA3FQ4lYt0WoVA==\",\n  \"zDSQ3NJuUGkVOlvVCATRwA==\",\n  \"zDUZCzQesFjO1JI3PwDjfg==\",\n  \"zEzWZ6l7EKoVUxvk/l78Mw==\",\n  \"zJ7ScHNxr2leCDNNcuDApA==\",\n  \"zNLlWGW/aKBhUwQZ4DZWoQ==\",\n  \"zVupSPz7cD0v/mD/eUIIjg==\",\n  \"zZtYkKU50PPEj6qSbO5/Sw==\",\n  \"za4rzveYVMFe3Gw531DQJQ==\",\n  \"zaqyy3GaJ7cp8qDoLJWcTw==\",\n  \"zbjXhZaeyMfdTb2zxvmRMg==\",\n  \"zeELfk015D5krExLKRUYtg==\",\n  \"zeHF6fdeqcOId3fRUGscRw==\",\n  \"zgEyxj/sCs63O98sZS94Yw==\",\n  \"zi04Yc01ZheuFAQc59E45A==\",\n  \"zirOtGUXeRL22ezfotZfQg==\",\n  \"zm+z+OOyHhljV2TjA3U9zw==\",\n  \"zrZWcqQsUE3ocWE0fG+SOA==\",\n  \"ztULoqHvCOE6qV7ocqa4/w==\",\n  \"zwQ/3MzTJ9rfBmrANIh14w==\",\n  \"zwY6tCjjya/bgrYaCncaag==\",\n  \"zxsSqovedB3HT99jVblCnQ==\",\n  \"zyA9f5J7mw5InjhcfeumAQ==\",\n]);\n"
  },
  {
    "path": "lib/HighlightsFeed.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\"use strict\";\n\nconst { Services } = ChromeUtils.import(\"resource://gre/modules/Services.jsm\");\n\nconst { actionTypes: at } = ChromeUtils.import(\n  \"resource://activity-stream/common/Actions.jsm\"\n);\n\nconst { shortURL } = ChromeUtils.import(\n  \"resource://activity-stream/lib/ShortURL.jsm\"\n);\nconst { SectionsManager } = ChromeUtils.import(\n  \"resource://activity-stream/lib/SectionsManager.jsm\"\n);\nconst {\n  TOP_SITES_DEFAULT_ROWS,\n  TOP_SITES_MAX_SITES_PER_ROW,\n} = ChromeUtils.import(\"resource://activity-stream/common/Reducers.jsm\");\nconst { Dedupe } = ChromeUtils.import(\n  \"resource://activity-stream/common/Dedupe.jsm\"\n);\n\nChromeUtils.defineModuleGetter(\n  this,\n  \"filterAdult\",\n  \"resource://activity-stream/lib/FilterAdult.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"LinksCache\",\n  \"resource://activity-stream/lib/LinksCache.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"NewTabUtils\",\n  \"resource://gre/modules/NewTabUtils.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"Screenshots\",\n  \"resource://activity-stream/lib/Screenshots.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"PageThumbs\",\n  \"resource://gre/modules/PageThumbs.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"DownloadsManager\",\n  \"resource://activity-stream/lib/DownloadsManager.jsm\"\n);\n\nconst HIGHLIGHTS_MAX_LENGTH = 16;\nconst MANY_EXTRA_LENGTH =\n  HIGHLIGHTS_MAX_LENGTH * 5 +\n  TOP_SITES_DEFAULT_ROWS * TOP_SITES_MAX_SITES_PER_ROW;\nconst SECTION_ID = \"highlights\";\nconst SYNC_BOOKMARKS_FINISHED_EVENT = \"weave:engine:sync:applied\";\nconst BOOKMARKS_RESTORE_SUCCESS_EVENT = \"bookmarks-restore-success\";\nconst BOOKMARKS_RESTORE_FAILED_EVENT = \"bookmarks-restore-failed\";\nconst RECENT_DOWNLOAD_THRESHOLD = 36 * 60 * 60 * 1000;\n\nthis.HighlightsFeed = class HighlightsFeed {\n  constructor() {\n    this.dedupe = new Dedupe(this._dedupeKey);\n    this.linksCache = new LinksCache(\n      NewTabUtils.activityStreamLinks,\n      \"getHighlights\",\n      [\"image\"]\n    );\n    PageThumbs.addExpirationFilter(this);\n    this.downloadsManager = new DownloadsManager();\n  }\n\n  _dedupeKey(site) {\n    // Treat bookmarks, pocket, and downloaded items as un-dedupable, otherwise show one of a url\n    return (\n      site &&\n      (site.pocket_id || site.type === \"bookmark\" || site.type === \"download\"\n        ? {}\n        : site.url)\n    );\n  }\n\n  init() {\n    Services.obs.addObserver(this, SYNC_BOOKMARKS_FINISHED_EVENT);\n    Services.obs.addObserver(this, BOOKMARKS_RESTORE_SUCCESS_EVENT);\n    Services.obs.addObserver(this, BOOKMARKS_RESTORE_FAILED_EVENT);\n    SectionsManager.onceInitialized(this.postInit.bind(this));\n  }\n\n  postInit() {\n    SectionsManager.enableSection(SECTION_ID);\n    this.fetchHighlights({ broadcast: true });\n    this.downloadsManager.init(this.store);\n  }\n\n  uninit() {\n    SectionsManager.disableSection(SECTION_ID);\n    PageThumbs.removeExpirationFilter(this);\n    Services.obs.removeObserver(this, SYNC_BOOKMARKS_FINISHED_EVENT);\n    Services.obs.removeObserver(this, BOOKMARKS_RESTORE_SUCCESS_EVENT);\n    Services.obs.removeObserver(this, BOOKMARKS_RESTORE_FAILED_EVENT);\n  }\n\n  observe(subject, topic, data) {\n    // When we receive a notification that a sync has happened for bookmarks,\n    // or Places finished importing or restoring bookmarks, refresh highlights\n    const manyBookmarksChanged =\n      (topic === SYNC_BOOKMARKS_FINISHED_EVENT && data === \"bookmarks\") ||\n      topic === BOOKMARKS_RESTORE_SUCCESS_EVENT ||\n      topic === BOOKMARKS_RESTORE_FAILED_EVENT;\n    if (manyBookmarksChanged) {\n      this.fetchHighlights({ broadcast: true });\n    }\n  }\n\n  filterForThumbnailExpiration(callback) {\n    const state = this.store\n      .getState()\n      .Sections.find(section => section.id === SECTION_ID);\n\n    callback(\n      state && state.initialized\n        ? state.rows.reduce((acc, site) => {\n            // Screenshots call in `fetchImage` will search for preview_image_url or\n            // fallback to URL, so we prevent both from being expired.\n            acc.push(site.url);\n            if (site.preview_image_url) {\n              acc.push(site.preview_image_url);\n            }\n            return acc;\n          }, [])\n        : []\n    );\n  }\n\n  /**\n   * Chronologically sort highlights of all types except 'visited'. Then just append\n   * the rest at the end of highlights.\n   * @param {Array} pages The full list of links to order.\n   * @return {Array} A sorted array of highlights\n   */\n  _orderHighlights(pages) {\n    const splitHighlights = { chronologicalCandidates: [], visited: [] };\n    for (let page of pages) {\n      if (page.type === \"history\") {\n        splitHighlights.visited.push(page);\n      } else {\n        splitHighlights.chronologicalCandidates.push(page);\n      }\n    }\n\n    return splitHighlights.chronologicalCandidates\n      .sort((a, b) => a.date_added < b.date_added)\n      .concat(splitHighlights.visited);\n  }\n\n  /**\n   * Refresh the highlights data for content.\n   * @param {bool} options.broadcast Should the update be broadcasted.\n   */\n  async fetchHighlights(options = {}) {\n    // If TopSites are enabled we need them for deduping, so wait for\n    // TOP_SITES_UPDATED. We also need the section to be registered to update\n    // state, so wait for postInit triggered by SectionsManager initializing.\n    if (\n      (!this.store.getState().TopSites.initialized &&\n        this.store.getState().Prefs.values[\"feeds.topsites\"]) ||\n      !this.store.getState().Sections.length\n    ) {\n      return;\n    }\n\n    // We broadcast when we want to force an update, so get fresh links\n    if (options.broadcast) {\n      this.linksCache.expire();\n    }\n\n    // Request more than the expected length to allow for items being removed by\n    // deduping against Top Sites or multiple history from the same domain, etc.\n    const manyPages = await this.linksCache.request({\n      numItems: MANY_EXTRA_LENGTH,\n      excludeBookmarks: !this.store.getState().Prefs.values[\n        \"section.highlights.includeBookmarks\"\n      ],\n      excludeHistory: !this.store.getState().Prefs.values[\n        \"section.highlights.includeVisited\"\n      ],\n      excludePocket: !this.store.getState().Prefs.values[\n        \"section.highlights.includePocket\"\n      ],\n    });\n\n    if (\n      this.store.getState().Prefs.values[\"section.highlights.includeDownloads\"]\n    ) {\n      // We only want 1 download that is less than 36 hours old, and the file currently exists\n      let results = await this.downloadsManager.getDownloads(\n        RECENT_DOWNLOAD_THRESHOLD,\n        { numItems: 1, onlySucceeded: true, onlyExists: true }\n      );\n      if (results.length) {\n        // We only want 1 download, the most recent one\n        manyPages.push({\n          ...results[0],\n          type: \"download\",\n        });\n      }\n    }\n\n    const orderedPages = this._orderHighlights(manyPages);\n\n    // Remove adult highlights if we need to\n    const checkedAdult = this.store.getState().Prefs.values.filterAdult\n      ? filterAdult(orderedPages)\n      : orderedPages;\n\n    // Remove any Highlights that are in Top Sites already\n    const [, deduped] = this.dedupe.group(\n      this.store.getState().TopSites.rows,\n      checkedAdult\n    );\n\n    // Keep all \"bookmark\"s and at most one (most recent) \"history\" per host\n    const highlights = [];\n    const hosts = new Set();\n    for (const page of deduped) {\n      const hostname = shortURL(page);\n      // Skip this history page if we already something from the same host\n      if (page.type === \"history\" && hosts.has(hostname)) {\n        continue;\n      }\n\n      // If we already have the image for the card, use that immediately. Else\n      // asynchronously fetch the image. NEVER fetch a screenshot for downloads\n      if (!page.image && page.type !== \"download\") {\n        this.fetchImage(page);\n      }\n\n      // Adjust the type for 'history' items that are also 'bookmarked' when we\n      // want to include bookmarks\n      if (\n        page.type === \"history\" &&\n        page.bookmarkGuid &&\n        this.store.getState().Prefs.values[\n          \"section.highlights.includeBookmarks\"\n        ]\n      ) {\n        page.type = \"bookmark\";\n      }\n\n      // We want the page, so update various fields for UI\n      Object.assign(page, {\n        hasImage: page.type !== \"download\", // Downloads do not have an image - all else types fall back to a screenshot\n        hostname,\n        type: page.type,\n        pocket_id: page.pocket_id,\n      });\n\n      // Add the \"bookmark\", \"pocket\", or not-skipped \"history\"\n      highlights.push(page);\n      hosts.add(hostname);\n\n      // Remove internal properties that might be updated after dispatch\n      delete page.__sharedCache;\n\n      // Skip the rest if we have enough items\n      if (highlights.length === HIGHLIGHTS_MAX_LENGTH) {\n        break;\n      }\n    }\n\n    const { initialized } = this.store\n      .getState()\n      .Sections.find(section => section.id === SECTION_ID);\n    // Broadcast when required or if it is the first update.\n    const shouldBroadcast = options.broadcast || !initialized;\n\n    SectionsManager.updateSection(\n      SECTION_ID,\n      { rows: highlights },\n      shouldBroadcast\n    );\n  }\n\n  /**\n   * Fetch an image for a given highlight and update the card with it. If no\n   * image is available then fallback to fetching a screenshot.\n   */\n  fetchImage(page) {\n    // Request a screenshot if we don't already have one pending\n    const { preview_image_url: imageUrl, url } = page;\n    return Screenshots.maybeCacheScreenshot(\n      page,\n      imageUrl || url,\n      \"image\",\n      image => {\n        SectionsManager.updateSectionCard(SECTION_ID, url, { image }, true);\n      }\n    );\n  }\n\n  onAction(action) {\n    // Relay the downloads actions to DownloadsManager - it is a child of HighlightsFeed\n    this.downloadsManager.onAction(action);\n    switch (action.type) {\n      case at.INIT:\n        this.init();\n        break;\n      case at.SYSTEM_TICK:\n      case at.TOP_SITES_UPDATED:\n        this.fetchHighlights({ broadcast: false });\n        break;\n      case at.PREF_CHANGED:\n        // Update existing pages when the user changes what should be shown\n        if (action.data.name.startsWith(\"section.highlights.include\")) {\n          this.fetchHighlights({ broadcast: true });\n        }\n        break;\n      case at.PLACES_HISTORY_CLEARED:\n      case at.PLACES_LINK_BLOCKED:\n      case at.DOWNLOAD_CHANGED:\n      case at.POCKET_LINK_DELETED_OR_ARCHIVED:\n        this.fetchHighlights({ broadcast: true });\n        break;\n      case at.PLACES_LINKS_CHANGED:\n      case at.PLACES_SAVED_TO_POCKET:\n        this.linksCache.expire();\n        this.fetchHighlights({ broadcast: false });\n        break;\n      case at.UNINIT:\n        this.uninit();\n        break;\n    }\n  }\n};\n\nconst EXPORTED_SYMBOLS = [\n  \"HighlightsFeed\",\n  \"SECTION_ID\",\n  \"MANY_EXTRA_LENGTH\",\n  \"SYNC_BOOKMARKS_FINISHED_EVENT\",\n  \"BOOKMARKS_RESTORE_SUCCESS_EVENT\",\n  \"BOOKMARKS_RESTORE_FAILED_EVENT\",\n];\n"
  },
  {
    "path": "lib/LinksCache.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\"use strict\";\n\nconst EXPORTED_SYMBOLS = [\"LinksCache\"];\n\n// This should be slightly less than SYSTEM_TICK_INTERVAL as timer\n// comparisons are too exact while the async/await functionality will make the\n// last recorded time a little bit later. This causes the comparasion to skip\n// updates.\n// It should be 10% less than SYSTEM_TICK to update at least once every 5 mins.\n// https://github.com/mozilla/activity-stream/pull/3695#discussion_r144678214\nconst EXPIRATION_TIME = 4.5 * 60 * 1000; // 4.5 minutes\n\n/**\n * Cache link results from a provided object property and refresh after some\n * amount of time has passed. Allows for migrating data from previously cached\n * links to the new links with the same url.\n */\nthis.LinksCache = class LinksCache {\n  /**\n   * Create a links cache for a given object property.\n   *\n   * @param {object} linkObject Object containing the link property\n   * @param {string} linkProperty Name of property on object to access\n   * @param {array} properties Optional properties list to migrate to new links.\n   * @param {function} shouldRefresh Optional callback receiving the old and new\n   *                                 options to refresh even when not expired.\n   */\n  constructor(\n    linkObject,\n    linkProperty,\n    properties = [],\n    shouldRefresh = () => {}\n  ) {\n    this.clear();\n\n    // Allow getting links from both methods and array properties\n    this.linkGetter = options => {\n      const ret = linkObject[linkProperty];\n      return typeof ret === \"function\" ? ret.call(linkObject, options) : ret;\n    };\n\n    // Always migrate the shared cache data in addition to any custom properties\n    this.migrateProperties = [\"__sharedCache\", ...properties];\n    this.shouldRefresh = shouldRefresh;\n  }\n\n  /**\n   * Clear the cached data.\n   */\n  clear() {\n    this.cache = Promise.resolve([]);\n    this.lastOptions = {};\n    this.expire();\n  }\n\n  /**\n   * Force the next request to update the cache.\n   */\n  expire() {\n    delete this.lastUpdate;\n  }\n\n  /**\n   * Request data and update the cache if necessary.\n   *\n   * @param {object} options Optional data to pass to the underlying method.\n   * @returns {promise(array)} Links array with objects that can be modified.\n   */\n  async request(options = {}) {\n    // Update the cache if the data has been expired\n    const now = Date.now();\n    if (\n      this.lastUpdate === undefined ||\n      now > this.lastUpdate + EXPIRATION_TIME ||\n      // Allow custom rules around refreshing based on options\n      this.shouldRefresh(this.lastOptions, options)\n    ) {\n      // Update request state early so concurrent requests can refer to it\n      this.lastOptions = options;\n      this.lastUpdate = now;\n\n      // Save a promise before awaits, so other requests wait for correct data\n      // eslint-disable-next-line no-async-promise-executor\n      this.cache = new Promise(async (resolve, reject) => {\n        try {\n          // Allow fast lookup of old links by url that might need to migrate\n          const toMigrate = new Map();\n          for (const oldLink of await this.cache) {\n            if (oldLink) {\n              toMigrate.set(oldLink.url, oldLink);\n            }\n          }\n\n          // Update the cache with migrated links without modifying source objects\n          resolve(\n            (await this.linkGetter(options)).map(link => {\n              // Keep original array hole positions\n              if (!link) {\n                return link;\n              }\n\n              // Migrate data to the new link copy if we have an old link\n              const newLink = Object.assign({}, link);\n              const oldLink = toMigrate.get(newLink.url);\n              if (oldLink) {\n                for (const property of this.migrateProperties) {\n                  const oldValue = oldLink[property];\n                  if (oldValue !== undefined) {\n                    newLink[property] = oldValue;\n                  }\n                }\n              } else {\n                // Share data among link copies and new links from future requests\n                newLink.__sharedCache = {};\n              }\n              // Provide a helper to update the cached link\n              newLink.__sharedCache.updateLink = (property, value) => {\n                newLink[property] = value;\n              };\n\n              return newLink;\n            })\n          );\n        } catch (error) {\n          reject(error);\n        }\n      });\n    }\n\n    // Provide a shallow copy of the cached link objects for callers to modify\n    return (await this.cache).map(link => link && Object.assign({}, link));\n  }\n};\n"
  },
  {
    "path": "lib/NaiveBayesTextTagger.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\"use strict\";\n\nconst { toksToTfIdfVector } = ChromeUtils.import(\n  \"resource://activity-stream/lib/Tokenize.jsm\"\n);\n\nthis.NaiveBayesTextTagger = class NaiveBayesTextTagger {\n  constructor(model) {\n    this.model = model;\n  }\n\n  /**\n   * Determines if the tokenized text belongs to class according to binary naive Bayes\n   * classifier. Returns an object containing the class label (\"label\"), and\n   * the log probability (\"logProb\") that the text belongs to that class. If\n   * the positive class is more likely, then \"label\" is the positive class\n   * label. If the negative class is matched, then \"label\" is set to null.\n   */\n  tagTokens(tokens) {\n    let fv = toksToTfIdfVector(tokens, this.model.vocab_idfs);\n\n    let bestLogProb = null;\n    let bestClassId = -1;\n    let bestClassLabel = null;\n    let logSumExp = 0.0; // will be P(x). Used to create a proper probability\n    for (let classId = 0; classId < this.model.classes.length; classId++) {\n      let classModel = this.model.classes[classId];\n      let classLogProb = classModel.log_prior;\n\n      // dot fv with the class model\n      for (let pair of Object.values(fv)) {\n        let [termId, tfidf] = pair;\n        classLogProb += tfidf * classModel.feature_log_probs[termId];\n      }\n\n      if (bestLogProb === null || classLogProb > bestLogProb) {\n        bestLogProb = classLogProb;\n        bestClassId = classId;\n      }\n      logSumExp += Math.exp(classLogProb);\n    }\n\n    // now normalize the probability by dividing by P(x)\n    logSumExp = Math.log(logSumExp);\n    bestLogProb -= logSumExp;\n    if (bestClassId === this.model.positive_class_id) {\n      bestClassLabel = this.model.positive_class_label;\n    } else {\n      bestClassLabel = null;\n    }\n\n    let confident =\n      bestClassId === this.model.positive_class_id &&\n      bestLogProb > this.model.positive_class_threshold_log_prob;\n    return {\n      label: bestClassLabel,\n      logProb: bestLogProb,\n      confident,\n    };\n  }\n};\n\nconst EXPORTED_SYMBOLS = [\"NaiveBayesTextTagger\"];\n"
  },
  {
    "path": "lib/NewTabInit.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\"use strict\";\n\nconst { actionCreators: ac, actionTypes: at } = ChromeUtils.import(\n  \"resource://activity-stream/common/Actions.jsm\"\n);\n\n/**\n * NewTabInit - A placeholder for now. This will send a copy of the state to all\n *              newly opened tabs.\n */\nthis.NewTabInit = class NewTabInit {\n  constructor() {\n    this._repliedEarlyTabs = new Map();\n  }\n\n  reply(target) {\n    // Skip this reply if we already replied to an early tab\n    if (this._repliedEarlyTabs.get(target)) {\n      return;\n    }\n\n    const action = {\n      type: at.NEW_TAB_INITIAL_STATE,\n      data: this.store.getState(),\n    };\n    this.store.dispatch(ac.AlsoToOneContent(action, target));\n\n    // Remember that this early tab has already gotten a rehydration response in\n    // case it thought we lost its initial REQUEST and asked again\n    if (this._repliedEarlyTabs.has(target)) {\n      this._repliedEarlyTabs.set(target, true);\n    }\n  }\n\n  onAction(action) {\n    switch (action.type) {\n      case at.NEW_TAB_STATE_REQUEST:\n        this.reply(action.meta.fromTarget);\n        break;\n      case at.NEW_TAB_INIT:\n        // Initialize data for early tabs that might REQUEST twice\n        if (action.data.simulated) {\n          this._repliedEarlyTabs.set(action.data.portID, false);\n        }\n        break;\n      case at.NEW_TAB_UNLOAD:\n        // Clean up for any tab (no-op if not an early tab)\n        this._repliedEarlyTabs.delete(action.meta.fromTarget);\n        break;\n    }\n  }\n};\n\nconst EXPORTED_SYMBOLS = [\"NewTabInit\"];\n"
  },
  {
    "path": "lib/NmfTextTagger.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\"use strict\";\n\nconst { toksToTfIdfVector } = ChromeUtils.import(\n  \"resource://activity-stream/lib/Tokenize.jsm\"\n);\n\nthis.NmfTextTagger = class NmfTextTagger {\n  constructor(model) {\n    this.model = model;\n  }\n\n  /**\n   * A multiclass classifier that scores tokenized text against several classes through\n   * inference of a nonnegative matrix factorization of TF-IDF vectors and\n   * class labels. Returns a map of class labels as string keys to scores.\n   * (Higher is more confident.) All classes get scored, so it is up to\n   * consumer of this data determine what classes are most valuable.\n   */\n  tagTokens(tokens) {\n    let fv = toksToTfIdfVector(tokens, this.model.vocab_idfs);\n    let fve = Object.values(fv);\n\n    // normalize by the sum of the vector\n    let sum = 0.0;\n    for (let pair of fve) {\n      // eslint-disable-next-line prefer-destructuring\n      sum += pair[1];\n    }\n    for (let i = 0; i < fve.length; i++) {\n      // eslint-disable-next-line prefer-destructuring\n      fve[i][1] /= sum;\n    }\n\n    // dot the document with each topic vector so that we can transform it into\n    // the latent space\n    let toksInLatentSpace = [];\n    for (let topicVect of this.model.topic_word) {\n      let fvDotTwv = 0;\n      // dot fv with each topic word vector\n      for (let pair of fve) {\n        let [termId, tfidf] = pair;\n        fvDotTwv += tfidf * topicVect[termId];\n      }\n      toksInLatentSpace.push(fvDotTwv);\n    }\n\n    // now project toksInLatentSpace back into class space\n    let predictions = {};\n    Object.keys(this.model.document_topic).forEach(topic => {\n      let score = 0;\n      for (let i = 0; i < toksInLatentSpace.length; i++) {\n        score += toksInLatentSpace[i] * this.model.document_topic[topic][i];\n      }\n      predictions[topic] = score;\n    });\n\n    return predictions;\n  }\n};\n\nconst EXPORTED_SYMBOLS = [\"NmfTextTagger\"];\n"
  },
  {
    "path": "lib/OnboardingMessageProvider.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\"use strict\";\n/* globals Localization */\nChromeUtils.defineModuleGetter(\n  this,\n  \"AttributionCode\",\n  \"resource:///modules/AttributionCode.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"AddonRepository\",\n  \"resource://gre/modules/addons/AddonRepository.jsm\"\n);\nconst { Services } = ChromeUtils.import(\"resource://gre/modules/Services.jsm\");\nconst FX_MONITOR_CLIENT_ID = \"802d56ef2a9af9fa\";\n\nconst L10N = new Localization([\n  \"branding/brand.ftl\",\n  \"browser/branding/brandings.ftl\",\n  \"browser/branding/sync-brand.ftl\",\n  \"browser/newtab/onboarding.ftl\",\n]);\n\nconst TRAILHEAD_ONBOARDING_TEMPLATE = {\n  trigger: { id: \"firstRun\" },\n  template: \"trailhead\",\n  includeBundle: {\n    length: 3,\n    template: \"onboarding\",\n    trigger: { id: \"showOnboarding\" },\n  },\n};\n\nconst TRAILHEAD_MODAL_VARIANT_CONTENT = {\n  className: \"joinCohort\",\n  benefits: [\"sync\", \"monitor\", \"lockwise\"].map(id => ({\n    id,\n    title: { string_id: `onboarding-benefit-${id}-title` },\n    text: { string_id: `onboarding-benefit-${id}-text` },\n  })),\n  learn: {\n    text: { string_id: \"onboarding-welcome-modal-family-learn-more\" },\n    url: \"https://www.mozilla.org/firefox/accounts/\",\n  },\n  form: {\n    title: { string_id: \"onboarding-welcome-form-header\" },\n    text: { string_id: \"onboarding-join-form-body\" },\n    email: { string_id: \"onboarding-join-form-email\" },\n    button: { string_id: \"onboarding-join-form-continue\" },\n  },\n  skipButton: { string_id: \"onboarding-start-browsing-button-label\" },\n};\n\nconst TRAILHEAD_FULL_PAGE_CONTENT = {\n  title: { string_id: \"onboarding-welcome-body\" },\n  learn: {\n    text: { string_id: \"onboarding-welcome-learn-more\" },\n    url: \"https://www.mozilla.org/firefox/accounts/\",\n  },\n  form: {\n    title: { string_id: \"onboarding-welcome-form-header\" },\n    text: { string_id: \"onboarding-join-form-body\" },\n    email: { string_id: \"onboarding-fullpage-form-email\" },\n    button: { string_id: \"onboarding-join-form-continue\" },\n  },\n};\n\nconst JOIN_CONTENT = {\n  className: \"joinCohort\",\n  title: { string_id: \"onboarding-welcome-body\" },\n  benefits: [\"products\", \"knowledge\", \"privacy\"].map(id => ({\n    id,\n    title: { string_id: `onboarding-benefit-${id}-title` },\n    text: { string_id: `onboarding-benefit-${id}-text` },\n  })),\n  learn: {\n    text: { string_id: \"onboarding-welcome-learn-more\" },\n    url: \"https://www.mozilla.org/firefox/accounts/\",\n  },\n  form: {\n    title: { string_id: \"onboarding-join-form-header\" },\n    text: { string_id: \"onboarding-join-form-body\" },\n    email: { string_id: \"onboarding-join-form-email\" },\n    button: { string_id: \"onboarding-join-form-continue\" },\n  },\n  skipButton: { string_id: \"onboarding-start-browsing-button-label\" },\n};\n\nconst ONBOARDING_MESSAGES = () => [\n  {\n    id: \"TRAILHEAD_1\",\n    utm_term: \"trailhead-join\",\n    ...TRAILHEAD_ONBOARDING_TEMPLATE,\n    content: {\n      ...JOIN_CONTENT,\n    },\n  },\n  {\n    id: \"TRAILHEAD_2\",\n    targeting: \"trailheadInterrupt == 'sync'\",\n    utm_term: \"trailhead-sync\",\n    ...TRAILHEAD_ONBOARDING_TEMPLATE,\n    content: {\n      className: \"syncCohort\",\n      title: { string_id: \"onboarding-sync-welcome-header\" },\n      subtitle: { string_id: \"onboarding-sync-welcome-content\" },\n      benefits: [],\n      learn: {\n        text: { string_id: \"onboarding-sync-welcome-learn-more-link\" },\n        url: \"https://www.mozilla.org/firefox/accounts/\",\n      },\n      form: {\n        title: { string_id: \"onboarding-sync-form-header\" },\n        text: { string_id: \"onboarding-sync-form-sub-header\" },\n        email: { string_id: \"onboarding-sync-form-input\" },\n        button: { string_id: \"onboarding-sync-form-continue-button\" },\n      },\n      skipButton: { string_id: \"onboarding-sync-form-skip-login-button\" },\n    },\n  },\n  {\n    id: \"TRAILHEAD_3\",\n    targeting: \"trailheadInterrupt == 'cards'\",\n    utm_term: \"trailhead-cards\",\n    ...TRAILHEAD_ONBOARDING_TEMPLATE,\n  },\n  {\n    id: \"TRAILHEAD_4\",\n    template: \"trailhead\",\n    targeting: \"trailheadInterrupt == 'nofirstrun'\",\n    trigger: { id: \"firstRun\" },\n  },\n  {\n    id: \"TRAILHEAD_5\",\n    targeting: \"trailheadInterrupt == 'modal_control'\",\n    utm_term: \"trailhead-modal_control\",\n    ...TRAILHEAD_ONBOARDING_TEMPLATE,\n    content: {\n      ...JOIN_CONTENT,\n    },\n  },\n  {\n    id: \"TRAILHEAD_6\",\n    targeting: \"trailheadInterrupt == 'modal_variant_a'\",\n    utm_term: \"trailhead-modal_variant_a\",\n    ...TRAILHEAD_ONBOARDING_TEMPLATE,\n    content: {\n      ...TRAILHEAD_MODAL_VARIANT_CONTENT,\n      title: { string_id: \"onboarding-welcome-modal-get-body\" },\n    },\n  },\n  {\n    id: \"TRAILHEAD_7\",\n    targeting: \"trailheadInterrupt == 'modal_variant_b'\",\n    utm_term: \"trailhead-modal_variant_b\",\n    ...TRAILHEAD_ONBOARDING_TEMPLATE,\n    content: {\n      ...TRAILHEAD_MODAL_VARIANT_CONTENT,\n      title: { string_id: \"onboarding-welcome-modal-supercharge-body\" },\n    },\n  },\n  {\n    id: \"TRAILHEAD_8\",\n    targeting: \"trailheadInterrupt == 'modal_variant_c'\",\n    utm_term: \"trailhead-modal_variant_c\",\n    ...TRAILHEAD_ONBOARDING_TEMPLATE,\n    content: {\n      ...TRAILHEAD_MODAL_VARIANT_CONTENT,\n      title: { string_id: \"onboarding-welcome-modal-privacy-body\" },\n    },\n  },\n  {\n    id: \"TRAILHEAD_9\",\n    targeting: \"trailheadInterrupt == 'modal_variant_f'\",\n    utm_term: \"trailhead-modal_variant_f\",\n    ...TRAILHEAD_ONBOARDING_TEMPLATE,\n    content: {\n      ...JOIN_CONTENT,\n      form: TRAILHEAD_MODAL_VARIANT_CONTENT.form,\n    },\n  },\n  {\n    id: \"FULL_PAGE_1\",\n    targeting: \"trailheadInterrupt == 'full_page_d'\",\n    utm_term: \"trailhead-full_page_d\",\n    ...TRAILHEAD_ONBOARDING_TEMPLATE,\n    content: {\n      ...TRAILHEAD_FULL_PAGE_CONTENT,\n    },\n    template: \"full_page_interrupt\",\n  },\n  {\n    id: \"FULL_PAGE_2\",\n    targeting: \"trailheadInterrupt == 'full_page_e'\",\n    utm_term: \"trailhead-full_page_e\",\n    ...TRAILHEAD_ONBOARDING_TEMPLATE,\n    content: {\n      className: \"fullPageCardsAtTop\",\n      ...TRAILHEAD_FULL_PAGE_CONTENT,\n    },\n    template: \"full_page_interrupt\",\n  },\n  {\n    id: \"EXTENDED_TRIPLETS_1\",\n    template: \"extended_triplets\",\n    campaign: \"firstrun_triplets\",\n    targeting:\n      \"trailheadTriplet && ((currentDate|date - profileAgeCreated) / 86400000) < 7\",\n    includeBundle: {\n      length: 3,\n      template: \"onboarding\",\n      trigger: { id: \"showOnboarding\" },\n    },\n    frequency: { lifetime: 5 },\n    utm_term: \"trailhead-cards\",\n  },\n  {\n    id: \"TRAILHEAD_CARD_1\",\n    template: \"onboarding\",\n    bundled: 3,\n    order: 2,\n    content: {\n      title: { string_id: \"onboarding-tracking-protection-title2\" },\n      text: { string_id: \"onboarding-tracking-protection-text2\" },\n      icon: \"tracking\",\n      primary_button: {\n        label: { string_id: \"onboarding-tracking-protection-button2\" },\n        action:\n          Services.locale.appLocaleAsLangTag.substr(0, 2) === \"en\"\n            ? {\n                type: \"OPEN_URL\",\n                data: {\n                  args: \"https://mzl.la/ETPdefault\",\n                  where: \"tabshifted\",\n                },\n              }\n            : {\n                type: \"OPEN_PREFERENCES_PAGE\",\n                data: { category: \"privacy-trackingprotection\" },\n              },\n      },\n    },\n    targeting: \"trailheadTriplet == 'privacy'\",\n    trigger: { id: \"showOnboarding\" },\n  },\n  {\n    id: \"TRAILHEAD_CARD_2\",\n    template: \"onboarding\",\n    bundled: 3,\n    order: 1,\n    content: {\n      title: { string_id: \"onboarding-data-sync-title\" },\n      text: { string_id: \"onboarding-data-sync-text2\" },\n      icon: \"devices\",\n      primary_button: {\n        label: { string_id: \"onboarding-data-sync-button2\" },\n        action: {\n          type: \"OPEN_URL\",\n          addFlowParams: true,\n          data: {\n            args:\n              \"https://accounts.firefox.com/?service=sync&action=email&context=fx_desktop_v3&entrypoint=activity-stream-firstrun&style=trailhead\",\n            where: \"tabshifted\",\n          },\n        },\n      },\n    },\n    targeting:\n      \"trailheadTriplet in ['supercharge', 'static'] || ( 'dynamic' in trailheadTriplet && usesFirefoxSync == false)\",\n    trigger: { id: \"showOnboarding\" },\n  },\n  {\n    id: \"TRAILHEAD_CARD_3\",\n    template: \"onboarding\",\n    bundled: 3,\n    order: 2,\n    content: {\n      title: { string_id: \"onboarding-firefox-monitor-title\" },\n      text: { string_id: \"onboarding-firefox-monitor-text2\" },\n      icon: \"ffmonitor\",\n      primary_button: {\n        label: { string_id: \"onboarding-firefox-monitor-button\" },\n        action: {\n          type: \"OPEN_URL\",\n          data: { args: \"https://monitor.firefox.com/\", where: \"tabshifted\" },\n        },\n      },\n    },\n    // Use service oauth client_id to identify 'Firefox Monitor' service attached to Firefox Account\n    // https://docs.telemetry.mozilla.org/datasets/fxa_metrics/attribution.html#service-attribution\n    targeting: `trailheadTriplet in ['supercharge', 'static'] || ('dynamic' in trailheadTriplet && !(\"${FX_MONITOR_CLIENT_ID}\" in attachedFxAOAuthClients|mapToProperty('id')))`,\n    trigger: { id: \"showOnboarding\" },\n  },\n  {\n    id: \"TRAILHEAD_CARD_4\",\n    template: \"onboarding\",\n    bundled: 3,\n    order: 3,\n    content: {\n      title: { string_id: \"onboarding-browse-privately-title\" },\n      text: { string_id: \"onboarding-browse-privately-text\" },\n      icon: \"private\",\n      primary_button: {\n        label: { string_id: \"onboarding-browse-privately-button\" },\n        action: { type: \"OPEN_PRIVATE_BROWSER_WINDOW\" },\n      },\n    },\n    targeting: \"'dynamic' in trailheadTriplet\",\n    trigger: { id: \"showOnboarding\" },\n  },\n  {\n    id: \"TRAILHEAD_CARD_5\",\n    template: \"onboarding\",\n    bundled: 3,\n    order: 5,\n    content: {\n      title: { string_id: \"onboarding-firefox-send-title\" },\n      text: { string_id: \"onboarding-firefox-send-text2\" },\n      icon: \"ffsend\",\n      primary_button: {\n        label: { string_id: \"onboarding-firefox-send-button\" },\n        action: {\n          type: \"OPEN_URL\",\n          data: { args: \"https://send.firefox.com/\", where: \"tabshifted\" },\n        },\n      },\n    },\n    targeting: \"trailheadTriplet == 'payoff'\",\n    trigger: { id: \"showOnboarding\" },\n  },\n  {\n    id: \"TRAILHEAD_CARD_6\",\n    template: \"onboarding\",\n    bundled: 3,\n    order: 6,\n    content: {\n      title: { string_id: \"onboarding-mobile-phone-title\" },\n      text: { string_id: \"onboarding-mobile-phone-text\" },\n      icon: \"mobile\",\n      primary_button: {\n        label: { string_id: \"onboarding-mobile-phone-button\" },\n        action: {\n          type: \"OPEN_URL\",\n          data: {\n            args: \"https://www.mozilla.org/firefox/mobile/\",\n            where: \"tabshifted\",\n          },\n        },\n      },\n    },\n    targeting:\n      \"trailheadTriplet in ['supercharge', 'static'] || ('dynamic' in trailheadTriplet && sync.mobileDevices < 1)\",\n    trigger: { id: \"showOnboarding\" },\n  },\n  {\n    id: \"TRAILHEAD_CARD_7\",\n    template: \"onboarding\",\n    bundled: 3,\n    order: 4,\n    content: {\n      title: { string_id: \"onboarding-send-tabs-title\" },\n      text: { string_id: \"onboarding-send-tabs-text2\" },\n      icon: \"sendtab\",\n      primary_button: {\n        label: { string_id: \"onboarding-send-tabs-button\" },\n        action: {\n          type: \"OPEN_URL\",\n          data: {\n            args:\n              \"https://support.mozilla.org/kb/send-tab-firefox-desktop-other-devices\",\n            where: \"tabshifted\",\n          },\n        },\n      },\n    },\n    targeting: \"'dynamic' in trailheadTriplet\",\n    trigger: { id: \"showOnboarding\" },\n  },\n  {\n    id: \"TRAILHEAD_CARD_8\",\n    template: \"onboarding\",\n    bundled: 3,\n    order: 2,\n    content: {\n      title: { string_id: \"onboarding-pocket-anywhere-title\" },\n      text: { string_id: \"onboarding-pocket-anywhere-text2\" },\n      icon: \"pocket\",\n      primary_button: {\n        label: { string_id: \"onboarding-pocket-anywhere-button\" },\n        action: {\n          type: \"OPEN_URL\",\n          data: {\n            args: \"https://getpocket.com/firefox_learnmore\",\n            where: \"tabshifted\",\n          },\n        },\n      },\n    },\n    targeting: \"trailheadTriplet == 'multidevice'\",\n    trigger: { id: \"showOnboarding\" },\n  },\n  {\n    id: \"TRAILHEAD_CARD_9\",\n    template: \"onboarding\",\n    bundled: 3,\n    order: 7,\n    content: {\n      title: { string_id: \"onboarding-lockwise-strong-passwords-title\" },\n      text: { string_id: \"onboarding-lockwise-strong-passwords-text\" },\n      icon: \"lockwise\",\n      primary_button: {\n        label: { string_id: \"onboarding-lockwise-strong-passwords-button\" },\n        action: {\n          type: \"OPEN_ABOUT_PAGE\",\n          data: { args: \"logins\", where: \"tabshifted\" },\n        },\n      },\n    },\n    targeting: \"'dynamic' in trailheadTriplet\",\n    trigger: { id: \"showOnboarding\" },\n  },\n  {\n    id: \"TRAILHEAD_CARD_10\",\n    template: \"onboarding\",\n    bundled: 3,\n    order: 4,\n    content: {\n      title: { string_id: \"onboarding-facebook-container-title\" },\n      text: { string_id: \"onboarding-facebook-container-text2\" },\n      icon: \"fbcont\",\n      primary_button: {\n        label: { string_id: \"onboarding-facebook-container-button\" },\n        action: {\n          type: \"OPEN_URL\",\n          data: {\n            args:\n              \"https://addons.mozilla.org/firefox/addon/facebook-container/\",\n            where: \"tabshifted\",\n          },\n        },\n      },\n    },\n    targeting: \"trailheadTriplet == 'payoff'\",\n    trigger: { id: \"showOnboarding\" },\n  },\n  {\n    id: \"TRAILHEAD_CARD_11\",\n    template: \"onboarding\",\n    bundled: 3,\n    order: 0,\n    content: {\n      title: { string_id: \"onboarding-import-browser-settings-title\" },\n      text: { string_id: \"onboarding-import-browser-settings-text\" },\n      icon: \"import\",\n      primary_button: {\n        label: { string_id: \"onboarding-import-browser-settings-button\" },\n        action: { type: \"SHOW_MIGRATION_WIZARD\" },\n      },\n    },\n    targeting: \"trailheadTriplet == 'dynamic_chrome'\",\n    trigger: { id: \"showOnboarding\" },\n  },\n  {\n    id: \"RETURN_TO_AMO_1\",\n    template: \"return_to_amo_overlay\",\n    content: {\n      header: { string_id: \"onboarding-welcome-header\" },\n      title: { string_id: \"return-to-amo-sub-header\" },\n      addon_icon: null,\n      icon: \"gift-extension\",\n      text: {\n        string_id: \"return-to-amo-addon-header\",\n        args: { \"addon-name\": null },\n      },\n      primary_button: {\n        label: { string_id: \"return-to-amo-extension-button\" },\n        action: {\n          type: \"INSTALL_ADDON_FROM_URL\",\n          data: { url: null, telemetrySource: \"rtamo\" },\n        },\n      },\n      secondary_button: {\n        label: { string_id: \"return-to-amo-get-started-button\" },\n      },\n    },\n    includeBundle: {\n      length: 3,\n      template: \"onboarding\",\n      trigger: { id: \"showOnboarding\" },\n    },\n    targeting:\n      \"attributionData.campaign == 'non-fx-button' && attributionData.source == 'addons.mozilla.org'\",\n    trigger: { id: \"firstRun\" },\n  },\n  {\n    id: \"FXA_ACCOUNTS_BADGE\",\n    template: \"toolbar_badge\",\n    content: {\n      delay: 10000, // delay for 10 seconds\n      target: \"fxa-toolbar-menu-button\",\n    },\n    // Never accessed the FxA panel && doesn't use Firefox sync & has FxA enabled\n    targeting: `isFxABadgeEnabled && !hasAccessedFxAPanel && !usesFirefoxSync && isFxAEnabled == true`,\n    trigger: { id: \"toolbarBadgeUpdate\" },\n  },\n  {\n    id: \"PROTECTIONS_PANEL_1\",\n    template: \"protections_panel\",\n    content: {\n      title: { string_id: \"cfr-protections-panel-header\" },\n      body: { string_id: \"cfr-protections-panel-body\" },\n      link_text: { string_id: \"cfr-protections-panel-link-text\" },\n      cta_url: `${Services.urlFormatter.formatURLPref(\n        \"app.support.baseURL\"\n      )}etp-promotions?as=u&utm_source=inproduct`,\n      cta_type: \"OPEN_URL\",\n    },\n    trigger: { id: \"protectionsPanelOpen\" },\n  },\n];\n\nconst OnboardingMessageProvider = {\n  async getExtraAttributes() {\n    const [header, button_label] = await L10N.formatMessages([\n      { id: \"onboarding-welcome-header\" },\n      { id: \"onboarding-start-browsing-button-label\" },\n    ]);\n    return { header: header.value, button_label: button_label.value };\n  },\n  async getMessages() {\n    const messages = await this.translateMessages(await ONBOARDING_MESSAGES());\n    return messages;\n  },\n  async getUntranslatedMessages() {\n    // This is helpful for jsonSchema testing - since we are localizing in the provider\n    const messages = await ONBOARDING_MESSAGES();\n    return messages;\n  },\n  async translateMessages(messages) {\n    let translatedMessages = [];\n    for (const msg of messages) {\n      let translatedMessage = { ...msg };\n\n      // If the message has no content, do not attempt to translate it\n      if (!translatedMessage.content) {\n        translatedMessages.push(translatedMessage);\n        continue;\n      }\n\n      // We need some addon info if we are showing return to amo overlay, so fetch\n      // that, and update the message accordingly\n      if (msg.template === \"return_to_amo_overlay\") {\n        try {\n          const { name, iconURL, url } = await this.getAddonInfo();\n          // If we do not have all the data from the AMO api to indicate to the user\n          // what they are installing we don't want to show the message\n          if (!name || !iconURL || !url) {\n            continue;\n          }\n\n          msg.content.text.args[\"addon-name\"] = name;\n          msg.content.addon_icon = iconURL;\n          msg.content.primary_button.action.data.url = url;\n        } catch (e) {\n          continue;\n        }\n\n        // We know we want to show this message, so translate message strings\n        const [\n          primary_button_string,\n          title_string,\n          text_string,\n        ] = await L10N.formatMessages([\n          { id: msg.content.primary_button.label.string_id },\n          { id: msg.content.title.string_id },\n          { id: msg.content.text.string_id, args: msg.content.text.args },\n        ]);\n        translatedMessage.content.primary_button.label =\n          primary_button_string.value;\n        translatedMessage.content.title = title_string.value;\n        translatedMessage.content.text = text_string.value;\n      }\n\n      // Translate any secondary buttons separately\n      if (msg.content.secondary_button) {\n        const [secondary_button_string] = await L10N.formatMessages([\n          { id: msg.content.secondary_button.label.string_id },\n        ]);\n        translatedMessage.content.secondary_button.label =\n          secondary_button_string.value;\n      }\n      if (msg.content.header) {\n        const [header_string] = await L10N.formatMessages([\n          { id: msg.content.header.string_id },\n        ]);\n        translatedMessage.content.header = header_string.value;\n      }\n      translatedMessages.push(translatedMessage);\n    }\n    return translatedMessages;\n  },\n  async getAddonInfo() {\n    try {\n      let { content, source } = await AttributionCode.getAttrDataAsync();\n      if (!content || source !== \"addons.mozilla.org\") {\n        return null;\n      }\n      // Attribution data can be double encoded\n      while (content.includes(\"%\")) {\n        try {\n          const result = decodeURIComponent(content);\n          if (result === content) {\n            break;\n          }\n          content = result;\n        } catch (e) {\n          break;\n        }\n      }\n      const [addon] = await AddonRepository.getAddonsByIDs([content]);\n      if (addon.sourceURI.scheme !== \"https\") {\n        return null;\n      }\n      return {\n        name: addon.name,\n        url: addon.sourceURI.spec,\n        iconURL: addon.icons[\"64\"] || addon.icons[\"32\"],\n      };\n    } catch (e) {\n      Cu.reportError(\n        \"Failed to get the latest add-on version for Return to AMO\"\n      );\n      return null;\n    }\n  },\n};\nthis.OnboardingMessageProvider = OnboardingMessageProvider;\n\nconst EXPORTED_SYMBOLS = [\"OnboardingMessageProvider\"];\n"
  },
  {
    "path": "lib/PanelTestProvider.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\"use strict\";\n\nconst TWO_DAYS = 2 * 24 * 3600 * 1000;\n\nconst MESSAGES = () => [\n  {\n    id: \"SIMPLE_FXA_BOOKMARK_TEST_FLUENT\",\n    template: \"fxa_bookmark_panel\",\n    content: {\n      title: { string_id: \"cfr-doorhanger-bookmark-fxa-header\" },\n      text: { string_id: \"cfr-doorhanger-bookmark-fxa-body\" },\n      cta: { string_id: \"cfr-doorhanger-bookmark-fxa-link-text\" },\n      color: \"white\",\n      background_color_1: \"#7d31ae\",\n      background_color_2: \"#5033be\",\n      info_icon: {\n        tooltiptext: {\n          string_id: \"cfr-doorhanger-bookmark-fxa-info-icon-tooltip\",\n        },\n      },\n      close_button: {\n        tooltiptext: {\n          string_id: \"cfr-doorhanger-bookmark-fxa-close-btn-tooltip\",\n        },\n      },\n    },\n    trigger: { id: \"bookmark-panel\" },\n  },\n  {\n    id: \"SIMPLE_FXA_BOOKMARK_TEST_NON_FLUENT\",\n    template: \"fxa_bookmark_panel\",\n    content: {\n      title: \"Bookmark Message Title\",\n      text: \"Bookmark Message Body\",\n      cta: \"Sync bookmarks now\",\n      color: \"white\",\n      background_color_1: \"#7d31ae\",\n      background_color_2: \"#5033be\",\n      info_icon: {\n        tooltiptext: \"Toggle tooltip\",\n      },\n      close_button: {\n        tooltiptext: \"Close tooltip\",\n      },\n    },\n    trigger: { id: \"bookmark-panel\" },\n  },\n  {\n    id: \"WNP_THANK_YOU\",\n    template: \"update_action\",\n    content: {\n      action: {\n        id: \"moments-wnp\",\n        data: {\n          url:\n            \"https://www.mozilla.org/%LOCALE%/etc/firefox/retention/thank-you-a/\",\n          expireDelta: TWO_DAYS,\n        },\n      },\n    },\n    trigger: { id: \"momentsUpdate\" },\n  },\n  {\n    id: \"WHATS_NEW_PIP_72\",\n    template: \"whatsnew_panel_message\",\n    order: 4,\n    content: {\n      bucket_id: \"WHATS_NEW_72\",\n      published_date: 1574776601000,\n      title: { string_id: \"cfr-whatsnew-pip-header\" },\n      icon_url:\n        \"resource://activity-stream/data/content/assets/remote/pip-message-icon.svg\",\n      icon_alt: \"\",\n      body: { string_id: \"cfr-whatsnew-pip-body\" },\n      cta_url:\n        \"https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/whats-new-notifications\",\n      cta_type: \"OPEN_URL\",\n      link_text: { string_id: \"cfr-whatsnew-pip-cta\" },\n    },\n    targeting: `firefoxVersion >= 72`,\n    trigger: { id: \"whatsNewPanelOpened\" },\n  },\n  {\n    id: \"WHATS_NEW_PERMISSION_PROMPT_72\",\n    template: \"whatsnew_panel_message\",\n    order: 5,\n    content: {\n      bucket_id: \"WHATS_NEW_72\",\n      published_date: 1574776601000,\n      title: { string_id: \"cfr-whatsnew-permission-prompt-header\" },\n      body: { string_id: \"cfr-whatsnew-permission-prompt-body\" },\n      cta_url:\n        \"https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/whats-new-notifications\",\n      cta_type: \"OPEN_URL\",\n      link_text: { string_id: \"cfr-whatsnew-permission-prompt-cta\" },\n    },\n    targeting: `firefoxVersion >= 72`,\n    trigger: { id: \"whatsNewPanelOpened\" },\n  },\n  {\n    id: \"WHATS_NEW_FINGERPRINTER_COUNTER_72\",\n    template: \"whatsnew_panel_message\",\n    order: 6,\n    content: {\n      bucket_id: \"WHATS_NEW_72\",\n      published_date: 1574776601000,\n      layout: \"tracking-protections\",\n      layout_title_content_variable: \"fingerprinterCount\",\n      title: { string_id: \"cfr-whatsnew-fingerprinter-counter-header\" },\n      subtitle: { string_id: \"cfr-whatsnew-tracking-blocked-subtitle\" },\n      icon_url:\n        \"resource://activity-stream/data/content/assets/protection-report-icon.png\",\n      icon_alt: \"\",\n      body: { string_id: \"cfr-whatsnew-fingerprinter-counter-body\" },\n      link_text: { string_id: \"cfr-whatsnew-tracking-blocked-link-text\" },\n      cta_url: \"protections\",\n      cta_type: \"OPEN_ABOUT_PAGE\",\n    },\n    targeting: `firefoxVersion >= 72`,\n    trigger: { id: \"whatsNewPanelOpened\" },\n  },\n  {\n    id: \"WHATS_NEW_FINGERPRINTER_COUNTER_ALT\",\n    template: \"whatsnew_panel_message\",\n    order: 6,\n    content: {\n      bucket_id: \"WHATS_NEW_72\",\n      published_date: 1574776601000,\n      title: { string_id: \"cfr-whatsnew-fingerprinter-counter-header-alt\" },\n      icon_url:\n        \"resource://activity-stream/data/content/assets/protection-report-icon.png\",\n      icon_alt: \"\",\n      body: { string_id: \"cfr-whatsnew-fingerprinter-counter-body-alt\" },\n      link_text: { string_id: \"cfr-whatsnew-tracking-blocked-link-text\" },\n      cta_url: \"protections\",\n      cta_type: \"OPEN_ABOUT_PAGE\",\n    },\n    targeting: `firefoxVersion >= 72`,\n    trigger: { id: \"whatsNewPanelOpened\" },\n  },\n  {\n    id: \"WHATS_NEW_70_1\",\n    template: \"whatsnew_panel_message\",\n    order: 3,\n    content: {\n      bucket_id: \"WHATS_NEW_70_1\",\n      published_date: 1560969794394,\n      title: \"Protection Is Our Focus\",\n      icon_url:\n        \"resource://activity-stream/data/content/assets/whatsnew-send-icon.png\",\n      icon_alt: \"Firefox Send Logo\",\n      body:\n        \"The New Enhanced Tracking Protection, gives you the best level of protection and performance. Discover how this version is the safest version of firefox ever made.\",\n      cta_url: \"https://blog.mozilla.org/\",\n      cta_type: \"OPEN_URL\",\n    },\n    targeting: `firefoxVersion > 69`,\n    trigger: { id: \"whatsNewPanelOpened\" },\n  },\n  {\n    id: \"WHATS_NEW_70_2\",\n    template: \"whatsnew_panel_message\",\n    order: 1,\n    content: {\n      bucket_id: \"WHATS_NEW_70_1\",\n      published_date: 1560969794394,\n      title: \"Another thing new in Firefox 70\",\n      body:\n        \"The New Enhanced Tracking Protection, gives you the best level of protection and performance. Discover how this version is the safest version of firefox ever made.\",\n      link_text: \"Learn more on our blog\",\n      cta_url: \"https://blog.mozilla.org/\",\n      cta_type: \"OPEN_URL\",\n    },\n    targeting: `firefoxVersion > 69`,\n    trigger: { id: \"whatsNewPanelOpened\" },\n  },\n  {\n    id: \"WHATS_NEW_69_1\",\n    template: \"whatsnew_panel_message\",\n    order: 1,\n    content: {\n      bucket_id: \"WHATS_NEW_69_1\",\n      published_date: 1557346235089,\n      title: \"Something new in Firefox 69\",\n      body:\n        \"The New Enhanced Tracking Protection, gives you the best level of protection and performance. Discover how this version is the safest version of firefox ever made.\",\n      link_text: \"Learn more on our blog\",\n      cta_url: \"https://blog.mozilla.org/\",\n      cta_type: \"OPEN_URL\",\n    },\n    targeting: `firefoxVersion > 68`,\n    trigger: { id: \"whatsNewPanelOpened\" },\n  },\n  {\n    id: \"WHATS_NEW_70_3\",\n    template: \"whatsnew_panel_message\",\n    order: 2,\n    content: {\n      bucket_id: \"WHATS_NEW_70_3\",\n      published_date: 1560969794394,\n      layout: \"tracking-protections\",\n      layout_title_content_variable: \"blockedCount\",\n      title: { string_id: \"cfr-whatsnew-tracking-blocked-title\" },\n      subtitle: { string_id: \"cfr-whatsnew-tracking-blocked-subtitle\" },\n      icon_url:\n        \"resource://activity-stream/data/content/assets/protection-report-icon.png\",\n      icon_alt: \"Protection Report icon\",\n      body: { string_id: \"cfr-whatsnew-tracking-protect-body\" },\n      link_text: { string_id: \"cfr-whatsnew-tracking-blocked-link-text\" },\n      cta_url: \"protections\",\n      cta_type: \"OPEN_ABOUT_PAGE\",\n    },\n    targeting: `firefoxVersion > 69 && totalBlockedCount > 0`,\n    trigger: { id: \"whatsNewPanelOpened\" },\n  },\n  {\n    id: \"BOOKMARK_CFR\",\n    template: \"cfr_doorhanger\",\n    content: {\n      layout: \"icon_and_message\",\n      category: \"cfrFeatures\",\n      notification_text: { string_id: \"cfr-doorhanger-feature-notification\" },\n      heading_text: { string_id: \"cfr-doorhanger-sync-bookmarks-header\" },\n      info_icon: {\n        label: { string_id: \"cfr-doorhanger-extension-sumo-link\" },\n        sumo_path: \"https://example.com\",\n      },\n      text: { string_id: \"cfr-doorhanger-sync-bookmarks-body\" },\n      icon: \"chrome://branding/content/icon64.png\",\n      buttons: {\n        primary: {\n          label: { string_id: \"cfr-doorhanger-sync-bookmarks-ok-button\" },\n          action: {\n            type: \"OPEN_PREFERENCES_PAGE\",\n            data: { category: \"sync\" },\n          },\n        },\n        secondary: [\n          {\n            label: { string_id: \"cfr-doorhanger-extension-cancel-button\" },\n            action: { type: \"CANCEL\" },\n          },\n          {\n            label: {\n              string_id: \"cfr-doorhanger-extension-never-show-recommendation\",\n            },\n          },\n          {\n            label: {\n              string_id: \"cfr-doorhanger-extension-manage-settings-button\",\n            },\n            action: {\n              type: \"OPEN_PREFERENCES_PAGE\",\n              data: { category: \"general-cfrfeatures\" },\n            },\n          },\n        ],\n      },\n    },\n    targeting: \"true\",\n    trigger: {\n      id: \"openBookmarkedURL\",\n    },\n  },\n  {\n    id: \"PDF_URL_FFX_SEND\",\n    template: \"cfr_doorhanger\",\n    content: {\n      layout: \"icon_and_message\",\n      category: \"cfrFeatures\",\n      notification_text: { string_id: \"cfr-doorhanger-extension-notification\" },\n      heading_text: { string_id: \"cfr-doorhanger-firefox-send-header\" },\n      info_icon: {\n        label: { string_id: \"cfr-doorhanger-extension-sumo-link\" },\n        sumo_path: \"https://example.com\",\n      },\n      text: { string_id: \"cfr-doorhanger-firefox-send-body\" },\n      icon: \"chrome://branding/content/icon64.png\",\n      buttons: {\n        primary: {\n          label: { string_id: \"cfr-doorhanger-firefox-send-ok-button\" },\n          action: {\n            type: \"OPEN_URL\",\n            data: {\n              args:\n                \"https://send.firefox.com/login/?utm_source=activity-stream&entrypoint=activity-stream-cfr-pdf\",\n              where: \"tabshifted\",\n            },\n          },\n        },\n        secondary: [\n          {\n            label: { string_id: \"cfr-doorhanger-extension-cancel-button\" },\n            action: { type: \"CANCEL\" },\n          },\n          {\n            label: {\n              string_id: \"cfr-doorhanger-extension-never-show-recommendation\",\n            },\n          },\n          {\n            label: {\n              string_id: \"cfr-doorhanger-extension-manage-settings-button\",\n            },\n            action: {\n              type: \"OPEN_PREFERENCES_PAGE\",\n              data: { category: \"general-cfrfeatures\" },\n            },\n          },\n        ],\n      },\n    },\n    targeting: \"true\",\n    trigger: {\n      id: \"openURL\",\n      patterns: [\"*://*/*.pdf\"],\n    },\n  },\n  {\n    id: \"SEND_TAB_CFR\",\n    template: \"cfr_doorhanger\",\n    content: {\n      layout: \"icon_and_message\",\n      category: \"cfrFeatures\",\n      notification_text: { string_id: \"cfr-doorhanger-extension-notification\" },\n      heading_text: { string_id: \"cfr-doorhanger-send-tab-header\" },\n      info_icon: {\n        label: { string_id: \"cfr-doorhanger-extension-sumo-link\" },\n        sumo_path: \"https://example.com\",\n      },\n      text: { string_id: \"cfr-doorhanger-send-tab-body\" },\n      icon: \"chrome://branding/content/icon64.png\",\n      buttons: {\n        primary: {\n          label: { string_id: \"cfr-doorhanger-send-tab-ok-button\" },\n          action: {\n            type: \"HIGHLIGHT_FEATURE\",\n            data: { args: \"pageAction-sendToDevice\" },\n          },\n        },\n        secondary: [\n          {\n            label: { string_id: \"cfr-doorhanger-extension-cancel-button\" },\n            action: { type: \"CANCEL\" },\n          },\n          {\n            label: {\n              string_id: \"cfr-doorhanger-extension-never-show-recommendation\",\n            },\n          },\n          {\n            label: {\n              string_id: \"cfr-doorhanger-extension-manage-settings-button\",\n            },\n            action: {\n              type: \"OPEN_PREFERENCES_PAGE\",\n              data: { category: \"general-cfrfeatures\" },\n            },\n          },\n        ],\n      },\n    },\n    targeting: \"true\",\n    trigger: {\n      // Match any URL that has a Reader Mode icon\n      id: \"openArticleURL\",\n      patterns: [\"*://*/*\"],\n    },\n  },\n  {\n    id: \"SEND_RECIPE_TAB_CFR\",\n    template: \"cfr_doorhanger\",\n    // Higher priority because this has the same targeting rules as\n    // SEND_TAB_CFR but is more specific\n    priority: 1,\n    content: {\n      layout: \"icon_and_message\",\n      category: \"cfrFeatures\",\n      notification_text: { string_id: \"cfr-doorhanger-extension-notification\" },\n      heading_text: { string_id: \"cfr-doorhanger-send-tab-recipe-header\" },\n      info_icon: {\n        label: { string_id: \"cfr-doorhanger-extension-sumo-link\" },\n        sumo_path: \"https://example.com\",\n      },\n      text: { string_id: \"cfr-doorhanger-send-tab-body\" },\n      icon: \"chrome://branding/content/icon64.png\",\n      buttons: {\n        primary: {\n          label: { string_id: \"cfr-doorhanger-send-tab-ok-button\" },\n          action: {\n            type: \"HIGHLIGHT_FEATURE\",\n            data: { args: \"pageAction-sendToDevice\" },\n          },\n        },\n        secondary: [\n          {\n            label: { string_id: \"cfr-doorhanger-extension-cancel-button\" },\n            action: { type: \"CANCEL\" },\n          },\n          {\n            label: {\n              string_id: \"cfr-doorhanger-extension-never-show-recommendation\",\n            },\n          },\n          {\n            label: {\n              string_id: \"cfr-doorhanger-extension-manage-settings-button\",\n            },\n            action: {\n              type: \"OPEN_PREFERENCES_PAGE\",\n              data: { category: \"general-cfrfeatures\" },\n            },\n          },\n        ],\n      },\n    },\n    targeting: \"true\",\n    trigger: {\n      id: \"openArticleURL\",\n      params: [\"www.allrecipes.com\", \"allrecipes.com\"],\n    },\n  },\n  {\n    id: \"PERSONALIZED_CFR_MESSAGE\",\n    template: \"cfr_doorhanger\",\n    content: {\n      layout: \"icon_and_message\",\n      category: \"cfrFeatures\",\n      notification_text: \"Personalized CFR Recommendation\",\n      heading_text: { string_id: \"cfr-doorhanger-firefox-send-header\" },\n      info_icon: {\n        label: { string_id: \"cfr-doorhanger-extension-sumo-link\" },\n        sumo_path: \"https://example.com\",\n      },\n      text: { string_id: \"cfr-doorhanger-firefox-send-body\" },\n      icon: \"chrome://branding/content/icon64.png\",\n      buttons: {\n        primary: {\n          label: { string_id: \"cfr-doorhanger-firefox-send-ok-button\" },\n          action: {\n            type: \"OPEN_URL\",\n            data: {\n              args:\n                \"https://send.firefox.com/login/?utm_source=activity-stream&entrypoint=activity-stream-cfr-pdf\",\n              where: \"tabshifted\",\n            },\n          },\n        },\n        secondary: [\n          {\n            label: { string_id: \"cfr-doorhanger-extension-cancel-button\" },\n            action: { type: \"CANCEL\" },\n          },\n          {\n            label: {\n              string_id: \"cfr-doorhanger-extension-never-show-recommendation\",\n            },\n          },\n          {\n            label: {\n              string_id: \"cfr-doorhanger-extension-manage-settings-button\",\n            },\n            action: {\n              type: \"OPEN_PREFERENCES_PAGE\",\n              data: { category: \"general-cfrfeatures\" },\n            },\n          },\n        ],\n      },\n    },\n    targeting: \"scores.PERSONALIZED_CFR_MESSAGE.score > scoreThreshold\",\n    trigger: {\n      id: \"openURL\",\n      patterns: [\"*://*/*.pdf\"],\n    },\n  },\n];\n\nconst PanelTestProvider = {\n  getMessages() {\n    return MESSAGES().map(message => ({\n      ...message,\n      targeting: `providerCohorts.panel_local_testing == \"SHOW_TEST\"`,\n    }));\n  },\n};\nthis.PanelTestProvider = PanelTestProvider;\n\nconst EXPORTED_SYMBOLS = [\"PanelTestProvider\"];\n"
  },
  {
    "path": "lib/PersistentCache.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\"use strict\";\n\nconst { XPCOMUtils } = ChromeUtils.import(\n  \"resource://gre/modules/XPCOMUtils.jsm\"\n);\n\nChromeUtils.defineModuleGetter(this, \"OS\", \"resource://gre/modules/osfile.jsm\");\nXPCOMUtils.defineLazyGlobalGetters(this, [\"fetch\"]);\n\n/**\n * A file (disk) based persistent cache of a JSON serializable object.\n */\nthis.PersistentCache = class PersistentCache {\n  /**\n   * Create a cache object based on a name.\n   *\n   * @param {string} name Name of the cache. It will be used to create the filename.\n   * @param {boolean} preload (optional). Whether the cache should be preloaded from file. Defaults to false.\n   */\n  constructor(name, preload = false) {\n    this.name = name;\n    this._filename = `activity-stream.${name}.json`;\n    if (preload) {\n      this._load();\n    }\n  }\n\n  /**\n   * Set a value to be cached with the specified key.\n   *\n   * @param {string} key The cache key.\n   * @param {object} value The data to be cached.\n   */\n  async set(key, value) {\n    const data = await this._load();\n    data[key] = value;\n    await this._persist(data);\n  }\n\n  /**\n   * Get a value from the cache.\n   *\n   * @param {string} key (optional) The cache key. If not provided, we return the full cache.\n   * @returns {object} The cached data.\n   */\n  async get(key) {\n    const data = await this._load();\n    return key ? data[key] : data;\n  }\n\n  /**\n   * Load the cache into memory if it isn't already.\n   */\n  _load() {\n    return (\n      this._cache ||\n      // eslint-disable-next-line no-async-promise-executor\n      (this._cache = new Promise(async (resolve, reject) => {\n        let filepath;\n        try {\n          filepath = OS.Path.join(\n            OS.Constants.Path.localProfileDir,\n            this._filename\n          );\n        } catch (error) {\n          reject(error);\n          return;\n        }\n\n        let file;\n        try {\n          file = await fetch(`file://${filepath}`);\n        } catch (error) {} // Cache file doesn't exist yet.\n\n        let data = {};\n        if (file) {\n          try {\n            data = await file.json();\n          } catch (error) {\n            Cu.reportError(\n              `Failed to parse ${this._filename}: ${error.message}`\n            );\n          }\n        }\n\n        resolve(data);\n      }))\n    );\n  }\n\n  /**\n   * Persist the cache to file.\n   */\n  _persist(data) {\n    const filepath = OS.Path.join(\n      OS.Constants.Path.localProfileDir,\n      this._filename\n    );\n    return OS.File.writeAtomic(filepath, JSON.stringify(data), {\n      tmpPath: `${filepath}.tmp`,\n    });\n  }\n};\n\nconst EXPORTED_SYMBOLS = [\"PersistentCache\"];\n"
  },
  {
    "path": "lib/PersonalityProvider.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\"use strict\";\n\nconst { RemoteSettings } = ChromeUtils.import(\n  \"resource://services-settings/remote-settings.js\"\n);\n\nconst { actionCreators: ac } = ChromeUtils.import(\n  \"resource://activity-stream/common/Actions.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"perfService\",\n  \"resource://activity-stream/common/PerfService.jsm\"\n);\n\nconst { NaiveBayesTextTagger } = ChromeUtils.import(\n  \"resource://activity-stream/lib/NaiveBayesTextTagger.jsm\"\n);\nconst { NmfTextTagger } = ChromeUtils.import(\n  \"resource://activity-stream/lib/NmfTextTagger.jsm\"\n);\nconst { RecipeExecutor } = ChromeUtils.import(\n  \"resource://activity-stream/lib/RecipeExecutor.jsm\"\n);\n\nChromeUtils.defineModuleGetter(\n  this,\n  \"NewTabUtils\",\n  \"resource://gre/modules/NewTabUtils.jsm\"\n);\n\nconst { Services } = ChromeUtils.import(\"resource://gre/modules/Services.jsm\");\nconst { XPCOMUtils } = ChromeUtils.import(\n  \"resource://gre/modules/XPCOMUtils.jsm\"\n);\nChromeUtils.defineModuleGetter(this, \"OS\", \"resource://gre/modules/osfile.jsm\");\nXPCOMUtils.defineLazyGlobalGetters(this, [\"fetch\"]);\n\nXPCOMUtils.defineLazyGetter(this, \"gTextDecoder\", () => new TextDecoder());\n\nXPCOMUtils.defineLazyGetter(this, \"baseAttachmentsURL\", async () => {\n  const server = Services.prefs.getCharPref(\"services.settings.server\");\n  const serverInfo = await (await fetch(`${server}/`, {\n    credentials: \"omit\",\n  })).json();\n  const {\n    capabilities: {\n      attachments: { base_url },\n    },\n  } = serverInfo;\n  return base_url;\n});\n\nconst PERSONALITY_PROVIDER_DIR = OS.Path.join(\n  OS.Constants.Path.localProfileDir,\n  \"personality-provider\"\n);\nconst RECIPE_NAME = \"personality-provider-recipe\";\nconst MODELS_NAME = \"personality-provider-models\";\n\nfunction getHash(aStr) {\n  // return the two-digit hexadecimal code for a byte\n  let toHexString = charCode => `0${charCode.toString(16)}`.slice(-2);\n  let hasher = Cc[\"@mozilla.org/security/hash;1\"].createInstance(\n    Ci.nsICryptoHash\n  );\n  hasher.init(Ci.nsICryptoHash.SHA256);\n  let stringStream = Cc[\"@mozilla.org/io/string-input-stream;1\"].createInstance(\n    Ci.nsIStringInputStream\n  );\n  stringStream.data = aStr;\n  hasher.updateFromStream(stringStream, -1);\n\n  // convert the binary hash data to a hex string.\n  let binary = hasher.finish(false);\n  return Array.from(binary, (c, i) => toHexString(binary.charCodeAt(i)))\n    .join(\"\")\n    .toLowerCase();\n}\n\n/**\n * V2 provider builds and ranks an interest profile (also called an “interest vector”) off the browse history.\n * This allows Firefox to classify pages into topics, by examining the text found on the page.\n * It does this by looking at the history text content, title, and description.\n */\nthis.PersonalityProvider = class PersonalityProvider {\n  constructor(\n    timeSegments,\n    parameterSets,\n    maxHistoryQueryResults,\n    version,\n    scores,\n    v2Params\n  ) {\n    this.v2Params = v2Params || {};\n    this.dispatch = this.v2Params.dispatch || (() => {});\n    this.modelKeys = this.v2Params.modelKeys;\n    this.timeSegments = timeSegments;\n    this.parameterSets = parameterSets;\n    this.maxHistoryQueryResults = maxHistoryQueryResults;\n    this.version = version;\n    this.scores = scores || {};\n    this.interestConfig = this.scores.interestConfig;\n    this.interestVector = this.scores.interestVector;\n    this.onSync = this.onSync.bind(this);\n    this.setupSyncAttachment(RECIPE_NAME);\n    this.setupSyncAttachment(MODELS_NAME);\n  }\n\n  async onSync(event) {\n    const {\n      data: { created, updated, deleted },\n    } = event;\n\n    // Remove every removed attachment.\n    const toRemove = deleted.concat(updated.map(u => u.old));\n    await Promise.all(toRemove.map(record => this.deleteAttachment(record)));\n\n    // Download every new/updated attachment.\n    const toDownload = created.concat(updated.map(u => u.new));\n    await Promise.all(\n      toDownload.map(record => this.maybeDownloadAttachment(record))\n    );\n  }\n\n  setupSyncAttachment(collection) {\n    RemoteSettings(collection).on(\"sync\", this.onSync);\n  }\n\n  /**\n   * Downloads the attachment to disk assuming the dir already exists\n   * and any existing files matching the filename are clobbered.\n   */\n  async _downloadAttachment(record) {\n    const {\n      attachment: { location, filename },\n    } = record;\n    const remoteFilePath = (await baseAttachmentsURL) + location;\n    const localFilePath = OS.Path.join(PERSONALITY_PROVIDER_DIR, filename);\n    const headers = new Headers();\n    headers.set(\"Accept-Encoding\", \"gzip\");\n    const resp = await fetch(remoteFilePath, { headers, credentials: \"omit\" });\n    if (!resp.ok) {\n      Cu.reportError(`Failed to fetch ${remoteFilePath}: ${resp.status}`);\n      return;\n    }\n    const buffer = await resp.arrayBuffer();\n    const bytes = new Uint8Array(buffer);\n    await OS.File.writeAtomic(localFilePath, bytes, {\n      tmpPath: `${localFilePath}.tmp`,\n    });\n  }\n\n  /**\n   * Attempts to download the attachment, but only if it doesn't already exist.\n   */\n  async maybeDownloadAttachment(record, retries = 3) {\n    const {\n      attachment: { filename, hash, size },\n    } = record;\n    await OS.File.makeDir(PERSONALITY_PROVIDER_DIR);\n    const localFilePath = OS.Path.join(PERSONALITY_PROVIDER_DIR, filename);\n\n    let retry = 0;\n    while (\n      retry++ < retries &&\n      (!(await OS.File.exists(localFilePath)) ||\n        (await OS.File.stat(localFilePath)).size !== size ||\n        getHash(await this._getFileStr(localFilePath)) !== hash)\n    ) {\n      await this._downloadAttachment(record);\n    }\n  }\n\n  async deleteAttachment(record) {\n    const {\n      attachment: { filename },\n    } = record;\n    await OS.File.makeDir(PERSONALITY_PROVIDER_DIR);\n    const path = OS.Path.join(PERSONALITY_PROVIDER_DIR, filename);\n\n    await OS.File.remove(path, { ignoreAbsent: true });\n    return OS.File.removeEmptyDir(PERSONALITY_PROVIDER_DIR, {\n      ignoreAbsent: true,\n    });\n  }\n\n  /**\n   * Gets contents of the attachment if it already exists on file,\n   * and if not attempts to download it.\n   */\n  async getAttachment(record) {\n    const {\n      attachment: { filename },\n    } = record;\n    const filepath = OS.Path.join(PERSONALITY_PROVIDER_DIR, filename);\n\n    try {\n      await this.maybeDownloadAttachment(record);\n      return JSON.parse(await this._getFileStr(filepath));\n    } catch (error) {\n      Cu.reportError(`Failed to load ${filepath}: ${error.message}`);\n    }\n    return {};\n  }\n\n  // A helper function to read and decode a file, it isn't a stand alone function.\n  // If you use this, ensure you check the file exists and you have a try catch.\n  async _getFileStr(filepath) {\n    const binaryData = await OS.File.read(filepath);\n    return gTextDecoder.decode(binaryData);\n  }\n\n  async init(callback) {\n    const perfStart = perfService.absNow();\n    this.interestConfig = this.interestConfig || (await this.getRecipe());\n    if (!this.interestConfig) {\n      this.dispatch(\n        ac.PerfEvent({ event: \"PERSONALIZATION_V2_GET_RECIPE_ERROR\" })\n      );\n      return;\n    }\n    this.recipeExecutor = await this.generateRecipeExecutor();\n    if (!this.recipeExecutor) {\n      this.dispatch(\n        ac.PerfEvent({\n          event: \"PERSONALIZATION_V2_GENERATE_RECIPE_EXECUTOR_ERROR\",\n        })\n      );\n      return;\n    }\n    this.interestVector =\n      this.interestVector || (await this.createInterestVector());\n    if (!this.interestVector) {\n      this.dispatch(\n        ac.PerfEvent({\n          event: \"PERSONALIZATION_V2_CREATE_INTEREST_VECTOR_ERROR\",\n        })\n      );\n      return;\n    }\n\n    this.dispatch(\n      ac.PerfEvent({\n        event: \"PERSONALIZATION_V2_TOTAL_DURATION\",\n        value: Math.round(perfService.absNow() - perfStart),\n      })\n    );\n\n    this.initialized = true;\n    if (callback) {\n      callback();\n    }\n  }\n\n  async getFromRemoteSettings(name) {\n    const result = await RemoteSettings(name).get();\n    return Promise.all(\n      result.map(async record => ({\n        ...(await this.getAttachment(record)),\n        recordKey: record.key,\n      }))\n    );\n  }\n\n  /**\n   * Returns a Recipe from remote settings to be consumed by a RecipeExecutor.\n   * A Recipe is a set of instructions on how to processes a RecipeExecutor.\n   */\n  async getRecipe() {\n    if (!this.recipes || !this.recipes.length) {\n      const start = perfService.absNow();\n      this.recipes = await this.getFromRemoteSettings(RECIPE_NAME);\n      this.dispatch(\n        ac.PerfEvent({\n          event: \"PERSONALIZATION_V2_GET_RECIPE_DURATION\",\n          value: Math.round(perfService.absNow() - start),\n        })\n      );\n    }\n    return this.recipes[0];\n  }\n\n  /**\n   * Returns a Recipe Executor.\n   * A Recipe Executor is a set of actions that can be consumed by a Recipe.\n   * The Recipe determines the order and specifics of which the actions are called.\n   */\n  async generateRecipeExecutor() {\n    if (!this.taggers) {\n      const startTaggers = perfService.absNow();\n      let nbTaggers = [];\n      let nmfTaggers = {};\n      const models = await this.getFromRemoteSettings(MODELS_NAME);\n\n      if (models.length === 0) {\n        return null;\n      }\n\n      for (let model of models) {\n        if (!this.modelKeys.includes(model.recordKey)) {\n          continue;\n        }\n        if (model.model_type === \"nb\") {\n          nbTaggers.push(new NaiveBayesTextTagger(model));\n        } else if (model.model_type === \"nmf\") {\n          nmfTaggers[model.parent_tag] = new NmfTextTagger(model);\n        }\n      }\n      this.dispatch(\n        ac.PerfEvent({\n          event: \"PERSONALIZATION_V2_TAGGERS_DURATION\",\n          value: Math.round(perfService.absNow() - startTaggers),\n        })\n      );\n      this.taggers = { nbTaggers, nmfTaggers };\n    }\n    const startRecipeExecutor = perfService.absNow();\n    const recipeExecutor = new RecipeExecutor(\n      this.taggers.nbTaggers,\n      this.taggers.nmfTaggers\n    );\n    this.dispatch(\n      ac.PerfEvent({\n        event: \"PERSONALIZATION_V2_RECIPE_EXECUTOR_DURATION\",\n        value: Math.round(perfService.absNow() - startRecipeExecutor),\n      })\n    );\n    return recipeExecutor;\n  }\n\n  /**\n   * Grabs a slice of browse history for building a interest vector\n   */\n  async fetchHistory(columns, beginTimeSecs, endTimeSecs) {\n    let sql = `SELECT url, title, visit_count, frecency, last_visit_date, description\n    FROM moz_places\n    WHERE last_visit_date >= ${beginTimeSecs * 1000000}\n    AND last_visit_date < ${endTimeSecs * 1000000}`;\n    columns.forEach(requiredColumn => {\n      sql += ` AND IFNULL(${requiredColumn}, '') <> ''`;\n    });\n    sql += \" LIMIT 30000\";\n\n    const { activityStreamProvider } = NewTabUtils;\n    const history = await activityStreamProvider.executePlacesQuery(sql, {\n      columns,\n      params: {},\n    });\n\n    return history;\n  }\n\n  /**\n   * Examines the user's browse history and returns an interest vector that\n   * describes the topics the user frequently browses.\n   */\n  async createInterestVector() {\n    let interestVector = {};\n    let endTimeSecs = new Date().getTime() / 1000;\n    let beginTimeSecs = endTimeSecs - this.interestConfig.history_limit_secs;\n    let history = await this.fetchHistory(\n      this.interestConfig.history_required_fields,\n      beginTimeSecs,\n      endTimeSecs\n    );\n\n    this.dispatch(\n      ac.PerfEvent({\n        event: \"PERSONALIZATION_V2_HISTORY_SIZE\",\n        value: history.length,\n      })\n    );\n\n    const start = perfService.absNow();\n    for (let historyRec of history) {\n      let ivItem = this.recipeExecutor.executeRecipe(\n        historyRec,\n        this.interestConfig.history_item_builder\n      );\n      if (ivItem === null) {\n        continue;\n      }\n      interestVector = this.recipeExecutor.executeCombinerRecipe(\n        interestVector,\n        ivItem,\n        this.interestConfig.interest_combiner\n      );\n      if (interestVector === null) {\n        return null;\n      }\n    }\n\n    const finalResult = this.recipeExecutor.executeRecipe(\n      interestVector,\n      this.interestConfig.interest_finalizer\n    );\n\n    this.dispatch(\n      ac.PerfEvent({\n        event: \"PERSONALIZATION_V2_CREATE_INTEREST_VECTOR_DURATION\",\n        value: Math.round(perfService.absNow() - start),\n      })\n    );\n    return finalResult;\n  }\n\n  /**\n   * Calculates a score of a Pocket item when compared to the user's interest\n   * vector. Returns the score. Higher scores are better. Assumes this.interestVector\n   * is populated.\n   */\n  calculateItemRelevanceScore(pocketItem) {\n    if (!this.initialized) {\n      return pocketItem.item_score || 1;\n    }\n    let scorableItem = this.recipeExecutor.executeRecipe(\n      pocketItem,\n      this.interestConfig.item_to_rank_builder\n    );\n    if (scorableItem === null) {\n      return -1;\n    }\n\n    let rankingVector = JSON.parse(JSON.stringify(this.interestVector));\n\n    Object.keys(scorableItem).forEach(key => {\n      rankingVector[key] = scorableItem[key];\n    });\n\n    rankingVector = this.recipeExecutor.executeRecipe(\n      rankingVector,\n      this.interestConfig.item_ranker\n    );\n\n    if (rankingVector === null) {\n      return -1;\n    }\n    return rankingVector.score;\n  }\n\n  /**\n   * Returns an object holding the settings and affinity scores of this provider instance.\n   */\n  getAffinities() {\n    return {\n      timeSegments: this.timeSegments,\n      parameterSets: this.parameterSets,\n      maxHistoryQueryResults: this.maxHistoryQueryResults,\n      version: this.version,\n      scores: {\n        interestConfig: this.interestConfig,\n        interestVector: this.interestVector,\n        taggers: this.taggers,\n      },\n    };\n  }\n};\n\nconst EXPORTED_SYMBOLS = [\"PersonalityProvider\"];\n"
  },
  {
    "path": "lib/PlacesFeed.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\"use strict\";\n\nconst { Services } = ChromeUtils.import(\"resource://gre/modules/Services.jsm\");\n\nconst { actionCreators: ac, actionTypes: at } = ChromeUtils.import(\n  \"resource://activity-stream/common/Actions.jsm\"\n);\n\nChromeUtils.defineModuleGetter(\n  this,\n  \"NewTabUtils\",\n  \"resource://gre/modules/NewTabUtils.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"PlacesUtils\",\n  \"resource://gre/modules/PlacesUtils.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"PrivateBrowsingUtils\",\n  \"resource://gre/modules/PrivateBrowsingUtils.jsm\"\n);\n\nconst LINK_BLOCKED_EVENT = \"newtab-linkBlocked\";\nconst PLACES_LINKS_CHANGED_DELAY_TIME = 1000; // time in ms to delay timer for places links changed events\n\n/**\n * Observer - a wrapper around history/bookmark observers to add the QueryInterface.\n */\nclass Observer {\n  constructor(dispatch, observerInterface) {\n    this.dispatch = dispatch;\n    this.QueryInterface = ChromeUtils.generateQI([\n      observerInterface,\n      Ci.nsISupportsWeakReference,\n    ]);\n  }\n}\n\n/**\n * HistoryObserver - observes events from PlacesUtils.history\n */\nclass HistoryObserver extends Observer {\n  constructor(dispatch) {\n    super(dispatch, Ci.nsINavHistoryObserver);\n  }\n\n  /**\n   * onDeleteURI - Called when an link is deleted from history.\n   *\n   * @param  {obj} uri        A URI object representing the link's url\n   *         {str} uri.spec   The URI as a string\n   */\n  onDeleteURI(uri) {\n    this.dispatch({ type: at.PLACES_LINKS_CHANGED });\n    this.dispatch({\n      type: at.PLACES_LINK_DELETED,\n      data: { url: uri.spec },\n    });\n  }\n\n  /**\n   * onClearHistory - Called when the user clears their entire history.\n   */\n  onClearHistory() {\n    this.dispatch({ type: at.PLACES_HISTORY_CLEARED });\n  }\n\n  // Empty functions to make xpconnect happy\n  onBeginUpdateBatch() {}\n\n  onEndUpdateBatch() {}\n\n  onTitleChanged() {}\n\n  onFrecencyChanged() {}\n\n  onManyFrecenciesChanged() {}\n\n  onPageChanged() {}\n\n  onDeleteVisits() {}\n}\n\n/**\n * BookmarksObserver - observes events from PlacesUtils.bookmarks\n */\nclass BookmarksObserver extends Observer {\n  constructor(dispatch) {\n    super(dispatch, Ci.nsINavBookmarkObserver);\n    this.skipTags = true;\n  }\n\n  /**\n   * onItemRemoved - Called when a bookmark is removed\n   *\n   * @param  {str} id\n   * @param  {str} folderId\n   * @param  {int} index\n   * @param  {int} type       Indicates if the bookmark is an actual bookmark,\n   *                          a folder, or a separator.\n   * @param  {str} uri\n   * @param  {str} guid      The unique id of the bookmark\n   */\n  // eslint-disable-next-line max-params\n  onItemRemoved(id, folderId, index, type, uri, guid, parentGuid, source) {\n    if (\n      type === PlacesUtils.bookmarks.TYPE_BOOKMARK &&\n      source !== PlacesUtils.bookmarks.SOURCES.IMPORT &&\n      source !== PlacesUtils.bookmarks.SOURCES.RESTORE &&\n      source !== PlacesUtils.bookmarks.SOURCES.RESTORE_ON_STARTUP &&\n      source !== PlacesUtils.bookmarks.SOURCES.SYNC\n    ) {\n      this.dispatch({ type: at.PLACES_LINKS_CHANGED });\n      this.dispatch({\n        type: at.PLACES_BOOKMARK_REMOVED,\n        data: { url: uri.spec, bookmarkGuid: guid },\n      });\n    }\n  }\n\n  // Empty functions to make xpconnect happy\n  onBeginUpdateBatch() {}\n\n  onEndUpdateBatch() {}\n\n  onItemVisited() {}\n\n  onItemMoved() {}\n\n  // Disabled due to performance cost, see Issue 3203 /\n  // https://bugzilla.mozilla.org/show_bug.cgi?id=1392267.\n  onItemChanged() {}\n}\n\n/**\n * PlacesObserver - observes events from PlacesUtils.observers\n */\nclass PlacesObserver extends Observer {\n  constructor(dispatch) {\n    super(dispatch, Ci.nsINavBookmarkObserver);\n    this.handlePlacesEvent = this.handlePlacesEvent.bind(this);\n  }\n\n  handlePlacesEvent(events) {\n    for (let {\n      itemType,\n      source,\n      dateAdded,\n      guid,\n      title,\n      url,\n      isTagging,\n    } of events) {\n      // Skips items that are not bookmarks (like folders), about:* pages or\n      // default bookmarks, added when the profile is created.\n      if (\n        isTagging ||\n        itemType !== PlacesUtils.bookmarks.TYPE_BOOKMARK ||\n        source === PlacesUtils.bookmarks.SOURCES.IMPORT ||\n        source === PlacesUtils.bookmarks.SOURCES.RESTORE ||\n        source === PlacesUtils.bookmarks.SOURCES.RESTORE_ON_STARTUP ||\n        source === PlacesUtils.bookmarks.SOURCES.SYNC ||\n        (!url.startsWith(\"http://\") && !url.startsWith(\"https://\"))\n      ) {\n        return;\n      }\n\n      this.dispatch({ type: at.PLACES_LINKS_CHANGED });\n      this.dispatch({\n        type: at.PLACES_BOOKMARK_ADDED,\n        data: {\n          bookmarkGuid: guid,\n          bookmarkTitle: title,\n          dateAdded: dateAdded * 1000,\n          url,\n        },\n      });\n    }\n  }\n}\n\nclass PlacesFeed {\n  constructor() {\n    this.placesChangedTimer = null;\n    this.customDispatch = this.customDispatch.bind(this);\n    this.historyObserver = new HistoryObserver(this.customDispatch);\n    this.bookmarksObserver = new BookmarksObserver(this.customDispatch);\n    this.placesObserver = new PlacesObserver(this.customDispatch);\n  }\n\n  addObservers() {\n    // NB: Directly get services without importing the *BIG* PlacesUtils module\n    Cc[\"@mozilla.org/browser/nav-history-service;1\"]\n      .getService(Ci.nsINavHistoryService)\n      .addObserver(this.historyObserver, true);\n    Cc[\"@mozilla.org/browser/nav-bookmarks-service;1\"]\n      .getService(Ci.nsINavBookmarksService)\n      .addObserver(this.bookmarksObserver, true);\n    PlacesUtils.observers.addListener(\n      [\"bookmark-added\"],\n      this.placesObserver.handlePlacesEvent\n    );\n\n    Services.obs.addObserver(this, LINK_BLOCKED_EVENT);\n  }\n\n  /**\n   * setTimeout - A custom function that creates an nsITimer that can be cancelled\n   *\n   * @param {func} callback       A function to be executed after the timer expires\n   * @param {int}  delay          The time (in ms) the timer should wait before the function is executed\n   */\n  setTimeout(callback, delay) {\n    let timer = Cc[\"@mozilla.org/timer;1\"].createInstance(Ci.nsITimer);\n    timer.initWithCallback(callback, delay, Ci.nsITimer.TYPE_ONE_SHOT);\n    return timer;\n  }\n\n  customDispatch(action) {\n    // If we are changing many links at once, delay this action and only dispatch\n    // one action at the end\n    if (action.type === at.PLACES_LINKS_CHANGED) {\n      if (this.placesChangedTimer) {\n        this.placesChangedTimer.delay = PLACES_LINKS_CHANGED_DELAY_TIME;\n      } else {\n        this.placesChangedTimer = this.setTimeout(() => {\n          this.placesChangedTimer = null;\n          this.store.dispatch(ac.OnlyToMain(action));\n        }, PLACES_LINKS_CHANGED_DELAY_TIME);\n      }\n    } else {\n      this.store.dispatch(ac.BroadcastToContent(action));\n    }\n  }\n\n  removeObservers() {\n    if (this.placesChangedTimer) {\n      this.placesChangedTimer.cancel();\n      this.placesChangedTimer = null;\n    }\n    PlacesUtils.history.removeObserver(this.historyObserver);\n    PlacesUtils.bookmarks.removeObserver(this.bookmarksObserver);\n    PlacesUtils.observers.removeListener(\n      [\"bookmark-added\"],\n      this.placesObserver.handlePlacesEvent\n    );\n    Services.obs.removeObserver(this, LINK_BLOCKED_EVENT);\n  }\n\n  /**\n   * observe - An observer for the LINK_BLOCKED_EVENT.\n   *           Called when a link is blocked.\n   *           Links can be blocked outside of newtab,\n   *           which is why we need to listen to this\n   *           on such a generic level.\n   *\n   * @param  {null} subject\n   * @param  {str} topic   The name of the event\n   * @param  {str} value   The data associated with the event\n   */\n  observe(subject, topic, value) {\n    if (topic === LINK_BLOCKED_EVENT) {\n      this.store.dispatch(\n        ac.BroadcastToContent({\n          type: at.PLACES_LINK_BLOCKED,\n          data: { url: value },\n        })\n      );\n    }\n  }\n\n  /**\n   * Open a link in a desired destination defaulting to action's event.\n   */\n  openLink(action, where = \"\", isPrivate = false) {\n    const params = {\n      private: isPrivate,\n      triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal(\n        {}\n      ),\n    };\n\n    // Always include the referrer (even for http links) if we have one\n    const { event, referrer, typedBonus } = action.data;\n    if (referrer) {\n      const ReferrerInfo = Components.Constructor(\n        \"@mozilla.org/referrer-info;1\",\n        \"nsIReferrerInfo\",\n        \"init\"\n      );\n      params.referrerInfo = new ReferrerInfo(\n        Ci.nsIReferrerInfo.UNSAFE_URL,\n        true,\n        Services.io.newURI(referrer)\n      );\n    }\n\n    // Pocket gives us a special reader URL to open their stories in\n    const urlToOpen =\n      action.data.type === \"pocket\" ? action.data.open_url : action.data.url;\n\n    // Mark the page as typed for frecency bonus before opening the link\n    if (typedBonus) {\n      PlacesUtils.history.markPageAsTyped(Services.io.newURI(urlToOpen));\n    }\n\n    const win = action._target.browser.ownerGlobal;\n    win.openLinkIn(urlToOpen, where || win.whereToOpenLink(event), params);\n  }\n\n  async saveToPocket(site, browser) {\n    const { url, title } = site;\n    try {\n      let data = await NewTabUtils.activityStreamLinks.addPocketEntry(\n        url,\n        title,\n        browser\n      );\n      if (data) {\n        this.store.dispatch(\n          ac.BroadcastToContent({\n            type: at.PLACES_SAVED_TO_POCKET,\n            data: {\n              url,\n              open_url: data.item.open_url,\n              title,\n              pocket_id: data.item.item_id,\n            },\n          })\n        );\n      }\n    } catch (err) {\n      Cu.reportError(err);\n    }\n  }\n\n  /**\n   * Deletes an item from a user's saved to Pocket feed\n   * @param {int} itemID\n   *  The unique ID given by Pocket for that item; used to look the item up when deleting\n   */\n  async deleteFromPocket(itemID) {\n    try {\n      await NewTabUtils.activityStreamLinks.deletePocketEntry(itemID);\n      this.store.dispatch({ type: at.POCKET_LINK_DELETED_OR_ARCHIVED });\n    } catch (err) {\n      Cu.reportError(err);\n    }\n  }\n\n  /**\n   * Archives an item from a user's saved to Pocket feed\n   * @param {int} itemID\n   *  The unique ID given by Pocket for that item; used to look the item up when archiving\n   */\n  async archiveFromPocket(itemID) {\n    try {\n      await NewTabUtils.activityStreamLinks.archivePocketEntry(itemID);\n      this.store.dispatch({ type: at.POCKET_LINK_DELETED_OR_ARCHIVED });\n    } catch (err) {\n      Cu.reportError(err);\n    }\n  }\n\n  fillSearchTopSiteTerm({ _target, data }) {\n    _target.browser.ownerGlobal.gURLBar.search(`${data.label} `);\n  }\n\n  _getSearchPrefix(isPrivateWindow) {\n    const searchAliases =\n      Services.search[\n        isPrivateWindow ? \"defaultPrivateEngine\" : \"defaultEngine\"\n      ].wrappedJSObject.__internalAliases;\n    if (searchAliases && searchAliases.length) {\n      return `${searchAliases[0]} `;\n    }\n    return \"\";\n  }\n\n  handoffSearchToAwesomebar({ _target, data, meta }) {\n    const searchAlias = this._getSearchPrefix(\n      PrivateBrowsingUtils.isBrowserPrivate(_target.browser)\n    );\n    const urlBar = _target.browser.ownerGlobal.gURLBar;\n    let isFirstChange = true;\n\n    if (!data || !data.text) {\n      urlBar.setHiddenFocus();\n    } else {\n      // Pass the provided text to the awesomebar. Prepend the @engine shortcut.\n      urlBar.search(`${searchAlias}${data.text}`);\n      isFirstChange = false;\n    }\n\n    const checkFirstChange = () => {\n      // Check if this is the first change since we hidden focused. If it is,\n      // remove hidden focus styles, prepend the search alias and hide the\n      // in-content search.\n      if (isFirstChange) {\n        isFirstChange = false;\n        urlBar.removeHiddenFocus();\n        urlBar.search(searchAlias);\n        this.store.dispatch(\n          ac.OnlyToOneContent({ type: at.HIDE_SEARCH }, meta.fromTarget)\n        );\n        urlBar.removeEventListener(\"compositionstart\", checkFirstChange);\n        urlBar.removeEventListener(\"paste\", checkFirstChange);\n      }\n    };\n\n    const onKeydown = ev => {\n      // Check if the keydown will cause a value change.\n      if (ev.key.length === 1 && !ev.altKey && !ev.ctrlKey && !ev.metaKey) {\n        checkFirstChange();\n      }\n      // If the Esc button is pressed, we are done. Show in-content search and cleanup.\n      if (ev.key === \"Escape\") {\n        onDone(); // eslint-disable-line no-use-before-define\n      }\n    };\n\n    const onDone = () => {\n      // We are done. Show in-content search again and cleanup.\n      this.store.dispatch(\n        ac.OnlyToOneContent({ type: at.SHOW_SEARCH }, meta.fromTarget)\n      );\n      urlBar.removeHiddenFocus();\n\n      urlBar.removeEventListener(\"keydown\", onKeydown);\n      urlBar.removeEventListener(\"mousedown\", onDone);\n      urlBar.removeEventListener(\"blur\", onDone);\n      urlBar.removeEventListener(\"compositionstart\", checkFirstChange);\n      urlBar.removeEventListener(\"paste\", checkFirstChange);\n    };\n\n    urlBar.addEventListener(\"keydown\", onKeydown);\n    urlBar.addEventListener(\"mousedown\", onDone);\n    urlBar.addEventListener(\"blur\", onDone);\n    urlBar.addEventListener(\"compositionstart\", checkFirstChange);\n    urlBar.addEventListener(\"paste\", checkFirstChange);\n  }\n\n  onAction(action) {\n    switch (action.type) {\n      case at.INIT:\n        // Briefly avoid loading services for observing for better startup timing\n        Services.tm.dispatchToMainThread(() => this.addObservers());\n        break;\n      case at.UNINIT:\n        this.removeObservers();\n        break;\n      case at.BLOCK_URL: {\n        const { url, pocket_id } = action.data;\n        NewTabUtils.activityStreamLinks.blockURL({ url, pocket_id });\n        break;\n      }\n      case at.BOOKMARK_URL:\n        NewTabUtils.activityStreamLinks.addBookmark(\n          action.data,\n          action._target.browser.ownerGlobal\n        );\n        break;\n      case at.DELETE_BOOKMARK_BY_ID:\n        NewTabUtils.activityStreamLinks.deleteBookmark(action.data);\n        break;\n      case at.DELETE_HISTORY_URL: {\n        const { url, forceBlock, pocket_id } = action.data;\n        NewTabUtils.activityStreamLinks.deleteHistoryEntry(url);\n        if (forceBlock) {\n          NewTabUtils.activityStreamLinks.blockURL({ url, pocket_id });\n        }\n        break;\n      }\n      case at.OPEN_NEW_WINDOW:\n        this.openLink(action, \"window\");\n        break;\n      case at.OPEN_PRIVATE_WINDOW:\n        this.openLink(action, \"window\", true);\n        break;\n      case at.SAVE_TO_POCKET:\n        this.saveToPocket(action.data.site, action._target.browser);\n        break;\n      case at.DELETE_FROM_POCKET:\n        this.deleteFromPocket(action.data.pocket_id);\n        break;\n      case at.ARCHIVE_FROM_POCKET:\n        this.archiveFromPocket(action.data.pocket_id);\n        break;\n      case at.FILL_SEARCH_TERM:\n        this.fillSearchTopSiteTerm(action);\n        break;\n      case at.HANDOFF_SEARCH_TO_AWESOMEBAR:\n        this.handoffSearchToAwesomebar(action);\n        break;\n      case at.OPEN_LINK: {\n        this.openLink(action);\n        break;\n      }\n    }\n  }\n}\n\nthis.PlacesFeed = PlacesFeed;\n\n// Exported for testing only\nPlacesFeed.HistoryObserver = HistoryObserver;\nPlacesFeed.BookmarksObserver = BookmarksObserver;\nPlacesFeed.PlacesObserver = PlacesObserver;\n\nconst EXPORTED_SYMBOLS = [\"PlacesFeed\"];\n"
  },
  {
    "path": "lib/PrefsFeed.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\"use strict\";\n\nconst { actionCreators: ac, actionTypes: at } = ChromeUtils.import(\n  \"resource://activity-stream/common/Actions.jsm\"\n);\nconst { Prefs } = ChromeUtils.import(\n  \"resource://activity-stream/lib/ActivityStreamPrefs.jsm\"\n);\nconst { Services } = ChromeUtils.import(\"resource://gre/modules/Services.jsm\");\n\nChromeUtils.defineModuleGetter(\n  this,\n  \"PrivateBrowsingUtils\",\n  \"resource://gre/modules/PrivateBrowsingUtils.jsm\"\n);\n\nChromeUtils.defineModuleGetter(\n  this,\n  \"AppConstants\",\n  \"resource://gre/modules/AppConstants.jsm\"\n);\n\nthis.PrefsFeed = class PrefsFeed {\n  constructor(prefMap) {\n    this._prefMap = prefMap;\n    this._prefs = new Prefs();\n  }\n\n  onPrefChanged(name, value) {\n    const prefItem = this._prefMap.get(name);\n    if (prefItem) {\n      this.store.dispatch(\n        ac[prefItem.skipBroadcast ? \"OnlyToMain\" : \"BroadcastToContent\"]({\n          type: at.PREF_CHANGED,\n          data: { name, value },\n        })\n      );\n    }\n  }\n\n  init() {\n    this._prefs.observeBranch(this);\n    this._storage = this.store.dbStorage.getDbTable(\"sectionPrefs\");\n\n    // Get the initial value of each activity stream pref\n    const values = {};\n    for (const name of this._prefMap.keys()) {\n      values[name] = this._prefs.get(name);\n    }\n\n    // These are not prefs, but are needed to determine stuff in content that can only be\n    // computed in main process\n    values.isPrivateBrowsingEnabled = PrivateBrowsingUtils.enabled;\n    values.platform = AppConstants.platform;\n\n    // Get the firefox accounts url for links and to send firstrun metrics to.\n    values.fxa_endpoint = Services.prefs.getStringPref(\n      \"browser.newtabpage.activity-stream.fxaccounts.endpoint\",\n      \"https://accounts.firefox.com\"\n    );\n\n    // Get the firefox update channel with values as default, nightly, beta or release\n    values.appUpdateChannel = Services.prefs.getStringPref(\n      \"app.update.channel\",\n      \"\"\n    );\n\n    // Read the pref for search shortcuts top sites experiment from firefox.js and store it\n    // in our interal list of prefs to watch\n    let searchTopSiteExperimentPrefValue = Services.prefs.getBoolPref(\n      \"browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts\"\n    );\n    values[\n      \"improvesearch.topSiteSearchShortcuts\"\n    ] = searchTopSiteExperimentPrefValue;\n    this._prefMap.set(\"improvesearch.topSiteSearchShortcuts\", {\n      value: searchTopSiteExperimentPrefValue,\n    });\n\n    // Read the pref for search hand-off from firefox.js and store it\n    // in our interal list of prefs to watch\n    let handoffToAwesomebarPrefValue = Services.prefs.getBoolPref(\n      \"browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar\"\n    );\n    values[\"improvesearch.handoffToAwesomebar\"] = handoffToAwesomebarPrefValue;\n    this._prefMap.set(\"improvesearch.handoffToAwesomebar\", {\n      value: handoffToAwesomebarPrefValue,\n    });\n\n    let discoveryStreamEnabled = Services.prefs.getBoolPref(\n      \"browser.newtabpage.activity-stream.discoverystream.enabled\",\n      false\n    );\n    let discoveryStreamHardcodedBasicLayout = Services.prefs.getBoolPref(\n      \"browser.newtabpage.activity-stream.discoverystream.hardcoded-basic-layout\",\n      false\n    );\n    let discoveryStreamSpocsEndpoint = Services.prefs.getStringPref(\n      \"browser.newtabpage.activity-stream.discoverystream.spocs-endpoint\",\n      \"\"\n    );\n    let discoveryStreamLangLayoutConfig = Services.prefs.getStringPref(\n      \"browser.newtabpage.activity-stream.discoverystream.lang-layout-config\",\n      \"\"\n    );\n    values[\"discoverystream.enabled\"] = discoveryStreamEnabled;\n    this._prefMap.set(\"discoverystream.enabled\", {\n      value: discoveryStreamEnabled,\n    });\n    values[\n      \"discoverystream.hardcoded-basic-layout\"\n    ] = discoveryStreamHardcodedBasicLayout;\n    this._prefMap.set(\"discoverystream.hardcoded-basic-layout\", {\n      value: discoveryStreamHardcodedBasicLayout,\n    });\n    values[\"discoverystream.spocs-endpoint\"] = discoveryStreamSpocsEndpoint;\n    this._prefMap.set(\"discoverystream.spocs-endpoint\", {\n      value: discoveryStreamSpocsEndpoint,\n    });\n    values[\n      \"discoverystream.lang-layout-config\"\n    ] = discoveryStreamLangLayoutConfig;\n    this._prefMap.set(\"discoverystream.lang-layout-config\", {\n      value: discoveryStreamLangLayoutConfig,\n    });\n\n    // Set the initial state of all prefs in redux\n    this.store.dispatch(\n      ac.BroadcastToContent({ type: at.PREFS_INITIAL_VALUES, data: values })\n    );\n  }\n\n  removeListeners() {\n    this._prefs.ignoreBranch(this);\n  }\n\n  async _setIndexedDBPref(id, value) {\n    const name = id === \"topsites\" ? id : `feeds.section.${id}`;\n    try {\n      await this._storage.set(name, value);\n    } catch (e) {\n      Cu.reportError(\"Could not set section preferences.\");\n    }\n  }\n\n  onAction(action) {\n    switch (action.type) {\n      case at.INIT:\n        this.init();\n        break;\n      case at.UNINIT:\n        this.removeListeners();\n        break;\n      case at.CLEAR_PREF:\n        Services.prefs.clearUserPref(this._prefs._branchStr + action.data.name);\n        break;\n      case at.SET_PREF:\n        this._prefs.set(action.data.name, action.data.value);\n        break;\n      case at.UPDATE_SECTION_PREFS:\n        this._setIndexedDBPref(action.data.id, action.data.value);\n        break;\n    }\n  }\n};\n\nconst EXPORTED_SYMBOLS = [\"PrefsFeed\"];\n"
  },
  {
    "path": "lib/RecipeExecutor.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\"use strict\";\n\nconst { tokenize } = ChromeUtils.import(\n  \"resource://activity-stream/lib/Tokenize.jsm\"\n);\n\n/**\n * RecipeExecutor is the core feature engineering pipeline for the in-browser\n * personalization work. These pipelines are called \"recipes\". A recipe is an\n * array of objects that define a \"step\" in the recipe. A step is simply an\n * object with a field \"function\" that specifies what is being done in the step\n * along with other fields that are semantically defined for that step.\n *\n * There are two types of recipes \"builder\" recipes and \"combiner\" recipes. Builder\n * recipes mutate an object until it matches some set of critera. Combiner\n * recipes take two objects, (a \"left\" and a \"right\"), and specify the steps\n * to merge the right object into the left object.\n *\n * A short nonsense example recipe is:\n * [ {\"function\": \"get_url_domain\", \"path_length\": 1, \"field\": \"url\", \"dest\": \"url_domain\"},\n *   {\"function\": \"nb_tag\", \"fields\": [\"title\", \"description\"]},\n *   {\"function\": \"conditionally_nmf_tag\", \"fields\": [\"title\", \"description\"]} ]\n *\n * Recipes are sandboxed by the fact that the step functions must be explicitly\n * whitelisted. Functions whitelisted for builder recipes are specifed in the\n * RecipeExecutor.ITEM_BUILDER_REGISTRY, while combiner functions are whitelisted\n * in RecipeExecutor.ITEM_COMBINER_REGISTRY .\n */\nthis.RecipeExecutor = class RecipeExecutor {\n  constructor(nbTaggers, nmfTaggers) {\n    this.ITEM_BUILDER_REGISTRY = {\n      nb_tag: this.naiveBayesTag,\n      conditionally_nmf_tag: this.conditionallyNmfTag,\n      accept_item_by_field_value: this.acceptItemByFieldValue,\n      tokenize_url: this.tokenizeUrl,\n      get_url_domain: this.getUrlDomain,\n      tokenize_field: this.tokenizeField,\n      copy_value: this.copyValue,\n      keep_top_k: this.keepTopK,\n      scalar_multiply: this.scalarMultiply,\n      elementwise_multiply: this.elementwiseMultiply,\n      vector_multiply: this.vectorMultiply,\n      scalar_add: this.scalarAdd,\n      vector_add: this.vectorAdd,\n      make_boolean: this.makeBoolean,\n      whitelist_fields: this.whitelistFields,\n      filter_by_value: this.filterByValue,\n      l2_normalize: this.l2Normalize,\n      prob_normalize: this.probNormalize,\n      set_default: this.setDefault,\n      lookup_value: this.lookupValue,\n      copy_to_map: this.copyToMap,\n      scalar_multiply_tag: this.scalarMultiplyTag,\n      apply_softmax_tags: this.applySoftmaxTags,\n    };\n    this.ITEM_COMBINER_REGISTRY = {\n      combiner_add: this.combinerAdd,\n      combiner_max: this.combinerMax,\n      combiner_collect_values: this.combinerCollectValues,\n    };\n    this.nbTaggers = nbTaggers;\n    this.nmfTaggers = nmfTaggers;\n  }\n\n  /**\n   * Determines the type of a field. Valid types are:\n   *   string\n   *   number\n   *   array\n   *   map (strings to anything)\n   */\n  _typeOf(data) {\n    let t = typeof data;\n    if (t === \"object\") {\n      if (data === null) {\n        return \"null\";\n      }\n      if (Array.isArray(data)) {\n        return \"array\";\n      }\n      return \"map\";\n    }\n    return t;\n  }\n\n  /**\n   * Returns a scalar, either because it was a constant, or by\n   * looking it up from the item. Allows for a default value if the lookup\n   * fails.\n   */\n  _lookupScalar(item, k, dfault) {\n    if (this._typeOf(k) === \"number\") {\n      return k;\n    } else if (\n      this._typeOf(k) === \"string\" &&\n      k in item &&\n      this._typeOf(item[k]) === \"number\"\n    ) {\n      return item[k];\n    }\n    return dfault;\n  }\n\n  /**\n   * Simply appends all the strings from a set fields together. If the field\n   * is a list, then the cells of the list are append.\n   */\n  _assembleText(item, fields) {\n    let textArr = [];\n    for (let field of fields) {\n      if (field in item) {\n        let type = this._typeOf(item[field]);\n        if (type === \"string\") {\n          textArr.push(item[field]);\n        } else if (type === \"array\") {\n          for (let ele of item[field]) {\n            textArr.push(String(ele));\n          }\n        } else {\n          textArr.push(String(item[field]));\n        }\n      }\n    }\n    return textArr.join(\" \");\n  }\n\n  /**\n   * Runs the naive bayes text taggers over a set of text fields. Stores the\n   * results in new fields:\n   *  nb_tags:         a map of text strings to probabilites\n   *  nb_tokens:       the tokenized text that was tagged\n   *\n   * Config:\n   *  fields:          an array containing a list of fields to concatenate and tag\n   */\n  naiveBayesTag(item, config) {\n    let text = this._assembleText(item, config.fields);\n    let tokens = tokenize(text);\n    let tags = {};\n    let extended_tags = {};\n\n    for (let nbTagger of this.nbTaggers) {\n      let result = nbTagger.tagTokens(tokens);\n      if (result.label !== null && result.confident) {\n        extended_tags[result.label] = result;\n        tags[result.label] = Math.exp(result.logProb);\n      }\n    }\n    item.nb_tags = tags;\n    item.nb_tags_extended = extended_tags;\n    item.nb_tokens = tokens;\n    return item;\n  }\n\n  /**\n   * Selectively runs NMF text taggers depending on which tags were found\n   * by the naive bayes taggers. Writes the results in into new fields:\n   *  nmf_tags_parent_weights:  map of pareent tags to probabilites of those parent tags\n   *  nmf_tags:                 map of strings to maps of strings to probabilities\n   *  nmf_tags_parent           map of child tags to parent tags\n   *\n   * Config:\n   *  Not configurable\n   */\n  conditionallyNmfTag(item, config) {\n    let nestedNmfTags = {};\n    let parentTags = {};\n    let parentWeights = {};\n\n    if (!(\"nb_tags\" in item) || !(\"nb_tokens\" in item)) {\n      return null;\n    }\n\n    Object.keys(item.nb_tags).forEach(parentTag => {\n      let nmfTagger = this.nmfTaggers[parentTag];\n      if (nmfTagger !== undefined) {\n        nestedNmfTags[parentTag] = {};\n        parentWeights[parentTag] = item.nb_tags[parentTag];\n        let nmfTags = nmfTagger.tagTokens(item.nb_tokens);\n        Object.keys(nmfTags).forEach(nmfTag => {\n          nestedNmfTags[parentTag][nmfTag] = nmfTags[nmfTag];\n          parentTags[nmfTag] = parentTag;\n        });\n      }\n    });\n\n    item.nmf_tags = nestedNmfTags;\n    item.nmf_tags_parent = parentTags;\n    item.nmf_tags_parent_weights = parentWeights;\n\n    return item;\n  }\n\n  /**\n   * Checks a field's value against another value (either from another field\n   * or a constant). If the test passes, then the item is emitted, otherwise\n   * the pipeline is aborted.\n   *\n   * Config:\n   *  field      Field to read the value to test. Left side of operator.\n   *  op         one of ==, !=, <, <=, >, >=\n   *  rhsValue   Constant value to compare against. Right side of operator.\n   *  rhsField   Field to read value to compare against. Right side of operator.\n   *\n   * NOTE: rhsValue takes precidence over rhsField.\n   */\n  acceptItemByFieldValue(item, config) {\n    if (!(config.field in item)) {\n      return null;\n    }\n    let rhs = null;\n    if (\"rhsValue\" in config) {\n      rhs = config.rhsValue;\n    } else if (\"rhsField\" in config && config.rhsField in item) {\n      rhs = item[config.rhsField];\n    }\n    if (rhs === null) {\n      return null;\n    }\n\n    if (\n      // eslint-disable-next-line eqeqeq\n      (config.op === \"==\" && item[config.field] == rhs) ||\n      // eslint-disable-next-line eqeqeq\n      (config.op === \"!=\" && item[config.field] != rhs) ||\n      (config.op === \"<\" && item[config.field] < rhs) ||\n      (config.op === \"<=\" && item[config.field] <= rhs) ||\n      (config.op === \">\" && item[config.field] > rhs) ||\n      (config.op === \">=\" && item[config.field] >= rhs)\n    ) {\n      return item;\n    }\n\n    return null;\n  }\n\n  /**\n   * Splits a URL into text-like tokens.\n   *\n   * Config:\n   *  field   Field containing a URL\n   *  dest    Field to write the tokens to as an array of strings\n   *\n   * NOTE: Any initial 'www' on the hostname is removed.\n   */\n  tokenizeUrl(item, config) {\n    if (!(config.field in item)) {\n      return null;\n    }\n\n    let url = new URL(item[config.field]);\n    let domain = url.hostname;\n    if (domain.startsWith(\"www.\")) {\n      domain = domain.substring(4);\n    }\n    let toks = tokenize(domain);\n    let pathToks = tokenize(\n      decodeURIComponent(url.pathname.replace(/\\+/g, \" \"))\n    );\n    for (let tok of pathToks) {\n      toks.push(tok);\n    }\n    for (let pair of url.searchParams.entries()) {\n      let k = tokenize(decodeURIComponent(pair[0].replace(/\\+/g, \" \")));\n      for (let tok of k) {\n        toks.push(tok);\n      }\n      if (pair[1] !== null && pair[1] !== \"\") {\n        let v = tokenize(decodeURIComponent(pair[1].replace(/\\+/g, \" \")));\n        for (let tok of v) {\n          toks.push(tok);\n        }\n      }\n    }\n    item[config.dest] = toks;\n\n    return item;\n  }\n\n  /**\n   * Gets the hostname (minus any initial \"www.\" along with the left most\n   * directories on the path.\n   *\n   * Config:\n   *  field          Field containing the URL\n   *  dest           Field to write the array of strings to\n   *  path_length    OPTIONAL (DEFAULT: 0) Number of leftmost subdirectories to include\n   */\n  getUrlDomain(item, config) {\n    if (!(config.field in item)) {\n      return null;\n    }\n\n    let url = new URL(item[config.field]);\n    let domain = url.hostname.toLocaleLowerCase();\n    if (domain.startsWith(\"www.\")) {\n      domain = domain.substring(4);\n    }\n    item[config.dest] = domain;\n    let pathLength = 0;\n    if (\"path_length\" in config) {\n      pathLength = config.path_length;\n    }\n    if (pathLength > 0) {\n      item[config.dest] += url.pathname\n        .toLocaleLowerCase()\n        .split(\"/\")\n        .slice(0, pathLength + 1)\n        .join(\"/\");\n    }\n\n    return item;\n  }\n\n  /**\n   * Splits a field into tokens.\n   * Config:\n   *  field         Field containing a string to tokenize\n   *  dest          Field to write the array of strings to\n   */\n  tokenizeField(item, config) {\n    if (!(config.field in item)) {\n      return null;\n    }\n\n    item[config.dest] = tokenize(item[config.field]);\n\n    return item;\n  }\n\n  /**\n   * Deep copy from one field to another.\n   * Config:\n   *  src           Field to read from\n   *  dest          Field to write to\n   */\n  copyValue(item, config) {\n    if (!(config.src in item)) {\n      return null;\n    }\n\n    item[config.dest] = JSON.parse(JSON.stringify(item[config.src]));\n\n    return item;\n  }\n\n  /**\n   * Converts a field containing a map of strings to a map of strings\n   * to numbers, to a map of strings to numbers containing at most k elements.\n   * This operation is performed by first, promoting all the subkeys up one\n   * level, and then taking the top (or bottom) k values.\n   *\n   * Config:\n   *  field         Points to a map of strings to a map of strings to numbers\n   *  k             Maximum number of items to keep\n   *  descending    OPTIONAL (DEFAULT: True) Sorts score in descending  order\n   *                  (i.e. keeps maximum)\n   */\n  keepTopK(item, config) {\n    if (!(config.field in item)) {\n      return null;\n    }\n    let k = this._lookupScalar(item, config.k, 1048576);\n    let descending = !(\"descending\" in config) || config.descending !== false;\n\n    // we can't sort by the values in the map, so we have to convert this\n    // to an array, and then sort.\n    let sortable = [];\n    Object.keys(item[config.field]).forEach(outerKey => {\n      let innerType = this._typeOf(item[config.field][outerKey]);\n      if (innerType === \"map\") {\n        Object.keys(item[config.field][outerKey]).forEach(innerKey => {\n          sortable.push({\n            key: innerKey,\n            value: item[config.field][outerKey][innerKey],\n          });\n        });\n      } else {\n        sortable.push({ key: outerKey, value: item[config.field][outerKey] });\n      }\n    });\n\n    sortable.sort((a, b) => {\n      if (descending) {\n        return b.value - a.value;\n      }\n      return a.value - b.value;\n    });\n\n    // now take the top k\n    let newMap = {};\n    let i = 0;\n    for (let pair of sortable) {\n      if (i >= k) {\n        break;\n      }\n      newMap[pair.key] = pair.value;\n      i++;\n    }\n    item[config.field] = newMap;\n\n    return item;\n  }\n\n  /**\n   * Scalar multiplies a vector by some constant\n   *\n   * Config:\n   *  field         Points to:\n   *                   a map of strings to numbers\n   *                   an array of numbers\n   *                   a number\n   *  k             Either a number, or a string. If it's a number then This\n   *                  is the scalar value to multiply by. If it's a string,\n   *                  the value in the pointed to field is used.\n   *  default       OPTIONAL (DEFAULT: 0), If k is a string, and no numeric\n   *                  value is found, then use this value.\n   */\n  scalarMultiply(item, config) {\n    if (!(config.field in item)) {\n      return null;\n    }\n    let k = this._lookupScalar(item, config.k, config.dfault);\n\n    let fieldType = this._typeOf(item[config.field]);\n    if (fieldType === \"number\") {\n      item[config.field] *= k;\n    } else if (fieldType === \"array\") {\n      for (let i = 0; i < item[config.field].length; i++) {\n        item[config.field][i] *= k;\n      }\n    } else if (fieldType === \"map\") {\n      Object.keys(item[config.field]).forEach(key => {\n        item[config.field][key] *= k;\n      });\n    } else {\n      return null;\n    }\n\n    return item;\n  }\n\n  /**\n   * Elementwise multiplies either two maps or two arrays together, storing\n   * the result in left. If left and right are of the same type, results in an\n   * error.\n   *\n   * Maps are special case. For maps the left must be a nested map such as:\n   * { k1: { k11: 1, k12: 2}, k2: { k21: 3, k22: 4 } } and right needs to be\n   * simple map such as: { k1: 5, k2: 6} .  The operation is then to mulitply\n   * every value of every right key, to every value every subkey where the\n   * parent keys match. Using the previous examples, the result would be:\n   * { k1: { k11: 5, k12: 10 }, k2: { k21: 18, k22: 24 } } .\n   *\n   * Config:\n   *  left\n   *  right\n   */\n  elementwiseMultiply(item, config) {\n    if (!(config.left in item) || !(config.right in item)) {\n      return null;\n    }\n    let leftType = this._typeOf(item[config.left]);\n    if (leftType !== this._typeOf(item[config.right])) {\n      return null;\n    }\n    if (leftType === \"array\") {\n      if (item[config.left].length !== item[config.right].length) {\n        return null;\n      }\n      for (let i = 0; i < item[config.left].length; i++) {\n        item[config.left][i] *= item[config.right][i];\n      }\n    } else if (leftType === \"map\") {\n      Object.keys(item[config.left]).forEach(outerKey => {\n        let r = 0.0;\n        if (outerKey in item[config.right]) {\n          r = item[config.right][outerKey];\n        }\n        Object.keys(item[config.left][outerKey]).forEach(innerKey => {\n          item[config.left][outerKey][innerKey] *= r;\n        });\n      });\n    } else if (leftType === \"number\") {\n      item[config.left] *= item[config.right];\n    } else {\n      return null;\n    }\n\n    return item;\n  }\n\n  /**\n   * Vector multiplies (i.e. dot products) two vectors and stores the result in\n   * third field. Both vectors must either by maps, or arrays of numbers with\n   * the same length.\n   *\n   * Config:\n   *   left       A field pointing to either a map of strings to numbers,\n   *                or an array of numbers\n   *   right      A field pointing to either a map of strings to numbers,\n   *                or an array of numbers\n   *   dest       The field to store the dot product.\n   */\n  vectorMultiply(item, config) {\n    if (!(config.left in item) || !(config.right in item)) {\n      return null;\n    }\n\n    let leftType = this._typeOf(item[config.left]);\n    if (leftType !== this._typeOf(item[config.right])) {\n      return null;\n    }\n\n    let destVal = 0.0;\n    if (leftType === \"array\") {\n      if (item[config.left].length !== item[config.right].length) {\n        return null;\n      }\n      for (let i = 0; i < item[config.left].length; i++) {\n        destVal += item[config.left][i] * item[config.right][i];\n      }\n    } else if (leftType === \"map\") {\n      Object.keys(item[config.left]).forEach(key => {\n        if (key in item[config.right]) {\n          destVal += item[config.left][key] * item[config.right][key];\n        }\n      });\n    } else {\n      return null;\n    }\n\n    item[config.dest] = destVal;\n    return item;\n  }\n\n  /**\n   * Adds a constant value to all elements in the field. Mathematically,\n   * this is the same as taking a 1-vector, scalar multiplying it by k,\n   * and then vector adding it to a field.\n   *\n   * Config:\n   *  field     A field pointing to either a map of strings to numbers,\n   *                  or an array of numbers\n   *  k             Either a number, or a string. If it's a number then This\n   *                  is the scalar value to multiply by. If it's a string,\n   *                  the value in the pointed to field is used.\n   *  default       OPTIONAL (DEFAULT: 0), If k is a string, and no numeric\n   *                  value is found, then use this value.\n   */\n  scalarAdd(item, config) {\n    let k = this._lookupScalar(item, config.k, config.dfault);\n    if (!(config.field in item)) {\n      return null;\n    }\n\n    let fieldType = this._typeOf(item[config.field]);\n    if (fieldType === \"array\") {\n      for (let i = 0; i < item[config.field].length; i++) {\n        item[config.field][i] += k;\n      }\n    } else if (fieldType === \"map\") {\n      Object.keys(item[config.field]).forEach(key => {\n        item[config.field][key] += k;\n      });\n    } else if (fieldType === \"number\") {\n      item[config.field] += k;\n    } else {\n      return null;\n    }\n\n    return item;\n  }\n\n  /**\n   * Adds two vectors together and stores the result in left.\n   *\n   * Config:\n   *  left      A field pointing to either a map of strings to numbers,\n   *                  or an array of numbers\n   *  right     A field pointing to either a map of strings to numbers,\n   *                  or an array of numbers\n   */\n  vectorAdd(item, config) {\n    if (!(config.left in item)) {\n      return this.copyValue(item, { src: config.right, dest: config.left });\n    }\n    if (!(config.right in item)) {\n      return null;\n    }\n\n    let leftType = this._typeOf(item[config.left]);\n    if (leftType !== this._typeOf(item[config.right])) {\n      return null;\n    }\n    if (leftType === \"array\") {\n      if (item[config.left].length !== item[config.right].length) {\n        return null;\n      }\n      for (let i = 0; i < item[config.left].length; i++) {\n        item[config.left][i] += item[config.right][i];\n      }\n      return item;\n    } else if (leftType === \"map\") {\n      Object.keys(item[config.right]).forEach(key => {\n        let v = 0;\n        if (key in item[config.left]) {\n          v = item[config.left][key];\n        }\n        item[config.left][key] = v + item[config.right][key];\n      });\n      return item;\n    }\n\n    return null;\n  }\n\n  /**\n   * Converts a vector from real values to boolean integers. (i.e. either 1/0\n   * or 1/-1).\n   *\n   * Config:\n   *   field            Field containing either a mpa of strings to numbers or\n   *                      an array of numbers to  convert.\n   *   threshold        OPTIONAL (DEFAULT: 0) Values above this will be replaced\n   *                      with 1.0. Those below will be converted to 0.\n   *   keep_negative    OPTIONAL (DEFAULT: False) If true, values below the\n   *                      threshold will be converted to -1 instead of 0.\n   */\n  makeBoolean(item, config) {\n    if (!(config.field in item)) {\n      return null;\n    }\n    let threshold = this._lookupScalar(item, config.threshold, 0.0);\n    let type = this._typeOf(item[config.field]);\n    if (type === \"array\") {\n      for (let i = 0; i < item[config.field].length; i++) {\n        if (item[config.field][i] > threshold) {\n          item[config.field][i] = 1.0;\n        } else if (config.keep_negative) {\n          item[config.field][i] = -1.0;\n        } else {\n          item[config.field][i] = 0.0;\n        }\n      }\n    } else if (type === \"map\") {\n      Object.keys(item[config.field]).forEach(key => {\n        let value = item[config.field][key];\n        if (value > threshold) {\n          item[config.field][key] = 1.0;\n        } else if (config.keep_negative) {\n          item[config.field][key] = -1.0;\n        } else {\n          item[config.field][key] = 0.0;\n        }\n      });\n    } else if (type === \"number\") {\n      let value = item[config.field];\n      if (value > threshold) {\n        item[config.field] = 1.0;\n      } else if (config.keep_negative) {\n        item[config.field] = -1.0;\n      } else {\n        item[config.field] = 0.0;\n      }\n    } else {\n      return null;\n    }\n\n    return item;\n  }\n\n  /**\n   * Removes all keys from the item except for the ones specified.\n   *\n   * fields           An array of strings indicating the fields to keep\n   */\n  whitelistFields(item, config) {\n    let newItem = {};\n    for (let ele of config.fields) {\n      if (ele in item) {\n        newItem[ele] = item[ele];\n      }\n    }\n    return newItem;\n  }\n\n  /**\n   * Removes all keys whose value does not exceed some threshold.\n   *\n   * Config:\n   *   field         Points to a map of strings to numbers\n   *   threshold     Values must exceed this value, otherwise they are removed.\n   */\n  filterByValue(item, config) {\n    if (!(config.field in item)) {\n      return null;\n    }\n    let threshold = this._lookupScalar(item, config.threshold, 0.0);\n    let filtered = {};\n    Object.keys(item[config.field]).forEach(key => {\n      let value = item[config.field][key];\n      if (value > threshold) {\n        filtered[key] = value;\n      }\n    });\n    item[config.field] = filtered;\n\n    return item;\n  }\n\n  /**\n   * Rewrites a field so that its values are now L2 normed.\n   *\n   * Config:\n   *  field         Points to a map of strings to numbers, or an array of numbers\n   */\n  l2Normalize(item, config) {\n    if (!(config.field in item)) {\n      return null;\n    }\n    let data = item[config.field];\n    let type = this._typeOf(data);\n    if (type === \"array\") {\n      let norm = 0.0;\n      for (let datum of data) {\n        norm += datum * datum;\n      }\n      norm = Math.sqrt(norm);\n      if (norm !== 0) {\n        for (let i = 0; i < data.length; i++) {\n          data[i] /= norm;\n        }\n      }\n    } else if (type === \"map\") {\n      let norm = 0.0;\n      Object.keys(data).forEach(key => {\n        norm += data[key] * data[key];\n      });\n      norm = Math.sqrt(norm);\n      if (norm !== 0) {\n        Object.keys(data).forEach(key => {\n          data[key] /= norm;\n        });\n      }\n    } else {\n      return null;\n    }\n\n    item[config.field] = data;\n\n    return item;\n  }\n\n  /**\n   * Rewrites a field so that all of its values sum to 1.0\n   *\n   * Config:\n   *  field         Points to a map of strings to numbers, or an array of numbers\n   */\n  probNormalize(item, config) {\n    if (!(config.field in item)) {\n      return null;\n    }\n    let data = item[config.field];\n    let type = this._typeOf(data);\n    if (type === \"array\") {\n      let norm = 0.0;\n      for (let datum of data) {\n        norm += datum;\n      }\n      if (norm !== 0) {\n        for (let i = 0; i < data.length; i++) {\n          data[i] /= norm;\n        }\n      }\n    } else if (type === \"map\") {\n      let norm = 0.0;\n      Object.keys(item[config.field]).forEach(key => {\n        norm += item[config.field][key];\n      });\n      if (norm !== 0) {\n        Object.keys(item[config.field]).forEach(key => {\n          item[config.field][key] /= norm;\n        });\n      }\n    } else {\n      return null;\n    }\n\n    return item;\n  }\n\n  /**\n   * Stores a value, if it is not already present\n   *\n   * Config:\n   *  field             field to write to if it is missing\n   *  value             value to store in that field\n   */\n  setDefault(item, config) {\n    let val = this._lookupScalar(item, config.value, config.value);\n    if (!(config.field in item)) {\n      item[config.field] = val;\n    }\n\n    return item;\n  }\n\n  /**\n   * Selctively promotes an value from an inner map up to the outer map\n   *\n   * Config:\n   *  haystack            Points to a map of strings to values\n   *  needle              Key inside the map we should promote up\n   *  dest                Where we should write the value of haystack[needle]\n   */\n  lookupValue(item, config) {\n    if (config.haystack in item && config.needle in item[config.haystack]) {\n      item[config.dest] = item[config.haystack][config.needle];\n    }\n\n    return item;\n  }\n\n  /**\n   * Demotes a field into a map\n   *\n   * Config:\n   *  src               Field to copy\n   *  dest_map          Points to a map\n   *  dest_key          Key inside dest_map to copy src to\n   */\n  copyToMap(item, config) {\n    if (config.src in item) {\n      if (!(config.dest_map in item)) {\n        item[config.dest_map] = {};\n      }\n      item[config.dest_map][config.dest_key] = item[config.src];\n    }\n\n    return item;\n  }\n\n  /**\n   * Config:\n   *  field             Points to a string to number map\n   *  k                 Scalar to multiply the values by\n   *  log_scale         Boolean, if true, then the values will be transformed\n   *                      by a logrithm prior to multiplications\n   */\n  scalarMultiplyTag(item, config) {\n    let EPSILON = 0.000001;\n    if (!(config.field in item)) {\n      return null;\n    }\n    let k = this._lookupScalar(item, config.k, 1);\n    let type = this._typeOf(item[config.field]);\n    if (type === \"map\") {\n      Object.keys(item[config.field]).forEach(parentKey => {\n        Object.keys(item[config.field][parentKey]).forEach(key => {\n          let v = item[config.field][parentKey][key];\n          if (config.log_scale) {\n            v = Math.log(v + EPSILON);\n          }\n          item[config.field][parentKey][key] = v * k;\n        });\n      });\n    } else {\n      return null;\n    }\n\n    return item;\n  }\n\n  /**\n   * Independently applies softmax across all subtags.\n   *\n   * Config:\n   *   field        Points to a map of strings with values being another map of strings\n   */\n  applySoftmaxTags(item, config) {\n    let type = this._typeOf(item[config.field]);\n    if (type !== \"map\") {\n      return null;\n    }\n\n    let abort = false;\n    let softmaxSum = {};\n    Object.keys(item[config.field]).forEach(tag => {\n      if (this._typeOf(item[config.field][tag]) !== \"map\") {\n        abort = true;\n        return;\n      }\n      if (abort) {\n        return;\n      }\n      softmaxSum[tag] = 0;\n      Object.keys(item[config.field][tag]).forEach(subtag => {\n        if (this._typeOf(item[config.field][tag][subtag]) !== \"number\") {\n          abort = true;\n          return;\n        }\n        let score = item[config.field][tag][subtag];\n        softmaxSum[tag] += Math.exp(score);\n      });\n    });\n    if (abort) {\n      return null;\n    }\n\n    Object.keys(item[config.field]).forEach(tag => {\n      Object.keys(item[config.field][tag]).forEach(subtag => {\n        item[config.field][tag][subtag] =\n          Math.exp(item[config.field][tag][subtag]) / softmaxSum[tag];\n      });\n    });\n\n    return item;\n  }\n\n  /**\n   * Vector adds a field and stores the result in left.\n   *\n   * Config:\n   *   field              The field to vector add\n   */\n  combinerAdd(left, right, config) {\n    if (!(config.field in right)) {\n      return left;\n    }\n    let type = this._typeOf(right[config.field]);\n    if (!(config.field in left)) {\n      if (type === \"map\") {\n        left[config.field] = {};\n      } else if (type === \"array\") {\n        left[config.field] = [];\n      } else if (type === \"number\") {\n        left[config.field] = 0;\n      } else {\n        return null;\n      }\n    }\n    if (type !== this._typeOf(left[config.field])) {\n      return null;\n    }\n    if (type === \"map\") {\n      Object.keys(right[config.field]).forEach(key => {\n        if (!(key in left[config.field])) {\n          left[config.field][key] = 0;\n        }\n        left[config.field][key] += right[config.field][key];\n      });\n    } else if (type === \"array\") {\n      for (let i = 0; i < right[config.field].length; i++) {\n        if (i < left[config.field].length) {\n          left[config.field][i] += right[config.field][i];\n        } else {\n          left[config.field].push(right[config.field][i]);\n        }\n      }\n    } else if (type === \"number\") {\n      left[config.field] += right[config.field];\n    } else {\n      return null;\n    }\n\n    return left;\n  }\n\n  /**\n   * Stores the maximum value of the field in left.\n   *\n   * Config:\n   *   field              The field to vector add\n   */\n  combinerMax(left, right, config) {\n    if (!(config.field in right)) {\n      return left;\n    }\n    let type = this._typeOf(right[config.field]);\n    if (!(config.field in left)) {\n      if (type === \"map\") {\n        left[config.field] = {};\n      } else if (type === \"array\") {\n        left[config.field] = [];\n      } else if (type === \"number\") {\n        left[config.field] = 0;\n      } else {\n        return null;\n      }\n    }\n    if (type !== this._typeOf(left[config.field])) {\n      return null;\n    }\n    if (type === \"map\") {\n      Object.keys(right[config.field]).forEach(key => {\n        if (\n          !(key in left[config.field]) ||\n          right[config.field][key] > left[config.field][key]\n        ) {\n          left[config.field][key] = right[config.field][key];\n        }\n      });\n    } else if (type === \"array\") {\n      for (let i = 0; i < right[config.field].length; i++) {\n        if (i < left[config.field].length) {\n          if (left[config.field][i] < right[config.field][i]) {\n            left[config.field][i] = right[config.field][i];\n          }\n        } else {\n          left[config.field].push(right[config.field][i]);\n        }\n      }\n    } else if (type === \"number\") {\n      if (left[config.field] < right[config.field]) {\n        left[config.field] = right[config.field];\n      }\n    } else {\n      return null;\n    }\n\n    return left;\n  }\n\n  /**\n   * Associates a value in right with another value in right. This association\n   * is then stored in a map in left.\n   *\n   *     For example: If a sequence of rights is:\n   *     { 'tags': {}, 'url_domain': 'maseratiusa.com/maserati', 'time': 41 }\n   *     { 'tags': {}, 'url_domain': 'mbusa.com/mercedes',       'time': 21 }\n   *     { 'tags': {}, 'url_domain': 'maseratiusa.com/maserati', 'time': 34 }\n   *\n   *     Then assuming a 'sum' operation, left can build a map that would look like:\n   *     {\n   *         'maseratiusa.com/maserati': 75,\n   *         'mbusa.com/mercedes': 21,\n   *     }\n   *\n   * Fields:\n   *  left_field              field in the left to store / update the map\n   *  right_key_field         Field in the right to use as a key\n   *  right_value_field       Field in the right to use as a value\n   *  operation               One of \"sum\", \"max\", \"overwrite\", \"count\"\n   */\n  combinerCollectValues(left, right, config) {\n    let op;\n    if (config.operation === \"sum\") {\n      op = (a, b) => a + b;\n    } else if (config.operation === \"max\") {\n      op = (a, b) => (a > b ? a : b);\n    } else if (config.operation === \"overwrite\") {\n      op = (a, b) => b;\n    } else if (config.operation === \"count\") {\n      op = (a, b) => a + 1;\n    } else {\n      return null;\n    }\n    if (!(config.left_field in left)) {\n      left[config.left_field] = {};\n    }\n    if (\n      !(config.right_key_field in right) ||\n      !(config.right_value_field in right)\n    ) {\n      return left;\n    }\n\n    let key = right[config.right_key_field];\n    let rightValue = right[config.right_value_field];\n    let leftValue = 0.0;\n    if (key in left[config.left_field]) {\n      leftValue = left[config.left_field][key];\n    }\n\n    left[config.left_field][key] = op(leftValue, rightValue);\n\n    return left;\n  }\n\n  /**\n   * Executes a recipe. Returns an object on success, or null on failure.\n   */\n  executeRecipe(item, recipe) {\n    let newItem = item;\n    for (let step of recipe) {\n      let op = this.ITEM_BUILDER_REGISTRY[step.function];\n      if (op === undefined) {\n        return null;\n      }\n      newItem = op.call(this, newItem, step);\n      if (newItem === null) {\n        break;\n      }\n    }\n    return newItem;\n  }\n\n  /**\n   * Executes a recipe. Returns an object on success, or null on failure.\n   */\n  executeCombinerRecipe(item1, item2, recipe) {\n    let newItem1 = item1;\n    for (let step of recipe) {\n      let op = this.ITEM_COMBINER_REGISTRY[step.function];\n      if (op === undefined) {\n        return null;\n      }\n      newItem1 = op.call(this, newItem1, item2, step);\n      if (newItem1 === null) {\n        break;\n      }\n    }\n\n    return newItem1;\n  }\n};\n\nconst EXPORTED_SYMBOLS = [\"RecipeExecutor\"];\n"
  },
  {
    "path": "lib/RemoteL10n.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\"use strict\";\n\n/**\n * The downloaded Fluent file is located in this sub-directory of the local\n * profile directory.\n */\nconst RS_DOWNLOADED_FILE_SUBDIR = \"settings/main/ms-language-packs\";\nconst USE_REMOTE_L10N_PREF =\n  \"browser.newtabpage.activity-stream.asrouter.useRemoteL10n\";\n\nconst { XPCOMUtils } = ChromeUtils.import(\n  \"resource://gre/modules/XPCOMUtils.jsm\"\n);\n\nXPCOMUtils.defineLazyModuleGetters(this, {\n  L10nRegistry: \"resource://gre/modules/L10nRegistry.jsm\",\n  FileSource: \"resource://gre/modules/L10nRegistry.jsm\",\n  OS: \"resource://gre/modules/osfile.jsm\",\n  Services: \"resource://gre/modules/Services.jsm\",\n});\n\nclass _RemoteL10n {\n  constructor() {\n    this._l10n = null;\n  }\n\n  /**\n   * Creates a new DOMLocalization instance with the Fluent file from Remote Settings.\n   *\n   * Note: it will use the local Fluent file in any of following cases:\n   *   * the remote Fluent file is not available\n   *   * it was told to use the local Fluent file\n   */\n  _createDOML10n() {\n    /* istanbul ignore next */\n    async function* generateBundles(resourceIds) {\n      const appLocale = Services.locale.appLocaleAsBCP47;\n      const appLocales = Services.locale.appLocalesAsBCP47;\n      const l10nFluentDir = OS.Path.join(\n        OS.Constants.Path.localProfileDir,\n        RS_DOWNLOADED_FILE_SUBDIR\n      );\n      const fs = new FileSource(\"cfr\", [appLocale], `file://${l10nFluentDir}/`);\n      // In the case that the Fluent file has not been downloaded from Remote Settings,\n      // `fetchFile` will return `false` and fall back to the packaged Fluent file.\n      const resource = await fs.fetchFile(appLocale, \"asrouter.ftl\");\n      for await (let bundle of L10nRegistry.generateBundles(\n        appLocales.slice(0, 1),\n        resourceIds\n      )) {\n        // Override built-in messages with the resource loaded from remote settings for\n        // the app locale, i.e. the first item of `appLocales`.\n        if (resource) {\n          bundle.addResource(resource, { allowOverrides: true });\n        }\n        yield bundle;\n      }\n      // Now generating bundles for the rest of locales of `appLocales`.\n      yield* L10nRegistry.generateBundles(appLocales.slice(1), resourceIds);\n    }\n\n    return new DOMLocalization(\n      [\n        \"browser/newtab/asrouter.ftl\",\n        \"browser/branding/brandings.ftl\",\n        \"browser/branding/sync-brand.ftl\",\n        \"branding/brand.ftl\",\n      ],\n      Services.prefs.getBoolPref(USE_REMOTE_L10N_PREF, true)\n        ? generateBundles\n        : undefined\n    );\n  }\n\n  get l10n() {\n    if (!this._l10n) {\n      this._l10n = this._createDOML10n();\n    }\n    return this._l10n;\n  }\n\n  reloadL10n() {\n    this._l10n = null;\n  }\n}\n\nthis.RemoteL10n = new _RemoteL10n();\n\nconst EXPORTED_SYMBOLS = [\"RemoteL10n\", \"_RemoteL10n\"];\n"
  },
  {
    "path": "lib/Screenshots.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\"use strict\";\n\nconst EXPORTED_SYMBOLS = [\"Screenshots\"];\n\nCu.importGlobalProperties([\"fetch\"]);\n\nChromeUtils.defineModuleGetter(\n  this,\n  \"BackgroundPageThumbs\",\n  \"resource://gre/modules/BackgroundPageThumbs.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"PageThumbs\",\n  \"resource://gre/modules/PageThumbs.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"PrivateBrowsingUtils\",\n  \"resource://gre/modules/PrivateBrowsingUtils.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"Services\",\n  \"resource://gre/modules/Services.jsm\"\n);\n\nconst GREY_10 = \"#F9F9FA\";\n\nthis.Screenshots = {\n  /**\n   * Get a screenshot / thumbnail for a url. Either returns the disk cached\n   * image or initiates a background request for the url.\n   *\n   * @param url {string} The url to get a thumbnail\n   * @return {Promise} Resolves a custom object or null if failed\n   */\n  async getScreenshotForURL(url) {\n    try {\n      await BackgroundPageThumbs.captureIfMissing(url, {\n        backgroundColor: GREY_10,\n      });\n      const imgPath = PageThumbs.getThumbnailPath(url);\n\n      const filePathResponse = await fetch(`file://${imgPath}`);\n      const fileContents = await filePathResponse.blob();\n\n      // Check if the file is empty, which indicates there isn't actually a\n      // thumbnail, so callers can show a failure state.\n      if (fileContents.size === 0) {\n        return null;\n      }\n\n      return { path: imgPath, data: fileContents };\n    } catch (err) {\n      Cu.reportError(`getScreenshot(${url}) failed: ${err}`);\n    }\n\n    // We must have failed to get the screenshot, so persist the failure by\n    // storing an empty file. Future calls will then skip requesting and return\n    // failure, so do the same thing here. The empty file should not expire with\n    // the usual filtering process to avoid repeated background requests, which\n    // can cause unwanted high CPU, network and memory usage - Bug 1384094\n    try {\n      await PageThumbs._store(url, url, null, true);\n    } catch (err) {\n      // Probably failed to create the empty file, but not much more we can do.\n    }\n    return null;\n  },\n\n  /**\n   * Checks if all the open windows are private browsing windows. If so, we do not\n   * want to collect screenshots. If there exists at least 1 non-private window,\n   * we are ok to collect screenshots.\n   */\n  _shouldGetScreenshots() {\n    for (let win of Services.wm.getEnumerator(\"navigator:browser\")) {\n      if (!PrivateBrowsingUtils.isWindowPrivate(win)) {\n        // As soon as we encounter 1 non-private window, screenshots are fair game.\n        return true;\n      }\n    }\n    return false;\n  },\n\n  /**\n   * Conditionally get a screenshot for a link if there's no existing pending\n   * screenshot. Updates the cached link's desired property with the result.\n   *\n   * @param link {object} Link object to update\n   * @param url {string} Url to get a screenshot of\n   * @param property {string} Name of property on object to set\n   @ @param onScreenshot {function} Callback for when the screenshot loads\n   */\n  async maybeCacheScreenshot(link, url, property, onScreenshot) {\n    // If there are only private windows open, do not collect screenshots\n    if (!this._shouldGetScreenshots()) {\n      return;\n    }\n    // Nothing to do if we already have a pending screenshot or\n    // if a previous request failed and returned null.\n    const cache = link.__sharedCache;\n    if (cache.fetchingScreenshot || link[property] !== undefined) {\n      return;\n    }\n\n    // Save the promise to the cache so other links get it immediately\n    cache.fetchingScreenshot = this.getScreenshotForURL(url);\n\n    // Clean up now that we got the screenshot\n    const screenshot = await cache.fetchingScreenshot;\n    delete cache.fetchingScreenshot;\n\n    // Update the cache for future links and call back for existing content\n    cache.updateLink(property, screenshot);\n    onScreenshot(screenshot);\n  },\n};\n"
  },
  {
    "path": "lib/SearchShortcuts.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\"use strict\";\n\nconst { Services } = ChromeUtils.import(\"resource://gre/modules/Services.jsm\");\n\n// List of sites we match against Topsites in order to identify sites\n// that should be converted to search Topsites\nconst SEARCH_SHORTCUTS = [\n  { keyword: \"@amazon\", shortURL: \"amazon\", url: \"https://amazon.com\" },\n  { keyword: \"@\\u767E\\u5EA6\", shortURL: \"baidu\", url: \"https://baidu.com\" },\n  { keyword: \"@google\", shortURL: \"google\", url: \"https://google.com\" },\n  {\n    keyword: \"@\\u044F\\u043D\\u0434\\u0435\\u043A\\u0441\",\n    shortURL: \"yandex\",\n    url: \"https://yandex.com\",\n  },\n];\nthis.SEARCH_SHORTCUTS = SEARCH_SHORTCUTS;\n\n// These can be added via the editor but will not be added organically\nthis.CUSTOM_SEARCH_SHORTCUTS = [\n  ...SEARCH_SHORTCUTS,\n  { keyword: \"@bing\", shortURL: \"bing\", url: \"https://bing.com\" },\n  {\n    keyword: \"@duckduckgo\",\n    shortURL: \"duckduckgo\",\n    url: \"https://duckduckgo.com\",\n  },\n  { keyword: \"@ebay\", shortURL: \"ebay\", url: \"https://ebay.com\" },\n  { keyword: \"@twitter\", shortURL: \"twitter\", url: \"https://twitter.com\" },\n  {\n    keyword: \"@wikipedia\",\n    shortURL: \"wikipedia\",\n    url: \"https://wikipedia.org\",\n  },\n];\n\n// Note: you must add the activity stream branch to the beginning of this if using outside activity stream\nthis.SEARCH_SHORTCUTS_EXPERIMENT = \"improvesearch.topSiteSearchShortcuts\";\nthis.SEARCH_SHORTCUTS_SEARCH_ENGINES_PREF =\n  \"improvesearch.topSiteSearchShortcuts.searchEngines\";\nthis.SEARCH_SHORTCUTS_HAVE_PINNED_PREF =\n  \"improvesearch.topSiteSearchShortcuts.havePinned\";\n\nfunction getSearchProvider(candidateShortURL) {\n  return (\n    SEARCH_SHORTCUTS.filter(match => candidateShortURL === match.shortURL)[0] ||\n    null\n  );\n}\nthis.getSearchProvider = getSearchProvider;\n\n// Check topsite against predefined list of valid search engines\n// https://searchfox.org/mozilla-central/rev/ca869724246f4230b272ed1c8b9944596e80d920/toolkit/components/search/nsSearchService.js#939\nasync function checkHasSearchEngine(keyword) {\n  return (await Services.search.getDefaultEngines()).find(e =>\n    e.wrappedJSObject._internalAliases.includes(keyword)\n  );\n}\nthis.checkHasSearchEngine = checkHasSearchEngine;\n\nconst EXPORTED_SYMBOLS = [\n  \"checkHasSearchEngine\",\n  \"getSearchProvider\",\n  \"SEARCH_SHORTCUTS\",\n  \"CUSTOM_SEARCH_SHORTCUTS\",\n  \"SEARCH_SHORTCUTS_EXPERIMENT\",\n  \"SEARCH_SHORTCUTS_SEARCH_ENGINES_PREF\",\n  \"SEARCH_SHORTCUTS_HAVE_PINNED_PREF\",\n];\n"
  },
  {
    "path": "lib/SectionsManager.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\"use strict\";\n\nconst { EventEmitter } = ChromeUtils.import(\n  \"resource://gre/modules/EventEmitter.jsm\"\n);\nconst { Services } = ChromeUtils.import(\"resource://gre/modules/Services.jsm\");\nconst { actionCreators: ac, actionTypes: at } = ChromeUtils.import(\n  \"resource://activity-stream/common/Actions.jsm\"\n);\nconst { getDefaultOptions } = ChromeUtils.import(\n  \"resource://activity-stream/lib/ActivityStreamStorage.jsm\"\n);\n\nChromeUtils.defineModuleGetter(\n  this,\n  \"PlacesUtils\",\n  \"resource://gre/modules/PlacesUtils.jsm\"\n);\n\n/*\n * Generators for built in sections, keyed by the pref name for their feed.\n * Built in sections may depend on options stored as serialised JSON in the pref\n * `${feed_pref_name}.options`.\n */\nconst BUILT_IN_SECTIONS = {\n  \"feeds.section.topstories\": options => ({\n    id: \"topstories\",\n    pref: {\n      titleString: {\n        id: \"home-prefs-recommended-by-header\",\n        values: { provider: options.provider_name },\n      },\n      descString: { id: \"home-prefs-recommended-by-description\" },\n      nestedPrefs: options.show_spocs\n        ? [\n            {\n              name: \"showSponsored\",\n              titleString: \"home-prefs-recommended-by-option-sponsored-stories\",\n              icon: \"icon-info\",\n            },\n          ]\n        : [],\n      learnMore: {\n        link: {\n          href: \"https://getpocket.com/firefox/new_tab_learn_more\",\n          id: \"home-prefs-recommended-by-learn-more\",\n        },\n      },\n    },\n    shouldHidePref: options.hidden,\n    eventSource: \"TOP_STORIES\",\n    icon: options.provider_icon,\n    title: {\n      id: \"newtab-section-header-pocket\",\n      values: { provider: options.provider_name },\n    },\n    learnMore: {\n      link: {\n        href: \"https://getpocket.com/firefox/new_tab_learn_more\",\n        message: { id: \"newtab-pocket-whats-pocket\" },\n      },\n    },\n    privacyNoticeURL:\n      \"https://www.mozilla.org/privacy/firefox/#suggest-relevant-content\",\n    compactCards: false,\n    rowsPref: \"section.topstories.rows\",\n    maxRows: 4,\n    availableLinkMenuOptions: [\n      \"CheckBookmarkOrArchive\",\n      \"CheckSavedToPocket\",\n      \"Separator\",\n      \"OpenInNewWindow\",\n      \"OpenInPrivateWindow\",\n      \"Separator\",\n      \"BlockUrl\",\n    ],\n    emptyState: {\n      message: {\n        id: \"newtab-empty-section-topstories\",\n        values: { provider: options.provider_name },\n      },\n      icon: \"check\",\n    },\n    shouldSendImpressionStats: true,\n    dedupeFrom: [\"highlights\"],\n  }),\n  \"feeds.section.highlights\": options => ({\n    id: \"highlights\",\n    pref: {\n      titleString: { id: \"home-prefs-highlights-header\" },\n      descString: { id: \"home-prefs-highlights-description\" },\n      nestedPrefs: [\n        {\n          name: \"section.highlights.includeVisited\",\n          titleString: \"home-prefs-highlights-option-visited-pages\",\n        },\n        {\n          name: \"section.highlights.includeBookmarks\",\n          titleString: \"home-prefs-highlights-options-bookmarks\",\n        },\n        {\n          name: \"section.highlights.includeDownloads\",\n          titleString: \"home-prefs-highlights-option-most-recent-download\",\n        },\n        {\n          name: \"section.highlights.includePocket\",\n          titleString: \"home-prefs-highlights-option-saved-to-pocket\",\n        },\n      ],\n    },\n    shouldHidePref: false,\n    eventSource: \"HIGHLIGHTS\",\n    icon: \"highlights\",\n    title: { id: \"newtab-section-header-highlights\" },\n    compactCards: true,\n    rowsPref: \"section.highlights.rows\",\n    maxRows: 4,\n    emptyState: {\n      message: { id: \"newtab-empty-section-highlights\" },\n      icon: \"highlights\",\n    },\n    shouldSendImpressionStats: false,\n  }),\n};\n\nconst SectionsManager = {\n  ACTIONS_TO_PROXY: [\"WEBEXT_CLICK\", \"WEBEXT_DISMISS\"],\n  CONTEXT_MENU_PREFS: { CheckSavedToPocket: \"extensions.pocket.enabled\" },\n  CONTEXT_MENU_OPTIONS_FOR_HIGHLIGHT_TYPES: {\n    history: [\n      \"CheckBookmark\",\n      \"CheckSavedToPocket\",\n      \"Separator\",\n      \"OpenInNewWindow\",\n      \"OpenInPrivateWindow\",\n      \"Separator\",\n      \"BlockUrl\",\n      \"DeleteUrl\",\n    ],\n    bookmark: [\n      \"CheckBookmark\",\n      \"CheckSavedToPocket\",\n      \"Separator\",\n      \"OpenInNewWindow\",\n      \"OpenInPrivateWindow\",\n      \"Separator\",\n      \"BlockUrl\",\n      \"DeleteUrl\",\n    ],\n    pocket: [\n      \"ArchiveFromPocket\",\n      \"CheckSavedToPocket\",\n      \"Separator\",\n      \"OpenInNewWindow\",\n      \"OpenInPrivateWindow\",\n      \"Separator\",\n      \"BlockUrl\",\n    ],\n    download: [\n      \"OpenFile\",\n      \"ShowFile\",\n      \"Separator\",\n      \"GoToDownloadPage\",\n      \"CopyDownloadLink\",\n      \"Separator\",\n      \"RemoveDownload\",\n      \"BlockUrl\",\n    ],\n  },\n  initialized: false,\n  sections: new Map(),\n  async init(prefs = {}, storage) {\n    this._storage = storage;\n\n    for (const feedPrefName of Object.keys(BUILT_IN_SECTIONS)) {\n      const optionsPrefName = `${feedPrefName}.options`;\n      await this.addBuiltInSection(feedPrefName, prefs[optionsPrefName]);\n\n      this._dedupeConfiguration = [];\n      this.sections.forEach(section => {\n        if (section.dedupeFrom) {\n          this._dedupeConfiguration.push({\n            id: section.id,\n            dedupeFrom: section.dedupeFrom,\n          });\n        }\n      });\n    }\n\n    Object.keys(this.CONTEXT_MENU_PREFS).forEach(k =>\n      Services.prefs.addObserver(this.CONTEXT_MENU_PREFS[k], this)\n    );\n\n    this.initialized = true;\n    this.emit(this.INIT);\n  },\n  observe(subject, topic, data) {\n    switch (topic) {\n      case \"nsPref:changed\":\n        for (const pref of Object.keys(this.CONTEXT_MENU_PREFS)) {\n          if (data === this.CONTEXT_MENU_PREFS[pref]) {\n            this.updateSections();\n          }\n        }\n        break;\n    }\n  },\n  updateSectionPrefs(id, collapsed) {\n    const section = this.sections.get(id);\n    if (!section) {\n      return;\n    }\n\n    const updatedSection = Object.assign({}, section, {\n      pref: Object.assign({}, section.pref, collapsed),\n    });\n    this.updateSection(id, updatedSection, true);\n  },\n  async addBuiltInSection(feedPrefName, optionsPrefValue = \"{}\") {\n    let options;\n    let storedPrefs;\n    try {\n      options = JSON.parse(optionsPrefValue);\n    } catch (e) {\n      options = {};\n      Cu.reportError(`Problem parsing options pref for ${feedPrefName}`);\n    }\n    try {\n      storedPrefs = (await this._storage.get(feedPrefName)) || {};\n    } catch (e) {\n      storedPrefs = {};\n      Cu.reportError(`Problem getting stored prefs for ${feedPrefName}`);\n    }\n    const defaultSection = BUILT_IN_SECTIONS[feedPrefName](options);\n    const section = Object.assign({}, defaultSection, {\n      pref: Object.assign(\n        {},\n        defaultSection.pref,\n        getDefaultOptions(storedPrefs)\n      ),\n    });\n    section.pref.feed = feedPrefName;\n    this.addSection(section.id, Object.assign(section, { options }));\n  },\n  addSection(id, options) {\n    this.updateLinkMenuOptions(options, id);\n    this.sections.set(id, options);\n    this.emit(this.ADD_SECTION, id, options);\n  },\n  removeSection(id) {\n    this.emit(this.REMOVE_SECTION, id);\n    this.sections.delete(id);\n  },\n  enableSection(id) {\n    this.updateSection(id, { enabled: true }, true);\n    this.emit(this.ENABLE_SECTION, id);\n  },\n  disableSection(id) {\n    this.updateSection(\n      id,\n      { enabled: false, rows: [], initialized: false },\n      true\n    );\n    this.emit(this.DISABLE_SECTION, id);\n  },\n  updateSections() {\n    this.sections.forEach((section, id) =>\n      this.updateSection(id, section, true)\n    );\n  },\n  updateSection(id, options, shouldBroadcast) {\n    this.updateLinkMenuOptions(options, id);\n    if (this.sections.has(id)) {\n      const optionsWithDedupe = Object.assign({}, options, {\n        dedupeConfigurations: this._dedupeConfiguration,\n      });\n      this.sections.set(id, Object.assign(this.sections.get(id), options));\n      this.emit(this.UPDATE_SECTION, id, optionsWithDedupe, shouldBroadcast);\n    }\n  },\n\n  /**\n   * Save metadata to places db and add a visit for that URL.\n   */\n  updateBookmarkMetadata({ url }) {\n    this.sections.forEach((section, id) => {\n      if (id === \"highlights\") {\n        // Skip Highlights cards, we already have that metadata.\n        return;\n      }\n      if (section.rows) {\n        section.rows.forEach(card => {\n          if (\n            card.url === url &&\n            card.description &&\n            card.title &&\n            card.image\n          ) {\n            PlacesUtils.history.update({\n              url: card.url,\n              title: card.title,\n              description: card.description,\n              previewImageURL: card.image,\n            });\n            // Highlights query skips bookmarks with no visits.\n            PlacesUtils.history.insert({\n              url,\n              title: card.title,\n              visits: [{}],\n            });\n          }\n        });\n      }\n    });\n  },\n\n  /**\n   * Sets the section's context menu options. These are all available context menu\n   * options minus the ones that are tied to a pref (see CONTEXT_MENU_PREFS) set\n   * to false.\n   *\n   * @param options section options\n   * @param id      section ID\n   */\n  updateLinkMenuOptions(options, id) {\n    if (options.availableLinkMenuOptions) {\n      options.contextMenuOptions = options.availableLinkMenuOptions.filter(\n        o =>\n          !this.CONTEXT_MENU_PREFS[o] ||\n          Services.prefs.getBoolPref(this.CONTEXT_MENU_PREFS[o])\n      );\n    }\n\n    // Once we have rows, we can give each card it's own context menu based on it's type.\n    // We only want to do this for highlights because those have different data types.\n    // All other sections (built by the web extension API) will have the same context menu per section\n    if (options.rows && id === \"highlights\") {\n      this._addCardTypeLinkMenuOptions(options.rows);\n    }\n  },\n\n  /**\n   * Sets each card in highlights' context menu options based on the card's type.\n   * (See types.js for a list of types)\n   *\n   * @param rows section rows containing a type for each card\n   */\n  _addCardTypeLinkMenuOptions(rows) {\n    for (let card of rows) {\n      if (!this.CONTEXT_MENU_OPTIONS_FOR_HIGHLIGHT_TYPES[card.type]) {\n        Cu.reportError(\n          `No context menu for highlight type ${card.type} is configured`\n        );\n      } else {\n        card.contextMenuOptions = this.CONTEXT_MENU_OPTIONS_FOR_HIGHLIGHT_TYPES[\n          card.type\n        ];\n\n        // Remove any options that shouldn't be there based on CONTEXT_MENU_PREFS.\n        // For example: If the Pocket extension is disabled, we should remove the CheckSavedToPocket option\n        // for each card that has it\n        card.contextMenuOptions = card.contextMenuOptions.filter(\n          o =>\n            !this.CONTEXT_MENU_PREFS[o] ||\n            Services.prefs.getBoolPref(this.CONTEXT_MENU_PREFS[o])\n        );\n      }\n    }\n  },\n\n  /**\n   * Update a specific section card by its url. This allows an action to be\n   * broadcast to all existing pages to update a specific card without having to\n   * also force-update the rest of the section's cards and state on those pages.\n   *\n   * @param id              The id of the section with the card to be updated\n   * @param url             The url of the card to update\n   * @param options         The options to update for the card\n   * @param shouldBroadcast Whether or not to broadcast the update\n   */\n  updateSectionCard(id, url, options, shouldBroadcast) {\n    if (this.sections.has(id)) {\n      const card = this.sections.get(id).rows.find(elem => elem.url === url);\n      if (card) {\n        Object.assign(card, options);\n      }\n      this.emit(this.UPDATE_SECTION_CARD, id, url, options, shouldBroadcast);\n    }\n  },\n  removeSectionCard(sectionId, url) {\n    if (!this.sections.has(sectionId)) {\n      return;\n    }\n    const rows = this.sections\n      .get(sectionId)\n      .rows.filter(row => row.url !== url);\n    this.updateSection(sectionId, { rows }, true);\n  },\n  onceInitialized(callback) {\n    if (this.initialized) {\n      callback();\n    } else {\n      this.once(this.INIT, callback);\n    }\n  },\n  uninit() {\n    Object.keys(this.CONTEXT_MENU_PREFS).forEach(k =>\n      Services.prefs.removeObserver(this.CONTEXT_MENU_PREFS[k], this)\n    );\n    SectionsManager.initialized = false;\n  },\n};\n\nfor (const action of [\n  \"ACTION_DISPATCHED\",\n  \"ADD_SECTION\",\n  \"REMOVE_SECTION\",\n  \"ENABLE_SECTION\",\n  \"DISABLE_SECTION\",\n  \"UPDATE_SECTION\",\n  \"UPDATE_SECTION_CARD\",\n  \"INIT\",\n  \"UNINIT\",\n]) {\n  SectionsManager[action] = action;\n}\n\nEventEmitter.decorate(SectionsManager);\n\nclass SectionsFeed {\n  constructor() {\n    this.init = this.init.bind(this);\n    this.onAddSection = this.onAddSection.bind(this);\n    this.onRemoveSection = this.onRemoveSection.bind(this);\n    this.onUpdateSection = this.onUpdateSection.bind(this);\n    this.onUpdateSectionCard = this.onUpdateSectionCard.bind(this);\n  }\n\n  init() {\n    SectionsManager.on(SectionsManager.ADD_SECTION, this.onAddSection);\n    SectionsManager.on(SectionsManager.REMOVE_SECTION, this.onRemoveSection);\n    SectionsManager.on(SectionsManager.UPDATE_SECTION, this.onUpdateSection);\n    SectionsManager.on(\n      SectionsManager.UPDATE_SECTION_CARD,\n      this.onUpdateSectionCard\n    );\n    // Catch any sections that have already been added\n    SectionsManager.sections.forEach((section, id) =>\n      this.onAddSection(SectionsManager.ADD_SECTION, id, section)\n    );\n  }\n\n  uninit() {\n    SectionsManager.uninit();\n    SectionsManager.emit(SectionsManager.UNINIT);\n    SectionsManager.off(SectionsManager.ADD_SECTION, this.onAddSection);\n    SectionsManager.off(SectionsManager.REMOVE_SECTION, this.onRemoveSection);\n    SectionsManager.off(SectionsManager.UPDATE_SECTION, this.onUpdateSection);\n    SectionsManager.off(\n      SectionsManager.UPDATE_SECTION_CARD,\n      this.onUpdateSectionCard\n    );\n  }\n\n  onAddSection(event, id, options) {\n    if (options) {\n      this.store.dispatch(\n        ac.BroadcastToContent({\n          type: at.SECTION_REGISTER,\n          data: Object.assign({ id }, options),\n        })\n      );\n\n      // Make sure the section is in sectionOrder pref. Otherwise, prepend it.\n      const orderedSections = this.orderedSectionIds;\n      if (!orderedSections.includes(id)) {\n        orderedSections.unshift(id);\n        this.store.dispatch(\n          ac.SetPref(\"sectionOrder\", orderedSections.join(\",\"))\n        );\n      }\n    }\n  }\n\n  onRemoveSection(event, id) {\n    this.store.dispatch(\n      ac.BroadcastToContent({ type: at.SECTION_DEREGISTER, data: id })\n    );\n  }\n\n  onUpdateSection(event, id, options, shouldBroadcast = false) {\n    if (options) {\n      const action = {\n        type: at.SECTION_UPDATE,\n        data: Object.assign(options, { id }),\n      };\n      this.store.dispatch(\n        shouldBroadcast\n          ? ac.BroadcastToContent(action)\n          : ac.AlsoToPreloaded(action)\n      );\n    }\n  }\n\n  onUpdateSectionCard(event, id, url, options, shouldBroadcast = false) {\n    if (options) {\n      const action = {\n        type: at.SECTION_UPDATE_CARD,\n        data: { id, url, options },\n      };\n      this.store.dispatch(\n        shouldBroadcast\n          ? ac.BroadcastToContent(action)\n          : ac.AlsoToPreloaded(action)\n      );\n    }\n  }\n\n  get orderedSectionIds() {\n    return this.store.getState().Prefs.values.sectionOrder.split(\",\");\n  }\n\n  get enabledSectionIds() {\n    let sections = this.store\n      .getState()\n      .Sections.filter(section => section.enabled)\n      .map(s => s.id);\n    // Top Sites is a special case. Append if the feed is enabled.\n    if (this.store.getState().Prefs.values[\"feeds.topsites\"]) {\n      sections.push(\"topsites\");\n    }\n    return sections;\n  }\n\n  moveSection(id, direction) {\n    const orderedSections = this.orderedSectionIds;\n    const enabledSections = this.enabledSectionIds;\n    let index = orderedSections.indexOf(id);\n    orderedSections.splice(index, 1);\n    if (direction > 0) {\n      // \"Move Down\"\n      while (index < orderedSections.length) {\n        // If the section at the index is enabled/visible, insert moved section after.\n        // Otherwise, move on to the next spot and check it.\n        if (enabledSections.includes(orderedSections[index++])) {\n          break;\n        }\n      }\n    } else {\n      // \"Move Up\"\n      while (index > 0) {\n        // If the section at the previous index is enabled/visible, insert moved section there.\n        // Otherwise, move on to the previous spot and check it.\n        index--;\n        if (enabledSections.includes(orderedSections[index])) {\n          break;\n        }\n      }\n    }\n\n    orderedSections.splice(index, 0, id);\n    this.store.dispatch(ac.SetPref(\"sectionOrder\", orderedSections.join(\",\")));\n  }\n\n  async onAction(action) {\n    switch (action.type) {\n      case at.INIT:\n        SectionsManager.onceInitialized(this.init);\n        break;\n      // Wait for pref values, as some sections have options stored in prefs\n      case at.PREFS_INITIAL_VALUES:\n        SectionsManager.init(\n          action.data,\n          this.store.dbStorage.getDbTable(\"sectionPrefs\")\n        );\n        break;\n      case at.PREF_CHANGED: {\n        if (action.data) {\n          const matched = action.data.name.match(\n            /^(feeds.section.(\\S+)).options$/i\n          );\n          if (matched) {\n            await SectionsManager.addBuiltInSection(\n              matched[1],\n              action.data.value\n            );\n            this.store.dispatch({\n              type: at.SECTION_OPTIONS_CHANGED,\n              data: matched[2],\n            });\n          }\n        }\n        break;\n      }\n      case at.UPDATE_SECTION_PREFS:\n        SectionsManager.updateSectionPrefs(action.data.id, action.data.value);\n        break;\n      case at.PLACES_BOOKMARK_ADDED:\n        SectionsManager.updateBookmarkMetadata(action.data);\n        break;\n      case at.WEBEXT_DISMISS:\n        if (action.data) {\n          SectionsManager.removeSectionCard(\n            action.data.source,\n            action.data.url\n          );\n        }\n        break;\n      case at.SECTION_DISABLE:\n        SectionsManager.disableSection(action.data);\n        break;\n      case at.SECTION_ENABLE:\n        SectionsManager.enableSection(action.data);\n        break;\n      case at.SECTION_MOVE:\n        this.moveSection(action.data.id, action.data.direction);\n        break;\n      case at.UNINIT:\n        this.uninit();\n        break;\n    }\n    if (\n      SectionsManager.ACTIONS_TO_PROXY.includes(action.type) &&\n      SectionsManager.sections.size > 0\n    ) {\n      SectionsManager.emit(\n        SectionsManager.ACTION_DISPATCHED,\n        action.type,\n        action.data\n      );\n    }\n  }\n}\n\nthis.SectionsFeed = SectionsFeed;\nthis.SectionsManager = SectionsManager;\nconst EXPORTED_SYMBOLS = [\"SectionsFeed\", \"SectionsManager\"];\n"
  },
  {
    "path": "lib/ShortURL.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nconst { XPCOMUtils } = ChromeUtils.import(\n  \"resource://gre/modules/XPCOMUtils.jsm\"\n);\nconst { Services } = ChromeUtils.import(\"resource://gre/modules/Services.jsm\");\n\nXPCOMUtils.defineLazyServiceGetter(\n  this,\n  \"IDNService\",\n  \"@mozilla.org/network/idn-service;1\",\n  \"nsIIDNService\"\n);\n\nXPCOMUtils.defineLazyGlobalGetters(this, [\"URL\"]);\n\n/**\n * Properly convert internationalized domain names.\n * @param {string} host Domain hostname.\n * @returns {string} Hostname suitable to be displayed.\n */\nfunction handleIDNHost(hostname) {\n  try {\n    return IDNService.convertToDisplayIDN(hostname, {});\n  } catch (e) {\n    // If something goes wrong (e.g. host is an IP address) just fail back\n    // to the full domain.\n    return hostname;\n  }\n}\n\n/**\n * Get the effective top level domain of a host.\n * @param {string} host The host to be analyzed.\n * @return {str} The suffix or empty string if there's no suffix.\n */\nfunction getETLD(host) {\n  try {\n    return Services.eTLD.getPublicSuffixFromHost(host);\n  } catch (err) {\n    return \"\";\n  }\n}\n\n/**\n * shortURL - Creates a short version of a link's url, used for display purposes\n *            e.g. {url: http://www.foosite.com}  =>  \"foosite\"\n *\n * @param  {obj} link A link object\n *         {str} link.url (required)- The url of the link\n * @return {str}   A short url\n */\nfunction shortURL({ url }) {\n  if (!url) {\n    return \"\";\n  }\n\n  // Make sure we have a valid / parseable url\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch (ex) {\n    // Not entirely sure what we have, but just give it back\n    return url;\n  }\n\n  // Clean up the url (lowercase hostname via URL and remove www.)\n  const hostname = parsed.hostname.replace(/^www\\./i, \"\");\n\n  // Remove the eTLD (e.g., com, net) and the preceding period from the hostname\n  const eTLD = getETLD(hostname);\n  const eTLDExtra = eTLD.length ? -(eTLD.length + 1) : Infinity;\n\n  // Ideally get the short eTLD-less host but fall back to longer url parts\n  return (\n    handleIDNHost(hostname.slice(0, eTLDExtra) || hostname) ||\n    parsed.pathname ||\n    parsed.href\n  );\n}\n\nconst EXPORTED_SYMBOLS = [\"shortURL\", \"getETLD\"];\n"
  },
  {
    "path": "lib/SiteClassifier.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\"use strict\";\n\nconst { RemoteSettings } = ChromeUtils.import(\n  \"resource://services-settings/remote-settings.js\"\n);\n\n// Returns whether the passed in params match the criteria.\n// To match, they must contain all the params specified in criteria and the values\n// must match if a value is provided in criteria.\nfunction _hasParams(criteria, params) {\n  for (let param of criteria) {\n    const val = params.get(param.key);\n    if (\n      val === null ||\n      (param.value && param.value !== val) ||\n      (param.prefix && !val.startsWith(param.prefix))\n    ) {\n      return false;\n    }\n  }\n  return true;\n}\n\n/**\n * classifySite\n * Classifies a given URL into a category based on classification data from RemoteSettings.\n * The data from remote settings can match a category by one of the following:\n *  - match the exact URL\n *  - match the hostname or second level domain (sld)\n *  - match query parameter(s), and optionally their values or prefixes\n *  - match both (hostname or sld) and query parameter(s)\n *\n * The data looks like:\n * [{\n *    \"type\": \"hostname-and-params-match\",\n *    \"criteria\": [\n *      {\n *        \"url\": \"https://matchurl.com\",\n *        \"hostname\": \"matchhostname.com\",\n *        \"sld\": \"secondleveldomain\",\n *        \"params\": [\n *          {\n *            \"key\": \"matchparam\",\n *            \"value\": \"matchvalue\",\n *            \"prefix\": \"matchpPrefix\",\n *          },\n *        ],\n *      },\n *    ],\n *    \"weight\": 300,\n *  },...]\n */\nasync function classifySite(url, RS = RemoteSettings) {\n  let category = \"other\";\n  let parsedURL;\n\n  // Try to parse the url.\n  for (let _url of [url, `https://${url}`]) {\n    try {\n      parsedURL = new URL(_url);\n      break;\n    } catch (e) {}\n  }\n\n  if (parsedURL) {\n    // If we parsed successfully, find a match.\n    const hostname = parsedURL.hostname.replace(/^www\\./i, \"\");\n    const params = parsedURL.searchParams;\n    // NOTE: there will be an initial/default local copy of the data in m-c.\n    // Therefore, this should never return an empty list [].\n    const siteTypes = await RS(\"sites-classification\").get();\n    const sortedSiteTypes = siteTypes.sort(\n      (x, y) => (y.weight || 0) - (x.weight || 0)\n    );\n    for (let type of sortedSiteTypes) {\n      for (let criteria of type.criteria) {\n        if (criteria.url && criteria.url !== url) {\n          continue;\n        }\n        if (criteria.hostname && criteria.hostname !== hostname) {\n          continue;\n        }\n        if (criteria.sld && criteria.sld !== hostname.split(\".\")[0]) {\n          continue;\n        }\n        if (criteria.params && !_hasParams(criteria.params, params)) {\n          continue;\n        }\n        return type.type;\n      }\n    }\n  }\n  return category;\n}\n\nconst EXPORTED_SYMBOLS = [\"classifySite\"];\n"
  },
  {
    "path": "lib/SnippetsTestMessageProvider.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\"use strict\";\n\nconst TEST_ICON = \"chrome://branding/content/icon64.png\";\nconst TEST_ICON_16 = \"chrome://branding/content/icon16.png\";\nconst TEST_ICON_BW =\n  \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAAHdElNRQfjBQ8QDifrKGc/AAABf0lEQVQoz4WRO08UUQCFvztzd1AgG9jRgGwkhEoMIYGSygYt+A00tpZGY0jYxAJKEwkNjX9AK2xACx4dhFiQQCiMMRr2kYXdnQcz7L0z91qAMVac6hTfSU7OgVsk/prtyfSNfRb7ge2cd7dmVucP/wM2lwqVqoyICahRx9Nz71+8AnAAvlTct+dSYDBYcgJ+Fj68XFu/AfamnIoWFoHFYrAUuYMSn55/fAIOxIs1t4MhQpNxRYsUD0ld7r8DCfZph4QecrqkhCREgMLSeISQkAy0UBgE0CYgIkeRA9HdsCQhpEGCxichpItHigEcPH4XJLRbTf8STY0iiiuu60Ifxexx04F0N+aCgJCAhPQmD/cp/RC5A79WvUyhUHSIidAIoESv9VfAhW9n8+XqTCoyMsz1cviMMrGz9BrjAuboYHZajyXCInEocI8yvccbC+0muABanR4/tONjQz3DzgNKtj9sfv66XD9B/3tT9g/akb7h0bJwzxqqmlRHLr4rLPwBlYWoYj77l2AAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTktMDUtMTVUMTY6MTQ6MzkrMDA6MDD5/4XBAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE5LTA1LTE1VDE2OjE0OjM5KzAwOjAwiKI9fQAAAABJRU5ErkJggg==\";\n\nconst MESSAGES = () => [\n  {\n    id: \"SIMPLE_TEST_1\",\n    template: \"simple_snippet\",\n    campaign: \"test_campaign_blocking\",\n    content: {\n      icon: TEST_ICON,\n      icon_dark_theme: TEST_ICON_BW,\n      title: \"Firefox Account!\",\n      title_icon: TEST_ICON_16,\n      title_icon_dark_theme: TEST_ICON_BW,\n      text:\n        \"<syncLink>Sync it, link it, take it with you</syncLink>. All this and more with a Firefox Account.\",\n      links: {\n        syncLink: { url: \"https://www.mozilla.org/en-US/firefox/accounts\" },\n      },\n      block_button_text: \"Block\",\n    },\n  },\n  {\n    id: \"SIMPLE_TEST_1_NO_DARK_THEME\",\n    template: \"simple_snippet\",\n    campaign: \"test_campaign_blocking\",\n    content: {\n      icon: TEST_ICON,\n      icon_dark_theme: \"\",\n      title: \"Firefox Account!\",\n      title_icon: TEST_ICON_16,\n      title_icon_dark_theme: \"\",\n      text:\n        \"<syncLink>Sync it, link it, take it with you</syncLink>. All this and more with a Firefox Account.\",\n      links: {\n        syncLink: { url: \"https://www.mozilla.org/en-US/firefox/accounts\" },\n      },\n      block_button_text: \"Block\",\n    },\n  },\n  {\n    id: \"SIMPLE_TEST_1_SAME_CAMPAIGN\",\n    template: \"simple_snippet\",\n    campaign: \"test_campaign_blocking\",\n    content: {\n      icon: TEST_ICON,\n      icon_dark_theme: TEST_ICON_BW,\n      text:\n        \"<syncLink>Sync it, link it, take it with you</syncLink>. All this and more with a Firefox Account.\",\n      links: {\n        syncLink: { url: \"https://www.mozilla.org/en-US/firefox/accounts\" },\n      },\n      block_button_text: \"Block\",\n    },\n  },\n  {\n    id: \"SIMPLE_TEST_TALL\",\n    template: \"simple_snippet\",\n    content: {\n      icon: TEST_ICON,\n      icon_dark_theme: TEST_ICON_BW,\n      text:\n        \"<syncLink>Sync it, link it, take it with you</syncLink>. All this and more with a Firefox Account.\",\n      links: {\n        syncLink: { url: \"https://www.mozilla.org/en-US/firefox/accounts\" },\n      },\n      button_label: \"Get one now!\",\n      button_url: \"https://www.mozilla.org/en-US/firefox/accounts\",\n      block_button_text: \"Block\",\n      tall: true,\n    },\n  },\n  {\n    id: \"SIMPLE_TEST_BUTTON_URL_1\",\n    template: \"simple_snippet\",\n    content: {\n      icon: TEST_ICON,\n      icon_dark_theme: TEST_ICON_BW,\n      button_label: \"Get one now!\",\n      button_url: \"https://www.mozilla.org/en-US/firefox/accounts\",\n      text:\n        \"Sync it, link it, take it with you. All this and more with a Firefox Account.\",\n      block_button_text: \"Block\",\n    },\n  },\n  {\n    id: \"SIMPLE_TEST_BUTTON_ACTION_1\",\n    template: \"simple_snippet\",\n    content: {\n      icon: TEST_ICON,\n      icon_dark_theme: TEST_ICON_BW,\n      button_label: \"Open about:config\",\n      button_action: \"OPEN_ABOUT_PAGE\",\n      button_action_args: \"config\",\n      text: \"Testing the OPEN_ABOUT_PAGE action\",\n      block_button_text: \"Block\",\n    },\n  },\n  {\n    id: \"SIMPLE_WITH_TITLE_TEST_1\",\n    template: \"simple_snippet\",\n    content: {\n      icon: TEST_ICON,\n      icon_dark_theme: TEST_ICON_BW,\n      title: \"Ready to sync?\",\n      text: \"Get connected with a <syncLink>Firefox account</syncLink>.\",\n      links: {\n        syncLink: { url: \"https://www.mozilla.org/en-US/firefox/accounts\" },\n      },\n      block_button_text: \"Block\",\n    },\n  },\n  {\n    id: \"NEWSLETTER_TEST_DEFAULTS\",\n    template: \"newsletter_snippet\",\n    content: {\n      scene1_icon: TEST_ICON,\n      scene1_icon_dark_theme: TEST_ICON_BW,\n      scene1_title: \"Be a part of a movement.\",\n      scene1_title_icon: TEST_ICON_16,\n      scene1_title_icon_dark_theme: TEST_ICON_BW,\n      scene1_text:\n        \"Internet shutdowns, hackers, harassment &ndash; the health of the internet is on the line. Sign up and Mozilla will keep you updated on how you can help.\",\n      scene1_button_label: \"Continue\",\n      scene1_button_color: \"#712b00\",\n      scene1_button_background_color: \"#ff9400\",\n      scene2_title: \"Let's do this!\",\n      locale: \"en-CA\",\n      scene2_dismiss_button_text: \"Dismiss\",\n      scene2_text:\n        \"Sign up for the Mozilla newsletter and we will keep you updated on how you can help.\",\n      scene2_privacy_html:\n        \"I'm okay with Mozilla handling my info as explained in this <privacyLink>Privacy Notice</privacyLink>.\",\n      scene2_newsletter: \"mozilla-foundation\",\n      success_text: \"Check your inbox for the confirmation!\",\n      error_text: \"Error!\",\n      retry_button_label: \"Try again?\",\n      links: {\n        privacyLink: {\n          url:\n            \"https://www.mozilla.org/privacy/websites/?sample_rate=0.001&snippet_name=7894\",\n        },\n      },\n    },\n  },\n  {\n    id: \"NEWSLETTER_TEST_1\",\n    template: \"newsletter_snippet\",\n    content: {\n      scene1_icon: TEST_ICON,\n      scene1_icon_dark_theme: TEST_ICON_BW,\n      scene1_title: \"Be a part of a movement.\",\n      scene1_title_icon: \"\",\n      scene1_text:\n        \"Internet shutdowns, hackers, harassment &ndash; the health of the internet is on the line. Sign up and Mozilla will keep you updated on how you can help.\",\n      scene1_button_label: \"Continue\",\n      scene1_button_color: \"#712b00\",\n      scene1_button_background_color: \"#ff9400\",\n      scene2_title: \"Let's do this!\",\n      locale: \"en-CA\",\n      scene2_dismiss_button_text: \"Dismiss\",\n      scene2_text:\n        \"Sign up for the Mozilla newsletter and we will keep you updated on how you can help.\",\n      scene2_privacy_html:\n        \"I'm okay with Mozilla handling my info as explained in this <privacyLink>Privacy Notice</privacyLink>.\",\n      scene2_button_label: \"Sign Me up\",\n      scene2_email_placeholder_text: \"Your email here\",\n      scene2_newsletter: \"mozilla-foundation\",\n      success_text: \"Check your inbox for the confirmation!\",\n      error_text: \"Error!\",\n      links: {\n        privacyLink: {\n          url:\n            \"https://www.mozilla.org/privacy/websites/?sample_rate=0.001&snippet_name=7894\",\n        },\n      },\n    },\n  },\n  {\n    id: \"NEWSLETTER_TEST_SCENE1_SECTION_TITLE_ICON\",\n    template: \"newsletter_snippet\",\n    content: {\n      scene1_icon: TEST_ICON,\n      scene1_icon_dark_theme: TEST_ICON_BW,\n      scene1_title: \"Be a part of a movement.\",\n      scene1_title_icon: \"\",\n      scene1_text:\n        \"Internet shutdowns, hackers, harassment &ndash; the health of the internet is on the line. Sign up and Mozilla will keep you updated on how you can help.\",\n      scene1_button_label: \"Continue\",\n      scene1_button_color: \"#712b00\",\n      scene1_button_background_color: \"#ff9400\",\n      scene1_section_title_icon:\n        \"resource://activity-stream/data/content/assets/glyph-pocket-16.svg\",\n      scene1_section_title_text:\n        \"All the Firefox news that's fit to Firefox print!\",\n      scene2_title: \"Let's do this!\",\n      locale: \"en-CA\",\n      scene2_dismiss_button_text: \"Dismiss\",\n      scene2_text:\n        \"Sign up for the Mozilla newsletter and we will keep you updated on how you can help.\",\n      scene2_privacy_html:\n        \"I'm okay with Mozilla handling my info as explained in this <privacyLink>Privacy Notice</privacyLink>.\",\n      scene2_button_label: \"Sign Me up\",\n      scene2_email_placeholder_text: \"Your email here\",\n      scene2_newsletter: \"mozilla-foundation\",\n      success_text: \"Check your inbox for the confirmation!\",\n      error_text: \"Error!\",\n      links: {\n        privacyLink: {\n          url:\n            \"https://www.mozilla.org/privacy/websites/?sample_rate=0.001&snippet_name=7894\",\n        },\n      },\n    },\n  },\n  {\n    id: \"FXA_SNIPPET_TEST_1\",\n    template: \"fxa_signup_snippet\",\n    content: {\n      scene1_icon: TEST_ICON,\n      scene1_icon_dark_theme: TEST_ICON_BW,\n      scene1_button_label: \"Get connected with sync!\",\n      scene1_button_color: \"#712b00\",\n      scene1_button_background_color: \"#ff9400\",\n\n      scene1_text:\n        \"Connect to Firefox by securely syncing passwords, bookmarks, and open tabs.\",\n      scene1_title: \"Browser better.\",\n      scene1_title_icon: TEST_ICON_16,\n      scene1_title_icon_dark_theme: TEST_ICON_BW,\n\n      scene2_text:\n        \"Connect to your Firefox account to securely sync passwords, bookmarks, and open tabs.\",\n      scene2_title: \"Title 123\",\n      scene2_email_placeholder_text: \"Your email\",\n      scene2_button_label: \"Continue\",\n      scene2_dismiss_button_text: \"Dismiss\",\n    },\n  },\n  {\n    id: \"FXA_SNIPPET_TEST_TITLE_ICON\",\n    template: \"fxa_signup_snippet\",\n    content: {\n      scene1_icon: TEST_ICON,\n      scene1_icon_dark_theme: TEST_ICON_BW,\n      scene1_button_label: \"Get connected with sync!\",\n      scene1_button_color: \"#712b00\",\n      scene1_button_background_color: \"#ff9400\",\n\n      scene1_text:\n        \"Connect to Firefox by securely syncing passwords, bookmarks, and open tabs.\",\n      scene1_title: \"Browser better.\",\n      scene1_title_icon: TEST_ICON_16,\n      scene1_title_icon_dark_theme: TEST_ICON_BW,\n\n      scene1_section_title_icon:\n        \"resource://activity-stream/data/content/assets/glyph-pocket-16.svg\",\n      scene1_section_title_text: \"Firefox Accounts: Receivable benefits\",\n\n      scene2_text:\n        \"Connect to your Firefox account to securely sync passwords, bookmarks, and open tabs.\",\n      scene2_title: \"Title 123\",\n      scene2_email_placeholder_text: \"Your email\",\n      scene2_button_label: \"Continue\",\n      scene2_dismiss_button_text: \"Dismiss\",\n    },\n  },\n  {\n    id: \"SNIPPETS_SEND_TO_DEVICE_TEST\",\n    template: \"send_to_device_snippet\",\n    content: {\n      include_sms: true,\n      locale: \"en-CA\",\n      country: \"us\",\n      message_id_sms: \"ff-mobilesn-download\",\n      message_id_email: \"download-firefox-mobile\",\n\n      scene1_button_background_color: \"#6200a4\",\n      scene1_button_color: \"#FFFFFF\",\n      scene1_button_label: \"Install now\",\n      scene1_icon: TEST_ICON,\n      scene1_icon_dark_theme: TEST_ICON_BW,\n      scene1_text: \"Browse without compromise with Firefox Mobile.\",\n      scene1_title: \"Full-featured. Customizable. Lightning fast\",\n      scene1_title_icon: TEST_ICON_16,\n      scene1_title_icon_dark_theme: TEST_ICON_BW,\n\n      scene2_button_label: \"Send\",\n      scene2_disclaimer_html:\n        \"The intended recipient of the email must have consented. <privacyLink>Learn more</privacyLink>.\",\n      scene2_dismiss_button_text: \"Dismiss\",\n      scene2_icon: TEST_ICON,\n      scene2_icon_dark_theme: TEST_ICON_BW,\n      scene2_input_placeholder: \"Your email address or phone number\",\n      scene2_text:\n        \"Send Firefox to your phone and take a powerful independent browser with you.\",\n      scene2_title: \"Let's do this!\",\n\n      error_text: \"Oops, there was a problem.\",\n      success_title: \"Your download link was sent.\",\n      success_text: \"Check your device for the email message!\",\n      links: {\n        privacyLink: {\n          url:\n            \"https://www.mozilla.org/privacy/websites/?sample_rate=0.001&snippet_name=7894\",\n        },\n      },\n    },\n  },\n  {\n    id: \"SNIPPETS_SEND_TO_DEVICE_TEST_NO_DARK_THEME\",\n    template: \"send_to_device_snippet\",\n    content: {\n      include_sms: true,\n      locale: \"en-CA\",\n      country: \"us\",\n      message_id_sms: \"ff-mobilesn-download\",\n      message_id_email: \"download-firefox-mobile\",\n\n      scene1_button_background_color: \"#6200a4\",\n      scene1_button_color: \"#FFFFFF\",\n      scene1_button_label: \"Install now\",\n      scene1_icon: TEST_ICON,\n      scene1_icon_dark_theme: \"\",\n      scene1_text: \"Browse without compromise with Firefox Mobile.\",\n      scene1_title: \"Full-featured. Customizable. Lightning fast\",\n      scene1_title_icon: TEST_ICON_16,\n      scene1_title_icon_dark_theme: \"\",\n\n      scene2_button_label: \"Send\",\n      scene2_disclaimer_html:\n        \"The intended recipient of the email must have consented. <privacyLink>Learn more</privacyLink>.\",\n      scene2_dismiss_button_text: \"Dismiss\",\n      scene2_icon: TEST_ICON,\n      scene2_icon_dark_theme: \"\",\n      scene2_input_placeholder: \"Your email address or phone number\",\n      scene2_text:\n        \"Send Firefox to your phone and take a powerful independent browser with you.\",\n      scene2_title: \"Let's do this!\",\n\n      error_text: \"Oops, there was a problem.\",\n      success_title: \"Your download link was sent.\",\n      success_text: \"Check your device for the email message!\",\n      links: {\n        privacyLink: {\n          url:\n            \"https://www.mozilla.org/privacy/websites/?sample_rate=0.001&snippet_name=7894\",\n        },\n      },\n    },\n  },\n  {\n    id: \"SNIPPETS_SEND_TO_DEVICE_TEST_SECTION_TITLE_ICON\",\n    template: \"send_to_device_snippet\",\n    content: {\n      include_sms: true,\n      locale: \"en-CA\",\n      country: \"us\",\n      message_id_sms: \"ff-mobilesn-download\",\n      message_id_email: \"download-firefox-mobile\",\n\n      scene1_button_background_color: \"#6200a4\",\n      scene1_button_color: \"#FFFFFF\",\n      scene1_button_label: \"Install now\",\n      scene1_icon: TEST_ICON,\n      scene1_icon_dark_theme: TEST_ICON_BW,\n      scene1_text: \"Browse without compromise with Firefox Mobile.\",\n      scene1_title: \"Full-featured. Customizable. Lightning fast\",\n      scene1_title_icon: TEST_ICON_16,\n      scene1_title_icon_dark_theme: TEST_ICON_BW,\n      scene1_section_title_icon:\n        \"resource://activity-stream/data/content/assets/glyph-pocket-16.svg\",\n      scene1_section_title_text: \"Send Firefox to your mobile device!\",\n\n      scene2_button_label: \"Send\",\n      scene2_disclaimer_html:\n        \"The intended recipient of the email must have consented. <privacyLink>Learn more</privacyLink>.\",\n      scene2_dismiss_button_text: \"Dismiss\",\n      scene2_icon: TEST_ICON,\n      scene2_icon_dark_theme: TEST_ICON_BW,\n      scene2_input_placeholder: \"Your email address or phone number\",\n      scene2_text:\n        \"Send Firefox to your phone and take a powerful independent browser with you.\",\n      scene2_title: \"Let's do this!\",\n\n      error_text: \"Oops, there was a problem.\",\n      success_title: \"Your download link was sent.\",\n      success_text: \"Check your device for the email message!\",\n      links: {\n        privacyLink: {\n          url:\n            \"https://www.mozilla.org/privacy/websites/?sample_rate=0.001&snippet_name=7894\",\n        },\n      },\n    },\n  },\n  {\n    id: \"EOY_TEST_1\",\n    template: \"eoy_snippet\",\n    content: {\n      highlight_color: \"#f05\",\n      background_color: \"#ddd\",\n      text_color: \"yellow\",\n      selected_button: \"donation_amount_first\",\n      icon: TEST_ICON,\n      icon_dark_theme: TEST_ICON_BW,\n      button_label: \"Donate\",\n      monthly_checkbox_label_text: \"Make my donation monthly\",\n      currency_code: \"usd\",\n      donation_amount_first: 50,\n      donation_amount_second: 25,\n      donation_amount_third: 10,\n      donation_amount_fourth: 5,\n      donation_form_url:\n        \"https://donate.mozilla.org/pl/?utm_source=desktop-snippet&amp;utm_medium=snippet&amp;utm_campaign=donate&amp;utm_term=7556\",\n      text:\n        \"Big corporations want to restrict how we access the web. Fake news is making it harder for us to find the truth. Online bullies are silencing inspired voices. The <em>not-for-profit Mozilla Foundation</em> fights for a healthy internet with programs like our Tech Policy Fellowships and Internet Health Report; <b>will you donate today</b>?\",\n    },\n  },\n  {\n    id: \"EOY_BOLD_TEST_1\",\n    template: \"eoy_snippet\",\n    content: {\n      icon: TEST_ICON,\n      icon_dark_theme: TEST_ICON_BW,\n      selected_button: \"donation_amount_second\",\n      button_label: \"Donate\",\n      monthly_checkbox_label_text: \"Make my donation monthly\",\n      currency_code: \"usd\",\n      donation_amount_first: 50,\n      donation_amount_second: 25,\n      donation_amount_third: 10,\n      donation_amount_fourth: 5,\n      donation_form_url: \"https://donate.mozilla.org\",\n      text:\n        \"Big corporations want to restrict how we access the web. Fake news is making it harder for us to find the truth. Online bullies are silencing inspired voices. The <em>not-for-profit Mozilla Foundation</em> fights for a healthy internet with programs like our Tech Policy Fellowships and Internet Health Report; <b>will you donate today</b>?\",\n      test: \"bold\",\n    },\n  },\n  {\n    id: \"EOY_TAKEOVER_TEST_1\",\n    template: \"eoy_snippet\",\n    content: {\n      icon: TEST_ICON,\n      icon_dark_theme: TEST_ICON_BW,\n      button_label: \"Donate\",\n      monthly_checkbox_label_text: \"Make my donation monthly\",\n      currency_code: \"usd\",\n      donation_amount_first: 50,\n      donation_amount_second: 25,\n      donation_amount_third: 10,\n      donation_amount_fourth: 5,\n      donation_form_url: \"https://donate.mozilla.org\",\n      text:\n        \"Big corporations want to restrict how we access the web. Fake news is making it harder for us to find the truth. Online bullies are silencing inspired voices. The <em>not-for-profit Mozilla Foundation</em> fights for a healthy internet with programs like our Tech Policy Fellowships and Internet Health Report; <b>will you donate today</b>?\",\n      test: \"takeover\",\n    },\n  },\n  {\n    id: \"SIMPLE_TEST_WITH_SECTION_HEADING\",\n    template: \"simple_snippet\",\n    content: {\n      button_label: \"Get one now!\",\n      button_url: \"https://www.mozilla.org/en-US/firefox/accounts\",\n      icon: TEST_ICON,\n      icon_dark_theme: TEST_ICON_BW,\n      title: \"Firefox Account!\",\n      text:\n        \"<syncLink>Sync it, link it, take it with you</syncLink>. All this and more with a Firefox Account.\",\n      links: {\n        syncLink: { url: \"https://www.mozilla.org/en-US/firefox/accounts\" },\n      },\n      block_button_text: \"Block\",\n      section_title_icon:\n        \"resource://activity-stream/data/content/assets/glyph-pocket-16.svg\",\n      section_title_text: \"Messages from Mozilla\",\n    },\n  },\n  {\n    id: \"SIMPLE_TEST_WITH_SECTION_HEADING_AND_LINK\",\n    template: \"simple_snippet\",\n    content: {\n      icon: TEST_ICON,\n      icon_dark_theme: TEST_ICON_BW,\n      title: \"Firefox Account!\",\n      text:\n        \"Sync it, link it, take it with you. All this and more with a Firefox Account.\",\n      block_button_text: \"Block\",\n      section_title_icon:\n        \"resource://activity-stream/data/content/assets/glyph-pocket-16.svg\",\n      section_title_text: \"Messages from Mozilla (click for info)\",\n      section_title_url: \"https://www.mozilla.org/about\",\n    },\n  },\n  {\n    id: \"SIMPLE_BELOW_SEARCH_TEST_1\",\n    template: \"simple_below_search_snippet\",\n    content: {\n      icon: TEST_ICON,\n      icon_dark_theme: TEST_ICON_BW,\n      text:\n        \"Securely store passwords, bookmarks, and more with a Firefox Account. <syncLink>Sign up</syncLink>\",\n      links: {\n        syncLink: { url: \"https://www.mozilla.org/en-US/firefox/accounts\" },\n      },\n      block_button_text: \"Block\",\n    },\n  },\n  {\n    id: \"SIMPLE_BELOW_SEARCH_TEST_2\",\n    template: \"simple_below_search_snippet\",\n    content: {\n      icon: TEST_ICON,\n      icon_dark_theme: TEST_ICON_BW,\n      text:\n        \"<syncLink>Connect your Firefox Account to Sync</syncLink> your protected passwords, open tabs and bookmarks, and they'll always be available to you - on all of your devices.\",\n      links: {\n        syncLink: { url: \"https://www.mozilla.org/en-US/firefox/accounts\" },\n      },\n      block_button_text: \"Block\",\n    },\n  },\n  {\n    id: \"SIMPLE_BELOW_SEARCH_TEST_TITLE\",\n    template: \"simple_below_search_snippet\",\n    content: {\n      icon: TEST_ICON,\n      icon_dark_theme: TEST_ICON_BW,\n      title: \"See if you've been part of an online data breach.\",\n      text:\n        \"Securely store passwords, bookmarks, and more with a Firefox Account. <syncLink>Sign up</syncLink>\",\n      links: {\n        syncLink: { url: \"https://www.mozilla.org/en-US/firefox/accounts\" },\n      },\n      block_button_text: \"Block\",\n    },\n  },\n  {\n    id: \"SPECIAL_SNIPPET_BUTTON_1\",\n    template: \"simple_below_search_snippet\",\n    content: {\n      icon: TEST_ICON,\n      icon_dark_theme: TEST_ICON_BW,\n      button_label: \"Find Out Now\",\n      button_url: \"https://www.mozilla.org/en-US/firefox/accounts\",\n      title: \"See if you've been part of an online data breach.\",\n      text: \"Firefox Monitor tells you what hackers already know about you.\",\n      block_button_text: \"Block\",\n    },\n  },\n  {\n    id: \"SPECIAL_SNIPPET_LONG_CONTENT\",\n    template: \"simple_below_search_snippet\",\n    content: {\n      icon: TEST_ICON,\n      icon_dark_theme: TEST_ICON_BW,\n      button_label: \"Find Out Now\",\n      button_url: \"https://www.mozilla.org/en-US/firefox/accounts\",\n      title: \"See if you've been part of an online data breach.\",\n      text:\n        \"Firefox Monitor tells you what hackers already know about you. Here's some extra text to make the content really long.\",\n      block_button_text: \"Block\",\n    },\n  },\n  {\n    id: \"SPECIAL_SNIPPET_NO_TITLE\",\n    template: \"simple_below_search_snippet\",\n    content: {\n      icon: TEST_ICON,\n      icon_dark_theme: TEST_ICON_BW,\n      button_label: \"Find Out Now\",\n      button_url: \"https://www.mozilla.org/en-US/firefox/accounts\",\n      text: \"Firefox Monitor tells you what hackers already know about you.\",\n      block_button_text: \"Block\",\n    },\n  },\n  {\n    id: \"SPECIAL_SNIPPET_MONITOR\",\n    template: \"simple_below_search_snippet\",\n    content: {\n      icon: TEST_ICON,\n      title: \"See if you've been part of an online data breach.\",\n      text: \"Firefox Monitor tells you what hackers already know about you.\",\n      button_label: \"Get monitor\",\n      button_action: \"ENABLE_FIREFOX_MONITOR\",\n      button_action_args: {\n        url:\n          \"https://monitor.firefox.com/oauth/init?utm_source=snippets&utm_campaign=monitor-snippet-test&form_type=email&entrypoint=newtab\",\n        flowRequestParams: {\n          entrypoint: \"snippets\",\n          utm_term: \"monitor\",\n          form_type: \"email\",\n        },\n      },\n      block_button_text: \"Block\",\n    },\n  },\n];\n\nconst SnippetsTestMessageProvider = {\n  getMessages() {\n    return (\n      MESSAGES()\n        // Ensures we never actually show test except when triggered by debug tools\n        .map(message => ({\n          ...message,\n          targeting: `providerCohorts.snippets_local_testing == \"SHOW_TEST\"`,\n        }))\n    );\n  },\n};\nthis.SnippetsTestMessageProvider = SnippetsTestMessageProvider;\n\nconst EXPORTED_SYMBOLS = [\"SnippetsTestMessageProvider\"];\n"
  },
  {
    "path": "lib/Store.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\"use strict\";\n\nconst { ActivityStreamMessageChannel } = ChromeUtils.import(\n  \"resource://activity-stream/lib/ActivityStreamMessageChannel.jsm\"\n);\nconst { ActivityStreamStorage } = ChromeUtils.import(\n  \"resource://activity-stream/lib/ActivityStreamStorage.jsm\"\n);\nconst { Prefs } = ChromeUtils.import(\n  \"resource://activity-stream/lib/ActivityStreamPrefs.jsm\"\n);\nconst { reducers } = ChromeUtils.import(\n  \"resource://activity-stream/common/Reducers.jsm\"\n);\nconst { redux } = ChromeUtils.import(\n  \"resource://activity-stream/vendor/Redux.jsm\"\n);\n\n/**\n * Store - This has a similar structure to a redux store, but includes some extra\n *         functionality to allow for routing of actions between the Main processes\n *         and child processes via a ActivityStreamMessageChannel.\n *         It also accepts an array of \"Feeds\" on inititalization, which\n *         can listen for any action that is dispatched through the store.\n */\nthis.Store = class Store {\n  /**\n   * constructor - The redux store and message manager are created here,\n   *               but no listeners are added until \"init\" is called.\n   */\n  constructor() {\n    this._middleware = this._middleware.bind(this);\n    // Bind each redux method so we can call it directly from the Store. E.g.,\n    // store.dispatch() will call store._store.dispatch();\n    for (const method of [\"dispatch\", \"getState\", \"subscribe\"]) {\n      this[method] = (...args) => this._store[method](...args);\n    }\n    this.feeds = new Map();\n    this._prefs = new Prefs();\n    this._messageChannel = new ActivityStreamMessageChannel({\n      dispatch: this.dispatch,\n    });\n    this._store = redux.createStore(\n      redux.combineReducers(reducers),\n      redux.applyMiddleware(this._middleware, this._messageChannel.middleware)\n    );\n    this.storage = null;\n  }\n\n  /**\n   * _middleware - This is redux middleware consumed by redux.createStore.\n   *               it calls each feed's .onAction method, if one\n   *               is defined.\n   */\n  _middleware() {\n    return next => action => {\n      next(action);\n      for (const store of this.feeds.values()) {\n        if (store.onAction) {\n          store.onAction(action);\n        }\n      }\n    };\n  }\n\n  /**\n   * initFeed - Initializes a feed by calling its constructor function\n   *\n   * @param  {string} feedName The name of a feed, as defined in the object\n   *                           passed to Store.init\n   * @param {Action} initAction An optional action to initialize the feed\n   */\n  initFeed(feedName, initAction) {\n    const feed = this._feedFactories.get(feedName)();\n    feed.store = this;\n    this.feeds.set(feedName, feed);\n    if (initAction && feed.onAction) {\n      feed.onAction(initAction);\n    }\n  }\n\n  /**\n   * uninitFeed - Removes a feed and calls its uninit function if defined\n   *\n   * @param  {string} feedName The name of a feed, as defined in the object\n   *                           passed to Store.init\n   * @param {Action} uninitAction An optional action to uninitialize the feed\n   */\n  uninitFeed(feedName, uninitAction) {\n    const feed = this.feeds.get(feedName);\n    if (!feed) {\n      return;\n    }\n    if (uninitAction && feed.onAction) {\n      feed.onAction(uninitAction);\n    }\n    this.feeds.delete(feedName);\n  }\n\n  /**\n   * onPrefChanged - Listener for handling feed changes.\n   */\n  onPrefChanged(name, value) {\n    if (this._feedFactories.has(name)) {\n      if (value) {\n        this.initFeed(name, this._initAction);\n      } else {\n        this.uninitFeed(name, this._uninitAction);\n      }\n    }\n  }\n\n  /**\n   * init - Initializes the ActivityStreamMessageChannel channel, and adds feeds.\n   *\n   * Note that it intentionally initializes the TelemetryFeed first so that the\n   * addon is able to report the init errors from other feeds.\n   *\n   * @param  {Map} feedFactories A Map of feeds with the name of the pref for\n   *                                the feed as the key and a function that\n   *                                constructs an instance of the feed.\n   * @param {Action} initAction An optional action that will be dispatched\n   *                            to feeds when they're created.\n   * @param {Action} uninitAction An optional action for when feeds uninit.\n   */\n  async init(feedFactories, initAction, uninitAction) {\n    this._feedFactories = feedFactories;\n    this._initAction = initAction;\n    this._uninitAction = uninitAction;\n\n    const telemetryKey = \"feeds.telemetry\";\n    if (feedFactories.has(telemetryKey) && this._prefs.get(telemetryKey)) {\n      this.initFeed(telemetryKey);\n    }\n\n    await this._initIndexedDB(telemetryKey);\n\n    for (const pref of feedFactories.keys()) {\n      if (pref !== telemetryKey && this._prefs.get(pref)) {\n        this.initFeed(pref);\n      }\n    }\n\n    this._prefs.observeBranch(this);\n    this._messageChannel.createChannel();\n\n    // Dispatch an initial action after all enabled feeds are ready\n    if (initAction) {\n      this.dispatch(initAction);\n    }\n\n    // Dispatch NEW_TAB_INIT/NEW_TAB_LOAD events after INIT event.\n    this._messageChannel.simulateMessagesForExistingTabs();\n  }\n\n  async _initIndexedDB(telemetryKey) {\n    this.dbStorage = new ActivityStreamStorage({\n      storeNames: [\"sectionPrefs\", \"snippets\"],\n      telemetry: this.feeds.get(telemetryKey),\n    });\n    // Accessing the db causes the object stores to be created / migrated.\n    // This needs to happen before other instances try to access the db, which\n    // would update only a subset of the stores to the latest version.\n    try {\n      await this.dbStorage.db; // eslint-disable-line no-unused-expressions\n    } catch (e) {\n      this.dbStorage.telemetry = null;\n    }\n  }\n\n  /**\n   * uninit -  Uninitalizes each feed, clears them, and destroys the message\n   *           manager channel.\n   *\n   * @return {type}  description\n   */\n  uninit() {\n    if (this._uninitAction) {\n      this.dispatch(this._uninitAction);\n    }\n    this._prefs.ignoreBranch(this);\n    this.feeds.clear();\n    this._feedFactories = null;\n    this._messageChannel.destroyChannel();\n  }\n};\n\nconst EXPORTED_SYMBOLS = [\"Store\"];\n"
  },
  {
    "path": "lib/SystemTickFeed.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\"use strict\";\n\nconst { actionTypes: at } = ChromeUtils.import(\n  \"resource://activity-stream/common/Actions.jsm\"\n);\n\nChromeUtils.defineModuleGetter(\n  this,\n  \"setInterval\",\n  \"resource://gre/modules/Timer.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"clearInterval\",\n  \"resource://gre/modules/Timer.jsm\"\n);\n\n// Frequency at which SYSTEM_TICK events are fired\nconst SYSTEM_TICK_INTERVAL = 5 * 60 * 1000;\n\nthis.SystemTickFeed = class SystemTickFeed {\n  init() {\n    this.intervalId = setInterval(\n      () => this.store.dispatch({ type: at.SYSTEM_TICK }),\n      SYSTEM_TICK_INTERVAL\n    );\n  }\n\n  onAction(action) {\n    switch (action.type) {\n      case at.INIT:\n        this.init();\n        break;\n      case at.UNINIT:\n        clearInterval(this.intervalId);\n        break;\n    }\n  }\n};\n\nthis.SYSTEM_TICK_INTERVAL = SYSTEM_TICK_INTERVAL;\nconst EXPORTED_SYMBOLS = [\"SystemTickFeed\", \"SYSTEM_TICK_INTERVAL\"];\n"
  },
  {
    "path": "lib/TelemetryFeed.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\n\"use strict\";\n\nconst { Services } = ChromeUtils.import(\"resource://gre/modules/Services.jsm\");\nconst { XPCOMUtils } = ChromeUtils.import(\n  \"resource://gre/modules/XPCOMUtils.jsm\"\n);\n\nconst { actionTypes: at, actionUtils: au } = ChromeUtils.import(\n  \"resource://activity-stream/common/Actions.jsm\"\n);\nconst { Prefs } = ChromeUtils.import(\n  \"resource://activity-stream/lib/ActivityStreamPrefs.jsm\"\n);\nconst { classifySite } = ChromeUtils.import(\n  \"resource://activity-stream/lib/SiteClassifier.jsm\"\n);\n\nChromeUtils.defineModuleGetter(\n  this,\n  \"ASRouterPreferences\",\n  \"resource://activity-stream/lib/ASRouterPreferences.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"perfService\",\n  \"resource://activity-stream/common/PerfService.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"PingCentre\",\n  \"resource:///modules/PingCentre.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"UTEventReporting\",\n  \"resource://activity-stream/lib/UTEventReporting.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"UpdateUtils\",\n  \"resource://gre/modules/UpdateUtils.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"HomePage\",\n  \"resource:///modules/HomePage.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"ExtensionSettingsStore\",\n  \"resource://gre/modules/ExtensionSettingsStore.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"PrivateBrowsingUtils\",\n  \"resource://gre/modules/PrivateBrowsingUtils.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"ClientID\",\n  \"resource://gre/modules/ClientID.jsm\"\n);\n\nXPCOMUtils.defineLazyServiceGetters(this, {\n  gUUIDGenerator: [\"@mozilla.org/uuid-generator;1\", \"nsIUUIDGenerator\"],\n  aboutNewTabService: [\n    \"@mozilla.org/browser/aboutnewtab-service;1\",\n    \"nsIAboutNewTabService\",\n  ],\n});\n\nconst ACTIVITY_STREAM_ID = \"activity-stream\";\nconst DOMWINDOW_OPENED_TOPIC = \"domwindowopened\";\nconst DOMWINDOW_UNLOAD_TOPIC = \"unload\";\nconst TAB_PINNED_EVENT = \"TabPinned\";\n\n// This is a mapping table between the user preferences and its encoding code\nconst USER_PREFS_ENCODING = {\n  showSearch: 1 << 0,\n  \"feeds.topsites\": 1 << 1,\n  \"feeds.section.topstories\": 1 << 2,\n  \"feeds.section.highlights\": 1 << 3,\n  \"feeds.snippets\": 1 << 4,\n  showSponsored: 1 << 5,\n  \"asrouter.userprefs.cfr.addons\": 1 << 6,\n  \"asrouter.userprefs.cfr.features\": 1 << 7,\n};\n\nconst PREF_IMPRESSION_ID = \"impressionId\";\nconst TELEMETRY_PREF = \"telemetry\";\nconst EVENTS_TELEMETRY_PREF = \"telemetry.ut.events\";\nconst STRUCTURED_INGESTION_TELEMETRY_PREF = \"telemetry.structuredIngestion\";\nconst STRUCTURED_INGESTION_ENDPOINT_PREF =\n  \"telemetry.structuredIngestion.endpoint\";\n// List of namespaces for the structured ingestion system.\n// They are defined in https://github.com/mozilla-services/mozilla-pipeline-schemas\nconst STRUCTURED_INGESTION_NAMESPACE_AS = \"activity-stream\";\nconst STRUCTURED_INGESTION_NAMESPACE_MS = \"messaging-system\";\n\nthis.TelemetryFeed = class TelemetryFeed {\n  constructor(options) {\n    this.sessions = new Map();\n    this._prefs = new Prefs();\n    this._impressionId = this.getOrCreateImpressionId();\n    this._aboutHomeSeen = false;\n    this._classifySite = classifySite;\n    this._addWindowListeners = this._addWindowListeners.bind(this);\n    this.handleEvent = this.handleEvent.bind(this);\n  }\n\n  get telemetryEnabled() {\n    return this._prefs.get(TELEMETRY_PREF);\n  }\n\n  get eventTelemetryEnabled() {\n    return this._prefs.get(EVENTS_TELEMETRY_PREF);\n  }\n\n  get structuredIngestionTelemetryEnabled() {\n    return this._prefs.get(STRUCTURED_INGESTION_TELEMETRY_PREF);\n  }\n\n  get structuredIngestionEndpointBase() {\n    return this._prefs.get(STRUCTURED_INGESTION_ENDPOINT_PREF);\n  }\n\n  get telemetryClientId() {\n    Object.defineProperty(this, \"telemetryClientId\", {\n      value: ClientID.getClientID(),\n    });\n    return this.telemetryClientId;\n  }\n\n  init() {\n    Services.obs.addObserver(\n      this.browserOpenNewtabStart,\n      \"browser-open-newtab-start\"\n    );\n    // Add pin tab event listeners on future windows\n    Services.obs.addObserver(this._addWindowListeners, DOMWINDOW_OPENED_TOPIC);\n    // Listen for pin tab events on all open windows\n    for (let win of Services.wm.getEnumerator(\"navigator:browser\")) {\n      this._addWindowListeners(win);\n    }\n  }\n\n  handleEvent(event) {\n    switch (event.type) {\n      case TAB_PINNED_EVENT:\n        this.countPinnedTab(event.target);\n        break;\n      case DOMWINDOW_UNLOAD_TOPIC:\n        this._removeWindowListeners(event.target);\n        break;\n    }\n  }\n\n  _removeWindowListeners(win) {\n    win.removeEventListener(DOMWINDOW_UNLOAD_TOPIC, this.handleEvent);\n    win.removeEventListener(TAB_PINNED_EVENT, this.handleEvent);\n  }\n\n  _addWindowListeners(win) {\n    win.addEventListener(DOMWINDOW_UNLOAD_TOPIC, this.handleEvent);\n    win.addEventListener(TAB_PINNED_EVENT, this.handleEvent);\n  }\n\n  countPinnedTab(target, source = \"TAB_CONTEXT_MENU\") {\n    const win = target.ownerGlobal;\n    if (PrivateBrowsingUtils.isWindowPrivate(win)) {\n      return;\n    }\n    const event = Object.assign(this.createPing(), {\n      action: \"activity_stream_user_event\",\n      event: TAB_PINNED_EVENT.toUpperCase(),\n      value: { total_pinned_tabs: this.countTotalPinnedTabs() },\n      source,\n      // These fields are required but not relevant for this ping\n      page: \"n/a\",\n      session_id: \"n/a\",\n    });\n    this.sendEvent(event);\n  }\n\n  countTotalPinnedTabs() {\n    let pinnedTabs = 0;\n    for (let win of Services.wm.getEnumerator(\"navigator:browser\")) {\n      if (win.closed || PrivateBrowsingUtils.isWindowPrivate(win)) {\n        continue;\n      }\n      for (let tab of win.gBrowser.tabs) {\n        pinnedTabs += tab.pinned ? 1 : 0;\n      }\n    }\n\n    return pinnedTabs;\n  }\n\n  getOrCreateImpressionId() {\n    let impressionId = this._prefs.get(PREF_IMPRESSION_ID);\n    if (!impressionId) {\n      impressionId = String(gUUIDGenerator.generateUUID());\n      this._prefs.set(PREF_IMPRESSION_ID, impressionId);\n    }\n    return impressionId;\n  }\n\n  browserOpenNewtabStart() {\n    perfService.mark(\"browser-open-newtab-start\");\n  }\n\n  setLoadTriggerInfo(port) {\n    // XXX note that there is a race condition here; we're assuming that no\n    // other tab will be interleaving calls to browserOpenNewtabStart and\n    // when at.NEW_TAB_INIT gets triggered by RemotePages and calls this\n    // method.  For manually created windows, it's hard to imagine us hitting\n    // this race condition.\n    //\n    // However, for session restore, where multiple windows with multiple tabs\n    // might be restored much closer together in time, it's somewhat less hard,\n    // though it should still be pretty rare.\n    //\n    // The fix to this would be making all of the load-trigger notifications\n    // return some data with their notifications, and somehow propagate that\n    // data through closures into the tab itself so that we could match them\n    //\n    // As of this writing (very early days of system add-on perf telemetry),\n    // the hypothesis is that hitting this race should be so rare that makes\n    // more sense to live with the slight data inaccuracy that it would\n    // introduce, rather than doing the correct but complicated thing.  It may\n    // well be worth reexamining this hypothesis after we have more experience\n    // with the data.\n\n    let data_to_save;\n    try {\n      data_to_save = {\n        load_trigger_ts: perfService.getMostRecentAbsMarkStartByName(\n          \"browser-open-newtab-start\"\n        ),\n        load_trigger_type: \"menu_plus_or_keyboard\",\n      };\n    } catch (e) {\n      // if no mark was returned, we have nothing to save\n      return;\n    }\n    this.saveSessionPerfData(port, data_to_save);\n  }\n\n  /**\n   * Lazily initialize PingCentre for Activity Stream to send pings\n   */\n  get pingCentre() {\n    Object.defineProperty(this, \"pingCentre\", {\n      value: new PingCentre({ topic: ACTIVITY_STREAM_ID }),\n    });\n    return this.pingCentre;\n  }\n\n  /**\n   * Lazily initialize UTEventReporting to send pings\n   */\n  get utEvents() {\n    Object.defineProperty(this, \"utEvents\", { value: new UTEventReporting() });\n    return this.utEvents;\n  }\n\n  /**\n   * Get encoded user preferences, multiple prefs will be combined via bitwise OR operator\n   */\n  get userPreferences() {\n    let prefs = 0;\n\n    for (const pref of Object.keys(USER_PREFS_ENCODING)) {\n      if (this._prefs.get(pref)) {\n        prefs |= USER_PREFS_ENCODING[pref];\n      }\n    }\n    return prefs;\n  }\n\n  /**\n   *  Check if it is in the CFR experiment cohort. ASRouterPreferences lazily parses AS router pref.\n   */\n  get isInCFRCohort() {\n    for (let provider of ASRouterPreferences.providers) {\n      if (provider.id === \"cfr\" && provider.enabled && provider.cohort) {\n        return true;\n      }\n    }\n    return false;\n  }\n\n  /**\n   * addSession - Start tracking a new session\n   *\n   * @param  {string} id the portID of the open session\n   * @param  {string} the URL being loaded for this session (optional)\n   * @return {obj}    Session object\n   */\n  addSession(id, url) {\n    // XXX refactor to use setLoadTriggerInfo or saveSessionPerfData\n\n    // \"unexpected\" will be overwritten when appropriate\n    let load_trigger_type = \"unexpected\";\n    let load_trigger_ts;\n\n    if (!this._aboutHomeSeen && url === \"about:home\") {\n      this._aboutHomeSeen = true;\n\n      // XXX note that this will be incorrectly set in the following cases:\n      // session_restore following by clicking on the toolbar button,\n      // or someone who has changed their default home page preference to\n      // something else and later clicks the toolbar.  It will also be\n      // incorrectly unset if someone changes their \"Home Page\" preference to\n      // about:newtab.\n      //\n      // That said, the ratio of these mistakes to correct cases should\n      // be very small, and these issues should follow away as we implement\n      // the remaining load_trigger_type values for about:home in issue 3556.\n      //\n      // XXX file a bug to implement remaining about:home cases so this\n      // problem will go away and link to it here.\n      load_trigger_type = \"first_window_opened\";\n\n      // The real perceived trigger of first_window_opened is the OS-level\n      // clicking of the icon.  We use perfService.timeOrigin because it's the\n      // earliest number on this time scale that's easy to get.; We could\n      // actually use 0, but maybe that could be before the browser started?\n      // [bug 1401406](https://bugzilla.mozilla.org/show_bug.cgi?id=1401406)\n      // getting sorted out may help clarify. Even better, presumably, would be\n      // to use the process creation time for the main process, which is\n      // available, but somewhat harder to get. However, these are all more or\n      // less proxies for the same thing, so it's not clear how much the better\n      // numbers really matter, since we (activity stream) only control a\n      // relatively small amount of the code that's executing between the\n      // OS-click and when the first <browser> element starts loading.  That\n      // said, it's conceivable that it could help us catch regressions in the\n      // number of cycles early chrome code takes to execute, but it's likely\n      // that there are more direct ways to measure that.\n      load_trigger_ts = perfService.timeOrigin;\n    }\n\n    const session = {\n      session_id: String(gUUIDGenerator.generateUUID()),\n      // \"unknown\" will be overwritten when appropriate\n      page: url ? url : \"unknown\",\n      perf: {\n        load_trigger_type,\n        is_preloaded: false,\n      },\n    };\n\n    if (load_trigger_ts) {\n      session.perf.load_trigger_ts = load_trigger_ts;\n    }\n\n    this.sessions.set(id, session);\n    return session;\n  }\n\n  /**\n   * endSession - Stop tracking a session\n   *\n   * @param  {string} portID the portID of the session that just closed\n   */\n  endSession(portID) {\n    const session = this.sessions.get(portID);\n\n    if (!session) {\n      // It's possible the tab was never visible – in which case, there was no user session.\n      return;\n    }\n\n    this.sendDiscoveryStreamLoadedContent(portID, session);\n    this.sendDiscoveryStreamImpressions(portID, session);\n\n    if (session.perf.visibility_event_rcvd_ts) {\n      session.session_duration = Math.round(\n        perfService.absNow() - session.perf.visibility_event_rcvd_ts\n      );\n    } else {\n      // This session was never shown (i.e. the hidden preloaded newtab), there was no user session either.\n      this.sessions.delete(portID);\n      return;\n    }\n\n    let sessionEndEvent = this.createSessionEndEvent(session);\n    this.sendEvent(sessionEndEvent);\n    this.sendUTEvent(sessionEndEvent, this.utEvents.sendSessionEndEvent);\n    this.sessions.delete(portID);\n  }\n\n  /**\n   * Send impression pings for Discovery Stream for a given session.\n   *\n   * @note the impression reports are stored in session.impressionSets for different\n   * sources, and will be sent separately accordingly.\n   *\n   * @param {String} port  The session port with which this is associated\n   * @param {Object} session  The session object\n   */\n  sendDiscoveryStreamImpressions(port, session) {\n    const { impressionSets } = session;\n\n    if (!impressionSets) {\n      return;\n    }\n\n    Object.keys(impressionSets).forEach(source => {\n      const payload = this.createImpressionStats(port, {\n        source,\n        tiles: impressionSets[source],\n      });\n      this.sendStructuredIngestionEvent(\n        payload,\n        STRUCTURED_INGESTION_NAMESPACE_AS,\n        \"impression-stats\",\n        \"1\"\n      );\n    });\n  }\n\n  /**\n   * Send loaded content pings for Discovery Stream for a given session.\n   *\n   * @note the loaded content reports are stored in session.loadedContentSets for different\n   * sources, and will be sent separately accordingly.\n   *\n   * @param {String} port  The session port with which this is associated\n   * @param {Object} session  The session object\n   */\n  sendDiscoveryStreamLoadedContent(port, session) {\n    const { loadedContentSets } = session;\n\n    if (!loadedContentSets) {\n      return;\n    }\n\n    Object.keys(loadedContentSets).forEach(source => {\n      const tiles = loadedContentSets[source];\n      const payload = this.createImpressionStats(port, {\n        source,\n        tiles,\n        loaded: tiles.length,\n      });\n      this.sendStructuredIngestionEvent(\n        payload,\n        STRUCTURED_INGESTION_NAMESPACE_AS,\n        \"impression-stats\",\n        \"1\"\n      );\n    });\n  }\n\n  /**\n   * handleNewTabInit - Handle NEW_TAB_INIT, which creates a new session and sets the a flag\n   *                    for session.perf based on whether or not this new tab is preloaded\n   *\n   * @param  {obj} action the Action object\n   */\n  handleNewTabInit(action) {\n    const session = this.addSession(\n      au.getPortIdOfSender(action),\n      action.data.url\n    );\n    session.perf.is_preloaded =\n      action.data.browser.getAttribute(\"preloadedState\") === \"preloaded\";\n  }\n\n  /**\n   * createPing - Create a ping with common properties\n   *\n   * @param  {string} id The portID of the session, if a session is relevant (optional)\n   * @return {obj}    A telemetry ping\n   */\n  createPing(portID) {\n    const ping = {\n      addon_version: Services.appinfo.appBuildID,\n      locale: Services.locale.appLocaleAsLangTag,\n      user_prefs: this.userPreferences,\n    };\n\n    // If the ping is part of a user session, add session-related info\n    if (portID) {\n      const session = this.sessions.get(portID) || this.addSession(portID);\n      Object.assign(ping, { session_id: session.session_id });\n\n      if (session.page) {\n        Object.assign(ping, { page: session.page });\n      }\n    }\n    return ping;\n  }\n\n  /**\n   * createImpressionStats - Create a ping for an impression stats\n   *\n   * @param  {string} portID The portID of the open session\n   * @param  {ob} data The data object to be included in the ping.\n   * @return {obj}    A telemetry ping\n   */\n  createImpressionStats(portID, data) {\n    return Object.assign(this.createPing(portID), data, {\n      action: \"activity_stream_impression_stats\",\n      impression_id: this._impressionId,\n      client_id: \"n/a\",\n      session_id: \"n/a\",\n    });\n  }\n\n  createSpocsFillPing(data) {\n    return Object.assign(this.createPing(null), data, {\n      impression_id: this._impressionId,\n      session_id: \"n/a\",\n    });\n  }\n\n  createUserEvent(action) {\n    return Object.assign(\n      this.createPing(au.getPortIdOfSender(action)),\n      action.data,\n      { action: \"activity_stream_user_event\" }\n    );\n  }\n\n  createUndesiredEvent(action) {\n    return Object.assign(\n      this.createPing(au.getPortIdOfSender(action)),\n      { value: 0 }, // Default value\n      action.data,\n      { action: \"activity_stream_undesired_event\" }\n    );\n  }\n\n  createPerformanceEvent(action) {\n    return Object.assign(this.createPing(), action.data, {\n      action: \"activity_stream_performance_event\",\n    });\n  }\n\n  createSessionEndEvent(session) {\n    return Object.assign(this.createPing(), {\n      session_id: session.session_id,\n      page: session.page,\n      session_duration: session.session_duration,\n      action: \"activity_stream_session\",\n      perf: session.perf,\n    });\n  }\n\n  /**\n   * Create a ping for AS router event. The client_id is set to \"n/a\" by default,\n   * different component can override this by its own telemetry collection policy.\n   */\n  async createASRouterEvent(action) {\n    let event = {\n      ...action.data,\n      addon_version: Services.appinfo.appBuildID,\n      locale: Services.locale.appLocaleAsLangTag,\n    };\n    if (event.event_context && typeof event.event_context === \"object\") {\n      event.event_context = JSON.stringify(event.event_context);\n    }\n    switch (event.action) {\n      case \"cfr_user_event\":\n        event = await this.applyCFRPolicy(event);\n        break;\n      case \"snippets_user_event\":\n        event = await this.applySnippetsPolicy(event);\n        break;\n      // Bug 1594125 added a new onboarding-like provider called `whats-new-panel`.\n      case \"whats-new-panel_user_event\":\n      case \"onboarding_user_event\":\n        event = await this.applyOnboardingPolicy(event);\n        break;\n      case \"asrouter_undesired_event\":\n        event = this.applyUndesiredEventPolicy(event);\n        break;\n      default:\n        event = { ping: event };\n        break;\n    }\n    return event;\n  }\n\n  /**\n   * Per Bug 1484035, CFR metrics comply with following policies:\n   * 1). In release, it collects impression_id, and treats bucket_id as message_id\n   * 2). In prerelease, it collects client_id and message_id\n   * 3). In shield experiments conducted in release, it collects client_id and message_id\n   */\n  async applyCFRPolicy(ping) {\n    if (\n      UpdateUtils.getUpdateChannel(true) === \"release\" &&\n      !this.isInCFRCohort\n    ) {\n      ping.message_id = \"n/a\";\n      ping.impression_id = this._impressionId;\n    } else {\n      ping.client_id = await this.telemetryClientId;\n    }\n    delete ping.action;\n    return { ping, pingType: \"cfr\" };\n  }\n\n  /**\n   * Per Bug 1485069, all the metrics for Snippets in AS router use client_id in\n   * all the release channels\n   */\n  async applySnippetsPolicy(ping) {\n    ping.client_id = await this.telemetryClientId;\n    delete ping.action;\n    return { ping, pingType: \"snippets\" };\n  }\n\n  /**\n   * Per Bug 1482134, all the metrics for Onboarding in AS router use client_id in\n   * all the release channels\n   */\n  async applyOnboardingPolicy(ping) {\n    ping.client_id = await this.telemetryClientId;\n    delete ping.action;\n    return { ping, pingType: \"onboarding\" };\n  }\n\n  applyUndesiredEventPolicy(ping) {\n    ping.impression_id = this._impressionId;\n    delete ping.action;\n    return { ping, pingType: \"undesired-events\" };\n  }\n\n  sendEvent(event_object) {\n    switch (event_object.action) {\n      case \"activity_stream_user_event\":\n        this.sendEventPing(event_object);\n        break;\n    }\n  }\n\n  async sendEventPing(ping) {\n    delete ping.action;\n    ping.client_id = await this.telemetryClientId;\n    if (ping.value && typeof ping.value === \"object\") {\n      ping.value = JSON.stringify(ping.value);\n    }\n    this.sendStructuredIngestionEvent(\n      ping,\n      STRUCTURED_INGESTION_NAMESPACE_AS,\n      \"events\",\n      1\n    );\n  }\n\n  sendUTEvent(event_object, eventFunction) {\n    if (this.telemetryEnabled && this.eventTelemetryEnabled) {\n      eventFunction(event_object);\n    }\n  }\n\n  /**\n   * Generates an endpoint for Structured Ingestion telemetry pipeline. Note that\n   * Structured Ingestion requires a different endpoint for each ping. See more\n   * details about endpoint schema at:\n   * https://github.com/mozilla/gcp-ingestion/blob/master/docs/edge.md#postput-request\n   *\n   * @param {String} namespace Namespace of the ping, such as \"activity-stream\" or \"messaging-system\".\n   * @param {String} pingType  Type of the ping, such as \"impression-stats\".\n   * @param {String} version   Endpoint version for this ping type.\n   */\n  _generateStructuredIngestionEndpoint(namespace, pingType, version) {\n    const uuid = gUUIDGenerator.generateUUID().toString();\n    // Structured Ingestion does not support the UUID generated by gUUIDGenerator,\n    // because it contains leading and trailing braces. Need to trim them first.\n    const docID = uuid.slice(1, -1);\n    const extension = `${namespace}/${pingType}/${version}/${docID}`;\n    return `${this.structuredIngestionEndpointBase}/${extension}`;\n  }\n\n  sendStructuredIngestionEvent(eventObject, namespace, pingType, version) {\n    if (this.telemetryEnabled && this.structuredIngestionTelemetryEnabled) {\n      this.pingCentre.sendStructuredIngestionPing(\n        eventObject,\n        this._generateStructuredIngestionEndpoint(namespace, pingType, version),\n        { filter: ACTIVITY_STREAM_ID }\n      );\n    }\n  }\n\n  handleImpressionStats(action) {\n    const payload = this.createImpressionStats(\n      au.getPortIdOfSender(action),\n      action.data\n    );\n    this.sendStructuredIngestionEvent(\n      payload,\n      STRUCTURED_INGESTION_NAMESPACE_AS,\n      \"impression-stats\",\n      \"1\"\n    );\n  }\n\n  handleUserEvent(action) {\n    let userEvent = this.createUserEvent(action);\n    this.sendEvent(userEvent);\n    this.sendUTEvent(userEvent, this.utEvents.sendUserEvent);\n  }\n\n  async handleASRouterUserEvent(action) {\n    const { ping, pingType } = await this.createASRouterEvent(action);\n    if (!pingType) {\n      Cu.reportError(\"Unknown ping type for ASRouter telemetry\");\n      return;\n    }\n    this.sendStructuredIngestionEvent(\n      ping,\n      STRUCTURED_INGESTION_NAMESPACE_MS,\n      pingType,\n      \"1\"\n    );\n  }\n\n  handleUndesiredEvent(action) {\n    this.sendEvent(this.createUndesiredEvent(action));\n  }\n\n  handleTrailheadEnrollEvent(action) {\n    // Unlike `sendUTEvent`, we always send the event if AS's telemetry is enabled\n    // regardless of `this.eventTelemetryEnabled`.\n    if (this.telemetryEnabled) {\n      this.utEvents.sendTrailheadEnrollEvent(action.data);\n    }\n  }\n\n  async sendPageTakeoverData() {\n    if (this.telemetryEnabled) {\n      const value = {};\n      let newtabAffected = false;\n      let homeAffected = false;\n\n      // Check whether or not about:home and about:newtab are set to a custom URL.\n      // If so, classify them.\n      if (\n        Services.prefs.getBoolPref(\"browser.newtabpage.enabled\") &&\n        aboutNewTabService.overridden &&\n        !aboutNewTabService.newTabURL.startsWith(\"moz-extension://\")\n      ) {\n        value.newtab_url_category = await this._classifySite(\n          aboutNewTabService.newTabURL\n        );\n        newtabAffected = true;\n      }\n      // Check if the newtab page setting is controlled by an extension.\n      await ExtensionSettingsStore.initialize();\n      const newtabExtensionInfo = ExtensionSettingsStore.getSetting(\n        \"url_overrides\",\n        \"newTabURL\"\n      );\n      if (newtabExtensionInfo && newtabExtensionInfo.id) {\n        value.newtab_extension_id = newtabExtensionInfo.id;\n        newtabAffected = true;\n      }\n\n      const homePageURL = HomePage.get();\n      if (\n        ![\"about:home\", \"about:blank\"].includes(homePageURL) &&\n        !homePageURL.startsWith(\"moz-extension://\")\n      ) {\n        value.home_url_category = await this._classifySite(homePageURL);\n        homeAffected = true;\n      }\n      const homeExtensionInfo = ExtensionSettingsStore.getSetting(\n        \"prefs\",\n        \"homepage_override\"\n      );\n      if (homeExtensionInfo && homeExtensionInfo.id) {\n        value.home_extension_id = homeExtensionInfo.id;\n        homeAffected = true;\n      }\n\n      let page;\n      if (newtabAffected && homeAffected) {\n        page = \"both\";\n      } else if (newtabAffected) {\n        page = \"about:newtab\";\n      } else if (homeAffected) {\n        page = \"about:home\";\n      }\n\n      if (page) {\n        const event = Object.assign(this.createPing(), {\n          action: \"activity_stream_user_event\",\n          event: \"PAGE_TAKEOVER_DATA\",\n          value,\n          page,\n          session_id: \"n/a\",\n        });\n        this.sendEvent(event);\n      }\n    }\n  }\n\n  onAction(action) {\n    switch (action.type) {\n      case at.INIT:\n        this.init();\n        this.sendPageTakeoverData();\n        break;\n      case at.NEW_TAB_INIT:\n        this.handleNewTabInit(action);\n        break;\n      case at.NEW_TAB_UNLOAD:\n        this.endSession(au.getPortIdOfSender(action));\n        break;\n      case at.SAVE_SESSION_PERF_DATA:\n        this.saveSessionPerfData(au.getPortIdOfSender(action), action.data);\n        break;\n      case at.TELEMETRY_IMPRESSION_STATS:\n        this.handleImpressionStats(action);\n        break;\n      case at.DISCOVERY_STREAM_IMPRESSION_STATS:\n        this.handleDiscoveryStreamImpressionStats(\n          au.getPortIdOfSender(action),\n          action.data\n        );\n        break;\n      case at.DISCOVERY_STREAM_LOADED_CONTENT:\n        this.handleDiscoveryStreamLoadedContent(\n          au.getPortIdOfSender(action),\n          action.data\n        );\n        break;\n      case at.DISCOVERY_STREAM_SPOCS_FILL:\n        this.handleDiscoveryStreamSpocsFill(action.data);\n        break;\n      case at.TELEMETRY_UNDESIRED_EVENT:\n        this.handleUndesiredEvent(action);\n        break;\n      case at.TELEMETRY_USER_EVENT:\n        this.handleUserEvent(action);\n        break;\n      case at.AS_ROUTER_TELEMETRY_USER_EVENT:\n        this.handleASRouterUserEvent(action);\n        break;\n      case at.TELEMETRY_PERFORMANCE_EVENT:\n        this.sendEvent(this.createPerformanceEvent(action));\n        break;\n      case at.TRAILHEAD_ENROLL_EVENT:\n        this.handleTrailheadEnrollEvent(action);\n        break;\n      case at.UNINIT:\n        this.uninit();\n        break;\n    }\n  }\n\n  /**\n   * Handle impression stats actions from Discovery Stream. The data will be\n   * stored into the session.impressionSets object for the given port, so that\n   * it is sent to the server when the session ends.\n   *\n   * @note session.impressionSets will be keyed on `source` of the `data`,\n   * all the data will be appended to an array for the same source.\n   *\n   * @param {String} port  The session port with which this is associated\n   * @param {Object} data  The impression data structured as {source: \"SOURCE\", tiles: [{id: 123}]}\n   *\n   */\n  handleDiscoveryStreamImpressionStats(port, data) {\n    let session = this.sessions.get(port);\n\n    if (!session) {\n      throw new Error(\"Session does not exist.\");\n    }\n\n    const impressionSets = session.impressionSets || {};\n    const impressions = impressionSets[data.source] || [];\n    // The payload might contain other properties, we need `id`, `pos` and potentially `shim` here.\n    data.tiles.forEach(tile =>\n      impressions.push({\n        id: tile.id,\n        pos: tile.pos,\n        ...(tile.shim ? { shim: tile.shim } : {}),\n      })\n    );\n    impressionSets[data.source] = impressions;\n    session.impressionSets = impressionSets;\n  }\n\n  /**\n   * Handle loaded content actions from Discovery Stream. The data will be\n   * stored into the session.loadedContentSets object for the given port, so that\n   * it is sent to the server when the session ends.\n   *\n   * @note session.loadedContentSets will be keyed on `source` of the `data`,\n   * all the data will be appended to an array for the same source.\n   *\n   * @param {String} port  The session port with which this is associated\n   * @param {Object} data  The loaded content structured as {source: \"SOURCE\", tiles: [{id: 123}]}\n   *\n   */\n  handleDiscoveryStreamLoadedContent(port, data) {\n    let session = this.sessions.get(port);\n\n    if (!session) {\n      throw new Error(\"Session does not exist.\");\n    }\n\n    const loadedContentSets = session.loadedContentSets || {};\n    const loadedContents = loadedContentSets[data.source] || [];\n    // The payload might contain other properties, we need `id` and `pos` here.\n    data.tiles.forEach(tile =>\n      loadedContents.push({ id: tile.id, pos: tile.pos })\n    );\n    loadedContentSets[data.source] = loadedContents;\n    session.loadedContentSets = loadedContentSets;\n  }\n\n  /**\n   * Handl SPOCS Fill actions from Discovery Stream.\n   *\n   * @param {Object} data\n   *   The SPOCS Fill event structured as:\n   *   {\n   *     spoc_fills: [\n   *       {\n   *         id: 123,\n   *         displayed: 0,\n   *         reason: \"frequency_cap\",\n   *         full_recalc: 1\n   *        },\n   *        {\n   *          id: 124,\n   *          displayed: 1,\n   *          reason: \"n/a\",\n   *          full_recalc: 1\n   *        }\n   *      ]\n   *    }\n   */\n  handleDiscoveryStreamSpocsFill(data) {\n    const payload = this.createSpocsFillPing(data);\n    this.sendStructuredIngestionEvent(\n      payload,\n      STRUCTURED_INGESTION_NAMESPACE_AS,\n      \"spoc-fills\",\n      \"1\"\n    );\n  }\n\n  /**\n   * Take all enumerable members of the data object and merge them into\n   * the session.perf object for the given port, so that it is sent to the\n   * server when the session ends.  All members of the data object should\n   * be valid values of the perf object, as defined in pings.js and the\n   * data*.md documentation.\n   *\n   * @note Any existing keys with the same names already in the\n   * session perf object will be overwritten by values passed in here.\n   *\n   * @param {String} port  The session with which this is associated\n   * @param {Object} data  The perf data to be\n   */\n  saveSessionPerfData(port, data) {\n    // XXX should use try/catch and send a bad state indicator if this\n    // get blows up.\n    let session = this.sessions.get(port);\n\n    // XXX Partial workaround for #3118; avoids the worst incorrect associations\n    // of times with browsers, by associating the load trigger with the\n    // visibility event as the user is most likely associating the trigger to\n    // the tab just shown. This helps avoid associating with a preloaded\n    // browser as those don't get the event until shown. Better fix for more\n    // cases forthcoming.\n    //\n    // XXX the about:home check (and the corresponding test) should go away\n    // once the load_trigger stuff in addSession is refactored into\n    // setLoadTriggerInfo.\n    //\n    if (data.visibility_event_rcvd_ts && session.page !== \"about:home\") {\n      this.setLoadTriggerInfo(port);\n    }\n\n    let timestamp = data.topsites_first_painted_ts;\n\n    if (\n      timestamp &&\n      session.page === \"about:home\" &&\n      !HomePage.overridden &&\n      Services.prefs.getIntPref(\"browser.startup.page\") === 1\n    ) {\n      aboutNewTabService.maybeRecordTopsitesPainted(timestamp);\n    }\n\n    Object.assign(session.perf, data);\n  }\n\n  uninit() {\n    try {\n      Services.obs.removeObserver(\n        this.browserOpenNewtabStart,\n        \"browser-open-newtab-start\"\n      );\n      Services.obs.removeObserver(\n        this._addWindowListeners,\n        DOMWINDOW_OPENED_TOPIC\n      );\n    } catch (e) {\n      // Operation can fail when uninit is called before\n      // init has finished setting up the observer\n    }\n\n    // Only uninit if the getter has initialized it\n    if (Object.prototype.hasOwnProperty.call(this, \"pingCentre\")) {\n      this.pingCentre.uninit();\n    }\n    if (Object.prototype.hasOwnProperty.call(this, \"utEvents\")) {\n      this.utEvents.uninit();\n    }\n\n    // TODO: Send any unfinished sessions\n  }\n};\n\nconst EXPORTED_SYMBOLS = [\n  \"TelemetryFeed\",\n  \"USER_PREFS_ENCODING\",\n  \"PREF_IMPRESSION_ID\",\n  \"TELEMETRY_PREF\",\n  \"EVENTS_TELEMETRY_PREF\",\n  \"STRUCTURED_INGESTION_TELEMETRY_PREF\",\n  \"STRUCTURED_INGESTION_ENDPOINT_PREF\",\n];\n"
  },
  {
    "path": "lib/TippyTopProvider.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nconst { XPCOMUtils } = ChromeUtils.import(\n  \"resource://gre/modules/XPCOMUtils.jsm\"\n);\n\nXPCOMUtils.defineLazyGlobalGetters(this, [\"fetch\", \"URL\"]);\n\nconst TIPPYTOP_JSON_PATH =\n  \"resource://activity-stream/data/content/tippytop/top_sites.json\";\nconst TIPPYTOP_URL_PREFIX =\n  \"resource://activity-stream/data/content/tippytop/images/\";\n\nfunction getDomain(url) {\n  let domain;\n  try {\n    domain = new URL(url).hostname;\n  } catch (ex) {}\n  if (domain && domain.startsWith(\"www.\")) {\n    domain = domain.slice(4);\n  }\n  return domain;\n}\n\nthis.TippyTopProvider = class TippyTopProvider {\n  constructor() {\n    this._sitesByDomain = new Map();\n    this.initialized = false;\n  }\n\n  async init() {\n    // Load the Tippy Top sites from the json manifest.\n    try {\n      for (const site of await (await fetch(TIPPYTOP_JSON_PATH, {\n        credentials: \"omit\",\n      })).json()) {\n        // The tippy top manifest can have a url property (string) or a\n        // urls property (array of strings)\n        for (const url of site.url ? [site.url] : site.urls || []) {\n          this._sitesByDomain.set(getDomain(url), site);\n        }\n      }\n      this.initialized = true;\n    } catch (error) {\n      Cu.reportError(\"Failed to load tippy top manifest.\");\n    }\n  }\n\n  processSite(site) {\n    const tippyTop = this._sitesByDomain.get(getDomain(site.url));\n    if (tippyTop) {\n      site.tippyTopIcon = TIPPYTOP_URL_PREFIX + tippyTop.image_url;\n      site.backgroundColor = tippyTop.background_color;\n    }\n    return site;\n  }\n};\n\nconst EXPORTED_SYMBOLS = [\"TippyTopProvider\", \"getDomain\"];\n"
  },
  {
    "path": "lib/Tokenize.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\"use strict\";\n\n// Unicode specifies certain mnemonics for code pages and character classes.\n// They call them \"character properties\" https://en.wikipedia.org/wiki/Unicode_character_property .\n// These mnemonics are have been adopted by many regular expression libraries,\n// however the standard Javascript regexp system doesn't support unicode\n// character properties, so we have to define these ourself.\n//\n// Each of these sections contains the characters values / ranges for specific\n// character property: Whitespace, Symbol (S), Punctuation (P), Number (N),\n// Mark (M), and Letter (L).\nconst UNICODE_SPACE =\n  \"\\x20\\xA0\\u1680\\u2000-\\u200A\\u2028\\u2029\\u202F\\u205F\\u3000\";\nconst UNICODE_SYMBOL =\n  \"\\\\x24\\\\x2B\\x3C-\\x3E\\\\x5E\\x60\\\\x7C\\x7E\\xA2-\\xA6\\xA8\\xA9\\xAC\\xAE-\\xB1\\xB4\\xB8\\xD7\\xF7\\u02C2-\\u02C5\\u02D2-\\u02DF\\u02E5-\\u02EB\\u02ED\\u02EF-\\u02FF\\u0375\\u0384\\u0385\\u03F6\\u0482\\u058D-\\u058F\\u0606-\\u0608\\u060B\\u060E\\u060F\\u06DE\\u06E9\\u06FD\\u06FE\\u07F6\\u09F2\\u09F3\\u09FA\\u09FB\\u0AF1\\u0B70\\u0BF3-\\u0BFA\\u0C7F\\u0D4F\\u0D79\\u0E3F\\u0F01-\\u0F03\\u0F13\\u0F15-\\u0F17\\u0F1A-\\u0F1F\\u0F34\\u0F36\\u0F38\\u0FBE-\\u0FC5\\u0FC7-\\u0FCC\\u0FCE\\u0FCF\\u0FD5-\\u0FD8\\u109E\\u109F\\u1390-\\u1399\\u17DB\\u1940\\u19DE-\\u19FF\\u1B61-\\u1B6A\\u1B74-\\u1B7C\\u1FBD\\u1FBF-\\u1FC1\\u1FCD-\\u1FCF\\u1FDD-\\u1FDF\\u1FED-\\u1FEF\\u1FFD\\u1FFE\\u2044\\u2052\\u207A-\\u207C\\u208A-\\u208C\\u20A0-\\u20BE\\u2100\\u2101\\u2103-\\u2106\\u2108\\u2109\\u2114\\u2116-\\u2118\\u211E-\\u2123\\u2125\\u2127\\u2129\\u212E\\u213A\\u213B\\u2140-\\u2144\\u214A-\\u214D\\u214F\\u218A\\u218B\\u2190-\\u2307\\u230C-\\u2328\\u232B-\\u23FE\\u2400-\\u2426\\u2440-\\u244A\\u249C-\\u24E9\\u2500-\\u2767\\u2794-\\u27C4\\u27C7-\\u27E5\\u27F0-\\u2982\\u2999-\\u29D7\\u29DC-\\u29FB\\u29FE-\\u2B73\\u2B76-\\u2B95\\u2B98-\\u2BB9\\u2BBD-\\u2BC8\\u2BCA-\\u2BD1\\u2BEC-\\u2BEF\\u2CE5-\\u2CEA\\u2E80-\\u2E99\\u2E9B-\\u2EF3\\u2F00-\\u2FD5\\u2FF0-\\u2FFB\\u3004\\u3012\\u3013\\u3020\\u3036\\u3037\\u303E\\u303F\\u309B\\u309C\\u3190\\u3191\\u3196-\\u319F\\u31C0-\\u31E3\\u3200-\\u321E\\u322A-\\u3247\\u3250\\u3260-\\u327F\\u328A-\\u32B0\\u32C0-\\u32FE\\u3300-\\u33FF\\u4DC0-\\u4DFF\\uA490-\\uA4C6\\uA700-\\uA716\\uA720\\uA721\\uA789\\uA78A\\uA828-\\uA82B\\uA836-\\uA839\\uAA77-\\uAA79\\uAB5B\\uFB29\\uFBB2-\\uFBC1\\uFDFC\\uFDFD\\uFE62\\uFE64-\\uFE66\\uFE69\\uFF04\\uFF0B\\uFF1C-\\uFF1E\\uFF3E\\uFF40\\uFF5C\\uFF5E\\uFFE0-\\uFFE6\\uFFE8-\\uFFEE\\uFFFC\\uFFFD\";\nconst UNICODE_PUNCT =\n  \"\\x21-\\x23\\x25-\\\\x2A\\x2C-\\x2F\\x3A\\x3B\\\\x3F\\x40\\\\x5B-\\\\x5D\\x5F\\\\x7B\\x7D\\xA1\\xA7\\xAB\\xB6\\xB7\\xBB\\xBF\\u037E\\u0387\\u055A-\\u055F\\u0589\\u058A\\u05BE\\u05C0\\u05C3\\u05C6\\u05F3\\u05F4\\u0609\\u060A\\u060C\\u060D\\u061B\\u061E\\u061F\\u066A-\\u066D\\u06D4\\u0700-\\u070D\\u07F7-\\u07F9\\u0830-\\u083E\\u085E\\u0964\\u0965\\u0970\\u0AF0\\u0DF4\\u0E4F\\u0E5A\\u0E5B\\u0F04-\\u0F12\\u0F14\\u0F3A-\\u0F3D\\u0F85\\u0FD0-\\u0FD4\\u0FD9\\u0FDA\\u104A-\\u104F\\u10FB\\u1360-\\u1368\\u1400\\u166D\\u166E\\u169B\\u169C\\u16EB-\\u16ED\\u1735\\u1736\\u17D4-\\u17D6\\u17D8-\\u17DA\\u1800-\\u180A\\u1944\\u1945\\u1A1E\\u1A1F\\u1AA0-\\u1AA6\\u1AA8-\\u1AAD\\u1B5A-\\u1B60\\u1BFC-\\u1BFF\\u1C3B-\\u1C3F\\u1C7E\\u1C7F\\u1CC0-\\u1CC7\\u1CD3\\u2010-\\u2027\\u2030-\\u2043\\u2045-\\u2051\\u2053-\\u205E\\u207D\\u207E\\u208D\\u208E\\u2308-\\u230B\\u2329\\u232A\\u2768-\\u2775\\u27C5\\u27C6\\u27E6-\\u27EF\\u2983-\\u2998\\u29D8-\\u29DB\\u29FC\\u29FD\\u2CF9-\\u2CFC\\u2CFE\\u2CFF\\u2D70\\u2E00-\\u2E2E\\u2E30-\\u2E44\\u3001-\\u3003\\u3008-\\u3011\\u3014-\\u301F\\u3030\\u303D\\u30A0\\u30FB\\uA4FE\\uA4FF\\uA60D-\\uA60F\\uA673\\uA67E\\uA6F2-\\uA6F7\\uA874-\\uA877\\uA8CE\\uA8CF\\uA8F8-\\uA8FA\\uA8FC\\uA92E\\uA92F\\uA95F\\uA9C1-\\uA9CD\\uA9DE\\uA9DF\\uAA5C-\\uAA5F\\uAADE\\uAADF\\uAAF0\\uAAF1\\uABEB\\uFD3E\\uFD3F\\uFE10-\\uFE19\\uFE30-\\uFE52\\uFE54-\\uFE61\\uFE63\\uFE68\\uFE6A\\uFE6B\\uFF01-\\uFF03\\uFF05-\\uFF0A\\uFF0C-\\uFF0F\\uFF1A\\uFF1B\\uFF1F\\uFF20\\uFF3B-\\uFF3D\\uFF3F\\uFF5B\\uFF5D\\uFF5F-\\uFF65\";\n\nconst UNICODE_NUMBER =\n  \"0-9\\xB2\\xB3\\xB9\\xBC-\\xBE\\u0660-\\u0669\\u06F0-\\u06F9\\u07C0-\\u07C9\\u0966-\\u096F\\u09E6-\\u09EF\\u09F4-\\u09F9\\u0A66-\\u0A6F\\u0AE6-\\u0AEF\\u0B66-\\u0B6F\\u0B72-\\u0B77\\u0BE6-\\u0BF2\\u0C66-\\u0C6F\\u0C78-\\u0C7E\\u0CE6-\\u0CEF\\u0D58-\\u0D5E\\u0D66-\\u0D78\\u0DE6-\\u0DEF\\u0E50-\\u0E59\\u0ED0-\\u0ED9\\u0F20-\\u0F33\\u1040-\\u1049\\u1090-\\u1099\\u1369-\\u137C\\u16EE-\\u16F0\\u17E0-\\u17E9\\u17F0-\\u17F9\\u1810-\\u1819\\u1946-\\u194F\\u19D0-\\u19DA\\u1A80-\\u1A89\\u1A90-\\u1A99\\u1B50-\\u1B59\\u1BB0-\\u1BB9\\u1C40-\\u1C49\\u1C50-\\u1C59\\u2070\\u2074-\\u2079\\u2080-\\u2089\\u2150-\\u2182\\u2185-\\u2189\\u2460-\\u249B\\u24EA-\\u24FF\\u2776-\\u2793\\u2CFD\\u3007\\u3021-\\u3029\\u3038-\\u303A\\u3192-\\u3195\\u3220-\\u3229\\u3248-\\u324F\\u3251-\\u325F\\u3280-\\u3289\\u32B1-\\u32BF\\uA620-\\uA629\\uA6E6-\\uA6EF\\uA830-\\uA835\\uA8D0-\\uA8D9\\uA900-\\uA909\\uA9D0-\\uA9D9\\uA9F0-\\uA9F9\\uAA50-\\uAA59\\uABF0-\\uABF9\\uFF10-\\uFF19\";\nconst UNICODE_MARK =\n  \"\\u0300-\\u036F\\u0483-\\u0489\\u0591-\\u05BD\\u05BF\\u05C1\\u05C2\\u05C4\\u05C5\\u05C7\\u0610-\\u061A\\u064B-\\u065F\\u0670\\u06D6-\\u06DC\\u06DF-\\u06E4\\u06E7\\u06E8\\u06EA-\\u06ED\\u0711\\u0730-\\u074A\\u07A6-\\u07B0\\u07EB-\\u07F3\\u0816-\\u0819\\u081B-\\u0823\\u0825-\\u0827\\u0829-\\u082D\\u0859-\\u085B\\u08D4-\\u08E1\\u08E3-\\u0903\\u093A-\\u093C\\u093E-\\u094F\\u0951-\\u0957\\u0962\\u0963\\u0981-\\u0983\\u09BC\\u09BE-\\u09C4\\u09C7\\u09C8\\u09CB-\\u09CD\\u09D7\\u09E2\\u09E3\\u0A01-\\u0A03\\u0A3C\\u0A3E-\\u0A42\\u0A47\\u0A48\\u0A4B-\\u0A4D\\u0A51\\u0A70\\u0A71\\u0A75\\u0A81-\\u0A83\\u0ABC\\u0ABE-\\u0AC5\\u0AC7-\\u0AC9\\u0ACB-\\u0ACD\\u0AE2\\u0AE3\\u0B01-\\u0B03\\u0B3C\\u0B3E-\\u0B44\\u0B47\\u0B48\\u0B4B-\\u0B4D\\u0B56\\u0B57\\u0B62\\u0B63\\u0B82\\u0BBE-\\u0BC2\\u0BC6-\\u0BC8\\u0BCA-\\u0BCD\\u0BD7\\u0C00-\\u0C03\\u0C3E-\\u0C44\\u0C46-\\u0C48\\u0C4A-\\u0C4D\\u0C55\\u0C56\\u0C62\\u0C63\\u0C81-\\u0C83\\u0CBC\\u0CBE-\\u0CC4\\u0CC6-\\u0CC8\\u0CCA-\\u0CCD\\u0CD5\\u0CD6\\u0CE2\\u0CE3\\u0D01-\\u0D03\\u0D3E-\\u0D44\\u0D46-\\u0D48\\u0D4A-\\u0D4D\\u0D57\\u0D62\\u0D63\\u0D82\\u0D83\\u0DCA\\u0DCF-\\u0DD4\\u0DD6\\u0DD8-\\u0DDF\\u0DF2\\u0DF3\\u0E31\\u0E34-\\u0E3A\\u0E47-\\u0E4E\\u0EB1\\u0EB4-\\u0EB9\\u0EBB\\u0EBC\\u0EC8-\\u0ECD\\u0F18\\u0F19\\u0F35\\u0F37\\u0F39\\u0F3E\\u0F3F\\u0F71-\\u0F84\\u0F86\\u0F87\\u0F8D-\\u0F97\\u0F99-\\u0FBC\\u0FC6\\u102B-\\u103E\\u1056-\\u1059\\u105E-\\u1060\\u1062-\\u1064\\u1067-\\u106D\\u1071-\\u1074\\u1082-\\u108D\\u108F\\u109A-\\u109D\\u135D-\\u135F\\u1712-\\u1714\\u1732-\\u1734\\u1752\\u1753\\u1772\\u1773\\u17B4-\\u17D3\\u17DD\\u180B-\\u180D\\u1885\\u1886\\u18A9\\u1920-\\u192B\\u1930-\\u193B\\u1A17-\\u1A1B\\u1A55-\\u1A5E\\u1A60-\\u1A7C\\u1A7F\\u1AB0-\\u1ABE\\u1B00-\\u1B04\\u1B34-\\u1B44\\u1B6B-\\u1B73\\u1B80-\\u1B82\\u1BA1-\\u1BAD\\u1BE6-\\u1BF3\\u1C24-\\u1C37\\u1CD0-\\u1CD2\\u1CD4-\\u1CE8\\u1CED\\u1CF2-\\u1CF4\\u1CF8\\u1CF9\\u1DC0-\\u1DF5\\u1DFB-\\u1DFF\\u20D0-\\u20F0\\u2CEF-\\u2CF1\\u2D7F\\u2DE0-\\u2DFF\\u302A-\\u302F\\u3099\\u309A\\uA66F-\\uA672\\uA674-\\uA67D\\uA69E\\uA69F\\uA6F0\\uA6F1\\uA802\\uA806\\uA80B\\uA823-\\uA827\\uA880\\uA881\\uA8B4-\\uA8C5\\uA8E0-\\uA8F1\\uA926-\\uA92D\\uA947-\\uA953\\uA980-\\uA983\\uA9B3-\\uA9C0\\uA9E5\\uAA29-\\uAA36\\uAA43\\uAA4C\\uAA4D\\uAA7B-\\uAA7D\\uAAB0\\uAAB2-\\uAAB4\\uAAB7\\uAAB8\\uAABE\\uAABF\\uAAC1\\uAAEB-\\uAAEF\\uAAF5\\uAAF6\\uABE3-\\uABEA\\uABEC\\uABED\\uFB1E\\uFE00-\\uFE0F\\uFE20-\\uFE2F\";\nconst UNICODE_LETTER =\n  \"A-Za-z\\xAA\\xB5\\xBA\\xC0-\\xD6\\xD8-\\xF6\\xF8-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE\\u0370-\\u0374\\u0376\\u0377\\u037A-\\u037D\\u037F\\u0386\\u0388-\\u038A\\u038C\\u038E-\\u03A1\\u03A3-\\u03F5\\u03F7-\\u0481\\u048A-\\u052F\\u0531-\\u0556\\u0559\\u0561-\\u0587\\u05D0-\\u05EA\\u05F0-\\u05F2\\u0620-\\u064A\\u066E\\u066F\\u0671-\\u06D3\\u06D5\\u06E5\\u06E6\\u06EE\\u06EF\\u06FA-\\u06FC\\u06FF\\u0710\\u0712-\\u072F\\u074D-\\u07A5\\u07B1\\u07CA-\\u07EA\\u07F4\\u07F5\\u07FA\\u0800-\\u0815\\u081A\\u0824\\u0828\\u0840-\\u0858\\u08A0-\\u08B4\\u08B6-\\u08BD\\u0904-\\u0939\\u093D\\u0950\\u0958-\\u0961\\u0971-\\u0980\\u0985-\\u098C\\u098F\\u0990\\u0993-\\u09A8\\u09AA-\\u09B0\\u09B2\\u09B6-\\u09B9\\u09BD\\u09CE\\u09DC\\u09DD\\u09DF-\\u09E1\\u09F0\\u09F1\\u0A05-\\u0A0A\\u0A0F\\u0A10\\u0A13-\\u0A28\\u0A2A-\\u0A30\\u0A32\\u0A33\\u0A35\\u0A36\\u0A38\\u0A39\\u0A59-\\u0A5C\\u0A5E\\u0A72-\\u0A74\\u0A85-\\u0A8D\\u0A8F-\\u0A91\\u0A93-\\u0AA8\\u0AAA-\\u0AB0\\u0AB2\\u0AB3\\u0AB5-\\u0AB9\\u0ABD\\u0AD0\\u0AE0\\u0AE1\\u0AF9\\u0B05-\\u0B0C\\u0B0F\\u0B10\\u0B13-\\u0B28\\u0B2A-\\u0B30\\u0B32\\u0B33\\u0B35-\\u0B39\\u0B3D\\u0B5C\\u0B5D\\u0B5F-\\u0B61\\u0B71\\u0B83\\u0B85-\\u0B8A\\u0B8E-\\u0B90\\u0B92-\\u0B95\\u0B99\\u0B9A\\u0B9C\\u0B9E\\u0B9F\\u0BA3\\u0BA4\\u0BA8-\\u0BAA\\u0BAE-\\u0BB9\\u0BD0\\u0C05-\\u0C0C\\u0C0E-\\u0C10\\u0C12-\\u0C28\\u0C2A-\\u0C39\\u0C3D\\u0C58-\\u0C5A\\u0C60\\u0C61\\u0C80\\u0C85-\\u0C8C\\u0C8E-\\u0C90\\u0C92-\\u0CA8\\u0CAA-\\u0CB3\\u0CB5-\\u0CB9\\u0CBD\\u0CDE\\u0CE0\\u0CE1\\u0CF1\\u0CF2\\u0D05-\\u0D0C\\u0D0E-\\u0D10\\u0D12-\\u0D3A\\u0D3D\\u0D4E\\u0D54-\\u0D56\\u0D5F-\\u0D61\\u0D7A-\\u0D7F\\u0D85-\\u0D96\\u0D9A-\\u0DB1\\u0DB3-\\u0DBB\\u0DBD\\u0DC0-\\u0DC6\\u0E01-\\u0E30\\u0E32\\u0E33\\u0E40-\\u0E46\\u0E81\\u0E82\\u0E84\\u0E87\\u0E88\\u0E8A\\u0E8D\\u0E94-\\u0E97\\u0E99-\\u0E9F\\u0EA1-\\u0EA3\\u0EA5\\u0EA7\\u0EAA\\u0EAB\\u0EAD-\\u0EB0\\u0EB2\\u0EB3\\u0EBD\\u0EC0-\\u0EC4\\u0EC6\\u0EDC-\\u0EDF\\u0F00\\u0F40-\\u0F47\\u0F49-\\u0F6C\\u0F88-\\u0F8C\\u1000-\\u102A\\u103F\\u1050-\\u1055\\u105A-\\u105D\\u1061\\u1065\\u1066\\u106E-\\u1070\\u1075-\\u1081\\u108E\\u10A0-\\u10C5\\u10C7\\u10CD\\u10D0-\\u10FA\\u10FC-\\u1248\\u124A-\\u124D\\u1250-\\u1256\\u1258\\u125A-\\u125D\\u1260-\\u1288\\u128A-\\u128D\\u1290-\\u12B0\\u12B2-\\u12B5\\u12B8-\\u12BE\\u12C0\\u12C2-\\u12C5\\u12C8-\\u12D6\\u12D8-\\u1310\\u1312-\\u1315\\u1318-\\u135A\\u1380-\\u138F\\u13A0-\\u13F5\\u13F8-\\u13FD\\u1401-\\u166C\\u166F-\\u167F\\u1681-\\u169A\\u16A0-\\u16EA\\u16F1-\\u16F8\\u1700-\\u170C\\u170E-\\u1711\\u1720-\\u1731\\u1740-\\u1751\\u1760-\\u176C\\u176E-\\u1770\\u1780-\\u17B3\\u17D7\\u17DC\\u1820-\\u1877\\u1880-\\u1884\\u1887-\\u18A8\\u18AA\\u18B0-\\u18F5\\u1900-\\u191E\\u1950-\\u196D\\u1970-\\u1974\\u1980-\\u19AB\\u19B0-\\u19C9\\u1A00-\\u1A16\\u1A20-\\u1A54\\u1AA7\\u1B05-\\u1B33\\u1B45-\\u1B4B\\u1B83-\\u1BA0\\u1BAE\\u1BAF\\u1BBA-\\u1BE5\\u1C00-\\u1C23\\u1C4D-\\u1C4F\\u1C5A-\\u1C7D\\u1C80-\\u1C88\\u1CE9-\\u1CEC\\u1CEE-\\u1CF1\\u1CF5\\u1CF6\\u1D00-\\u1DBF\\u1E00-\\u1F15\\u1F18-\\u1F1D\\u1F20-\\u1F45\\u1F48-\\u1F4D\\u1F50-\\u1F57\\u1F59\\u1F5B\\u1F5D\\u1F5F-\\u1F7D\\u1F80-\\u1FB4\\u1FB6-\\u1FBC\\u1FBE\\u1FC2-\\u1FC4\\u1FC6-\\u1FCC\\u1FD0-\\u1FD3\\u1FD6-\\u1FDB\\u1FE0-\\u1FEC\\u1FF2-\\u1FF4\\u1FF6-\\u1FFC\\u2071\\u207F\\u2090-\\u209C\\u2102\\u2107\\u210A-\\u2113\\u2115\\u2119-\\u211D\\u2124\\u2126\\u2128\\u212A-\\u212D\\u212F-\\u2139\\u213C-\\u213F\\u2145-\\u2149\\u214E\\u2183\\u2184\\u2C00-\\u2C2E\\u2C30-\\u2C5E\\u2C60-\\u2CE4\\u2CEB-\\u2CEE\\u2CF2\\u2CF3\\u2D00-\\u2D25\\u2D27\\u2D2D\\u2D30-\\u2D67\\u2D6F\\u2D80-\\u2D96\\u2DA0-\\u2DA6\\u2DA8-\\u2DAE\\u2DB0-\\u2DB6\\u2DB8-\\u2DBE\\u2DC0-\\u2DC6\\u2DC8-\\u2DCE\\u2DD0-\\u2DD6\\u2DD8-\\u2DDE\\u2E2F\\u3005\\u3006\\u3031-\\u3035\\u303B\\u303C\\u3041-\\u3096\\u309D-\\u309F\\u30A1-\\u30FA\\u30FC-\\u30FF\\u3105-\\u312D\\u3131-\\u318E\\u31A0-\\u31BA\\u31F0-\\u31FF\\u3400-\\u4DB5\\u4E00-\\u9FD5\\uA000-\\uA48C\\uA4D0-\\uA4FD\\uA500-\\uA60C\\uA610-\\uA61F\\uA62A\\uA62B\\uA640-\\uA66E\\uA67F-\\uA69D\\uA6A0-\\uA6E5\\uA717-\\uA71F\\uA722-\\uA788\\uA78B-\\uA7AE\\uA7B0-\\uA7B7\\uA7F7-\\uA801\\uA803-\\uA805\\uA807-\\uA80A\\uA80C-\\uA822\\uA840-\\uA873\\uA882-\\uA8B3\\uA8F2-\\uA8F7\\uA8FB\\uA8FD\\uA90A-\\uA925\\uA930-\\uA946\\uA960-\\uA97C\\uA984-\\uA9B2\\uA9CF\\uA9E0-\\uA9E4\\uA9E6-\\uA9EF\\uA9FA-\\uA9FE\\uAA00-\\uAA28\\uAA40-\\uAA42\\uAA44-\\uAA4B\\uAA60-\\uAA76\\uAA7A\\uAA7E-\\uAAAF\\uAAB1\\uAAB5\\uAAB6\\uAAB9-\\uAABD\\uAAC0\\uAAC2\\uAADB-\\uAADD\\uAAE0-\\uAAEA\\uAAF2-\\uAAF4\\uAB01-\\uAB06\\uAB09-\\uAB0E\\uAB11-\\uAB16\\uAB20-\\uAB26\\uAB28-\\uAB2E\\uAB30-\\uAB5A\\uAB5C-\\uAB65\\uAB70-\\uABE2\\uAC00-\\uD7A3\\uD7B0-\\uD7C6\\uD7CB-\\uD7FB\\uF900-\\uFA6D\\uFA70-\\uFAD9\\uFB00-\\uFB06\\uFB13-\\uFB17\\uFB1D\\uFB1F-\\uFB28\\uFB2A-\\uFB36\\uFB38-\\uFB3C\\uFB3E\\uFB40\\uFB41\\uFB43\\uFB44\\uFB46-\\uFBB1\\uFBD3-\\uFD3D\\uFD50-\\uFD8F\\uFD92-\\uFDC7\\uFDF0-\\uFDFB\\uFE70-\\uFE74\\uFE76-\\uFEFC\\uFF21-\\uFF3A\\uFF41-\\uFF5A\\uFF66-\\uFFBE\\uFFC2-\\uFFC7\\uFFCA-\\uFFCF\\uFFD2-\\uFFD7\\uFFDA-\\uFFDC\";\n\nconst REGEXP_SPLITS = new RegExp(\n  `[${UNICODE_SPACE}${UNICODE_SYMBOL}${UNICODE_PUNCT}]+`\n);\n// Match all token characters, so okay for regex to split multiple code points\n// eslint-disable-next-line no-misleading-character-class\nconst REGEXP_ALPHANUMS = new RegExp(\n  `^[${UNICODE_NUMBER}${UNICODE_MARK}${UNICODE_LETTER}]+$`\n);\n\n/**\n * Downcases the text, and splits it into consecutive alphanumeric characters.\n * This is locale aware, and so will not strip accents. This uses \"word\n * breaks\", and os is not appropriate for languages without them\n * (e.g. Chinese).\n */\nfunction tokenize(text) {\n  return text\n    .toLocaleLowerCase()\n    .split(REGEXP_SPLITS)\n    .filter(tok => tok.match(REGEXP_ALPHANUMS));\n}\n\n/**\n * Converts a sequence of tokens into an L2 normed TF-IDF. Any terms that are\n * not preindexed (i.e. does have a computed inverse document frequency) will\n * be dropped.\n */\nfunction toksToTfIdfVector(tokens, vocab_idfs) {\n  let tfidfs = {};\n\n  // calcualte the term frequencies\n  for (let tok of tokens) {\n    if (!(tok in vocab_idfs)) {\n      continue;\n    }\n    if (!(tok in tfidfs)) {\n      tfidfs[tok] = [vocab_idfs[tok][0], 1];\n    } else {\n      tfidfs[tok][1]++;\n    }\n  }\n\n  // now multiply by the log inverse document frequencies, then take\n  // the L2 norm of this.\n  let l2Norm = 0.0;\n  Object.keys(tfidfs).forEach(tok => {\n    tfidfs[tok][1] *= vocab_idfs[tok][1];\n    l2Norm += tfidfs[tok][1] * tfidfs[tok][1];\n  });\n  l2Norm = Math.sqrt(l2Norm);\n  Object.keys(tfidfs).forEach(tok => {\n    tfidfs[tok][1] /= l2Norm;\n  });\n\n  return tfidfs;\n}\n\nconst EXPORTED_SYMBOLS = [\"tokenize\", \"toksToTfIdfVector\"];\n"
  },
  {
    "path": "lib/ToolbarBadgeHub.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\"use strict\";\n\nconst { XPCOMUtils } = ChromeUtils.import(\n  \"resource://gre/modules/XPCOMUtils.jsm\"\n);\n\nXPCOMUtils.defineLazyModuleGetters(this, {\n  EveryWindow: \"resource:///modules/EveryWindow.jsm\",\n  ToolbarPanelHub: \"resource://activity-stream/lib/ToolbarPanelHub.jsm\",\n  Services: \"resource://gre/modules/Services.jsm\",\n  PrivateBrowsingUtils: \"resource://gre/modules/PrivateBrowsingUtils.jsm\",\n});\n\nconst {\n  setInterval,\n  clearInterval,\n  requestIdleCallback,\n  setTimeout,\n  clearTimeout,\n} = ChromeUtils.import(\"resource://gre/modules/Timer.jsm\");\n\n// Frequency at which to check for new messages\nconst SYSTEM_TICK_INTERVAL = 5 * 60 * 1000;\nlet notificationsByWindow = new WeakMap();\n\nclass _ToolbarBadgeHub {\n  constructor() {\n    this.id = \"toolbar-badge-hub\";\n    this.state = null;\n    this.prefs = {\n      WHATSNEW_TOOLBAR_PANEL: \"browser.messaging-system.whatsNewPanel.enabled\",\n      HOMEPAGE_OVERRIDE_PREF: \"browser.startup.homepage_override.once\",\n    };\n    this.removeAllNotifications = this.removeAllNotifications.bind(this);\n    this.removeToolbarNotification = this.removeToolbarNotification.bind(this);\n    this.addToolbarNotification = this.addToolbarNotification.bind(this);\n    this.registerBadgeToAllWindows = this.registerBadgeToAllWindows.bind(this);\n    this._sendTelemetry = this._sendTelemetry.bind(this);\n    this.sendUserEventTelemetry = this.sendUserEventTelemetry.bind(this);\n    this.checkHomepageOverridePref = this.checkHomepageOverridePref.bind(this);\n\n    this._handleMessageRequest = null;\n    this._addImpression = null;\n    this._blockMessageById = null;\n    this._dispatch = null;\n  }\n\n  async init(\n    waitForInitialized,\n    {\n      handleMessageRequest,\n      addImpression,\n      blockMessageById,\n      unblockMessageById,\n      dispatch,\n    }\n  ) {\n    this._handleMessageRequest = handleMessageRequest;\n    this._blockMessageById = blockMessageById;\n    this._unblockMessageById = unblockMessageById;\n    this._addImpression = addImpression;\n    this._dispatch = dispatch;\n    // Need to wait for ASRouter to initialize before trying to fetch messages\n    await waitForInitialized;\n    this.messageRequest({\n      triggerId: \"toolbarBadgeUpdate\",\n      template: \"toolbar_badge\",\n    });\n    // Listen for pref changes that could trigger new badges\n    Services.prefs.addObserver(this.prefs.WHATSNEW_TOOLBAR_PANEL, this);\n    const _intervalId = setInterval(\n      () => this.checkHomepageOverridePref(),\n      SYSTEM_TICK_INTERVAL\n    );\n    this.state = { _intervalId };\n  }\n\n  /**\n   * Pref is set via Remote Settings message. We want to continously\n   * monitor new messages that come in to ensure the one with the\n   * highest priority is set.\n   */\n  checkHomepageOverridePref() {\n    const prefValue = Services.prefs.getStringPref(\n      this.prefs.HOMEPAGE_OVERRIDE_PREF,\n      \"\"\n    );\n    if (prefValue) {\n      // If the pref is set it means the user has not yet seen this message.\n      // We clear the pref value and re-evaluate all possible messages to ensure\n      // we don't have a higher priority message to show.\n      Services.prefs.clearUserPref(this.prefs.HOMEPAGE_OVERRIDE_PREF);\n      let message_id;\n      try {\n        message_id = JSON.parse(prefValue).message_id;\n      } catch (e) {}\n      if (message_id) {\n        this._unblockMessageById(message_id);\n      }\n    }\n\n    this.messageRequest({\n      triggerId: \"momentsUpdate\",\n      template: \"update_action\",\n    });\n  }\n\n  observe(aSubject, aTopic, aPrefName) {\n    switch (aPrefName) {\n      case this.prefs.WHATSNEW_TOOLBAR_PANEL:\n        this.messageRequest({\n          triggerId: \"toolbarBadgeUpdate\",\n          template: \"toolbar_badge\",\n        });\n        break;\n    }\n  }\n\n  maybeInsertFTL(win) {\n    win.MozXULElement.insertFTLIfNeeded(\"browser/newtab/asrouter.ftl\");\n  }\n\n  executeAction({ id, data, message_id }) {\n    switch (id) {\n      case \"show-whatsnew-button\":\n        ToolbarPanelHub.enableToolbarButton();\n        ToolbarPanelHub.enableAppmenuButton();\n        break;\n      case \"moments-wnp\":\n        const { url, expireDelta } = data;\n        let { expire } = data;\n        if (!expire) {\n          expire = this.getExpirationDate(expireDelta);\n        }\n        Services.prefs.setStringPref(\n          this.prefs.HOMEPAGE_OVERRIDE_PREF,\n          JSON.stringify({ message_id, url, expire })\n        );\n        // Block immediately after taking the action\n        this._blockMessageById(message_id);\n        break;\n    }\n  }\n\n  /**\n   * If we don't have `expire` defined with the message it could be because\n   * it depends on user dependent parameters. Since the message matched\n   * targeting we calculate `expire` based on the current timestamp and the\n   * `expireDelta` which defines for how long it should be available.\n   * @param expireDelta {number} - Offset in milliseconds from the current date\n   */\n  getExpirationDate(expireDelta) {\n    return Date.now() + expireDelta;\n  }\n\n  _clearBadgeTimeout() {\n    if (this.state.showBadgeTimeoutId) {\n      clearTimeout(this.state.showBadgeTimeoutId);\n    }\n  }\n\n  removeAllNotifications(event) {\n    if (event) {\n      // ignore right clicks\n      if (\n        (event.type === \"mousedown\" || event.type === \"click\") &&\n        event.button !== 0\n      ) {\n        return;\n      }\n      // ignore keyboard access that is not one of the usual accessor keys\n      if (\n        event.type === \"keypress\" &&\n        event.key !== \" \" &&\n        event.key !== \"Enter\"\n      ) {\n        return;\n      }\n\n      event.target.removeEventListener(\n        \"mousedown\",\n        this.removeAllNotifications\n      );\n      event.target.removeEventListener(\"keypress\", this.removeAllNotifications);\n      // If we have an event it means the user interacted with the badge\n      // we should send telemetry\n      if (this.state.notification) {\n        this.sendUserEventTelemetry(\"CLICK\", this.state.notification);\n      }\n    }\n    // Will call uninit on every window\n    EveryWindow.unregisterCallback(this.id);\n    if (this.state.notification) {\n      this._blockMessageById(this.state.notification.id);\n    }\n    this._clearBadgeTimeout();\n    this.state = {};\n  }\n\n  removeToolbarNotification(toolbarButton) {\n    // Remove it from the element that displays the badge\n    toolbarButton\n      .querySelector(\".toolbarbutton-badge\")\n      .classList.remove(\"feature-callout\");\n    toolbarButton.removeAttribute(\"badged\");\n    // Remove id used for for aria-label badge description\n    const notificationDescription = toolbarButton.querySelector(\n      \"#toolbarbutton-notification-description\"\n    );\n    if (notificationDescription) {\n      notificationDescription.remove();\n      toolbarButton.removeAttribute(\"aria-labelledby\");\n      toolbarButton.removeAttribute(\"aria-describedby\");\n    }\n  }\n\n  addToolbarNotification(win, message) {\n    const document = win.browser.ownerDocument;\n    if (message.content.action) {\n      this.executeAction({ ...message.content.action, message_id: message.id });\n    }\n    let toolbarbutton = document.getElementById(message.content.target);\n    if (toolbarbutton) {\n      const badge = toolbarbutton.querySelector(\".toolbarbutton-badge\");\n      badge.classList.add(\"feature-callout\");\n      toolbarbutton.setAttribute(\"badged\", true);\n      // If we have additional aria-label information for the notification\n      // we add this content to the hidden `toolbarbutton-text` node.\n      // We then use `aria-labelledby` to link this description to the button\n      // that received the notification badge.\n      if (message.content.badgeDescription) {\n        // Insert strings as soon as we know we're showing them\n        this.maybeInsertFTL(win);\n        toolbarbutton.setAttribute(\n          \"aria-labelledby\",\n          `toolbarbutton-notification-description ${message.content.target}`\n        );\n        // Because tooltiptext is different to the label, it gets duplicated as\n        // the description. Setting `describedby` to the same value as\n        // `labelledby` will be detected by the a11y code and the description\n        // will be removed.\n        toolbarbutton.setAttribute(\n          \"aria-describedby\",\n          `toolbarbutton-notification-description ${message.content.target}`\n        );\n        const descriptionEl = document.createElement(\"span\");\n        descriptionEl.setAttribute(\n          \"id\",\n          \"toolbarbutton-notification-description\"\n        );\n        descriptionEl.setAttribute(\"hidden\", true);\n        document.l10n.setAttributes(\n          descriptionEl,\n          message.content.badgeDescription.string_id\n        );\n        toolbarbutton.appendChild(descriptionEl);\n      }\n      // `mousedown` event required because of the `onmousedown` defined on\n      // the button that prevents `click` events from firing\n      toolbarbutton.addEventListener(\"mousedown\", this.removeAllNotifications);\n      // `keypress` event required for keyboard accessibility\n      toolbarbutton.addEventListener(\"keypress\", this.removeAllNotifications);\n      this.state = { notification: { id: message.id } };\n\n      // Impression should be added when the badge becomes visible\n      this._addImpression(message);\n      // Send a telemetry ping when adding the notification badge\n      this.sendUserEventTelemetry(\"IMPRESSION\", message);\n\n      return toolbarbutton;\n    }\n\n    return null;\n  }\n\n  registerBadgeToAllWindows(message) {\n    if (message.template === \"update_action\") {\n      this.executeAction({ ...message.content.action, message_id: message.id });\n      // No badge to set only an action to execute\n      return;\n    }\n\n    EveryWindow.registerCallback(\n      this.id,\n      win => {\n        if (notificationsByWindow.has(win)) {\n          // nothing to do\n          return;\n        }\n        const el = this.addToolbarNotification(win, message);\n        notificationsByWindow.set(win, el);\n      },\n      win => {\n        const el = notificationsByWindow.get(win);\n        if (el) {\n          this.removeToolbarNotification(el);\n        }\n        notificationsByWindow.delete(win);\n      }\n    );\n  }\n\n  registerBadgeNotificationListener(message, options = {}) {\n    // We need to clear any existing notifications and only show\n    // the one set by devtools\n    if (options.force) {\n      this.removeAllNotifications();\n      // When debugging immediately show the badge\n      this.registerBadgeToAllWindows(message);\n      return;\n    }\n\n    if (message.content.delay) {\n      this.state.showBadgeTimeoutId = setTimeout(() => {\n        requestIdleCallback(() => this.registerBadgeToAllWindows(message));\n      }, message.content.delay);\n    } else {\n      this.registerBadgeToAllWindows(message);\n    }\n  }\n\n  async messageRequest({ triggerId, template }) {\n    const message = await this._handleMessageRequest({\n      triggerId,\n      template,\n    });\n    if (message) {\n      this.registerBadgeNotificationListener(message);\n    }\n  }\n\n  _sendTelemetry(ping) {\n    this._dispatch({\n      type: \"TOOLBAR_BADGE_TELEMETRY\",\n      data: { action: \"cfr_user_event\", source: \"CFR\", ...ping },\n    });\n  }\n\n  sendUserEventTelemetry(event, message) {\n    const win = Services.wm.getMostRecentWindow(\"navigator:browser\");\n    // Only send pings for non private browsing windows\n    if (\n      win &&\n      !PrivateBrowsingUtils.isBrowserPrivate(\n        win.ownerGlobal.gBrowser.selectedBrowser\n      )\n    ) {\n      this._sendTelemetry({\n        message_id: message.id,\n        bucket_id: message.id,\n        event,\n      });\n    }\n  }\n\n  uninit() {\n    this._clearBadgeTimeout();\n    clearInterval(this.state._intervalId);\n    this.state = null;\n    notificationsByWindow = new WeakMap();\n    Services.prefs.removeObserver(this.prefs.WHATSNEW_TOOLBAR_PANEL, this);\n  }\n}\n\nthis._ToolbarBadgeHub = _ToolbarBadgeHub;\n\n/**\n * ToolbarBadgeHub - singleton instance of _ToolbarBadgeHub that can initiate\n * message requests and render messages.\n */\nthis.ToolbarBadgeHub = new _ToolbarBadgeHub();\n\nconst EXPORTED_SYMBOLS = [\"ToolbarBadgeHub\", \"_ToolbarBadgeHub\"];\n"
  },
  {
    "path": "lib/ToolbarPanelHub.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\"use strict\";\n\nconst { XPCOMUtils } = ChromeUtils.import(\n  \"resource://gre/modules/XPCOMUtils.jsm\"\n);\nXPCOMUtils.defineLazyModuleGetters(this, {\n  Services: \"resource://gre/modules/Services.jsm\",\n  EveryWindow: \"resource:///modules/EveryWindow.jsm\",\n  PrivateBrowsingUtils: \"resource://gre/modules/PrivateBrowsingUtils.jsm\",\n  RemoteL10n: \"resource://activity-stream/lib/RemoteL10n.jsm\",\n});\nXPCOMUtils.defineLazyServiceGetter(\n  this,\n  \"TrackingDBService\",\n  \"@mozilla.org/tracking-db-service;1\",\n  \"nsITrackingDBService\"\n);\n\nconst idToTextMap = new Map([\n  [Ci.nsITrackingDBService.TRACKERS_ID, \"trackerCount\"],\n  [Ci.nsITrackingDBService.TRACKING_COOKIES_ID, \"cookieCount\"],\n  [Ci.nsITrackingDBService.CRYPTOMINERS_ID, \"cryptominerCount\"],\n  [Ci.nsITrackingDBService.FINGERPRINTERS_ID, \"fingerprinterCount\"],\n  [Ci.nsITrackingDBService.SOCIAL_ID, \"socialCount\"],\n]);\n\nconst WHATSNEW_ENABLED_PREF = \"browser.messaging-system.whatsNewPanel.enabled\";\nconst PROTECTIONS_PANEL_INFOMSG_PREF =\n  \"browser.protections_panel.infoMessage.seen\";\n\nconst TOOLBAR_BUTTON_ID = \"whats-new-menu-button\";\nconst APPMENU_BUTTON_ID = \"appMenu-whatsnew-button\";\n\nconst BUTTON_STRING_ID = \"cfr-whatsnew-button\";\nconst WHATS_NEW_PANEL_SELECTOR = \"PanelUI-whatsNew-message-container\";\n\nclass _ToolbarPanelHub {\n  constructor() {\n    this.triggerId = \"whatsNewPanelOpened\";\n    this._showAppmenuButton = this._showAppmenuButton.bind(this);\n    this._hideAppmenuButton = this._hideAppmenuButton.bind(this);\n    this._showToolbarButton = this._showToolbarButton.bind(this);\n    this._hideToolbarButton = this._hideToolbarButton.bind(this);\n    this.insertProtectionPanelMessage = this.insertProtectionPanelMessage.bind(\n      this\n    );\n\n    this.state = {};\n  }\n\n  async init(waitForInitialized, { getMessages, dispatch, handleUserAction }) {\n    this._getMessages = getMessages;\n    this._dispatch = dispatch;\n    this._handleUserAction = handleUserAction;\n    // Wait for ASRouter messages to become available in order to know\n    // if we can show the What's New panel\n    await waitForInitialized;\n    if (this.whatsNewPanelEnabled) {\n      // Enable the application menu button so that the user can access\n      // the panel outside of the toolbar button\n      this.enableAppmenuButton();\n    }\n    // Listen for pref changes that could turn off the feature\n    Services.prefs.addObserver(WHATSNEW_ENABLED_PREF, this);\n\n    this.state = {\n      protectionPanelMessageSeen: Services.prefs.getBoolPref(\n        PROTECTIONS_PANEL_INFOMSG_PREF,\n        false\n      ),\n    };\n  }\n\n  uninit() {\n    EveryWindow.unregisterCallback(TOOLBAR_BUTTON_ID);\n    EveryWindow.unregisterCallback(APPMENU_BUTTON_ID);\n    Services.prefs.removeObserver(WHATSNEW_ENABLED_PREF, this);\n  }\n\n  observe(aSubject, aTopic, aPrefName) {\n    switch (aPrefName) {\n      case WHATSNEW_ENABLED_PREF:\n        if (!this.whatsNewPanelEnabled) {\n          this.uninit();\n        }\n        break;\n    }\n  }\n\n  get messages() {\n    return this._getMessages({\n      template: \"whatsnew_panel_message\",\n      triggerId: \"whatsNewPanelOpened\",\n      returnAll: true,\n    });\n  }\n\n  get whatsNewPanelEnabled() {\n    return Services.prefs.getBoolPref(WHATSNEW_ENABLED_PREF, false);\n  }\n\n  maybeInsertFTL(win) {\n    win.MozXULElement.insertFTLIfNeeded(\"browser/newtab/asrouter.ftl\");\n    win.MozXULElement.insertFTLIfNeeded(\"browser/branding/brandings.ftl\");\n    win.MozXULElement.insertFTLIfNeeded(\"browser/branding/sync-brand.ftl\");\n  }\n\n  // Turns on the Appmenu (hamburger menu) button for all open windows and future windows.\n  async enableAppmenuButton() {\n    if ((await this.messages).length) {\n      EveryWindow.registerCallback(\n        APPMENU_BUTTON_ID,\n        this._showAppmenuButton,\n        this._hideAppmenuButton\n      );\n    }\n  }\n\n  // Turns on the Toolbar button for all open windows and future windows.\n  async enableToolbarButton() {\n    if ((await this.messages).length) {\n      EveryWindow.registerCallback(\n        TOOLBAR_BUTTON_ID,\n        this._showToolbarButton,\n        this._hideToolbarButton\n      );\n    }\n  }\n\n  // When the panel is hidden we want to run some cleanup\n  _onPanelHidden(win) {\n    const panelContainer = win.document.getElementById(\n      \"customizationui-widget-panel\"\n    );\n    // When the panel is hidden we want to remove any toolbar buttons that\n    // might have been added as an entry point to the panel\n    const removeToolbarButton = () => {\n      EveryWindow.unregisterCallback(TOOLBAR_BUTTON_ID);\n    };\n    if (!panelContainer) {\n      return;\n    }\n    panelContainer.addEventListener(\"popuphidden\", removeToolbarButton, {\n      once: true,\n    });\n  }\n\n  // Newer messages first and use `order` field to decide between messages\n  // with the same timestamp\n  _sortWhatsNewMessages(m1, m2) {\n    // Sort by published_date in descending order.\n    if (m1.content.published_date === m2.content.published_date) {\n      // Ascending order\n      return m1.order - m2.order;\n    }\n    if (m1.content.published_date > m2.content.published_date) {\n      return -1;\n    }\n    return 1;\n  }\n\n  // Render what's new messages into the panel.\n  async renderMessages(win, doc, containerId, options = {}) {\n    const messages =\n      (options.force && options.messages) ||\n      (await this.messages).sort(this._sortWhatsNewMessages);\n    const container = doc.getElementById(containerId);\n\n    if (messages) {\n      // Targeting attribute state might have changed making new messages\n      // available and old messages invalid, we need to refresh\n      for (const prevMessageEl of container.querySelectorAll(\n        \".whatsNew-message\"\n      )) {\n        container.removeChild(prevMessageEl);\n      }\n      let previousDate = 0;\n      // Get and store any variable part of the message content\n      this.state.contentArguments = await this._contentArguments();\n      for (let message of messages) {\n        container.appendChild(\n          await this._createMessageElements(win, doc, message, previousDate)\n        );\n        previousDate = message.content.published_date;\n      }\n    }\n\n    this._onPanelHidden(win);\n\n    // Panel impressions are not associated with one particular message\n    // but with a set of messages. We concatenate message ids and send them\n    // back for every impression.\n    const eventId = {\n      id: messages\n        .map(({ id }) => id)\n        .sort()\n        .join(\",\"),\n    };\n    // Check `mainview` attribute to determine if the panel is shown as a\n    // subview (inside the application menu) or as a toolbar dropdown.\n    // https://searchfox.org/mozilla-central/rev/07f7390618692fa4f2a674a96b9b677df3a13450/browser/components/customizableui/PanelMultiView.jsm#1268\n    const mainview = win.PanelUI.whatsNewPanel.hasAttribute(\"mainview\");\n    this.sendUserEventTelemetry(win, \"IMPRESSION\", eventId, {\n      value: { view: mainview ? \"toolbar_dropdown\" : \"application_menu\" },\n    });\n  }\n\n  removeMessages(win, containerId) {\n    const doc = win.document;\n    const messageNodes = doc\n      .getElementById(containerId)\n      .querySelectorAll(\".whatsNew-message\");\n    for (const messageNode of messageNodes) {\n      messageNode.remove();\n    }\n  }\n\n  /**\n   * Dispatch the action defined in the message and user telemetry event.\n   */\n  _dispatchUserAction(win, message) {\n    let url;\n    try {\n      // Set platform specific path variables for SUMO articles\n      url = Services.urlFormatter.formatURL(message.content.cta_url);\n    } catch (e) {\n      Cu.reportError(e);\n      url = message.content.cta_url;\n    }\n    this._handleUserAction({\n      target: win,\n      data: {\n        type: message.content.cta_type,\n        data: {\n          args: url,\n          where: \"tabshifted\",\n        },\n      },\n    });\n\n    this.sendUserEventTelemetry(win, \"CLICK\", message);\n  }\n\n  /**\n   * Attach event listener to dispatch message defined action.\n   */\n  _attachClickListener(win, element, message) {\n    // Add event listener for `mouseup` not to overlap with the\n    // `mousedown` & `click` events dispatched from PanelMultiView.jsm\n    // https://searchfox.org/mozilla-central/rev/7531325c8660cfa61bf71725f83501028178cbb9/browser/components/customizableui/PanelMultiView.jsm#1830-1837\n    element.addEventListener(\"mouseup\", () => {\n      this._dispatchUserAction(win, message);\n    });\n  }\n\n  async _createMessageElements(win, doc, message, previousDate) {\n    const { content } = message;\n    const messageEl = await this._createElement(doc, \"div\");\n    messageEl.classList.add(\"whatsNew-message\");\n\n    // Only render date if it is different from the one rendered before.\n    if (content.published_date !== previousDate) {\n      messageEl.appendChild(\n        await this._createElement(doc, \"p\", {\n          classList: \"whatsNew-message-date\",\n          content: new Date(content.published_date).toLocaleDateString(\n            \"default\",\n            {\n              month: \"long\",\n              day: \"numeric\",\n              year: \"numeric\",\n            }\n          ),\n        })\n      );\n    }\n\n    const wrapperEl = await this._createElement(doc, \"button\");\n    wrapperEl.doCommand = () => this._dispatchUserAction(win, message);\n    wrapperEl.classList.add(\"whatsNew-message-body\");\n    messageEl.appendChild(wrapperEl);\n\n    if (content.icon_url) {\n      wrapperEl.classList.add(\"has-icon\");\n      const iconEl = await this._createElement(doc, \"img\");\n      iconEl.src = content.icon_url;\n      iconEl.classList.add(\"whatsNew-message-icon\");\n      await this._setTextAttribute(iconEl, \"alt\", content.icon_alt);\n      wrapperEl.appendChild(iconEl);\n    }\n\n    wrapperEl.appendChild(await this._createMessageContent(win, doc, content));\n\n    if (content.link_text) {\n      const anchorEl = await this._createElement(doc, \"a\", {\n        classList: \"text-link\",\n        content: content.link_text,\n      });\n      anchorEl.doCommand = () => this._dispatchUserAction(win, message);\n      wrapperEl.appendChild(anchorEl);\n    }\n\n    // Attach event listener on entire message container\n    this._attachClickListener(win, messageEl, message);\n\n    return messageEl;\n  }\n\n  /**\n   * Return message title (optional subtitle) and body\n   */\n  async _createMessageContent(win, doc, content) {\n    const wrapperEl = new win.DocumentFragment();\n\n    wrapperEl.appendChild(\n      await this._createElement(doc, \"h2\", {\n        classList: \"whatsNew-message-title\",\n        content: content.title,\n      })\n    );\n\n    switch (content.layout) {\n      case \"tracking-protections\":\n        await wrapperEl.appendChild(\n          await this._createElement(doc, \"h4\", {\n            classList: \"whatsNew-message-subtitle\",\n            content: content.subtitle,\n          })\n        );\n        wrapperEl.appendChild(\n          await this._createElement(doc, \"h2\", {\n            classList: \"whatsNew-message-title-large\",\n            content: this.state.contentArguments[\n              content.layout_title_content_variable\n            ],\n          })\n        );\n        break;\n    }\n\n    wrapperEl.appendChild(\n      await this._createElement(doc, \"p\", { content: content.body })\n    );\n\n    return wrapperEl;\n  }\n\n  async _createHeroElement(win, doc, message) {\n    const messageEl = await this._createElement(doc, \"div\");\n    messageEl.setAttribute(\"id\", \"protections-popup-message\");\n    messageEl.classList.add(\"whatsNew-hero-message\");\n    const wrapperEl = await this._createElement(doc, \"div\");\n    wrapperEl.classList.add(\"whatsNew-message-body\");\n    messageEl.appendChild(wrapperEl);\n\n    wrapperEl.appendChild(\n      await this._createElement(doc, \"h2\", {\n        classList: \"whatsNew-message-title\",\n        content: message.content.title,\n      })\n    );\n    wrapperEl.appendChild(\n      await this._createElement(doc, \"p\", { content: message.content.body })\n    );\n\n    if (message.content.link_text) {\n      let linkEl = await this._createElement(doc, \"a\", {\n        classList: \"text-link\",\n        content: message.content.link_text,\n      });\n      wrapperEl.appendChild(linkEl);\n      this._attachClickListener(win, linkEl, message);\n    } else {\n      this._attachClickListener(win, wrapperEl, message);\n    }\n\n    return messageEl;\n  }\n\n  async _createElement(doc, elem, options = {}) {\n    const node = doc.createElementNS(\"http://www.w3.org/1999/xhtml\", elem);\n    if (options.classList) {\n      node.classList.add(options.classList);\n    }\n    if (options.content) {\n      await this._setString(node, options.content);\n    }\n\n    return node;\n  }\n\n  async _contentArguments() {\n    // Between now and 6 weeks ago\n    const dateTo = new Date();\n    const dateFrom = new Date(dateTo.getTime() - 42 * 24 * 60 * 60 * 1000);\n    const eventsByDate = await TrackingDBService.getEventsByDateRange(\n      dateFrom,\n      dateTo\n    );\n    // Make sure we set all types of possible values to 0 because they might\n    // be referenced by fluent strings\n    let totalEvents = { blockedCount: 0 };\n    for (let blockedType of idToTextMap.values()) {\n      totalEvents[blockedType] = 0;\n    }\n    // Count all events in the past 6 weeks. Returns an object with:\n    // `blockedCount` total number of blocked resources\n    // {tracker|cookie|social...} breakdown by event type as defined by `idToTextMap`\n    totalEvents = eventsByDate.reduce((acc, day) => {\n      const type = day.getResultByName(\"type\");\n      const count = day.getResultByName(\"count\");\n      acc[idToTextMap.get(type)] = (acc[idToTextMap.get(type)] || 0) + count;\n      acc.blockedCount += count;\n      return acc;\n    }, totalEvents);\n    return {\n      // Keys need to match variable names used in asrouter.ftl\n      // `earliestDate` will be either 6 weeks ago or when tracking recording\n      // started. Whichever is more recent.\n      earliestDate: Math.max(\n        new Date(await TrackingDBService.getEarliestRecordedDate()),\n        dateFrom\n      ),\n      ...totalEvents,\n    };\n  }\n\n  // If `string_id` is present it means we are relying on fluent for translations.\n  // Otherwise, we have a vanilla string.\n  async _setString(el, stringObj) {\n    if (stringObj && stringObj.string_id) {\n      const [{ value }] = await RemoteL10n.l10n.formatMessages([\n        {\n          id: stringObj.string_id,\n          // Pass all available arguments to Fluent\n          args: this.state.contentArguments,\n        },\n      ]);\n      el.textContent = value;\n    } else {\n      el.textContent = stringObj;\n    }\n  }\n\n  // If `string_id` is present it means we are relying on fluent for translations.\n  // Otherwise, we have a vanilla string.\n  async _setTextAttribute(el, attr, stringObj) {\n    if (stringObj && stringObj.string_id) {\n      const [{ attributes }] = await RemoteL10n.l10n.formatMessages([\n        {\n          id: stringObj.string_id,\n          // Pass all available arguments to Fluent\n          args: this.state.contentArguments,\n        },\n      ]);\n      if (attributes) {\n        const { value } = attributes.find(({ name }) => name === attr);\n        el.setAttribute(attr, value);\n      }\n    } else {\n      el.setAttribute(attr, stringObj);\n    }\n  }\n\n  async _showAppmenuButton(win) {\n    this.maybeInsertFTL(win);\n    await this._showElement(\n      win.browser.ownerDocument,\n      APPMENU_BUTTON_ID,\n      BUTTON_STRING_ID\n    );\n  }\n\n  _hideAppmenuButton(win) {\n    this._hideElement(win.browser.ownerDocument, APPMENU_BUTTON_ID);\n  }\n\n  _showToolbarButton(win) {\n    const document = win.browser.ownerDocument;\n    this.maybeInsertFTL(win);\n    return this._showElement(document, TOOLBAR_BUTTON_ID, BUTTON_STRING_ID);\n  }\n\n  _hideToolbarButton(win) {\n    this._hideElement(win.browser.ownerDocument, TOOLBAR_BUTTON_ID);\n  }\n\n  async _showElement(document, id, string_id) {\n    const el = document.getElementById(id);\n    await this._setTextAttribute(el, \"label\", { string_id });\n    await this._setTextAttribute(el, \"tooltiptext\", { string_id });\n    el.removeAttribute(\"hidden\");\n  }\n\n  _hideElement(document, id) {\n    document.getElementById(id).setAttribute(\"hidden\", true);\n  }\n\n  _sendTelemetry(ping) {\n    this._dispatch({\n      type: \"TOOLBAR_PANEL_TELEMETRY\",\n      data: { action: \"cfr_user_event\", source: \"CFR\", ...ping },\n    });\n  }\n\n  sendUserEventTelemetry(win, event, message, options = {}) {\n    // Only send pings for non private browsing windows\n    if (\n      win &&\n      !PrivateBrowsingUtils.isBrowserPrivate(\n        win.ownerGlobal.gBrowser.selectedBrowser\n      )\n    ) {\n      this._sendTelemetry({\n        message_id: message.id,\n        bucket_id: message.id,\n        event,\n        event_context: options.value,\n      });\n    }\n  }\n\n  /**\n   * Inserts a message into the Protections Panel. The message is visible once\n   * and afterwards set in a collapsed state. It can be shown again using the\n   * info button in the panel header.\n   */\n  async insertProtectionPanelMessage(event) {\n    const win = event.target.ownerGlobal;\n    this.maybeInsertFTL(win);\n\n    const doc = event.target.ownerDocument;\n    const container = doc.getElementById(\"messaging-system-message-container\");\n    const infoButton = doc.getElementById(\"protections-popup-info-button\");\n    const panelContainer = doc.getElementById(\"protections-popup\");\n    const toggleMessage = () => {\n      container.toggleAttribute(\"disabled\");\n      infoButton.toggleAttribute(\"checked\");\n      panelContainer.toggleAttribute(\"infoMessageShowing\");\n    };\n    if (!container.childElementCount) {\n      const message = await this._getMessages({\n        template: \"protections_panel\",\n        triggerId: \"protectionsPanelOpen\",\n      });\n      if (message) {\n        const messageEl = await this._createHeroElement(win, doc, message);\n        container.appendChild(messageEl);\n        infoButton.addEventListener(\"click\", toggleMessage);\n        this.sendUserEventTelemetry(win, \"IMPRESSION\", message);\n      }\n    }\n    // Message is collapsed by default. If it was never shown before we want\n    // to expand it\n    if (\n      !this.state.protectionPanelMessageSeen &&\n      container.hasAttribute(\"disabled\")\n    ) {\n      toggleMessage();\n    }\n    // Save state that we displayed the message\n    if (!this.state.protectionPanelMessageSeen) {\n      Services.prefs.setBoolPref(PROTECTIONS_PANEL_INFOMSG_PREF, true);\n      this.state.protectionPanelMessageSeen = true;\n    }\n    // Collapse the message after the panel is hidden so we don't get the\n    // animation when opening the panel\n    panelContainer.addEventListener(\n      \"popuphidden\",\n      () => {\n        if (\n          this.state.protectionPanelMessageSeen &&\n          !container.hasAttribute(\"disabled\")\n        ) {\n          toggleMessage();\n        }\n      },\n      {\n        once: true,\n      }\n    );\n  }\n\n  /**\n   * @param {object} browser MessageChannel target argument as a response to a user action\n   * @param {object} message Message selected from devtools page\n   */\n  forceShowMessage(browser, message) {\n    const win = browser.browser.ownerGlobal;\n    const doc = browser.browser.ownerDocument;\n    this.removeMessages(win, WHATS_NEW_PANEL_SELECTOR);\n    this.renderMessages(win, doc, WHATS_NEW_PANEL_SELECTOR, {\n      force: true,\n      messages: [message],\n    });\n    win.PanelUI.panel.addEventListener(\"popuphidden\", event =>\n      this.removeMessages(event.target.ownerGlobal, WHATS_NEW_PANEL_SELECTOR)\n    );\n  }\n}\n\nthis._ToolbarPanelHub = _ToolbarPanelHub;\n\n/**\n * ToolbarPanelHub - singleton instance of _ToolbarPanelHub that can initiate\n * message requests and render messages.\n */\nthis.ToolbarPanelHub = new _ToolbarPanelHub();\n\nconst EXPORTED_SYMBOLS = [\"ToolbarPanelHub\", \"_ToolbarPanelHub\"];\n"
  },
  {
    "path": "lib/TopSitesFeed.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\"use strict\";\n\nconst { Services } = ChromeUtils.import(\"resource://gre/modules/Services.jsm\");\nconst { XPCOMUtils } = ChromeUtils.import(\n  \"resource://gre/modules/XPCOMUtils.jsm\"\n);\n\nconst { actionCreators: ac, actionTypes: at } = ChromeUtils.import(\n  \"resource://activity-stream/common/Actions.jsm\"\n);\nconst { TippyTopProvider } = ChromeUtils.import(\n  \"resource://activity-stream/lib/TippyTopProvider.jsm\"\n);\nconst { insertPinned, TOP_SITES_MAX_SITES_PER_ROW } = ChromeUtils.import(\n  \"resource://activity-stream/common/Reducers.jsm\"\n);\nconst { Dedupe } = ChromeUtils.import(\n  \"resource://activity-stream/common/Dedupe.jsm\"\n);\nconst { shortURL } = ChromeUtils.import(\n  \"resource://activity-stream/lib/ShortURL.jsm\"\n);\nconst { getDefaultOptions } = ChromeUtils.import(\n  \"resource://activity-stream/lib/ActivityStreamStorage.jsm\"\n);\nconst {\n  CUSTOM_SEARCH_SHORTCUTS,\n  SEARCH_SHORTCUTS_EXPERIMENT,\n  SEARCH_SHORTCUTS_SEARCH_ENGINES_PREF,\n  SEARCH_SHORTCUTS_HAVE_PINNED_PREF,\n  checkHasSearchEngine,\n  getSearchProvider,\n} = ChromeUtils.import(\"resource://activity-stream/lib/SearchShortcuts.jsm\");\n\nChromeUtils.defineModuleGetter(\n  this,\n  \"filterAdult\",\n  \"resource://activity-stream/lib/FilterAdult.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"LinksCache\",\n  \"resource://activity-stream/lib/LinksCache.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"NewTabUtils\",\n  \"resource://gre/modules/NewTabUtils.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"Screenshots\",\n  \"resource://activity-stream/lib/Screenshots.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"PageThumbs\",\n  \"resource://gre/modules/PageThumbs.jsm\"\n);\n\nconst DEFAULT_SITES_PREF = \"default.sites\";\nconst DEFAULT_TOP_SITES = [];\nconst FRECENCY_THRESHOLD = 100 + 1; // 1 visit (skip first-run/one-time pages)\nconst MIN_FAVICON_SIZE = 96;\nconst CACHED_LINK_PROPS_TO_MIGRATE = [\"screenshot\", \"customScreenshot\"];\nconst PINNED_FAVICON_PROPS_TO_MIGRATE = [\n  \"favicon\",\n  \"faviconRef\",\n  \"faviconSize\",\n];\nconst SECTION_ID = \"topsites\";\nconst ROWS_PREF = \"topSitesRows\";\n\n// Search experiment stuff\nconst FILTER_DEFAULT_SEARCH_PREF = \"improvesearch.noDefaultSearchTile\";\nconst SEARCH_FILTERS = [\n  \"google\",\n  \"search.yahoo\",\n  \"yahoo\",\n  \"bing\",\n  \"ask\",\n  \"duckduckgo\",\n];\n\nfunction getShortURLForCurrentSearch() {\n  const url = shortURL({ url: Services.search.defaultEngine.searchForm });\n  return url;\n}\n\nthis.TopSitesFeed = class TopSitesFeed {\n  constructor() {\n    this._tippyTopProvider = new TippyTopProvider();\n    XPCOMUtils.defineLazyGetter(\n      this,\n      \"_currentSearchHostname\",\n      getShortURLForCurrentSearch\n    );\n    this.dedupe = new Dedupe(this._dedupeKey);\n    this.frecentCache = new LinksCache(\n      NewTabUtils.activityStreamLinks,\n      \"getTopSites\",\n      CACHED_LINK_PROPS_TO_MIGRATE,\n      (oldOptions, newOptions) =>\n        // Refresh if no old options or requesting more items\n        !(oldOptions.numItems >= newOptions.numItems)\n    );\n    this.pinnedCache = new LinksCache(NewTabUtils.pinnedLinks, \"links\", [\n      ...CACHED_LINK_PROPS_TO_MIGRATE,\n      ...PINNED_FAVICON_PROPS_TO_MIGRATE,\n    ]);\n    PageThumbs.addExpirationFilter(this);\n  }\n\n  init() {\n    // If the feed was previously disabled PREFS_INITIAL_VALUES was never received\n    this.refreshDefaults(\n      this.store.getState().Prefs.values[DEFAULT_SITES_PREF]\n    );\n    this._storage = this.store.dbStorage.getDbTable(\"sectionPrefs\");\n    this.refresh({ broadcast: true });\n    Services.obs.addObserver(this, \"browser-search-engine-modified\");\n  }\n\n  uninit() {\n    PageThumbs.removeExpirationFilter(this);\n    Services.obs.removeObserver(this, \"browser-search-engine-modified\");\n  }\n\n  observe(subj, topic, data) {\n    // We should update the current top sites if the search engine has been changed since\n    // the search engine that gets filtered out of top sites has changed.\n    if (\n      topic === \"browser-search-engine-modified\" &&\n      data === \"engine-default\" &&\n      this.store.getState().Prefs.values[FILTER_DEFAULT_SEARCH_PREF]\n    ) {\n      delete this._currentSearchHostname;\n      this._currentSearchHostname = getShortURLForCurrentSearch();\n      this.refresh({ broadcast: true });\n    }\n  }\n\n  _dedupeKey(site) {\n    return site && site.hostname;\n  }\n\n  refreshDefaults(sites) {\n    // Clear out the array of any previous defaults\n    DEFAULT_TOP_SITES.length = 0;\n\n    // Add default sites if any based on the pref\n    if (sites) {\n      for (const url of sites.split(\",\")) {\n        const site = {\n          isDefault: true,\n          url,\n        };\n        site.hostname = shortURL(site);\n        DEFAULT_TOP_SITES.push(site);\n      }\n    }\n  }\n\n  filterForThumbnailExpiration(callback) {\n    const { rows } = this.store.getState().TopSites;\n    callback(\n      rows.reduce((acc, site) => {\n        acc.push(site.url);\n        if (site.customScreenshotURL) {\n          acc.push(site.customScreenshotURL);\n        }\n        return acc;\n      }, [])\n    );\n  }\n\n  /**\n   * shouldFilterSearchTile - is default filtering enabled and does a given hostname match the user's default search engine?\n   *\n   * @param {string} hostname a top site hostname, such as \"amazon\" or \"foo\"\n   * @returns {bool}\n   */\n  shouldFilterSearchTile(hostname) {\n    if (\n      this.store.getState().Prefs.values[FILTER_DEFAULT_SEARCH_PREF] &&\n      (SEARCH_FILTERS.includes(hostname) ||\n        hostname === this._currentSearchHostname)\n    ) {\n      return true;\n    }\n    return false;\n  }\n\n  /**\n   * _maybeInsertSearchShortcuts - if the search shortcuts experiment is running,\n   *                               insert search shortcuts if needed\n   * @param {Array} plainPinnedSites (from the pinnedSitesCache)\n   * @returns {Boolean} Did we insert any search shortcuts?\n   */\n  async _maybeInsertSearchShortcuts(plainPinnedSites) {\n    // Only insert shortcuts if the experiment is running\n    if (this.store.getState().Prefs.values[SEARCH_SHORTCUTS_EXPERIMENT]) {\n      // We don't want to insert shortcuts we've previously inserted\n      const prevInsertedShortcuts = this.store\n        .getState()\n        .Prefs.values[SEARCH_SHORTCUTS_HAVE_PINNED_PREF].split(\",\")\n        .filter(s => s); // Filter out empty strings\n      const newInsertedShortcuts = [];\n\n      const shouldPin = this.store\n        .getState()\n        .Prefs.values[SEARCH_SHORTCUTS_SEARCH_ENGINES_PREF].split(\",\")\n        .map(getSearchProvider)\n        .filter(s => s && s.shortURL !== this._currentSearchHostname);\n\n      // If we've previously inserted all search shortcuts return early\n      if (\n        shouldPin.every(shortcut =>\n          prevInsertedShortcuts.includes(shortcut.shortURL)\n        )\n      ) {\n        return false;\n      }\n\n      const numberOfSlots =\n        this.store.getState().Prefs.values[ROWS_PREF] *\n        TOP_SITES_MAX_SITES_PER_ROW;\n\n      // The plainPinnedSites array is populated with pinned sites at their\n      // respective indices, and null everywhere else, but is not always the\n      // right length\n      const emptySlots = Math.max(numberOfSlots - plainPinnedSites.length, 0);\n      const pinnedSites = [...plainPinnedSites].concat(\n        Array(emptySlots).fill(null)\n      );\n\n      const tryToInsertSearchShortcut = async shortcut => {\n        const nextAvailable = pinnedSites.indexOf(null);\n        // Only add a search shortcut if the site isn't already pinned, we\n        // haven't previously inserted it, there's space to pin it, and the\n        // search engine is available in Firefox\n        if (\n          !pinnedSites.find(s => s && s.hostname === shortcut.shortURL) &&\n          !prevInsertedShortcuts.includes(shortcut.shortURL) &&\n          nextAvailable > -1 &&\n          (await checkHasSearchEngine(shortcut.keyword))\n        ) {\n          const site = await this.topSiteToSearchTopSite({ url: shortcut.url });\n          this._pinSiteAt(site, nextAvailable);\n          pinnedSites[nextAvailable] = site;\n          newInsertedShortcuts.push(shortcut.shortURL);\n        }\n      };\n\n      for (let shortcut of shouldPin) {\n        await tryToInsertSearchShortcut(shortcut);\n      }\n\n      if (newInsertedShortcuts.length) {\n        this.store.dispatch(\n          ac.SetPref(\n            SEARCH_SHORTCUTS_HAVE_PINNED_PREF,\n            prevInsertedShortcuts.concat(newInsertedShortcuts).join(\",\")\n          )\n        );\n        return true;\n      }\n    }\n\n    return false;\n  }\n\n  async getLinksWithDefaults() {\n    const numItems =\n      this.store.getState().Prefs.values[ROWS_PREF] *\n      TOP_SITES_MAX_SITES_PER_ROW;\n    const searchShortcutsExperiment = this.store.getState().Prefs.values[\n      SEARCH_SHORTCUTS_EXPERIMENT\n    ];\n    // We must wait for search services to initialize in order to access default\n    // search engine properties without triggering a synchronous initialization\n    await Services.search.init();\n\n    // Get all frecent sites from history.\n    let frecent = [];\n    const cache = await this.frecentCache.request({\n      // We need to overquery due to the top 5 alexa search + default search possibly being removed\n      numItems: numItems + SEARCH_FILTERS.length + 1,\n      topsiteFrecency: FRECENCY_THRESHOLD,\n    });\n    for (let link of cache) {\n      const hostname = shortURL(link);\n      if (!this.shouldFilterSearchTile(hostname)) {\n        frecent.push({\n          ...(searchShortcutsExperiment\n            ? await this.topSiteToSearchTopSite(link)\n            : link),\n          hostname,\n        });\n      }\n    }\n\n    // Remove any defaults that have been blocked.\n    let notBlockedDefaultSites = [];\n    for (let link of DEFAULT_TOP_SITES) {\n      const searchProvider = getSearchProvider(shortURL(link));\n      if (NewTabUtils.blockedLinks.isBlocked({ url: link.url })) {\n        continue;\n      } else if (this.shouldFilterSearchTile(link.hostname)) {\n        continue;\n        // If we've previously blocked a search shortcut, remove the default top site\n        // that matches the hostname\n      } else if (\n        searchProvider &&\n        NewTabUtils.blockedLinks.isBlocked({ url: searchProvider.url })\n      ) {\n        continue;\n      }\n      notBlockedDefaultSites.push(\n        searchShortcutsExperiment\n          ? await this.topSiteToSearchTopSite(link)\n          : link\n      );\n    }\n\n    // Get pinned links augmented with desired properties\n    let plainPinned = await this.pinnedCache.request();\n\n    // Insert search shortcuts if we need to.\n    // _maybeInsertSearchShortcuts returns true if any search shortcuts are\n    // inserted, meaning we need to expire and refresh the pinnedCache\n    if (await this._maybeInsertSearchShortcuts(plainPinned)) {\n      this.pinnedCache.expire();\n      plainPinned = await this.pinnedCache.request();\n    }\n\n    const pinned = await Promise.all(\n      plainPinned.map(async link => {\n        if (!link) {\n          return link;\n        }\n\n        // Copy all properties from a frecent link and add more\n        const finder = other => other.url === link.url;\n\n        // Remove frecent link's screenshot if pinned link has a custom one\n        const frecentSite = frecent.find(finder);\n        if (frecentSite && link.customScreenshotURL) {\n          delete frecentSite.screenshot;\n        }\n        // If the link is a frecent site, do not copy over 'isDefault', else check\n        // if the site is a default site\n        const copy = Object.assign(\n          {},\n          frecentSite || { isDefault: !!notBlockedDefaultSites.find(finder) },\n          link,\n          { hostname: shortURL(link) },\n          { searchTopSite: !!link.searchTopSite }\n        );\n\n        // Add in favicons if we don't already have it\n        if (!copy.favicon) {\n          try {\n            NewTabUtils.activityStreamProvider._faviconBytesToDataURI(\n              await NewTabUtils.activityStreamProvider._addFavicons([copy])\n            );\n\n            for (const prop of PINNED_FAVICON_PROPS_TO_MIGRATE) {\n              copy.__sharedCache.updateLink(prop, copy[prop]);\n            }\n          } catch (e) {\n            // Some issue with favicon, so just continue without one\n          }\n        }\n\n        return copy;\n      })\n    );\n\n    // Remove any duplicates from frecent and default sites\n    const [, dedupedFrecent, dedupedDefaults] = this.dedupe.group(\n      pinned,\n      frecent,\n      notBlockedDefaultSites\n    );\n    const dedupedUnpinned = [...dedupedFrecent, ...dedupedDefaults];\n\n    // Remove adult sites if we need to\n    const checkedAdult = this.store.getState().Prefs.values.filterAdult\n      ? filterAdult(dedupedUnpinned)\n      : dedupedUnpinned;\n\n    // Insert the original pinned sites into the deduped frecent and defaults\n    const withPinned = insertPinned(checkedAdult, pinned).slice(0, numItems);\n\n    // Now, get a tippy top icon, a rich icon, or screenshot for every item\n    for (const link of withPinned) {\n      if (link) {\n        // If there is a custom screenshot this is the only image we display\n        if (link.customScreenshotURL) {\n          this._fetchScreenshot(link, link.customScreenshotURL);\n        } else if (link.searchTopSite && !link.isDefault) {\n          this._tippyTopProvider.processSite(link);\n        } else {\n          this._fetchIcon(link);\n        }\n\n        // Remove internal properties that might be updated after dispatch\n        delete link.__sharedCache;\n\n        // Indicate that these links should get a frecency bonus when clicked\n        link.typedBonus = true;\n      }\n    }\n\n    return withPinned;\n  }\n\n  /**\n   * Refresh the top sites data for content.\n   * @param {bool} options.broadcast Should the update be broadcasted.\n   */\n  async refresh(options = {}) {\n    if (!this._tippyTopProvider.initialized) {\n      await this._tippyTopProvider.init();\n    }\n\n    const links = await this.getLinksWithDefaults();\n    const newAction = { type: at.TOP_SITES_UPDATED, data: { links } };\n    let storedPrefs;\n    try {\n      storedPrefs = (await this._storage.get(SECTION_ID)) || {};\n    } catch (e) {\n      storedPrefs = {};\n      Cu.reportError(\"Problem getting stored prefs for TopSites\");\n    }\n    newAction.data.pref = getDefaultOptions(storedPrefs);\n\n    if (options.broadcast) {\n      // Broadcast an update to all open content pages\n      this.store.dispatch(ac.BroadcastToContent(newAction));\n    } else {\n      // Don't broadcast only update the state and update the preloaded tab.\n      this.store.dispatch(ac.AlsoToPreloaded(newAction));\n    }\n  }\n\n  async updateCustomSearchShortcuts() {\n    if (!this.store.getState().Prefs.values[SEARCH_SHORTCUTS_EXPERIMENT]) {\n      return;\n    }\n\n    if (!this._tippyTopProvider.initialized) {\n      await this._tippyTopProvider.init();\n    }\n\n    // Populate the state with available search shortcuts\n    const searchShortcuts = (await Services.search.getDefaultEngines()).reduce(\n      (result, engine) => {\n        const shortcut = CUSTOM_SEARCH_SHORTCUTS.find(s =>\n          engine.wrappedJSObject._internalAliases.includes(s.keyword)\n        );\n        if (shortcut) {\n          result.push(this._tippyTopProvider.processSite({ ...shortcut }));\n        }\n        return result;\n      },\n      []\n    );\n    this.store.dispatch(\n      ac.BroadcastToContent({\n        type: at.UPDATE_SEARCH_SHORTCUTS,\n        data: { searchShortcuts },\n      })\n    );\n  }\n\n  async topSiteToSearchTopSite(site) {\n    const searchProvider = getSearchProvider(shortURL(site));\n    if (\n      !searchProvider ||\n      !(await checkHasSearchEngine(searchProvider.keyword))\n    ) {\n      return site;\n    }\n    return {\n      ...site,\n      searchTopSite: true,\n      label: searchProvider.keyword,\n    };\n  }\n\n  /**\n   * Get an image for the link preferring tippy top, rich favicon, screenshots.\n   */\n  async _fetchIcon(link) {\n    // Nothing to do if we already have a rich icon from the page\n    if (link.favicon && link.faviconSize >= MIN_FAVICON_SIZE) {\n      return;\n    }\n\n    // Nothing more to do if we can use a default tippy top icon\n    this._tippyTopProvider.processSite(link);\n    if (link.tippyTopIcon) {\n      return;\n    }\n\n    // Make a request for a better icon\n    this._requestRichIcon(link.url);\n\n    // Also request a screenshot if we don't have one yet\n    await this._fetchScreenshot(link, link.url);\n  }\n\n  /**\n   * Fetch, cache and broadcast a screenshot for a specific topsite.\n   * @param link cached topsite object\n   * @param url where to fetch the image from\n   */\n  async _fetchScreenshot(link, url) {\n    if (link.screenshot) {\n      return;\n    }\n    await Screenshots.maybeCacheScreenshot(\n      link,\n      url,\n      \"screenshot\",\n      screenshot =>\n        this.store.dispatch(\n          ac.BroadcastToContent({\n            data: { screenshot, url: link.url },\n            type: at.SCREENSHOT_UPDATED,\n          })\n        )\n    );\n  }\n\n  /**\n   * Dispatch screenshot preview to target or notify if request failed.\n   * @param customScreenshotURL {string} The URL used to capture the screenshot\n   * @param target {string} Id of content process where to dispatch the result\n   */\n  async getScreenshotPreview(url, target) {\n    const preview = (await Screenshots.getScreenshotForURL(url)) || \"\";\n    this.store.dispatch(\n      ac.OnlyToOneContent(\n        {\n          data: { url, preview },\n          type: at.PREVIEW_RESPONSE,\n        },\n        target\n      )\n    );\n  }\n\n  _requestRichIcon(url) {\n    this.store.dispatch({\n      type: at.RICH_ICON_MISSING,\n      data: { url },\n    });\n  }\n\n  updateSectionPrefs(collapsed) {\n    this.store.dispatch(\n      ac.BroadcastToContent({\n        type: at.TOP_SITES_PREFS_UPDATED,\n        data: { pref: collapsed },\n      })\n    );\n  }\n\n  /**\n   * Inform others that top sites data has been updated due to pinned changes.\n   */\n  _broadcastPinnedSitesUpdated() {\n    // Pinned data changed, so make sure we get latest\n    this.pinnedCache.expire();\n\n    // Refresh to update pinned sites with screenshots, trigger deduping, etc.\n    this.refresh({ broadcast: true });\n  }\n\n  /**\n   * Pin a site at a specific position saving only the desired keys.\n   * @param customScreenshotURL {string} User set URL of preview image for site\n   * @param label {string} User set string of custom site name\n   */\n  async _pinSiteAt({ customScreenshotURL, label, url, searchTopSite }, index) {\n    const toPin = { url };\n    if (label) {\n      toPin.label = label;\n    }\n    if (customScreenshotURL) {\n      toPin.customScreenshotURL = customScreenshotURL;\n    }\n    if (searchTopSite) {\n      toPin.searchTopSite = searchTopSite;\n    }\n    NewTabUtils.pinnedLinks.pin(toPin, index);\n\n    await this._clearLinkCustomScreenshot({ customScreenshotURL, url });\n  }\n\n  async _clearLinkCustomScreenshot(site) {\n    // If screenshot url changed or was removed we need to update the cached link obj\n    if (site.customScreenshotURL !== undefined) {\n      const pinned = await this.pinnedCache.request();\n      const link = pinned.find(pin => pin && pin.url === site.url);\n      if (link && link.customScreenshotURL !== site.customScreenshotURL) {\n        link.__sharedCache.updateLink(\"screenshot\", undefined);\n      }\n    }\n  }\n\n  /**\n   * Handle a pin action of a site to a position.\n   */\n  async pin(action) {\n    const { site, index } = action.data;\n    // If valid index provided, pin at that position\n    if (index >= 0) {\n      await this._pinSiteAt(site, index);\n      this._broadcastPinnedSitesUpdated();\n    } else {\n      // Bug 1458658. If the top site is being pinned from an 'Add a Top Site' option,\n      // then we want to make sure to unblock that link if it has previously been\n      // blocked. We know if the site has been added because the index will be -1.\n      if (index === -1) {\n        NewTabUtils.blockedLinks.unblock({ url: site.url });\n        this.frecentCache.expire();\n      }\n      this.insert(action);\n    }\n  }\n\n  /**\n   * Handle an unpin action of a site.\n   */\n  unpin(action) {\n    const { site } = action.data;\n    NewTabUtils.pinnedLinks.unpin(site);\n    this._broadcastPinnedSitesUpdated();\n  }\n\n  disableSearchImprovements() {\n    Services.prefs.clearUserPref(\n      `browser.newtabpage.activity-stream.${SEARCH_SHORTCUTS_HAVE_PINNED_PREF}`\n    );\n    this.unpinAllSearchShortcuts();\n  }\n\n  unpinAllSearchShortcuts() {\n    for (let pinnedLink of NewTabUtils.pinnedLinks.links) {\n      if (pinnedLink && pinnedLink.searchTopSite) {\n        NewTabUtils.pinnedLinks.unpin(pinnedLink);\n      }\n    }\n    this.pinnedCache.expire();\n  }\n\n  /**\n   * Insert a site to pin at a position shifting over any other pinned sites.\n   */\n  _insertPin(site, index, draggedFromIndex) {\n    // Don't insert any pins past the end of the visible top sites. Otherwise,\n    // we can end up with a bunch of pinned sites that can never be unpinned again\n    // from the UI.\n    const topSitesCount =\n      this.store.getState().Prefs.values[ROWS_PREF] *\n      TOP_SITES_MAX_SITES_PER_ROW;\n    if (index >= topSitesCount) {\n      return;\n    }\n\n    let pinned = NewTabUtils.pinnedLinks.links;\n    if (!pinned[index]) {\n      this._pinSiteAt(site, index);\n    } else {\n      pinned[draggedFromIndex] = null;\n      // Find the hole to shift the pinned site(s) towards. We shift towards the\n      // hole left by the site being dragged.\n      let holeIndex = index;\n      const indexStep = index > draggedFromIndex ? -1 : 1;\n      while (pinned[holeIndex]) {\n        holeIndex += indexStep;\n      }\n      if (holeIndex >= topSitesCount || holeIndex < 0) {\n        // There are no holes, so we will effectively unpin the last slot and shifting\n        // towards it. This only happens when adding a new top site to an already\n        // fully pinned grid.\n        holeIndex = topSitesCount - 1;\n      }\n\n      // Shift towards the hole.\n      const shiftingStep = holeIndex > index ? -1 : 1;\n      while (holeIndex !== index) {\n        const nextIndex = holeIndex + shiftingStep;\n        this._pinSiteAt(pinned[nextIndex], holeIndex);\n        holeIndex = nextIndex;\n      }\n      this._pinSiteAt(site, index);\n    }\n  }\n\n  /**\n   * Handle an insert (drop/add) action of a site.\n   */\n  async insert(action) {\n    let { index } = action.data;\n    // Treat invalid pin index values (e.g., -1, undefined) as insert in the first position\n    if (!(index > 0)) {\n      index = 0;\n    }\n\n    // Inserting a top site pins it in the specified slot, pushing over any link already\n    // pinned in the slot (unless it's the last slot, then it replaces).\n    this._insertPin(\n      action.data.site,\n      index,\n      action.data.draggedFromIndex !== undefined\n        ? action.data.draggedFromIndex\n        : this.store.getState().Prefs.values[ROWS_PREF] *\n            TOP_SITES_MAX_SITES_PER_ROW\n    );\n\n    await this._clearLinkCustomScreenshot(action.data.site);\n    this._broadcastPinnedSitesUpdated();\n  }\n\n  updatePinnedSearchShortcuts({ addedShortcuts, deletedShortcuts }) {\n    // Unpin the deletedShortcuts.\n    deletedShortcuts.forEach(({ url }) => {\n      NewTabUtils.pinnedLinks.unpin({ url });\n    });\n\n    // Pin the addedShortcuts.\n    const numberOfSlots =\n      this.store.getState().Prefs.values[ROWS_PREF] *\n      TOP_SITES_MAX_SITES_PER_ROW;\n    addedShortcuts.forEach(shortcut => {\n      // Find first hole in pinnedLinks.\n      let index = NewTabUtils.pinnedLinks.links.findIndex(link => !link);\n      if (\n        index < 0 &&\n        NewTabUtils.pinnedLinks.links.length + 1 < numberOfSlots\n      ) {\n        // pinnedLinks can have less slots than the total available.\n        index = NewTabUtils.pinnedLinks.links.length;\n      }\n      if (index >= 0) {\n        NewTabUtils.pinnedLinks.pin(shortcut, index);\n      } else {\n        // No slots available, we need to do an insert in first slot and push over other pinned links.\n        this._insertPin(shortcut, 0, numberOfSlots);\n      }\n    });\n\n    this._broadcastPinnedSitesUpdated();\n  }\n\n  onAction(action) {\n    switch (action.type) {\n      case at.INIT:\n        this.init();\n        this.updateCustomSearchShortcuts();\n        break;\n      case at.SYSTEM_TICK:\n        this.refresh({ broadcast: false });\n        break;\n      // All these actions mean we need new top sites\n      case at.PLACES_HISTORY_CLEARED:\n      case at.PLACES_LINK_DELETED:\n        this.frecentCache.expire();\n        this.refresh({ broadcast: true });\n        break;\n      case at.PLACES_LINKS_CHANGED:\n        this.frecentCache.expire();\n        this.refresh({ broadcast: false });\n        break;\n      case at.PLACES_LINK_BLOCKED:\n        this.frecentCache.expire();\n        this.pinnedCache.expire();\n        this.refresh({ broadcast: true });\n        break;\n      case at.PREF_CHANGED:\n        switch (action.data.name) {\n          case DEFAULT_SITES_PREF:\n            this.refreshDefaults(action.data.value);\n            break;\n          case ROWS_PREF:\n          case FILTER_DEFAULT_SEARCH_PREF:\n          case SEARCH_SHORTCUTS_SEARCH_ENGINES_PREF:\n            this.refresh({ broadcast: true });\n            break;\n          case SEARCH_SHORTCUTS_EXPERIMENT:\n            if (action.data.value) {\n              this.updateCustomSearchShortcuts();\n            } else {\n              this.disableSearchImprovements();\n            }\n            this.refresh({ broadcast: true });\n        }\n        break;\n      case at.UPDATE_SECTION_PREFS:\n        if (action.data.id === SECTION_ID) {\n          this.updateSectionPrefs(action.data.value);\n        }\n        break;\n      case at.PREFS_INITIAL_VALUES:\n        this.refreshDefaults(action.data[DEFAULT_SITES_PREF]);\n        break;\n      case at.TOP_SITES_PIN:\n        this.pin(action);\n        break;\n      case at.TOP_SITES_UNPIN:\n        this.unpin(action);\n        break;\n      case at.TOP_SITES_INSERT:\n        this.insert(action);\n        break;\n      case at.PREVIEW_REQUEST:\n        this.getScreenshotPreview(action.data.url, action.meta.fromTarget);\n        break;\n      case at.UPDATE_PINNED_SEARCH_SHORTCUTS:\n        this.updatePinnedSearchShortcuts(action.data);\n        break;\n      case at.UNINIT:\n        this.uninit();\n        break;\n    }\n  }\n};\n\nthis.DEFAULT_TOP_SITES = DEFAULT_TOP_SITES;\nconst EXPORTED_SYMBOLS = [\"TopSitesFeed\", \"DEFAULT_TOP_SITES\"];\n"
  },
  {
    "path": "lib/TopStoriesFeed.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\"use strict\";\n\nconst { XPCOMUtils } = ChromeUtils.import(\n  \"resource://gre/modules/XPCOMUtils.jsm\"\n);\nconst { Services } = ChromeUtils.import(\"resource://gre/modules/Services.jsm\");\nconst { NewTabUtils } = ChromeUtils.import(\n  \"resource://gre/modules/NewTabUtils.jsm\"\n);\nXPCOMUtils.defineLazyGlobalGetters(this, [\"fetch\"]);\n\nconst { actionTypes: at, actionCreators: ac } = ChromeUtils.import(\n  \"resource://activity-stream/common/Actions.jsm\"\n);\nconst { Prefs } = ChromeUtils.import(\n  \"resource://activity-stream/lib/ActivityStreamPrefs.jsm\"\n);\nconst { shortURL } = ChromeUtils.import(\n  \"resource://activity-stream/lib/ShortURL.jsm\"\n);\nconst { SectionsManager } = ChromeUtils.import(\n  \"resource://activity-stream/lib/SectionsManager.jsm\"\n);\nconst { UserDomainAffinityProvider } = ChromeUtils.import(\n  \"resource://activity-stream/lib/UserDomainAffinityProvider.jsm\"\n);\nconst { PersonalityProvider } = ChromeUtils.import(\n  \"resource://activity-stream/lib/PersonalityProvider.jsm\"\n);\nconst { PersistentCache } = ChromeUtils.import(\n  \"resource://activity-stream/lib/PersistentCache.jsm\"\n);\n\nChromeUtils.defineModuleGetter(\n  this,\n  \"perfService\",\n  \"resource://activity-stream/common/PerfService.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"pktApi\",\n  \"chrome://pocket/content/pktApi.jsm\"\n);\n\nconst STORIES_UPDATE_TIME = 30 * 60 * 1000; // 30 minutes\nconst TOPICS_UPDATE_TIME = 3 * 60 * 60 * 1000; // 3 hours\nconst STORIES_NOW_THRESHOLD = 24 * 60 * 60 * 1000; // 24 hours\nconst MIN_DOMAIN_AFFINITIES_UPDATE_TIME = 12 * 60 * 60 * 1000; // 12 hours\nconst DEFAULT_RECS_EXPIRE_TIME = 60 * 60 * 1000; // 1 hour\nconst SECTION_ID = \"topstories\";\nconst IMPRESSION_SOURCE = \"TOP_STORIES\";\nconst SPOC_IMPRESSION_TRACKING_PREF =\n  \"feeds.section.topstories.spoc.impressions\";\nconst DISCOVERY_STREAM_PREF_ENABLED = \"discoverystream.enabled\";\nconst DISCOVERY_STREAM_PREF_ENABLED_PATH =\n  \"browser.newtabpage.activity-stream.discoverystream.enabled\";\nconst REC_IMPRESSION_TRACKING_PREF = \"feeds.section.topstories.rec.impressions\";\nconst OPTIONS_PREF = \"feeds.section.topstories.options\";\nconst MAX_LIFETIME_CAP = 500; // Guard against misconfiguration on the server\nconst DISCOVERY_STREAM_PREF = \"discoverystream.config\";\n\nthis.TopStoriesFeed = class TopStoriesFeed {\n  constructor(ds) {\n    // Use discoverystream config pref default values for fast path and\n    // if needed lazy load activity stream top stories feed based on\n    // actual user preference when INIT and PREF_CHANGED is invoked\n    this.discoveryStreamEnabled =\n      ds &&\n      ds.value &&\n      JSON.parse(ds.value).enabled &&\n      Services.prefs.getBoolPref(DISCOVERY_STREAM_PREF_ENABLED_PATH, false);\n    if (!this.discoveryStreamEnabled) {\n      this.initializeProperties();\n    }\n  }\n\n  initializeProperties() {\n    this.contentUpdateQueue = [];\n    this.spocCampaignMap = new Map();\n    this.cache = new PersistentCache(SECTION_ID, true);\n    this._prefs = new Prefs();\n    this.propertiesInitialized = true;\n  }\n\n  async onInit() {\n    SectionsManager.enableSection(SECTION_ID);\n    if (this.discoveryStreamEnabled) {\n      return;\n    }\n\n    try {\n      const { options } = SectionsManager.sections.get(SECTION_ID);\n      const apiKey = this.getApiKeyFromPref(options.api_key_pref);\n      this.stories_endpoint = this.produceFinalEndpointUrl(\n        options.stories_endpoint,\n        apiKey\n      );\n      this.topics_endpoint = this.produceFinalEndpointUrl(\n        options.topics_endpoint,\n        apiKey\n      );\n      this.read_more_endpoint = options.read_more_endpoint;\n      this.stories_referrer = options.stories_referrer;\n      this.personalized = options.personalized;\n      this.show_spocs = options.show_spocs;\n      this.maxHistoryQueryResults = options.maxHistoryQueryResults;\n      this.storiesLastUpdated = 0;\n      this.topicsLastUpdated = 0;\n      this.storiesLoaded = false;\n      this.domainAffinitiesLastUpdated = 0;\n      this.processAffinityProividerVersion(options);\n      this.dispatchPocketCta(this._prefs.get(\"pocketCta\"), false);\n      Services.obs.addObserver(this, \"idle-daily\");\n\n      // Cache is used for new page loads, which shouldn't have changed data.\n      // If we have changed data, cache should be cleared,\n      // and last updated should be 0, and we can fetch.\n      let { stories, topics } = await this.loadCachedData();\n      if (this.storiesLastUpdated === 0) {\n        stories = await this.fetchStories();\n      }\n      if (this.topicsLastUpdated === 0) {\n        topics = await this.fetchTopics();\n      }\n      this.doContentUpdate({ stories, topics }, true);\n      this.storiesLoaded = true;\n\n      // This is filtered so an update function can return true to retry on the next run\n      this.contentUpdateQueue = this.contentUpdateQueue.filter(update =>\n        update()\n      );\n    } catch (e) {\n      Cu.reportError(`Problem initializing top stories feed: ${e.message}`);\n    }\n  }\n\n  init() {\n    SectionsManager.onceInitialized(this.onInit.bind(this));\n  }\n\n  observe(subject, topic, data) {\n    switch (topic) {\n      case \"idle-daily\":\n        this.updateDomainAffinityScores();\n        break;\n    }\n  }\n\n  async clearCache() {\n    await this.cache.set(\"stories\", {});\n    await this.cache.set(\"topics\", {});\n    await this.cache.set(\"spocs\", {});\n  }\n\n  uninit() {\n    this.storiesLoaded = false;\n    try {\n      Services.obs.removeObserver(this, \"idle-daily\");\n    } catch (e) {\n      // Attempt to remove unassociated observer which is possible when discovery stream\n      // is enabled and user never used activity stream experience\n    }\n    SectionsManager.disableSection(SECTION_ID);\n  }\n\n  getPocketState(target) {\n    const action = { type: at.POCKET_LOGGED_IN, data: pktApi.isUserLoggedIn() };\n    this.store.dispatch(ac.OnlyToOneContent(action, target));\n  }\n\n  dispatchPocketCta(data, shouldBroadcast) {\n    const action = { type: at.POCKET_CTA, data: JSON.parse(data) };\n    this.store.dispatch(\n      shouldBroadcast\n        ? ac.BroadcastToContent(action)\n        : ac.AlsoToPreloaded(action)\n    );\n  }\n\n  /**\n   * doContentUpdate - Updates topics and stories in the topstories section.\n   *\n   *                   Sections have one update action for the whole section.\n   *                   Redux creates a state race condition if you call the same action,\n   *                   twice, concurrently. Because of this, doContentUpdate is\n   *                   one place to update both topics and stories in a single action.\n   *\n   *                   Section updates used old topics if none are available,\n   *                   but clear stories if none are available. Because of this, if no\n   *                   stories are passed, we instead use the existing stories in state.\n   *\n   * @param {Object} This is an object with potential new stories or topics.\n   * @param {Boolean} shouldBroadcast If we should update existing tabs or not. For first page\n   *                  loads or pref changes, we want to update existing tabs,\n   *                  for system tick or other updates we do not.\n   */\n  doContentUpdate({ stories, topics }, shouldBroadcast) {\n    let updateProps = {};\n    if (stories) {\n      updateProps.rows = stories;\n    } else {\n      const { Sections } = this.store.getState();\n      if (Sections && Sections.find) {\n        updateProps.rows = Sections.find(s => s.id === SECTION_ID).rows;\n      }\n    }\n    if (topics) {\n      Object.assign(updateProps, {\n        topics,\n        read_more_endpoint: this.read_more_endpoint,\n      });\n    }\n\n    // We should only be calling this once per init.\n    this.dispatchUpdateEvent(shouldBroadcast, updateProps);\n  }\n\n  async onPersonalityProviderInit() {\n    const data = await this.cache.get();\n    let stories = data.stories && data.stories.recommendations;\n    this.stories = this.rotate(this.transform(stories));\n    this.doContentUpdate({ stories: this.stories }, false);\n\n    const affinities = this.affinityProvider.getAffinities();\n    this.domainAffinitiesLastUpdated = Date.now();\n    affinities._timestamp = this.domainAffinitiesLastUpdated;\n    this.cache.set(\"domainAffinities\", affinities);\n  }\n\n  affinityProividerSwitcher(...args) {\n    const { affinityProviderV2 } = this;\n    if (affinityProviderV2 && affinityProviderV2.use_v2) {\n      const provider = this.PersonalityProvider(...args, {\n        modelKeys: affinityProviderV2.model_keys,\n        dispatch: this.store.dispatch,\n      });\n      provider.init(this.onPersonalityProviderInit.bind(this));\n      return provider;\n    }\n\n    const start = perfService.absNow();\n    const v1Provider = this.UserDomainAffinityProvider(...args);\n    this.store.dispatch(\n      ac.PerfEvent({\n        event: \"topstories.domain.affinity.calculation.ms\",\n        value: Math.round(perfService.absNow() - start),\n      })\n    );\n\n    return v1Provider;\n  }\n\n  PersonalityProvider(...args) {\n    return new PersonalityProvider(...args);\n  }\n\n  UserDomainAffinityProvider(...args) {\n    return new UserDomainAffinityProvider(...args);\n  }\n\n  async fetchStories() {\n    if (!this.stories_endpoint) {\n      return null;\n    }\n    try {\n      const response = await fetch(this.stories_endpoint, {\n        credentials: \"omit\",\n      });\n      if (!response.ok) {\n        throw new Error(\n          `Stories endpoint returned unexpected status: ${response.status}`\n        );\n      }\n\n      const body = await response.json();\n      this.updateSettings(body.settings);\n      this.stories = this.rotate(this.transform(body.recommendations));\n      this.cleanUpTopRecImpressionPref();\n\n      if (this.show_spocs && body.spocs) {\n        this.spocCampaignMap = new Map(\n          body.spocs.map(s => [s.id, `${s.campaign_id}`])\n        );\n        this.spocs = this.transform(body.spocs).filter(\n          s => s.score >= s.min_score\n        );\n        this.cleanUpCampaignImpressionPref();\n      }\n      this.storiesLastUpdated = Date.now();\n      body._timestamp = this.storiesLastUpdated;\n      this.cache.set(\"stories\", body);\n    } catch (error) {\n      Cu.reportError(`Failed to fetch content: ${error.message}`);\n    }\n    return this.stories;\n  }\n\n  async loadCachedData() {\n    const data = await this.cache.get();\n    let stories = data.stories && data.stories.recommendations;\n    let topics = data.topics && data.topics.topics;\n\n    let affinities = data.domainAffinities;\n    if (this.personalized && affinities && affinities.scores) {\n      this.affinityProvider = this.affinityProividerSwitcher(\n        affinities.timeSegments,\n        affinities.parameterSets,\n        affinities.maxHistoryQueryResults,\n        affinities.version,\n        affinities.scores\n      );\n      this.domainAffinitiesLastUpdated = affinities._timestamp;\n    }\n    if (stories && !!stories.length && this.storiesLastUpdated === 0) {\n      this.updateSettings(data.stories.settings);\n      this.stories = this.rotate(this.transform(stories));\n      this.storiesLastUpdated = data.stories._timestamp;\n      if (data.stories.spocs && data.stories.spocs.length) {\n        this.spocCampaignMap = new Map(\n          data.stories.spocs.map(s => [s.id, `${s.campaign_id}`])\n        );\n        this.spocs = this.transform(data.stories.spocs).filter(\n          s => s.score >= s.min_score\n        );\n        this.cleanUpCampaignImpressionPref();\n      }\n    }\n    if (topics && !!topics.length && this.topicsLastUpdated === 0) {\n      this.topics = topics;\n      this.topicsLastUpdated = data.topics._timestamp;\n    }\n\n    return { topics: this.topics, stories: this.stories };\n  }\n\n  dispatchRelevanceScore(start) {\n    let event = \"PERSONALIZATION_V1_ITEM_RELEVANCE_SCORE_DURATION\";\n    let initialized = true;\n    if (!this.personalized) {\n      return;\n    }\n    const { affinityProviderV2 } = this;\n    if (affinityProviderV2 && affinityProviderV2.use_v2) {\n      if (this.affinityProvider) {\n        initialized = this.affinityProvider.initialized;\n        event = \"PERSONALIZATION_V2_ITEM_RELEVANCE_SCORE_DURATION\";\n      }\n    }\n\n    // If v2 is not yet initialized we don't bother tracking yet.\n    // Before it is initialized it doesn't do any ranking.\n    // Once it's initialized it ensures ranking is done.\n    // v1 doesn't have any initialized issues around ranking,\n    // and should be ready right away.\n    if (initialized) {\n      this.store.dispatch(\n        ac.PerfEvent({\n          event,\n          value: Math.round(perfService.absNow() - start),\n        })\n      );\n    }\n  }\n\n  transform(items) {\n    if (!items) {\n      return [];\n    }\n\n    const scoreStart = perfService.absNow();\n    const calcResult = items\n      .filter(s => !NewTabUtils.blockedLinks.isBlocked({ url: s.url }))\n      .map(s => {\n        let mapped = {\n          guid: s.id,\n          hostname: s.domain || shortURL(Object.assign({}, s, { url: s.url })),\n          type:\n            Date.now() - s.published_timestamp * 1000 <= STORIES_NOW_THRESHOLD\n              ? \"now\"\n              : \"trending\",\n          context: s.context,\n          icon: s.icon,\n          title: s.title,\n          description: s.excerpt,\n          image: this.normalizeUrl(s.image_src),\n          referrer: this.stories_referrer,\n          url: s.url,\n          min_score: s.min_score || 0,\n          score:\n            this.personalized && this.affinityProvider\n              ? this.affinityProvider.calculateItemRelevanceScore(s)\n              : s.item_score || 1,\n          spoc_meta: this.show_spocs\n            ? { campaign_id: s.campaign_id, caps: s.caps }\n            : {},\n        };\n\n        // Very old cached spocs may not contain an `expiration_timestamp` property\n        if (s.expiration_timestamp) {\n          mapped.expiration_timestamp = s.expiration_timestamp;\n        }\n\n        return mapped;\n      })\n      .sort(this.personalized ? this.compareScore : (a, b) => 0);\n\n    this.dispatchRelevanceScore(scoreStart);\n    return calcResult;\n  }\n\n  async fetchTopics() {\n    if (!this.topics_endpoint) {\n      return null;\n    }\n    try {\n      const response = await fetch(this.topics_endpoint, {\n        credentials: \"omit\",\n      });\n      if (!response.ok) {\n        throw new Error(\n          `Topics endpoint returned unexpected status: ${response.status}`\n        );\n      }\n      const body = await response.json();\n      const { topics } = body;\n      if (topics) {\n        this.topics = topics;\n        this.topicsLastUpdated = Date.now();\n        body._timestamp = this.topicsLastUpdated;\n        this.cache.set(\"topics\", body);\n      }\n    } catch (error) {\n      Cu.reportError(`Failed to fetch topics: ${error.message}`);\n    }\n    return this.topics;\n  }\n\n  dispatchUpdateEvent(shouldBroadcast, data) {\n    SectionsManager.updateSection(SECTION_ID, data, shouldBroadcast);\n  }\n\n  compareScore(a, b) {\n    return b.score - a.score;\n  }\n\n  updateSettings(settings) {\n    if (!this.personalized) {\n      return;\n    }\n\n    this.spocsPerNewTabs = settings.spocsPerNewTabs; // Probability of a new tab getting a spoc [0,1]\n    this.timeSegments = settings.timeSegments;\n    this.domainAffinityParameterSets = settings.domainAffinityParameterSets;\n    this.recsExpireTime = settings.recsExpireTime;\n    this.version = settings.version;\n\n    if (\n      this.affinityProvider &&\n      this.affinityProvider.version !== this.version\n    ) {\n      this.resetDomainAffinityScores();\n    }\n  }\n\n  updateDomainAffinityScores() {\n    if (\n      !this.personalized ||\n      !this.domainAffinityParameterSets ||\n      Date.now() - this.domainAffinitiesLastUpdated <\n        MIN_DOMAIN_AFFINITIES_UPDATE_TIME\n    ) {\n      return;\n    }\n\n    this.affinityProvider = this.affinityProividerSwitcher(\n      this.timeSegments,\n      this.domainAffinityParameterSets,\n      this.maxHistoryQueryResults,\n      this.version,\n      undefined\n    );\n\n    const affinities = this.affinityProvider.getAffinities();\n    this.domainAffinitiesLastUpdated = Date.now();\n    affinities._timestamp = this.domainAffinitiesLastUpdated;\n    this.cache.set(\"domainAffinities\", affinities);\n  }\n\n  resetDomainAffinityScores() {\n    delete this.affinityProvider;\n    this.cache.set(\"domainAffinities\", {});\n  }\n\n  // If personalization is turned on, we have to rotate stories on the client so that\n  // active stories are at the front of the list, followed by stories that have expired\n  // impressions i.e. have been displayed for longer than recsExpireTime.\n  rotate(items) {\n    if (!this.personalized || items.length <= 3) {\n      return items;\n    }\n\n    const maxImpressionAge = Math.max(\n      this.recsExpireTime * 1000 || DEFAULT_RECS_EXPIRE_TIME,\n      DEFAULT_RECS_EXPIRE_TIME\n    );\n    const impressions = this.readImpressionsPref(REC_IMPRESSION_TRACKING_PREF);\n    const expired = [];\n    const active = [];\n    for (const item of items) {\n      if (\n        impressions[item.guid] &&\n        Date.now() - impressions[item.guid] >= maxImpressionAge\n      ) {\n        expired.push(item);\n      } else {\n        active.push(item);\n      }\n    }\n    return active.concat(expired);\n  }\n\n  getApiKeyFromPref(apiKeyPref) {\n    if (!apiKeyPref) {\n      return apiKeyPref;\n    }\n\n    return (\n      this._prefs.get(apiKeyPref) || Services.prefs.getCharPref(apiKeyPref)\n    );\n  }\n\n  produceFinalEndpointUrl(url, apiKey) {\n    if (!url) {\n      return url;\n    }\n    if (url.includes(\"$apiKey\") && !apiKey) {\n      throw new Error(`An API key was specified but none configured: ${url}`);\n    }\n    return url.replace(\"$apiKey\", apiKey);\n  }\n\n  // Need to remove parenthesis from image URLs as React will otherwise\n  // fail to render them properly as part of the card template.\n  normalizeUrl(url) {\n    if (url) {\n      return url.replace(/\\(/g, \"%28\").replace(/\\)/g, \"%29\");\n    }\n    return url;\n  }\n\n  shouldShowSpocs() {\n    return this.show_spocs && this.store.getState().Prefs.values.showSponsored;\n  }\n\n  dispatchSpocDone(target) {\n    const action = { type: at.POCKET_WAITING_FOR_SPOC, data: false };\n    this.store.dispatch(ac.OnlyToOneContent(action, target));\n  }\n\n  filterSpocs() {\n    if (!this.shouldShowSpocs()) {\n      return [];\n    }\n\n    if (Math.random() > this.spocsPerNewTabs) {\n      return [];\n    }\n\n    if (!this.spocs || !this.spocs.length) {\n      // We have stories but no spocs so there's nothing to do and this update can be\n      // removed from the queue.\n      return [];\n    }\n\n    // Filter spocs based on frequency caps\n    const impressions = this.readImpressionsPref(SPOC_IMPRESSION_TRACKING_PREF);\n    let spocs = this.spocs.filter(s =>\n      this.isBelowFrequencyCap(impressions, s)\n    );\n\n    // Filter out expired spocs based on `expiration_timestamp`\n    spocs = spocs.filter(spoc => {\n      // If cached data is so old it doesn't contain this property, assume the spoc is ok to show\n      if (!(`expiration_timestamp` in spoc)) {\n        return true;\n      }\n      // `expiration_timestamp` is the number of seconds elapsed since January 1, 1970 00:00:00 UTC\n      return spoc.expiration_timestamp * 1000 > Date.now();\n    });\n\n    return spocs;\n  }\n\n  maybeAddSpoc(target) {\n    const updateContent = () => {\n      let spocs = this.filterSpocs();\n\n      if (!spocs.length) {\n        this.dispatchSpocDone(target);\n        return false;\n      }\n\n      // Create a new array with a spoc inserted at index 2\n      const section = this.store\n        .getState()\n        .Sections.find(s => s.id === SECTION_ID);\n      let rows = section.rows.slice(0, this.stories.length);\n      rows.splice(2, 0, Object.assign(spocs[0], { pinned: true }));\n\n      // Send a content update to the target tab\n      const action = {\n        type: at.SECTION_UPDATE,\n        data: Object.assign({ rows }, { id: SECTION_ID }),\n      };\n      this.store.dispatch(ac.OnlyToOneContent(action, target));\n      this.dispatchSpocDone(target);\n      return false;\n    };\n\n    if (this.storiesLoaded) {\n      updateContent();\n    } else {\n      // Delay updating tab content until initial data has been fetched\n      this.contentUpdateQueue.push(updateContent);\n    }\n  }\n\n  // Frequency caps are based on campaigns, which may include multiple spocs.\n  // We currently support two types of frequency caps:\n  // - lifetime: Indicates how many times spocs from a campaign can be shown in total\n  // - period: Indicates how many times spocs from a campaign can be shown within a period\n  //\n  // So, for example, the feed configuration below defines that for campaign 1 no more\n  // than 5 spocs can be show in total, and no more than 2 per hour.\n  // \"campaign_id\": 1,\n  // \"caps\": {\n  //  \"lifetime\": 5,\n  //  \"campaign\": {\n  //    \"count\": 2,\n  //    \"period\": 3600\n  //  }\n  // }\n  isBelowFrequencyCap(impressions, spoc) {\n    const campaignImpressions = impressions[spoc.spoc_meta.campaign_id];\n    if (!campaignImpressions) {\n      return true;\n    }\n\n    const lifeTimeCap = Math.min(\n      spoc.spoc_meta.caps && spoc.spoc_meta.caps.lifetime,\n      MAX_LIFETIME_CAP\n    );\n    const lifeTimeCapExceeded = campaignImpressions.length >= lifeTimeCap;\n    if (lifeTimeCapExceeded) {\n      return false;\n    }\n\n    const campaignCap =\n      (spoc.spoc_meta.caps && spoc.spoc_meta.caps.campaign) || {};\n    const campaignCapExceeded =\n      campaignImpressions.filter(\n        i => Date.now() - i < campaignCap.period * 1000\n      ).length >= campaignCap.count;\n    return !campaignCapExceeded;\n  }\n\n  // Clean up campaign impression pref by removing all campaigns that are no\n  // longer part of the response, and are therefore considered inactive.\n  cleanUpCampaignImpressionPref() {\n    const campaignIds = new Set(this.spocCampaignMap.values());\n    this.cleanUpImpressionPref(\n      id => !campaignIds.has(id),\n      SPOC_IMPRESSION_TRACKING_PREF\n    );\n  }\n\n  // Clean up rec impression pref by removing all stories that are no\n  // longer part of the response.\n  cleanUpTopRecImpressionPref() {\n    const activeStories = new Set(this.stories.map(s => `${s.guid}`));\n    this.cleanUpImpressionPref(\n      id => !activeStories.has(id),\n      REC_IMPRESSION_TRACKING_PREF\n    );\n  }\n\n  /**\n   * Cleans up the provided impression pref (spocs or recs).\n   *\n   * @param isExpired predicate (boolean-valued function) that returns whether or not\n   * the impression for the given key is expired.\n   * @param pref the impression pref to clean up.\n   */\n  cleanUpImpressionPref(isExpired, pref) {\n    const impressions = this.readImpressionsPref(pref);\n    let changed = false;\n\n    Object.keys(impressions).forEach(id => {\n      if (isExpired(id)) {\n        changed = true;\n        delete impressions[id];\n      }\n    });\n\n    if (changed) {\n      this.writeImpressionsPref(pref, impressions);\n    }\n  }\n\n  // Sets a pref mapping campaign IDs to timestamp arrays.\n  // The timestamps represent impressions which are used to calculate frequency caps.\n  recordCampaignImpression(campaignId) {\n    let impressions = this.readImpressionsPref(SPOC_IMPRESSION_TRACKING_PREF);\n\n    const timeStamps = impressions[campaignId] || [];\n    timeStamps.push(Date.now());\n    impressions = Object.assign(impressions, { [campaignId]: timeStamps });\n\n    this.writeImpressionsPref(SPOC_IMPRESSION_TRACKING_PREF, impressions);\n  }\n\n  // Sets a pref mapping story (rec) IDs to a single timestamp (time of first impression).\n  // We use these timestamps to guarantee a story doesn't stay on top for longer than\n  // configured in the feed settings (settings.recsExpireTime).\n  recordTopRecImpressions(topItems) {\n    let impressions = this.readImpressionsPref(REC_IMPRESSION_TRACKING_PREF);\n    let changed = false;\n\n    topItems.forEach(t => {\n      if (!impressions[t]) {\n        changed = true;\n        impressions = Object.assign(impressions, { [t]: Date.now() });\n      }\n    });\n\n    if (changed) {\n      this.writeImpressionsPref(REC_IMPRESSION_TRACKING_PREF, impressions);\n    }\n  }\n\n  readImpressionsPref(pref) {\n    const prefVal = this._prefs.get(pref);\n    return prefVal ? JSON.parse(prefVal) : {};\n  }\n\n  writeImpressionsPref(pref, impressions) {\n    this._prefs.set(pref, JSON.stringify(impressions));\n  }\n\n  async removeSpocs() {\n    // Quick hack so that SPOCS are removed from all open and preloaded tabs when\n    // they are disabled. The longer term fix should probably be to remove them\n    // in the Reducer.\n    await this.clearCache();\n    this.uninit();\n    this.init();\n  }\n\n  /**\n   * Decides if we need to change the personality provider version or not.\n   * Changes the version if it determines we need to.\n   *\n   * @param data {object} The top stories pref, we need version and model_keys\n   * @return {boolean} Returns true only if the version was changed.\n   */\n  processAffinityProividerVersion(data) {\n    const version2 = data.version === 2 && !this.affinityProviderV2;\n    const version1 = data.version === 1 && this.affinityProviderV2;\n    if (version2 || version1) {\n      if (version1) {\n        this.affinityProviderV2 = null;\n      } else {\n        this.affinityProviderV2 = {\n          use_v2: true,\n          model_keys: data.model_keys,\n        };\n      }\n      return true;\n    }\n    return false;\n  }\n\n  lazyLoadTopStories(dsPref) {\n    let _dsPref = dsPref;\n    if (!_dsPref) {\n      _dsPref = this.store.getState().Prefs.values[DISCOVERY_STREAM_PREF];\n    }\n\n    try {\n      this.discoveryStreamEnabled =\n        JSON.parse(_dsPref).enabled &&\n        this.store.getState().Prefs.values[DISCOVERY_STREAM_PREF_ENABLED];\n    } catch (e) {\n      // Load activity stream top stories if fail to determine discovery stream state\n      this.discoveryStreamEnabled = false;\n    }\n\n    // Return without invoking initialization if top stories are loaded\n    if (this.storiesLoaded) {\n      return;\n    }\n\n    if (!this.discoveryStreamEnabled && !this.propertiesInitialized) {\n      this.initializeProperties();\n    }\n    this.init();\n  }\n\n  handleDisabled(action) {\n    switch (action.type) {\n      case at.INIT:\n        this.lazyLoadTopStories();\n        break;\n      case at.PREF_CHANGED:\n        if (action.data.name === DISCOVERY_STREAM_PREF) {\n          this.lazyLoadTopStories(action.data.value);\n        }\n        if (action.data.name === DISCOVERY_STREAM_PREF_ENABLED) {\n          this.lazyLoadTopStories();\n        }\n        break;\n      case at.UNINIT:\n        this.uninit();\n        break;\n    }\n  }\n\n  async onAction(action) {\n    if (this.discoveryStreamEnabled) {\n      this.handleDisabled(action);\n      return;\n    }\n    switch (action.type) {\n      // Check discoverystream pref and load activity stream top stories only if needed\n      case at.INIT:\n        this.lazyLoadTopStories();\n        break;\n      case at.SYSTEM_TICK:\n        let stories;\n        let topics;\n        if (Date.now() - this.storiesLastUpdated >= STORIES_UPDATE_TIME) {\n          stories = await this.fetchStories();\n        }\n        if (Date.now() - this.topicsLastUpdated >= TOPICS_UPDATE_TIME) {\n          topics = await this.fetchTopics();\n        }\n        this.doContentUpdate({ stories, topics }, false);\n        break;\n      case at.UNINIT:\n        this.uninit();\n        break;\n      case at.NEW_TAB_REHYDRATED:\n        this.getPocketState(action.meta.fromTarget);\n        this.maybeAddSpoc(action.meta.fromTarget);\n        break;\n      case at.SECTION_OPTIONS_CHANGED:\n        if (action.data === SECTION_ID) {\n          await this.clearCache();\n          this.uninit();\n          this.init();\n        }\n        break;\n      case at.PLACES_LINK_BLOCKED:\n        if (this.spocs) {\n          this.spocs = this.spocs.filter(s => s.url !== action.data.url);\n        }\n        break;\n      case at.PLACES_HISTORY_CLEARED:\n        if (this.personalized) {\n          this.resetDomainAffinityScores();\n        }\n        break;\n      case at.TELEMETRY_IMPRESSION_STATS: {\n        // We want to make sure we only track impressions from Top Stories,\n        // otherwise unexpected things that are not properly handled can happen.\n        // Example: Impressions from spocs on Discovery Stream can cause the\n        // Top Stories impressions pref to continuously grow, see bug #1523408\n        if (action.data.source === IMPRESSION_SOURCE) {\n          const payload = action.data;\n          const viewImpression = !(\n            \"click\" in payload ||\n            \"block\" in payload ||\n            \"pocket\" in payload\n          );\n          if (payload.tiles && viewImpression) {\n            if (this.shouldShowSpocs()) {\n              payload.tiles.forEach(t => {\n                if (this.spocCampaignMap.has(t.id)) {\n                  this.recordCampaignImpression(this.spocCampaignMap.get(t.id));\n                }\n              });\n            }\n            if (this.personalized) {\n              const topRecs = payload.tiles\n                .filter(t => !this.spocCampaignMap.has(t.id))\n                .map(t => t.id);\n              this.recordTopRecImpressions(topRecs);\n            }\n          }\n        }\n        break;\n      }\n      case at.PREF_CHANGED:\n        if (action.data.name === DISCOVERY_STREAM_PREF) {\n          this.lazyLoadTopStories(action.data.value);\n        }\n        // Check if spocs was disabled. Remove them if they were.\n        if (action.data.name === \"showSponsored\" && !action.data.value) {\n          await this.removeSpocs();\n        }\n        if (action.data.name === \"pocketCta\") {\n          this.dispatchPocketCta(action.data.value, true);\n        }\n        if (action.data.name === OPTIONS_PREF) {\n          try {\n            const options = JSON.parse(action.data.value);\n            if (this.processAffinityProividerVersion(options)) {\n              await this.clearCache();\n              this.uninit();\n              this.init();\n            }\n          } catch (e) {\n            Cu.reportError(\n              `Problem initializing affinity provider v2: ${e.message}`\n            );\n          }\n        }\n        break;\n    }\n  }\n};\n\nthis.STORIES_UPDATE_TIME = STORIES_UPDATE_TIME;\nthis.TOPICS_UPDATE_TIME = TOPICS_UPDATE_TIME;\nthis.SECTION_ID = SECTION_ID;\nthis.SPOC_IMPRESSION_TRACKING_PREF = SPOC_IMPRESSION_TRACKING_PREF;\nthis.REC_IMPRESSION_TRACKING_PREF = REC_IMPRESSION_TRACKING_PREF;\nthis.MIN_DOMAIN_AFFINITIES_UPDATE_TIME = MIN_DOMAIN_AFFINITIES_UPDATE_TIME;\nthis.DEFAULT_RECS_EXPIRE_TIME = DEFAULT_RECS_EXPIRE_TIME;\nconst EXPORTED_SYMBOLS = [\n  \"TopStoriesFeed\",\n  \"STORIES_UPDATE_TIME\",\n  \"TOPICS_UPDATE_TIME\",\n  \"SECTION_ID\",\n  \"SPOC_IMPRESSION_TRACKING_PREF\",\n  \"MIN_DOMAIN_AFFINITIES_UPDATE_TIME\",\n  \"REC_IMPRESSION_TRACKING_PREF\",\n  \"DEFAULT_RECS_EXPIRE_TIME\",\n];\n"
  },
  {
    "path": "lib/UTEventReporting.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\n\"use strict\";\n\nconst { Services } = ChromeUtils.import(\"resource://gre/modules/Services.jsm\");\n\n/**\n * Note: the schema can be found in\n * https://searchfox.org/mozilla-central/source/toolkit/components/telemetry/Events.yaml\n */\nconst EXTRAS_FIELD_NAMES = [\n  \"addon_version\",\n  \"session_id\",\n  \"page\",\n  \"user_prefs\",\n  \"action_position\",\n];\n\nthis.UTEventReporting = class UTEventReporting {\n  constructor() {\n    Services.telemetry.setEventRecordingEnabled(\"activity_stream\", true);\n    this.sendUserEvent = this.sendUserEvent.bind(this);\n    this.sendSessionEndEvent = this.sendSessionEndEvent.bind(this);\n    this.sendTrailheadEnrollEvent = this.sendTrailheadEnrollEvent.bind(this);\n  }\n\n  _createExtras(data) {\n    // Make a copy of the given data and delete/modify it as needed.\n    let utExtras = Object.assign({}, data);\n    for (let field of Object.keys(utExtras)) {\n      if (EXTRAS_FIELD_NAMES.includes(field)) {\n        utExtras[field] = String(utExtras[field]);\n        continue;\n      }\n      delete utExtras[field];\n    }\n    return utExtras;\n  }\n\n  sendUserEvent(data) {\n    let mainFields = [\"event\", \"source\"];\n    let eventFields = mainFields.map(field => String(data[field]) || null);\n\n    Services.telemetry.recordEvent(\n      \"activity_stream\",\n      \"event\",\n      ...eventFields,\n      this._createExtras(data)\n    );\n  }\n\n  sendSessionEndEvent(data) {\n    Services.telemetry.recordEvent(\n      \"activity_stream\",\n      \"end\",\n      \"session\",\n      String(data.session_duration),\n      this._createExtras(data)\n    );\n  }\n\n  sendTrailheadEnrollEvent(data) {\n    Services.telemetry.recordEvent(\n      \"activity_stream\",\n      \"enroll\",\n      \"preference_study\",\n      data.experiment,\n      {\n        experimentType: data.type,\n        branch: data.branch,\n      }\n    );\n  }\n\n  uninit() {\n    Services.telemetry.setEventRecordingEnabled(\"activity_stream\", false);\n  }\n};\n\nconst EXPORTED_SYMBOLS = [\"UTEventReporting\"];\n"
  },
  {
    "path": "lib/UserDomainAffinityProvider.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\"use strict\";\n\nconst { Services } = ChromeUtils.import(\"resource://gre/modules/Services.jsm\");\n\nChromeUtils.defineModuleGetter(\n  this,\n  \"PlacesUtils\",\n  \"resource://gre/modules/PlacesUtils.jsm\"\n);\n\nconst DEFAULT_TIME_SEGMENTS = [\n  { id: \"hour\", startTime: 3600, endTime: 0, weightPosition: 1 },\n  { id: \"day\", startTime: 86400, endTime: 3600, weightPosition: 0.75 },\n  { id: \"week\", startTime: 604800, endTime: 86400, weightPosition: 0.5 },\n  { id: \"weekPlus\", startTime: 0, endTime: 604800, weightPosition: 0.25 },\n  { id: \"alltime\", startTime: 0, endTime: 0, weightPosition: 0.25 },\n];\n\nconst DEFAULT_PARAMETER_SETS = {\n  \"linear-frequency\": {\n    recencyFactor: 0.4,\n    frequencyFactor: 0.5,\n    combinedDomainFactor: 0.5,\n    perfectFrequencyVisits: 10,\n    perfectCombinedDomainScore: 2,\n    multiDomainBoost: 0.1,\n    itemScoreFactor: 0,\n  },\n};\n\nconst DEFAULT_MAX_HISTORY_QUERY_RESULTS = 1000;\n\nfunction merge(...args) {\n  return Object.assign.apply(this, args);\n}\n\n/**\n * Provides functionality to personalize content recommendations by calculating\n * user domain affinity scores. These scores are used to calculate relevance\n * scores for items/recs/stories that have domain affinities.\n *\n * The algorithm works as follows:\n *\n * - The recommendation endpoint returns a settings object containing\n * timeSegments and parametersets.\n *\n * - For every time segment we calculate the corresponding domain visit counts,\n * yielding result objects of the following structure: {\"mozilla.org\": 12,\n * \"mozilla.com\": 34} (see UserDomainAffinityProvider#queryVisits)\n *\n * - These visit counts are transformed to domain affinity scores for all\n * provided parameter sets: {\"mozilla.org\": {\"paramSet1\": 0.8,\n * \"paramSet2\": 0.9}, \"mozilla.org\": {\"paramSet1\": 1, \"paramSet2\": 0.9}}\n * (see UserDomainAffinityProvider#calculateScoresForParameterSets)\n *\n * - The parameter sets provide factors for weighting which allows for\n * flexible targeting. The functionality to calculate final scores can\n * be seen in UserDomainAffinityProvider#calculateScores\n *\n * - The user domain affinity scores are summed up across all time segments\n * see UserDomainAffinityProvider#calculateAllUserDomainAffinityScores\n *\n * - An item's domain affinities are matched to the user's domain affinity\n * scores by calculating an item relevance score\n * (see UserDomainAffinityProvider#calculateItemRelevanceScore)\n *\n * - The item relevance scores are used to sort items (see TopStoriesFeed for\n * more details)\n *\n * - The data structure was chosen to allow for fast cache lookups during\n * relevance score calculation. While user domain affinities are calculated\n * infrequently (i.e. only once a day), the item relevance score (potentially)\n * needs to be calculated every time the feed updates. Therefore allowing cache\n * lookups of scores[domain][parameterSet] is beneficial\n */\nthis.UserDomainAffinityProvider = class UserDomainAffinityProvider {\n  constructor(\n    timeSegments = DEFAULT_TIME_SEGMENTS,\n    parameterSets = DEFAULT_PARAMETER_SETS,\n    maxHistoryQueryResults = DEFAULT_MAX_HISTORY_QUERY_RESULTS,\n    version,\n    scores\n  ) {\n    this.timeSegments = timeSegments;\n    this.maxHistoryQueryResults = maxHistoryQueryResults;\n    this.version = version;\n    if (scores) {\n      this.parameterSets = parameterSets;\n      this.scores = scores;\n    } else {\n      this.parameterSets = this.prepareParameterSets(parameterSets);\n      this.scores = this.calculateAllUserDomainAffinityScores();\n    }\n  }\n\n  /**\n   * Adds dynamic parameters to the given parameter sets that need to be\n   * computed based on time segments.\n   *\n   * @param ps The parameter sets\n   * @return Updated parameter sets with additional fields (i.e. timeSegmentWeights)\n   */\n  prepareParameterSets(ps) {\n    return (\n      Object.keys(ps)\n        // Add timeSegmentWeight fields to param sets e.g. timeSegmentWeights: {\"hour\": 1, \"day\": 0.8915, ...}\n        .map(k => ({\n          [k]: merge(ps[k], {\n            timeSegmentWeights: this.calculateTimeSegmentWeights(\n              ps[k].recencyFactor\n            ),\n          }),\n        }))\n        .reduce((acc, cur) => merge(acc, cur))\n    );\n  }\n\n  /**\n   * Calculates a time segment weight based on the provided recencyFactor.\n   *\n   * @param recencyFactor The recency factor indicating how to weigh recency\n   * @return An object containing time segment weights: {\"hour\": 0.987, \"day\": 1}\n   */\n  calculateTimeSegmentWeights(recencyFactor) {\n    return this.timeSegments.reduce(\n      (acc, cur) =>\n        merge(acc, {\n          [cur.id]: this.calculateScore(cur.weightPosition, 1, recencyFactor),\n        }),\n      {}\n    );\n  }\n\n  /**\n   * Calculates user domain affinity scores based on browsing history and the\n   * available times segments and parameter sets.\n   */\n  calculateAllUserDomainAffinityScores() {\n    return (\n      this.timeSegments\n        // Calculate parameter set specific domain scores for each time segment\n        // => [{\"a.com\": {\"ps1\": 12, \"ps2\": 34}, \"b.com\": {\"ps1\": 56, \"ps2\": 78}}, ...]\n        .map(ts => this.calculateUserDomainAffinityScores(ts))\n        // Keep format, but reduce to single object, with combined scores across all time segments\n        // => \"{a.com\":{\"ps1\":2,\"ps2\":2}, \"b.com\":{\"ps1\":3,\"ps2\":3}}\"\"\n        .reduce((acc, cur) => this._combineScores(acc, cur))\n    );\n  }\n\n  /**\n   * Calculates the user domain affinity scores for the given time segment.\n   *\n   * @param ts The time segment\n   * @return The parameter specific scores for all domains with visits in\n   * this time segment: {\"a.com\": {\"ps1\": 12, \"ps2\": 34}, \"b.com\" ...}\n   */\n  calculateUserDomainAffinityScores(ts) {\n    // Returns domains and visit counts for this time segment: {\"a.com\": 1, \"b.com\": 2}\n    let visits = this.queryVisits(ts);\n\n    return Object.keys(visits).reduce(\n      (acc, d) =>\n        merge(acc, {\n          [d]: this.calculateScoresForParameterSets(ts, visits[d]),\n        }),\n      {}\n    );\n  }\n\n  /**\n   * Calculates the scores for all parameter sets for the given time segment\n   * and domain visit count.\n   *\n   * @param ts The time segment\n   * @param vc The domain visit count in the given time segment\n   * @return The parameter specific scores for the visit count in\n   * this time segment: {\"ps1\": 12, \"ps2\": 34}\n   */\n  calculateScoresForParameterSets(ts, vc) {\n    return Object.keys(this.parameterSets).reduce(\n      (acc, ps) =>\n        merge(acc, {\n          [ps]: this.calculateScoreForParameterSet(\n            ts,\n            vc,\n            this.parameterSets[ps]\n          ),\n        }),\n      {}\n    );\n  }\n\n  /**\n   * Calculates the final affinity score in the given time segment for the given parameter set\n   *\n   * @param timeSegment The time segment\n   * @param visitCount The domain visit count in the given time segment\n   * @param parameterSet The parameter set to use for scoring\n   * @return The final score\n   */\n  calculateScoreForParameterSet(timeSegment, visitCount, parameterSet) {\n    return this.calculateScore(\n      visitCount * parameterSet.timeSegmentWeights[timeSegment.id],\n      parameterSet.perfectFrequencyVisits,\n      parameterSet.frequencyFactor\n    );\n  }\n\n  /**\n   * Keeps the same format, but reduces the two objects to a single object, with\n   * combined scores across all time segments  => {a.com\":{\"ps1\":2,\"ps2\":2},\n   * \"b.com\":{\"ps1\":3,\"ps2\":3}}\n   */\n  _combineScores(a, b) {\n    // Merge both score objects so we get a combined object holding all domains.\n    // This is so we can combine them without missing domains that are in a and not in b and vice versa.\n    const c = merge({}, a, b);\n    return Object.keys(c).reduce(\n      (acc, d) => merge(acc, this._combine(a, b, c, d)),\n      {}\n    );\n  }\n\n  _combine(a, b, c, d) {\n    return (\n      Object.keys(c[d])\n        // Summing up the parameter set specific scores of each domain\n        .map(ps => ({\n          [d]: {\n            [ps]: Math.min(\n              1,\n              ((a[d] && a[d][ps]) || 0) + ((b[d] && b[d][ps]) || 0)\n            ),\n          },\n        }))\n        // Reducing from an array of objects with a single parameter set to a single object\n        // [{\"a.com\":{\"ps1\":11}}, {\"a.com: {\"ps2\":12}}] => {\"a.com\":{\"ps1\":11,\"ps2\":12}}\n        .reduce((acc, cur) => ({ [d]: merge(acc[d], cur[d]) }))\n    );\n  }\n\n  /**\n   * Calculates a value on the curve described by the provided parameters. The curve we're using is\n   * (a^(b*x) - 1) / (a^b - 1): https://www.desmos.com/calculator/maqhpttupp\n   *\n   * @param {number} score A value between 0 and maxScore, representing x.\n   * @param {number} maxScore Highest possible score.\n   * @param {number} factor The slope describing the curve to get to maxScore. A low slope value\n   * [0, 0.5] results in a log-shaped curve, a high slope [0.5, 1] results in a exp-shaped curve,\n   * a slope of exactly 0.5 is linear.\n   * @param {number} ease Adjusts how much bend is in the curve i.e. how dramatic the maximum\n   * effect of the slope can be. This represents b in the formula above.\n   * @return {number} the final score\n   */\n  calculateScore(score, maxScore, factor, ease = 2) {\n    let a = 0;\n    let x = Math.max(0, score / maxScore);\n\n    if (x >= 1) {\n      return 1;\n    }\n\n    if (factor === 0.5) {\n      return x;\n    }\n\n    if (factor < 0.5) {\n      // We want a log-shaped curve so we scale \"a\" between 0 and .99\n      a = (factor / 0.5) * 0.49;\n    } else if (factor > 0.5) {\n      // We want an exp-shaped curve so we scale \"a\" between 1.01 and 10\n      a = 1 + ((factor - 0.5) / 0.5) * 9;\n    }\n\n    return (Math.pow(a, ease * x) - 1) / (Math.pow(a, ease) - 1);\n  }\n\n  /**\n   * Queries the visit counts in the given time segment.\n   *\n   * @param ts the time segment\n   * @return the visit count object: {\"a.com\": 1, \"b.com\": 2}\n   */\n  queryVisits(ts) {\n    const visitCounts = {};\n    const query = PlacesUtils.history.getNewQuery();\n    const wwwRegEx = /^www\\./;\n\n    query.beginTimeReference = query.TIME_RELATIVE_NOW;\n    query.beginTime =\n      ts.startTime && ts.startTime !== 0\n        ? -(ts.startTime * 1000 * 1000)\n        : -(Date.now() * 1000);\n\n    query.endTimeReference = query.TIME_RELATIVE_NOW;\n    query.endTime =\n      ts.endTime && ts.endTime !== 0 ? -(ts.endTime * 1000 * 1000) : 0;\n\n    const options = PlacesUtils.history.getNewQueryOptions();\n    options.sortingMode = options.SORT_BY_VISITCOUNT_DESCENDING;\n    options.maxResults = this.maxHistoryQueryResults;\n\n    const { root } = PlacesUtils.history.executeQuery(query, options);\n    root.containerOpen = true;\n    for (let i = 0; i < root.childCount; i++) {\n      let node = root.getChild(i);\n      let host = Services.io.newURI(node.uri).host.replace(wwwRegEx, \"\");\n      if (!visitCounts[host]) {\n        visitCounts[host] = 0;\n      }\n      visitCounts[host] += node.accessCount;\n    }\n    root.containerOpen = false;\n    return visitCounts;\n  }\n\n  /**\n   * Calculates an item's relevance score.\n   *\n   * @param item the item (story), must contain domain affinities, otherwise a\n   * score of 1 is returned.\n   * @return the calculated item's score or 1 if item has no domain_affinities\n   * or references an unknown parameter set.\n   */\n  calculateItemRelevanceScore(item) {\n    const params = this.parameterSets[item.parameter_set];\n    if (!item.domain_affinities || !params) {\n      return item.item_score;\n    }\n\n    const scores = Object.keys(item.domain_affinities).reduce(\n      (acc, d) => {\n        let userDomainAffinityScore = this.scores[d]\n          ? this.scores[d][item.parameter_set]\n          : false;\n        if (userDomainAffinityScore) {\n          acc.combinedDomainScore +=\n            userDomainAffinityScore * item.domain_affinities[d];\n          acc.matchingDomainsCount++;\n        }\n        return acc;\n      },\n      { combinedDomainScore: 0, matchingDomainsCount: 0 }\n    );\n\n    // Boost the score as configured in the provided parameter set\n    const boostedCombinedDomainScore =\n      scores.combinedDomainScore *\n      Math.pow(params.multiDomainBoost + 1, scores.matchingDomainsCount);\n\n    // Calculate what the score would be if the item score is ignored\n    const normalizedCombinedDomainScore = this.calculateScore(\n      boostedCombinedDomainScore,\n      params.perfectCombinedDomainScore,\n      params.combinedDomainFactor\n    );\n\n    // Calculate the final relevance score using the itemScoreFactor. The itemScoreFactor\n    // allows weighting the item score in relation to the normalizedCombinedDomainScore:\n    // An itemScoreFactor of 1 results in the item score and ignores the combined domain score\n    // An itemScoreFactor of 0.5 results in the the average of item score and combined domain score\n    // An itemScoreFactor of 0 results in the combined domain score and ignores the item score\n    return (\n      params.itemScoreFactor *\n        (item.item_score - normalizedCombinedDomainScore) +\n      normalizedCombinedDomainScore\n    );\n  }\n\n  /**\n   * Returns an object holding the settings and affinity scores of this provider instance.\n   */\n  getAffinities() {\n    return {\n      timeSegments: this.timeSegments,\n      parameterSets: this.parameterSets,\n      maxHistoryQueryResults: this.maxHistoryQueryResults,\n      version: this.version,\n      scores: this.scores,\n    };\n  }\n};\n\nconst EXPORTED_SYMBOLS = [\"UserDomainAffinityProvider\"];\n"
  },
  {
    "path": "loaders/inject-loader.js",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\n// Note: this is based on https://github.com/plasticine/inject-loader,\n// patched to make istanbul work properly\n\nconst loaderUtils = require(\"loader-utils\");\nconst QUOTE_REGEX_STRING = \"['|\\\"]{1}\";\n\nconst hasOnlyExcludeFlags = query =>\n  Object.keys(query).filter(key => query[key] === true).length === 0;\nconst escapePath = path => path.replace(\"/\", \"\\\\/\");\n\nfunction createRequireStringRegex(query) {\n  const regexArray = [];\n\n  // if there is no query then replace everything\n  if (Object.keys(query).length === 0) {\n    regexArray.push(\"([^\\\\)]+)\");\n  } else if (hasOnlyExcludeFlags(query)) {\n    // if there are only negation matches in the query then replace everything\n    // except them\n    Object.keys(query).forEach(key =>\n      regexArray.push(`(?!${QUOTE_REGEX_STRING}${escapePath(key)})`)\n    );\n    regexArray.push(\"([^\\\\)]+)\");\n  } else {\n    regexArray.push(`(${QUOTE_REGEX_STRING}(`);\n    regexArray.push(\n      Object.keys(query)\n        .map(key => escapePath(key))\n        .join(\"|\")\n    );\n    regexArray.push(`)${QUOTE_REGEX_STRING})`);\n  }\n\n  // Wrap the regex to match `require()`\n  regexArray.unshift(\"require\\\\(\");\n  regexArray.push(\"\\\\)\");\n\n  return new RegExp(regexArray.join(\"\"), \"g\");\n}\n\nmodule.exports = function inject(src) {\n  if (this.cacheable) {\n    this.cacheable();\n  }\n  const regex = createRequireStringRegex(loaderUtils.getOptions(this) || {});\n\n  return `module.exports = function inject(injections) {\n  var module = {exports: {}};\n  var exports = module.exports;\n  ${src.replace(regex, \"(injections[$1] || /* istanbul ignore next */ $&)\")}\n  return module.exports;\n}\\n`;\n};\n"
  },
  {
    "path": "mochitest.sh",
    "content": "#!/bin/bash\n\nexport SHELL=/bin/bash\nexport TASKCLUSTER_ROOT_URL=\"https://taskcluster.net\"\n# Display required for `browser_parsable_css` tests\nexport DISPLAY=:99.0\n# Required to support the unicode in the output\nexport LC_ALL=C.UTF-8\n/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x16 -extension RANDR\n\n# Pull latest m-c and update tip\ncd /mozilla-central && hg pull && hg update -C\n\n# Build Activity Stream and copy the output to m-c\ncd /activity-stream && npm install . && npm run buildmc\n\n# Build latest m-c with Activity Stream changes\ncd /mozilla-central && rm -rf ./objdir-frontend && ./mach build \\\n  && ./mach lint browser/components/newtab \\\n  && ./mach lint -l codespell browser/locales/en-US/browser/newtab \\\n  && ./mach test browser/components/newtab/test/browser --headless \\\n  && ./mach test browser/components/newtab/test/xpcshell \\\n  && ./mach test --log-tbpl test_run_log \\\n    browser/base/content/test/about/browser_aboutHome_search_telemetry.js \\\n    browser/base/content/test/static/browser_parsable_css.js \\\n    browser/base/content/test/tabs/browser_new_tab_in_privileged_process_pref.js \\\n    browser/components/enterprisepolicies/tests/browser/browser_policy_set_homepage.js \\\n    browser/components/extensions/test/browser/browser_ext_topSites.js \\\n    browser/components/preferences/in-content/tests/browser_hometab_restore_defaults.js \\\n    browser/components/preferences/in-content/tests/browser_newtab_menu.js \\\n    browser/components/preferences/in-content/tests/browser_search_subdialogs_within_preferences_1.js \\\n    browser/components/search/test/browser/browser_google_behavior.js \\\n    browser/modules/test/browser/browser_UsageTelemetry_content.js \\\n  && ! grep -q TEST-UNEXPECTED test_run_log \\\n  && RUN_FIND_DUPES=1 ./mach package \\\n  && ./mach test --appname=dist all_files_referenced --headless\n"
  },
  {
    "path": "moz.build",
    "content": "# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-\n# vim: set filetype=python:\n# This Source Code Form is subject to the terms of the Mozilla Public\n# License, v. 2.0. If a copy of the MPL was not distributed with this\n# file, You can obtain one at http://mozilla.org/MPL/2.0/.\n\nwith Files(\"**\"):\n    BUG_COMPONENT = (\"Firefox\", \"New Tab Page\")\n\nBROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']\n\nSPHINX_TREES['docs'] = 'docs'\n\nXPCSHELL_TESTS_MANIFESTS += [\n    'test/xpcshell/xpcshell.ini',\n]\n\nXPIDL_SOURCES += [\n    'nsIAboutNewTabService.idl',\n]\n\nXPIDL_MODULE = 'browser-newtab'\n\nEXTRA_JS_MODULES += [\n    'AboutNewTabService.jsm',\n]\n\nXPCOM_MANIFESTS += [\n    'components.conf',\n]\n\nJAR_MANIFESTS += ['jar.mn']\n"
  },
  {
    "path": "nsIAboutNewTabService.idl",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\n#include \"nsISupports.idl\"\n\n/**\n * Allows to override about:newtab to point to a different location\n * than the one specified within AboutRedirector.cpp\n */\n\n[scriptable, uuid(dfcd2adc-7867-4d3a-ba70-17501f208142)]\ninterface nsIAboutNewTabService : nsISupports\n{\n  /**\n   * Returns the url of the resource for the newtab page if not overridden,\n   * otherwise a string represenation of the new URL.\n   */\n  attribute ACString newTabURL;\n\n  /**\n   * Returns the default URL (local or activity stream depending on pref)\n   */\n  attribute ACString defaultURL;\n\n  /**\n   * Returns the about:welcome URL.\n   */\n  attribute ACString welcomeURL;\n\n  /**\n   * Returns true if opening the New Tab page will notify the user of a change.\n   */\n  attribute bool willNotifyUser;\n\n  /**\n   * Returns true if the default resource got overridden.\n   */\n  readonly attribute bool overridden;\n\n  /**\n   * Returns true if the default resource is activity stream and isn't\n   * overridden\n   */\n  readonly attribute bool activityStreamEnabled;\n\n  /**\n   * Returns true if the the debug pref for activity stream is true\n   */\n  readonly attribute bool activityStreamDebug;\n\n  /**\n   * Resets to the default resource and also resets the\n   * overridden attribute to false.\n   */\n  void resetNewTabURL();\n\n  /**\n  * Records a scalar metric for how long it takes to pain Top Sites, this will\n  * only record the first timestamp, all the subsequent calls will be ignored.\n  */\n  void maybeRecordTopsitesPainted(in unsigned long long timestamp);\n};\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"activity-streams\",\n  \"description\": \"A rich visual history feed and a reimagined home page make it easier than ever to find exactly what you're looking for in Firefox.\\n\\nLearn more about this Test Pilot experiment at https://testpilot.firefox.com/.\",\n  \"version\": \"1.14.3\",\n  \"author\": \"Mozilla (https://mozilla.org/)\",\n  \"bugs\": {\n    \"url\": \"https://github.com/mozilla/activity-stream/issues\"\n  },\n  \"dependencies\": {\n    \"fluent\": \"0.12.0\",\n    \"fluent-react\": \"0.8.4\",\n    \"react\": \"16.8.6\",\n    \"react-dom\": \"16.8.6\",\n    \"react-redux\": \"7.0.3\",\n    \"react-transition-group\": \"4.2.1\",\n    \"redux\": \"4.0.1\",\n    \"reselect\": \"4.0.0\"\n  },\n  \"devDependencies\": {\n    \"@babel/core\": \"7.4.5\",\n    \"@babel/plugin-proposal-async-generator-functions\": \"7.2.0\",\n    \"@babel/preset-react\": \"7.0.0\",\n    \"acorn\": \"6.1.1\",\n    \"babel-eslint\": \"10.0.3\",\n    \"babel-loader\": \"8.0.6\",\n    \"babel-plugin-jsm-to-commonjs\": \"0.5.0\",\n    \"babel-plugin-jsm-to-esmodules\": \"0.6.0\",\n    \"chai\": \"4.2.0\",\n    \"chai-json-schema\": \"1.5.1\",\n    \"cpx\": \"1.5.0\",\n    \"enzyme\": \"3.9.0\",\n    \"enzyme-adapter-react-16\": \"1.13.2\",\n    \"eslint\": \"6.2.2\",\n    \"eslint-config-prettier\": \"4.2.0\",\n    \"eslint-plugin-fetch-options\": \"0.0.5\",\n    \"eslint-plugin-html\": \"6.0.0\",\n    \"eslint-plugin-import\": \"2.17.3\",\n    \"eslint-plugin-jsx-a11y\": \"6.2.1\",\n    \"eslint-plugin-mozilla\": \"2.1.0\",\n    \"eslint-plugin-no-unsanitized\": \"3.0.2\",\n    \"eslint-plugin-prettier\": \"3.0.1\",\n    \"eslint-plugin-react\": \"7.13.0\",\n    \"eslint-plugin-react-hooks\": \"1.6.0\",\n    \"istanbul-instrumenter-loader\": \"3.0.1\",\n    \"joi-browser\": \"13.4.0\",\n    \"karma\": \"4.1.0\",\n    \"karma-chai\": \"0.1.0\",\n    \"karma-coverage-istanbul-reporter\": \"2.0.5\",\n    \"karma-firefox-launcher\": \"1.1.0\",\n    \"karma-json-reporter\": \"1.2.1\",\n    \"karma-mocha\": \"1.3.0\",\n    \"karma-mocha-reporter\": \"2.2.5\",\n    \"karma-sinon\": \"1.0.5\",\n    \"karma-sourcemap-loader\": \"0.3.7\",\n    \"karma-webpack\": \"3.0.5\",\n    \"loader-utils\": \"1.2.3\",\n    \"lodash\": \"4.17.14\",\n    \"minimist\": \"1.2.0\",\n    \"mocha\": \"6.1.4\",\n    \"mock-raf\": \"1.0.1\",\n    \"node-fetch\": \"2.6.0\",\n    \"node-sass\": \"4.12.0\",\n    \"npm-run-all\": \"4.1.5\",\n    \"prettier\": \"1.17.0\",\n    \"prop-types\": \"15.7.2\",\n    \"raw-loader\": \"2.0.0\",\n    \"react-test-renderer\": \"16.8.6\",\n    \"rimraf\": \"2.6.3\",\n    \"sass\": \"1.20.1\",\n    \"sass-lint\": \"1.13.1\",\n    \"shelljs\": \"0.8.3\",\n    \"sinon\": \"7.3.2\",\n    \"webpack\": \"4.32.2\",\n    \"webpack-cli\": \"3.3.2\",\n    \"yamscripts\": \"0.1.0\"\n  },\n  \"engines\": {\n    \"firefox\": \">=45.0 <=*\",\n    \"//\": \"when changing node versions, also edit .travis.yml and .nvmrc\",\n    \"node\": \"8.*\",\n    \"npm\": \"6.9\"\n  },\n  \"homepage\": \"https://github.com/mozilla/activity-stream\",\n  \"keywords\": [\n    \"mozilla\",\n    \"firefox\",\n    \"activity-stream\"\n  ],\n  \"license\": \"MPL-2.0\",\n  \"main\": \"bootstrap.js\",\n  \"repository\": \"mozilla/activity-stream\",\n  \"config\": {\n    \"mc_dir\": \"../mozilla-central\"\n  },\n  \"scripts\": {\n    \"mochitest\": \"(cd $npm_package_config_mc_dir && ./mach mochitest browser/components/newtab/test/browser --headless)\",\n    \"mochitest-debug\": \"(cd $npm_package_config_mc_dir && ./mach mochitest --jsdebugger browser/components/newtab/test/browser)\",\n    \"bundle\": \"npm-run-all bundle:*\",\n    \"bundle:webpack\": \"webpack --config webpack.system-addon.config.js\",\n    \"bundle:css\": \"node-sass content-src/styles -o css\",\n    \"bundle:html\": \"rimraf prerendered && node ./bin/render-activity-stream-html.js\",\n    \"buildmc\": \"npm-run-all buildmc:*\",\n    \"prebuildmc\": \"rimraf $npm_package_config_mc_dir/browser/components/newtab/\",\n    \"buildmc:bundle\": \"npm run  bundle\",\n    \"buildmc:copy\": \"rsync --exclude-from .mcignore -a . $npm_package_config_mc_dir/browser/components/newtab/\",\n    \"buildmc:copyPingCentre\": \"cpx \\\"ping-centre/PingCentre.jsm\\\" $npm_package_config_mc_dir/browser/modules\",\n    \"builduplift\": \"npm-run-all builduplift:*\",\n    \"prebuilduplift\": \"npm run prebuildmc\",\n    \"builduplift:bundle\": \"npm run  bundle\",\n    \"builduplift:copy\": \"npm run buildmc:copy\",\n    \"buildlibrary\": \"npm-run-all buildlibrary:*\",\n    \"buildlibrary:webpack\": \"webpack --config webpack.aboutlibrary.config.js\",\n    \"buildlibrary:css\": \"node-sass --source-map true --source-map-contents content-src/aboutlibrary -o aboutlibrary/content\",\n    \"buildlibrary:copy\": \"cpx \\\"aboutlibrary/**/{,.}*\\\" $npm_package_config_mc_dir/browser/components/library\",\n    \"startmc\": \"npm-run-all --parallel startmc:*\",\n    \"prestartmc\": \"npm run buildmc\",\n    \"startmc:copy\": \"cpx \\\"{{,.}*,!(node_modules)/**/{,.}*}\\\" $npm_package_config_mc_dir/browser/components/newtab/ -w\",\n    \"startmc:copyPingCentre\": \"npm run buildmc:copyPingCentre -- -w\",\n    \"startmc:watch\": \"npm run watchmc\",\n    \"watchmc\": \"npm-run-all --parallel watchmc:*\",\n    \"watchmc:webpack\": \"npm run bundle:webpack -- --env.development -w\",\n    \"watchmc:css\": \"npm run bundle:css && npm run bundle:css -- --source-map-embed --source-map-contents -w\",\n    \"importmc\": \"npm-run-all importmc:*\",\n    \"importmc:src\": \"rsync --exclude-from .mcignore -a $npm_package_config_mc_dir/browser/components/newtab/ .\",\n    \"testmc\": \"npm-run-all testmc:*\",\n    \"testmc:lint\": \"npm run lint\",\n    \"testmc:build\": \"npm run bundle:webpack\",\n    \"testmc:unit\": \"karma start karma.mc.config.js\",\n    \"tddmc\": \"karma start karma.mc.config.js --tdd\",\n    \"debugcoverage\": \"open logs/coverage/index.html\",\n    \"lint\": \"npm-run-all lint:*\",\n    \"lint:eslint-check\": \"eslint --cache --print-config AboutNewTabService.jsm | eslint-config-prettier-check\",\n    \"lint:eslint\": \"eslint --cache --ext=.js,.jsm,.jsx .\",\n    \"lint:sasslint\": \"sass-lint -v -q\",\n    \"test\": \"npm run testmc\",\n    \"tdd\": \"npm run tddmc\",\n    \"vendor\": \"npm-run-all vendor:*\",\n    \"vendor:react\": \"node ./bin/vendor-react.js\",\n    \"fix\": \"npm-run-all fix:*\",\n    \"fix:eslint\": \"npm run lint:eslint -- --fix\",\n    \"help\": \"yamscripts help\",\n    \"yamscripts\": \"yamscripts compile\",\n    \"__\": \"# NOTE: THESE SCRIPTS ARE COMPILED!!! EDIT yamscripts.yml instead!!!\"\n  },\n  \"title\": \"Activity Stream\",\n  \"permissions\": {\n    \"multiprocess\": true,\n    \"private-browsing\": true\n  }\n}\n"
  },
  {
    "path": "ping-centre/PingCentre.jsm",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nconst { Services } = ChromeUtils.import(\"resource://gre/modules/Services.jsm\");\n\nChromeUtils.defineModuleGetter(\n  this,\n  \"AppConstants\",\n  \"resource://gre/modules/AppConstants.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"UpdateUtils\",\n  \"resource://gre/modules/UpdateUtils.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"TelemetryEnvironment\",\n  \"resource://gre/modules/TelemetryEnvironment.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"ServiceRequest\",\n  \"resource://gre/modules/ServiceRequest.jsm\"\n);\n\nconst PREF_BRANCH = \"browser.ping-centre.\";\n\nconst TELEMETRY_PREF = `${PREF_BRANCH}telemetry`;\nconst LOGGING_PREF = `${PREF_BRANCH}log`;\nconst STRUCTURED_INGESTION_SEND_TIMEOUT = 30 * 1000; // 30 seconds\n\nconst FHR_UPLOAD_ENABLED_PREF = \"datareporting.healthreport.uploadEnabled\";\nconst BROWSER_SEARCH_REGION_PREF = \"browser.search.region\";\n\n// Only report region for following regions, to ensure that users in countries\n// with small user population (less than 10000) cannot be uniquely identified.\n// See bug 1421422 for more details.\nconst REGION_WHITELIST = new Set([\n  \"AE\",\n  \"AF\",\n  \"AL\",\n  \"AM\",\n  \"AR\",\n  \"AT\",\n  \"AU\",\n  \"AZ\",\n  \"BA\",\n  \"BD\",\n  \"BE\",\n  \"BF\",\n  \"BG\",\n  \"BJ\",\n  \"BO\",\n  \"BR\",\n  \"BY\",\n  \"CA\",\n  \"CH\",\n  \"CI\",\n  \"CL\",\n  \"CM\",\n  \"CN\",\n  \"CO\",\n  \"CR\",\n  \"CU\",\n  \"CY\",\n  \"CZ\",\n  \"DE\",\n  \"DK\",\n  \"DO\",\n  \"DZ\",\n  \"EC\",\n  \"EE\",\n  \"EG\",\n  \"ES\",\n  \"ET\",\n  \"FI\",\n  \"FR\",\n  \"GB\",\n  \"GE\",\n  \"GH\",\n  \"GP\",\n  \"GR\",\n  \"GT\",\n  \"HK\",\n  \"HN\",\n  \"HR\",\n  \"HU\",\n  \"ID\",\n  \"IE\",\n  \"IL\",\n  \"IN\",\n  \"IQ\",\n  \"IR\",\n  \"IS\",\n  \"IT\",\n  \"JM\",\n  \"JO\",\n  \"JP\",\n  \"KE\",\n  \"KH\",\n  \"KR\",\n  \"KW\",\n  \"KZ\",\n  \"LB\",\n  \"LK\",\n  \"LT\",\n  \"LU\",\n  \"LV\",\n  \"LY\",\n  \"MA\",\n  \"MD\",\n  \"ME\",\n  \"MG\",\n  \"MK\",\n  \"ML\",\n  \"MM\",\n  \"MN\",\n  \"MQ\",\n  \"MT\",\n  \"MU\",\n  \"MX\",\n  \"MY\",\n  \"MZ\",\n  \"NC\",\n  \"NG\",\n  \"NI\",\n  \"NL\",\n  \"NO\",\n  \"NP\",\n  \"NZ\",\n  \"OM\",\n  \"PA\",\n  \"PE\",\n  \"PH\",\n  \"PK\",\n  \"PL\",\n  \"PR\",\n  \"PS\",\n  \"PT\",\n  \"PY\",\n  \"QA\",\n  \"RE\",\n  \"RO\",\n  \"RS\",\n  \"RU\",\n  \"RW\",\n  \"SA\",\n  \"SD\",\n  \"SE\",\n  \"SG\",\n  \"SI\",\n  \"SK\",\n  \"SN\",\n  \"SV\",\n  \"SY\",\n  \"TG\",\n  \"TH\",\n  \"TN\",\n  \"TR\",\n  \"TT\",\n  \"TW\",\n  \"TZ\",\n  \"UA\",\n  \"UG\",\n  \"US\",\n  \"UY\",\n  \"UZ\",\n  \"VE\",\n  \"VN\",\n  \"ZA\",\n  \"ZM\",\n  \"ZW\",\n]);\n\n/**\n * Observe various notifications and send them to a telemetry endpoint.\n *\n * @param {Object} options\n * @param {string} options.topic - a unique ID for users of PingCentre to distinguish\n *                  their data on the server side.\n */\nclass PingCentre {\n  constructor(options) {\n    if (!options.topic) {\n      throw new Error(\"Must specify topic.\");\n    }\n\n    this._topic = options.topic;\n    this._prefs = Services.prefs.getBranch(\"\");\n\n    this._enabled = this._prefs.getBoolPref(TELEMETRY_PREF);\n    this._onTelemetryPrefChange = this._onTelemetryPrefChange.bind(this);\n    this._prefs.addObserver(TELEMETRY_PREF, this._onTelemetryPrefChange);\n\n    this._fhrEnabled = this._prefs.getBoolPref(FHR_UPLOAD_ENABLED_PREF);\n    this._onFhrPrefChange = this._onFhrPrefChange.bind(this);\n    this._prefs.addObserver(FHR_UPLOAD_ENABLED_PREF, this._onFhrPrefChange);\n\n    this.logging = this._prefs.getBoolPref(LOGGING_PREF);\n    this._onLoggingPrefChange = this._onLoggingPrefChange.bind(this);\n    this._prefs.addObserver(LOGGING_PREF, this._onLoggingPrefChange);\n  }\n\n  get enabled() {\n    return this._enabled && this._fhrEnabled;\n  }\n\n  _onLoggingPrefChange(aSubject, aTopic, prefKey) {\n    this.logging = this._prefs.getBoolPref(prefKey);\n  }\n\n  _onTelemetryPrefChange(aSubject, aTopic, prefKey) {\n    this._enabled = this._prefs.getBoolPref(prefKey);\n  }\n\n  _onFhrPrefChange(aSubject, aTopic, prefKey) {\n    this._fhrEnabled = this._prefs.getBoolPref(prefKey);\n  }\n\n  _createExperimentsString(activeExperiments, filter) {\n    let experimentsString = \"\";\n    for (let experimentID in activeExperiments) {\n      if (\n        !activeExperiments[experimentID] ||\n        !activeExperiments[experimentID].branch ||\n        (filter && !experimentID.includes(filter))\n      ) {\n        continue;\n      }\n      let expString = `${experimentID}:${\n        activeExperiments[experimentID].branch\n      }`;\n      experimentsString = experimentsString.concat(`${expString};`);\n    }\n    return experimentsString;\n  }\n\n  _getRegion() {\n    let region = \"UNSET\";\n\n    if (Services.prefs.prefHasUserValue(BROWSER_SEARCH_REGION_PREF)) {\n      region = Services.prefs.getStringPref(BROWSER_SEARCH_REGION_PREF);\n      if (region === \"\") {\n        region = \"EMPTY\";\n      } else if (!REGION_WHITELIST.has(region)) {\n        region = \"OTHER\";\n      }\n    }\n    return region;\n  }\n\n  _createStructuredIngestionPing(data, options = {}) {\n    let { filter } = options;\n    let experiments = TelemetryEnvironment.getActiveExperiments();\n    let experimentsString = this._createExperimentsString(experiments, filter);\n\n    let locale = data.locale || Services.locale.appLocaleAsLangTag;\n    const payload = Object.assign(\n      {\n        locale,\n        version: AppConstants.MOZ_APP_VERSION,\n        release_channel: UpdateUtils.getUpdateChannel(false),\n      },\n      data\n    );\n    if (experimentsString) {\n      payload.shield_id = experimentsString;\n    }\n\n    return payload;\n  }\n\n  static _gzipCompressString(string) {\n    let observer = {\n      buffer: \"\",\n      onStreamComplete(loader, context, status, length, result) {\n        this.buffer = String.fromCharCode(...result);\n      },\n    };\n\n    let scs = Cc[\"@mozilla.org/streamConverters;1\"].getService(\n      Ci.nsIStreamConverterService\n    );\n    let listener = Cc[\"@mozilla.org/network/stream-loader;1\"].createInstance(\n      Ci.nsIStreamLoader\n    );\n    listener.init(observer);\n    let converter = scs.asyncConvertData(\n      \"uncompressed\",\n      \"gzip\",\n      listener,\n      null\n    );\n    let stringStream = Cc[\n      \"@mozilla.org/io/string-input-stream;1\"\n    ].createInstance(Ci.nsIStringInputStream);\n    stringStream.data = string;\n    converter.onStartRequest(null, null);\n    converter.onDataAvailable(null, stringStream, 0, string.length);\n    converter.onStopRequest(null, null, null);\n    return observer.buffer;\n  }\n\n  static _sendInGzip(endpoint, payload) {\n    return new Promise((resolve, reject) => {\n      let request = new ServiceRequest({ mozAnon: true });\n      request.mozBackgroundRequest = true;\n      request.timeout = STRUCTURED_INGESTION_SEND_TIMEOUT;\n\n      request.open(\"POST\", endpoint, true);\n      request.overrideMimeType(\"text/plain\");\n      request.setRequestHeader(\n        \"Content-Type\",\n        \"application/json; charset=UTF-8\"\n      );\n      request.setRequestHeader(\"Content-Encoding\", \"gzip\");\n      request.setRequestHeader(\"Date\", new Date().toUTCString());\n\n      request.onload = event => {\n        if (request.status !== 200) {\n          reject(event);\n        } else {\n          resolve(event);\n        }\n      };\n      request.onerror = reject;\n      request.onabort = reject;\n      request.ontimeout = reject;\n\n      let payloadStream = Cc[\n        \"@mozilla.org/io/string-input-stream;1\"\n      ].createInstance(Ci.nsIStringInputStream);\n      payloadStream.data = PingCentre._gzipCompressString(payload);\n      request.sendInputStream(payloadStream);\n    });\n  }\n\n  /**\n   * Sends a ping to the Structured Ingestion telemetry pipeline.\n   *\n   * The payload would be compressed using gzip.\n   *\n   * @param {Object} data     The payload to be sent.\n   * @param {String} endpoint The destination endpoint. Note that Structured Ingestion\n   *                          requires a different endpoint for each ping. It's up to the\n   *                          caller to provide that. See more details at\n   *                          https://github.com/mozilla/gcp-ingestion/blob/master/docs/edge.md#postput-request\n   * @param {Object} options  Other options for this ping.\n   */\n  sendStructuredIngestionPing(data, endpoint, options) {\n    if (!this.enabled) {\n      return Promise.resolve();\n    }\n\n    const ping = this._createStructuredIngestionPing(data, options);\n    const payload = JSON.stringify(ping);\n\n    if (this.logging) {\n      Services.console.logStringMessage(\n        `TELEMETRY PING (STRUCTURED INGESTION): ${payload}\\n`\n      );\n    }\n\n    return PingCentre._sendInGzip(endpoint, payload).catch(event => {\n      Cu.reportError(\n        `Structured Ingestion ping failure with error: ${event.type}`\n      );\n    });\n  }\n\n  uninit() {\n    try {\n      this._prefs.removeObserver(TELEMETRY_PREF, this._onTelemetryPrefChange);\n      this._prefs.removeObserver(LOGGING_PREF, this._onLoggingPrefChange);\n      this._prefs.removeObserver(\n        FHR_UPLOAD_ENABLED_PREF,\n        this._onFhrPrefChange\n      );\n    } catch (e) {\n      Cu.reportError(e);\n    }\n  }\n}\n\nthis.PingCentre = PingCentre;\nthis.PingCentreConstants = {\n  FHR_UPLOAD_ENABLED_PREF,\n  TELEMETRY_PREF,\n  LOGGING_PREF,\n};\nconst EXPORTED_SYMBOLS = [\"PingCentre\", \"PingCentreConstants\"];\n"
  },
  {
    "path": "test/.eslintrc.js",
    "content": "module.exports = {\n  \"env\": {\n    \"mocha\": true\n  },\n  \"globals\": {\n    \"assert\": true,\n    \"chai\": true,\n    \"sinon\": true\n  },\n  \"rules\": {\n    \"func-name-matching\": 0,\n    \"import/no-commonjs\": 2,\n    \"lines-between-class-members\": 0,\n    \"react/jsx-no-bind\": 0,\n    \"require-await\": 0\n  }\n};\n"
  },
  {
    "path": "test/browser/blue_page.html",
    "content": "<html>\n  <head>\n    <meta charset=\"utf-8\">\n  </head>\n  <body style=\"background-color: blue\" />\n</html>\n"
  },
  {
    "path": "test/browser/browser.ini",
    "content": "[DEFAULT]\nsupport-files =\n  blue_page.html\n  red_page.html\n  head.js\nprefs =\n  browser.newtabpage.activity-stream.debug=false\n  browser.newtabpage.activity-stream.discoverystream.enabled=true\n  browser.newtabpage.activity-stream.discoverystream.endpoints=data:\n  browser.newtabpage.activity-stream.feeds.section.topstories=true\n  browser.newtabpage.activity-stream.feeds.section.topstories.options={\"provider_name\":\"\"}\n\n[browser_aboutwelcome.js]\n[browser_as_load_location.js]\n[browser_as_render.js]\n[browser_asrouter_snippets.js]\n[browser_asrouter_targeting.js]\n[browser_asrouter_trigger_listeners.js]\n[browser_discovery_render.js]\n[browser_discovery_styles.js]\n[browser_enabled_newtabpage.js]\n[browser_highlights_section.js]\n[browser_getScreenshots.js]\n[browser_newtab_overrides.js]\n[browser_onboarding_rtamo.js]\nskip-if = (os == \"linux\") # Test setup only implemented for OSX and Windows\n[browser_topsites_contextMenu_options.js]\n[browser_topsites_section.js]\n[browser_asrouter_cfr.js]\n[browser_asrouter_bookmarkpanel.js]\n[browser_asrouter_toolbarbadge.js]\n[browser_asrouter_whatsnewpanel.js]\n"
  },
  {
    "path": "test/browser/browser_aboutwelcome.js",
    "content": "\"use strict\";\n\nconst { ASRouter } = ChromeUtils.import(\n  \"resource://activity-stream/lib/ASRouter.jsm\"\n);\n\nconst BRANCH_PREF = \"trailhead.firstrun.branches\";\n\n/**\n * Sets the trailhead branch pref to the passed value.\n */\nasync function setTrailheadBranch(value) {\n  Services.prefs.setCharPref(BRANCH_PREF, value);\n\n  // Reset trailhead so it loads the new branch.\n  Services.prefs.clearUserPref(\"trailhead.firstrun.didSeeAboutWelcome\");\n  await ASRouter.setState({ trailheadInitialized: false });\n\n  registerCleanupFunction(() => {\n    Services.prefs.clearUserPref(BRANCH_PREF);\n  });\n}\n\n/**\n * Test a specific trailhead branch.\n */\nasync function test_trailhead_branch(\n  branchName,\n  expectedSelectors = [],\n  unexpectedSelectors = []\n) {\n  await setTrailheadBranch(branchName);\n\n  let tab = await BrowserTestUtils.openNewForegroundTab(\n    gBrowser,\n    \"about:welcome\",\n    false\n  );\n  let browser = tab.linkedBrowser;\n\n  await ContentTask.spawn(\n    browser,\n    { expectedSelectors, branchName, unexpectedSelectors },\n    async ({\n      expectedSelectors: expected,\n      branchName: branch,\n      unexpectedSelectors: unexpected,\n    }) => {\n      for (let selector of expected) {\n        await ContentTaskUtils.waitForCondition(\n          () => content.document.querySelector(selector),\n          `Should render ${selector} in the ${branch} branch`\n        );\n      }\n      for (let selector of unexpected) {\n        ok(\n          !content.document.querySelector(selector),\n          `Should not render ${selector} in the ${branch} branch`\n        );\n      }\n    }\n  );\n\n  BrowserTestUtils.removeTab(tab);\n}\n\n/**\n * Test the the various trailhead branches.\n */\nadd_task(async function test_trailhead_branches() {\n  await test_trailhead_branch(\n    \"join-dynamic\",\n    // Expected selectors:\n    [\n      \".trailhead.joinCohort\",\n      \"button[data-l10n-id=onboarding-data-sync-button2]\",\n      \"button[data-l10n-id=onboarding-firefox-monitor-button]\",\n      \"button[data-l10n-id=onboarding-browse-privately-button]\",\n    ]\n  );\n\n  // Validate sync card is not shown if user usesFirefoxSync\n  await pushPrefs([\"services.sync.username\", \"someone@foo.com\"]);\n  await test_trailhead_branch(\n    \"join-dynamic\",\n    // Expected selectors:\n    [\n      \".trailhead.joinCohort\",\n      \"button[data-l10n-id=onboarding-firefox-monitor-button]\",\n      \"button[data-l10n-id=onboarding-browse-privately-button]\",\n    ],\n    // Unexpected selectors:\n    [\"button[data-l10n-id=onboarding-data-sync-button2]\"]\n  );\n\n  // Validate multidevice card is not shown if user has mobile devices connected\n  await pushPrefs([\"services.sync.clients.devices.mobile\", 1]);\n  await test_trailhead_branch(\n    \"join-dynamic\",\n    // Expected selectors:\n    [\n      \".trailhead.joinCohort\",\n      \"button[data-l10n-id=onboarding-firefox-monitor-button]\",\n    ],\n    // Unexpected selectors:\n    [\"button[data-l10n-id=onboarding-mobile-phone-button\"]\n  );\n\n  await test_trailhead_branch(\n    \"sync-supercharge\",\n    // Expected selectors:\n    [\n      \".trailhead.syncCohort\",\n      \"button[data-l10n-id=onboarding-data-sync-button2]\",\n      \"button[data-l10n-id=onboarding-firefox-monitor-button]\",\n      \"button[data-l10n-id=onboarding-mobile-phone-button]\",\n    ]\n  );\n\n  await test_trailhead_branch(\n    \"modal_variant_a-supercharge\",\n    // Expected selectors:\n    [\n      \".trailhead.joinCohort\",\n      \"p[data-l10n-id=onboarding-benefit-sync-text]\",\n      \"p[data-l10n-id=onboarding-benefit-monitor-text]\",\n      \"p[data-l10n-id=onboarding-benefit-lockwise-text]\",\n    ]\n  );\n\n  await test_trailhead_branch(\n    \"modal_variant_f-supercharge\",\n    // Expected selectors:\n    [\n      \".trailhead.joinCohort\",\n      \"h3[data-l10n-id=onboarding-welcome-form-header]\",\n      \"p[data-l10n-id=onboarding-benefit-products-text]\",\n      \"p[data-l10n-id=onboarding-benefit-knowledge-text]\",\n      \"p[data-l10n-id=onboarding-benefit-privacy-text]\",\n    ]\n  );\n\n  await test_trailhead_branch(\n    \"full_page_d-supercharge\",\n    // Expected selectors:\n    [\n      \".trailhead-fullpage\",\n      \".trailheadCard\",\n      \"p[data-l10n-id=onboarding-benefit-products-text]\",\n      \"button[data-l10n-id=onboarding-join-form-continue]\",\n      \"button[data-l10n-id=onboarding-join-form-signin]\",\n    ]\n  );\n\n  await test_trailhead_branch(\n    \"full_page_e-supercharge\",\n    // Expected selectors:\n    [\n      \".fullPageCardsAtTop\",\n      \".trailhead-fullpage\",\n      \".trailheadCard\",\n      \"p[data-l10n-id=onboarding-benefit-products-text]\",\n      \"button[data-l10n-id=onboarding-join-form-continue]\",\n      \"button[data-l10n-id=onboarding-join-form-signin]\",\n    ]\n  );\n\n  await test_trailhead_branch(\n    \"nofirstrun\",\n    [],\n    // Unexpected selectors:\n    [\"#trailheadDialog\", \".trailheadCards\"]\n  );\n});\n"
  },
  {
    "path": "test/browser/browser_as_load_location.js",
    "content": "\"use strict\";\n\n/**\n * Helper to test that a newtab page loads its html document.\n *\n * @param selector {String} CSS selector to find an element in newtab content\n * @param message {String} Description of the test printed with the assertion\n */\nasync function checkNewtabLoads(selector, message) {\n  // simulate a newtab open as a user would\n  BrowserOpenTab();\n\n  // wait until the browser loads\n  let browser = gBrowser.selectedBrowser;\n  await waitForPreloaded(browser);\n\n  // check what the content task thinks has been loaded.\n  let found = await ContentTask.spawn(\n    browser,\n    selector,\n    arg => content.document.querySelector(arg) !== null\n  );\n  ok(found, message);\n\n  // avoid leakage\n  BrowserTestUtils.removeTab(gBrowser.selectedTab);\n}\n\n// Test with activity stream on\nasync function checkActivityStreamLoads() {\n  await checkNewtabLoads(\n    \"body.activity-stream\",\n    \"Got <body class='activity-stream'> Element\"\n  );\n}\n\n// Run a first time not from a preloaded browser\nadd_task(async function checkActivityStreamNotPreloadedLoad() {\n  NewTabPagePreloading.removePreloadedBrowser(window);\n  await checkActivityStreamLoads();\n});\n\n// Run a second time from a preloaded browser\nadd_task(checkActivityStreamLoads);\n"
  },
  {
    "path": "test/browser/browser_as_render.js",
    "content": "\"use strict\";\n\ntest_newtab({\n  async before({ pushPrefs }) {\n    await pushPrefs([\n      \"browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar\",\n      false,\n    ]);\n  },\n  test: function test_render_search() {\n    let search = content.document.getElementById(\"newtab-search-text\");\n    ok(search, \"Got the search box\");\n    isnot(\n      search.placeholder,\n      \"search_web_placeholder\",\n      \"Search box is localized\"\n    );\n  },\n});\n\ntest_newtab({\n  async before({ pushPrefs }) {\n    await pushPrefs([\n      \"browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar\",\n      true,\n    ]);\n  },\n  test: function test_render_search_handoff() {\n    let search = content.document.querySelector(\".search-handoff-button\");\n    ok(search, \"Got the search handoff button\");\n  },\n});\n\ntest_newtab(function test_render_topsites() {\n  let topSites = content.document.querySelector(\".top-sites-list\");\n  ok(topSites, \"Got the top sites section\");\n});\n\ntest_newtab({\n  async before({ pushPrefs }) {\n    await pushPrefs([\n      \"browser.newtabpage.activity-stream.feeds.topsites\",\n      false,\n    ]);\n  },\n  test: function test_render_no_topsites() {\n    let topSites = content.document.querySelector(\".top-sites-list\");\n    ok(!topSites, \"No top sites section\");\n  },\n});\n\n// This next test runs immediately after test_render_no_topsites to make sure\n// the topsites pref is restored\ntest_newtab(function test_render_topsites_again() {\n  let topSites = content.document.querySelector(\".top-sites-list\");\n  ok(topSites, \"Got the top sites section again\");\n});\n"
  },
  {
    "path": "test/browser/browser_asrouter_bookmarkpanel.js",
    "content": "const { PanelTestProvider } = ChromeUtils.import(\n  \"resource://activity-stream/lib/PanelTestProvider.jsm\"\n);\nconst { BookmarkPanelHub } = ChromeUtils.import(\n  \"resource://activity-stream/lib/BookmarkPanelHub.jsm\"\n);\n\nadd_task(async function test_fxa_message_shown() {\n  const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);\n\n  registerCleanupFunction(async () => {\n    await clearHistoryAndBookmarks();\n    BrowserTestUtils.removeTab(tab);\n  });\n\n  const testURL = \"data:text/plain,test cfr fxa bookmark panel message\";\n  const browser = gBrowser.selectedBrowser;\n\n  BrowserTestUtils.loadURI(browser, testURL);\n  await BrowserTestUtils.browserLoaded(browser, false, testURL);\n\n  const [msg] = PanelTestProvider.getMessages();\n  const response = BookmarkPanelHub.onResponse(\n    msg,\n    {\n      container: document.getElementById(\"editBookmarkPanelRecommendation\"),\n      infoButton: document.getElementById(\"editBookmarkPanelInfoButton\"),\n      recommendationContainer: document.getElementById(\n        \"editBookmarkPanelRecommendation\"\n      ),\n      url: testURL,\n      document,\n    },\n    window\n  );\n\n  Assert.ok(response, \"We sent a valid message\");\n\n  const popupShownPromise = BrowserTestUtils.waitForEvent(\n    StarUI.panel,\n    \"popupshown\"\n  );\n\n  // Wait for the bookmark panel state to settle and be ready to open the panel\n  await BrowserTestUtils.waitForCondition(\n    () => BookmarkingUI.status !== BookmarkingUI.STATUS_UPDATING\n  );\n\n  BookmarkingUI.star.click();\n\n  await popupShownPromise;\n\n  await BrowserTestUtils.waitForCondition(\n    () => document.getElementById(\"cfrMessageContainer\"),\n    `Should create a\n    container for the message`\n  );\n  for (const selector of [\n    \"#cfrClose\",\n    \"#editBookmarkPanelRecommendationTitle\",\n    \"#editBookmarkPanelRecommendationContent\",\n    \"#editBookmarkPanelRecommendationCta\",\n  ]) {\n    Assert.ok(\n      document.getElementById(\"cfrMessageContainer\").querySelector(selector),\n      `Should contain ${selector}`\n    );\n  }\n\n  const ftlFiles = Array.from(document.querySelectorAll(\"link\")).filter(\n    l =>\n      l.getAttribute(\"href\") === \"browser/newtab/asrouter.ftl\" ||\n      l.getAttribute(\"href\") === \"browser/branding/sync-brand.ftl\"\n  );\n\n  Assert.equal(\n    ftlFiles.length,\n    2,\n    \"Two fluent files required for translating the message\"\n  );\n\n  const popupHiddenPromise = BrowserTestUtils.waitForEvent(\n    StarUI.panel,\n    \"popuphidden\"\n  );\n\n  let removeButton = document.getElementById(\"editBookmarkPanelRemoveButton\");\n\n  removeButton.click();\n\n  await popupHiddenPromise;\n});\n"
  },
  {
    "path": "test/browser/browser_asrouter_cfr.js",
    "content": "const { CFRPageActions } = ChromeUtils.import(\n  \"resource://activity-stream/lib/CFRPageActions.jsm\"\n);\nconst { ASRouterTriggerListeners } = ChromeUtils.import(\n  \"resource://activity-stream/lib/ASRouterTriggerListeners.jsm\"\n);\nconst { ASRouter } = ChromeUtils.import(\n  \"resource://activity-stream/lib/ASRouter.jsm\"\n);\n\nconst createDummyRecommendation = ({\n  action,\n  category,\n  heading_text,\n  layout,\n  skip_address_bar_notifier,\n  template,\n}) => {\n  let recommendation = {\n    template,\n    content: {\n      layout: layout || \"addon_recommendation\",\n      category,\n      anchor_id: \"page-action-buttons\",\n      skip_address_bar_notifier,\n      heading_text: heading_text || \"Mochitest\",\n      info_icon: {\n        label: { attributes: { tooltiptext: \"Why am I seeing this\" } },\n        sumo_path: \"extensionrecommendations\",\n      },\n      icon: \"foo\",\n      icon_dark_theme: \"bar\",\n      learn_more: \"extensionrecommendations\",\n      addon: {\n        id: \"addon-id\",\n        title: \"Addon name\",\n        icon: \"foo\",\n        author: \"Author name\",\n        amo_url: \"https://example.com\",\n      },\n      descriptionDetails: { steps: [] },\n      text: \"Mochitest\",\n      buttons: {\n        primary: {\n          label: {\n            value: \"OK\",\n            attributes: { accesskey: \"O\" },\n          },\n          action: {\n            type: action.type,\n            data: {},\n          },\n        },\n        secondary: [\n          {\n            label: {\n              value: \"Cancel\",\n              attributes: { accesskey: \"C\" },\n            },\n          },\n          {\n            label: {\n              value: \"Cancel 1\",\n              attributes: { accesskey: \"A\" },\n            },\n          },\n          {\n            label: {\n              value: \"Cancel 2\",\n              attributes: { accesskey: \"B\" },\n            },\n          },\n        ],\n      },\n    },\n  };\n  recommendation.content.notification_text = new String(\"Mochitest\"); // eslint-disable-line\n  recommendation.content.notification_text.attributes = {\n    tooltiptext: \"Mochitest tooltip\",\n    \"a11y-announcement\": \"Mochitest announcement\",\n  };\n  return recommendation;\n};\n\nfunction checkCFRFeaturesElements(notification) {\n  Assert.ok(notification.hidden === false, \"Panel should be visible\");\n  Assert.equal(\n    notification.getAttribute(\"data-notification-category\"),\n    \"message_and_animation\",\n    \"Panel have correct data attribute\"\n  );\n  Assert.ok(\n    notification.querySelector(\n      \"#cfr-notification-footer-pintab-animation-container\"\n    ),\n    \"Pin tab animation exists\"\n  );\n  Assert.ok(\n    notification.querySelector(\"#cfr-notification-feature-steps\"),\n    \"Pin tab steps\"\n  );\n}\n\nfunction checkCFRAddonsElements(notification) {\n  Assert.ok(notification.hidden === false, \"Panel should be visible\");\n  Assert.equal(\n    notification.getAttribute(\"data-notification-category\"),\n    \"addon_recommendation\",\n    \"Panel have correct data attribute\"\n  );\n  Assert.ok(\n    notification.querySelector(\"#cfr-notification-footer-text-and-addon-info\"),\n    \"Panel should have addon info container\"\n  );\n  Assert.ok(\n    notification.querySelector(\"#cfr-notification-footer-filled-stars\"),\n    \"Panel should have addon rating info\"\n  );\n  Assert.ok(\n    notification.querySelector(\"#cfr-notification-author\"),\n    \"Panel should have author info\"\n  );\n}\n\nfunction checkCFRSocialTrackingProtection(notification) {\n  Assert.ok(notification.hidden === false, \"Panel should be visible\");\n  Assert.ok(\n    notification.getAttribute(\"data-notification-category\") ===\n      \"icon_and_message\",\n    \"Panel have corret data attribute\"\n  );\n  Assert.ok(\n    notification.querySelector(\"#cfr-notification-footer-learn-more-link\"),\n    \"Panel should have learn more link\"\n  );\n}\n\nfunction checkCFRTrackingProtectionMilestone(notification) {\n  Assert.ok(notification.hidden === false, \"Panel should be visible\");\n  Assert.ok(\n    notification.getAttribute(\"data-notification-category\") === \"short_message\",\n    \"Panel have correct data attribute\"\n  );\n}\n\nfunction clearNotifications() {\n  for (let notification of PopupNotifications._currentNotifications) {\n    notification.remove();\n  }\n\n  // Clicking the primary action also removes the notification\n  Assert.equal(\n    PopupNotifications._currentNotifications.length,\n    0,\n    \"Should have removed the notification\"\n  );\n}\n\nfunction trigger_cfr_panel(\n  browser,\n  trigger,\n  {\n    action = { type: \"FOO\" },\n    heading_text,\n    category = \"cfrAddons\",\n    layout,\n    skip_address_bar_notifier = false,\n    use_single_secondary_button = false,\n    template = \"cfr_doorhanger\",\n  } = {}\n) {\n  // a fake action type will result in the action being ignored\n  const recommendation = createDummyRecommendation({\n    action,\n    category,\n    heading_text,\n    layout,\n    skip_address_bar_notifier,\n    template,\n  });\n  if (category !== \"cfrAddons\") {\n    delete recommendation.content.addon;\n  }\n  if (use_single_secondary_button) {\n    recommendation.content.buttons.secondary = [\n      recommendation.content.buttons.secondary[0],\n    ];\n  }\n\n  clearNotifications();\n  if (recommendation.template === \"milestone_message\") {\n    return CFRPageActions.showMilestone(\n      browser,\n      recommendation,\n      // Use the real AS dispatch method to trigger real notifications\n      ASRouter.dispatch\n    );\n  }\n  return CFRPageActions.addRecommendation(\n    browser,\n    trigger,\n    recommendation,\n    // Use the real AS dispatch method to trigger real notifications\n    ASRouter.dispatch\n  );\n}\n\nadd_task(async function setup() {\n  // Store it in order to restore to the original value\n  const { _fetchLatestAddonVersion } = CFRPageActions;\n  // Prevent fetching the real addon url and making a network request\n  CFRPageActions._fetchLatestAddonVersion = x => \"http://example.com\";\n\n  registerCleanupFunction(() => {\n    CFRPageActions._fetchLatestAddonVersion = _fetchLatestAddonVersion;\n  });\n});\n\nadd_task(async function test_cfr_notification_show() {\n  // addRecommendation checks that scheme starts with http and host matches\n  let browser = gBrowser.selectedBrowser;\n  await BrowserTestUtils.loadURI(browser, \"http://example.com/\");\n  await BrowserTestUtils.browserLoaded(browser, false, \"http://example.com/\");\n\n  const response = await trigger_cfr_panel(browser, \"example.com\");\n  Assert.ok(\n    response,\n    \"Should return true if addRecommendation checks were successful\"\n  );\n\n  const oldFocus = document.activeElement;\n  const showPanel = BrowserTestUtils.waitForEvent(\n    PopupNotifications.panel,\n    \"popupshown\"\n  );\n  // Open the panel\n  document.getElementById(\"contextual-feature-recommendation\").click();\n  await showPanel;\n\n  Assert.ok(\n    document.getElementById(\"contextual-feature-recommendation-notification\")\n      .hidden === false,\n    \"Panel should be visible\"\n  );\n  Assert.equal(\n    document.activeElement,\n    oldFocus,\n    \"Focus didn't move when panel was shown\"\n  );\n\n  // Check there is a primary button and click it. It will trigger the callback.\n  Assert.ok(\n    document.getElementById(\"contextual-feature-recommendation-notification\")\n      .button\n  );\n  let hidePanel = BrowserTestUtils.waitForEvent(\n    PopupNotifications.panel,\n    \"popuphidden\"\n  );\n  document\n    .getElementById(\"contextual-feature-recommendation-notification\")\n    .button.click();\n  await hidePanel;\n\n  // Clicking the primary action also removes the notification\n  Assert.equal(\n    PopupNotifications._currentNotifications.length,\n    0,\n    \"Should have removed the notification\"\n  );\n});\n\nadd_task(async function test_cfr_notification_show() {\n  // addRecommendation checks that scheme starts with http and host matches\n  let browser = gBrowser.selectedBrowser;\n  await BrowserTestUtils.loadURI(browser, \"http://example.com/\");\n  await BrowserTestUtils.browserLoaded(browser, false, \"http://example.com/\");\n\n  let response = await trigger_cfr_panel(browser, \"example.com\", {\n    heading_text: \"First Message\",\n  });\n  Assert.ok(\n    response,\n    \"Should return true if addRecommendation checks were successful\"\n  );\n  const showPanel = BrowserTestUtils.waitForEvent(\n    PopupNotifications.panel,\n    \"popupshown\"\n  );\n\n  // Try adding another message\n  response = await trigger_cfr_panel(browser, \"example.com\", {\n    heading_text: \"Second Message\",\n  });\n  Assert.equal(\n    response,\n    false,\n    \"Should return false if second call did not add the message\"\n  );\n\n  // Open the panel\n  document.getElementById(\"contextual-feature-recommendation\").click();\n  await showPanel;\n\n  Assert.ok(\n    document.getElementById(\"contextual-feature-recommendation-notification\")\n      .hidden === false,\n    \"Panel should be visible\"\n  );\n\n  Assert.equal(\n    document.getElementById(\"cfr-notification-header-label\").value,\n    \"First Message\",\n    \"The first message should be visible\"\n  );\n\n  // Check there is a primary button and click it. It will trigger the callback.\n  Assert.ok(\n    document.getElementById(\"contextual-feature-recommendation-notification\")\n      .button\n  );\n  let hidePanel = BrowserTestUtils.waitForEvent(\n    PopupNotifications.panel,\n    \"popuphidden\"\n  );\n  document\n    .getElementById(\"contextual-feature-recommendation-notification\")\n    .button.click();\n  await hidePanel;\n\n  // Clicking the primary action also removes the notification\n  Assert.equal(\n    PopupNotifications._currentNotifications.length,\n    0,\n    \"Should have removed the notification\"\n  );\n});\n\nadd_task(async function test_cfr_addon_install() {\n  // addRecommendation checks that scheme starts with http and host matches\n  const browser = gBrowser.selectedBrowser;\n  await BrowserTestUtils.loadURI(browser, \"http://example.com/\");\n  await BrowserTestUtils.browserLoaded(browser, false, \"http://example.com/\");\n\n  const response = await trigger_cfr_panel(browser, \"example.com\", {\n    action: { type: \"INSTALL_ADDON_FROM_URL\" },\n  });\n  Assert.ok(\n    response,\n    \"Should return true if addRecommendation checks were successful\"\n  );\n\n  const showPanel = BrowserTestUtils.waitForEvent(\n    PopupNotifications.panel,\n    \"popupshown\"\n  );\n  // Open the panel\n  document.getElementById(\"contextual-feature-recommendation\").click();\n  await showPanel;\n\n  Assert.ok(\n    document.getElementById(\"contextual-feature-recommendation-notification\")\n      .hidden === false,\n    \"Panel should be visible\"\n  );\n  checkCFRAddonsElements(\n    document.getElementById(\"contextual-feature-recommendation-notification\")\n  );\n\n  // Check there is a primary button and click it. It will trigger the callback.\n  Assert.ok(\n    document.getElementById(\"contextual-feature-recommendation-notification\")\n      .button\n  );\n  const hidePanel = BrowserTestUtils.waitForEvent(\n    PopupNotifications.panel,\n    \"popuphidden\"\n  );\n  document\n    .getElementById(\"contextual-feature-recommendation-notification\")\n    .button.click();\n  await hidePanel;\n\n  await BrowserTestUtils.waitForEvent(PopupNotifications.panel, \"popupshown\");\n\n  let [notification] = PopupNotifications.panel.childNodes;\n  // Trying to install the addon will trigger a progress popup or an error popup if\n  // running the test multiple times in a row\n  Assert.ok(\n    notification.id === \"addon-progress-notification\" ||\n      notification.id === \"addon-install-failed-notification\",\n    \"Should try to install the addon\"\n  );\n\n  // This removes the `Addon install failure` notifications\n  while (PopupNotifications._currentNotifications.length) {\n    PopupNotifications.remove(PopupNotifications._currentNotifications[0]);\n  }\n  // There should be no more notifications left\n  Assert.equal(\n    PopupNotifications._currentNotifications.length,\n    0,\n    \"Should have removed the notification\"\n  );\n});\n\nadd_task(async function test_cfr_pin_tab_notification_show() {\n  // addRecommendation checks that scheme starts with http and host matches\n  let browser = gBrowser.selectedBrowser;\n  await BrowserTestUtils.loadURI(browser, \"http://example.com/\");\n  await BrowserTestUtils.browserLoaded(browser, false, \"http://example.com/\");\n\n  const response = await trigger_cfr_panel(browser, \"example.com\", {\n    action: { type: \"PIN_CURRENT_TAB\" },\n    category: \"cfrFeatures\",\n    layout: \"message_and_animation\",\n  });\n  Assert.ok(\n    response,\n    \"Should return true if addRecommendation checks were successful\"\n  );\n\n  const showPanel = BrowserTestUtils.waitForEvent(\n    PopupNotifications.panel,\n    \"popupshown\"\n  );\n  // Open the panel\n  document.getElementById(\"contextual-feature-recommendation\").click();\n  await showPanel;\n\n  const notification = document.getElementById(\n    \"contextual-feature-recommendation-notification\"\n  );\n  checkCFRFeaturesElements(notification);\n\n  // Check there is a primary button and click it. It will trigger the callback.\n  Assert.ok(notification.button);\n  let hidePanel = BrowserTestUtils.waitForEvent(\n    PopupNotifications.panel,\n    \"popuphidden\"\n  );\n  document\n    .getElementById(\"contextual-feature-recommendation-notification\")\n    .button.click();\n  await hidePanel;\n\n  await BrowserTestUtils.waitForCondition(\n    () => gBrowser.selectedTab.pinned,\n    \"Primary action should pin tab\"\n  );\n  Assert.ok(gBrowser.selectedTab.pinned, \"Current tab should be pinned\");\n  gBrowser.unpinTab(gBrowser.selectedTab);\n\n  // Clicking the primary action also removes the notification\n  Assert.equal(\n    PopupNotifications._currentNotifications.length,\n    0,\n    \"Should have removed the notification\"\n  );\n});\n\nadd_task(\n  async function test_cfr_social_tracking_protection_notification_show() {\n    // addRecommendation checks that scheme starts with http and host matches\n    let browser = gBrowser.selectedBrowser;\n    await BrowserTestUtils.loadURI(browser, \"http://example.com/\");\n    await BrowserTestUtils.browserLoaded(browser, false, \"http://example.com/\");\n\n    const showPanel = BrowserTestUtils.waitForEvent(\n      PopupNotifications.panel,\n      \"popupshown\"\n    );\n\n    const response = await trigger_cfr_panel(browser, \"example.com\", {\n      action: { type: \"OPEN_PROTECTION_PANEL\" },\n      category: \"cfrFeatures\",\n      layout: \"icon_and_message\",\n      skip_address_bar_notifier: true,\n      use_single_secondary_button: true,\n    });\n    Assert.ok(\n      response,\n      \"Should return true if addRecommendation checks were successful\"\n    );\n    await showPanel;\n\n    const notification = document.getElementById(\n      \"contextual-feature-recommendation-notification\"\n    );\n    checkCFRSocialTrackingProtection(notification);\n\n    // Check there is a primary button and click it. It will trigger the callback.\n    Assert.ok(notification.button);\n    let hidePanel = BrowserTestUtils.waitForEvent(\n      PopupNotifications.panel,\n      \"popuphidden\"\n    );\n    document\n      .getElementById(\"contextual-feature-recommendation-notification\")\n      .button.click();\n    await hidePanel;\n  }\n);\n\nadd_task(\n  async function test_cfr_tracking_protection_milestone_notification_show() {\n    await SpecialPowers.pushPrefEnv({\n      set: [\n        [\"browser.contentblocking.cfr-milestone.milestone-achieved\", 1000],\n        [\n          \"browser.newtabpage.activity-stream.asrouter.providers.cfr\",\n          `{\"id\":\"cfr\",\"enabled\":true,\"type\":\"local\",\"localProvider\":\"CFRMessageProvider\",\"frequency\":{\"custom\":[{\"period\":\"daily\",\"cap\":10}]},\"categories\":[\"cfrAddons\",\"cfrFeatures\"],\"updateCycleInMs\":3600000}`,\n        ],\n      ],\n    });\n\n    // addRecommendation checks that scheme starts with http and host matches\n    let browser = gBrowser.selectedBrowser;\n    await BrowserTestUtils.loadURI(browser, \"http://example.com/\");\n    await BrowserTestUtils.browserLoaded(browser, false, \"http://example.com/\");\n\n    const showPanel = BrowserTestUtils.waitForEvent(\n      PopupNotifications.panel,\n      \"popupshown\"\n    );\n\n    const response = await trigger_cfr_panel(browser, \"example.com\", {\n      action: { type: \"OPEN_PROTECTION_REPORT\" },\n      category: \"cfrFeatures\",\n      layout: \"short_message\",\n      skip_address_bar_notifier: true,\n      heading_text: \"Test Milestone Message\",\n      template: \"milestone_message\",\n    });\n    Assert.ok(\n      response,\n      \"Should return true if addRecommendation checks were successful\"\n    );\n    await showPanel;\n\n    const notification = document.getElementById(\n      \"contextual-feature-recommendation-notification\"\n    );\n    // checkCFRSocialTrackingProtection(notification);\n    checkCFRTrackingProtectionMilestone(notification);\n\n    // Check there is a primary button and click it. It will trigger the callback.\n    Assert.ok(notification.button);\n    let hidePanel = BrowserTestUtils.waitForEvent(\n      PopupNotifications.panel,\n      \"popuphidden\"\n    );\n    document\n      .getElementById(\"contextual-feature-recommendation-notification\")\n      .button.click();\n    await BrowserTestUtils.removeTab(gBrowser.selectedTab);\n    await hidePanel;\n  }\n);\n\nadd_task(async function test_cfr_features_and_addon_show() {\n  // addRecommendation checks that scheme starts with http and host matches\n  let browser = gBrowser.selectedBrowser;\n  await BrowserTestUtils.loadURI(browser, \"http://example.com/\");\n  await BrowserTestUtils.browserLoaded(browser, false, \"http://example.com/\");\n\n  // Trigger Feature CFR\n  let response = await trigger_cfr_panel(browser, \"example.com\", {\n    action: { type: \"PIN_CURRENT_TAB\" },\n    category: \"cfrFeatures\",\n    layout: \"message_and_animation\",\n  });\n  Assert.ok(\n    response,\n    \"Should return true if addRecommendation checks were successful\"\n  );\n\n  let showPanel = BrowserTestUtils.waitForEvent(\n    PopupNotifications.panel,\n    \"popupshown\"\n  );\n  // Open the panel\n  document.getElementById(\"contextual-feature-recommendation\").click();\n  await showPanel;\n\n  const notification = document.getElementById(\n    \"contextual-feature-recommendation-notification\"\n  );\n  checkCFRFeaturesElements(notification);\n\n  // Check there is a primary button and click it. It will trigger the callback.\n  Assert.ok(notification.button);\n  let hidePanel = BrowserTestUtils.waitForEvent(\n    PopupNotifications.panel,\n    \"popuphidden\"\n  );\n  document\n    .getElementById(\"contextual-feature-recommendation-notification\")\n    .button.click();\n  await hidePanel;\n\n  // Clicking the primary action also removes the notification\n  Assert.equal(\n    PopupNotifications._currentNotifications.length,\n    0,\n    \"Should have removed the notification\"\n  );\n\n  // Trigger Addon CFR\n  response = await trigger_cfr_panel(browser, \"example.com\");\n  Assert.ok(\n    response,\n    \"Should return true if addRecommendation checks were successful\"\n  );\n\n  showPanel = BrowserTestUtils.waitForEvent(\n    PopupNotifications.panel,\n    \"popupshown\"\n  );\n  // Open the panel\n  document.getElementById(\"contextual-feature-recommendation\").click();\n  await showPanel;\n\n  Assert.ok(\n    document.getElementById(\"contextual-feature-recommendation-notification\")\n      .hidden === false,\n    \"Panel should be visible\"\n  );\n  checkCFRAddonsElements(\n    document.getElementById(\"contextual-feature-recommendation-notification\")\n  );\n\n  // Check there is a primary button and click it. It will trigger the callback.\n  Assert.ok(notification.button);\n  hidePanel = BrowserTestUtils.waitForEvent(\n    PopupNotifications.panel,\n    \"popuphidden\"\n  );\n  document\n    .getElementById(\"contextual-feature-recommendation-notification\")\n    .button.click();\n  await hidePanel;\n\n  // Clicking the primary action also removes the notification\n  Assert.equal(\n    PopupNotifications._currentNotifications.length,\n    0,\n    \"Should have removed the notification\"\n  );\n});\n\nadd_task(async function test_cfr_addon_and_features_show() {\n  // addRecommendation checks that scheme starts with http and host matches\n  let browser = gBrowser.selectedBrowser;\n  await BrowserTestUtils.loadURI(browser, \"http://example.com/\");\n  await BrowserTestUtils.browserLoaded(browser, false, \"http://example.com/\");\n\n  // Trigger Feature CFR\n  let response = await trigger_cfr_panel(browser, \"example.com\");\n  Assert.ok(\n    response,\n    \"Should return true if addRecommendation checks were successful\"\n  );\n\n  let showPanel = BrowserTestUtils.waitForEvent(\n    PopupNotifications.panel,\n    \"popupshown\"\n  );\n  // Open the panel\n  document.getElementById(\"contextual-feature-recommendation\").click();\n  await showPanel;\n\n  const notification = document.getElementById(\n    \"contextual-feature-recommendation-notification\"\n  );\n  checkCFRAddonsElements(notification);\n\n  // Check there is a primary button and click it. It will trigger the callback.\n  Assert.ok(notification.button);\n  let hidePanel = BrowserTestUtils.waitForEvent(\n    PopupNotifications.panel,\n    \"popuphidden\"\n  );\n  document\n    .getElementById(\"contextual-feature-recommendation-notification\")\n    .button.click();\n  await hidePanel;\n\n  // Clicking the primary action also removes the notification\n  Assert.equal(\n    PopupNotifications._currentNotifications.length,\n    0,\n    \"Should have removed the notification\"\n  );\n\n  // Trigger Addon CFR\n  response = await trigger_cfr_panel(browser, \"example.com\", {\n    action: { type: \"PIN_CURRENT_TAB\" },\n    category: \"cfrAddons\",\n  });\n  Assert.ok(\n    response,\n    \"Should return true if addRecommendation checks were successful\"\n  );\n\n  showPanel = BrowserTestUtils.waitForEvent(\n    PopupNotifications.panel,\n    \"popupshown\"\n  );\n  // Open the panel\n  document.getElementById(\"contextual-feature-recommendation\").click();\n  await showPanel;\n\n  Assert.ok(\n    document.getElementById(\"contextual-feature-recommendation-notification\")\n      .hidden === false,\n    \"Panel should be visible\"\n  );\n  checkCFRAddonsElements(\n    document.getElementById(\"contextual-feature-recommendation-notification\")\n  );\n\n  // Check there is a primary button and click it. It will trigger the callback.\n  Assert.ok(notification.button);\n  hidePanel = BrowserTestUtils.waitForEvent(\n    PopupNotifications.panel,\n    \"popuphidden\"\n  );\n  document\n    .getElementById(\"contextual-feature-recommendation-notification\")\n    .button.click();\n  await hidePanel;\n\n  // Clicking the primary action also removes the notification\n  Assert.equal(\n    PopupNotifications._currentNotifications.length,\n    0,\n    \"Should have removed the notification\"\n  );\n});\n\nadd_task(async function test_onLocationChange_cb() {\n  let count = 0;\n  const triggerHandler = () => ++count;\n  const TEST_URL =\n    \"https://example.com/browser/browser/components/newtab/test/browser/blue_page.html\";\n  const browser = gBrowser.selectedBrowser;\n\n  await ASRouterTriggerListeners.get(\"openURL\").init(triggerHandler, [\n    \"example.com\",\n  ]);\n\n  await BrowserTestUtils.loadURI(browser, \"about:blank\");\n  await BrowserTestUtils.browserLoaded(browser, false, \"about:blank\");\n\n  await BrowserTestUtils.loadURI(browser, \"http://example.com/\");\n  await BrowserTestUtils.browserLoaded(browser, false, \"http://example.com/\");\n\n  Assert.equal(count, 1, \"Count navigation to example.com\");\n\n  // Anchor scroll triggers a location change event with the same document\n  // https://searchfox.org/mozilla-central/rev/8848b9741fc4ee4e9bc3ae83ea0fc048da39979f/uriloader/base/nsIWebProgressListener.idl#400-403\n  await BrowserTestUtils.loadURI(browser, \"http://example.com/#foo\");\n  await BrowserTestUtils.waitForLocationChange(\n    gBrowser,\n    \"http://example.com/#foo\"\n  );\n\n  Assert.equal(count, 1, \"It should ignore same page navigation\");\n\n  await BrowserTestUtils.loadURI(browser, TEST_URL);\n  await BrowserTestUtils.browserLoaded(browser, false, TEST_URL);\n\n  Assert.equal(count, 2, \"We moved to a new document\");\n\n  registerCleanupFunction(() => {\n    ASRouterTriggerListeners.get(\"openURL\").uninit();\n  });\n});\n\nadd_task(async function test_matchPattern() {\n  let count = 0;\n  const triggerHandler = () => ++count;\n  const frequentVisitsTrigger = ASRouterTriggerListeners.get(\"frequentVisits\");\n  await frequentVisitsTrigger.init(triggerHandler, [], [\"*://*.example.com/\"]);\n\n  const browser = gBrowser.selectedBrowser;\n  await BrowserTestUtils.loadURI(browser, \"http://example.com/\");\n  await BrowserTestUtils.browserLoaded(browser, false, \"http://example.com/\");\n\n  await BrowserTestUtils.waitForCondition(\n    () => frequentVisitsTrigger._visits.get(\"example.com\").length === 1,\n    \"Registered pattern matched the current location\"\n  );\n\n  await BrowserTestUtils.loadURI(browser, \"about:config\");\n  await BrowserTestUtils.browserLoaded(browser, false, \"about:config\");\n\n  await BrowserTestUtils.waitForCondition(\n    () => frequentVisitsTrigger._visits.get(\"example.com\").length === 1,\n    \"Navigated to a new page but not a match\"\n  );\n\n  await BrowserTestUtils.loadURI(browser, \"http://example.com/\");\n  await BrowserTestUtils.browserLoaded(browser, false, \"http://example.com/\");\n\n  await BrowserTestUtils.waitForCondition(\n    () => frequentVisitsTrigger._visits.get(\"example.com\").length === 1,\n    \"Navigated to a location that matches the pattern but within 15 mins\"\n  );\n\n  await BrowserTestUtils.loadURI(browser, \"http://www.example.com/\");\n  await BrowserTestUtils.browserLoaded(\n    browser,\n    false,\n    \"http://www.example.com/\"\n  );\n\n  await BrowserTestUtils.waitForCondition(\n    () => frequentVisitsTrigger._visits.get(\"www.example.com\").length === 1,\n    \"www.example.com is a different host that also matches the pattern.\"\n  );\n  await BrowserTestUtils.waitForCondition(\n    () => frequentVisitsTrigger._visits.get(\"example.com\").length === 1,\n    \"www.example.com is a different host that also matches the pattern.\"\n  );\n\n  registerCleanupFunction(() => {\n    ASRouterTriggerListeners.get(\"frequentVisits\").uninit();\n  });\n});\n\nadd_task(async function test_providerNames() {\n  const providersBranch =\n    \"browser.newtabpage.activity-stream.asrouter.providers.\";\n  const cfrProviderPrefs = Services.prefs.getChildList(providersBranch);\n  for (const prefName of cfrProviderPrefs) {\n    const prefValue = JSON.parse(Services.prefs.getStringPref(prefName));\n    if (prefValue.id) {\n      // Snippets are disabled in tests and value is set to []\n      Assert.equal(\n        prefValue.id,\n        prefName.slice(providersBranch.length),\n        \"Provider id and pref name do not match\"\n      );\n    }\n  }\n});\n\nadd_task(async function test_cfr_notification_keyboard() {\n  // addRecommendation checks that scheme starts with http and host matches\n  const browser = gBrowser.selectedBrowser;\n  await BrowserTestUtils.loadURI(browser, \"http://example.com/\");\n  await BrowserTestUtils.browserLoaded(browser, false, \"http://example.com/\");\n\n  const response = await trigger_cfr_panel(browser, \"example.com\");\n  Assert.ok(\n    response,\n    \"Should return true if addRecommendation checks were successful\"\n  );\n\n  // Open the panel with the keyboard.\n  // Toolbar buttons aren't always focusable; toolbar keyboard navigation\n  // makes them focusable on demand. Therefore, we must force focus.\n  const button = document.getElementById(\"contextual-feature-recommendation\");\n  button.setAttribute(\"tabindex\", \"-1\");\n  button.focus();\n  button.removeAttribute(\"tabindex\");\n\n  let focused = BrowserTestUtils.waitForEvent(\n    PopupNotifications.panel,\n    \"focus\",\n    true\n  );\n  EventUtils.synthesizeKey(\" \");\n  await focused;\n  Assert.ok(true, \"Focus inside panel after button pressed\");\n\n  let hidden = BrowserTestUtils.waitForEvent(\n    PopupNotifications.panel,\n    \"popuphidden\"\n  );\n  EventUtils.synthesizeKey(\"KEY_Escape\");\n  await hidden;\n  Assert.ok(true, \"Panel hidden after Escape pressed\");\n});\n"
  },
  {
    "path": "test/browser/browser_asrouter_snippets.js",
    "content": "\"use strict\";\n\nconst { ASRouter } = ChromeUtils.import(\n  \"resource://activity-stream/lib/ASRouter.jsm\"\n);\nconst { SnippetsTestMessageProvider } = ChromeUtils.import(\n  \"resource://activity-stream/lib/SnippetsTestMessageProvider.jsm\"\n);\n\ntest_newtab({\n  async before() {\n    let data = SnippetsTestMessageProvider.getMessages().find(\n      m => m.id === \"SIMPLE_BELOW_SEARCH_TEST_1\"\n    );\n    ASRouter.messageChannel.sendAsyncMessage(\"ASRouter:parent-to-child\", {\n      type: \"SET_MESSAGE\",\n      data,\n    });\n  },\n  test: async function test_simple_below_search_snippet() {\n    // Verify the simple_below_search_snippet renders in container below searchbox\n    // and nothing is rendered in the footer.\n    await ContentTaskUtils.waitForCondition(\n      () =>\n        content.document.querySelector(\n          \".below-search-snippet .SimpleBelowSearchSnippet\"\n        ),\n      \"Should find the snippet inside the below search container\"\n    );\n\n    is(\n      0,\n      content.document.querySelector(\"#footer-asrouter-container\").childNodes\n        .length,\n      \"Should not find any snippets in the footer container\"\n    );\n  },\n});\n\ntest_newtab({\n  async before() {\n    let data = SnippetsTestMessageProvider.getMessages().find(\n      m => m.id === \"SIMPLE_TEST_1\"\n    );\n    ASRouter.messageChannel.sendAsyncMessage(\"ASRouter:parent-to-child\", {\n      type: \"SET_MESSAGE\",\n      data,\n    });\n  },\n  test: async function test_simple_snippet() {\n    // Verify the simple_snippet renders in the footer and the container below\n    // searchbox is not rendered.\n    await ContentTaskUtils.waitForCondition(\n      () =>\n        content.document.querySelector(\n          \"#footer-asrouter-container .SimpleSnippet\"\n        ),\n      \"Should find the snippet inside the footer container\"\n    );\n\n    ok(\n      !content.document.querySelector(\".below-search-snippet\"),\n      \"Should not find any snippets below search\"\n    );\n  },\n});\n"
  },
  {
    "path": "test/browser/browser_asrouter_targeting.js",
    "content": "const { ASRouterTargeting, QueryCache } = ChromeUtils.import(\n  \"resource://activity-stream/lib/ASRouterTargeting.jsm\"\n);\nconst { AddonTestUtils } = ChromeUtils.import(\n  \"resource://testing-common/AddonTestUtils.jsm\"\n);\nconst { CFRMessageProvider } = ChromeUtils.import(\n  \"resource://activity-stream/lib/CFRMessageProvider.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"ProfileAge\",\n  \"resource://gre/modules/ProfileAge.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"AddonManager\",\n  \"resource://gre/modules/AddonManager.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"ShellService\",\n  \"resource:///modules/ShellService.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"NewTabUtils\",\n  \"resource://gre/modules/NewTabUtils.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"PlacesTestUtils\",\n  \"resource://testing-common/PlacesTestUtils.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"TelemetryEnvironment\",\n  \"resource://gre/modules/TelemetryEnvironment.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"AppConstants\",\n  \"resource://gre/modules/AppConstants.jsm\"\n);\n\n// ASRouterTargeting.isMatch\nadd_task(async function should_do_correct_targeting() {\n  is(\n    await ASRouterTargeting.isMatch(\"FOO\", { FOO: true }),\n    true,\n    \"should return true for a matching value\"\n  );\n  is(\n    await ASRouterTargeting.isMatch(\"!FOO\", { FOO: true }),\n    false,\n    \"should return false for a non-matching value\"\n  );\n});\n\nadd_task(async function should_handle_async_getters() {\n  const context = {\n    get FOO() {\n      return Promise.resolve(true);\n    },\n  };\n  is(\n    await ASRouterTargeting.isMatch(\"FOO\", context),\n    true,\n    \"should return true for a matching async value\"\n  );\n});\n\n// ASRouterTargeting.findMatchingMessage\nadd_task(async function find_matching_message() {\n  const messages = [\n    { id: \"foo\", targeting: \"FOO\" },\n    { id: \"bar\", targeting: \"!FOO\" },\n  ];\n  const context = { FOO: true };\n\n  const match = await ASRouterTargeting.findMatchingMessage({\n    messages,\n    context,\n  });\n\n  is(match, messages[0], \"should match and return the correct message\");\n});\n\nadd_task(async function return_nothing_for_no_matching_message() {\n  const messages = [{ id: \"bar\", targeting: \"!FOO\" }];\n  const context = { FOO: true };\n\n  const match = await ASRouterTargeting.findMatchingMessage({\n    messages,\n    context,\n  });\n\n  is(\n    match,\n    undefined,\n    \"should return nothing since no matching message exists\"\n  );\n});\n\nadd_task(async function check_syntax_error_handling() {\n  let result;\n  function onError(...args) {\n    result = args;\n  }\n\n  const messages = [{ id: \"foo\", targeting: \"foo === 0\" }];\n  const match = await ASRouterTargeting.findMatchingMessage({\n    messages,\n    onError,\n  });\n\n  is(\n    match,\n    undefined,\n    \"should return nothing since no valid matching message exists\"\n  );\n  // Note that in order for the following test to pass, we are expecting a particular filepath for mozjexl.\n  // If the location of this file has changed, the MOZ_JEXL_FILEPATH constant should be updated om ASRouterTargeting.jsm\n  is(\n    result[0],\n    ASRouterTargeting.ERROR_TYPES.MALFORMED_EXPRESSION,\n    \"should recognize the error as coming from mozjexl and call onError with the MALFORMED_EXPRESSION error type\"\n  );\n  ok(result[1].message, \"should call onError with the error from mozjexl\");\n  is(result[2], messages[0], \"should call onError with the invalid message\");\n});\n\nadd_task(async function check_other_error_handling() {\n  let result;\n  function onError(...args) {\n    result = args;\n  }\n\n  const messages = [{ id: \"foo\", targeting: \"foo\" }];\n  const context = {\n    get foo() {\n      throw new Error(\"test error\");\n    },\n  };\n  const match = await ASRouterTargeting.findMatchingMessage({\n    messages,\n    context,\n    onError,\n  });\n\n  is(\n    match,\n    undefined,\n    \"should return nothing since no valid matching message exists\"\n  );\n  // Note that in order for the following test to pass, we are expecting a particular filepath for mozjexl.\n  // If the location of this file has changed, the MOZ_JEXL_FILEPATH constant should be updated om ASRouterTargeting.jsm\n  is(\n    result[0],\n    ASRouterTargeting.ERROR_TYPES.OTHER_ERROR,\n    \"should not recognize the error as being an other error, not a mozjexl one\"\n  );\n  is(\n    result[1].message,\n    \"test error\",\n    \"should call onError with the error thrown in the context\"\n  );\n  is(result[2], messages[0], \"should call onError with the invalid message\");\n});\n\n// ASRouterTargeting.Environment\nadd_task(async function check_locale() {\n  ok(\n    Services.locale.appLocaleAsLangTag,\n    \"Services.locale.appLocaleAsLangTag exists\"\n  );\n  const message = {\n    id: \"foo\",\n    targeting: `locale == \"${Services.locale.appLocaleAsLangTag}\"`,\n  };\n  is(\n    await ASRouterTargeting.findMatchingMessage({ messages: [message] }),\n    message,\n    \"should select correct item when filtering by locale\"\n  );\n});\nadd_task(async function check_localeLanguageCode() {\n  const currentLanguageCode = Services.locale.appLocaleAsLangTag.substr(0, 2);\n  is(\n    Services.locale.negotiateLanguages(\n      [currentLanguageCode],\n      [Services.locale.appLocaleAsLangTag]\n    )[0],\n    Services.locale.appLocaleAsLangTag,\n    \"currentLanguageCode should resolve to the current locale (e.g en => en-US)\"\n  );\n  const message = {\n    id: \"foo\",\n    targeting: `localeLanguageCode == \"${currentLanguageCode}\"`,\n  };\n  is(\n    await ASRouterTargeting.findMatchingMessage({ messages: [message] }),\n    message,\n    \"should select correct item when filtering by localeLanguageCode\"\n  );\n});\n\nadd_task(async function checkProfileAgeCreated() {\n  let profileAccessor = await ProfileAge();\n  is(\n    await ASRouterTargeting.Environment.profileAgeCreated,\n    await profileAccessor.created,\n    \"should return correct profile age creation date\"\n  );\n\n  const message = {\n    id: \"foo\",\n    targeting: `profileAgeCreated > ${(await profileAccessor.created) - 100}`,\n  };\n  is(\n    await ASRouterTargeting.findMatchingMessage({ messages: [message] }),\n    message,\n    \"should select correct item by profile age created\"\n  );\n});\n\nadd_task(async function checkProfileAgeReset() {\n  let profileAccessor = await ProfileAge();\n  is(\n    await ASRouterTargeting.Environment.profileAgeReset,\n    await profileAccessor.reset,\n    \"should return correct profile age reset\"\n  );\n\n  const message = {\n    id: \"foo\",\n    targeting: `profileAgeReset == ${await profileAccessor.reset}`,\n  };\n  is(\n    await ASRouterTargeting.findMatchingMessage({ messages: [message] }),\n    message,\n    \"should select correct item by profile age reset\"\n  );\n});\n\nadd_task(async function checkCurrentDate() {\n  let message = {\n    id: \"foo\",\n    targeting: `currentDate < '${new Date(Date.now() + 5000)}'|date`,\n  };\n  is(\n    await ASRouterTargeting.findMatchingMessage({ messages: [message] }),\n    message,\n    \"should select message based on currentDate < timestamp\"\n  );\n\n  message = {\n    id: \"foo\",\n    targeting: `currentDate > '${new Date(Date.now() - 5000)}'|date`,\n  };\n  is(\n    await ASRouterTargeting.findMatchingMessage({ messages: [message] }),\n    message,\n    \"should select message based on currentDate > timestamp\"\n  );\n});\n\nadd_task(async function check_usesFirefoxSync() {\n  await pushPrefs([\"services.sync.username\", \"someone@foo.com\"]);\n  is(\n    await ASRouterTargeting.Environment.usesFirefoxSync,\n    true,\n    \"should return true if a fx account is set\"\n  );\n\n  const message = { id: \"foo\", targeting: \"usesFirefoxSync\" };\n  is(\n    await ASRouterTargeting.findMatchingMessage({ messages: [message] }),\n    message,\n    \"should select correct item by usesFirefoxSync\"\n  );\n});\n\nadd_task(async function check_isFxAEnabled() {\n  await pushPrefs([\"identity.fxaccounts.enabled\", false]);\n  is(\n    await ASRouterTargeting.Environment.isFxAEnabled,\n    false,\n    \"should return false if fxa is disabled\"\n  );\n\n  const message = { id: \"foo\", targeting: \"isFxAEnabled\" };\n  is(\n    await ASRouterTargeting.findMatchingMessage({ messages: [message] }),\n    undefined,\n    \"should not select a message if fxa is disabled\"\n  );\n});\n\nadd_task(async function check_isFxAEnabled() {\n  await pushPrefs([\"identity.fxaccounts.enabled\", true]);\n  is(\n    await ASRouterTargeting.Environment.isFxAEnabled,\n    true,\n    \"should return true if fxa is enabled\"\n  );\n\n  const message = { id: \"foo\", targeting: \"isFxAEnabled\" };\n  is(\n    await ASRouterTargeting.findMatchingMessage({ messages: [message] }),\n    message,\n    \"should select the correct message\"\n  );\n});\n\nadd_task(async function check_totalBookmarksCount() {\n  // Make sure we remove default bookmarks so they don't interfere\n  await clearHistoryAndBookmarks();\n  const message = { id: \"foo\", targeting: \"totalBookmarksCount > 0\" };\n\n  const results = await ASRouterTargeting.findMatchingMessage({\n    messages: [message],\n  });\n  is(\n    results ? JSON.stringify(results) : results,\n    undefined,\n    \"Should not select any message because bookmarks count is not 0\"\n  );\n\n  const bookmark = await PlacesUtils.bookmarks.insert({\n    parentGuid: PlacesUtils.bookmarks.unfiledGuid,\n    title: \"foo\",\n    url: \"https://mozilla1.com/nowNew\",\n  });\n\n  QueryCache.queries.TotalBookmarksCount.expire();\n\n  is(\n    await ASRouterTargeting.findMatchingMessage({ messages: [message] }),\n    message,\n    \"Should select correct item after bookmarks are added.\"\n  );\n\n  // Cleanup\n  await PlacesUtils.bookmarks.remove(bookmark.guid);\n});\n\nadd_task(async function check_needsUpdate() {\n  QueryCache.queries.CheckBrowserNeedsUpdate.setUp(true);\n\n  const message = { id: \"foo\", targeting: \"needsUpdate\" };\n\n  is(\n    await ASRouterTargeting.findMatchingMessage({ messages: [message] }),\n    message,\n    \"Should select message because update count > 0\"\n  );\n\n  QueryCache.queries.CheckBrowserNeedsUpdate.setUp(false);\n\n  is(\n    await ASRouterTargeting.findMatchingMessage({ messages: [message] }),\n    null,\n    \"Should not select message because update count == 0\"\n  );\n});\n\nadd_task(async function checksearchEngines() {\n  const result = await ASRouterTargeting.Environment.searchEngines;\n  const expectedInstalled = (await Services.search.getVisibleEngines())\n    .map(engine => engine.identifier)\n    .sort()\n    .join(\",\");\n  ok(\n    result.installed.length,\n    \"searchEngines.installed should be a non-empty array\"\n  );\n  is(\n    result.installed.sort().join(\",\"),\n    expectedInstalled,\n    \"searchEngines.installed should be an array of visible search engines\"\n  );\n  ok(\n    result.current && typeof result.current === \"string\",\n    \"searchEngines.current should be a truthy string\"\n  );\n  is(\n    result.current,\n    (await Services.search.getDefault()).identifier,\n    \"searchEngines.current should be the current engine name\"\n  );\n\n  const message = {\n    id: \"foo\",\n    targeting: `searchEngines[.current == ${\n      (await Services.search.getDefault()).identifier\n    }]`,\n  };\n  is(\n    await ASRouterTargeting.findMatchingMessage({ messages: [message] }),\n    message,\n    \"should select correct item by searchEngines.current\"\n  );\n\n  const message2 = {\n    id: \"foo\",\n    targeting: `searchEngines[${\n      (await Services.search.getVisibleEngines())[0].identifier\n    } in .installed]`,\n  };\n  is(\n    await ASRouterTargeting.findMatchingMessage({ messages: [message2] }),\n    message2,\n    \"should select correct item by searchEngines.installed\"\n  );\n});\n\nadd_task(async function checkisDefaultBrowser() {\n  const expected = ShellService.isDefaultBrowser();\n  const result = ASRouterTargeting.Environment.isDefaultBrowser;\n  is(typeof result, \"boolean\", \"isDefaultBrowser should be a boolean value\");\n  is(\n    result,\n    expected,\n    \"isDefaultBrowser should be equal to ShellService.isDefaultBrowser()\"\n  );\n  const message = {\n    id: \"foo\",\n    targeting: `isDefaultBrowser == ${expected.toString()}`,\n  };\n  is(\n    await ASRouterTargeting.findMatchingMessage({ messages: [message] }),\n    message,\n    \"should select correct item by isDefaultBrowser\"\n  );\n});\n\nadd_task(async function checkdevToolsOpenedCount() {\n  await pushPrefs([\"devtools.selfxss.count\", 5]);\n  is(\n    ASRouterTargeting.Environment.devToolsOpenedCount,\n    5,\n    \"devToolsOpenedCount should be equal to devtools.selfxss.count pref value\"\n  );\n  const message = { id: \"foo\", targeting: \"devToolsOpenedCount >= 5\" };\n  is(\n    await ASRouterTargeting.findMatchingMessage({ messages: [message] }),\n    message,\n    \"should select correct item by devToolsOpenedCount\"\n  );\n});\n\nadd_task(async function check_platformName() {\n  const message = {\n    id: \"foo\",\n    targeting: `platformName == \"${AppConstants.platform}\"`,\n  };\n  is(\n    await ASRouterTargeting.findMatchingMessage({ messages: [message] }),\n    message,\n    \"should select correct item by platformName\"\n  );\n});\n\nAddonTestUtils.initMochitest(this);\n\nadd_task(async function checkAddonsInfo() {\n  const FAKE_ID = \"testaddon@tests.mozilla.org\";\n  const FAKE_NAME = \"Test Addon\";\n  const FAKE_VERSION = \"0.5.7\";\n\n  const xpi = AddonTestUtils.createTempWebExtensionFile({\n    manifest: {\n      applications: { gecko: { id: FAKE_ID } },\n      name: FAKE_NAME,\n      version: FAKE_VERSION,\n    },\n  });\n\n  await Promise.all([\n    AddonTestUtils.promiseWebExtensionStartup(FAKE_ID),\n    AddonManager.installTemporaryAddon(xpi),\n  ]);\n\n  const { addons } = await AddonManager.getActiveAddons([\n    \"extension\",\n    \"service\",\n  ]);\n\n  const { addons: asRouterAddons, isFullData } = await ASRouterTargeting\n    .Environment.addonsInfo;\n\n  ok(\n    addons.every(({ id }) => asRouterAddons[id]),\n    \"should contain every addon\"\n  );\n\n  ok(\n    Object.getOwnPropertyNames(asRouterAddons).every(id =>\n      addons.some(addon => addon.id === id)\n    ),\n    \"should contain no incorrect addons\"\n  );\n\n  const testAddon = asRouterAddons[FAKE_ID];\n\n  ok(\n    Object.prototype.hasOwnProperty.call(testAddon, \"version\") &&\n      testAddon.version === FAKE_VERSION,\n    \"should correctly provide `version` property\"\n  );\n\n  ok(\n    Object.prototype.hasOwnProperty.call(testAddon, \"type\") &&\n      testAddon.type === \"extension\",\n    \"should correctly provide `type` property\"\n  );\n\n  ok(\n    Object.prototype.hasOwnProperty.call(testAddon, \"isSystem\") &&\n      testAddon.isSystem === false,\n    \"should correctly provide `isSystem` property\"\n  );\n\n  ok(\n    Object.prototype.hasOwnProperty.call(testAddon, \"isWebExtension\") &&\n      testAddon.isWebExtension === true,\n    \"should correctly provide `isWebExtension` property\"\n  );\n\n  // As we installed our test addon the addons database must be initialised, so\n  // (in this test environment) we expect to receive \"full\" data\n\n  ok(isFullData, \"should receive full data\");\n\n  ok(\n    Object.prototype.hasOwnProperty.call(testAddon, \"name\") &&\n      testAddon.name === FAKE_NAME,\n    \"should correctly provide `name` property from full data\"\n  );\n\n  ok(\n    Object.prototype.hasOwnProperty.call(testAddon, \"userDisabled\") &&\n      testAddon.userDisabled === false,\n    \"should correctly provide `userDisabled` property from full data\"\n  );\n\n  ok(\n    Object.prototype.hasOwnProperty.call(testAddon, \"installDate\") &&\n      Math.abs(Date.now() - new Date(testAddon.installDate)) < 60 * 1000,\n    \"should correctly provide `installDate` property from full data\"\n  );\n});\n\nadd_task(async function checkFrecentSites() {\n  const now = Date.now();\n  const timeDaysAgo = numDays => now - numDays * 24 * 60 * 60 * 1000;\n\n  const visits = [];\n  for (const [uri, count, visitDate] of [\n    [\"https://mozilla1.com/\", 10, timeDaysAgo(0)], // frecency 1000\n    [\"https://mozilla2.com/\", 5, timeDaysAgo(1)], // frecency 500\n    [\"https://mozilla3.com/\", 1, timeDaysAgo(2)], // frecency 100\n  ]) {\n    [...Array(count).keys()].forEach(() =>\n      visits.push({\n        uri,\n        visitDate: visitDate * 1000, // Places expects microseconds\n      })\n    );\n  }\n\n  await PlacesTestUtils.addVisits(visits);\n\n  let message = {\n    id: \"foo\",\n    targeting: \"'mozilla3.com' in topFrecentSites|mapToProperty('host')\",\n  };\n  is(\n    await ASRouterTargeting.findMatchingMessage({ messages: [message] }),\n    message,\n    \"should select correct item by host in topFrecentSites\"\n  );\n\n  message = {\n    id: \"foo\",\n    targeting: \"'non-existent.com' in topFrecentSites|mapToProperty('host')\",\n  };\n  is(\n    await ASRouterTargeting.findMatchingMessage({ messages: [message] }),\n    undefined,\n    \"should not select incorrect item by host in topFrecentSites\"\n  );\n\n  message = {\n    id: \"foo\",\n    targeting:\n      \"'mozilla2.com' in topFrecentSites[.frecency >= 400]|mapToProperty('host')\",\n  };\n  is(\n    await ASRouterTargeting.findMatchingMessage({ messages: [message] }),\n    message,\n    \"should select correct item when filtering by frecency\"\n  );\n\n  message = {\n    id: \"foo\",\n    targeting:\n      \"'mozilla2.com' in topFrecentSites[.frecency >= 600]|mapToProperty('host')\",\n  };\n  is(\n    await ASRouterTargeting.findMatchingMessage({ messages: [message] }),\n    undefined,\n    \"should not select incorrect item when filtering by frecency\"\n  );\n\n  message = {\n    id: \"foo\",\n    targeting: `'mozilla2.com' in topFrecentSites[.lastVisitDate >= ${timeDaysAgo(\n      1\n    ) - 1}]|mapToProperty('host')`,\n  };\n  is(\n    await ASRouterTargeting.findMatchingMessage({ messages: [message] }),\n    message,\n    \"should select correct item when filtering by lastVisitDate\"\n  );\n\n  message = {\n    id: \"foo\",\n    targeting: `'mozilla2.com' in topFrecentSites[.lastVisitDate >= ${timeDaysAgo(\n      0\n    ) - 1}]|mapToProperty('host')`,\n  };\n  is(\n    await ASRouterTargeting.findMatchingMessage({ messages: [message] }),\n    undefined,\n    \"should not select incorrect item when filtering by lastVisitDate\"\n  );\n\n  message = {\n    id: \"foo\",\n    targeting: `(topFrecentSites[.frecency >= 900 && .lastVisitDate >= ${timeDaysAgo(\n      1\n    ) -\n      1}]|mapToProperty('host') intersect ['mozilla3.com', 'mozilla2.com', 'mozilla1.com'])|length > 0`,\n  };\n  is(\n    await ASRouterTargeting.findMatchingMessage({ messages: [message] }),\n    message,\n    \"should select correct item when filtering by frecency and lastVisitDate with multiple candidate domains\"\n  );\n\n  // Cleanup\n  await clearHistoryAndBookmarks();\n});\n\nadd_task(async function check_pinned_sites() {\n  const originalPin = JSON.stringify(NewTabUtils.pinnedLinks.links);\n  const sitesToPin = [\n    { url: \"https://foo.com\" },\n    { url: \"https://bloo.com\" },\n    { url: \"https://floogle.com\", searchTopSite: true },\n  ];\n  sitesToPin.forEach(site =>\n    NewTabUtils.pinnedLinks.pin(site, NewTabUtils.pinnedLinks.links.length)\n  );\n\n  // Unpinning adds null to the list of pinned sites, which we should test that we handle gracefully for our targeting\n  NewTabUtils.pinnedLinks.unpin(sitesToPin[1]);\n  ok(\n    NewTabUtils.pinnedLinks.links.includes(null),\n    \"should have set an item in pinned links to null via unpinning for testing\"\n  );\n\n  let message;\n\n  message = {\n    id: \"foo\",\n    targeting: \"'https://foo.com' in pinnedSites|mapToProperty('url')\",\n  };\n  is(\n    await ASRouterTargeting.findMatchingMessage({ messages: [message] }),\n    message,\n    \"should select correct item by url in pinnedSites\"\n  );\n\n  message = {\n    id: \"foo\",\n    targeting: \"'foo.com' in pinnedSites|mapToProperty('host')\",\n  };\n  is(\n    await ASRouterTargeting.findMatchingMessage({ messages: [message] }),\n    message,\n    \"should select correct item by host in pinnedSites\"\n  );\n\n  message = {\n    id: \"foo\",\n    targeting:\n      \"'floogle.com' in pinnedSites[.searchTopSite == true]|mapToProperty('host')\",\n  };\n  is(\n    await ASRouterTargeting.findMatchingMessage({ messages: [message] }),\n    message,\n    \"should select correct item by host and searchTopSite in pinnedSites\"\n  );\n\n  // Cleanup\n  sitesToPin.forEach(site => NewTabUtils.pinnedLinks.unpin(site));\n\n  await clearHistoryAndBookmarks();\n  is(\n    JSON.stringify(NewTabUtils.pinnedLinks.links),\n    originalPin,\n    \"should restore pinned sites to its original state\"\n  );\n});\n\nadd_task(async function check_firefox_version() {\n  const message = { id: \"foo\", targeting: \"firefoxVersion > 0\" };\n  is(\n    await ASRouterTargeting.findMatchingMessage({ messages: [message] }),\n    message,\n    \"should select correct item when filtering by firefox version\"\n  );\n});\n\nadd_task(async function check_region() {\n  await SpecialPowers.pushPrefEnv({ set: [[\"browser.search.region\", \"DE\"]] });\n\n  const message = { id: \"foo\", targeting: \"region in ['DE']\" };\n  is(\n    await ASRouterTargeting.findMatchingMessage({ messages: [message] }),\n    message,\n    \"should select correct item when filtering by firefox geo\"\n  );\n});\n\nadd_task(async function check_browserSettings() {\n  is(\n    await JSON.stringify(ASRouterTargeting.Environment.browserSettings.update),\n    JSON.stringify(TelemetryEnvironment.currentEnvironment.settings.update),\n    \"should return correct update info\"\n  );\n});\n\nadd_task(async function check_sync() {\n  is(\n    await ASRouterTargeting.Environment.sync.desktopDevices,\n    Services.prefs.getIntPref(\"services.sync.clients.devices.desktop\", 0),\n    \"should return correct desktopDevices info\"\n  );\n  is(\n    await ASRouterTargeting.Environment.sync.mobileDevices,\n    Services.prefs.getIntPref(\"services.sync.clients.devices.mobile\", 0),\n    \"should return correct mobileDevices info\"\n  );\n  is(\n    await ASRouterTargeting.Environment.sync.totalDevices,\n    Services.prefs.getIntPref(\"services.sync.numClients\", 0),\n    \"should return correct mobileDevices info\"\n  );\n});\n\nadd_task(async function check_provider_cohorts() {\n  await pushPrefs([\n    \"browser.newtabpage.activity-stream.asrouter.providers.onboarding\",\n    JSON.stringify({\n      id: \"onboarding\",\n      messages: [],\n      enabled: true,\n      cohort: \"foo\",\n    }),\n  ]);\n  await pushPrefs([\n    \"browser.newtabpage.activity-stream.asrouter.providers.cfr\",\n    JSON.stringify({ id: \"cfr\", enabled: true, cohort: \"bar\" }),\n  ]);\n  is(\n    await ASRouterTargeting.Environment.providerCohorts.onboarding,\n    \"foo\",\n    \"should have cohort foo for onboarding\"\n  );\n  is(\n    await ASRouterTargeting.Environment.providerCohorts.cfr,\n    \"bar\",\n    \"should have cohort bar for cfr\"\n  );\n});\n\nadd_task(async function check_xpinstall_enabled() {\n  // should default to true if pref doesn't exist\n  is(await ASRouterTargeting.Environment.xpinstallEnabled, true);\n  // flip to false, check targeting reflects that\n  await pushPrefs([\"xpinstall.enabled\", false]);\n  is(await ASRouterTargeting.Environment.xpinstallEnabled, false);\n  // flip to true, check targeting reflects that\n  await pushPrefs([\"xpinstall.enabled\", true]);\n  is(await ASRouterTargeting.Environment.xpinstallEnabled, true);\n});\n\nadd_task(async function check_pinned_tabs() {\n  await BrowserTestUtils.withNewTab(\n    { gBrowser, url: \"about:blank\" },\n    async browser => {\n      is(\n        await ASRouterTargeting.Environment.hasPinnedTabs,\n        false,\n        \"No pin tabs yet\"\n      );\n\n      let tab = gBrowser.getTabForBrowser(browser);\n      gBrowser.pinTab(tab);\n\n      is(\n        await ASRouterTargeting.Environment.hasPinnedTabs,\n        true,\n        \"Should detect pinned tab\"\n      );\n\n      gBrowser.unpinTab(tab);\n    }\n  );\n});\n\nadd_task(async function check_hasAccessedFxAPanel() {\n  is(\n    await ASRouterTargeting.Environment.hasAccessedFxAPanel,\n    false,\n    \"Not accessed yet\"\n  );\n\n  await pushPrefs([\"identity.fxaccounts.toolbar.accessed\", true]);\n\n  is(\n    await ASRouterTargeting.Environment.hasAccessedFxAPanel,\n    true,\n    \"Should detect panel access\"\n  );\n});\n\nadd_task(async function check_isFxABadgeEnabled() {\n  is(\n    await ASRouterTargeting.Environment.isFxABadgeEnabled,\n    true,\n    \"Default pref value is true\"\n  );\n\n  await pushPrefs([\"browser.messaging-system.fxatoolbarbadge.enabled\", false]);\n\n  is(\n    await ASRouterTargeting.Environment.isFxABadgeEnabled,\n    false,\n    \"Value should be false according to pref\"\n  );\n});\n\nadd_task(async function check_isWhatsNewPanelEnabled() {\n  is(\n    await ASRouterTargeting.Environment.isWhatsNewPanelEnabled,\n    true,\n    \"Enabled by default\"\n  );\n\n  await pushPrefs([\"browser.messaging-system.whatsNewPanel.enabled\", false]);\n\n  is(\n    await ASRouterTargeting.Environment.isWhatsNewPanelEnabled,\n    false,\n    \"Should update based on pref, e.g., for holdback\"\n  );\n});\n\nadd_task(async function checkCFRFeaturesUserPref() {\n  await pushPrefs([\n    \"browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features\",\n    false,\n  ]);\n  is(\n    ASRouterTargeting.Environment.userPrefs.cfrFeatures,\n    false,\n    \"cfrFeature should be false according to pref\"\n  );\n  const message = { id: \"foo\", targeting: \"userPrefs.cfrFeatures == false\" };\n  is(\n    await ASRouterTargeting.findMatchingMessage({ messages: [message] }),\n    message,\n    \"should select correct item by cfrFeature\"\n  );\n});\n\nadd_task(async function checkCFRAddonsUserPref() {\n  await pushPrefs([\n    \"browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons\",\n    false,\n  ]);\n  is(\n    ASRouterTargeting.Environment.userPrefs.cfrAddons,\n    false,\n    \"cfrFeature should be false according to pref\"\n  );\n  const message = { id: \"foo\", targeting: \"userPrefs.cfrAddons == false\" };\n  is(\n    await ASRouterTargeting.findMatchingMessage({ messages: [message] }),\n    message,\n    \"should select correct item by cfrAddons\"\n  );\n});\n\nadd_task(async function check_blockedCountByType() {\n  const message = {\n    id: \"foo\",\n    targeting:\n      \"blockedCountByType.cryptominerCount == 0 && blockedCountByType.socialCount == 0\",\n  };\n\n  is(\n    await ASRouterTargeting.findMatchingMessage({ messages: [message] }),\n    message,\n    \"should select correct item\"\n  );\n});\n\nadd_task(async function checkCFRPinnedTabsTargetting() {\n  const now = Date.now();\n  const timeMinutesAgo = numMinutes => now - numMinutes * 60 * 1000;\n  const messages = CFRMessageProvider.getMessages();\n  const trigger = {\n    id: \"frequentVisits\",\n    context: {\n      recentVisits: [\n        { timestamp: timeMinutesAgo(61) },\n        { timestamp: timeMinutesAgo(30) },\n        { timestamp: timeMinutesAgo(1) },\n      ],\n    },\n    param: { host: \"github.com\", url: \"https://google.com\" },\n  };\n\n  is(\n    await ASRouterTargeting.findMatchingMessage({ messages, trigger }),\n    undefined,\n    \"should not select PIN_TAB mesage with only 2 visits in past hour\"\n  );\n\n  trigger.context.recentVisits.push({ timestamp: timeMinutesAgo(59) });\n  is(\n    (await ASRouterTargeting.findMatchingMessage({ messages, trigger })).id,\n    \"PIN_TAB\",\n    \"should select PIN_TAB mesage\"\n  );\n\n  await BrowserTestUtils.withNewTab(\n    { gBrowser, url: \"about:blank\" },\n    async browser => {\n      let tab = gBrowser.getTabForBrowser(browser);\n      gBrowser.pinTab(tab);\n      is(\n        await ASRouterTargeting.findMatchingMessage({ messages, trigger }),\n        undefined,\n        \"should not select PIN_TAB mesage if there is a pinned tab already\"\n      );\n      gBrowser.unpinTab(tab);\n    }\n  );\n\n  trigger.param = { host: \"foo.bar\", url: \"https://foo.bar\" };\n  is(\n    await ASRouterTargeting.findMatchingMessage({ messages, trigger }),\n    undefined,\n    \"should not select PIN_TAB mesage with a trigger param/host not in our hostlist\"\n  );\n});\n\nadd_task(async function checkPatternMatches() {\n  const now = Date.now();\n  const timeMinutesAgo = numMinutes => now - numMinutes * 60 * 1000;\n  const messages = [\n    {\n      id: \"message_with_pattern\",\n      targeting: \"true\",\n      trigger: { id: \"frequentVisits\", patterns: [\"*://*.github.com/\"] },\n    },\n  ];\n  const trigger = {\n    id: \"frequentVisits\",\n    context: {\n      recentVisits: [\n        { timestamp: timeMinutesAgo(33) },\n        { timestamp: timeMinutesAgo(17) },\n        { timestamp: timeMinutesAgo(1) },\n      ],\n    },\n    param: { host: \"github.com\", url: \"https://gist.github.com\" },\n  };\n\n  is(\n    (await ASRouterTargeting.findMatchingMessage({ messages, trigger })).id,\n    \"message_with_pattern\",\n    \"should select PIN_TAB mesage\"\n  );\n});\n\nadd_task(async function checkPatternsValid() {\n  const messages = CFRMessageProvider.getMessages().filter(\n    m => m.trigger.patterns\n  );\n\n  for (const message of messages) {\n    Assert.ok(new MatchPatternSet(message.trigger.patterns));\n  }\n});\n"
  },
  {
    "path": "test/browser/browser_asrouter_toolbarbadge.js",
    "content": "const { OnboardingMessageProvider } = ChromeUtils.import(\n  \"resource://activity-stream/lib/OnboardingMessageProvider.jsm\"\n);\nconst { ToolbarBadgeHub } = ChromeUtils.import(\n  \"resource://activity-stream/lib/ToolbarBadgeHub.jsm\"\n);\n\nadd_task(async function test_setup() {\n  // Cleanup pref value because we click the fxa accounts button.\n  // This is not required during tests because we \"force show\" the message\n  // by sending it directly to the Hub bypassing targeting.\n  registerCleanupFunction(() => {\n    Services.prefs.clearUserPref(\"identity.fxaccounts.toolbar.accessed\");\n  });\n});\n\nadd_task(async function test_fxa_badge_shown_nodelay() {\n  const [msg] = (await OnboardingMessageProvider.getMessages()).filter(\n    ({ id }) => id === \"FXA_ACCOUNTS_BADGE\"\n  );\n\n  Assert.ok(msg, \"FxA test message exists\");\n\n  // Ensure we badge immediately\n  msg.content.delay = undefined;\n\n  let browserWindow = Services.wm.getMostRecentWindow(\"navigator:browser\");\n  // Click the button and clear the badge that occurs normally at startup\n  let fxaButton = browserWindow.document.getElementById(msg.content.target);\n  fxaButton.click();\n\n  await BrowserTestUtils.waitForCondition(\n    () =>\n      !browserWindow.document\n        .getElementById(msg.content.target)\n        .querySelector(\".toolbarbutton-badge\")\n        .classList.contains(\"feature-callout\"),\n    \"Initially element is not badged\"\n  );\n\n  ToolbarBadgeHub.registerBadgeNotificationListener(msg);\n\n  await BrowserTestUtils.waitForCondition(\n    () =>\n      browserWindow.document\n        .getElementById(msg.content.target)\n        .querySelector(\".toolbarbutton-badge\")\n        .classList.contains(\"feature-callout\"),\n    \"Wait for element to be badged\"\n  );\n\n  let newWin = await BrowserTestUtils.openNewBrowserWindow();\n  browserWindow = Services.wm.getMostRecentWindow(\"navigator:browser\");\n\n  await BrowserTestUtils.waitForCondition(\n    () =>\n      browserWindow.document\n        .getElementById(msg.content.target)\n        .querySelector(\".toolbarbutton-badge\")\n        .classList.contains(\"feature-callout\"),\n    \"Wait for element to be badged\"\n  );\n\n  await BrowserTestUtils.closeWindow(newWin);\n  browserWindow = Services.wm.getMostRecentWindow(\"navigator:browser\");\n\n  // Click the button and clear the badge\n  fxaButton = document.getElementById(msg.content.target);\n  fxaButton.click();\n\n  await BrowserTestUtils.waitForCondition(\n    () =>\n      !browserWindow.document\n        .getElementById(msg.content.target)\n        .querySelector(\".toolbarbutton-badge\")\n        .classList.contains(\"feature-callout\"),\n    \"Button should no longer be badged\"\n  );\n});\n\nadd_task(async function test_fxa_badge_shown_withdelay() {\n  const [msg] = (await OnboardingMessageProvider.getMessages()).filter(\n    ({ id }) => id === \"FXA_ACCOUNTS_BADGE\"\n  );\n\n  Assert.ok(msg, \"FxA test message exists\");\n\n  // Enough to trigger the setTimeout badging\n  msg.content.delay = 1;\n\n  let browserWindow = Services.wm.getMostRecentWindow(\"navigator:browser\");\n  // Click the button and clear the badge that occurs normally at startup\n  let fxaButton = browserWindow.document.getElementById(msg.content.target);\n  fxaButton.click();\n\n  await BrowserTestUtils.waitForCondition(\n    () =>\n      !browserWindow.document\n        .getElementById(msg.content.target)\n        .querySelector(\".toolbarbutton-badge\")\n        .classList.contains(\"feature-callout\"),\n    \"Initially element is not badged\"\n  );\n\n  ToolbarBadgeHub.registerBadgeNotificationListener(msg);\n\n  await BrowserTestUtils.waitForCondition(\n    () =>\n      browserWindow.document\n        .getElementById(msg.content.target)\n        .querySelector(\".toolbarbutton-badge\")\n        .classList.contains(\"feature-callout\"),\n    \"Wait for element to be badged\"\n  );\n\n  let newWin = await BrowserTestUtils.openNewBrowserWindow();\n  browserWindow = Services.wm.getMostRecentWindow(\"navigator:browser\");\n\n  await BrowserTestUtils.waitForCondition(\n    () =>\n      browserWindow.document\n        .getElementById(msg.content.target)\n        .querySelector(\".toolbarbutton-badge\")\n        .classList.contains(\"feature-callout\"),\n    \"Wait for element to be badged\"\n  );\n\n  await BrowserTestUtils.closeWindow(newWin);\n  browserWindow = Services.wm.getMostRecentWindow(\"navigator:browser\");\n\n  // Click the button and clear the badge\n  fxaButton = document.getElementById(msg.content.target);\n  fxaButton.click();\n\n  await BrowserTestUtils.waitForCondition(\n    () =>\n      !browserWindow.document\n        .getElementById(msg.content.target)\n        .querySelector(\".toolbarbutton-badge\")\n        .classList.contains(\"feature-callout\"),\n    \"Button should no longer be badged\"\n  );\n});\n"
  },
  {
    "path": "test/browser/browser_asrouter_trigger_listeners.js",
    "content": "ChromeUtils.defineModuleGetter(\n  this,\n  \"ASRouterTriggerListeners\",\n  \"resource://activity-stream/lib/ASRouterTriggerListeners.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"TestUtils\",\n  \"resource://testing-common/TestUtils.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"PrivateBrowsingUtils\",\n  \"resource://gre/modules/PrivateBrowsingUtils.jsm\"\n);\n\nasync function openURLInWindow(window, url) {\n  const { selectedBrowser } = window.gBrowser;\n  await BrowserTestUtils.loadURI(selectedBrowser, url);\n  await BrowserTestUtils.browserLoaded(selectedBrowser, false, url);\n}\n\nadd_task(async function check_matchPatternFailureCase() {\n  const articleTrigger = ASRouterTriggerListeners.get(\"openArticleURL\");\n\n  articleTrigger.uninit();\n\n  articleTrigger.init(() => {}, [], [\"example.com\"]);\n\n  is(\n    articleTrigger._matchPatternSet.matches(\"http://example.com\"),\n    false,\n    \"Should fail, bad pattern\"\n  );\n\n  articleTrigger.init(() => {}, [], [\"*://*.example.com\"]);\n\n  is(\n    articleTrigger._matchPatternSet.matches(\"http://www.example.com\"),\n    true,\n    \"Should work, updated pattern\"\n  );\n\n  articleTrigger.uninit();\n});\n\nadd_task(async function check_openArticleURL() {\n  const TEST_URL =\n    \"https://example.com/browser/browser/components/newtab/test/browser/red_page.html\";\n  const articleTrigger = ASRouterTriggerListeners.get(\"openArticleURL\");\n\n  // Previously initialized by the Router\n  articleTrigger.uninit();\n\n  // Initialize the trigger with a new triggerHandler that resolves a promise\n  // with the URL match\n  const listenerTriggered = new Promise(resolve =>\n    articleTrigger.init((browser, match) => resolve(match), [\"example.com\"])\n  );\n\n  const win = await BrowserTestUtils.openNewBrowserWindow();\n  await openURLInWindow(win, TEST_URL);\n  // Send a message from the content page (the TEST_URL) to the parent\n  // This should trigger the `receiveMessage` cb in the articleTrigger\n  await ContentTask.spawn(win.gBrowser.selectedBrowser, null, async () => {\n    sendAsyncMessage(\"Reader:UpdateReaderButton\", { isArticle: true });\n  });\n\n  await listenerTriggered.then(data =>\n    is(\n      data.param.url,\n      TEST_URL,\n      \"We should match on the TEST_URL as a website article\"\n    )\n  );\n\n  // Cleanup\n  articleTrigger.uninit();\n  await BrowserTestUtils.closeWindow(win);\n});\n\nadd_task(async function check_openURL_listener() {\n  const TEST_URL =\n    \"https://example.com/browser/browser/components/newtab/test/browser/red_page.html\";\n\n  let urlVisitCount = 0;\n  const triggerHandler = () => urlVisitCount++;\n  const openURLListener = ASRouterTriggerListeners.get(\"openURL\");\n\n  // Previously initialized by the Router\n  openURLListener.uninit();\n\n  const normalWindow = await BrowserTestUtils.openNewBrowserWindow();\n  const privateWindow = await BrowserTestUtils.openNewBrowserWindow({\n    private: true,\n  });\n\n  // Initialise listener\n  await openURLListener.init(triggerHandler, [\"example.com\"]);\n\n  await openURLInWindow(normalWindow, TEST_URL);\n  await BrowserTestUtils.waitForCondition(\n    () => urlVisitCount !== 0,\n    \"Wait for the location change listener to run\"\n  );\n  is(urlVisitCount, 1, \"should receive page visits from existing windows\");\n\n  await openURLInWindow(normalWindow, \"http://www.example.com/abc\");\n  is(urlVisitCount, 1, \"should not receive page visits for different domains\");\n\n  await openURLInWindow(privateWindow, TEST_URL);\n  is(\n    urlVisitCount,\n    1,\n    \"should not receive page visits from existing private windows\"\n  );\n\n  const secondNormalWindow = await BrowserTestUtils.openNewBrowserWindow();\n  await openURLInWindow(secondNormalWindow, TEST_URL);\n  await BrowserTestUtils.waitForCondition(\n    () => urlVisitCount === 2,\n    \"Wait for the location change listener to run\"\n  );\n  is(urlVisitCount, 2, \"should receive page visits from newly opened windows\");\n\n  const secondPrivateWindow = await BrowserTestUtils.openNewBrowserWindow({\n    private: true,\n  });\n  await openURLInWindow(secondPrivateWindow, TEST_URL);\n  is(\n    urlVisitCount,\n    2,\n    \"should not receive page visits from newly opened private windows\"\n  );\n\n  // Uninitialise listener\n  openURLListener.uninit();\n\n  await openURLInWindow(normalWindow, TEST_URL);\n  is(\n    urlVisitCount,\n    2,\n    \"should now not receive page visits from existing windows\"\n  );\n\n  const thirdNormalWindow = await BrowserTestUtils.openNewBrowserWindow();\n  await openURLInWindow(thirdNormalWindow, TEST_URL);\n  is(\n    urlVisitCount,\n    2,\n    \"should now not receive page visits from newly opened windows\"\n  );\n\n  // Cleanup\n  const windows = [\n    normalWindow,\n    privateWindow,\n    secondNormalWindow,\n    secondPrivateWindow,\n    thirdNormalWindow,\n  ];\n  await Promise.all(windows.map(win => BrowserTestUtils.closeWindow(win)));\n});\n\nadd_task(async function check_newSavedLogin_listener() {\n  const TEST_URL =\n    \"https://example.com/browser/browser/components/newtab/test/browser/red_page.html\";\n\n  let loginsSaved = 0;\n  const triggerHandler = () => loginsSaved++;\n  const newSavedLoginListener = ASRouterTriggerListeners.get(\"newSavedLogin\");\n\n  // Previously initialized by the Router\n  newSavedLoginListener.uninit();\n\n  // Initialise listener\n  await newSavedLoginListener.init(triggerHandler);\n\n  await BrowserTestUtils.withNewTab(\n    TEST_URL,\n    async function triggerNewSavedPassword(browser) {\n      Services.obs.notifyObservers(browser, \"LoginStats:NewSavedPassword\");\n      await BrowserTestUtils.waitForCondition(\n        () => loginsSaved !== 0,\n        \"Wait for the observer notification to run\"\n      );\n      is(loginsSaved, 1, \"should receive observer notification\");\n    }\n  );\n\n  // Uninitialise listener\n  newSavedLoginListener.uninit();\n\n  await BrowserTestUtils.withNewTab(\n    TEST_URL,\n    async function triggerNewSavedPasswordAfterUninit(browser) {\n      Services.obs.notifyObservers(browser, \"LoginStats:NewSavedPassword\");\n      await new Promise(resolve => executeSoon(resolve));\n      is(loginsSaved, 1, \"shouldn't receive obs. notification after uninit\");\n    }\n  );\n});\n\nadd_task(async function check_trackingProtection_listener() {\n  const TEST_URL =\n    \"https://example.com/browser/browser/components/newtab/test/browser/red_page.html\";\n\n  const event1 = 0x0001;\n  const event2 = 0x0010;\n  const event3 = 0x0100;\n  const event4 = 0x1000;\n\n  // Initialise listener to listen 2 events, for any incoming event e,\n  // it will be triggered if and only if:\n  // 1. (e & event1) && (e & event2)\n  // 2. (e & event3)\n  const bindEvents = [event1 | event2, event3];\n\n  let observerEvent = 0;\n  let pageLoadSum = 0;\n  const triggerHandler = (target, trigger) => {\n    const {\n      id,\n      param: { host, type },\n      context: { pageLoad },\n    } = trigger;\n    is(id, \"trackingProtection\", \"should match event name\");\n    is(host, TEST_URL, \"should match test URL\");\n    is(\n      bindEvents.filter(e => (type & e) === e).length,\n      1,\n      `event ${type} is valid`\n    );\n    ok(pageLoadSum <= pageLoad, \"pageLoad is non-decreasing\");\n\n    observerEvent += 1;\n    pageLoadSum = pageLoad;\n  };\n  const trackingProtectionListener = ASRouterTriggerListeners.get(\n    \"trackingProtection\"\n  );\n\n  // Previously initialized by the Router\n  trackingProtectionListener.uninit();\n\n  await trackingProtectionListener.init(triggerHandler, bindEvents);\n\n  await BrowserTestUtils.withNewTab(\n    TEST_URL,\n    async function triggerTrackingProtection(browser) {\n      Services.obs.notifyObservers(\n        {\n          wrappedJSObject: {\n            browser,\n            host: TEST_URL,\n            event: event1, // won't trigger\n          },\n        },\n        \"SiteProtection:ContentBlockingEvent\"\n      );\n    }\n  );\n\n  is(observerEvent, 0, \"shouldn't receive unrelated observer notification\");\n  is(pageLoadSum, 0, \"shouldn't receive unrelated observer notification\");\n\n  await BrowserTestUtils.withNewTab(\n    TEST_URL,\n    async function triggerTrackingProtection(browser) {\n      Services.obs.notifyObservers(\n        {\n          wrappedJSObject: {\n            browser,\n            host: TEST_URL,\n            event: event3, // will trigger\n          },\n        },\n        \"SiteProtection:ContentBlockingEvent\"\n      );\n\n      await BrowserTestUtils.waitForCondition(\n        () => observerEvent !== 0,\n        \"Wait for the observer notification to run\"\n      );\n      is(observerEvent, 1, \"should receive observer notification\");\n      is(pageLoadSum, 2, \"should receive observer notification\");\n\n      Services.obs.notifyObservers(\n        {\n          wrappedJSObject: {\n            browser,\n            host: TEST_URL,\n            event: event1 | event2 | event4, // still trigger\n          },\n        },\n        \"SiteProtection:ContentBlockingEvent\"\n      );\n\n      await BrowserTestUtils.waitForCondition(\n        () => observerEvent !== 1,\n        \"Wait for the observer notification to run\"\n      );\n      is(observerEvent, 2, \"should receive another observer notification\");\n      is(pageLoadSum, 2, \"should receive another observer notification\");\n\n      Services.obs.notifyObservers(\n        {\n          wrappedJSObject: {\n            browser,\n            host: TEST_URL,\n            event: event1, // no trigger\n          },\n        },\n        \"SiteProtection:ContentBlockingEvent\"\n      );\n\n      await new Promise(resolve => executeSoon(resolve));\n      is(observerEvent, 2, \"shouldn't receive unrelated notification\");\n      is(pageLoadSum, 2, \"shouldn't receive unrelated notification\");\n    }\n  );\n\n  // Uninitialise listener\n  trackingProtectionListener.uninit();\n\n  await BrowserTestUtils.withNewTab(\n    TEST_URL,\n    async function triggerTrackingProtectionAfterUninit(browser) {\n      Services.obs.notifyObservers(\n        {\n          wrappedJSObject: {\n            browser,\n            host: TEST_URL,\n            event: event3, // wont trigger after uninit\n          },\n        },\n        \"SiteProtection:ContentBlockingEvent\"\n      );\n      await new Promise(resolve => executeSoon(resolve));\n      is(observerEvent, 2, \"shouldn't receive obs. notification after uninit\");\n      is(pageLoadSum, 2, \"shouldn't receive obs. notification after uninit\");\n    }\n  );\n});\n\nadd_task(async function check_trackingProtectionMilestone_listener() {\n  const TEST_URL =\n    \"https://example.com/browser/browser/components/newtab/test/browser/red_page.html\";\n\n  let observerEvent = 0;\n  const triggerHandler = (target, trigger) => {\n    const {\n      id,\n      param: { host },\n    } = trigger;\n    is(id, \"trackingProtection\", \"should match event name\");\n    is(host, \"ContentBlockingMilestone\", \"Should be the correct event type\");\n    observerEvent += 1;\n  };\n  const trackingProtectionListener = ASRouterTriggerListeners.get(\n    \"trackingProtection\"\n  );\n\n  // Previously initialized by the Router\n  trackingProtectionListener.uninit();\n\n  // Initialise listener\n  trackingProtectionListener.init(triggerHandler, [\"ContentBlockingMilestone\"]);\n\n  await BrowserTestUtils.withNewTab(\n    TEST_URL,\n    async function triggerTrackingProtection(browser) {\n      Services.obs.notifyObservers(\n        {\n          wrappedJSObject: {\n            browser,\n            event: \"Other Event\",\n          },\n        },\n        \"SiteProtection:ContentBlockingMilestone\"\n      );\n    }\n  );\n\n  is(observerEvent, 0, \"shouldn't receive unrelated observer notification\");\n\n  await BrowserTestUtils.withNewTab(\n    TEST_URL,\n    async function triggerTrackingProtection(browser) {\n      Services.obs.notifyObservers(\n        {\n          wrappedJSObject: {\n            browser,\n            event: \"ContentBlockingMilestone\",\n          },\n        },\n        \"SiteProtection:ContentBlockingMilestone\"\n      );\n\n      await BrowserTestUtils.waitForCondition(\n        () => observerEvent !== 0,\n        \"Wait for the observer notification to run\"\n      );\n      is(observerEvent, 1, \"should receive observer notification\");\n    }\n  );\n\n  // Uninitialise listener\n  trackingProtectionListener.uninit();\n\n  await BrowserTestUtils.withNewTab(\n    TEST_URL,\n    async function triggerTrackingProtectionAfterUninit(browser) {\n      Services.obs.notifyObservers(\n        {\n          wrappedJSObject: {\n            browser,\n            event: \"ContentBlockingMilestone\",\n          },\n        },\n        \"SiteProtection:ContentBlockingMilestone\"\n      );\n      await new Promise(resolve => executeSoon(resolve));\n      is(observerEvent, 1, \"shouldn't receive obs. notification after uninit\");\n    }\n  );\n});\n"
  },
  {
    "path": "test/browser/browser_asrouter_whatsnewpanel.js",
    "content": "const { PanelTestProvider } = ChromeUtils.import(\n  \"resource://activity-stream/lib/PanelTestProvider.jsm\"\n);\nconst { ToolbarPanelHub } = ChromeUtils.import(\n  \"resource://activity-stream/lib/ToolbarPanelHub.jsm\"\n);\n\nadd_task(async function test_messages_rendering() {\n  const msgs = (await PanelTestProvider.getMessages()).filter(\n    ({ template }) => template === \"whatsnew_panel_message\"\n  );\n\n  Assert.ok(msgs.length, \"FxA test message exists\");\n\n  Object.defineProperty(ToolbarPanelHub, \"messages\", {\n    get: () => Promise.resolve(msgs),\n    configurable: true,\n  });\n\n  await ToolbarPanelHub.enableAppmenuButton();\n\n  const mainView = document.getElementById(\"appMenu-mainView\");\n  UITour.showMenu(window, \"appMenu\");\n  await BrowserTestUtils.waitForEvent(mainView, \"ViewShown\");\n\n  Assert.equal(mainView.hidden, false, \"Panel is visible\");\n\n  const whatsNewBtn = document.getElementById(\"appMenu-whatsnew-button\");\n  Assert.equal(whatsNewBtn.hidden, false, \"What's New is present\");\n\n  // Show the What's New Messages\n  whatsNewBtn.click();\n\n  const shownMessages = await BrowserTestUtils.waitForCondition(\n    () =>\n      document.getElementById(\"PanelUI-whatsNew-message-container\") &&\n      document.querySelectorAll(\n        \"#PanelUI-whatsNew-message-container .whatsNew-message\"\n      ).length\n  );\n  Assert.equal(\n    shownMessages,\n    msgs.length,\n    \"Expected number of What's New messages rendered.\"\n  );\n\n  UITour.hideMenu(window, \"appMenu\");\n});\n"
  },
  {
    "path": "test/browser/browser_discovery_render.js",
    "content": "\"use strict\";\n\nasync function before({ pushPrefs }) {\n  await pushPrefs([\n    \"browser.newtabpage.activity-stream.discoverystream.config\",\n    JSON.stringify({\n      collapsible: true,\n      enabled: true,\n      hardcoded_layout: true,\n    }),\n  ]);\n}\n\ntest_newtab({\n  before,\n  test: async function test_render_hardcoded() {\n    const topSites = await ContentTaskUtils.waitForCondition(() =>\n      content.document.querySelector(\".ds-top-sites\")\n    );\n    ok(topSites, \"Got the discovery stream top sites section\");\n\n    const learnMore = content.document.querySelector(\n      \".ds-layout a[href$=new_tab_learn_more]\"\n    );\n    is(\n      learnMore.textContent,\n      \"What’s Pocket?\",\n      \"Got the rendered Message with link text and url within discovery stream\"\n    );\n  },\n});\n"
  },
  {
    "path": "test/browser/browser_discovery_styles.js",
    "content": "\"use strict\";\n\nfunction fakePref(layout) {\n  return [\n    \"browser.newtabpage.activity-stream.discoverystream.config\",\n    JSON.stringify({\n      enabled: true,\n      layout_endpoint: `data:,${encodeURIComponent(JSON.stringify(layout))}`,\n    }),\n  ];\n}\n\ntest_newtab({\n  async before({ pushPrefs }) {\n    await pushPrefs(\n      fakePref({\n        layout: [\n          {\n            width: 12,\n            components: [\n              {\n                type: \"TopSites\",\n              },\n              {\n                type: \"HorizontalRule\",\n                styles: {\n                  hr: \"border-width: 3.14159mm\",\n                },\n              },\n            ],\n          },\n        ],\n      })\n    );\n  },\n  test: async function test_hr_override() {\n    const hr = await ContentTaskUtils.waitForCondition(() =>\n      content.document.querySelector(\"hr\")\n    );\n    ok(\n      content.getComputedStyle(hr).borderTopWidth.match(/11.?\\d*px/),\n      \"applied and normalized hr component width override\"\n    );\n  },\n});\n\ntest_newtab({\n  async before({ pushPrefs }) {\n    await pushPrefs(\n      fakePref({\n        layout: [\n          {\n            width: 12,\n            components: [\n              {\n                type: \"TopSites\",\n              },\n              {\n                type: \"HorizontalRule\",\n                styles: {\n                  \"*\": \"color: #f00\",\n                  \"\": \"font-size: 1.2345cm\",\n                  hr: \"font-weight: 12345\",\n                },\n              },\n            ],\n          },\n        ],\n      })\n    );\n  },\n  test: async function test_multiple_overrides() {\n    const hr = await ContentTaskUtils.waitForCondition(() =>\n      content.document.querySelector(\"hr\")\n    );\n    const styles = content.getComputedStyle(hr);\n    is(styles.color, \"rgb(255, 0, 0)\", \"applied and normalized color\");\n    is(styles.fontSize, \"46.65px\", \"applied and normalized font size\");\n    is(styles.fontWeight, \"400\", \"applied and normalized font weight\");\n  },\n});\n\ntest_newtab({\n  async before({ pushPrefs }) {\n    await pushPrefs(\n      fakePref({\n        layout: [\n          {\n            width: 12,\n            components: [\n              {\n                type: \"HorizontalRule\",\n                styles: {\n                  // NB: Use display: none to avoid network requests to unfiltered urls\n                  hr: `display: none;\n                   background-image: url(https://example.com/background);\n                   content: url(chrome://browser/content);\n                   cursor: url(  resource://activity-stream/cursor  ), auto;\n                   list-style-image: url('https://img-getpocket.cdn.mozilla.net/list');`,\n                },\n              },\n            ],\n          },\n        ],\n      })\n    );\n  },\n  test: async function test_url_filtering() {\n    const hr = await ContentTaskUtils.waitForCondition(() =>\n      content.document.querySelector(\"hr\")\n    );\n    const styles = content.getComputedStyle(hr);\n    is(\n      styles.backgroundImage,\n      \"none\",\n      \"filtered out invalid background image url\"\n    );\n    is(\n      styles.content,\n      `url(\"chrome://browser/content/browser.xul\")`,\n      \"applied, normalized and allowed content url\"\n    );\n    is(\n      styles.cursor,\n      `url(\"resource://activity-stream/cursor\"), auto`,\n      \"applied, normalized and allowed cursor url\"\n    );\n    is(\n      styles.listStyleImage,\n      `url(\"https://img-getpocket.cdn.mozilla.net/list\")`,\n      \"applied, normalized and allowed list style image url\"\n    );\n  },\n});\n\ntest_newtab({\n  async before({ pushPrefs }) {\n    await pushPrefs(\n      fakePref({\n        layout: [\n          {\n            width: 12,\n            components: [\n              {\n                type: \"HorizontalRule\",\n                styles: {\n                  \"@media (min-width: 0)\":\n                    \"content: url(chrome://browser/content)\",\n                  \"@media (min-width: 0) *\":\n                    \"content: url(chrome://browser/content)\",\n                  \"@media (min-width: 0) { * }\":\n                    \"content: url(chrome://browser/content)\",\n                },\n              },\n            ],\n          },\n        ],\n      })\n    );\n  },\n  test: async function test_atrule_filtering() {\n    const hr = await ContentTaskUtils.waitForCondition(() =>\n      content.document.querySelector(\"hr\")\n    );\n    is(\n      content.getComputedStyle(hr).content,\n      \"none\",\n      \"filtered out attempted @media query\"\n    );\n  },\n});\n"
  },
  {
    "path": "test/browser/browser_enabled_newtabpage.js",
    "content": "function checkSpec(uri, check, message) {\n  const { spec } = NetUtil.newChannel({\n    loadUsingSystemPrincipal: true,\n    uri,\n  }).URI;\n\n  info(`got ${spec} for ${uri}`);\n  check(spec, \"about:blank\", message);\n}\n\nadd_task(async function test_newtab_enabled() {\n  checkSpec(\n    \"about:newtab\",\n    isnot,\n    \"did not get blank for default about:newtab\"\n  );\n  checkSpec(\"about:home\", isnot, \"did not get blank for default about:home\");\n\n  await SpecialPowers.pushPrefEnv({\n    set: [[\"browser.newtabpage.enabled\", false]],\n  });\n\n  checkSpec(\"about:newtab\", is, \"got blank when newtab is not enabled\");\n  checkSpec(\"about:home\", isnot, \"still did not get blank for about:home\");\n});\n"
  },
  {
    "path": "test/browser/browser_getScreenshots.js",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\"use strict\";\n\n// a blue page\nconst TEST_URL =\n  \"https://example.com/browser/browser/components/newtab/test/browser/blue_page.html\";\nconst XHTMLNS = \"http://www.w3.org/1999/xhtml\";\n\nChromeUtils.defineModuleGetter(\n  this,\n  \"Screenshots\",\n  \"resource://activity-stream/lib/Screenshots.jsm\"\n);\n\nfunction get_pixels_for_blob(blob, width, height) {\n  return new Promise(resolve => {\n    // get the pixels out of the screenshot that we just took\n    let img = document.createElementNS(XHTMLNS, \"img\");\n    let imgPath = URL.createObjectURL(blob);\n    img.setAttribute(\"src\", imgPath);\n    img.addEventListener(\n      \"load\",\n      () => {\n        let canvas = document.createElementNS(XHTMLNS, \"canvas\");\n        canvas.setAttribute(\"width\", width);\n        canvas.setAttribute(\"height\", height);\n        let ctx = canvas.getContext(\"2d\");\n        ctx.drawImage(img, 0, 0, width, height);\n        const result = ctx.getImageData(0, 0, width, height).data;\n        URL.revokeObjectURL(imgPath);\n        resolve(result);\n      },\n      { once: true }\n    );\n  });\n}\n\nadd_task(async function test_screenshot() {\n  await SpecialPowers.pushPrefEnv({\n    set: [[\"browser.pagethumbnails.capturing_disabled\", false]],\n  });\n\n  // take a screenshot of a blue page and save it as a blob\n  const screenshotAsObject = await Screenshots.getScreenshotForURL(TEST_URL);\n  let pixels = await get_pixels_for_blob(screenshotAsObject.data, 10, 10);\n  let rgbaCount = { r: 0, g: 0, b: 0, a: 0 };\n  while (pixels.length) {\n    // break the pixels into arrays of 4 components [red, green, blue, alpha]\n    let [r, g, b, a, ...rest] = pixels;\n    pixels = rest;\n    // count the number of each coloured pixels\n    if (r === 255) {\n      rgbaCount.r += 1;\n    }\n    if (g === 255) {\n      rgbaCount.g += 1;\n    }\n    if (b === 255) {\n      rgbaCount.b += 1;\n    }\n    if (a === 255) {\n      rgbaCount.a += 1;\n    }\n  }\n\n  // in the end, we should only have 100 blue pixels (10 x 10) with full opacity\n  Assert.equal(rgbaCount.b, 100, \"Has 100 blue pixels\");\n  Assert.equal(rgbaCount.a, 100, \"Has full opacity\");\n  Assert.equal(rgbaCount.r, 0, \"Does not have any red pixels\");\n  Assert.equal(rgbaCount.g, 0, \"Does not have any green pixels\");\n});\n"
  },
  {
    "path": "test/browser/browser_highlights_section.js",
    "content": "\"use strict\";\n\n/**\n * Helper for setup and cleanup of Highlights section tests.\n * @param bookmarkCount Number of bookmark higlights to add\n * @param test The test case\n */\nfunction test_highlights(bookmarkCount, test) {\n  test_newtab({\n    async before({ tab }) {\n      if (bookmarkCount) {\n        await addHighlightsBookmarks(bookmarkCount);\n        // Wait for HighlightsFeed to update and display the items.\n        await ContentTask.spawn(tab.linkedBrowser, null, async () => {\n          await ContentTaskUtils.waitForCondition(\n            () =>\n              content.document.querySelector(\".card-outer:not(.placeholder)\"),\n            \"No highlights cards found.\"\n          );\n        });\n      }\n    },\n    test,\n    async after() {\n      await clearHistoryAndBookmarks();\n    },\n  });\n}\n\ntest_highlights(\n  2, // Number of highlights cards\n  function check_highlights_cards() {\n    let found = content.document.querySelectorAll(\n      \".card-outer:not(.placeholder)\"\n    ).length;\n    is(found, 2, \"there should be 2 highlights cards\");\n\n    found = content.document.querySelectorAll(\".section-list .placeholder\")\n      .length;\n    is(found, 2, \"there should be 1 row * 4 - 2 = 2 highlights placeholder\");\n\n    found = content.document.querySelectorAll(\n      \".card-context-icon.icon-bookmark-added\"\n    ).length;\n    is(found, 2, \"there should be 2 bookmark icons\");\n  }\n);\n\ntest_highlights(\n  1, // Number of highlights cards\n  function check_highlights_context_menu() {\n    const menuButton = content.document.querySelector(\n      \".card-outer .context-menu-button\"\n    );\n    // Open the menu.\n    menuButton.click();\n    const found = content.document.querySelector(\".card-outer .context-menu\");\n    ok(found && !found.hidden, \"Should find a visible context menu\");\n  }\n);\n\ntest_highlights(\n  1, // Number of highlights cards\n  async function check_highlights_context_menu() {\n    let found = content.document.querySelectorAll(\n      \".card-context-icon.icon-bookmark-added\"\n    ).length;\n    is(found, 1, \"there should be 1 bookmark icon\");\n\n    const menuButton = content.document.querySelector(\n      \".card-outer .context-menu-button\"\n    );\n    // Open the menu.\n    menuButton.click();\n    const contextMenu = content.document.querySelector(\n      \".card-outer .context-menu\"\n    );\n    ok(\n      contextMenu && !contextMenu.hidden,\n      \"Should find a visible context menu\"\n    );\n\n    const removeBookmarkBtn = contextMenu.querySelector(\n      \"button .icon-bookmark-added\"\n    );\n    removeBookmarkBtn.click();\n\n    await ContentTaskUtils.waitForCondition(\n      () =>\n        content.document.querySelectorAll(\n          \".card-context-icon.icon-bookmark-added\"\n        ).length === 0,\n      \"no more bookmark cards should be visible\"\n    );\n  }\n);\n"
  },
  {
    "path": "test/browser/browser_newtab_overrides.js",
    "content": "\"use strict\";\n\nXPCOMUtils.defineLazyServiceGetter(\n  this,\n  \"aboutNewTabService\",\n  \"@mozilla.org/browser/aboutnewtab-service;1\",\n  \"nsIAboutNewTabService\"\n);\n\nregisterCleanupFunction(() => {\n  aboutNewTabService.resetNewTabURL();\n});\n\nfunction nextChangeNotificationPromise(aNewURL, testMessage) {\n  return TestUtils.topicObserved(\"newtab-url-changed\", function observer(\n    aSubject,\n    aData\n  ) {\n    // jshint unused:false\n    Assert.equal(aData, aNewURL, testMessage);\n    return true;\n  });\n}\n\n/*\n * Tests that the default newtab page is always returned when one types \"about:newtab\" in the URL bar,\n * even when overridden.\n */\nadd_task(async function redirector_ignores_override() {\n  let overrides = [\"chrome://browser/content/aboutRobots.xhtml\", \"about:home\"];\n\n  for (let overrideURL of overrides) {\n    let notificationPromise = nextChangeNotificationPromise(\n      overrideURL,\n      `newtab page now points to ${overrideURL}`\n    );\n    aboutNewTabService.newTabURL = overrideURL;\n\n    await notificationPromise;\n    Assert.ok(aboutNewTabService.overridden, \"url has been overridden\");\n\n    let tabOptions = {\n      gBrowser,\n      url: \"about:newtab\",\n    };\n\n    /*\n     * Simulate typing \"about:newtab\" in the url bar.\n     *\n     * Bug 1240169 - We expect the redirector to lead the user to \"about:newtab\", the default URL,\n     * due to invoking AboutRedirector. A user interacting with the chrome otherwise would lead\n     * to the overriding URLs.\n     */\n    await BrowserTestUtils.withNewTab(tabOptions, async browser => {\n      await ContentTask.spawn(browser, {}, async () => {\n        Assert.equal(content.location.href, \"about:newtab\", \"Got right URL\");\n        Assert.equal(\n          content.document.location.href,\n          \"about:newtab\",\n          \"Got right URL\"\n        );\n        Assert.notEqual(\n          content.document.nodePrincipal,\n          Services.scriptSecurityManager.getSystemPrincipal(),\n          \"activity stream principal should not match systemPrincipal\"\n        );\n      });\n    }); // jshint ignore:line\n  }\n});\n\n/*\n * Tests loading an overridden newtab page by simulating opening a newtab page from chrome\n */\nadd_task(async function override_loads_in_browser() {\n  let overrides = [\n    \"chrome://browser/content/aboutRobots.xhtml\",\n    \"about:home\",\n    \" about:home\",\n  ];\n\n  for (let overrideURL of overrides) {\n    let notificationPromise = nextChangeNotificationPromise(\n      overrideURL.trim(),\n      `newtab page now points to ${overrideURL}`\n    );\n    aboutNewTabService.newTabURL = overrideURL;\n\n    await notificationPromise;\n    Assert.ok(aboutNewTabService.overridden, \"url has been overridden\");\n\n    // simulate a newtab open as a user would\n    BrowserOpenTab(); // jshint ignore:line\n\n    let browser = gBrowser.selectedBrowser;\n    await BrowserTestUtils.browserLoaded(browser);\n\n    await ContentTask.spawn(browser, { url: overrideURL }, async args => {\n      Assert.equal(content.location.href, args.url.trim(), \"Got right URL\");\n      Assert.equal(\n        content.document.location.href,\n        args.url.trim(),\n        \"Got right URL\"\n      );\n    }); // jshint ignore:line\n    BrowserTestUtils.removeTab(gBrowser.selectedTab);\n  }\n});\n\n/*\n * Tests edge cases when someone overrides the newtabpage with whitespace\n */\nadd_task(async function override_blank_loads_in_browser() {\n  let overrides = [\"\", \" \", \"\\n\\t\", \" about:blank\"];\n\n  for (let overrideURL of overrides) {\n    let notificationPromise = nextChangeNotificationPromise(\n      \"about:blank\",\n      \"newtab page now points to about:blank\"\n    );\n    aboutNewTabService.newTabURL = overrideURL;\n\n    await notificationPromise;\n    Assert.ok(aboutNewTabService.overridden, \"url has been overridden\");\n\n    // simulate a newtab open as a user would\n    BrowserOpenTab(); // jshint ignore:line\n\n    let browser = gBrowser.selectedBrowser;\n    await BrowserTestUtils.browserLoaded(browser);\n\n    await ContentTask.spawn(browser, {}, async () => {\n      Assert.equal(content.location.href, \"about:blank\", \"Got right URL\");\n      Assert.equal(\n        content.document.location.href,\n        \"about:blank\",\n        \"Got right URL\"\n      );\n    }); // jshint ignore:line\n    BrowserTestUtils.removeTab(gBrowser.selectedTab);\n  }\n});\n"
  },
  {
    "path": "test/browser/browser_onboarding_rtamo.js",
    "content": "const { ASRouter } = ChromeUtils.import(\n  \"resource://activity-stream/lib/ASRouter.jsm\"\n);\nconst { OnboardingMessageProvider } = ChromeUtils.import(\n  \"resource://activity-stream/lib/OnboardingMessageProvider.jsm\"\n);\nconst { AttributionCode } = ChromeUtils.import(\n  \"resource:///modules/AttributionCode.jsm\"\n);\n\nconst BRANCH_PREF = \"trailhead.firstrun.branches\";\n\nasync function setRTAMOOnboarding() {\n  await ASRouter.forceAttribution({\n    campaign: \"non-fx-button\",\n    source: \"addons.mozilla.org\",\n    content: \"iridium@particlecore.github.io\",\n  });\n  AttributionCode._clearCache();\n  const data = await AttributionCode.getAttrDataAsync();\n  Assert.equal(\n    data.source,\n    \"addons.mozilla.org\",\n    \"Attribution data should be set\"\n  );\n\n  Services.prefs.setCharPref(BRANCH_PREF, \"join-supercharge\");\n\n  // Reset trailhead so it loads the new branch.\n  Services.prefs.clearUserPref(\"trailhead.firstrun.didSeeAboutWelcome\");\n  await ASRouter.setState({ trailheadInitialized: false });\n  ASRouter._updateMessageProviders();\n  await ASRouter.loadMessagesFromAllProviders();\n\n  registerCleanupFunction(async () => {\n    // Separate cleanup methods between mac and windows\n    if (AppConstants.platform === \"macosx\") {\n      const { path } = Services.dirsvc.get(\"GreD\", Ci.nsIFile).parent.parent;\n      const attributionSvc = Cc[\"@mozilla.org/mac-attribution;1\"].getService(\n        Ci.nsIMacAttributionService\n      );\n      attributionSvc.setReferrerUrl(path, \"\", true);\n    }\n    // Clear cache call is only possible in a testing environment\n    let env = Cc[\"@mozilla.org/process/environment;1\"].getService(\n      Ci.nsIEnvironment\n    );\n    env.set(\"XPCSHELL_TEST_PROFILE_DIR\", \"testing\");\n    Services.prefs.clearUserPref(BRANCH_PREF);\n    await AttributionCode.deleteFileAsync();\n    AttributionCode._clearCache();\n  });\n}\n\nadd_task(async function setup() {\n  // Store it in order to restore to the original value\n  const { getAddonInfo } = OnboardingMessageProvider;\n  // Prevent fetching the real addon url and making a network request\n  OnboardingMessageProvider.getAddonInfo = () => ({\n    name: \"mochitest_name\",\n    iconURL: \"mochitest_iconURL\",\n    url: \"https://example.com\",\n  });\n\n  registerCleanupFunction(() => {\n    OnboardingMessageProvider.getAddonInfo = getAddonInfo;\n  });\n});\n\nadd_task(async () => {\n  await setRTAMOOnboarding();\n\n  let tab = await BrowserTestUtils.openNewForegroundTab(\n    gBrowser,\n    \"about:welcome\",\n    false\n  );\n  let browser = tab.linkedBrowser;\n\n  await ContentTask.spawn(browser, {}, async () => {\n    // Wait for Activity Stream to load\n    await ContentTaskUtils.waitForCondition(\n      () => content.document.querySelector(\".activity-stream\"),\n      `Should render Activity Stream`\n    );\n    await ContentTaskUtils.waitForCondition(\n      () => content.document.body.classList.contains(\"welcome\"),\n      \"The modal setup should be completed\"\n    );\n    await ContentTaskUtils.waitForCondition(\n      () => content.document.body.classList.contains(\"hide-main\"),\n      \"You shouldn't be able to see newtabpage content\"\n    );\n    for (let selector of [\n      // ReturnToAMO elements\n      \".ReturnToAMOOverlay\",\n      \".ReturnToAMOContainer\",\n      \".ReturnToAMOAddonContents\",\n      \".ReturnToAMOIcon\",\n    ]) {\n      ok(content.document.querySelector(selector), `Should render ${selector}`);\n    }\n    // Make sure strings are properly shown\n    Assert.equal(\n      content.document.querySelector(\".ReturnToAMOText\").innerText,\n      \"Now let’s get you mochitest_name.\"\n    );\n  });\n\n  BrowserTestUtils.removeTab(tab);\n});\n"
  },
  {
    "path": "test/browser/browser_topsites_contextMenu_options.js",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\n\"use strict\";\n\ntest_newtab({\n  before: setDefaultTopSites,\n  // Test verifies the menu options for a default top site.\n  test: async function defaultTopSites_menuOptions() {\n    const siteSelector =\n      \".top-site-outer:not(.search-shortcut):not(.placeholder)\";\n    await ContentTaskUtils.waitForCondition(\n      () => content.document.querySelector(siteSelector),\n      \"Topsite tippytop icon not found\"\n    );\n\n    const contextMenuItems = await content.openContextMenuAndGetOptions(\n      siteSelector\n    );\n    const contextMenuItemsText = contextMenuItems.map(v => v.textContent);\n\n    Assert.equal(\n      contextMenuItemsText.length,\n      5,\n      \"Number of options is correct\"\n    );\n\n    const expectedItemsText = [\n      \"Pin\",\n      \"Edit\",\n      \"Open in a New Window\",\n      \"Open in a New Private Window\",\n      \"Dismiss\",\n    ];\n\n    for (let i = 0; i < contextMenuItemsText.length; i++) {\n      Assert.equal(\n        contextMenuItemsText[i],\n        expectedItemsText[i],\n        \"Name option is correct\"\n      );\n    }\n  },\n});\n\ntest_newtab({\n  before: setDefaultTopSites,\n  // Test verifies that the next top site in queue replaces a dismissed top site.\n  test: async function defaultTopSites_dismiss() {\n    const siteSelector =\n      \".top-site-outer:not(.search-shortcut):not(.placeholder)\";\n    await ContentTaskUtils.waitForCondition(\n      () => content.document.querySelector(siteSelector),\n      \"Topsite tippytop icon not found\"\n    );\n\n    // Don't count search topsites\n    const defaultTopSitesNumber = content.document.querySelectorAll(\n      siteSelector\n    ).length;\n    Assert.equal(defaultTopSitesNumber, 5, \"5 top sites are loaded by default\");\n\n    // Skip the search topsites select the second default topsite\n    const secondTopSite = content.document\n      .querySelectorAll(siteSelector)[1]\n      .getAttribute(\"href\");\n\n    const contextMenuItems = await content.openContextMenuAndGetOptions(\n      siteSelector\n    );\n    Assert.equal(\n      contextMenuItems[4].textContent,\n      \"Dismiss\",\n      \"'Dismiss' is the 5th item in the context menu list\"\n    );\n\n    contextMenuItems[4].querySelector(\"button\").click();\n\n    // Wait for the topsite to be dismissed and the second one to replace it\n    await ContentTaskUtils.waitForCondition(\n      () =>\n        content.document.querySelector(siteSelector).getAttribute(\"href\") ===\n        secondTopSite,\n      \"First default topsite was dismissed\"\n    );\n\n    await ContentTaskUtils.waitForCondition(\n      () => content.document.querySelectorAll(siteSelector).length === 4,\n      \"4 top sites are displayed after one of them is dismissed\"\n    );\n  },\n  async after() {\n    await new Promise(resolve => NewTabUtils.undoAll(resolve));\n  },\n});\n\ntest_newtab({\n  before: setDefaultTopSites,\n  test: async function searchTopSites_dismiss() {\n    const siteSelector = \".search-shortcut\";\n    await ContentTaskUtils.waitForCondition(\n      () => content.document.querySelectorAll(siteSelector).length === 1,\n      \"1 search topsites is loaded by default\"\n    );\n\n    const contextMenuItems = await content.openContextMenuAndGetOptions(\n      siteSelector\n    );\n    is(\n      contextMenuItems.length,\n      2,\n      \"Search TopSites should only have Unpin and Dismiss\"\n    );\n\n    // Unpin\n    contextMenuItems[0].querySelector(\"button\").click();\n\n    await ContentTaskUtils.waitForCondition(\n      () => content.document.querySelectorAll(siteSelector).length === 1,\n      \"1 search topsite displayed after we unpin the other one\"\n    );\n  },\n  after: () => {\n    // Required for multiple test runs in the same browser, pref is used to\n    // prevent pinning the same search topsite twice\n    Services.prefs.clearUserPref(\n      \"browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts.havePinned\"\n    );\n  },\n});\n"
  },
  {
    "path": "test/browser/browser_topsites_section.js",
    "content": "\"use strict\";\n\n// Check TopSites edit modal and overlay show up.\ntest_newtab(\n  // it should be able to click the topsites add button to reveal the add top site modal and overlay.\n  function topsites_edit() {\n    // Open the section context menu.\n    content.document\n      .querySelector(\".top-sites .section-top-bar .context-menu-button\")\n      .click();\n    let contextMenu = content.document.querySelector(\n      \".top-sites .section-top-bar .context-menu\"\n    );\n    ok(contextMenu, \"Should find a visible topsite context menu\");\n\n    const topsitesAddBtn = content.document.querySelector(\n      \".top-sites .context-menu-item button\"\n    );\n    topsitesAddBtn.click();\n\n    let found = content.document.querySelector(\".topsite-form\");\n    ok(found && !found.hidden, \"Should find a visible topsite form\");\n\n    found = content.document.querySelector(\".modalOverlayOuter\");\n    ok(found && !found.hidden, \"Should find a visible overlay\");\n  }\n);\n\n// Test pin/unpin context menu options.\ntest_newtab({\n  before: setDefaultTopSites,\n  // it should pin the website when we click the first option of the topsite context menu.\n  test: async function topsites_pin_unpin() {\n    const siteSelector =\n      \".top-site-outer:not(.search-shortcut):not(.placeholder)\";\n    await ContentTaskUtils.waitForCondition(\n      () => content.document.querySelector(siteSelector),\n      \"Topsite tippytop icon not found\"\n    );\n    // There are only topsites on the page, the selector with find the first topsite menu button.\n    let topsiteEl = content.document.querySelector(siteSelector);\n    let topsiteContextBtn = topsiteEl.querySelector(\".context-menu-button\");\n    topsiteContextBtn.click();\n\n    await ContentTaskUtils.waitForCondition(\n      () => topsiteEl.querySelector(\".top-sites-list .context-menu\"),\n      \"No context menu found\"\n    );\n\n    let contextMenu = topsiteEl.querySelector(\".top-sites-list .context-menu\");\n    ok(contextMenu, \"Should find a topsite context menu\");\n\n    const pinUnpinTopsiteBtn = contextMenu.querySelector(\n      \".top-sites .context-menu-item button\"\n    );\n    // Pin the topsite.\n    pinUnpinTopsiteBtn.click();\n\n    // Need to wait for pin action.\n    await ContentTaskUtils.waitForCondition(\n      () => topsiteEl.querySelector(\".icon-pin-small\"),\n      \"No pinned icon found\"\n    );\n\n    let pinnedIcon = topsiteEl.querySelectorAll(\".icon-pin-small\").length;\n    is(pinnedIcon, 1, \"should find 1 pinned topsite\");\n\n    // Unpin the topsite.\n    topsiteContextBtn = topsiteEl.querySelector(\".context-menu-button\");\n    ok(topsiteContextBtn, \"Should find a context menu button\");\n    topsiteContextBtn.click();\n    topsiteEl.querySelector(\".context-menu-item button\").click();\n\n    // Need to wait for unpin action.\n    await ContentTaskUtils.waitForCondition(\n      () => !topsiteEl.querySelector(\".icon-pin-small\"),\n      \"Topsite should be unpinned\"\n    );\n  },\n});\n\n// Check Topsites add\ntest_newtab({\n  before: setDefaultTopSites,\n  // it should be able to click the topsites edit button to reveal the edit topsites modal and overlay.\n  test: async function topsites_add() {\n    let nativeInputValueSetter = Object.getOwnPropertyDescriptor(\n      content.window.HTMLInputElement.prototype,\n      \"value\"\n    ).set;\n    let event = new content.Event(\"input\", { bubbles: true });\n\n    // Find the add topsites button\n    content.document\n      .querySelector(\".top-sites .section-top-bar .context-menu-button\")\n      .click();\n    let contextMenu = content.document.querySelector(\n      \".top-sites .section-top-bar .context-menu\"\n    );\n    ok(contextMenu, \"Should find a visible topsite context menu\");\n\n    const topsitesAddBtn = content.document.querySelector(\n      \".top-sites .context-menu-item button\"\n    );\n    topsitesAddBtn.click();\n\n    let found = content.document.querySelector(\".modalOverlayOuter\");\n    ok(found && !found.hidden, \"Should find a visible overlay\");\n\n    // Write field title\n    let fieldTitle = content.document.querySelector(\".field input\");\n    ok(fieldTitle && !fieldTitle.hidden, \"Should find field title input\");\n\n    nativeInputValueSetter.call(fieldTitle, \"Bugzilla\");\n    fieldTitle.dispatchEvent(event);\n    is(fieldTitle.value, \"Bugzilla\", \"The field title should match\");\n\n    // Write field url\n    let fieldURL = content.document.querySelector(\".field.url input\");\n    ok(fieldURL && !fieldURL.hidden, \"Should find field url input\");\n\n    nativeInputValueSetter.call(fieldURL, \"https://bugzilla.mozilla.org\");\n    fieldURL.dispatchEvent(event);\n    is(\n      fieldURL.value,\n      \"https://bugzilla.mozilla.org\",\n      \"The field url should match\"\n    );\n\n    // Click the \"Add\" button\n    let addBtn = content.document.querySelector(\".done\");\n    addBtn.click();\n\n    // Wait for Topsite to be populated\n    await ContentTaskUtils.waitForCondition(\n      () =>\n        content.document\n          .querySelector(\".top-site-outer:first-child a\")\n          .getAttribute(\"href\") === \"https://bugzilla.mozilla.org\",\n      \"No Topsite found\"\n    );\n\n    // Remove topsite after test is complete\n    let topsiteContextBtn = content.document.querySelector(\n      \".top-sites-list .context-menu-button\"\n    );\n    topsiteContextBtn.click();\n    await ContentTaskUtils.waitForCondition(\n      () => content.document.querySelector(\".top-sites-list .context-menu\"),\n      \"No context menu found\"\n    );\n\n    let contextMen = content.document.querySelector(\n      \".top-sites-list .context-menu\"\n    );\n\n    const dismissBtn = contextMen.querySelector(\n      \".top-sites .context-menu-item button .icon-dismiss\"\n    );\n    dismissBtn.click();\n\n    // Wait for Topsite to be removed\n    await ContentTaskUtils.waitForCondition(\n      () =>\n        content.document\n          .querySelector(\".top-site-outer:first-child a\")\n          .getAttribute(\"href\") !== \"https://bugzilla.mozilla.org\",\n      \"Topsite not removed\"\n    );\n  },\n});\n\ntest_newtab({\n  before: setDefaultTopSites,\n  test: async function test_search_topsite_keyword() {\n    await ContentTaskUtils.waitForCondition(\n      () => content.document.querySelector(\".search-shortcut .title.pinned\"),\n      \"Wait for pinned search topsites\"\n    );\n\n    const searchTopSites = content.document.querySelectorAll(\".title.pinned\");\n    ok(\n      searchTopSites.length >= 1,\n      \"There should be at least 2 search topsites\"\n    );\n\n    searchTopSites[0].click();\n\n    return searchTopSites[0].innerText;\n  },\n  after(searchTopSiteTag) {\n    ok(\n      gURLBar.focused,\n      \"We clicked a search topsite the focus should be in location bar\"\n    );\n    ok(\n      gURLBar.value.includes(searchTopSiteTag),\n      \"Should contain the tag of the search topsite clicked\"\n    );\n  },\n});\n"
  },
  {
    "path": "test/browser/head.js",
    "content": "\"use strict\";\n\nChromeUtils.defineModuleGetter(\n  this,\n  \"PlacesTestUtils\",\n  \"resource://testing-common/PlacesTestUtils.jsm\"\n);\nChromeUtils.defineModuleGetter(\n  this,\n  \"QueryCache\",\n  \"resource://activity-stream/lib/ASRouterTargeting.jsm\"\n);\n\nfunction popPrefs() {\n  return SpecialPowers.popPrefEnv();\n}\nfunction pushPrefs(...prefs) {\n  return SpecialPowers.pushPrefEnv({ set: prefs });\n}\n\n// eslint-disable-next-line no-unused-vars\nasync function setDefaultTopSites() {\n  // The pref for TopSites is empty by default.\n  await pushPrefs([\n    \"browser.newtabpage.activity-stream.default.sites\",\n    \"https://www.youtube.com/,https://www.facebook.com/,https://www.amazon.com/,https://www.reddit.com/,https://www.wikipedia.org/,https://twitter.com/\",\n  ]);\n  // Toggle the feed off and on as a workaround to read the new prefs.\n  await pushPrefs([\"browser.newtabpage.activity-stream.feeds.topsites\", false]);\n  await pushPrefs([\"browser.newtabpage.activity-stream.feeds.topsites\", true]);\n  await pushPrefs([\n    \"browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts\",\n    true,\n  ]);\n}\n\n// eslint-disable-next-line no-unused-vars\nasync function clearHistoryAndBookmarks() {\n  await PlacesUtils.bookmarks.eraseEverything();\n  await PlacesUtils.history.clear();\n  QueryCache.expireAll();\n}\n\n/**\n * Helper to wait for potentially preloaded browsers to \"load\" where a preloaded\n * page has already loaded and won't trigger \"load\", and a \"load\"ed page might\n * not necessarily have had all its javascript/render logic executed.\n */\nasync function waitForPreloaded(browser) {\n  let readyState = await ContentTask.spawn(\n    browser,\n    {},\n    () => content.document.readyState\n  );\n  if (readyState !== \"complete\") {\n    await BrowserTestUtils.browserLoaded(browser);\n  }\n}\n\n/**\n * Helper to force the HighlightsFeed to update.\n */\nfunction refreshHighlightsFeed() {\n  // Toggling the pref will clear the feed cache and force a places query.\n  Services.prefs.setBoolPref(\n    \"browser.newtabpage.activity-stream.feeds.section.highlights\",\n    false\n  );\n  Services.prefs.setBoolPref(\n    \"browser.newtabpage.activity-stream.feeds.section.highlights\",\n    true\n  );\n}\n\n/**\n * Helper to populate the Highlights section with bookmark cards.\n * @param count Number of items to add.\n */\n// eslint-disable-next-line no-unused-vars\nasync function addHighlightsBookmarks(count) {\n  const bookmarks = new Array(count).fill(null).map((entry, i) => ({\n    parentGuid: PlacesUtils.bookmarks.unfiledGuid,\n    title: \"foo\",\n    url: `https://mozilla${i}.com/nowNew`,\n  }));\n\n  for (let placeInfo of bookmarks) {\n    await PlacesUtils.bookmarks.insert(placeInfo);\n    // Bookmarks need at least one visit to show up as highlights.\n    await PlacesTestUtils.addVisits(placeInfo.url);\n  }\n\n  // Force HighlightsFeed to make a request for the new items.\n  refreshHighlightsFeed();\n}\n\n/**\n * Helper to add various helpers to the content process by injecting variables\n * and functions to the `content` global.\n */\nfunction addContentHelpers() {\n  const { document } = content;\n  Object.assign(content, {\n    /**\n     * Click the context menu button for an item and get its options list.\n     *\n     * @param selector {String} Selector to get an item (e.g., top site, card)\n     * @return {Array} The nodes for the options.\n     */\n    async openContextMenuAndGetOptions(selector) {\n      const item = document.querySelector(selector);\n      const contextButton = item.querySelector(\".context-menu-button\");\n      contextButton.click();\n      // Gives fluent-dom the time to render strings\n      await new Promise(r => content.requestAnimationFrame(r));\n\n      const contextMenu = item.querySelector(\".context-menu\");\n      const contextMenuList = contextMenu.querySelector(\".context-menu-list\");\n      return [...contextMenuList.getElementsByClassName(\"context-menu-item\")];\n    },\n  });\n}\n\n/**\n * Helper to run Activity Stream about:newtab test tasks in content.\n *\n * @param testInfo {Function|Object}\n *   {Function} This parameter will be used as if the function were called with\n *              an Object with this parameter as \"test\" key's value.\n *   {Object} The following keys are expected:\n *     before {Function} Optional. Runs before and returns an arg for \"test\"\n *     test   {Function} The test to run in the about:newtab content task taking\n *                       an arg from \"before\" and returns a result to \"after\"\n *     after  {Function} Optional. Runs after and with the result of \"test\"\n */\n// eslint-disable-next-line no-unused-vars\nfunction test_newtab(testInfo) {\n  // Extract any test parts or default to just the single content task\n  let { before, test: contentTask, after } = testInfo;\n  if (!before) {\n    before = () => ({});\n  }\n  if (!contentTask) {\n    contentTask = testInfo;\n  }\n  if (!after) {\n    after = () => {};\n  }\n\n  // Helper to push prefs for just this test and pop them when done\n  let needPopPrefs = false;\n  let scopedPushPrefs = async (...args) => {\n    needPopPrefs = true;\n    await pushPrefs(...args);\n  };\n  let scopedPopPrefs = async () => {\n    if (needPopPrefs) {\n      await popPrefs();\n    }\n  };\n\n  // Make the test task with optional before/after and content task to run in a\n  // new tab that opens and closes.\n  let testTask = async () => {\n    // Open about:newtab without using the default load listener\n    let tab = await BrowserTestUtils.openNewForegroundTab(\n      gBrowser,\n      \"about:newtab\",\n      false\n    );\n\n    // Specially wait for potentially preloaded browsers\n    let browser = tab.linkedBrowser;\n    await waitForPreloaded(browser);\n\n    // Add shared helpers to the content process\n    ContentTask.spawn(browser, {}, addContentHelpers);\n\n    // Wait for React to render something\n    await BrowserTestUtils.waitForCondition(\n      () =>\n        ContentTask.spawn(\n          browser,\n          {},\n          () => content.document.getElementById(\"root\").children.length\n        ),\n      \"Should render activity stream content\"\n    );\n\n    // Chain together before -> contentTask -> after data passing\n    try {\n      let contentArg = await before({ pushPrefs: scopedPushPrefs, tab });\n      let contentResult = await ContentTask.spawn(\n        browser,\n        contentArg,\n        contentTask\n      );\n      await after(contentResult);\n    } finally {\n      // Clean up for next tests\n      await scopedPopPrefs();\n      BrowserTestUtils.removeTab(tab);\n    }\n  };\n\n  // Copy the name of the content task to identify the test\n  Object.defineProperty(testTask, \"name\", { value: contentTask.name });\n  add_task(testTask);\n}\n"
  },
  {
    "path": "test/browser/red_page.html",
    "content": "<html>\n  <head>\n    <meta charset=\"utf-8\">\n  </head>\n  <body style=\"background-color: red\" />\n</html>\n"
  },
  {
    "path": "test/schemas/pings.js",
    "content": "import { CONTENT_MESSAGE_TYPE, MAIN_MESSAGE_TYPE } from \"common/Actions.jsm\";\nimport Joi from \"joi-browser\";\n\nexport const baseKeys = {\n  // client_id will be set by PingCentre if it doesn't exist.\n  client_id: Joi.string().optional(),\n  addon_version: Joi.string().required(),\n  locale: Joi.string().required(),\n  session_id: Joi.string(),\n  page: Joi.valid([\n    \"about:home\",\n    \"about:newtab\",\n    \"about:welcome\",\n    \"both\",\n    \"unknown\",\n  ]),\n  user_prefs: Joi.number()\n    .integer()\n    .required(),\n};\n\nexport const BasePing = Joi.object()\n  .keys(baseKeys)\n  .options({ allowUnknown: true });\n\nexport const eventsTelemetryExtraKeys = Joi.object()\n  .keys({\n    session_id: baseKeys.session_id.required(),\n    page: baseKeys.page.required(),\n    addon_version: baseKeys.addon_version.required(),\n    user_prefs: baseKeys.user_prefs.required(),\n    action_position: Joi.string().optional(),\n  })\n  .options({ allowUnknown: false });\n\nexport const UserEventPing = Joi.object().keys(\n  Object.assign({}, baseKeys, {\n    session_id: baseKeys.session_id.required(),\n    page: baseKeys.page.required(),\n    source: Joi.string(),\n    event: Joi.string().required(),\n    action: Joi.valid(\"activity_stream_user_event\").required(),\n    metadata_source: Joi.string(),\n    highlight_type: Joi.valid([\"bookmarks\", \"recommendation\", \"history\"]),\n    recommender_type: Joi.string(),\n    value: Joi.object().keys({\n      newtab_url_category: Joi.string(),\n      newtab_extension_id: Joi.string(),\n      home_url_category: Joi.string(),\n      home_extension_id: Joi.string(),\n    }),\n  })\n);\n\nexport const UTUserEventPing = Joi.array().items(\n  Joi.string()\n    .required()\n    .valid(\"activity_stream\"),\n  Joi.string()\n    .required()\n    .valid(\"event\"),\n  Joi.string()\n    .required()\n    .valid([\n      \"CLICK\",\n      \"SEARCH\",\n      \"BLOCK\",\n      \"DELETE\",\n      \"DELETE_CONFIRM\",\n      \"DIALOG_CANCEL\",\n      \"DIALOG_OPEN\",\n      \"OPEN_NEW_WINDOW\",\n      \"OPEN_PRIVATE_WINDOW\",\n      \"OPEN_NEWTAB_PREFS\",\n      \"CLOSE_NEWTAB_PREFS\",\n      \"BOOKMARK_DELETE\",\n      \"BOOKMARK_ADD\",\n      \"PIN\",\n      \"UNPIN\",\n      \"SAVE_TO_POCKET\",\n    ]),\n  Joi.string().required(),\n  eventsTelemetryExtraKeys\n);\n\n// Use this to validate actions generated from Redux\nexport const UserEventAction = Joi.object().keys({\n  type: Joi.string().required(),\n  data: Joi.object()\n    .keys({\n      event: Joi.valid([\n        \"CLICK\",\n        \"SEARCH\",\n        \"SEARCH_HANDOFF\",\n        \"BLOCK\",\n        \"DELETE\",\n        \"DELETE_CONFIRM\",\n        \"DIALOG_CANCEL\",\n        \"DIALOG_OPEN\",\n        \"OPEN_NEW_WINDOW\",\n        \"OPEN_PRIVATE_WINDOW\",\n        \"OPEN_NEWTAB_PREFS\",\n        \"CLOSE_NEWTAB_PREFS\",\n        \"BOOKMARK_DELETE\",\n        \"BOOKMARK_ADD\",\n        \"PIN\",\n        \"PREVIEW_REQUEST\",\n        \"UNPIN\",\n        \"SAVE_TO_POCKET\",\n        \"MENU_MOVE_UP\",\n        \"MENU_MOVE_DOWN\",\n        \"SCREENSHOT_REQUEST\",\n        \"MENU_REMOVE\",\n        \"MENU_COLLAPSE\",\n        \"MENU_EXPAND\",\n        \"MENU_MANAGE\",\n        \"MENU_ADD_TOPSITE\",\n        \"MENU_PRIVACY_NOTICE\",\n        \"DELETE_FROM_POCKET\",\n        \"ARCHIVE_FROM_POCKET\",\n        \"SKIPPED_SIGNIN\",\n        \"SUBMIT_EMAIL\",\n        \"SUBMIT_SIGNIN\",\n        \"SHOW_PRIVACY_INFO\",\n        \"CLICK_PRIVACY_INFO\",\n      ]).required(),\n      source: Joi.valid([\"TOP_SITES\", \"TOP_STORIES\", \"HIGHLIGHTS\"]),\n      action_position: Joi.number().integer(),\n      value: Joi.object().keys({\n        icon_type: Joi.valid([\n          \"tippytop\",\n          \"rich_icon\",\n          \"screenshot_with_icon\",\n          \"screenshot\",\n          \"no_image\",\n          \"custom_screenshot\",\n        ]),\n        card_type: Joi.valid([\n          \"bookmark\",\n          \"trending\",\n          \"pinned\",\n          \"pocket\",\n          \"search\",\n          \"spoc\",\n          \"organic\",\n        ]),\n        search_vendor: Joi.valid([\"google\", \"amazon\"]),\n        has_flow_params: Joi.bool(),\n      }),\n    })\n    .required(),\n  meta: Joi.object()\n    .keys({\n      to: Joi.valid(MAIN_MESSAGE_TYPE).required(),\n      from: Joi.valid(CONTENT_MESSAGE_TYPE).required(),\n    })\n    .required(),\n});\n\nexport const UndesiredPing = Joi.object().keys(\n  Object.assign({}, baseKeys, {\n    source: Joi.string().required(),\n    event: Joi.string().required(),\n    action: Joi.valid(\"activity_stream_undesired_event\").required(),\n    value: Joi.number().required(),\n  })\n);\n\nexport const TileSchema = Joi.object().keys({\n  id: Joi.number()\n    .integer()\n    .required(),\n  pos: Joi.number().integer(),\n});\n\nexport const ImpressionStatsPing = Joi.object().keys(\n  Object.assign({}, baseKeys, {\n    source: Joi.string().required(),\n    impression_id: Joi.string().required(),\n    client_id: Joi.valid(\"n/a\").required(),\n    session_id: Joi.valid(\"n/a\").required(),\n    action: Joi.valid(\"activity_stream_impression_stats\").required(),\n    tiles: Joi.array()\n      .items(TileSchema)\n      .required(),\n    click: Joi.number().integer(),\n    block: Joi.number().integer(),\n    pocket: Joi.number().integer(),\n  })\n);\n\nexport const SpocsFillEntrySchema = Joi.object().keys({\n  id: Joi.number()\n    .integer()\n    .required(),\n  displayed: Joi.number()\n    .integer()\n    .required(),\n  reason: Joi.string().required(),\n  full_recalc: Joi.number()\n    .integer()\n    .required(),\n});\n\nexport const SpocsFillPing = Joi.object().keys(\n  Object.assign({}, baseKeys, {\n    impression_id: Joi.string().required(),\n    session_id: Joi.valid(\"n/a\").required(),\n    spoc_fills: Joi.array()\n      .items(SpocsFillEntrySchema)\n      .required(),\n  })\n);\n\nexport const PerfPing = Joi.object().keys(\n  Object.assign({}, baseKeys, {\n    source: Joi.string(),\n    event: Joi.string().required(),\n    action: Joi.valid(\"activity_stream_performance_event\").required(),\n    value: Joi.number().required(),\n  })\n);\n\nexport const SessionPing = Joi.object().keys(\n  Object.assign({}, baseKeys, {\n    session_id: baseKeys.session_id.required(),\n    page: baseKeys.page.required(),\n    session_duration: Joi.number().integer(),\n    action: Joi.valid(\"activity_stream_session\").required(),\n    perf: Joi.object()\n      .keys({\n        // How long it took in ms for data to be ready for display.\n        highlights_data_late_by_ms: Joi.number().positive(),\n\n        // Timestamp of the action perceived by the user to trigger the load\n        // of this page.\n        //\n        // Not required at least for the error cases where the\n        // observer event doesn't fire\n        load_trigger_ts: Joi.number()\n          .positive()\n          .notes([\"server counter\", \"server counter alert\"]),\n\n        // What was the perceived trigger of the load action?\n        //\n        // Not required at least for the error cases where the observer event\n        // doesn't fire\n        load_trigger_type: Joi.valid([\n          \"first_window_opened\",\n          \"menu_plus_or_keyboard\",\n          \"unexpected\",\n        ])\n          .notes([\"server counter\", \"server counter alert\"])\n          .required(),\n\n        // How long it took in ms for data to be ready for display.\n        topsites_data_late_by_ms: Joi.number().positive(),\n\n        // When did the topsites element finish painting?  Note that, at least for\n        // the first tab to be loaded, and maybe some others, this will be before\n        // topsites has yet to receive screenshots updates from the add-on code,\n        // and is therefore just showing placeholder screenshots.\n        topsites_first_painted_ts: Joi.number()\n          .positive()\n          .notes([\"server counter\", \"server counter alert\"]),\n\n        // Information about the quality of TopSites images and icons.\n        topsites_icon_stats: Joi.object().keys({\n          custom_screenshot: Joi.number(),\n          rich_icon: Joi.number(),\n          screenshot: Joi.number(),\n          screenshot_with_icon: Joi.number(),\n          tippytop: Joi.number(),\n          no_image: Joi.number(),\n        }),\n\n        // The count of pinned Top Sites.\n        topsites_pinned: Joi.number(),\n\n        // The count of search shortcut Top Sites.\n        topsites_search_shortcuts: Joi.number(),\n\n        // When the page itself receives an event that document.visibilityState\n        // == visible.\n        //\n        // Not required at least for the (error?) case where the\n        // visibility_event doesn't fire.  (It's not clear whether this\n        // can happen in practice, but if it does, we'd like to know about it).\n        visibility_event_rcvd_ts: Joi.number()\n          .positive()\n          .notes([\"server counter\", \"server counter alert\"]),\n\n        // The boolean to signify whether the page is preloaded or not.\n        is_preloaded: Joi.bool().required(),\n      })\n      .required(),\n  })\n);\n\nexport const ASRouterEventPing = Joi.object()\n  .keys({\n    addon_version: Joi.string().required(),\n    locale: Joi.string().required(),\n    message_id: Joi.string().required(),\n    event: Joi.string().required(),\n    client_id: Joi.string(),\n    impression_id: Joi.string(),\n  })\n  .or(\"client_id\", \"impression_id\");\n\nexport const UTSessionPing = Joi.array().items(\n  Joi.string()\n    .required()\n    .valid(\"activity_stream\"),\n  Joi.string()\n    .required()\n    .valid(\"end\"),\n  Joi.string()\n    .required()\n    .valid(\"session\"),\n  Joi.string().required(),\n  eventsTelemetryExtraKeys\n);\n\nexport const trailheadEnrollExtraKeys = Joi.object()\n  .keys({\n    experimentType: Joi.string().required(),\n    branch: Joi.string().required(),\n  })\n  .options({ allowUnknown: false });\n\nexport const UTTrailheadEnrollPing = Joi.array().items(\n  Joi.string()\n    .required()\n    .valid(\"activity_stream\"),\n  Joi.string()\n    .required()\n    .valid(\"enroll\"),\n  Joi.string()\n    .required()\n    .valid(\"preference_study\"),\n  Joi.string().required(),\n  trailheadEnrollExtraKeys\n);\n\nexport function chaiAssertions(_chai, utils) {\n  const { Assertion } = _chai;\n\n  Assertion.addMethod(\"validate\", function(schema, schemaName) {\n    const { error } = Joi.validate(this._obj, schema, { allowUnknown: false });\n    this.assert(\n      !error,\n      `Expected to be ${\n        schemaName ? `a valid ${schemaName}` : \"valid\"\n      } but there were errors: ${error}`\n    );\n  });\n\n  const assertions = {\n    /**\n     * assert.validate - Validates an item given a Joi schema\n     *\n     * @param  {any} actual The item to validate\n     * @param  {obj} schema A Joi schema\n     */\n    validate(actual, schema, schemaName) {\n      new Assertion(actual).validate(schema, schemaName);\n    },\n\n    /**\n     * isUserEventAction - Passes if the item is a valid UserEvent action\n     *\n     * @param  {any} actual The item to validate\n     */\n    isUserEventAction(actual) {\n      new Assertion(actual).validate(UserEventAction, \"UserEventAction\");\n    },\n  };\n\n  Object.assign(_chai.assert, assertions);\n}\n"
  },
  {
    "path": "test/unit/asrouter/ASRouter.test.js",
    "content": "import { _ASRouter, MessageLoaderUtils } from \"lib/ASRouter.jsm\";\nimport { ASRouterTargeting, QueryCache } from \"lib/ASRouterTargeting.jsm\";\nimport {\n  CHILD_TO_PARENT_MESSAGE_NAME,\n  FAKE_LOCAL_MESSAGES,\n  FAKE_LOCAL_PROVIDER,\n  FAKE_LOCAL_PROVIDERS,\n  FAKE_RECOMMENDATION,\n  FAKE_REMOTE_MESSAGES,\n  FAKE_REMOTE_PROVIDER,\n  FAKE_REMOTE_SETTINGS_PROVIDER,\n  FakeRemotePageManager,\n  PARENT_TO_CHILD_MESSAGE_NAME,\n} from \"./constants\";\nimport { actionCreators as ac } from \"common/Actions.jsm\";\nimport {\n  ASRouterPreferences,\n  TARGETING_PREFERENCES,\n} from \"lib/ASRouterPreferences.jsm\";\nimport { ASRouterTriggerListeners } from \"lib/ASRouterTriggerListeners.jsm\";\nimport { CFRPageActions } from \"lib/CFRPageActions.jsm\";\nimport { GlobalOverrider } from \"test/unit/utils\";\nimport { PanelTestProvider } from \"lib/PanelTestProvider.jsm\";\nimport ProviderResponseSchema from \"content-src/asrouter/schemas/provider-response.schema.json\";\nimport { SnippetsTestMessageProvider } from \"lib/SnippetsTestMessageProvider.jsm\";\n\nconst OUTGOING_MESSAGE_NAME = \"ASRouter:parent-to-child\";\nconst MESSAGE_PROVIDER_PREF_NAME =\n  \"browser.newtabpage.activity-stream.asrouter.providers.snippets\";\nconst FAKE_PROVIDERS = [\n  FAKE_LOCAL_PROVIDER,\n  FAKE_REMOTE_PROVIDER,\n  FAKE_REMOTE_SETTINGS_PROVIDER,\n];\nconst FAKE_BUNDLE = [FAKE_LOCAL_MESSAGES[1], FAKE_LOCAL_MESSAGES[2]];\nconst ONE_DAY_IN_MS = 24 * 60 * 60 * 1000;\nconst FAKE_RESPONSE_HEADERS = { get() {} };\n\nconst USE_REMOTE_L10N_PREF =\n  \"browser.newtabpage.activity-stream.asrouter.useRemoteL10n\";\n\n// Creates a message object that looks like messages returned by\n// RemotePageManager listeners\nfunction fakeAsyncMessage(action) {\n  return { data: action, target: new FakeRemotePageManager() };\n}\n// Create a message that looks like a user action\nfunction fakeExecuteUserAction(action) {\n  return fakeAsyncMessage({ data: action, type: \"USER_ACTION\" });\n}\n\ndescribe(\"ASRouter\", () => {\n  let Router;\n  let globals;\n  let channel;\n  let sandbox;\n  let messageBlockList;\n  let providerBlockList;\n  let messageImpressions;\n  let providerImpressions;\n  let previousSessionEnd;\n  let fetchStub;\n  let clock;\n  let getStringPrefStub;\n  let dispatchStub;\n  let fakeAttributionCode;\n  let FakeBookmarkPanelHub;\n  let FakeToolbarBadgeHub;\n  let FakeToolbarPanelHub;\n  let personalizedCfrScores;\n\n  function createFakeStorage() {\n    const getStub = sandbox.stub();\n    getStub.returns(Promise.resolve());\n    getStub\n      .withArgs(\"messageBlockList\")\n      .returns(Promise.resolve(messageBlockList));\n    getStub\n      .withArgs(\"providerBlockList\")\n      .returns(Promise.resolve(providerBlockList));\n    getStub\n      .withArgs(\"messageImpressions\")\n      .returns(Promise.resolve(messageImpressions));\n    getStub\n      .withArgs(\"providerImpressions\")\n      .returns(Promise.resolve(providerImpressions));\n    getStub\n      .withArgs(\"previousSessionEnd\")\n      .returns(Promise.resolve(previousSessionEnd));\n    return {\n      get: getStub,\n      set: sandbox.stub().returns(Promise.resolve()),\n    };\n  }\n\n  function setMessageProviderPref(value) {\n    sandbox.stub(ASRouterPreferences, \"providers\").get(() => value);\n  }\n\n  async function createRouterAndInit(providers = FAKE_PROVIDERS) {\n    setMessageProviderPref(providers);\n    channel = new FakeRemotePageManager();\n    dispatchStub = sandbox.stub();\n    // `.freeze` to catch any attempts at modifying the object\n    Router = new _ASRouter(Object.freeze(FAKE_LOCAL_PROVIDERS));\n    await Router.init(channel, createFakeStorage(), dispatchStub);\n  }\n\n  beforeEach(async () => {\n    globals = new GlobalOverrider();\n    messageBlockList = [];\n    providerBlockList = [];\n    messageImpressions = {};\n    providerImpressions = {};\n    previousSessionEnd = 100;\n    sandbox = sinon.createSandbox();\n    personalizedCfrScores = {};\n\n    sandbox.spy(ASRouterPreferences, \"init\");\n    sandbox.spy(ASRouterPreferences, \"uninit\");\n    sandbox.spy(ASRouterPreferences, \"addListener\");\n    sandbox.spy(ASRouterPreferences, \"removeListener\");\n    sandbox.stub(ASRouterPreferences, \"trailhead\").get(() => {\n      return { trailheadTriplet: \"test\" };\n    });\n    sandbox.replaceGetter(\n      ASRouterPreferences,\n      \"personalizedCfrScores\",\n      () => personalizedCfrScores\n    );\n\n    clock = sandbox.useFakeTimers();\n    fetchStub = sandbox\n      .stub(global, \"fetch\")\n      .withArgs(\"http://fake.com/endpoint\")\n      .resolves({\n        ok: true,\n        status: 200,\n        json: () => Promise.resolve({ messages: FAKE_REMOTE_MESSAGES }),\n        headers: FAKE_RESPONSE_HEADERS,\n      });\n    getStringPrefStub = sandbox.stub(global.Services.prefs, \"getStringPref\");\n\n    fakeAttributionCode = {\n      _clearCache: () => sinon.stub(),\n      getAttrDataAsync: () => Promise.resolve({ content: \"addonID\" }),\n    };\n    FakeBookmarkPanelHub = {\n      init: sandbox.stub(),\n      uninit: sandbox.stub(),\n      _forceShowMessage: sandbox.stub(),\n    };\n    FakeToolbarPanelHub = {\n      init: sandbox.stub(),\n      uninit: sandbox.stub(),\n      forceShowMessage: sandbox.stub(),\n    };\n    FakeToolbarBadgeHub = {\n      init: sandbox.stub(),\n      uninit: sandbox.stub(),\n      registerBadgeNotificationListener: sandbox.stub(),\n    };\n    globals.set({\n      ASRouterPreferences,\n      TARGETING_PREFERENCES,\n      ASRouterTargeting,\n      ASRouterTriggerListeners,\n      QueryCache,\n      AttributionCode: fakeAttributionCode,\n      // Testing framework doesn't know how to `defineLazyModuleGetter` so we're\n      // importing these modules into the global scope ourselves.\n      SnippetsTestMessageProvider,\n      PanelTestProvider,\n      BookmarkPanelHub: FakeBookmarkPanelHub,\n      ToolbarBadgeHub: FakeToolbarBadgeHub,\n      ToolbarPanelHub: FakeToolbarPanelHub,\n      KintoHttpClient: class {\n        bucket() {\n          return this;\n        }\n        collection() {\n          return this;\n        }\n        getRecord() {\n          return Promise.resolve({ data: {} });\n        }\n      },\n      Downloader: class {\n        download() {\n          return Promise.resolve(\"/path/to/downlowned\");\n        }\n      },\n    });\n    await createRouterAndInit();\n  });\n  afterEach(() => {\n    ASRouterPreferences.uninit();\n    sandbox.restore();\n    globals.restore();\n  });\n\n  describe(\".state\", () => {\n    it(\"should throw if an attempt to set .state was made\", () => {\n      assert.throws(() => {\n        Router.state = {};\n      });\n    });\n  });\n\n  describe(\"#init\", () => {\n    it(\"should add a message listener to the RemotePageManager for incoming messages\", () => {\n      assert.calledWith(\n        channel.addMessageListener,\n        CHILD_TO_PARENT_MESSAGE_NAME\n      );\n      const [, listenerAdded] = channel.addMessageListener.firstCall.args;\n      assert.isFunction(listenerAdded);\n    });\n    it(\"should set state.messageBlockList to the block list in persistent storage\", async () => {\n      messageBlockList = [\"foo\"];\n      Router = new _ASRouter();\n      await Router.init(channel, createFakeStorage(), dispatchStub);\n\n      assert.deepEqual(Router.state.messageBlockList, [\"foo\"]);\n    });\n    it(\"should initialize all the hub providers\", async () => {\n      // ASRouter init called in `beforeEach` block above\n\n      assert.calledOnce(FakeToolbarBadgeHub.init);\n      assert.calledOnce(FakeToolbarPanelHub.init);\n      assert.calledOnce(FakeBookmarkPanelHub.init);\n\n      assert.calledWithExactly(\n        FakeToolbarBadgeHub.init,\n        Router.waitForInitialized,\n        {\n          handleMessageRequest: Router.handleMessageRequest,\n          addImpression: Router.addImpression,\n          blockMessageById: Router.blockMessageById,\n          dispatch: Router.dispatch,\n          unblockMessageById: Router.unblockMessageById,\n        }\n      );\n\n      assert.calledWithExactly(\n        FakeToolbarPanelHub.init,\n        Router.waitForInitialized,\n        {\n          getMessages: Router.handleMessageRequest,\n          dispatch: Router.dispatch,\n          handleUserAction: Router.handleUserAction,\n        }\n      );\n\n      assert.calledWithExactly(\n        FakeBookmarkPanelHub.init,\n        Router.handleMessageRequest,\n        Router.addImpression,\n        Router.dispatch\n      );\n    });\n    it(\"should set state.messageImpressions to the messageImpressions object in persistent storage\", async () => {\n      // Note that messageImpressions are only kept if a message exists in router and has a .frequency property,\n      // otherwise they will be cleaned up by .cleanupImpressions()\n      const testMessage = { id: \"foo\", frequency: { lifetimeCap: 10 } };\n      messageImpressions = { foo: [0, 1, 2] };\n      setMessageProviderPref([\n        { id: \"onboarding\", type: \"local\", messages: [testMessage] },\n      ]);\n      Router = new _ASRouter();\n      await Router.init(channel, createFakeStorage(), dispatchStub);\n\n      assert.deepEqual(Router.state.messageImpressions, messageImpressions);\n    });\n    it(\"should await .loadMessagesFromAllProviders() and add messages from providers to state.messages\", async () => {\n      Router = new _ASRouter(Object.freeze(FAKE_LOCAL_PROVIDERS));\n\n      const loadMessagesSpy = sandbox.spy(\n        Router,\n        \"loadMessagesFromAllProviders\"\n      );\n      await Router.init(channel, createFakeStorage(), dispatchStub);\n\n      assert.calledOnce(loadMessagesSpy);\n      assert.isArray(Router.state.messages);\n      assert.lengthOf(\n        Router.state.messages,\n        FAKE_LOCAL_MESSAGES.length + FAKE_REMOTE_MESSAGES.length\n      );\n    });\n    it(\"should load additional whitelisted hosts\", async () => {\n      getStringPrefStub.returns('[\"whitelist.com\"]');\n      await createRouterAndInit();\n\n      assert.propertyVal(Router.WHITELIST_HOSTS, \"whitelist.com\", \"preview\");\n      // Should still include the defaults\n      assert.lengthOf(Object.keys(Router.WHITELIST_HOSTS), 3);\n    });\n    it(\"should fallback to defaults if pref parsing fails\", async () => {\n      getStringPrefStub.returns(\"err\");\n      await createRouterAndInit();\n\n      assert.lengthOf(Object.keys(Router.WHITELIST_HOSTS), 2);\n      assert.propertyVal(\n        Router.WHITELIST_HOSTS,\n        \"snippets-admin.mozilla.org\",\n        \"preview\"\n      );\n      assert.propertyVal(\n        Router.WHITELIST_HOSTS,\n        \"activity-stream-icons.services.mozilla.com\",\n        \"production\"\n      );\n    });\n    it(\"should set this.dispatchToAS to the third parameter passed to .init()\", async () => {\n      assert.equal(Router.dispatchToAS, dispatchStub);\n    });\n    it(\"should set state.previousSessionEnd from IndexedDB\", async () => {\n      previousSessionEnd = 200;\n      await createRouterAndInit();\n\n      assert.equal(Router.state.previousSessionEnd, previousSessionEnd);\n    });\n    it(\"should dispatch a AS_ROUTER_INITIALIZED event to AS with ASRouterPreferences.specialConditions\", async () => {\n      assert.calledWith(\n        Router.dispatchToAS,\n        ac.BroadcastToContent({\n          type: \"AS_ROUTER_INITIALIZED\",\n          data: ASRouterPreferences.specialConditions,\n        })\n      );\n    });\n    it(\"should add observer for `intl:app-locales-changed`\", async () => {\n      sandbox.spy(global.Services.obs, \"addObserver\");\n      await createRouterAndInit();\n\n      assert.calledOnce(global.Services.obs.addObserver);\n      assert.equal(\n        global.Services.obs.addObserver.args[0][1],\n        \"intl:app-locales-changed\"\n      );\n    });\n    it(\"should add a pref observer\", async () => {\n      sandbox.spy(global.Services.prefs, \"addObserver\");\n      await createRouterAndInit();\n\n      assert.calledOnce(global.Services.prefs.addObserver);\n      assert.calledWithExactly(\n        global.Services.prefs.addObserver,\n        USE_REMOTE_L10N_PREF,\n        Router\n      );\n    });\n    describe(\"lazily loading local test providers\", () => {\n      afterEach(() => {\n        Router.uninit();\n      });\n      it(\"should add the local test providers on init if devtools are enabled\", async () => {\n        sandbox.stub(ASRouterPreferences, \"devtoolsEnabled\").get(() => true);\n\n        await createRouterAndInit();\n\n        assert.property(Router._localProviders, \"SnippetsTestMessageProvider\");\n        assert.property(Router._localProviders, \"PanelTestProvider\");\n      });\n      it(\"should not add the local test providers on init if devtools are disabled\", async () => {\n        sandbox.stub(ASRouterPreferences, \"devtoolsEnabled\").get(() => false);\n\n        await createRouterAndInit();\n\n        assert.notProperty(\n          Router._localProviders,\n          \"SnippetsTestMessageProvider\"\n        );\n        assert.notProperty(Router._localProviders, \"PanelTestProvider\");\n      });\n    });\n  });\n\n  describe(\"preference changes\", () => {\n    it(\"should call ASRouterPreferences.init and add a listener on init\", () => {\n      assert.calledOnce(ASRouterPreferences.init);\n      assert.calledWith(ASRouterPreferences.addListener, Router.onPrefChange);\n    });\n    it(\"should call ASRouterPreferences.uninit and remove the listener on uninit\", () => {\n      Router.uninit();\n      assert.calledOnce(ASRouterPreferences.uninit);\n      assert.calledWith(\n        ASRouterPreferences.removeListener,\n        Router.onPrefChange\n      );\n    });\n    it(\"should send a AS_ROUTER_TARGETING_UPDATE message\", async () => {\n      const messageTargeted = {\n        id: \"1\",\n        campaign: \"foocampaign\",\n        targeting: \"true\",\n      };\n      const messageNotTargeted = { id: \"2\", campaign: \"foocampaign\" };\n      await Router.setState({\n        messages: [messageTargeted, messageNotTargeted],\n      });\n      sandbox.stub(ASRouterTargeting, \"isMatch\").resolves(false);\n\n      await Router.onPrefChange(\"services.sync.username\");\n\n      assert.calledOnce(channel.sendAsyncMessage);\n      const [, { type, data }] = channel.sendAsyncMessage.firstCall.args;\n      assert.equal(type, \"AS_ROUTER_TARGETING_UPDATE\");\n      assert.equal(data[0], messageTargeted.id);\n      assert.lengthOf(data, 1);\n    });\n    it(\"should call loadMessagesFromAllProviders on pref change\", () => {\n      sandbox.spy(Router, \"loadMessagesFromAllProviders\");\n\n      ASRouterPreferences.observe(null, null, MESSAGE_PROVIDER_PREF_NAME);\n\n      assert.calledOnce(Router.loadMessagesFromAllProviders);\n    });\n    it(\"should update the list of providers on pref change\", () => {\n      const modifiedRemoteProvider = Object.assign({}, FAKE_REMOTE_PROVIDER, {\n        url: \"baz.com\",\n      });\n      setMessageProviderPref([\n        FAKE_LOCAL_PROVIDER,\n        modifiedRemoteProvider,\n        FAKE_REMOTE_SETTINGS_PROVIDER,\n      ]);\n\n      const { length } = Router.state.providers;\n\n      ASRouterPreferences.observe(null, null, MESSAGE_PROVIDER_PREF_NAME);\n\n      const provider = Router.state.providers.find(p => p.url === \"baz.com\");\n      assert.lengthOf(Router.state.providers, length);\n      assert.isDefined(provider);\n    });\n  });\n\n  describe(\"setState\", () => {\n    it(\"should broadcast a message to update the admin tool on a state change if the asrouter.devtoolsEnabled pref is\", async () => {\n      sandbox.stub(ASRouterPreferences, \"devtoolsEnabled\").get(() => true);\n      sandbox.stub(Router, \"getTargetingParameters\").resolves({});\n      await Router.setState({ foo: 123 });\n\n      assert.calledOnce(channel.sendAsyncMessage);\n      assert.deepEqual(channel.sendAsyncMessage.firstCall.args[1], {\n        type: \"ADMIN_SET_STATE\",\n        data: Object.assign({}, Router.state, {\n          providerPrefs: ASRouterPreferences.providers,\n          userPrefs: ASRouterPreferences.getAllUserPreferences(),\n          targetingParameters: {},\n          trailhead: ASRouterPreferences.trailhead,\n          errors: Router.errors,\n        }),\n      });\n    });\n    it(\"should not send a message on a state change asrouter.devtoolsEnabled pref is on\", async () => {\n      sandbox.stub(ASRouterPreferences, \"devtoolsEnabled\").get(() => false);\n      await Router.setState({ foo: 123 });\n\n      assert.notCalled(channel.sendAsyncMessage);\n    });\n  });\n\n  describe(\"getTargetingParameters\", () => {\n    it(\"should return the targeting parameters\", async () => {\n      const stub = sandbox.stub().resolves(\"foo\");\n      const obj = { foo: 1 };\n      sandbox.stub(obj, \"foo\").get(stub);\n      const result = await Router.getTargetingParameters(obj, obj);\n\n      assert.calledTwice(stub);\n      assert.propertyVal(result, \"foo\", \"foo\");\n    });\n  });\n\n  describe(\"evaluateExpression\", () => {\n    let stub;\n    beforeEach(async () => {\n      stub = sandbox.stub();\n      stub.resolves(\"foo\");\n      sandbox.stub(ASRouterTargeting, \"isMatch\").callsFake(stub);\n    });\n    afterEach(() => {\n      sandbox.restore();\n    });\n    it(\"should call ASRouterTargeting to evaluate\", async () => {\n      const targetStub = { sendAsyncMessage: sandbox.stub() };\n\n      await Router.evaluateExpression(targetStub, {});\n\n      assert.calledOnce(targetStub.sendAsyncMessage);\n      assert.equal(\n        targetStub.sendAsyncMessage.firstCall.args[1].data.evaluationStatus\n          .result,\n        \"foo\"\n      );\n      assert.isTrue(\n        targetStub.sendAsyncMessage.firstCall.args[1].data.evaluationStatus\n          .success\n      );\n    });\n    it(\"should catch evaluation errors\", async () => {\n      stub.returns(Promise.reject(new Error(\"fake error\")));\n      const targetStub = { sendAsyncMessage: sandbox.stub() };\n\n      await Router.evaluateExpression(targetStub, {});\n\n      assert.isFalse(\n        targetStub.sendAsyncMessage.firstCall.args[1].data.evaluationStatus\n          .success\n      );\n    });\n  });\n\n  describe(\"#routeMessageToTarget\", () => {\n    let target;\n    beforeEach(() => {\n      sandbox.stub(CFRPageActions, \"forceRecommendation\");\n      sandbox.stub(CFRPageActions, \"addRecommendation\");\n      sandbox.stub(CFRPageActions, \"showMilestone\");\n      target = { sendAsyncMessage: sandbox.stub() };\n    });\n    it(\"should route whatsnew_panel_message message to the right hub\", () => {\n      Router.routeMessageToTarget(\n        { template: \"whatsnew_panel_message\" },\n        target,\n        \"\",\n        true\n      );\n\n      assert.calledOnce(FakeToolbarPanelHub.forceShowMessage);\n      assert.notCalled(FakeToolbarBadgeHub.registerBadgeNotificationListener);\n      assert.notCalled(FakeBookmarkPanelHub._forceShowMessage);\n      assert.notCalled(CFRPageActions.addRecommendation);\n      assert.notCalled(CFRPageActions.forceRecommendation);\n      assert.notCalled(CFRPageActions.showMilestone);\n      assert.notCalled(target.sendAsyncMessage);\n    });\n    it(\"should route toolbar_badge message to the right hub\", () => {\n      Router.routeMessageToTarget({ template: \"toolbar_badge\" }, target);\n\n      assert.calledOnce(FakeToolbarBadgeHub.registerBadgeNotificationListener);\n      assert.notCalled(FakeToolbarPanelHub.forceShowMessage);\n      assert.notCalled(FakeBookmarkPanelHub._forceShowMessage);\n      assert.notCalled(CFRPageActions.addRecommendation);\n      assert.notCalled(CFRPageActions.forceRecommendation);\n      assert.notCalled(CFRPageActions.showMilestone);\n      assert.notCalled(target.sendAsyncMessage);\n    });\n    it(\"should route milestone_message to the right hub\", () => {\n      Router.routeMessageToTarget({ template: \"milestone_message\" }, target);\n\n      assert.calledOnce(CFRPageActions.showMilestone);\n      assert.notCalled(CFRPageActions.addRecommendation);\n      assert.notCalled(CFRPageActions.forceRecommendation);\n      assert.notCalled(FakeBookmarkPanelHub._forceShowMessage);\n      assert.notCalled(FakeToolbarBadgeHub.registerBadgeNotificationListener);\n      assert.notCalled(FakeToolbarPanelHub.forceShowMessage);\n      assert.notCalled(target.sendAsyncMessage);\n    });\n    it(\"should route fxa_bookmark_panel message to the right hub force = true\", () => {\n      Router.routeMessageToTarget(\n        { template: \"fxa_bookmark_panel\" },\n        target,\n        {},\n        true\n      );\n\n      assert.calledOnce(FakeBookmarkPanelHub._forceShowMessage);\n      assert.notCalled(FakeToolbarPanelHub.forceShowMessage);\n      assert.notCalled(FakeToolbarBadgeHub.registerBadgeNotificationListener);\n      assert.notCalled(CFRPageActions.addRecommendation);\n      assert.notCalled(CFRPageActions.forceRecommendation);\n      assert.notCalled(CFRPageActions.showMilestone);\n      assert.notCalled(target.sendAsyncMessage);\n    });\n    it(\"should route cfr_doorhanger message to the right hub force = false\", () => {\n      Router.routeMessageToTarget(\n        { template: \"cfr_doorhanger\" },\n        target,\n        { param: {} },\n        false\n      );\n\n      assert.calledOnce(CFRPageActions.addRecommendation);\n      assert.notCalled(FakeToolbarPanelHub.forceShowMessage);\n      assert.notCalled(FakeBookmarkPanelHub._forceShowMessage);\n      assert.notCalled(FakeToolbarBadgeHub.registerBadgeNotificationListener);\n      assert.notCalled(CFRPageActions.forceRecommendation);\n      assert.notCalled(CFRPageActions.showMilestone);\n      assert.notCalled(target.sendAsyncMessage);\n    });\n    it(\"should route cfr_doorhanger message to the right hub force = true\", () => {\n      Router.routeMessageToTarget(\n        { template: \"cfr_doorhanger\" },\n        target,\n        {},\n        true\n      );\n\n      assert.calledOnce(CFRPageActions.forceRecommendation);\n      assert.notCalled(FakeToolbarPanelHub.forceShowMessage);\n      assert.notCalled(CFRPageActions.addRecommendation);\n      assert.notCalled(CFRPageActions.showMilestone);\n      assert.notCalled(FakeBookmarkPanelHub._forceShowMessage);\n      assert.notCalled(FakeToolbarBadgeHub.registerBadgeNotificationListener);\n      assert.notCalled(target.sendAsyncMessage);\n    });\n    it(\"should route default to sending to content\", () => {\n      Router.routeMessageToTarget({ template: \"snippets\" }, target, {}, true);\n\n      assert.calledOnce(target.sendAsyncMessage);\n      assert.notCalled(FakeToolbarPanelHub.forceShowMessage);\n      assert.notCalled(CFRPageActions.forceRecommendation);\n      assert.notCalled(CFRPageActions.addRecommendation);\n      assert.notCalled(CFRPageActions.showMilestone);\n      assert.notCalled(FakeBookmarkPanelHub._forceShowMessage);\n      assert.notCalled(FakeToolbarBadgeHub.registerBadgeNotificationListener);\n    });\n  });\n\n  describe(\"#loadMessagesFromAllProviders\", () => {\n    function assertRouterContainsMessages(messages) {\n      const messageIdsInRouter = Router.state.messages.map(m => m.id);\n      for (const message of messages) {\n        assert.include(messageIdsInRouter, message.id);\n      }\n    }\n\n    it(\"should not trigger an update if not enough time has passed for a provider\", async () => {\n      await createRouterAndInit([\n        {\n          id: \"remotey\",\n          type: \"remote\",\n          enabled: true,\n          url: \"http://fake.com/endpoint\",\n          updateCycleInMs: 300,\n        },\n      ]);\n\n      const previousState = Router.state;\n\n      // Since we've previously gotten messages during init and we haven't advanced our fake timer,\n      // no updates should be triggered.\n      await Router.loadMessagesFromAllProviders();\n      assert.equal(Router.state, previousState);\n    });\n    it(\"should not trigger an update if we only have local providers\", async () => {\n      await createRouterAndInit([\n        {\n          id: \"foo\",\n          type: \"local\",\n          enabled: true,\n          messages: FAKE_LOCAL_MESSAGES,\n        },\n      ]);\n\n      const previousState = Router.state;\n\n      clock.tick(300);\n\n      await Router.loadMessagesFromAllProviders();\n      assert.equal(Router.state, previousState);\n    });\n    it(\"should apply personalization if defined\", async () => {\n      personalizedCfrScores = { FOO: 1, BAR: 2 };\n      const NEW_MESSAGES = [{ id: \"FOO\" }, { id: \"BAR\" }];\n\n      fetchStub.withArgs(\"http://foo.com\").resolves({\n        ok: true,\n        status: 200,\n        json: () => Promise.resolve({ messages: NEW_MESSAGES }),\n        headers: FAKE_RESPONSE_HEADERS,\n      });\n\n      await createRouterAndInit([\n        {\n          id: \"cfr\",\n          personalized: true,\n          personalizedModelVersion: \"42\",\n          type: \"remote\",\n          url: \"http://foo.com\",\n          enabled: true,\n          updateCycleInMs: 300,\n        },\n      ]);\n\n      await Router.loadMessagesFromAllProviders();\n\n      // Make sure messages are there\n      assertRouterContainsMessages(NEW_MESSAGES);\n\n      // Make sure they have a score and personalizedModelVersion\n      for (const expectedMessage of NEW_MESSAGES) {\n        const { id } = expectedMessage;\n        const message = Router.state.messages.find(msg => msg.id === id);\n        assert.propertyVal(message, \"score\", personalizedCfrScores[message.id]);\n        assert.propertyVal(message, \"personalizedModelVersion\", \"42\");\n      }\n    });\n    it(\"should update messages for a provider if enough time has passed, without removing messages for other providers\", async () => {\n      const NEW_MESSAGES = [{ id: \"new_123\" }];\n      await createRouterAndInit([\n        {\n          id: \"remotey\",\n          type: \"remote\",\n          url: \"http://fake.com/endpoint\",\n          enabled: true,\n          updateCycleInMs: 300,\n        },\n        {\n          id: \"alocalprovider\",\n          type: \"local\",\n          enabled: true,\n          messages: FAKE_LOCAL_MESSAGES,\n        },\n      ]);\n      fetchStub.withArgs(\"http://fake.com/endpoint\").resolves({\n        ok: true,\n        status: 200,\n        json: () => Promise.resolve({ messages: NEW_MESSAGES }),\n        headers: FAKE_RESPONSE_HEADERS,\n      });\n\n      clock.tick(301);\n      await Router.loadMessagesFromAllProviders();\n\n      // These are the new messages\n      assertRouterContainsMessages(NEW_MESSAGES);\n      // These are the local messages that should not have been deleted\n      assertRouterContainsMessages(FAKE_LOCAL_MESSAGES);\n    });\n    it(\"should parse the triggers in the messages and register the trigger listeners\", async () => {\n      sandbox.spy(\n        ASRouterTriggerListeners.get(\"openURL\"),\n        \"init\"\n      ); /* eslint-disable object-property-newline */\n\n      /* eslint-disable object-curly-newline */ await createRouterAndInit([\n        {\n          id: \"foo\",\n          type: \"local\",\n          enabled: true,\n          messages: [\n            {\n              id: \"foo\",\n              template: \"simple_template\",\n              trigger: { id: \"firstRun\" },\n              content: { title: \"Foo\", body: \"Foo123\" },\n            },\n            {\n              id: \"bar1\",\n              template: \"simple_template\",\n              trigger: {\n                id: \"openURL\",\n                params: [\"www.mozilla.org\", \"www.mozilla.com\"],\n              },\n              content: { title: \"Bar1\", body: \"Bar123\" },\n            },\n            {\n              id: \"bar2\",\n              template: \"simple_template\",\n              trigger: { id: \"openURL\", params: [\"www.example.com\"] },\n              content: { title: \"Bar2\", body: \"Bar123\" },\n            },\n          ],\n        },\n      ]); /* eslint-enable object-property-newline */\n      /* eslint-enable object-curly-newline */ assert.calledTwice(\n        ASRouterTriggerListeners.get(\"openURL\").init\n      );\n      assert.calledWithExactly(\n        ASRouterTriggerListeners.get(\"openURL\").init,\n        Router._triggerHandler,\n        [\"www.mozilla.org\", \"www.mozilla.com\"],\n        undefined\n      );\n      assert.calledWithExactly(\n        ASRouterTriggerListeners.get(\"openURL\").init,\n        Router._triggerHandler,\n        [\"www.example.com\"],\n        undefined\n      );\n    });\n    it(\"should gracefully handle RemoteSettings blowing up and dispatch undesired event\", async () => {\n      sandbox\n        .stub(MessageLoaderUtils, \"_getRemoteSettingsMessages\")\n        .rejects(\"fake error\");\n      await createRouterAndInit();\n      assert.calledWith(Router.dispatchToAS, {\n        data: {\n          action: \"asrouter_undesired_event\",\n          event: \"ASR_RS_ERROR\",\n          event_context: \"remotey-settingsy\",\n          message_id: \"n/a\",\n        },\n        meta: { from: \"ActivityStream:Content\", to: \"ActivityStream:Main\" },\n        type: \"AS_ROUTER_TELEMETRY_USER_EVENT\",\n      });\n    });\n    it(\"should dispatch undesired event if RemoteSettings returns no messages\", async () => {\n      sandbox\n        .stub(MessageLoaderUtils, \"_getRemoteSettingsMessages\")\n        .resolves([]);\n      await createRouterAndInit();\n      assert.calledWith(Router.dispatchToAS, {\n        data: {\n          action: \"asrouter_undesired_event\",\n          event: \"ASR_RS_NO_MESSAGES\",\n          event_context: \"remotey-settingsy\",\n          message_id: \"n/a\",\n        },\n        meta: { from: \"ActivityStream:Content\", to: \"ActivityStream:Main\" },\n        type: \"AS_ROUTER_TELEMETRY_USER_EVENT\",\n      });\n    });\n    it(\"should download the attachment if RemoteSettings returns some messages\", async () => {\n      sandbox\n        .stub(global.Services.locale, \"appLocaleAsLangTag\")\n        .get(() => \"en-US\");\n      sandbox\n        .stub(MessageLoaderUtils, \"_getRemoteSettingsMessages\")\n        .resolves([{ id: \"message_1\" }]);\n      const spy = sandbox.spy();\n      global.Downloader.prototype.download = spy;\n      const provider = {\n        id: \"cfr\",\n        enabled: true,\n        type: \"remote-settings\",\n        bucket: \"cfr\",\n      };\n      await createRouterAndInit([provider]);\n\n      assert.calledOnce(spy);\n    });\n    it(\"should dispatch undesired event if the ms-language-packs returns no messages\", async () => {\n      sandbox\n        .stub(global.Services.locale, \"appLocaleAsLangTag\")\n        .get(() => \"en-US\");\n      sandbox\n        .stub(MessageLoaderUtils, \"_getRemoteSettingsMessages\")\n        .resolves([{ id: \"message_1\" }]);\n      sandbox\n        .stub(global.KintoHttpClient.prototype, \"getRecord\")\n        .resolves(null);\n      const provider = {\n        id: \"cfr\",\n        enabled: true,\n        type: \"remote-settings\",\n        bucket: \"cfr\",\n      };\n      await createRouterAndInit([provider]);\n\n      assert.calledWith(Router.dispatchToAS, {\n        data: {\n          action: \"asrouter_undesired_event\",\n          event: \"ASR_RS_NO_MESSAGES\",\n          event_context: \"ms-language-packs\",\n          message_id: \"n/a\",\n        },\n        meta: { from: \"ActivityStream:Content\", to: \"ActivityStream:Main\" },\n        type: \"AS_ROUTER_TELEMETRY_USER_EVENT\",\n      });\n    });\n  });\n\n  describe(\"#_updateMessageProviders\", () => {\n    it(\"should correctly replace %STARTPAGE_VERSION% in remote provider urls\", () => {\n      // If this test fails, you need to update the constant STARTPAGE_VERSION in\n      // ASRouter.jsm to match the `version` property of provider-response-schema.json\n      const expectedStartpageVersion = ProviderResponseSchema.version;\n      const provider = {\n        id: \"foo\",\n        enabled: true,\n        type: \"remote\",\n        url: \"https://www.mozilla.org/%STARTPAGE_VERSION%/\",\n      };\n      setMessageProviderPref([provider]);\n      Router._updateMessageProviders();\n      assert.equal(\n        Router.state.providers[0].url,\n        `https://www.mozilla.org/${parseInt(expectedStartpageVersion, 10)}/`\n      );\n    });\n    it(\"should replace other params in remote provider urls by calling Services.urlFormater.formatURL\", () => {\n      const url = \"https://www.example.com/\";\n      const replacedUrl = \"https://www.foo.bar/\";\n      const stub = sandbox\n        .stub(global.Services.urlFormatter, \"formatURL\")\n        .withArgs(url)\n        .returns(replacedUrl);\n      const provider = { id: \"foo\", enabled: true, type: \"remote\", url };\n      setMessageProviderPref([provider]);\n      Router._updateMessageProviders();\n      assert.calledOnce(stub);\n      assert.calledWithExactly(stub, url);\n      assert.equal(Router.state.providers[0].url, replacedUrl);\n    });\n    it(\"should only add the providers that are enabled\", () => {\n      const providers = [\n        {\n          id: \"foo\",\n          enabled: false,\n          type: \"remote\",\n          url: \"https://www.foo.com/\",\n        },\n        {\n          id: \"bar\",\n          enabled: true,\n          type: \"remote\",\n          url: \"https://www.bar.com/\",\n        },\n      ];\n      setMessageProviderPref(providers);\n      Router._updateMessageProviders();\n      assert.equal(Router.state.providers.length, 1);\n      assert.equal(Router.state.providers[0].id, providers[1].id);\n    });\n    it(\"should return provider `foo` because both categories are enabled\", () => {\n      const providers = [\n        {\n          id: \"foo\",\n          enabled: true,\n          categories: [\"cfrFeatures\", \"cfrAddons\"],\n          type: \"remote\",\n          url: \"https://www.foo.com/\",\n        },\n      ];\n      sandbox.stub(ASRouterPreferences, \"providers\").value(providers);\n      sandbox\n        .stub(ASRouterPreferences, \"getUserPreference\")\n        .withArgs(\"cfrFeatures\")\n        .returns(true)\n        .withArgs(\"cfrAddons\")\n        .returns(true);\n      Router._updateMessageProviders();\n      assert.equal(Router.state.providers.length, 1);\n      assert.equal(Router.state.providers[0].id, providers[0].id);\n    });\n    it(\"should return provider `foo` because at least 1 category is enabled\", () => {\n      const providers = [\n        {\n          id: \"foo\",\n          enabled: true,\n          categories: [\"cfrFeatures\", \"cfrAddons\"],\n          type: \"remote\",\n          url: \"https://www.foo.com/\",\n        },\n      ];\n      sandbox.stub(ASRouterPreferences, \"providers\").value(providers);\n      sandbox\n        .stub(ASRouterPreferences, \"getUserPreference\")\n        .withArgs(\"cfrFeatures\")\n        .returns(false)\n        .withArgs(\"cfrAddons\")\n        .returns(true);\n      Router._updateMessageProviders();\n      assert.equal(Router.state.providers.length, 1);\n      assert.equal(Router.state.providers[0].id, providers[0].id);\n    });\n    it(\"should not return provider `foo` because no categories are enabled\", () => {\n      const providers = [\n        {\n          id: \"foo\",\n          enabled: true,\n          categories: [\"cfrFeatures\", \"cfrAddons\"],\n          type: \"remote\",\n          url: \"https://www.foo.com/\",\n        },\n      ];\n      sandbox.stub(ASRouterPreferences, \"providers\").value(providers);\n      sandbox\n        .stub(ASRouterPreferences, \"getUserPreference\")\n        .withArgs(\"cfrFeatures\")\n        .returns(false)\n        .withArgs(\"cfrAddons\")\n        .returns(false);\n      Router._updateMessageProviders();\n      assert.equal(Router.state.providers.length, 0);\n    });\n  });\n\n  describe(\"#handleMessageRequest\", () => {\n    it(\"should not return a blocked message\", async () => {\n      // Block all messages except the first\n      await Router.setState(() => ({\n        messages: [\n          { id: \"foo\", provider: \"snippets\" },\n          { id: \"bar\", provider: \"snippets\" },\n        ],\n        messageBlockList: [\"foo\"],\n      }));\n      const result = await Router.handleMessageRequest({\n        provider: \"snippets\",\n      });\n      assert.equal(result.id, \"bar\");\n    });\n    it(\"should not return a message from a blocked campaign\", async () => {\n      // Block all messages except the first\n      await Router.setState(() => ({\n        messages: [\n          { id: \"foo\", provider: \"snippets\", campaign: \"foocampaign\" },\n          { id: \"bar\", provider: \"snippets\" },\n        ],\n        messageBlockList: [\"foocampaign\"],\n      }));\n\n      const result = await Router.handleMessageRequest({\n        provider: \"snippets\",\n      });\n\n      assert.equal(result.id, \"bar\");\n    });\n    it(\"should not return a message from a blocked provider\", async () => {\n      // There are only two providers; block the FAKE_LOCAL_PROVIDER, leaving\n      // only FAKE_REMOTE_PROVIDER unblocked, which provides only one message\n      await Router.setState(() => ({\n        providerBlockList: [\"snippets\"],\n      }));\n\n      await Router.setState(() => ({\n        messages: [{ id: \"foo\", provider: \"snippets\" }],\n        messageBlockList: [\"foocampaign\"],\n      }));\n\n      const result = await Router.handleMessageRequest({\n        provider: \"snippets\",\n      });\n\n      assert.isNull(result);\n    });\n    it(\"should get unblocked messages that match the trigger\", async () => {\n      const message1 = {\n        id: \"1\",\n        campaign: \"foocampaign\",\n        trigger: { id: \"foo\" },\n      };\n      const message2 = {\n        id: \"2\",\n        campaign: \"foocampaign\",\n        trigger: { id: \"bar\" },\n      };\n      await Router.setState({ messages: [message2, message1] });\n      // Just return the first message provided as arg\n      sandbox\n        .stub(ASRouterTargeting, \"findMatchingMessage\")\n        .callsFake(({ messages }) => messages[0]);\n\n      const result = Router.handleMessageRequest({ triggerId: \"foo\" });\n\n      assert.deepEqual(result, message1);\n    });\n    it(\"should get unblocked messages that match trigger and template\", async () => {\n      const message1 = {\n        id: \"1\",\n        campaign: \"foocampaign\",\n        template: \"badge\",\n        trigger: { id: \"foo\" },\n      };\n      const message2 = {\n        id: \"2\",\n        campaign: \"foocampaign\",\n        template: \"snippet\",\n        trigger: { id: \"foo\" },\n      };\n      await Router.setState({ messages: [message2, message1] });\n      // Just return the first message provided as arg\n      sandbox\n        .stub(ASRouterTargeting, \"findMatchingMessage\")\n        .callsFake(({ messages }) => messages[0]);\n\n      const result = Router.handleMessageRequest({\n        triggerId: \"foo\",\n        template: \"badge\",\n      });\n\n      assert.deepEqual(result, message1);\n    });\n    it(\"should have messageImpressions in the message context\", () => {\n      assert.propertyVal(\n        Router._getMessagesContext(),\n        \"messageImpressions\",\n        Router.state.messageImpressions\n      );\n    });\n    it(\"should return all unblocked messages that match the template, trigger if returnAll=true\", async () => {\n      const message1 = {\n        id: \"1\",\n        template: \"whatsnew_panel_message\",\n        trigger: { id: \"whatsNewPanelOpened\" },\n      };\n      const message2 = {\n        id: \"2\",\n        template: \"whatsnew_panel_message\",\n        trigger: { id: \"whatsNewPanelOpened\" },\n      };\n      const message3 = {\n        id: \"3\",\n        template: \"badge\",\n      };\n      sandbox\n        .stub(ASRouterTargeting, \"findMatchingMessage\")\n        .callsFake(() => [message2, message1]);\n      await Router.setState({ messages: [message3, message2, message1] });\n      const result = await Router.handleMessageRequest({\n        template: \"whatsnew-panel\",\n        triggerId: \"whatsNewPanelOpened\",\n        returnAll: true,\n      });\n\n      assert.deepEqual(result, [message2, message1]);\n    });\n    it(\"should forward trigger param info\", async () => {\n      const trigger = {\n        triggerId: \"foo\",\n        triggerParam: \"bar\",\n        triggerContext: \"context\",\n      };\n      const message1 = {\n        id: \"1\",\n        campaign: \"foocampaign\",\n        trigger: { id: \"foo\" },\n      };\n      const message2 = {\n        id: \"2\",\n        campaign: \"foocampaign\",\n        trigger: { id: \"bar\" },\n      };\n      await Router.setState({ messages: [message2, message1] });\n      // Just return the first message provided as arg\n      const stub = sandbox.stub(ASRouterTargeting, \"findMatchingMessage\");\n\n      Router.handleMessageRequest(trigger);\n\n      assert.calledOnce(stub);\n\n      const [options] = stub.firstCall.args;\n      assert.propertyVal(options.trigger, \"id\", trigger.triggerId);\n      assert.propertyVal(options.trigger, \"param\", trigger.triggerParam);\n      assert.propertyVal(options.trigger, \"context\", trigger.triggerContext);\n      assert.propertyVal(options, \"shouldCache\", false);\n    });\n    it(\"should cache snippets messages\", async () => {\n      const trigger = {\n        triggerId: \"foo\",\n        triggerParam: \"bar\",\n        triggerContext: \"context\",\n      };\n      const message1 = {\n        id: \"1\",\n        provider: \"snippets\",\n        campaign: \"foocampaign\",\n        trigger: { id: \"foo\" },\n      };\n      const message2 = {\n        id: \"2\",\n        campaign: \"foocampaign\",\n        trigger: { id: \"bar\" },\n      };\n      await Router.setState({ messages: [message2, message1] });\n      // Just return the first message provided as arg\n      const stub = sandbox.stub(ASRouterTargeting, \"findMatchingMessage\");\n\n      Router.handleMessageRequest(trigger);\n\n      assert.calledOnce(stub);\n\n      const [options] = stub.firstCall.args;\n      assert.propertyVal(options, \"shouldCache\", true);\n    });\n    it(\"should filter out messages without a trigger (or different) when a triggerId is defined\", async () => {\n      const trigger = { triggerId: \"foo\" };\n      const message1 = {\n        id: \"1\",\n        campaign: \"foocampaign\",\n        trigger: { id: \"foo\" },\n      };\n      const message2 = {\n        id: \"2\",\n        campaign: \"foocampaign\",\n        trigger: { id: \"bar\" },\n      };\n      const message3 = {\n        id: \"3\",\n        campaign: \"bazcampaign\",\n      };\n      await Router.setState({ messages: [message2, message1, message3] });\n      // Just return the first message provided as arg\n      sandbox\n        .stub(ASRouterTargeting, \"findMatchingMessage\")\n        .callsFake(args => args.messages);\n\n      const result = Router.handleMessageRequest(trigger);\n\n      assert.lengthOf(result, 1);\n      assert.deepEqual(result[0], message1);\n    });\n  });\n\n  describe(\"#uninit\", () => {\n    it(\"should remove the message listener on the RemotePageManager\", () => {\n      const [, listenerAdded] = channel.addMessageListener.firstCall.args;\n      assert.isFunction(listenerAdded);\n\n      Router.uninit();\n\n      assert.calledWith(\n        channel.removeMessageListener,\n        CHILD_TO_PARENT_MESSAGE_NAME,\n        listenerAdded\n      );\n    });\n    it(\"should unregister the trigger listeners\", () => {\n      for (const listener of ASRouterTriggerListeners.values()) {\n        sandbox.spy(listener, \"uninit\");\n      }\n\n      Router.uninit();\n\n      for (const listener of ASRouterTriggerListeners.values()) {\n        assert.calledOnce(listener.uninit);\n      }\n    });\n    it(\"should set .dispatchToAS to null\", () => {\n      Router.uninit();\n      assert.isNull(Router.dispatchToAS);\n    });\n    it(\"should save previousSessionEnd\", () => {\n      Router.uninit();\n\n      assert.calledOnce(Router._storage.set);\n      assert.calledWithExactly(\n        Router._storage.set,\n        \"previousSessionEnd\",\n        sinon.match.number\n      );\n    });\n    it(\"should remove the observer for `intl:app-locales-changed`\", () => {\n      sandbox.spy(global.Services.obs, \"removeObserver\");\n      Router.uninit();\n\n      assert.calledOnce(global.Services.obs.removeObserver);\n      assert.equal(\n        global.Services.obs.removeObserver.args[0][1],\n        \"intl:app-locales-changed\"\n      );\n    });\n    it(\"should remove the pref observer for `USE_REMOTE_L10N_PREF`\", async () => {\n      sandbox.spy(global.Services.prefs, \"removeObserver\");\n      Router.uninit();\n\n      // Grab the last call as #uninit() also involves multiple calls of `Services.prefs.removeObserver`.\n      const call = global.Services.prefs.removeObserver.lastCall;\n      assert.calledWithExactly(call, USE_REMOTE_L10N_PREF, Router);\n    });\n  });\n\n  describe(\"onMessage\", () => {\n    describe(\"#onMessage: NEWTAB_MESSAGE_REQUEST\", () => {\n      it(\"should send a message back to the to the target\", async () => {\n        // force the only message to be a regular message so getRandomItemFromArray picks it\n        await Router.setState({\n          messages: [{ id: \"foo\", provider: \"snippets\" }],\n        });\n        const msg = fakeAsyncMessage({ type: \"NEWTAB_MESSAGE_REQUEST\" });\n        await Router.onMessage(msg);\n\n        assert.calledWith(\n          msg.target.sendAsyncMessage,\n          PARENT_TO_CHILD_MESSAGE_NAME,\n          { type: \"SET_MESSAGE\", data: { id: \"foo\", provider: \"snippets\" } }\n        );\n      });\n      it(\"should send a message back to the to the target if there is a bundle, too\", async () => {\n        // force the only message to be a bundled message so getRandomItemFromArray picks it\n        sandbox.stub(Router, \"_findProvider\").returns(null);\n        await Router.setState({\n          messages: [\n            {\n              id: \"foo1\",\n              provider: \"snippets\",\n              template: \"simple_template\",\n              bundled: 1,\n              content: { title: \"Foo1\", body: \"Foo123-1\" },\n            },\n          ],\n        });\n        const msg = fakeAsyncMessage({ type: \"NEWTAB_MESSAGE_REQUEST\" });\n        await Router.onMessage(msg);\n        assert.calledWith(\n          msg.target.sendAsyncMessage,\n          PARENT_TO_CHILD_MESSAGE_NAME\n        );\n        assert.equal(\n          msg.target.sendAsyncMessage.firstCall.args[1].type,\n          \"SET_BUNDLED_MESSAGES\"\n        );\n      });\n      it(\"should properly order the message's bundle if specified\", async () => {\n        // force the only messages to be a bundled messages so getRandomItemFromArray picks one of them\n        sandbox.stub(Router, \"_findProvider\").returns(null);\n        const firstMessage = {\n          id: \"foo2\",\n          provider: \"snippets\",\n          template: \"simple_template\",\n          bundled: 2,\n          order: 1,\n          content: { title: \"Foo2\", body: \"Foo123-2\" },\n        };\n        const secondMessage = {\n          id: \"foo1\",\n          provider: \"snippets\",\n          template: \"simple_template\",\n          bundled: 2,\n          order: 2,\n          content: { title: \"Foo1\", body: \"Foo123-1\" },\n        };\n        await Router.setState({ messages: [secondMessage, firstMessage] });\n        const msg = fakeAsyncMessage({ type: \"NEWTAB_MESSAGE_REQUEST\" });\n        await Router.onMessage(msg);\n        assert.calledWith(\n          msg.target.sendAsyncMessage,\n          PARENT_TO_CHILD_MESSAGE_NAME\n        );\n        assert.equal(\n          msg.target.sendAsyncMessage.firstCall.args[1].type,\n          \"SET_BUNDLED_MESSAGES\"\n        );\n        assert.equal(\n          msg.target.sendAsyncMessage.firstCall.args[1].data.bundle[0].content,\n          firstMessage.content\n        );\n        assert.equal(\n          msg.target.sendAsyncMessage.firstCall.args[1].data.bundle[1].content,\n          secondMessage.content\n        );\n      });\n      it(\"should return a null bundle if we do not have enough messages to fill the bundle\", async () => {\n        // force the only message to be a bundled message that needs 2 messages in the bundle\n        await Router.setState({\n          messages: [\n            {\n              id: \"foo1\",\n              template: \"simple_template\",\n              bundled: 2,\n              content: { title: \"Foo1\", body: \"Foo123-1\" },\n            },\n          ],\n        });\n        const bundle = await Router._getBundledMessages(\n          Router.state.messages[0]\n        );\n        assert.equal(bundle, null);\n      });\n      it(\"should send down extra attributes in the bundle if they exist\", async () => {\n        sandbox.stub(Router, \"_findProvider\").returns({\n          getExtraAttributes() {\n            return Promise.resolve({ header: \"header\" });\n          },\n        });\n        await Router.setState({\n          messages: [\n            {\n              id: \"foo1\",\n              template: \"simple_template\",\n              bundled: 1,\n              content: { title: \"Foo1\", body: \"Foo123-1\" },\n            },\n          ],\n        });\n        const result = await Router._getBundledMessages(\n          Router.state.messages[0]\n        );\n        assert.equal(result.extraTemplateStrings.header, \"header\");\n      });\n      it(\"should send a CLEAR_ALL message if no bundle available\", async () => {\n        // force the only message to be a bundled message that needs 2 messages in the bundle\n        await Router.setState({\n          messages: [\n            {\n              id: \"foo1\",\n              provider: \"snippets\",\n              template: \"simple_template\",\n              bundled: 2,\n              content: { title: \"Foo1\", body: \"Foo123-1\" },\n            },\n          ],\n        });\n        const msg = fakeAsyncMessage({ type: \"NEWTAB_MESSAGE_REQUEST\" });\n        await Router.onMessage(msg);\n        assert.calledWith(\n          msg.target.sendAsyncMessage,\n          PARENT_TO_CHILD_MESSAGE_NAME,\n          { type: \"CLEAR_ALL\" }\n        );\n      });\n      it(\"should send a CLEAR_ALL message if no messages are available\", async () => {\n        await Router.setState({ messages: [] });\n        const msg = fakeAsyncMessage({ type: \"NEWTAB_MESSAGE_REQUEST\" });\n        await Router.onMessage(msg);\n\n        assert.calledWith(\n          msg.target.sendAsyncMessage,\n          PARENT_TO_CHILD_MESSAGE_NAME,\n          { type: \"CLEAR_ALL\" }\n        );\n      });\n      it(\"should make a request to the provided endpoint on NEWTAB_MESSAGE_REQUEST\", async () => {\n        const url = \"https://snippets-admin.mozilla.org/foo\";\n        const msg = fakeAsyncMessage({\n          type: \"NEWTAB_MESSAGE_REQUEST\",\n          data: { endpoint: { url } },\n        });\n        await Router.onMessage(msg);\n\n        assert.calledWith(global.fetch, url);\n        assert.lengthOf(Router.state.providers.filter(p => p.url === url), 0);\n      });\n      it(\"should make a request to the provided endpoint on ADMIN_CONNECT_STATE and remove the endpoint\", async () => {\n        const url = \"https://snippets-admin.mozilla.org/foo\";\n        const msg = fakeAsyncMessage({\n          type: \"ADMIN_CONNECT_STATE\",\n          data: { endpoint: { url } },\n        });\n        await Router.onMessage(msg);\n\n        assert.calledWith(global.fetch, url);\n        assert.lengthOf(Router.state.providers.filter(p => p.url === url), 0);\n      });\n      it(\"should dispatch SNIPPETS_PREVIEW_MODE when adding a preview endpoint\", async () => {\n        const url = \"https://snippets-admin.mozilla.org/foo\";\n        const msg = fakeAsyncMessage({\n          type: \"NEWTAB_MESSAGE_REQUEST\",\n          data: { endpoint: { url } },\n        });\n        await Router.onMessage(msg);\n\n        assert.calledWithExactly(\n          Router.dispatchToAS,\n          ac.OnlyToOneContent(\n            { type: \"SNIPPETS_PREVIEW_MODE\" },\n            msg.target.portID\n          )\n        );\n      });\n      it(\"should not add a url that is not from a whitelisted host\", async () => {\n        const url = \"https://mozilla.org\";\n        const msg = fakeAsyncMessage({\n          type: \"NEWTAB_MESSAGE_REQUEST\",\n          data: { endpoint: { url } },\n        });\n        await Router.onMessage(msg);\n\n        assert.lengthOf(Router.state.providers.filter(p => p.url === url), 0);\n      });\n      it(\"should reject bad urls\", async () => {\n        const url = \"foo\";\n        const msg = fakeAsyncMessage({\n          type: \"NEWTAB_MESSAGE_REQUEST\",\n          data: { endpoint: { url } },\n        });\n        await Router.onMessage(msg);\n\n        assert.lengthOf(Router.state.providers.filter(p => p.url === url), 0);\n      });\n      it(\"should handle onboarding message provider\", async () => {\n        const handleMessageRequestStub = sandbox.stub(\n          Router,\n          \"handleMessageRequest\"\n        );\n        handleMessageRequestStub\n          .withArgs({\n            template: \"extended_triplets\",\n          })\n          .resolves({ id: \"foo\" });\n        sandbox.stub(Router, \"sendNewTabMessage\").resolves();\n        const msg = fakeAsyncMessage({\n          type: \"NEWTAB_MESSAGE_REQUEST\",\n          data: {},\n        });\n        await Router.onMessage(msg);\n\n        assert.calledOnce(Router.sendNewTabMessage);\n      });\n      it(\"should fallback to snippets if onboarding message provider returned none\", async () => {\n        const handleMessageRequestStub = sandbox.stub(\n          Router,\n          \"handleMessageRequest\"\n        );\n        handleMessageRequestStub\n          .withArgs({\n            template: \"extended_triplets\",\n          })\n          .resolves(null);\n        const msg = fakeAsyncMessage({\n          type: \"NEWTAB_MESSAGE_REQUEST\",\n          data: {},\n        });\n        await Router.onMessage(msg);\n\n        assert.calledTwice(handleMessageRequestStub);\n        assert.calledWithExactly(handleMessageRequestStub, {\n          template: \"extended_triplets\",\n        });\n        assert.calledWithExactly(handleMessageRequestStub, {\n          provider: \"snippets\",\n        });\n      });\n    });\n\n    describe(\"#onMessage: BLOCK_MESSAGE_BY_ID\", () => {\n      it(\"should add the id to the messageBlockList and broadcast a CLEAR_MESSAGE message with the id\", async () => {\n        const msg = fakeAsyncMessage({\n          type: \"BLOCK_MESSAGE_BY_ID\",\n          data: { id: \"foo\" },\n        });\n        await Router.onMessage(msg);\n\n        assert.isTrue(Router.state.messageBlockList.includes(\"foo\"));\n        assert.calledWith(\n          channel.sendAsyncMessage,\n          PARENT_TO_CHILD_MESSAGE_NAME,\n          { type: \"CLEAR_MESSAGE\", data: { id: \"foo\" } }\n        );\n      });\n      it(\"should only send CLEAR_MESSAGE to preloaded if action.data.preloadedOnly is true\", async () => {\n        sandbox.stub(Router, \"sendAsyncMessageToPreloaded\");\n        const msg = fakeAsyncMessage({\n          type: \"BLOCK_MESSAGE_BY_ID\",\n          data: { id: \"foo\", preloadedOnly: true },\n        });\n        await Router.onMessage(msg);\n\n        assert.calledWith(Router.sendAsyncMessageToPreloaded, {\n          type: \"CLEAR_MESSAGE\",\n          data: { id: \"foo\" },\n        });\n      });\n      it(\"should add the campaign to the messageBlockList instead of id if .campaign is specified and not select messages of that campaign again\", async () => {\n        await Router.setState({\n          messages: [\n            { id: \"1\", campaign: \"foocampaign\" },\n            { id: \"2\", campaign: \"foocampaign\" },\n          ],\n        });\n        const msg = fakeAsyncMessage({\n          type: \"BLOCK_MESSAGE_BY_ID\",\n          data: { id: \"1\" },\n        });\n        await Router.onMessage(msg);\n\n        assert.isTrue(Router.state.messageBlockList.includes(\"foocampaign\"));\n        assert.isEmpty(Router._getUnblockedMessages());\n      });\n      it(\"should not broadcast CLEAR_MESSAGE if preventDismiss is true\", async () => {\n        const msg = fakeAsyncMessage({\n          type: \"BLOCK_MESSAGE_BY_ID\",\n          data: { id: \"foo\", preventDismiss: true },\n        });\n        await Router.onMessage(msg);\n\n        assert.notCalled(channel.sendAsyncMessage);\n      });\n    });\n\n    describe(\"#onMessage: DISMISS_MESSAGE_BY_ID\", () => {\n      it(\"should reply with CLEAR_MESSAGE with the correct id\", async () => {\n        const msg = fakeAsyncMessage({\n          type: \"DISMISS_MESSAGE_BY_ID\",\n          data: { id: \"foo\" },\n        });\n\n        await Router.onMessage(msg);\n\n        assert.calledWith(\n          channel.sendAsyncMessage,\n          PARENT_TO_CHILD_MESSAGE_NAME,\n          { type: \"CLEAR_MESSAGE\", data: { id: \"foo\" } }\n        );\n      });\n    });\n\n    describe(\"#onMessage: BLOCK_PROVIDER_BY_ID\", () => {\n      it(\"should add the provider id to the providerBlockList and broadcast a CLEAR_PROVIDER with the provider id\", async () => {\n        const msg = fakeAsyncMessage({\n          type: \"BLOCK_PROVIDER_BY_ID\",\n          data: { id: \"bar\" },\n        });\n        await Router.onMessage(msg);\n\n        assert.isTrue(Router.state.providerBlockList.includes(\"bar\"));\n        assert.calledWith(\n          channel.sendAsyncMessage,\n          PARENT_TO_CHILD_MESSAGE_NAME,\n          { type: \"CLEAR_PROVIDER\", data: { id: \"bar\" } }\n        );\n      });\n    });\n\n    describe(\"#onMessage: UNBLOCK_MESSAGE_BY_ID\", () => {\n      it(\"should remove the id from the messageBlockList\", async () => {\n        await Router.onMessage(\n          fakeAsyncMessage({ type: \"BLOCK_MESSAGE_BY_ID\", data: { id: \"foo\" } })\n        );\n        assert.isTrue(Router.state.messageBlockList.includes(\"foo\"));\n        await Router.onMessage(\n          fakeAsyncMessage({\n            type: \"UNBLOCK_MESSAGE_BY_ID\",\n            data: { id: \"foo\" },\n          })\n        );\n\n        assert.isFalse(Router.state.messageBlockList.includes(\"foo\"));\n      });\n      it(\"should remove the campaign from the messageBlockList if it is defined\", async () => {\n        await Router.setState({ messages: [{ id: \"1\", campaign: \"foo\" }] });\n        await Router.onMessage(\n          fakeAsyncMessage({ type: \"BLOCK_MESSAGE_BY_ID\", data: { id: \"1\" } })\n        );\n        assert.isTrue(\n          Router.state.messageBlockList.includes(\"foo\"),\n          \"blocklist has campaign id\"\n        );\n        await Router.onMessage(\n          fakeAsyncMessage({ type: \"UNBLOCK_MESSAGE_BY_ID\", data: { id: \"1\" } })\n        );\n\n        assert.isFalse(\n          Router.state.messageBlockList.includes(\"foo\"),\n          \"campaign id removed from blocklist\"\n        );\n      });\n      it(\"should save the messageBlockList\", async () => {\n        await Router.onMessage(\n          fakeAsyncMessage({\n            type: \"UNBLOCK_MESSAGE_BY_ID\",\n            data: { id: \"foo\" },\n          })\n        );\n\n        assert.calledWithExactly(Router._storage.set, \"messageBlockList\", []);\n      });\n    });\n\n    describe(\"#onMessage: UNBLOCK_PROVIDER_BY_ID\", () => {\n      it(\"should remove the id from the providerBlockList\", async () => {\n        await Router.onMessage(\n          fakeAsyncMessage({\n            type: \"BLOCK_PROVIDER_BY_ID\",\n            data: { id: \"foo\" },\n          })\n        );\n        assert.isTrue(Router.state.providerBlockList.includes(\"foo\"));\n        await Router.onMessage(\n          fakeAsyncMessage({\n            type: \"UNBLOCK_PROVIDER_BY_ID\",\n            data: { id: \"foo\" },\n          })\n        );\n\n        assert.isFalse(Router.state.providerBlockList.includes(\"foo\"));\n      });\n      it(\"should save the providerBlockList\", async () => {\n        await Router.onMessage(\n          fakeAsyncMessage({\n            type: \"UNBLOCK_PROVIDER_BY_ID\",\n            data: { id: \"foo\" },\n          })\n        );\n\n        assert.calledWithExactly(Router._storage.set, \"providerBlockList\", []);\n      });\n    });\n\n    describe(\"#onMessage: UNBLOCK_BUNDLE\", () => {\n      it(\"should remove all the ids in the bundle from the messageBlockList\", async () => {\n        await Router.onMessage(\n          fakeAsyncMessage({\n            type: \"BLOCK_BUNDLE\",\n            data: { bundle: FAKE_BUNDLE },\n          })\n        );\n        assert.isTrue(\n          Router.state.messageBlockList.includes(FAKE_BUNDLE[0].id)\n        );\n        assert.isTrue(\n          Router.state.messageBlockList.includes(FAKE_BUNDLE[1].id)\n        );\n        await Router.onMessage(\n          fakeAsyncMessage({\n            type: \"UNBLOCK_BUNDLE\",\n            data: { bundle: FAKE_BUNDLE },\n          })\n        );\n\n        assert.isFalse(\n          Router.state.messageBlockList.includes(FAKE_BUNDLE[0].id)\n        );\n        assert.isFalse(\n          Router.state.messageBlockList.includes(FAKE_BUNDLE[1].id)\n        );\n      });\n      it(\"should save the messageBlockList\", async () => {\n        await Router.onMessage(\n          fakeAsyncMessage({\n            type: \"UNBLOCK_BUNDLE\",\n            data: { bundle: FAKE_BUNDLE },\n          })\n        );\n\n        assert.calledWithExactly(Router._storage.set, \"messageBlockList\", []);\n      });\n    });\n\n    describe(\"#onMessage: ADMIN_CONNECT_STATE\", () => {\n      it(\"should send a message containing the whole state\", async () => {\n        sandbox.stub(Router, \"getTargetingParameters\").resolves({});\n        const msg = fakeAsyncMessage({ type: \"ADMIN_CONNECT_STATE\" });\n\n        await Router.onMessage(msg);\n        assert.calledOnce(msg.target.sendAsyncMessage);\n        assert.deepEqual(msg.target.sendAsyncMessage.firstCall.args[1], {\n          type: \"ADMIN_SET_STATE\",\n          data: Object.assign({}, Router.state, {\n            providerPrefs: ASRouterPreferences.providers,\n            userPrefs: ASRouterPreferences.getAllUserPreferences(),\n            targetingParameters: {},\n            trailhead: ASRouterPreferences.trailhead,\n            errors: Router.errors,\n          }),\n        });\n      });\n    });\n\n    describe(\"#onMessage: NEWTAB_MESSAGE_REQUEST\", () => {\n      it(\"should call sendNewTabMessage on NEWTAB_MESSAGE_REQUEST\", async () => {\n        sandbox.stub(Router, \"sendNewTabMessage\").resolves();\n        const data = { endpoint: \"foo\" };\n        const msg = fakeAsyncMessage({ type: \"NEWTAB_MESSAGE_REQUEST\", data });\n\n        await Router.onMessage(msg);\n\n        assert.calledOnce(Router.sendNewTabMessage);\n        assert.calledWithExactly(\n          Router.sendNewTabMessage,\n          sinon.match.instanceOf(FakeRemotePageManager),\n          data\n        );\n      });\n      it(\"should return the preview message if that's available and remove it from Router.state\", async () => {\n        const expectedObj = { provider: \"preview\" };\n        Router.setState({ messages: [expectedObj] });\n\n        await Router.sendNewTabMessage(channel, { endpoint: \"foo.com\" });\n\n        assert.calledWith(\n          channel.sendAsyncMessage,\n          PARENT_TO_CHILD_MESSAGE_NAME,\n          { type: \"SET_MESSAGE\", data: expectedObj }\n        );\n        assert.isUndefined(\n          Router.state.messages.find(m => m.provider === \"preview\")\n        );\n      });\n      it(\"should call _getBundledMessages if we request a message that needs to be bundled\", async () => {\n        sandbox.stub(Router, \"_getBundledMessages\").resolves();\n        // forcefully pick a message which needs to be bundled (the second message in FAKE_LOCAL_MESSAGES)\n        const [, testMessage] = Router.state.messages;\n        const msg = fakeAsyncMessage({\n          type: \"OVERRIDE_MESSAGE\",\n          data: { id: testMessage.id },\n        });\n        await Router.onMessage(msg);\n\n        assert.calledOnce(Router._getBundledMessages);\n      });\n      it(\"should properly pick another message of the same template if it is bundled; force = true\", async () => {\n        // forcefully pick a message which needs to be bundled (the second message in FAKE_LOCAL_MESSAGES)\n        const [, testMessage1, testMessage2] = Router.state.messages;\n        const msg = fakeAsyncMessage({\n          type: \"OVERRIDE_MESSAGE\",\n          data: { id: testMessage1.id },\n        });\n        await Router.onMessage(msg);\n\n        // Expected object should have some properties of the original message it picked (testMessage1)\n        // plus the bundled content of the others that it picked of the same template (testMessage2)\n        const expectedObj = {\n          template: testMessage1.template,\n          provider: testMessage1.provider,\n          bundle: [\n            { content: testMessage1.content, id: testMessage1.id, order: 1 },\n            { content: testMessage2.content, id: testMessage2.id },\n          ],\n        };\n        assert.calledWith(\n          msg.target.sendAsyncMessage,\n          PARENT_TO_CHILD_MESSAGE_NAME,\n          { type: \"SET_BUNDLED_MESSAGES\", data: expectedObj }\n        );\n      });\n      it(\"should properly pick another message of the same template if it is bundled; force = false\", async () => {\n        // forcefully pick a message which needs to be bundled (the second message in FAKE_LOCAL_MESSAGES)\n        const [, testMessage1, testMessage2] = Router.state.messages;\n        const msg = fakeAsyncMessage({\n          type: \"OVERRIDE_MESSAGE\",\n          data: { id: testMessage1.id },\n        });\n        await Router.setMessageById(testMessage1.id, msg.target, false);\n\n        // Expected object should have some properties of the original message it picked (testMessage1)\n        // plus the bundled content of the others that it picked of the same template (testMessage2)\n        const expectedObj = {\n          template: testMessage1.template,\n          provider: testMessage1.provider,\n          bundle: [\n            { content: testMessage1.content, id: testMessage1.id, order: 1 },\n            {\n              content: testMessage2.content,\n              id: testMessage2.id,\n              order: 2,\n              blockOnClick: false,\n            },\n          ],\n        };\n        assert.calledWith(\n          msg.target.sendAsyncMessage,\n          PARENT_TO_CHILD_MESSAGE_NAME,\n          { type: \"SET_BUNDLED_MESSAGES\", data: expectedObj }\n        );\n      });\n      it(\"should get the bundle and send the message if the message has a bundle\", async () => {\n        sandbox.stub(Router, \"sendNewTabMessage\").resolves();\n        const msg = fakeAsyncMessage({ type: \"NEWTAB_MESSAGE_REQUEST\" });\n        msg.bundled = 2; // force this message to want to be bundled\n        await Router.onMessage(msg);\n        assert.calledOnce(Router.sendNewTabMessage);\n      });\n    });\n\n    describe(\"#onMessage: TRIGGER\", () => {\n      it(\"should pass the trigger to ASRouterTargeting on TRIGGER message\", async () => {\n        sandbox.stub(ASRouterTargeting, \"findMatchingMessage\").resolves();\n        const msg = fakeAsyncMessage({\n          type: \"TRIGGER\",\n          data: { trigger: { id: \"firstRun\" } },\n        });\n        await Router.onMessage(msg);\n\n        assert.calledOnce(ASRouterTargeting.findMatchingMessage);\n        assert.deepEqual(\n          ASRouterTargeting.findMatchingMessage.firstCall.args[0].trigger,\n          {\n            id: \"firstRun\",\n            param: undefined,\n            context: undefined,\n          }\n        );\n      });\n      it(\"should pick a message with the right targeting and trigger\", async () => {\n        let messages = [\n          {\n            id: \"foo1\",\n            template: \"simple_template\",\n            bundled: 2,\n            trigger: { id: \"foo\" },\n            content: { title: \"Foo1\", body: \"Foo123-1\" },\n          },\n          {\n            id: \"foo2\",\n            template: \"simple_template\",\n            bundled: 2,\n            trigger: { id: \"bar\" },\n            content: { title: \"Foo2\", body: \"Foo123-2\" },\n          },\n          {\n            id: \"foo3\",\n            template: \"simple_template\",\n            bundled: 2,\n            trigger: { id: \"foo\" },\n            content: { title: \"Foo3\", body: \"Foo123-3\" },\n          },\n        ];\n        sandbox.stub(Router, \"_findProvider\").returns(null);\n        await Router.setState({ messages });\n        const { target } = fakeAsyncMessage({\n          type: \"TRIGGER\",\n          data: { trigger: { id: \"foo\" } },\n        });\n        let { bundle } = await Router._getBundledMessages(messages[0], target, {\n          id: \"foo\",\n        });\n        assert.equal(bundle.length, 2);\n        // it should have picked foo1 and foo3 only\n        assert.isTrue(\n          bundle.every(elem => elem.id === \"foo1\" || elem.id === \"foo3\")\n        );\n      });\n      it(\"should have previousSessionEnd in the message context\", () => {\n        assert.propertyVal(\n          Router._getMessagesContext(),\n          \"previousSessionEnd\",\n          100\n        );\n      });\n    });\n\n    describe(\".includeBundle\", () => {\n      let msg;\n      beforeEach(async () => {\n        let messages = [\n          {\n            id: \"trailhead\",\n            template: \"trailhead\",\n            includeBundle: {\n              length: 3,\n              template: \"foo\",\n              trigger: { id: \"foo\" },\n            },\n            trigger: { id: \"firstRun\" },\n            content: {},\n          },\n          {\n            id: \"foo2\",\n            template: \"foo\",\n            bundled: 3,\n            order: 2,\n            trigger: { id: \"foo\" },\n            content: { title: \"Foo2\", body: \"Foo123-2\" },\n          },\n          {\n            id: \"foo3\",\n            template: \"foo\",\n            bundled: 3,\n            order: 3,\n            trigger: { id: \"foo\" },\n            content: { title: \"Foo3\", body: \"Foo123-3\" },\n          },\n          {\n            id: \"foo4\",\n            template: \"foo\",\n            bundled: 3,\n            order: 1,\n            trigger: { id: \"foo\" },\n            content: { title: \"Foo4\", body: \"Foo123-4\" },\n          },\n          {\n            id: \"foo5\",\n            template: \"foo\",\n            bundled: 3,\n            order: 4,\n            trigger: { id: \"foo\" },\n            content: { title: \"Foo5\", body: \"Foo123-5\" },\n          },\n        ];\n\n        sandbox.stub(Router, \"_findProvider\").returns(null);\n        await Router.setState({ messages });\n\n        msg = fakeAsyncMessage({\n          type: \"TRIGGER\",\n          data: { trigger: { id: \"firstRun\" } },\n        });\n      });\n\n      it(\"should send a message with .includeBundle property with specified length and template\", async () => {\n        await Router.onMessage(msg);\n        const [, resp] = msg.target.sendAsyncMessage.firstCall.args;\n        assert.propertyVal(resp, \"type\", \"SET_MESSAGE\");\n        assert.isArray(resp.data.bundle, \"resp.data.bundle\");\n        assert.lengthOf(resp.data.bundle, 3, \"resp.data.bundle\");\n      });\n\n      it(\"should set blockOnClick property by default false on returned ordered bundle messages\", async () => {\n        const expectedBundle = [\n          {\n            content: { title: \"Foo4\", body: \"Foo123-4\" },\n            id: \"foo4\",\n            order: 1,\n            blockOnClick: false,\n          },\n          {\n            content: { title: \"Foo2\", body: \"Foo123-2\" },\n            id: \"foo2\",\n            order: 2,\n            blockOnClick: false,\n          },\n          {\n            content: { title: \"Foo3\", body: \"Foo123-3\" },\n            id: \"foo3\",\n            order: 3,\n            blockOnClick: false,\n          },\n        ];\n\n        await Router.onMessage(msg);\n        const [, resp] = msg.target.sendAsyncMessage.firstCall.args;\n\n        for (let i = 0; i < 3; i++) {\n          assert.deepEqual(resp.data.bundle[i], expectedBundle[i]);\n        }\n      });\n\n      it(\"should set blockOnClick property true for dynamic triplet and matching messages more than 3\", async () => {\n        sandbox.replaceGetter(ASRouterPreferences, \"trailhead\", function() {\n          return {\n            trailheadInterrupt: \"join\",\n            trailheadTriplet: \"dynamic\",\n          };\n        });\n        await Router.onMessage(msg);\n        const [, resp] = msg.target.sendAsyncMessage.firstCall.args;\n        const expectedBundle = [\n          {\n            content: { title: \"Foo4\", body: \"Foo123-4\" },\n            id: \"foo4\",\n            order: 1,\n            blockOnClick: true,\n          },\n          {\n            content: { title: \"Foo2\", body: \"Foo123-2\" },\n            id: \"foo2\",\n            order: 2,\n            blockOnClick: true,\n          },\n          {\n            content: { title: \"Foo3\", body: \"Foo123-3\" },\n            id: \"foo3\",\n            order: 3,\n            blockOnClick: true,\n          },\n        ];\n\n        for (let i = 0; i < 3; i++) {\n          assert.deepEqual(resp.data.bundle[i], expectedBundle[i]);\n        }\n      });\n\n      it(\"should set blockOnClick property true for triplet branch name that starts with 'dynamic' and matching messages more than 3\", async () => {\n        sandbox.replaceGetter(ASRouterPreferences, \"trailhead\", function() {\n          return {\n            trailheadInterrupt: \"join\",\n            trailheadTriplet: \"dynamic_test\",\n          };\n        });\n        await Router.onMessage(msg);\n        const [, resp] = msg.target.sendAsyncMessage.firstCall.args;\n        const expectedBundle = [\n          {\n            content: { title: \"Foo4\", body: \"Foo123-4\" },\n            id: \"foo4\",\n            order: 1,\n            blockOnClick: true,\n          },\n          {\n            content: { title: \"Foo2\", body: \"Foo123-2\" },\n            id: \"foo2\",\n            order: 2,\n            blockOnClick: true,\n          },\n          {\n            content: { title: \"Foo3\", body: \"Foo123-3\" },\n            id: \"foo3\",\n            order: 3,\n            blockOnClick: true,\n          },\n        ];\n\n        for (let i = 0; i < 3; i++) {\n          assert.deepEqual(resp.data.bundle[i], expectedBundle[i]);\n        }\n      });\n    });\n\n    describe(\"#onMessage: OVERRIDE_MESSAGE\", () => {\n      it(\"should broadcast a SET_MESSAGE message to all clients with a particular id\", async () => {\n        const [testMessage] = Router.state.messages;\n        const msg = fakeAsyncMessage({\n          type: \"OVERRIDE_MESSAGE\",\n          data: { id: testMessage.id },\n        });\n        await Router.onMessage(msg);\n\n        assert.calledWith(\n          msg.target.sendAsyncMessage,\n          PARENT_TO_CHILD_MESSAGE_NAME,\n          { type: \"SET_MESSAGE\", data: testMessage }\n        );\n      });\n\n      it(\"should call CFRPageActions.forceRecommendation if the template is cfr_action and force is true\", async () => {\n        sandbox.stub(CFRPageActions, \"forceRecommendation\");\n        const testMessage = { id: \"foo\", template: \"cfr_doorhanger\" };\n        await Router.setState({ messages: [testMessage] });\n        const msg = fakeAsyncMessage({\n          type: \"OVERRIDE_MESSAGE\",\n          data: { id: testMessage.id },\n        });\n        await Router.onMessage(msg);\n\n        assert.notCalled(msg.target.sendAsyncMessage);\n        assert.calledOnce(CFRPageActions.forceRecommendation);\n      });\n\n      it(\"should call BookmarkPanelHub._forceShowMessage the provider is cfr-fxa\", async () => {\n        const testMessage = { id: \"foo\", template: \"fxa_bookmark_panel\" };\n        await Router.setState({ messages: [testMessage] });\n        const msg = fakeAsyncMessage({\n          type: \"OVERRIDE_MESSAGE\",\n          data: { id: testMessage.id },\n        });\n        await Router.onMessage(msg);\n\n        assert.notCalled(msg.target.sendAsyncMessage);\n        assert.calledOnce(FakeBookmarkPanelHub._forceShowMessage);\n      });\n\n      it(\"should call CFRPageActions.addRecommendation if the template is cfr_action and force is false\", async () => {\n        sandbox.stub(CFRPageActions, \"addRecommendation\");\n        const testMessage = { id: \"foo\", template: \"cfr_doorhanger\" };\n        await Router.setState({ messages: [testMessage] });\n        await Router._sendMessageToTarget(\n          testMessage,\n          {},\n          { param: {} },\n          false\n        );\n\n        assert.calledOnce(CFRPageActions.addRecommendation);\n      });\n\n      it(\"should broadcast CLEAR_ALL if provided id did not resolve to a message\", async () => {\n        const msg = fakeAsyncMessage({\n          type: \"OVERRIDE_MESSAGE\",\n          data: { id: -1 },\n        });\n        await Router.onMessage(msg);\n\n        assert.calledWith(\n          msg.target.sendAsyncMessage,\n          PARENT_TO_CHILD_MESSAGE_NAME,\n          { type: \"CLEAR_ALL\" }\n        );\n      });\n    });\n\n    describe(\"#onMessage: Onboarding actions\", () => {\n      it(\"should call OpenBrowserWindow with a private window on OPEN_PRIVATE_BROWSER_WINDOW\", async () => {\n        let [testMessage] = Router.state.messages;\n        const msg = fakeExecuteUserAction({\n          type: \"OPEN_PRIVATE_BROWSER_WINDOW\",\n          data: testMessage,\n        });\n        await Router.onMessage(msg);\n\n        assert.calledWith(msg.target.browser.ownerGlobal.OpenBrowserWindow, {\n          private: true,\n        });\n      });\n      it(\"should call openLinkIn with the correct params on OPEN_URL\", async () => {\n        let [testMessage] = Router.state.messages;\n        testMessage.button_action = {\n          type: \"OPEN_URL\",\n          data: { args: \"some/url.com\", where: \"tabshifted\" },\n        };\n        const msg = fakeExecuteUserAction(testMessage.button_action);\n        await Router.onMessage(msg);\n\n        assert.calledOnce(msg.target.browser.ownerGlobal.openLinkIn);\n        assert.calledWith(\n          msg.target.browser.ownerGlobal.openLinkIn,\n          \"some/url.com\",\n          \"tabshifted\",\n          { private: false, triggeringPrincipal: undefined, csp: null }\n        );\n      });\n      it(\"should call openLinkIn with the correct params on OPEN_ABOUT_PAGE\", async () => {\n        let [testMessage] = Router.state.messages;\n        testMessage.button_action = {\n          type: \"OPEN_ABOUT_PAGE\",\n          data: { args: \"something\" },\n        };\n        const msg = fakeExecuteUserAction(testMessage.button_action);\n        await Router.onMessage(msg);\n\n        assert.calledOnce(msg.target.browser.ownerGlobal.openTrustedLinkIn);\n        assert.calledWith(\n          msg.target.browser.ownerGlobal.openTrustedLinkIn,\n          \"about:something\",\n          \"tab\"\n        );\n      });\n      it(\"should call MigrationUtils.showMigrationWizard on SHOW_MIGRATION_WIZARD\", async () => {\n        let [testMessage] = Router.state.messages;\n        testMessage.button_action = {\n          type: \"SHOW_MIGRATION_WIZARD\",\n        };\n        const msg = fakeExecuteUserAction(testMessage.button_action);\n        globals.set(\"MigrationUtils\", {\n          showMigrationWizard: sandbox\n            .stub()\n            .withArgs(msg.target.browser.ownerGlobal, [\"test\"]),\n          MIGRATION_ENTRYPOINT_NEWTAB: \"test\",\n        });\n        await Router.onMessage(msg);\n\n        assert.calledOnce(MigrationUtils.showMigrationWizard);\n        assert.calledWith(\n          MigrationUtils.showMigrationWizard,\n          msg.target.browser.ownerGlobal,\n          [MigrationUtils.MIGRATION_ENTRYPOINT_NEWTAB]\n        );\n      });\n    });\n\n    describe(\"#onMessage: SHOW_FIREFOX_ACCOUNTS\", () => {\n      beforeEach(() => {\n        globals.set(\"FxAccounts\", {\n          config: {\n            promiseConnectAccountURI: sandbox.stub().resolves(\"some/url\"),\n          },\n        });\n      });\n      it(\"should call openLinkIn with the correct params on OPEN_URL\", async () => {\n        let [testMessage] = Router.state.messages;\n        testMessage.button_action = { type: \"SHOW_FIREFOX_ACCOUNTS\" };\n        const msg = fakeExecuteUserAction(testMessage.button_action);\n        await Router.onMessage(msg);\n\n        assert.calledOnce(msg.target.browser.ownerGlobal.openLinkIn);\n        assert.calledWith(\n          msg.target.browser.ownerGlobal.openLinkIn,\n          \"some/url\",\n          \"current\",\n          { private: false, triggeringPrincipal: undefined, csp: null }\n        );\n      });\n    });\n\n    describe(\"#onMessage: OPEN_PREFERENCES_PAGE\", () => {\n      it(\"should call openPreferences with the correct params on OPEN_PREFERENCES_PAGE\", async () => {\n        let [testMessage] = Router.state.messages;\n        testMessage.button_action = {\n          type: \"OPEN_PREFERENCES_PAGE\",\n          data: { category: \"something\" },\n        };\n        const msg = fakeExecuteUserAction(testMessage.button_action);\n        await Router.onMessage(msg);\n\n        assert.calledOnce(msg.target.browser.ownerGlobal.openPreferences);\n        assert.calledWith(\n          msg.target.browser.ownerGlobal.openPreferences,\n          \"something\"\n        );\n      });\n    });\n\n    describe(\"#onMessage: INSTALL_ADDON_FROM_URL\", () => {\n      it(\"should call installAddonFromURL with correct arguments\", async () => {\n        sandbox.stub(MessageLoaderUtils, \"installAddonFromURL\").resolves(null);\n        const msg = fakeExecuteUserAction({\n          type: \"INSTALL_ADDON_FROM_URL\",\n          data: { url: \"foo.com\", telemetrySource: \"foo\" },\n        });\n\n        await Router.onMessage(msg);\n\n        assert.calledOnce(MessageLoaderUtils.installAddonFromURL);\n        assert.calledWithExactly(\n          MessageLoaderUtils.installAddonFromURL,\n          msg.target.browser,\n          \"foo.com\",\n          \"foo\"\n        );\n      });\n\n      it(\"should add/remove observers for `webextension-install-notify`\", async () => {\n        sandbox.spy(global.Services.obs, \"addObserver\");\n        sandbox.spy(global.Services.obs, \"removeObserver\");\n\n        sandbox.stub(MessageLoaderUtils, \"installAddonFromURL\").resolves(null);\n        const msg = fakeExecuteUserAction({\n          type: \"INSTALL_ADDON_FROM_URL\",\n          data: { url: \"foo.com\" },\n        });\n\n        await Router.onMessage(msg);\n\n        assert.calledOnce(global.Services.obs.addObserver);\n\n        const [cb] = global.Services.obs.addObserver.firstCall.args;\n\n        cb();\n\n        assert.calledOnce(global.Services.obs.removeObserver);\n        assert.calledOnce(channel.sendAsyncMessage);\n      });\n    });\n\n    describe(\"#onMessage: PIN_CURRENT_TAB\", () => {\n      it(\"should call pin tab with the selectedTab\", async () => {\n        const msg = fakeExecuteUserAction({ type: \"PIN_CURRENT_TAB\" });\n        const { gBrowser, ConfirmationHint } = msg.target.browser.ownerGlobal;\n\n        await Router.onMessage(msg);\n\n        assert.calledOnce(gBrowser.pinTab);\n        assert.calledWithExactly(gBrowser.pinTab, gBrowser.selectedTab);\n        assert.calledOnce(ConfirmationHint.show);\n        assert.calledWithExactly(\n          ConfirmationHint.show,\n          gBrowser.selectedTab,\n          \"pinTab\",\n          { showDescription: true }\n        );\n      });\n    });\n\n    describe(\"#onMessage: OPEN_PROTECTION_PANEL\", () => {\n      it(\"should open protection panel\", async () => {\n        const msg = fakeExecuteUserAction({ type: \"OPEN_PROTECTION_PANEL\" });\n        let { gProtectionsHandler } = msg.target.browser.ownerGlobal;\n\n        await Router.onMessage(msg);\n\n        assert.calledOnce(gProtectionsHandler.showProtectionsPopup);\n        assert.calledWithExactly(gProtectionsHandler.showProtectionsPopup, {});\n      });\n    });\n\n    describe(\"#onMessage: OPEN_PROTECTION_REPORT\", () => {\n      it(\"should open protection report\", async () => {\n        const msg = fakeExecuteUserAction({ type: \"OPEN_PROTECTION_REPORT\" });\n        let { gProtectionsHandler } = msg.target.browser.ownerGlobal;\n\n        await Router.onMessage(msg);\n\n        assert.calledOnce(gProtectionsHandler.openProtections);\n      });\n    });\n\n    describe(\"#onMessage: DISABLE_STP_DOORHANGERS\", () => {\n      it(\"should block STP related messages\", async () => {\n        const msg = fakeExecuteUserAction({ type: \"DISABLE_STP_DOORHANGERS\" });\n\n        assert.deepEqual(Router.state.messageBlockList, []);\n\n        await Router.onMessage(msg);\n\n        assert.deepEqual(Router.state.messageBlockList, [\n          \"SOCIAL_TRACKING_PROTECTION\",\n          \"FINGERPRINTERS_PROTECTION\",\n          \"CRYPTOMINERS_PROTECTION\",\n        ]);\n      });\n    });\n\n    describe(\"#dispatch(action, target)\", () => {\n      it(\"should an action and target to onMessage\", async () => {\n        // use the IMPRESSION action to make sure actions are actually getting processed\n        sandbox.stub(Router, \"addImpression\");\n        sandbox.spy(Router, \"onMessage\");\n        const target = {};\n        const action = { type: \"IMPRESSION\" };\n\n        Router.dispatch(action, target);\n\n        assert.calledWith(Router.onMessage, { data: action, target });\n        assert.calledOnce(Router.addImpression);\n      });\n    });\n\n    describe(\"#onMessage: DOORHANGER_TELEMETRY\", () => {\n      it(\"should dispatch an AS_ROUTER_TELEMETRY_USER_EVENT on DOORHANGER_TELEMETRY message\", async () => {\n        const msg = fakeAsyncMessage({\n          type: \"DOORHANGER_TELEMETRY\",\n          data: { message_id: \"foo\" },\n        });\n        dispatchStub.reset();\n\n        await Router.onMessage(msg);\n\n        assert.calledOnce(dispatchStub);\n        const [action] = dispatchStub.firstCall.args;\n        assert.equal(action.type, \"AS_ROUTER_TELEMETRY_USER_EVENT\");\n        assert.equal(action.data.message_id, \"foo\");\n      });\n    });\n\n    describe(\"#onMessage: EXPIRE_QUERY_CACHE\", () => {\n      it(\"should clear all QueryCache getters\", async () => {\n        const msg = fakeAsyncMessage({ type: \"EXPIRE_QUERY_CACHE\" });\n        sandbox.stub(QueryCache, \"expireAll\");\n\n        await Router.onMessage(msg);\n\n        assert.calledOnce(QueryCache.expireAll);\n      });\n    });\n\n    describe(\"#onMessage: ENABLE_PROVIDER\", () => {\n      it(\"should enable the provider via ASRouterPreferences\", async () => {\n        const msg = fakeAsyncMessage({ type: \"ENABLE_PROVIDER\", data: \"foo\" });\n        sandbox.stub(ASRouterPreferences, \"enableOrDisableProvider\");\n\n        await Router.onMessage(msg);\n\n        assert.calledWith(\n          ASRouterPreferences.enableOrDisableProvider,\n          \"foo\",\n          true\n        );\n      });\n    });\n\n    describe(\"#onMessage: DISABLE_PROVIDER\", () => {\n      it(\"should disable the provider via ASRouterPreferences\", async () => {\n        const msg = fakeAsyncMessage({ type: \"DISABLE_PROVIDER\", data: \"foo\" });\n        sandbox.stub(ASRouterPreferences, \"enableOrDisableProvider\");\n\n        await Router.onMessage(msg);\n\n        assert.calledWith(\n          ASRouterPreferences.enableOrDisableProvider,\n          \"foo\",\n          false\n        );\n      });\n    });\n\n    describe(\"#onMessage: RESET_PROVIDER_PREF\", () => {\n      it(\"should reset provider pref via ASRouterPreferences\", async () => {\n        const msg = fakeAsyncMessage({\n          type: \"RESET_PROVIDER_PREF\",\n          data: \"foo\",\n        });\n        sandbox.stub(ASRouterPreferences, \"resetProviderPref\");\n\n        await Router.onMessage(msg);\n\n        assert.calledOnce(ASRouterPreferences.resetProviderPref);\n      });\n    });\n    describe(\"#onMessage: SET_PROVIDER_USER_PREF\", () => {\n      it(\"should set provider user pref via ASRouterPreferences\", async () => {\n        const msg = fakeAsyncMessage({\n          type: \"SET_PROVIDER_USER_PREF\",\n          data: { id: \"foo\", value: true },\n        });\n        sandbox.stub(ASRouterPreferences, \"setUserPreference\");\n\n        await Router.onMessage(msg);\n\n        assert.calledWith(ASRouterPreferences.setUserPreference, \"foo\", true);\n      });\n    });\n    describe(\"#onMessage: EVALUATE_JEXL_EXPRESSION\", () => {\n      it(\"should call evaluateExpression\", async () => {\n        const msg = fakeAsyncMessage({\n          type: \"EVALUATE_JEXL_EXPRESSION\",\n          data: { foo: true },\n        });\n        sandbox.stub(Router, \"evaluateExpression\");\n\n        await Router.onMessage(msg);\n\n        assert.calledOnce(Router.evaluateExpression);\n        assert.calledWithExactly(\n          Router.evaluateExpression,\n          msg.target,\n          msg.data.data\n        );\n      });\n    });\n    describe(\"#onMessage: FORCE_ATTRIBUTION\", () => {\n      beforeEach(() => {\n        global.Cc[\"@mozilla.org/mac-attribution;1\"] = {\n          getService: () => ({ setReferrerUrl: sinon.spy() }),\n        };\n        global.Cc[\"@mozilla.org/process/environment;1\"] = {\n          getService: () => ({ set: sandbox.stub() }),\n        };\n      });\n      afterEach(() => {\n        globals.restore();\n      });\n      it(\"should call forceAttribution\", async () => {\n        const msg = fakeAsyncMessage({\n          type: \"FORCE_ATTRIBUTION\",\n          data: { foo: true },\n        });\n        sandbox.stub(Router, \"forceAttribution\");\n\n        await Router.onMessage(msg);\n\n        assert.calledOnce(Router.forceAttribution);\n        assert.calledWithExactly(Router.forceAttribution, msg.data.data);\n      });\n      it(\"should force attribution and update providers\", async () => {\n        sandbox.stub(Router, \"_updateMessageProviders\");\n        sandbox.stub(Router, \"loadMessagesFromAllProviders\");\n        sandbox.stub(fakeAttributionCode, \"_clearCache\");\n        sandbox.stub(fakeAttributionCode, \"getAttrDataAsync\");\n        const msg = fakeAsyncMessage({\n          type: \"FORCE_ATTRIBUTION\",\n          data: { foo: true },\n        });\n        await Router.onMessage(msg);\n\n        assert.calledOnce(fakeAttributionCode._clearCache);\n        assert.calledOnce(fakeAttributionCode.getAttrDataAsync);\n        assert.calledOnce(Router._updateMessageProviders);\n        assert.calledOnce(Router.loadMessagesFromAllProviders);\n      });\n    });\n    describe(\"#onMessage: default\", () => {\n      it(\"should report unknown messages\", () => {\n        const msg = fakeAsyncMessage({ type: \"FOO\" });\n        sandbox.stub(Cu, \"reportError\");\n\n        Router.onMessage(msg);\n\n        assert.calledOnce(Cu.reportError);\n      });\n    });\n  });\n\n  describe(\"_triggerHandler\", () => {\n    it(\"should call #onMessage with the correct trigger\", () => {\n      const getter = sandbox.stub();\n      getter.returns(false);\n      sandbox.stub(global.BrowserHandler, \"kiosk\").get(getter);\n      sinon.spy(Router, \"onMessage\");\n      const target = {};\n      const trigger = { id: \"FAKE_TRIGGER\", param: \"some fake param\" };\n      Router._triggerHandler(target, trigger);\n      assert.calledOnce(Router.onMessage);\n      assert.calledWithExactly(Router.onMessage, {\n        target,\n        data: { type: \"TRIGGER\", data: { trigger } },\n      });\n    });\n  });\n\n  describe(\"_triggerHandler_kiosk\", () => {\n    it(\"should not call #onMessage\", () => {\n      const getter = sandbox.stub();\n      getter.returns(true);\n      sandbox.stub(global.BrowserHandler, \"kiosk\").get(getter);\n      sinon.spy(Router, \"onMessage\");\n      const target = {};\n      const trigger = { id: \"FAKE_TRIGGER\", param: \"some fake param\" };\n      Router._triggerHandler(target, trigger);\n      assert.notCalled(Router.onMessage);\n    });\n  });\n\n  describe(\"#UITour\", () => {\n    let showMenuStub;\n    const highlightTarget = { target: \"target\" };\n    beforeEach(() => {\n      showMenuStub = sandbox.stub();\n      globals.set(\"UITour\", {\n        showMenu: showMenuStub,\n        getTarget: sandbox\n          .stub()\n          .withArgs(sinon.match.object, \"pageAaction-sendToDevice\")\n          .resolves(highlightTarget),\n        showHighlight: sandbox.stub(),\n      });\n    });\n    it(\"should call UITour.showMenu with the correct params on OPEN_APPLICATIONS_MENU\", async () => {\n      const msg = fakeExecuteUserAction({\n        type: \"OPEN_APPLICATIONS_MENU\",\n        data: { args: \"appMenu\" },\n      });\n      await Router.onMessage(msg);\n\n      assert.calledOnce(showMenuStub);\n      assert.calledWith(\n        showMenuStub,\n        msg.target.browser.ownerGlobal,\n        \"appMenu\"\n      );\n    });\n    it(\"should call UITour.showHighlight with the correct params on HIGHLIGHT_FEATURE\", async () => {\n      const msg = fakeExecuteUserAction({\n        type: \"HIGHLIGHT_FEATURE\",\n        data: { args: \"pageAction-sendToDevice\" },\n      });\n      await Router.onMessage(msg);\n\n      assert.calledOnce(UITour.getTarget);\n      assert.calledOnce(UITour.showHighlight);\n      assert.calledWith(\n        UITour.showHighlight,\n        msg.target.browser.ownerGlobal,\n        highlightTarget\n      );\n    });\n  });\n\n  describe(\"valid preview endpoint\", () => {\n    it(\"should report an error if url protocol is not https\", () => {\n      sandbox.stub(Cu, \"reportError\");\n\n      assert.equal(false, Router._validPreviewEndpoint(\"http://foo.com\"));\n      assert.calledTwice(Cu.reportError);\n    });\n  });\n\n  describe(\"impressions\", () => {\n    async function addProviderWithFrequency(id, frequency) {\n      await Router.setState(state => {\n        const newProvider = { id, frequency };\n        const providers = [...state.providers, newProvider];\n        return { providers };\n      });\n    }\n\n    describe(\"frequency normalisation\", () => {\n      beforeEach(async () => {\n        const messages = [\n          { frequency: { custom: [{ period: \"daily\", cap: 10 }] } },\n        ];\n        const provider = {\n          id: \"foo\",\n          frequency: { custom: [{ period: \"daily\", cap: 100 }] },\n          messages,\n          enabled: true,\n        };\n        await createRouterAndInit([provider]);\n      });\n\n      it(\"period aliases in provider frequency caps should be normalised\", () => {\n        const [provider] = Router.state.providers;\n        assert.equal(provider.frequency.custom[0].period, ONE_DAY_IN_MS);\n      });\n      it(\"period aliases in message frequency caps should be normalised\", async () => {\n        const [message] = Router.state.messages;\n        assert.equal(message.frequency.custom[0].period, ONE_DAY_IN_MS);\n      });\n    });\n\n    describe(\"#addImpression\", () => {\n      it(\"should add a message impression and update _storage with the current time if the message has frequency caps\", async () => {\n        clock.tick(42);\n        const msg = fakeAsyncMessage({\n          type: \"IMPRESSION\",\n          data: {\n            id: \"foo\",\n            provider: FAKE_LOCAL_PROVIDER.id,\n            frequency: { lifetime: 5 },\n          },\n        });\n        await Router.onMessage(msg);\n        assert.isArray(Router.state.messageImpressions.foo);\n        assert.deepEqual(Router.state.messageImpressions.foo, [42]);\n        assert.calledWith(Router._storage.set, \"messageImpressions\", {\n          foo: [42],\n        });\n      });\n      it(\"should not add a message impression if the message doesn't have frequency caps\", async () => {\n        // Note that storage.set is called during initialization, so it needs to be reset\n        Router._storage.set.reset();\n        clock.tick(42);\n        const msg = fakeAsyncMessage({\n          type: \"IMPRESSION\",\n          data: { id: \"foo\" },\n        });\n        await Router.onMessage(msg);\n        assert.notProperty(Router.state.messageImpressions, \"foo\");\n        assert.notCalled(Router._storage.set);\n      });\n      it(\"should add a provider impression and update _storage with the current time if the message's provider has frequency caps\", async () => {\n        clock.tick(42);\n        await addProviderWithFrequency(\"foo\", { lifetime: 5 });\n        const msg = fakeAsyncMessage({\n          type: \"IMPRESSION\",\n          data: { id: \"bar\", provider: \"foo\" },\n        });\n        await Router.onMessage(msg);\n        assert.isArray(Router.state.providerImpressions.foo);\n        assert.deepEqual(Router.state.providerImpressions.foo, [42]);\n        assert.calledWith(Router._storage.set, \"providerImpressions\", {\n          foo: [42],\n        });\n      });\n      it(\"should not add a provider impression if the message's provider doesn't have frequency caps\", async () => {\n        // Note that storage.set is called during initialization, so it needs to be reset\n        Router._storage.set.reset();\n        clock.tick(42);\n        // Add \"foo\" provider with no frequency\n        await addProviderWithFrequency(\"foo\", null);\n        const msg = fakeAsyncMessage({\n          type: \"IMPRESSION\",\n          data: { id: \"bar\", provider: \"foo\" },\n        });\n        await Router.onMessage(msg);\n        assert.notProperty(Router.state.providerImpressions, \"foo\");\n        assert.notCalled(Router._storage.set);\n      });\n      it(\"should only send impressions for one message\", async () => {\n        const getElementById = sandbox.stub().returns({\n          setAttribute: sandbox.stub(),\n          style: { setProperty: sandbox.stub() },\n          addEventListener: sandbox.stub(),\n        });\n        const data = {\n          param: { host: \"mozilla.com\", url: \"https://mozilla.com\" },\n        };\n        const target = {\n          sendAsyncMessage: sandbox.stub(),\n          documentURI: { scheme: \"https\", host: \"mozilla.com\" },\n        };\n        target.ownerGlobal = {\n          gBrowser: { selectedBrowser: target },\n          document: { getElementById },\n          promiseDocumentFlushed: sandbox.stub().resolves([{ width: 0 }]),\n          setTimeout: sandbox.stub(),\n          A11yUtils: { announce: sandbox.stub() },\n        };\n        const firstMessage = { ...FAKE_RECOMMENDATION, id: \"first_message\" };\n        const secondMessage = { ...FAKE_RECOMMENDATION, id: \"second_message\" };\n        await Router.setState({ messages: [firstMessage, secondMessage] });\n        global.DOMLocalization = class DOMLocalization {};\n        sandbox.spy(CFRPageActions, \"addRecommendation\");\n        sandbox.stub(Router, \"addImpression\").resolves();\n\n        await Router.setMessageById(\"first_message\", target, false, { data });\n        await Router.setMessageById(\"second_message\", target, false, { data });\n\n        assert.calledTwice(CFRPageActions.addRecommendation);\n        const [\n          firstReturn,\n          secondReturn,\n        ] = CFRPageActions.addRecommendation.returnValues;\n        assert.isTrue(await firstReturn);\n        // Adding the second message should fail.\n        assert.isFalse(await secondReturn);\n        assert.calledOnce(Router.addImpression);\n      });\n    });\n\n    describe(\"#isBelowFrequencyCaps\", () => {\n      it(\"should call #_isBelowItemFrequencyCap for the message and for the provider with the correct impressions and arguments\", async () => {\n        sinon.spy(Router, \"_isBelowItemFrequencyCap\");\n\n        const MAX_MESSAGE_LIFETIME_CAP = 100; // Defined in ASRouter\n        const fooMessageImpressions = [0, 1];\n        const barProviderImpressions = [0, 1, 2];\n\n        const message = {\n          id: \"foo\",\n          provider: \"bar\",\n          frequency: { lifetime: 3 },\n        };\n        const provider = { id: \"bar\", frequency: { lifetime: 5 } };\n\n        await Router.setState(state => {\n          // Add provider\n          const providers = [...state.providers, provider];\n          // Add fooMessageImpressions\n          // eslint-disable-next-line no-shadow\n          const messageImpressions = Object.assign(\n            {},\n            state.messageImpressions\n          );\n          messageImpressions.foo = fooMessageImpressions;\n          // Add barProviderImpressions\n          // eslint-disable-next-line no-shadow\n          const providerImpressions = Object.assign(\n            {},\n            state.providerImpressions\n          );\n          providerImpressions.bar = barProviderImpressions;\n          return { providers, messageImpressions, providerImpressions };\n        });\n\n        await Router.isBelowFrequencyCaps(message);\n\n        assert.calledTwice(Router._isBelowItemFrequencyCap);\n        assert.calledWithExactly(\n          Router._isBelowItemFrequencyCap,\n          message,\n          fooMessageImpressions,\n          MAX_MESSAGE_LIFETIME_CAP\n        );\n        assert.calledWithExactly(\n          Router._isBelowItemFrequencyCap,\n          provider,\n          barProviderImpressions\n        );\n      });\n    });\n\n    describe(\"#_isBelowItemFrequencyCap\", () => {\n      it(\"should return false if the # of impressions exceeds the maxLifetimeCap\", () => {\n        const item = { id: \"foo\", frequency: { lifetime: 5 } };\n        const impressions = [0, 1];\n        const maxLifetimeCap = 1;\n        const result = Router._isBelowItemFrequencyCap(\n          item,\n          impressions,\n          maxLifetimeCap\n        );\n        assert.isFalse(result);\n      });\n\n      describe(\"lifetime frequency caps\", () => {\n        it(\"should return true if .frequency is not defined on the item\", () => {\n          const item = { id: \"foo\" };\n          const impressions = [0, 1];\n          const result = Router._isBelowItemFrequencyCap(item, impressions);\n          assert.isTrue(result);\n        });\n        it(\"should return true if there are no impressions\", () => {\n          const item = {\n            id: \"foo\",\n            frequency: {\n              lifetime: 10,\n              custom: [{ period: ONE_DAY_IN_MS, cap: 2 }],\n            },\n          };\n          const impressions = [];\n          const result = Router._isBelowItemFrequencyCap(item, impressions);\n          assert.isTrue(result);\n        });\n        it(\"should return true if the # of impressions is less than .frequency.lifetime of the item\", () => {\n          const item = { id: \"foo\", frequency: { lifetime: 3 } };\n          const impressions = [0, 1];\n          const result = Router._isBelowItemFrequencyCap(item, impressions);\n          assert.isTrue(result);\n        });\n        it(\"should return false if the # of impressions is equal to .frequency.lifetime of the item\", async () => {\n          const item = { id: \"foo\", frequency: { lifetime: 3 } };\n          const impressions = [0, 1, 2];\n          const result = Router._isBelowItemFrequencyCap(item, impressions);\n          assert.isFalse(result);\n        });\n        it(\"should return false if the # of impressions is greater than .frequency.lifetime of the item\", async () => {\n          const item = { id: \"foo\", frequency: { lifetime: 3 } };\n          const impressions = [0, 1, 2, 3];\n          const result = Router._isBelowItemFrequencyCap(item, impressions);\n          assert.isFalse(result);\n        });\n      });\n\n      describe(\"custom frequency caps\", () => {\n        it(\"should return true if impressions in the time period < the cap and total impressions < the lifetime cap\", () => {\n          clock.tick(ONE_DAY_IN_MS + 10);\n          const item = {\n            id: \"foo\",\n            frequency: {\n              custom: [{ period: ONE_DAY_IN_MS, cap: 2 }],\n              lifetime: 3,\n            },\n          };\n          const impressions = [0, ONE_DAY_IN_MS + 1];\n          const result = Router._isBelowItemFrequencyCap(item, impressions);\n          assert.isTrue(result);\n        });\n        it(\"should return false if impressions in the time period > the cap and total impressions < the lifetime cap\", () => {\n          clock.tick(200);\n          const item = {\n            id: \"msg1\",\n            frequency: { custom: [{ period: 100, cap: 2 }], lifetime: 3 },\n          };\n          const impressions = [0, 160, 161];\n          const result = Router._isBelowItemFrequencyCap(item, impressions);\n          assert.isFalse(result);\n        });\n        it(\"should return false if impressions in one of the time periods > the cap and total impressions < the lifetime cap\", () => {\n          clock.tick(ONE_DAY_IN_MS + 200);\n          const itemTrue = {\n            id: \"msg2\",\n            frequency: { custom: [{ period: 100, cap: 2 }] },\n          };\n          const itemFalse = {\n            id: \"msg1\",\n            frequency: {\n              custom: [\n                { period: 100, cap: 2 },\n                { period: ONE_DAY_IN_MS, cap: 3 },\n              ],\n            },\n          };\n          const impressions = [\n            0,\n            ONE_DAY_IN_MS + 160,\n            ONE_DAY_IN_MS - 100,\n            ONE_DAY_IN_MS - 200,\n          ];\n          assert.isTrue(Router._isBelowItemFrequencyCap(itemTrue, impressions));\n          assert.isFalse(\n            Router._isBelowItemFrequencyCap(itemFalse, impressions)\n          );\n        });\n        it(\"should return false if impressions in the time period < the cap and total impressions > the lifetime cap\", () => {\n          clock.tick(ONE_DAY_IN_MS + 10);\n          const item = {\n            id: \"msg1\",\n            frequency: {\n              custom: [{ period: ONE_DAY_IN_MS, cap: 2 }],\n              lifetime: 3,\n            },\n          };\n          const impressions = [0, 1, 2, 3, ONE_DAY_IN_MS + 1];\n          const result = Router._isBelowItemFrequencyCap(item, impressions);\n          assert.isFalse(result);\n        });\n        it(\"should return true if daily impressions < the daily cap and there is no lifetime cap\", () => {\n          clock.tick(ONE_DAY_IN_MS + 10);\n          const item = {\n            id: \"msg1\",\n            frequency: { custom: [{ period: ONE_DAY_IN_MS, cap: 2 }] },\n          };\n          const impressions = [0, 1, 2, 3, ONE_DAY_IN_MS + 1];\n          const result = Router._isBelowItemFrequencyCap(item, impressions);\n          assert.isTrue(result);\n        });\n        it(\"should return false if daily impressions > the daily cap and there is no lifetime cap\", () => {\n          clock.tick(ONE_DAY_IN_MS + 10);\n          const item = {\n            id: \"msg1\",\n            frequency: { custom: [{ period: ONE_DAY_IN_MS, cap: 2 }] },\n          };\n          const impressions = [\n            0,\n            1,\n            2,\n            3,\n            ONE_DAY_IN_MS + 1,\n            ONE_DAY_IN_MS + 2,\n            ONE_DAY_IN_MS + 3,\n          ];\n          const result = Router._isBelowItemFrequencyCap(item, impressions);\n          assert.isFalse(result);\n        });\n      });\n    });\n\n    describe(\"#getLongestPeriod\", () => {\n      it(\"should return the period if there is only one definition\", () => {\n        const message = {\n          id: \"foo\",\n          frequency: { custom: [{ period: 200, cap: 2 }] },\n        };\n        assert.equal(Router.getLongestPeriod(message), 200);\n      });\n      it(\"should return the longest period if there are more than one definitions\", () => {\n        const message = {\n          id: \"foo\",\n          frequency: {\n            custom: [\n              { period: 1000, cap: 3 },\n              { period: ONE_DAY_IN_MS, cap: 5 },\n              { period: 100, cap: 2 },\n            ],\n          },\n        };\n        assert.equal(Router.getLongestPeriod(message), ONE_DAY_IN_MS);\n      });\n      it(\"should return null if there are is no .frequency\", () => {\n        const message = { id: \"foo\" };\n        assert.isNull(Router.getLongestPeriod(message));\n      });\n      it(\"should return null if there are is no .frequency.custom\", () => {\n        const message = { id: \"foo\", frequency: { lifetime: 10 } };\n        assert.isNull(Router.getLongestPeriod(message));\n      });\n    });\n\n    describe(\"cleanup on init\", () => {\n      it(\"should clear messageImpressions for messages which do not exist in state.messages\", async () => {\n        const messages = [{ id: \"foo\", frequency: { lifetime: 10 } }];\n        messageImpressions = { foo: [0], bar: [0, 1] };\n        // Impressions for \"bar\" should be removed since that id does not exist in messages\n        const result = { foo: [0] };\n\n        await createRouterAndInit([\n          { id: \"onboarding\", type: \"local\", messages, enabled: true },\n        ]);\n        assert.calledWith(Router._storage.set, \"messageImpressions\", result);\n        assert.deepEqual(Router.state.messageImpressions, result);\n      });\n      it(\"should clear messageImpressions older than the period if no lifetime impression cap is included\", async () => {\n        const CURRENT_TIME = ONE_DAY_IN_MS * 2;\n        clock.tick(CURRENT_TIME);\n        const messages = [\n          {\n            id: \"foo\",\n            frequency: { custom: [{ period: ONE_DAY_IN_MS, cap: 5 }] },\n          },\n        ];\n        messageImpressions = { foo: [0, 1, CURRENT_TIME - 10] };\n        // Only 0 and 1 are more than 24 hours before CURRENT_TIME\n        const result = { foo: [CURRENT_TIME - 10] };\n\n        await createRouterAndInit([\n          { id: \"onboarding\", type: \"local\", messages, enabled: true },\n        ]);\n        assert.calledWith(Router._storage.set, \"messageImpressions\", result);\n        assert.deepEqual(Router.state.messageImpressions, result);\n      });\n      it(\"should clear messageImpressions older than the longest period if no lifetime impression cap is included\", async () => {\n        const CURRENT_TIME = ONE_DAY_IN_MS * 2;\n        clock.tick(CURRENT_TIME);\n        const messages = [\n          {\n            id: \"foo\",\n            frequency: {\n              custom: [\n                { period: ONE_DAY_IN_MS, cap: 5 },\n                { period: 100, cap: 2 },\n              ],\n            },\n          },\n        ];\n        messageImpressions = { foo: [0, 1, CURRENT_TIME - 10] };\n        // Only 0 and 1 are more than 24 hours before CURRENT_TIME\n        const result = { foo: [CURRENT_TIME - 10] };\n\n        await createRouterAndInit([\n          { id: \"onboarding\", type: \"local\", messages, enabled: true },\n        ]);\n        assert.calledWith(Router._storage.set, \"messageImpressions\", result);\n        assert.deepEqual(Router.state.messageImpressions, result);\n      });\n      it(\"should clear messageImpressions if they are not properly formatted\", async () => {\n        const messages = [{ id: \"foo\", frequency: { lifetime: 10 } }];\n        // this is impromperly formatted since messageImpressions are supposed to be an array\n        messageImpressions = { foo: 0 };\n        const result = {};\n\n        await createRouterAndInit([\n          { id: \"onboarding\", type: \"local\", messages, enabled: true },\n        ]);\n        assert.calledWith(Router._storage.set, \"messageImpressions\", result);\n        assert.deepEqual(Router.state.messageImpressions, result);\n      });\n      it(\"should not clear messageImpressions for messages which do exist in state.messages\", async () => {\n        const messages = [\n          { id: \"foo\", frequency: { lifetime: 10 } },\n          { id: \"bar\", frequency: { lifetime: 10 } },\n        ];\n        messageImpressions = { foo: [0], bar: [] };\n\n        await createRouterAndInit([\n          { id: \"onboarding\", type: \"local\", messages, enabled: true },\n        ]);\n        assert.notCalled(Router._storage.set);\n        assert.deepEqual(Router.state.messageImpressions, messageImpressions);\n      });\n    });\n  });\n\n  describe(\"handle targeting errors\", () => {\n    it(\"should dispatch an event when a targeting expression throws an error\", async () => {\n      sandbox\n        .stub(global.FilterExpressions, \"eval\")\n        .returns(Promise.reject(new Error(\"fake error\")));\n      await Router.setState({\n        messages: [{ id: \"foo\", provider: \"snippets\", targeting: \"foo2.[[(\" }],\n      });\n      const msg = fakeAsyncMessage({ type: \"NEWTAB_MESSAGE_REQUEST\" });\n      dispatchStub.reset();\n\n      await Router.onMessage(msg);\n\n      assert.calledOnce(dispatchStub);\n      const [action] = dispatchStub.firstCall.args;\n      assert.equal(action.type, \"AS_ROUTER_TELEMETRY_USER_EVENT\");\n      assert.equal(action.data.message_id, \"foo\");\n    });\n  });\n\n  describe(\"#_onLocaleChanged\", () => {\n    it(\"should call _maybeUpdateL10nAttachment in the handler\", async () => {\n      sandbox.spy(Router, \"_maybeUpdateL10nAttachment\");\n      await Router._onLocaleChanged();\n\n      assert.calledOnce(Router._maybeUpdateL10nAttachment);\n    });\n  });\n\n  describe(\"#_maybeUpdateL10nAttachment\", () => {\n    it(\"should update the l10n attachment if the locale was changed\", async () => {\n      const getter = sandbox.stub();\n      getter.onFirstCall().returns(\"en-US\");\n      getter.onSecondCall().returns(\"fr\");\n      sandbox.stub(global.Services.locale, \"appLocaleAsLangTag\").get(getter);\n      const provider = {\n        id: \"cfr\",\n        enabled: true,\n        type: \"remote-settings\",\n        bucket: \"cfr\",\n      };\n      await createRouterAndInit([provider]);\n      sandbox.spy(Router, \"setState\");\n      sandbox.spy(Router, \"loadMessagesFromAllProviders\");\n\n      await Router._maybeUpdateL10nAttachment();\n\n      assert.calledWith(Router.setState, {\n        localeInUse: \"fr\",\n        providers: [\n          {\n            id: \"cfr\",\n            enabled: true,\n            type: \"remote-settings\",\n            bucket: \"cfr\",\n            lastUpdated: undefined,\n            errors: [],\n          },\n        ],\n      });\n      assert.calledOnce(Router.loadMessagesFromAllProviders);\n    });\n    it(\"should not update the l10n attachment if the provider doesn't need l10n attachment\", async () => {\n      const getter = sandbox.stub();\n      getter.onFirstCall().returns(\"en-US\");\n      getter.onSecondCall().returns(\"fr\");\n      sandbox.stub(global.Services.locale, \"appLocaleAsLangTag\").get(getter);\n      const provider = {\n        id: \"localProvider\",\n        enabled: true,\n        type: \"local\",\n      };\n      await createRouterAndInit([provider]);\n      sandbox.spy(Router, \"setState\");\n      sandbox.spy(Router, \"loadMessagesFromAllProviders\");\n\n      await Router._maybeUpdateL10nAttachment();\n\n      assert.notCalled(Router.setState);\n      assert.notCalled(Router.loadMessagesFromAllProviders);\n    });\n  });\n\n  describe(\"#observe\", () => {\n    it(\"should reload l10n for CFRPageActions when the `USE_REMOTE_L10N_PREF` pref is changed\", () => {\n      sandbox.spy(CFRPageActions, \"reloadL10n\");\n\n      Router.observe(\"\", \"\", USE_REMOTE_L10N_PREF);\n\n      assert.calledOnce(CFRPageActions.reloadL10n);\n    });\n    it(\"should not react to other pref changes\", () => {\n      sandbox.spy(CFRPageActions, \"reloadL10n\");\n\n      Router.observe(\"\", \"\", \"foo\");\n\n      assert.notCalled(CFRPageActions.reloadL10n);\n    });\n  });\n\n  describe(\"#sendAsyncMessageToPreloaded\", () => {\n    it(\"should send the message to the preloaded browser if there's data and a preloaded browser exists\", () => {\n      const port = {\n        browser: {\n          getAttribute() {\n            return \"preloaded\";\n          },\n        },\n        sendAsyncMessage: sinon.spy(),\n      };\n      Router.messageChannel.messagePorts.push(port);\n\n      const action = { type: \"FOO\" };\n      Router.sendAsyncMessageToPreloaded(action);\n      assert.calledWith(port.sendAsyncMessage, OUTGOING_MESSAGE_NAME, action);\n    });\n    it(\"should send the message to all the preloaded browsers if there's data and they exist\", () => {\n      const port = {\n        browser: {\n          getAttribute() {\n            return \"preloaded\";\n          },\n        },\n        sendAsyncMessage: sinon.spy(),\n      };\n      Router.messageChannel.messagePorts.push(port);\n      Router.messageChannel.messagePorts.push(port);\n      Router.sendAsyncMessageToPreloaded({ type: \"FOO\" });\n      assert.calledTwice(port.sendAsyncMessage);\n    });\n    it(\"should not send the message to the preloaded browser if there's no data and a preloaded browser does not exists\", () => {\n      const port = {\n        browser: {\n          getAttribute() {\n            return \"consumed\";\n          },\n        },\n        sendAsyncMessage: sinon.spy(),\n      };\n      Router.messageChannel.messagePorts.push(port);\n      Router.sendAsyncMessageToPreloaded({ type: \"FOO\" });\n      assert.notCalled(port.sendAsyncMessage);\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/asrouter/ASRouterFeed.test.js",
    "content": "import { _ASRouter, ASRouter } from \"lib/ASRouter.jsm\";\nimport { FAKE_LOCAL_PROVIDER, FakeRemotePageManager } from \"./constants\";\nimport { ASRouterFeed } from \"lib/ASRouterFeed.jsm\";\nimport { actionTypes as at } from \"common/Actions.jsm\";\nimport { GlobalOverrider } from \"test/unit/utils\";\nimport { ASRouterPreferences } from \"lib/ASRouterPreferences.jsm\";\n\ndescribe(\"ASRouterFeed\", () => {\n  let Router;\n  let feed;\n  let channel;\n  let sandbox;\n  let storage;\n  let globals;\n  let FakeBookmarkPanelHub;\n  let FakeToolbarBadgeHub;\n  let FakeToolbarPanelHub;\n  beforeEach(() => {\n    sandbox = sinon.createSandbox();\n    globals = new GlobalOverrider();\n    FakeBookmarkPanelHub = {\n      init: sandbox.stub(),\n      uninit: sandbox.stub(),\n    };\n    FakeToolbarBadgeHub = {\n      init: sandbox.stub(),\n    };\n    FakeToolbarPanelHub = {\n      init: sandbox.stub(),\n      uninit: sandbox.stub(),\n    };\n    globals.set({\n      ASRouterPreferences,\n      BookmarkPanelHub: FakeBookmarkPanelHub,\n      ToolbarBadgeHub: FakeToolbarBadgeHub,\n      ToolbarPanelHub: FakeToolbarPanelHub,\n    });\n\n    Router = new _ASRouter({ providers: [FAKE_LOCAL_PROVIDER] });\n    FakeToolbarPanelHub = {\n      init: sandbox.stub(),\n      uninit: sandbox.stub(),\n    };\n\n    storage = {\n      get: sandbox.stub().returns(Promise.resolve([])),\n      set: sandbox.stub().returns(Promise.resolve()),\n    };\n    feed = new ASRouterFeed({ router: Router }, storage);\n    channel = new FakeRemotePageManager();\n    feed.store = {\n      _messageChannel: { channel },\n      getState: () => ({}),\n      dbStorage: { getDbTable: sandbox.stub().returns({}) },\n    };\n  });\n  afterEach(() => {\n    sandbox.restore();\n  });\n  it(\"should set .router to the ASRouter singleton if none is specified in options\", () => {\n    feed = new ASRouterFeed();\n    assert.equal(feed.router, ASRouter);\n\n    feed = new ASRouterFeed({});\n    assert.equal(feed.router, ASRouter);\n  });\n  describe(\"#onAction: INIT\", () => {\n    it(\"should initialize the ASRouter if it is not initialized\", () => {\n      sandbox.stub(feed, \"enable\");\n\n      feed.onAction({ type: at.INIT });\n\n      assert.calledOnce(feed.enable);\n    });\n    it(\"should initialize ASRouter\", async () => {\n      sandbox.stub(Router, \"init\").returns(Promise.resolve());\n\n      await feed.enable();\n\n      assert.calledWith(Router.init, channel);\n      assert.calledOnce(feed.store.dbStorage.getDbTable);\n      assert.calledWithExactly(feed.store.dbStorage.getDbTable, \"snippets\");\n    });\n    it(\"should not re-initialize the ASRouter if it is already initialized\", async () => {\n      // Router starts initialized\n      await Router.init(new FakeRemotePageManager(), storage, () => {});\n      sinon.stub(Router, \"init\");\n\n      // call .onAction with INIT\n      feed.onAction({ type: at.INIT });\n\n      assert.notCalled(Router.init);\n    });\n  });\n  describe(\"#onAction: UNINIT\", () => {\n    it(\"should uninitialize the ASRouter\", async () => {\n      await Router.init(new FakeRemotePageManager(), storage, () => {});\n      sinon.stub(Router, \"uninit\");\n\n      feed.onAction({ type: at.UNINIT });\n\n      assert.calledOnce(Router.uninit);\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/asrouter/ASRouterPreferences.test.js",
    "content": "import {\n  _ASRouterPreferences,\n  ASRouterPreferences as ASRouterPreferencesSingleton,\n  getTrailheadConfigFromPref,\n  TEST_PROVIDERS,\n} from \"lib/ASRouterPreferences.jsm\";\nconst FAKE_PROVIDERS = [{ id: \"foo\" }, { id: \"bar\" }];\n\nconst PROVIDER_PREF_BRANCH =\n  \"browser.newtabpage.activity-stream.asrouter.providers.\";\nconst DEVTOOLS_PREF =\n  \"browser.newtabpage.activity-stream.asrouter.devtoolsEnabled\";\nconst SNIPPETS_USER_PREF = \"browser.newtabpage.activity-stream.feeds.snippets\";\nconst CFR_USER_PREF_ADDONS =\n  \"browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons\";\nconst CFR_USER_PREF_FEATURES =\n  \"browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features\";\n\n/** NUMBER_OF_PREFS_TO_OBSERVE includes:\n *  1. asrouter.providers. pref branch\n *  2. asrouter.devtoolsEnabled\n *  3. browser.newtabpage.activity-stream.feeds.snippets (user preference - snippets)\n *  4. browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons (user preference - cfr)\n *  4. browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features (user preference - cfr)\n *  5. services.sync.username\n */\nconst NUMBER_OF_PREFS_TO_OBSERVE = 6;\n\ndescribe(\"ASRouterPreferences\", () => {\n  let ASRouterPreferences;\n  let sandbox;\n  let addObserverStub;\n  let stringPrefStub;\n  let boolPrefStub;\n\n  beforeEach(() => {\n    ASRouterPreferences = new _ASRouterPreferences();\n\n    sandbox = sinon.createSandbox();\n    addObserverStub = sandbox.stub(global.Services.prefs, \"addObserver\");\n    stringPrefStub = sandbox.stub(global.Services.prefs, \"getStringPref\");\n    FAKE_PROVIDERS.forEach(provider => {\n      stringPrefStub\n        .withArgs(`${PROVIDER_PREF_BRANCH}${provider.id}`)\n        .returns(JSON.stringify(provider));\n    });\n    sandbox\n      .stub(global.Services.prefs, \"getChildList\")\n      .withArgs(PROVIDER_PREF_BRANCH)\n      .returns(\n        FAKE_PROVIDERS.map(provider => `${PROVIDER_PREF_BRANCH}${provider.id}`)\n      );\n\n    boolPrefStub = sandbox\n      .stub(global.Services.prefs, \"getBoolPref\")\n      .returns(false);\n  });\n\n  afterEach(() => {\n    sandbox.restore();\n  });\n\n  function getPrefNameForProvider(providerId) {\n    return `${PROVIDER_PREF_BRANCH}${providerId}`;\n  }\n\n  function setPrefForProvider(providerId, value) {\n    stringPrefStub\n      .withArgs(getPrefNameForProvider(providerId))\n      .returns(JSON.stringify(value));\n  }\n\n  it(\"ASRouterPreferences should be an instance of _ASRouterPreferences\", () => {\n    assert.instanceOf(ASRouterPreferencesSingleton, _ASRouterPreferences);\n  });\n  describe(\"#init\", () => {\n    it(\"should set ._initialized to true\", () => {\n      ASRouterPreferences.init();\n      assert.isTrue(ASRouterPreferences._initialized);\n    });\n    it(`should set ${NUMBER_OF_PREFS_TO_OBSERVE} observers and not re-initialize if already initialized`, () => {\n      ASRouterPreferences.init();\n      assert.callCount(addObserverStub, NUMBER_OF_PREFS_TO_OBSERVE);\n      ASRouterPreferences.init();\n      ASRouterPreferences.init();\n      assert.callCount(addObserverStub, NUMBER_OF_PREFS_TO_OBSERVE);\n    });\n  });\n  describe(\"#uninit\", () => {\n    it(\"should set ._initialized to false\", () => {\n      ASRouterPreferences.init();\n      ASRouterPreferences.uninit();\n      assert.isFalse(ASRouterPreferences._initialized);\n    });\n    it(\"should clear cached values for ._initialized, .devtoolsEnabled\", () => {\n      ASRouterPreferences.init();\n      // trigger caching\n      // eslint-disable-next-line no-unused-vars\n      const result = [\n        ASRouterPreferences.providers,\n        ASRouterPreferences.devtoolsEnabled,\n      ];\n      assert.isNotNull(\n        ASRouterPreferences._providers,\n        \"providers should not be null\"\n      );\n      assert.isNotNull(\n        ASRouterPreferences._devtoolsEnabled,\n        \"devtolosEnabled should not be null\"\n      );\n\n      ASRouterPreferences.uninit();\n      assert.isNull(ASRouterPreferences._providers);\n      assert.isNull(ASRouterPreferences._devtoolsEnabled);\n    });\n    it(\"should clear all listeners and remove observers (only once)\", () => {\n      const removeStub = sandbox.stub(global.Services.prefs, \"removeObserver\");\n      ASRouterPreferences.init();\n      ASRouterPreferences.addListener(() => {});\n      ASRouterPreferences.addListener(() => {});\n      assert.equal(ASRouterPreferences._callbacks.size, 2);\n      ASRouterPreferences.uninit();\n      // Tests to make sure we don't remove observers that weren't set\n      ASRouterPreferences.uninit();\n\n      assert.callCount(removeStub, NUMBER_OF_PREFS_TO_OBSERVE);\n      assert.calledWith(removeStub, PROVIDER_PREF_BRANCH);\n      assert.calledWith(removeStub, DEVTOOLS_PREF);\n      assert.isEmpty(ASRouterPreferences._callbacks);\n    });\n  });\n  describe(\".providers\", () => {\n    it(\"should return the value the first time .providers is accessed\", () => {\n      ASRouterPreferences.init();\n\n      const result = ASRouterPreferences.providers;\n      assert.deepEqual(result, FAKE_PROVIDERS);\n      // once per pref\n      assert.calledTwice(stringPrefStub);\n    });\n    it(\"should return the cached value the second time .providers is accessed\", () => {\n      ASRouterPreferences.init();\n      const [, secondCall] = [\n        ASRouterPreferences.providers,\n        ASRouterPreferences.providers,\n      ];\n\n      assert.deepEqual(secondCall, FAKE_PROVIDERS);\n      // once per pref\n      assert.calledTwice(stringPrefStub);\n    });\n    it(\"should just parse the pref each time if ASRouterPreferences hasn't been initialized yet\", () => {\n      // Intentionally not initialized\n      const [firstCall, secondCall] = [\n        ASRouterPreferences.providers,\n        ASRouterPreferences.providers,\n      ];\n\n      assert.deepEqual(firstCall, FAKE_PROVIDERS);\n      assert.deepEqual(secondCall, FAKE_PROVIDERS);\n      assert.callCount(stringPrefStub, 4);\n    });\n    it(\"should skip the pref without throwing if a pref is not parsable\", () => {\n      stringPrefStub.withArgs(`${PROVIDER_PREF_BRANCH}foo`).returns(\"not json\");\n      ASRouterPreferences.init();\n\n      assert.deepEqual(ASRouterPreferences.providers, [{ id: \"bar\" }]);\n    });\n    it(\"should include TEST_PROVIDERS if devtools is turned on\", () => {\n      boolPrefStub.withArgs(DEVTOOLS_PREF).returns(true);\n      ASRouterPreferences.init();\n\n      assert.deepEqual(ASRouterPreferences.providers, [\n        ...TEST_PROVIDERS,\n        ...FAKE_PROVIDERS,\n      ]);\n    });\n  });\n  describe(\".devtoolsEnabled\", () => {\n    it(\"should read the pref the first time .devtoolsEnabled is accessed\", () => {\n      ASRouterPreferences.init();\n\n      const result = ASRouterPreferences.devtoolsEnabled;\n      assert.deepEqual(result, false);\n      assert.calledOnce(boolPrefStub);\n    });\n    it(\"should return the cached value the second time .devtoolsEnabled is accessed\", () => {\n      ASRouterPreferences.init();\n      const [, secondCall] = [\n        ASRouterPreferences.devtoolsEnabled,\n        ASRouterPreferences.devtoolsEnabled,\n      ];\n\n      assert.deepEqual(secondCall, false);\n      assert.calledOnce(boolPrefStub);\n    });\n    it(\"should just parse the pref each time if ASRouterPreferences hasn't been initialized yet\", () => {\n      // Intentionally not initialized\n      const [firstCall, secondCall] = [\n        ASRouterPreferences.devtoolsEnabled,\n        ASRouterPreferences.devtoolsEnabled,\n      ];\n\n      assert.deepEqual(firstCall, false);\n      assert.deepEqual(secondCall, false);\n      assert.calledTwice(boolPrefStub);\n    });\n  });\n  describe(\"#getUserPreference(providerId)\", () => {\n    it(\"should return the user preference for snippets\", () => {\n      boolPrefStub.withArgs(SNIPPETS_USER_PREF).returns(true);\n      assert.isTrue(ASRouterPreferences.getUserPreference(\"snippets\"));\n    });\n  });\n  describe(\"#getAllUserPreferences\", () => {\n    it(\"should return all user preferences\", () => {\n      boolPrefStub.withArgs(SNIPPETS_USER_PREF).returns(true);\n      boolPrefStub.withArgs(CFR_USER_PREF_ADDONS).returns(false);\n      boolPrefStub.withArgs(CFR_USER_PREF_FEATURES).returns(true);\n      const result = ASRouterPreferences.getAllUserPreferences();\n      assert.deepEqual(result, {\n        snippets: true,\n        cfrAddons: false,\n        cfrFeatures: true,\n      });\n    });\n  });\n  describe(\"#enableOrDisableProvider\", () => {\n    it(\"should enable an existing provider if second param is true\", () => {\n      const setStub = sandbox.stub(global.Services.prefs, \"setStringPref\");\n      setPrefForProvider(\"foo\", { id: \"foo\", enabled: false });\n      assert.isFalse(ASRouterPreferences.providers[0].enabled);\n\n      ASRouterPreferences.enableOrDisableProvider(\"foo\", true);\n\n      assert.calledWith(\n        setStub,\n        getPrefNameForProvider(\"foo\"),\n        JSON.stringify({ id: \"foo\", enabled: true })\n      );\n    });\n    it(\"should disable an existing provider if second param is false\", () => {\n      const setStub = sandbox.stub(global.Services.prefs, \"setStringPref\");\n      setPrefForProvider(\"foo\", { id: \"foo\", enabled: true });\n      assert.isTrue(ASRouterPreferences.providers[0].enabled);\n\n      ASRouterPreferences.enableOrDisableProvider(\"foo\", false);\n\n      assert.calledWith(\n        setStub,\n        getPrefNameForProvider(\"foo\"),\n        JSON.stringify({ id: \"foo\", enabled: false })\n      );\n    });\n    it(\"should not throw if the id does not exist\", () => {\n      assert.doesNotThrow(() => {\n        ASRouterPreferences.enableOrDisableProvider(\"does_not_exist\", true);\n      });\n    });\n    it(\"should not throw if pref is not parseable\", () => {\n      stringPrefStub\n        .withArgs(getPrefNameForProvider(\"foo\"))\n        .returns(\"not valid\");\n      assert.doesNotThrow(() => {\n        ASRouterPreferences.enableOrDisableProvider(\"foo\", true);\n      });\n    });\n  });\n  describe(\"#setUserPreference\", () => {\n    it(\"should do nothing if the pref doesn't exist\", () => {\n      ASRouterPreferences.setUserPreference(\"foo\", true);\n      assert.notCalled(boolPrefStub);\n    });\n    it(\"should set the given pref\", () => {\n      const setStub = sandbox.stub(global.Services.prefs, \"setBoolPref\");\n      ASRouterPreferences.setUserPreference(\"snippets\", true);\n      assert.calledWith(setStub, SNIPPETS_USER_PREF, true);\n    });\n  });\n  describe(\"#resetProviderPref\", () => {\n    it(\"should reset the pref and user prefs\", () => {\n      const resetStub = sandbox.stub(global.Services.prefs, \"clearUserPref\");\n      ASRouterPreferences.resetProviderPref();\n      FAKE_PROVIDERS.forEach(provider => {\n        assert.calledWith(resetStub, getPrefNameForProvider(provider.id));\n      });\n      assert.calledWith(resetStub, SNIPPETS_USER_PREF);\n      assert.calledWith(resetStub, CFR_USER_PREF_ADDONS);\n      assert.calledWith(resetStub, CFR_USER_PREF_FEATURES);\n    });\n  });\n  describe(\"observer, listeners\", () => {\n    it(\"should invalidate .providers when the pref is changed\", () => {\n      const testProvider = { id: \"newstuff\" };\n      const newProviders = [...FAKE_PROVIDERS, testProvider];\n\n      ASRouterPreferences.init();\n\n      assert.deepEqual(ASRouterPreferences.providers, FAKE_PROVIDERS);\n      stringPrefStub\n        .withArgs(getPrefNameForProvider(testProvider.id))\n        .returns(JSON.stringify(testProvider));\n      global.Services.prefs.getChildList\n        .withArgs(PROVIDER_PREF_BRANCH)\n        .returns(\n          newProviders.map(provider => getPrefNameForProvider(provider.id))\n        );\n      ASRouterPreferences.observe(\n        null,\n        null,\n        getPrefNameForProvider(testProvider.id)\n      );\n\n      // Cache should be invalidated so we access the new value of the pref now\n      assert.deepEqual(ASRouterPreferences.providers, newProviders);\n    });\n    it(\"should invalidate .devtoolsEnabled and .providers when the pref is changed\", () => {\n      ASRouterPreferences.init();\n\n      assert.isFalse(ASRouterPreferences.devtoolsEnabled);\n      boolPrefStub.withArgs(DEVTOOLS_PREF).returns(true);\n      global.Services.prefs.getChildList\n        .withArgs(PROVIDER_PREF_BRANCH)\n        .returns([]);\n      ASRouterPreferences.observe(null, null, DEVTOOLS_PREF);\n\n      // Cache should be invalidated so we access the new value of the pref now\n      // Note that providers needs to be invalidated because devtools adds test content to it.\n      assert.isTrue(ASRouterPreferences.devtoolsEnabled);\n      assert.deepEqual(ASRouterPreferences.providers, TEST_PROVIDERS);\n    });\n    it(\"should call listeners added with .addListener\", () => {\n      const callback1 = sinon.stub();\n      const callback2 = sinon.stub();\n      ASRouterPreferences.init();\n      ASRouterPreferences.addListener(callback1);\n      ASRouterPreferences.addListener(callback2);\n\n      ASRouterPreferences.observe(null, null, getPrefNameForProvider(\"foo\"));\n      assert.calledWith(callback1, getPrefNameForProvider(\"foo\"));\n\n      ASRouterPreferences.observe(null, null, DEVTOOLS_PREF);\n      assert.calledWith(callback2, DEVTOOLS_PREF);\n    });\n    it(\"should not call listeners after they are removed with .removeListeners\", () => {\n      const callback = sinon.stub();\n      ASRouterPreferences.init();\n      ASRouterPreferences.addListener(callback);\n\n      ASRouterPreferences.observe(null, null, getPrefNameForProvider(\"foo\"));\n      assert.calledWith(callback, getPrefNameForProvider(\"foo\"));\n\n      callback.reset();\n      ASRouterPreferences.removeListener(callback);\n\n      ASRouterPreferences.observe(null, null, DEVTOOLS_PREF);\n      assert.notCalled(callback);\n    });\n  });\n  describe(\"#_transformPersonalizedCfrScores\", () => {\n    it(\"should report JSON.parse errors\", () => {\n      sandbox.stub(global.Cu, \"reportError\");\n\n      ASRouterPreferences._transformPersonalizedCfrScores(\"\");\n\n      assert.calledOnce(global.Cu.reportError);\n    });\n    it(\"should return an object parsed from a string\", () => {\n      const scores = { FOO: 3000, BAR: 4000 };\n      assert.deepEqual(\n        ASRouterPreferences._transformPersonalizedCfrScores(\n          JSON.stringify(scores)\n        ),\n        scores\n      );\n    });\n  });\n  describe(\"#getTrailheadConfigFromPref\", () => {\n    it(\"should return trailHeadTriplet and trailHeadInterrupt\", () => {\n      let result = getTrailheadConfigFromPref(\"foo-bar\");\n      assert.propertyVal(result, \"trailheadInterrupt\", \"foo\");\n      assert.propertyVal(result, \"trailheadTriplet\", \"bar\");\n    });\n    it(\"should return default values when pref is empty\", () => {\n      let result = getTrailheadConfigFromPref(\"\");\n      assert.propertyVal(result, \"trailheadInterrupt\", \"join\");\n      assert.propertyVal(result, \"trailheadTriplet\", \"supercharge\");\n    });\n    it(\"should return default trailHeadTriplet and trailHeadInterrupt when no hyphen\", () => {\n      let result = getTrailheadConfigFromPref(\"control\");\n      assert.propertyVal(result, \"trailheadInterrupt\", \"control\");\n      assert.propertyVal(result, \"trailheadTriplet\", \"supercharge\");\n    });\n    it(\"should return trailHeadTriplet and default trailHeadInterrupt when prefixed with hyphen\", () => {\n      let result = getTrailheadConfigFromPref(\"-control\");\n      assert.propertyVal(result, \"trailheadInterrupt\", \"join\");\n      assert.propertyVal(result, \"trailheadTriplet\", \"control\");\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/asrouter/ASRouterTargeting.test.js",
    "content": "import {\n  ASRouterTargeting,\n  CachedTargetingGetter,\n  getSortedMessages,\n} from \"lib/ASRouterTargeting.jsm\";\nimport { OnboardingMessageProvider } from \"lib/OnboardingMessageProvider.jsm\";\nimport { ASRouterPreferences } from \"lib/ASRouterPreferences.jsm\";\nimport { GlobalOverrider } from \"test/unit/utils\";\n\n// Note that tests for the ASRouterTargeting environment can be found in\n// test/functional/mochitest/browser_asrouter_targeting.js\n\ndescribe(\"#CachedTargetingGetter\", () => {\n  const sixHours = 6 * 60 * 60 * 1000;\n  let sandbox;\n  let clock;\n  let frecentStub;\n  let topsitesCache;\n  beforeEach(() => {\n    sandbox = sinon.createSandbox();\n    clock = sinon.useFakeTimers();\n    frecentStub = sandbox.stub(\n      global.NewTabUtils.activityStreamProvider,\n      \"getTopFrecentSites\"\n    );\n    sandbox.stub(global.Cu, \"reportError\");\n    topsitesCache = new CachedTargetingGetter(\"getTopFrecentSites\");\n  });\n\n  afterEach(() => {\n    sandbox.restore();\n    clock.restore();\n  });\n\n  it(\"should only make a request every 6 hours\", async () => {\n    frecentStub.resolves();\n    clock.tick(sixHours);\n\n    await topsitesCache.get();\n    await topsitesCache.get();\n\n    assert.calledOnce(\n      global.NewTabUtils.activityStreamProvider.getTopFrecentSites\n    );\n\n    clock.tick(sixHours);\n\n    await topsitesCache.get();\n\n    assert.calledTwice(\n      global.NewTabUtils.activityStreamProvider.getTopFrecentSites\n    );\n  });\n  it(\"throws when failing getter\", async () => {\n    frecentStub.rejects(new Error(\"fake error\"));\n    clock.tick(sixHours);\n\n    // assert.throws expect a function as the first parameter, try/catch is a\n    // workaround\n    let rejected = false;\n    try {\n      await topsitesCache.get();\n    } catch (e) {\n      rejected = true;\n    }\n\n    assert(rejected);\n  });\n  it(\"should check targeted message before message without targeting\", async () => {\n    const messages = await OnboardingMessageProvider.getUntranslatedMessages();\n    const stub = sandbox\n      .stub(ASRouterTargeting, \"checkMessageTargeting\")\n      .resolves();\n    const context = {\n      attributionData: {\n        campaign: \"non-fx-button\",\n        source: \"addons.mozilla.org\",\n      },\n    };\n    await ASRouterTargeting.findMatchingMessage({\n      messages,\n      trigger: { id: \"firstRun\" },\n      context,\n    });\n\n    const messageCount = messages.filter(\n      message => message.trigger && message.trigger.id === \"firstRun\"\n    ).length;\n\n    assert.equal(stub.callCount, messageCount);\n    const calls = stub.getCalls().map(call => call.args[0]);\n    const lastCall = calls[calls.length - 1];\n    assert.equal(lastCall.id, \"TRAILHEAD_1\");\n  });\n  describe(\"sortMessagesByPriority\", () => {\n    it(\"should sort messages in descending priority order\", async () => {\n      const [\n        m1,\n        m2,\n        m3,\n      ] = await OnboardingMessageProvider.getUntranslatedMessages();\n      const checkMessageTargetingStub = sandbox\n        .stub(ASRouterTargeting, \"checkMessageTargeting\")\n        .resolves(false);\n      sandbox.stub(ASRouterTargeting, \"isTriggerMatch\").resolves(true);\n\n      await ASRouterTargeting.findMatchingMessage({\n        messages: [\n          { ...m1, priority: 0 },\n          { ...m2, priority: 1 },\n          { ...m3, priority: 2 },\n        ],\n        trigger: \"testing\",\n      });\n\n      assert.equal(checkMessageTargetingStub.callCount, 3);\n\n      const [arg_m1] = checkMessageTargetingStub.firstCall.args;\n      assert.equal(arg_m1.id, m3.id);\n\n      const [arg_m2] = checkMessageTargetingStub.secondCall.args;\n      assert.equal(arg_m2.id, m2.id);\n\n      const [arg_m3] = checkMessageTargetingStub.thirdCall.args;\n      assert.equal(arg_m3.id, m1.id);\n    });\n    it(\"should sort messages with no priority last\", async () => {\n      const [\n        m1,\n        m2,\n        m3,\n      ] = await OnboardingMessageProvider.getUntranslatedMessages();\n      const checkMessageTargetingStub = sandbox\n        .stub(ASRouterTargeting, \"checkMessageTargeting\")\n        .resolves(false);\n      sandbox.stub(ASRouterTargeting, \"isTriggerMatch\").resolves(true);\n\n      await ASRouterTargeting.findMatchingMessage({\n        messages: [\n          { ...m1, priority: 0 },\n          { ...m2, priority: undefined },\n          { ...m3, priority: 2 },\n        ],\n        trigger: \"testing\",\n      });\n\n      assert.equal(checkMessageTargetingStub.callCount, 3);\n\n      const [arg_m1] = checkMessageTargetingStub.firstCall.args;\n      assert.equal(arg_m1.id, m3.id);\n\n      const [arg_m2] = checkMessageTargetingStub.secondCall.args;\n      assert.equal(arg_m2.id, m1.id);\n\n      const [arg_m3] = checkMessageTargetingStub.thirdCall.args;\n      assert.equal(arg_m3.id, m2.id);\n    });\n    it(\"should keep the order of messages with same priority unchanged\", async () => {\n      const [\n        m1,\n        m2,\n        m3,\n      ] = await OnboardingMessageProvider.getUntranslatedMessages();\n      const checkMessageTargetingStub = sandbox\n        .stub(ASRouterTargeting, \"checkMessageTargeting\")\n        .resolves(false);\n      sandbox.stub(ASRouterTargeting, \"isTriggerMatch\").resolves(true);\n\n      await ASRouterTargeting.findMatchingMessage({\n        messages: [\n          { ...m1, priority: 2, targeting: undefined, rank: 1 },\n          { ...m2, priority: undefined, targeting: undefined, rank: 1 },\n          { ...m3, priority: 2, targeting: undefined, rank: 1 },\n        ],\n        trigger: \"testing\",\n      });\n\n      assert.equal(checkMessageTargetingStub.callCount, 3);\n\n      const [arg_m1] = checkMessageTargetingStub.firstCall.args;\n      assert.equal(arg_m1.id, m1.id);\n\n      const [arg_m2] = checkMessageTargetingStub.secondCall.args;\n      assert.equal(arg_m2.id, m3.id);\n\n      const [arg_m3] = checkMessageTargetingStub.thirdCall.args;\n      assert.equal(arg_m3.id, m2.id);\n    });\n  });\n  describe(\"combineContexts\", () => {\n    it(\"should combine the properties of the two objects\", () => {\n      const joined = ASRouterTargeting.combineContexts(\n        {\n          get foo() {\n            return \"foo\";\n          },\n        },\n        {\n          get bar() {\n            return \"bar\";\n          },\n        }\n      );\n      assert.propertyVal(joined, \"foo\", \"foo\");\n      assert.propertyVal(joined, \"bar\", \"bar\");\n    });\n    it(\"should warn when properties overlap\", () => {\n      ASRouterTargeting.combineContexts(\n        {\n          get foo() {\n            return \"foo\";\n          },\n        },\n        {\n          get foo() {\n            return \"bar\";\n          },\n        }\n      );\n      assert.calledOnce(global.Cu.reportError);\n    });\n  });\n});\ndescribe(\"ASRouterTargeting\", () => {\n  let evalStub;\n  let sandbox;\n  let clock;\n  beforeEach(() => {\n    sandbox = sinon.createSandbox();\n    evalStub = sandbox.stub(global.FilterExpressions, \"eval\");\n    sandbox.replace(ASRouterTargeting, \"Environment\", {});\n    clock = sinon.useFakeTimers();\n  });\n  afterEach(() => {\n    clock.restore();\n    sandbox.restore();\n  });\n  it(\"should cache evaluation result\", async () => {\n    evalStub.resolves(true);\n\n    await ASRouterTargeting.checkMessageTargeting(\n      { targeting: \"jexl1\" },\n      {},\n      sandbox.stub(),\n      true\n    );\n    await ASRouterTargeting.checkMessageTargeting(\n      { targeting: \"jexl2\" },\n      {},\n      sandbox.stub(),\n      true\n    );\n    await ASRouterTargeting.checkMessageTargeting(\n      { targeting: \"jexl1\" },\n      {},\n      sandbox.stub(),\n      true\n    );\n\n    assert.calledTwice(evalStub);\n  });\n  it(\"should not cache evaluation result\", async () => {\n    evalStub.resolves(true);\n\n    await ASRouterTargeting.checkMessageTargeting(\n      { targeting: \"jexl\" },\n      {},\n      sandbox.stub(),\n      false\n    );\n    await ASRouterTargeting.checkMessageTargeting(\n      { targeting: \"jexl\" },\n      {},\n      sandbox.stub(),\n      false\n    );\n    await ASRouterTargeting.checkMessageTargeting(\n      { targeting: \"jexl\" },\n      {},\n      sandbox.stub(),\n      false\n    );\n\n    assert.calledThrice(evalStub);\n  });\n  it(\"should expire cache entries\", async () => {\n    evalStub.resolves(true);\n\n    await ASRouterTargeting.checkMessageTargeting(\n      { targeting: \"jexl\" },\n      {},\n      sandbox.stub(),\n      true\n    );\n    await ASRouterTargeting.checkMessageTargeting(\n      { targeting: \"jexl\" },\n      {},\n      sandbox.stub(),\n      true\n    );\n    clock.tick(60 * 1000 + 1);\n    await ASRouterTargeting.checkMessageTargeting(\n      { targeting: \"jexl\" },\n      {},\n      sandbox.stub(),\n      true\n    );\n\n    assert.calledTwice(evalStub);\n  });\n\n  describe(\"#findMatchingMessage\", () => {\n    let matchStub;\n    let messages = [\n      { id: \"FOO\", targeting: \"match\" },\n      { id: \"BAR\", targeting: \"match\" },\n      { id: \"BAZ\" },\n    ];\n    beforeEach(() => {\n      matchStub = sandbox\n        .stub(ASRouterTargeting, \"_isMessageMatch\")\n        .callsFake(message => message.targeting === \"match\");\n    });\n    it(\"should return an array of matches if returnAll is true\", async () => {\n      assert.deepEqual(\n        await ASRouterTargeting.findMatchingMessage({\n          messages,\n          returnAll: true,\n        }),\n        [{ id: \"FOO\", targeting: \"match\" }, { id: \"BAR\", targeting: \"match\" }]\n      );\n    });\n    it(\"should return an empty array if no matches were found and returnAll is true\", async () => {\n      matchStub.returns(false);\n      assert.deepEqual(\n        await ASRouterTargeting.findMatchingMessage({\n          messages,\n          returnAll: true,\n        }),\n        []\n      );\n    });\n    it(\"should return the first match if returnAll is false\", async () => {\n      assert.deepEqual(\n        await ASRouterTargeting.findMatchingMessage({\n          messages,\n        }),\n        messages[0]\n      );\n    });\n    it(\"should return null if if no matches were found and returnAll is false\", async () => {\n      matchStub.returns(false);\n      assert.deepEqual(\n        await ASRouterTargeting.findMatchingMessage({\n          messages,\n        }),\n        null\n      );\n    });\n  });\n});\n\n/**\n * Messages should be sorted in the following order:\n * 1. Rank\n * 2. Priority\n * 3. If the message has targeting\n * 4. Order or randomization, depending on input\n */\ndescribe(\"getSortedMessages\", () => {\n  let globals = new GlobalOverrider();\n  let sandbox;\n  let thresholdStub;\n  beforeEach(() => {\n    globals.set({ ASRouterPreferences });\n    sandbox = sinon.createSandbox();\n    thresholdStub = sandbox.stub();\n    sandbox.replaceGetter(\n      ASRouterPreferences,\n      \"personalizedCfrThreshold\",\n      thresholdStub\n    );\n  });\n  afterEach(() => {\n    sandbox.restore();\n    globals.restore();\n  });\n\n  /**\n   * assertSortsCorrectly - Tests to see if an array, when sorted with getSortedMessages,\n   *                        returns the items in the expected order.\n   *\n   * @param {Message[]} expectedOrderArray - The array of messages in its expected order\n   * @param {{}} options - The options param for getSortedMessages\n   * @returns\n   */\n  function assertSortsCorrectly(expectedOrderArray, options) {\n    const input = [...expectedOrderArray].reverse();\n    const result = getSortedMessages(input, options);\n    const indexes = result.map(message => expectedOrderArray.indexOf(message));\n    return assert.equal(\n      indexes.join(\",\"),\n      [...expectedOrderArray.keys()].join(\",\"),\n      \"Messsages are out of order\"\n    );\n  }\n\n  it(\"should sort messages by priority, then by targeting\", () => {\n    assertSortsCorrectly([\n      { priority: 100, targeting: \"isFoo\" },\n      { priority: 100 },\n      { priority: 99 },\n      { priority: 1, targeting: \"isFoo\" },\n      { priority: 1 },\n      {},\n    ]);\n  });\n  it(\"should sort messages by score first if defined\", () => {\n    assertSortsCorrectly([\n      { score: 7001 },\n      { score: 7000, priority: 1 },\n      { score: 7000, targeting: \"isFoo\" },\n      { score: 7000 },\n      { score: 6000, priority: 1000 },\n      { priority: 99999 },\n      {},\n    ]);\n  });\n  it(\"should sort messages by priority, then targeting, then order if ordered param is true\", () => {\n    assertSortsCorrectly(\n      [\n        { priority: 100, order: 4 },\n        { priority: 100, order: 5 },\n        { priority: 1, order: 3, targeting: \"isFoo\" },\n        { priority: 1, order: 0 },\n        { priority: 1, order: 1 },\n        { priority: 1, order: 2 },\n        { order: 0 },\n      ],\n      { ordered: true }\n    );\n  });\n  it(\"should filter messages below the personalizedCfrThreshold\", () => {\n    thresholdStub.returns(5000);\n    const result = getSortedMessages([{ score: 5000 }, { score: 4999 }, {}]);\n    assert.deepEqual(result, [{ score: 5000 }, {}]);\n  });\n  it(\"should not filter out messages without a score\", () => {\n    thresholdStub.returns(5000);\n    const result = getSortedMessages([{ score: 4999 }, { id: \"FOO\" }]);\n    assert.deepEqual(result, [{ id: \"FOO\" }]);\n  });\n  it(\"should not apply filter if the threshold is an invalid value\", () => {\n    let result;\n\n    thresholdStub.returns(undefined);\n    result = getSortedMessages([{ score: 5000 }, { score: 4999 }]);\n    assert.deepEqual(result, [{ score: 5000 }, { score: 4999 }]);\n\n    thresholdStub.returns(\"foo\");\n    result = getSortedMessages([{ score: 5000 }, { score: 4999 }]);\n    assert.deepEqual(result, [{ score: 5000 }, { score: 4999 }]);\n\n    thresholdStub.returns(5000);\n    result = getSortedMessages([{ score: 5000 }, { score: 4999 }]);\n    assert.deepEqual(result, [{ score: 5000 }]);\n  });\n});\n"
  },
  {
    "path": "test/unit/asrouter/ASRouterTriggerListeners.test.js",
    "content": "import { ASRouterTriggerListeners } from \"lib/ASRouterTriggerListeners.jsm\";\nimport { GlobalOverrider } from \"test/unit/utils\";\n\ndescribe(\"ASRouterTriggerListeners\", () => {\n  let sandbox;\n  let globals;\n  let existingWindow;\n  let isWindowPrivateStub;\n  const triggerHandler = () => {};\n  const openURLListener = ASRouterTriggerListeners.get(\"openURL\");\n  const frequentVisitsListener = ASRouterTriggerListeners.get(\"frequentVisits\");\n  const bookmarkedURLListener = ASRouterTriggerListeners.get(\n    \"openBookmarkedURL\"\n  );\n  const openArticleURLListener = ASRouterTriggerListeners.get(\"openArticleURL\");\n  const hosts = [\"www.mozilla.com\", \"www.mozilla.org\"];\n\n  beforeEach(async () => {\n    sandbox = sinon.createSandbox();\n    globals = new GlobalOverrider();\n    existingWindow = {\n      gBrowser: {\n        addTabsProgressListener: sandbox.stub(),\n        removeTabsProgressListener: sandbox.stub(),\n        currentURI: { host: \"\" },\n      },\n      addEventListener: sinon.stub(),\n      removeEventListener: sinon.stub(),\n    };\n    sandbox.spy(openURLListener, \"init\");\n    sandbox.spy(openURLListener, \"uninit\");\n    isWindowPrivateStub = sandbox.stub();\n    // Assume no window is private so that we execute the action\n    isWindowPrivateStub.returns(false);\n    globals.set(\"PrivateBrowsingUtils\", {\n      isWindowPrivate: isWindowPrivateStub,\n    });\n    const ewUninit = new Map();\n    globals.set(\"EveryWindow\", {\n      registerCallback: (id, init, uninit) => {\n        init(existingWindow);\n        ewUninit.set(id, uninit);\n      },\n      unregisterCallback: id => {\n        ewUninit.get(id)(existingWindow);\n      },\n    });\n  });\n  afterEach(() => {\n    sandbox.restore();\n    globals.restore();\n  });\n\n  describe(\"openBookmarkedURL\", () => {\n    let observerStub;\n    describe(\"#init\", () => {\n      beforeEach(() => {\n        observerStub = sandbox.stub(global.Services.obs, \"addObserver\");\n        sandbox\n          .stub(global.Services.wm, \"getMostRecentBrowserWindow\")\n          .returns({ gBrowser: { selectedBrowser: {} } });\n      });\n      afterEach(() => {\n        bookmarkedURLListener.uninit();\n      });\n      it(\"should set hosts to the recentBookmarks\", async () => {\n        await bookmarkedURLListener.init(sandbox.stub());\n\n        assert.calledOnce(observerStub);\n        assert.calledWithExactly(\n          observerStub,\n          bookmarkedURLListener,\n          \"bookmark-icon-updated\"\n        );\n      });\n      it(\"should provide id to triggerHandler\", async () => {\n        const newTriggerHandler = sinon.stub();\n        const subject = {};\n        await bookmarkedURLListener.init(newTriggerHandler);\n\n        bookmarkedURLListener.observe(\n          subject,\n          \"bookmark-icon-updated\",\n          \"starred\"\n        );\n\n        assert.calledOnce(newTriggerHandler);\n        assert.calledWithExactly(newTriggerHandler, subject, {\n          id: bookmarkedURLListener.id,\n        });\n      });\n    });\n  });\n\n  describe(\"openArticleURL\", () => {\n    describe(\"#init\", () => {\n      beforeEach(() => {\n        globals.set(\n          \"MatchPatternSet\",\n          sandbox.stub().callsFake(patterns => ({\n            patterns,\n            matches: url => patterns.has(url),\n          }))\n        );\n        sandbox.stub(global.Services.mm, \"addMessageListener\");\n        sandbox.stub(global.Services.mm, \"removeMessageListener\");\n      });\n      afterEach(() => {\n        openArticleURLListener.uninit();\n      });\n      it(\"setup an event listener on init\", () => {\n        openArticleURLListener.init(sandbox.stub(), hosts, hosts);\n\n        assert.calledOnce(global.Services.mm.addMessageListener);\n        assert.calledWithExactly(\n          global.Services.mm.addMessageListener,\n          openArticleURLListener.readerModeEvent,\n          sinon.match.object\n        );\n      });\n      it(\"should call triggerHandler correctly for matches [host match]\", () => {\n        const stub = sandbox.stub();\n        const target = { currentURI: { host: hosts[0], spec: hosts[1] } };\n        openArticleURLListener.init(stub, hosts, hosts);\n\n        const [\n          ,\n          { receiveMessage },\n        ] = global.Services.mm.addMessageListener.firstCall.args;\n        receiveMessage({ data: { isArticle: true }, target });\n\n        assert.calledOnce(stub);\n        assert.calledWithExactly(stub, target, {\n          id: openArticleURLListener.id,\n          param: { host: hosts[0], url: hosts[1] },\n        });\n      });\n      it(\"should call triggerHandler correctly for matches [pattern match]\", () => {\n        const stub = sandbox.stub();\n        const target = { currentURI: { host: null, spec: hosts[1] } };\n        openArticleURLListener.init(stub, hosts, hosts);\n\n        const [\n          ,\n          { receiveMessage },\n        ] = global.Services.mm.addMessageListener.firstCall.args;\n        receiveMessage({ data: { isArticle: true }, target });\n\n        assert.calledOnce(stub);\n        assert.calledWithExactly(stub, target, {\n          id: openArticleURLListener.id,\n          param: { host: null, url: hosts[1] },\n        });\n      });\n      it(\"should remove the message listener\", () => {\n        openArticleURLListener.init(sandbox.stub(), hosts, hosts);\n        openArticleURLListener.uninit();\n\n        assert.calledOnce(global.Services.mm.removeMessageListener);\n      });\n    });\n  });\n\n  describe(\"frequentVisits\", () => {\n    let _triggerHandler;\n    beforeEach(() => {\n      _triggerHandler = sandbox.stub();\n      sandbox.useFakeTimers();\n      frequentVisitsListener.init(_triggerHandler, hosts);\n    });\n    afterEach(() => {\n      sandbox.clock.restore();\n      frequentVisitsListener.uninit();\n    });\n    it(\"should be initialized\", () => {\n      assert.isTrue(frequentVisitsListener._initialized);\n    });\n    it(\"should listen for TabSelect events\", () => {\n      assert.calledOnce(existingWindow.addEventListener);\n      assert.calledWith(\n        existingWindow.addEventListener,\n        \"TabSelect\",\n        frequentVisitsListener.onTabSwitch\n      );\n    });\n    it(\"should call _triggerHandler if the visit is valid (is recoreded)\", () => {\n      frequentVisitsListener.triggerHandler({}, \"www.mozilla.com\");\n\n      assert.calledOnce(_triggerHandler);\n    });\n    it(\"should call _triggerHandler only once\", () => {\n      frequentVisitsListener.triggerHandler({}, \"www.mozilla.com\");\n      frequentVisitsListener.triggerHandler({}, \"www.mozilla.com\");\n\n      assert.calledOnce(_triggerHandler);\n    });\n    it(\"should call _triggerHandler again after 15 minutes\", () => {\n      frequentVisitsListener.triggerHandler({}, \"www.mozilla.com\");\n      sandbox.clock.tick(15 * 60 * 1000 + 1);\n      frequentVisitsListener.triggerHandler({}, \"www.mozilla.com\");\n\n      assert.calledTwice(_triggerHandler);\n    });\n    it(\"should call triggerHandler on valid hosts\", () => {\n      const stub = sandbox.stub(frequentVisitsListener, \"triggerHandler\");\n      existingWindow.gBrowser.currentURI.host = hosts[0]; // eslint-disable-line prefer-destructuring\n\n      frequentVisitsListener.onTabSwitch({\n        target: { ownerGlobal: existingWindow },\n      });\n\n      assert.calledOnce(stub);\n    });\n    it(\"should not call triggerHandler on invalid hosts\", () => {\n      const stub = sandbox.stub(frequentVisitsListener, \"triggerHandler\");\n      existingWindow.gBrowser.currentURI.host = \"foo.com\";\n\n      frequentVisitsListener.onTabSwitch({\n        target: { ownerGlobal: existingWindow },\n      });\n\n      assert.notCalled(stub);\n    });\n    describe(\"MatchPattern\", () => {\n      beforeEach(() => {\n        globals.set(\n          \"MatchPatternSet\",\n          sandbox.stub().callsFake(patterns => ({ patterns: patterns || [] }))\n        );\n      });\n      afterEach(() => {\n        frequentVisitsListener.uninit();\n      });\n      it(\"should create a matchPatternSet\", () => {\n        frequentVisitsListener.init(_triggerHandler, hosts, [\"pattern\"]);\n\n        assert.calledOnce(window.MatchPatternSet);\n        assert.calledWithExactly(window.MatchPatternSet, new Set([\"pattern\"]), {\n          ignorePath: true,\n        });\n      });\n      it(\"should allow to add multiple patterns and dedupe\", () => {\n        frequentVisitsListener.init(_triggerHandler, hosts, [\"pattern\"]);\n        frequentVisitsListener.init(_triggerHandler, hosts, [\"foo\"]);\n\n        assert.calledTwice(window.MatchPatternSet);\n        assert.calledWithExactly(\n          window.MatchPatternSet,\n          new Set([\"pattern\", \"foo\"]),\n          { ignorePath: true }\n        );\n      });\n      it(\"should handle bad arguments to MatchPatternSet\", () => {\n        const badArgs = [\"www.example.com\"];\n        window.MatchPatternSet.withArgs(new Set(badArgs)).throws();\n        frequentVisitsListener.init(_triggerHandler, hosts, badArgs);\n\n        // Fails with an empty MatchPatternSet\n        assert.property(frequentVisitsListener._matchPatternSet, \"patterns\");\n\n        // Second try is succesful\n        frequentVisitsListener.init(_triggerHandler, hosts, [\"foo\"]);\n\n        assert.property(frequentVisitsListener._matchPatternSet, \"patterns\");\n        assert.isTrue(\n          frequentVisitsListener._matchPatternSet.patterns.has(\"foo\")\n        );\n      });\n    });\n  });\n\n  describe(\"openURL listener\", () => {\n    it(\"should exist and initially be uninitialised\", () => {\n      assert.ok(openURLListener);\n      assert.notOk(openURLListener._initialized);\n    });\n\n    describe(\"#init\", () => {\n      beforeEach(() => {\n        openURLListener.init(triggerHandler, hosts);\n      });\n      afterEach(() => {\n        openURLListener.uninit();\n      });\n\n      it(\"should set ._initialized to true and save the triggerHandler and hosts\", () => {\n        assert.ok(openURLListener._initialized);\n        assert.deepEqual(openURLListener._hosts, new Set(hosts));\n        assert.equal(openURLListener._triggerHandler, triggerHandler);\n      });\n\n      it(\"should add tab progress listeners to all existing browser windows\", () => {\n        assert.calledOnce(existingWindow.gBrowser.addTabsProgressListener);\n        assert.calledWithExactly(\n          existingWindow.gBrowser.addTabsProgressListener,\n          openURLListener\n        );\n      });\n\n      it(\"if already initialised, should only update the trigger handler and add the new hosts\", () => {\n        const newHosts = [\"www.example.com\"];\n        const newTriggerHandler = () => {};\n        existingWindow.gBrowser.addTabsProgressListener.reset();\n\n        openURLListener.init(newTriggerHandler, newHosts);\n        assert.ok(openURLListener._initialized);\n        assert.deepEqual(\n          openURLListener._hosts,\n          new Set([...hosts, ...newHosts])\n        );\n        assert.equal(openURLListener._triggerHandler, newTriggerHandler);\n        assert.notCalled(existingWindow.gBrowser.addTabsProgressListener);\n      });\n    });\n\n    describe(\"#uninit\", () => {\n      beforeEach(async () => {\n        openURLListener.init(triggerHandler, hosts);\n        openURLListener.uninit();\n      });\n\n      it(\"should set ._initialized to false and clear the triggerHandler and hosts\", () => {\n        assert.notOk(openURLListener._initialized);\n        assert.equal(openURLListener._hosts, null);\n        assert.equal(openURLListener._triggerHandler, null);\n      });\n\n      it(\"should remove tab progress listeners from all existing browser windows\", () => {\n        assert.calledOnce(existingWindow.gBrowser.removeTabsProgressListener);\n        assert.calledWithExactly(\n          existingWindow.gBrowser.removeTabsProgressListener,\n          openURLListener\n        );\n      });\n\n      it(\"should do nothing if already uninitialised\", () => {\n        existingWindow.gBrowser.removeTabsProgressListener.reset();\n\n        openURLListener.uninit();\n        assert.notOk(openURLListener._initialized);\n        assert.notCalled(existingWindow.gBrowser.removeTabsProgressListener);\n      });\n    });\n\n    describe(\"#onLocationChange\", () => {\n      afterEach(() => {\n        openURLListener.uninit();\n        frequentVisitsListener.uninit();\n      });\n\n      it(\"should call the ._triggerHandler with the right arguments\", () => {\n        const newTriggerHandler = sinon.stub();\n        openURLListener.init(newTriggerHandler, hosts);\n\n        const browser = {};\n        const webProgress = { isTopLevel: true };\n        const location = \"www.mozilla.org\";\n        openURLListener.onLocationChange(browser, webProgress, undefined, {\n          host: location,\n          spec: location,\n        });\n        assert.calledOnce(newTriggerHandler);\n        assert.calledWithExactly(newTriggerHandler, browser, {\n          id: \"openURL\",\n          param: { host: \"www.mozilla.org\", url: \"www.mozilla.org\" },\n        });\n      });\n      it(\"should call triggerHandler for a redirect (openURL + frequentVisits)\", () => {\n        for (let trigger of [openURLListener, frequentVisitsListener]) {\n          const newTriggerHandler = sinon.stub();\n          trigger.init(newTriggerHandler, hosts);\n\n          const browser = {};\n          const webProgress = { isTopLevel: true };\n          const aLocationURI = {\n            host: \"subdomain.mozilla.org\",\n            spec: \"subdomain.mozilla.org\",\n          };\n          const aRequest = {\n            QueryInterface: sandbox.stub().returns({\n              originalURI: { spec: \"www.mozilla.org\", host: \"www.mozilla.org\" },\n            }),\n          };\n          trigger.onLocationChange(\n            browser,\n            webProgress,\n            aRequest,\n            aLocationURI\n          );\n          assert.calledOnce(aRequest.QueryInterface);\n          assert.calledOnce(newTriggerHandler);\n        }\n      });\n      it(\"should call triggerHandler with the right arguments (redirect)\", () => {\n        const newTriggerHandler = sinon.stub();\n        openURLListener.init(newTriggerHandler, hosts);\n\n        const browser = {};\n        const webProgress = { isTopLevel: true };\n        const aLocationURI = {\n          host: \"subdomain.mozilla.org\",\n          spec: \"subdomain.mozilla.org\",\n        };\n        const aRequest = {\n          QueryInterface: sandbox.stub().returns({\n            originalURI: { spec: \"www.mozilla.org\", host: \"www.mozilla.org\" },\n          }),\n        };\n        openURLListener.onLocationChange(\n          browser,\n          webProgress,\n          aRequest,\n          aLocationURI\n        );\n        assert.calledWithExactly(newTriggerHandler, browser, {\n          id: \"openURL\",\n          param: { host: \"www.mozilla.org\", url: \"www.mozilla.org\" },\n        });\n      });\n      it(\"should call triggerHandler for a redirect (openURL + frequentVisits)\", () => {\n        for (let trigger of [openURLListener, frequentVisitsListener]) {\n          const newTriggerHandler = sinon.stub();\n          trigger.init(newTriggerHandler, hosts);\n\n          const browser = {};\n          const webProgress = { isTopLevel: true };\n          const aLocationURI = {\n            host: \"subdomain.mozilla.org\",\n            spec: \"subdomain.mozilla.org\",\n          };\n          const aRequest = {\n            QueryInterface: sandbox.stub().returns({\n              originalURI: { spec: \"www.mozilla.org\", host: \"www.mozilla.org\" },\n            }),\n          };\n          trigger.onLocationChange(\n            browser,\n            webProgress,\n            aRequest,\n            aLocationURI\n          );\n          assert.calledOnce(aRequest.QueryInterface);\n          assert.calledOnce(newTriggerHandler);\n        }\n      });\n      it(\"should call triggerHandler with the right arguments (redirect)\", () => {\n        const newTriggerHandler = sinon.stub();\n        openURLListener.init(newTriggerHandler, hosts);\n\n        const browser = {};\n        const webProgress = { isTopLevel: true };\n        const aLocationURI = {\n          host: \"subdomain.mozilla.org\",\n          spec: \"subdomain.mozilla.org\",\n        };\n        const aRequest = {\n          QueryInterface: sandbox.stub().returns({\n            originalURI: { spec: \"www.mozilla.org\", host: \"www.mozilla.org\" },\n          }),\n        };\n        openURLListener.onLocationChange(\n          browser,\n          webProgress,\n          aRequest,\n          aLocationURI\n        );\n        assert.calledWithExactly(newTriggerHandler, browser, {\n          id: \"openURL\",\n          param: { host: \"www.mozilla.org\", url: \"www.mozilla.org\" },\n        });\n      });\n      it(\"should fail for subdomains (not redirect)\", () => {\n        const newTriggerHandler = sinon.stub();\n        openURLListener.init(newTriggerHandler, hosts);\n\n        const browser = {};\n        const webProgress = { isTopLevel: true };\n        const aLocationURI = {\n          host: \"subdomain.mozilla.org\",\n          spec: \"subdomain.mozilla.org\",\n        };\n        const aRequest = {\n          QueryInterface: sandbox.stub().returns({\n            originalURI: {\n              spec: \"subdomain.mozilla.org\",\n              host: \"subdomain.mozilla.org\",\n            },\n          }),\n        };\n        openURLListener.onLocationChange(\n          browser,\n          webProgress,\n          aRequest,\n          aLocationURI\n        );\n        assert.calledOnce(aRequest.QueryInterface);\n        assert.notCalled(newTriggerHandler);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/asrouter/CFRMessageProvider.test.js",
    "content": "import { CFRMessageProvider } from \"lib/CFRMessageProvider.jsm\";\nconst messages = CFRMessageProvider.getMessages();\n\nconst REGULAR_IDS = [\n  \"FACEBOOK_CONTAINER\",\n  \"GOOGLE_TRANSLATE\",\n  \"YOUTUBE_ENHANCE\",\n  // These are excluded for now.\n  // \"WIKIPEDIA_CONTEXT_MENU_SEARCH\",\n  // \"REDDIT_ENHANCEMENT\",\n];\n\ndescribe(\"CFRMessageProvider\", () => {\n  it(\"should have a total of 9 messages\", () => {\n    assert.lengthOf(messages, 9);\n  });\n  it(\"should have one message each for the three regular addons\", () => {\n    for (const id of REGULAR_IDS) {\n      const cohort3 = messages.find(msg => msg.id === `${id}_3`);\n      assert.ok(cohort3, `contains three day cohort for ${id}`);\n      assert.deepEqual(\n        cohort3.frequency,\n        { lifetime: 3 },\n        \"three day cohort has the right frequency cap\"\n      );\n      assert.notInclude(cohort3.targeting, `providerCohorts.cfr`);\n    }\n  });\n  it(\"should always have xpinstallEnabled as targeting if it is an addon\", () => {\n    for (const message of messages) {\n      // Ensure that the CFR messages that are recommending an addon have this targeting.\n      // In the future when we can do targeting based on category, this test will change.\n      // See bug 1494778 and 1497653\n      if (!message.content.layout) {\n        assert.include(message.targeting, `(xpinstallEnabled == true)`);\n      }\n    }\n  });\n  it(\"should restrict all messages to `en` locale for now (PIN TAB is handled separately)\", () => {\n    for (const message of messages.filter(m => !m.content.layout)) {\n      assert.include(message.targeting, `localeLanguageCode == \"en\"`);\n    }\n  });\n  it(\"should restrict locale for PIN_TAB message\", () => {\n    const pinTabMessage = messages.find(m => m.id === \"PIN_TAB\");\n\n    // 6 en-* locales, fr and de\n    assert.lengthOf(pinTabMessage.targeting.match(/en-|fr|de/g), 8);\n  });\n  it(\"should contain `www.` version of the hosts\", () => {\n    const pinTabMessage = messages.find(m => m.id === \"PIN_TAB\");\n\n    assert.isTrue(\n      !!pinTabMessage.trigger.params.filter(host => host.startsWith(\"www.\"))\n        .length\n    );\n  });\n});\n"
  },
  {
    "path": "test/unit/asrouter/CFRPageActions.test.js",
    "content": "import { CFRPageActions, PageAction } from \"lib/CFRPageActions.jsm\";\nimport { FAKE_RECOMMENDATION } from \"./constants\";\nimport { GlobalOverrider } from \"test/unit/utils\";\n\ndescribe(\"CFRPageActions\", () => {\n  let sandbox;\n  let clock;\n  let fakeRecommendation;\n  let fakeHost;\n  let fakeBrowser;\n  let dispatchStub;\n  let globals;\n  let containerElem;\n  let elements;\n  let announceStub;\n  let remoteL10n;\n\n  const elementIDs = [\n    \"urlbar\",\n    \"urlbar-input\",\n    \"contextual-feature-recommendation\",\n    \"cfr-button\",\n    \"cfr-label\",\n    \"contextual-feature-recommendation-notification\",\n    \"cfr-notification-header-label\",\n    \"cfr-notification-header-link\",\n    \"cfr-notification-header-image\",\n    \"cfr-notification-author\",\n    \"cfr-notification-footer\",\n    \"cfr-notification-footer-text\",\n    \"cfr-notification-footer-filled-stars\",\n    \"cfr-notification-footer-empty-stars\",\n    \"cfr-notification-footer-users\",\n    \"cfr-notification-footer-spacer\",\n    \"cfr-notification-footer-learn-more-link\",\n    \"cfr-notification-footer-pintab-animation-container\",\n    \"cfr-notification-footer-animation-button\",\n    \"cfr-notification-footer-animation-label\",\n  ];\n  const elementClassNames = [\"popup-notification-body-container\"];\n\n  beforeEach(() => {\n    sandbox = sinon.createSandbox();\n    clock = sandbox.useFakeTimers();\n\n    announceStub = sandbox.stub();\n    const A11yUtils = { announce: announceStub };\n    fakeRecommendation = { ...FAKE_RECOMMENDATION };\n    fakeHost = \"mozilla.org\";\n    fakeBrowser = {\n      documentURI: {\n        scheme: \"https\",\n        host: fakeHost,\n      },\n      ownerGlobal: window,\n    };\n    dispatchStub = sandbox.stub();\n\n    remoteL10n = {\n      l10n: {},\n      reloadL10n: sandbox.stub(),\n    };\n\n    globals = new GlobalOverrider();\n    globals.set({\n      RemoteL10n: remoteL10n,\n      promiseDocumentFlushed: sandbox\n        .stub()\n        .callsFake(fn => Promise.resolve(fn())),\n      PopupNotifications: {\n        show: sandbox.stub(),\n        remove: sandbox.stub(),\n      },\n      PrivateBrowsingUtils: { isWindowPrivate: sandbox.stub().returns(false) },\n      gBrowser: { selectedBrowser: fakeBrowser },\n      A11yUtils,\n    });\n    document.createXULElement = document.createElement;\n\n    elements = {};\n    const [body] = document.getElementsByTagName(\"body\");\n    containerElem = document.createElement(\"div\");\n    body.appendChild(containerElem);\n    for (const id of elementIDs) {\n      const elem = document.createElement(\"div\");\n      elem.setAttribute(\"id\", id);\n      containerElem.appendChild(elem);\n      elements[id] = elem;\n    }\n    for (const className of elementClassNames) {\n      const elem = document.createElement(\"div\");\n      elem.setAttribute(\"class\", className);\n      containerElem.appendChild(elem);\n      elements[className] = elem;\n    }\n  });\n\n  afterEach(() => {\n    CFRPageActions.clearRecommendations();\n    containerElem.remove();\n    sandbox.restore();\n    globals.restore();\n  });\n\n  describe(\"PageAction\", () => {\n    let pageAction;\n\n    beforeEach(() => {\n      pageAction = new PageAction(window, dispatchStub);\n    });\n\n    describe(\"#addImpression\", () => {\n      it(\"should call _sendTelemetry with the impression payload\", () => {\n        const recommendation = {\n          id: \"foo\",\n          content: { bucket_id: \"bar\" },\n        };\n        sandbox.spy(pageAction, \"_sendTelemetry\");\n\n        pageAction.addImpression(recommendation);\n\n        assert.calledWith(pageAction._sendTelemetry, {\n          message_id: \"foo\",\n          bucket_id: \"bar\",\n          event: \"IMPRESSION\",\n        });\n      });\n      it(\"should include modelVersion if presented in the message\", () => {\n        const recommendation = {\n          id: \"foo\",\n          content: { bucket_id: \"bar\" },\n          personalizedModelVersion: \"model_version_1\",\n        };\n        sandbox.spy(pageAction, \"_sendTelemetry\");\n\n        pageAction.addImpression(recommendation);\n\n        assert.calledWith(pageAction._sendTelemetry, {\n          message_id: \"foo\",\n          bucket_id: \"bar\",\n          event: \"IMPRESSION\",\n          event_context: {\n            modelVersion: \"model_version_1\",\n          },\n        });\n      });\n    });\n\n    describe(\"#showAddressBarNotifier\", () => {\n      it(\"should un-hideAddressBarNotifier the element and set the right label value\", async () => {\n        await pageAction.showAddressBarNotifier(fakeRecommendation);\n        assert.isFalse(pageAction.container.hidden);\n        assert.equal(\n          pageAction.label.value,\n          fakeRecommendation.content.notification_text\n        );\n      });\n      it(\"should wait for the document layout to flush\", async () => {\n        sandbox.spy(pageAction.label, \"getClientRects\");\n        await pageAction.showAddressBarNotifier(fakeRecommendation);\n        assert.calledOnce(global.promiseDocumentFlushed);\n        assert.callOrder(\n          global.promiseDocumentFlushed,\n          pageAction.label.getClientRects\n        );\n      });\n      it(\"should set the CSS variable --cfr-label-width correctly\", async () => {\n        await pageAction.showAddressBarNotifier(fakeRecommendation);\n        const expectedWidth = pageAction.label.getClientRects()[0].width;\n        assert.equal(\n          pageAction.urlbar.style.getPropertyValue(\"--cfr-label-width\"),\n          `${expectedWidth}px`\n        );\n      });\n      it(\"should cause an expansion, and dispatch an impression iff `expand` is true\", async () => {\n        sandbox.spy(pageAction, \"_clearScheduledStateChanges\");\n        sandbox.spy(pageAction, \"_expand\");\n        sandbox.spy(pageAction, \"_dispatchImpression\");\n\n        await pageAction.showAddressBarNotifier(fakeRecommendation);\n        assert.notCalled(pageAction._dispatchImpression);\n        clock.tick(1001);\n        assert.notEqual(\n          pageAction.urlbar.getAttribute(\"cfr-recommendation-state\"),\n          \"expanded\"\n        );\n\n        await pageAction.showAddressBarNotifier(fakeRecommendation, true);\n        assert.calledOnce(pageAction._clearScheduledStateChanges);\n        clock.tick(1001);\n        assert.equal(\n          pageAction.urlbar.getAttribute(\"cfr-recommendation-state\"),\n          \"expanded\"\n        );\n        assert.calledOnce(pageAction._dispatchImpression);\n        assert.calledWith(pageAction._dispatchImpression, fakeRecommendation);\n      });\n      it(\"should send telemetry if `expand` is true and the id and bucket_id are provided\", async () => {\n        await pageAction.showAddressBarNotifier(fakeRecommendation, true);\n        assert.calledWith(dispatchStub, {\n          type: \"DOORHANGER_TELEMETRY\",\n          data: {\n            action: \"cfr_user_event\",\n            source: \"CFR\",\n            message_id: fakeRecommendation.id,\n            bucket_id: fakeRecommendation.content.bucket_id,\n            event: \"IMPRESSION\",\n          },\n        });\n      });\n    });\n\n    describe(\"#hideAddressBarNotifier\", () => {\n      it(\"should hideAddressBarNotifier the container, cancel any state changes, and remove the state attribute\", () => {\n        sandbox.spy(pageAction, \"_clearScheduledStateChanges\");\n        pageAction.hideAddressBarNotifier();\n        assert.isTrue(pageAction.container.hidden);\n        assert.calledOnce(pageAction._clearScheduledStateChanges);\n        assert.isNull(\n          pageAction.urlbar.getAttribute(\"cfr-recommendation-state\")\n        );\n      });\n      it(\"should remove the `currentNotification`\", () => {\n        const notification = {};\n        pageAction.currentNotification = notification;\n        pageAction.hideAddressBarNotifier();\n        assert.calledWith(global.PopupNotifications.remove, notification);\n      });\n    });\n\n    describe(\"#_expand\", () => {\n      beforeEach(() => {\n        pageAction._clearScheduledStateChanges();\n        pageAction.urlbar.removeAttribute(\"cfr-recommendation-state\");\n      });\n      it(\"without a delay, should clear other state changes and set the state to 'expanded'\", () => {\n        sandbox.spy(pageAction, \"_clearScheduledStateChanges\");\n        pageAction._expand();\n        assert.calledOnce(pageAction._clearScheduledStateChanges);\n        assert.equal(\n          pageAction.urlbar.getAttribute(\"cfr-recommendation-state\"),\n          \"expanded\"\n        );\n      });\n      it(\"with a delay, should set the expanded state after the correct amount of time\", () => {\n        const delay = 1234;\n        pageAction._expand(delay);\n        // We expect that an expansion has been scheduled\n        assert.lengthOf(pageAction.stateTransitionTimeoutIDs, 1);\n        clock.tick(delay + 1);\n        assert.equal(\n          pageAction.urlbar.getAttribute(\"cfr-recommendation-state\"),\n          \"expanded\"\n        );\n      });\n    });\n\n    describe(\"#_collapse\", () => {\n      beforeEach(() => {\n        pageAction._clearScheduledStateChanges();\n        pageAction.urlbar.removeAttribute(\"cfr-recommendation-state\");\n      });\n      it(\"without a delay, should clear other state changes and set the state to collapsed only if it's already expanded\", () => {\n        sandbox.spy(pageAction, \"_clearScheduledStateChanges\");\n        pageAction._collapse();\n        assert.calledOnce(pageAction._clearScheduledStateChanges);\n        assert.isNull(\n          pageAction.urlbar.getAttribute(\"cfr-recommendation-state\")\n        );\n        pageAction.urlbar.setAttribute(\"cfr-recommendation-state\", \"expanded\");\n        pageAction._collapse();\n        assert.equal(\n          pageAction.urlbar.getAttribute(\"cfr-recommendation-state\"),\n          \"collapsed\"\n        );\n      });\n      it(\"with a delay, should set the collapsed state after the correct amount of time\", () => {\n        const delay = 1234;\n        pageAction._collapse(delay);\n        clock.tick(delay + 1);\n        // The state was _not_ \"expanded\" and so should not have been set to \"collapsed\"\n        assert.isNull(\n          pageAction.urlbar.getAttribute(\"cfr-recommendation-state\")\n        );\n\n        pageAction._expand();\n        pageAction._collapse(delay);\n        // We expect that a collapse has been scheduled\n        assert.lengthOf(pageAction.stateTransitionTimeoutIDs, 1);\n        clock.tick(delay + 1);\n        // This time it was \"expanded\" so should now (after the delay) be \"collapsed\"\n        assert.equal(\n          pageAction.urlbar.getAttribute(\"cfr-recommendation-state\"),\n          \"collapsed\"\n        );\n      });\n    });\n\n    describe(\"#_clearScheduledStateChanges\", () => {\n      it(\"should call .clearTimeout on all stored timeoutIDs\", () => {\n        pageAction.stateTransitionTimeoutIDs = [42, 73, 1997];\n        sandbox.spy(pageAction.window, \"clearTimeout\");\n        pageAction._clearScheduledStateChanges();\n        assert.calledThrice(pageAction.window.clearTimeout);\n        assert.calledWith(pageAction.window.clearTimeout, 42);\n        assert.calledWith(pageAction.window.clearTimeout, 73);\n        assert.calledWith(pageAction.window.clearTimeout, 1997);\n      });\n    });\n\n    describe(\"#_popupStateChange\", () => {\n      it(\"should collapse and remove the notification on 'dismissed'\", () => {\n        pageAction._expand();\n        const fakeNotification = {};\n\n        pageAction.currentNotification = fakeNotification;\n        pageAction._popupStateChange(\"dismissed\");\n        assert.equal(\n          pageAction.urlbar.getAttribute(\"cfr-recommendation-state\"),\n          \"collapsed\"\n        );\n        assert.calledOnce(global.PopupNotifications.remove);\n        assert.calledWith(global.PopupNotifications.remove, fakeNotification);\n      });\n      it(\"should collapse and remove the notification on 'removed'\", () => {\n        pageAction._expand();\n        const fakeNotification = {};\n\n        pageAction.currentNotification = fakeNotification;\n        pageAction._popupStateChange(\"removed\");\n        assert.equal(\n          pageAction.urlbar.getAttribute(\"cfr-recommendation-state\"),\n          \"collapsed\"\n        );\n        assert.calledOnce(global.PopupNotifications.remove);\n        assert.calledWith(global.PopupNotifications.remove, fakeNotification);\n      });\n      it(\"should do nothing for other states\", () => {\n        pageAction._popupStateChange(\"opened\");\n        assert.notCalled(global.PopupNotifications.remove);\n      });\n    });\n\n    describe(\"#dispatchUserAction\", () => {\n      it(\"should call ._dispatchToASRouter with the right action\", () => {\n        const fakeAction = {};\n        pageAction.dispatchUserAction(fakeAction);\n        assert.calledOnce(dispatchStub);\n        assert.calledWith(\n          dispatchStub,\n          { type: \"USER_ACTION\", data: fakeAction },\n          { browser: fakeBrowser }\n        );\n      });\n    });\n\n    describe(\"#_dispatchImpression\", () => {\n      it(\"should call ._dispatchToASRouter with the right action\", () => {\n        pageAction._dispatchImpression(\"fake impression\");\n        assert.calledWith(dispatchStub, {\n          type: \"IMPRESSION\",\n          data: \"fake impression\",\n        });\n      });\n    });\n\n    describe(\"#_sendTelemetry\", () => {\n      it(\"should call ._dispatchToASRouter with the right action\", () => {\n        const fakePing = { message_id: 42 };\n        pageAction._sendTelemetry(fakePing);\n        assert.calledWith(dispatchStub, {\n          type: \"DOORHANGER_TELEMETRY\",\n          data: { action: \"cfr_user_event\", source: \"CFR\", message_id: 42 },\n        });\n      });\n    });\n\n    describe(\"#_blockMessage\", () => {\n      it(\"should call ._dispatchToASRouter with the right action\", () => {\n        pageAction._blockMessage(\"fake id\");\n        assert.calledOnce(dispatchStub);\n        assert.calledWith(dispatchStub, {\n          type: \"BLOCK_MESSAGE_BY_ID\",\n          data: { id: \"fake id\" },\n        });\n      });\n    });\n\n    describe(\"#getStrings\", () => {\n      let formatMessagesStub;\n      const localeStrings = [\n        {\n          value: \"你好世界\",\n          attributes: [\n            { name: \"first_attr\", value: 42 },\n            { name: \"second_attr\", value: \"some string\" },\n            { name: \"third_attr\", value: [1, 2, 3] },\n          ],\n        },\n      ];\n\n      beforeEach(() => {\n        formatMessagesStub = sandbox\n          .stub()\n          .withArgs({ id: \"hello_world\" })\n          .resolves(localeStrings);\n        global.RemoteL10n.l10n.formatMessages = formatMessagesStub;\n      });\n\n      it(\"should return the argument if a string_id is not defined\", async () => {\n        assert.deepEqual(await pageAction.getStrings({}), {});\n        assert.equal(await pageAction.getStrings(\"some string\"), \"some string\");\n      });\n      it(\"should get the right locale string\", async () => {\n        assert.equal(\n          await pageAction.getStrings({ string_id: \"hello_world\" }),\n          localeStrings[0].value\n        );\n      });\n      it(\"should return the right sub-attribute if specified\", async () => {\n        assert.equal(\n          await pageAction.getStrings(\n            { string_id: \"hello_world\" },\n            \"second_attr\"\n          ),\n          \"some string\"\n        );\n      });\n      it(\"should attach attributes to string overrides\", async () => {\n        const fromJson = { value: \"Add Now\", attributes: { accesskey: \"A\" } };\n\n        const result = await pageAction.getStrings(fromJson);\n\n        assert.equal(result, fromJson.value);\n        assert.propertyVal(result.attributes, \"accesskey\", \"A\");\n      });\n      it(\"should return subAttributes when doing string overrides\", async () => {\n        const fromJson = { value: \"Add Now\", attributes: { accesskey: \"A\" } };\n\n        const result = await pageAction.getStrings(fromJson, \"accesskey\");\n\n        assert.equal(result, \"A\");\n      });\n      it(\"should resolve ftl strings and attach subAttributes\", async () => {\n        const fromFtl = { string_id: \"cfr-doorhanger-extension-ok-button\" };\n        formatMessagesStub.resolves([\n          { value: \"Add Now\", attributes: [{ name: \"accesskey\", value: \"A\" }] },\n        ]);\n\n        const result = await pageAction.getStrings(fromFtl);\n\n        assert.equal(result, \"Add Now\");\n        assert.propertyVal(result.attributes, \"accesskey\", \"A\");\n      });\n      it(\"should return subAttributes from ftl ids\", async () => {\n        const fromFtl = { string_id: \"cfr-doorhanger-extension-ok-button\" };\n        formatMessagesStub.resolves([\n          { value: \"Add Now\", attributes: [{ name: \"accesskey\", value: \"A\" }] },\n        ]);\n\n        const result = await pageAction.getStrings(fromFtl, \"accesskey\");\n\n        assert.equal(result, \"A\");\n      });\n      it(\"should report an error when no attributes are present but subAttribute is requested\", async () => {\n        const fromJson = { value: \"Foo\" };\n        const stub = sandbox.stub(global.Cu, \"reportError\");\n\n        await pageAction.getStrings(fromJson, \"accesskey\");\n\n        assert.calledOnce(stub);\n        stub.restore();\n      });\n    });\n\n    describe(\"#_showPopupOnClick\", () => {\n      let translateElementsStub;\n      let setAttributesStub;\n      let getStringsStub;\n      beforeEach(async () => {\n        CFRPageActions.PageActionMap.set(fakeBrowser.ownerGlobal, pageAction);\n        await CFRPageActions.addRecommendation(\n          fakeBrowser,\n          fakeHost,\n          fakeRecommendation,\n          dispatchStub\n        );\n        getStringsStub = sandbox.stub(pageAction, \"getStrings\").resolves(\"\");\n        getStringsStub\n          .callsFake(async a => a) // eslint-disable-line max-nested-callbacks\n          .withArgs({ string_id: \"primary_button_id\" })\n          .resolves({ value: \"Primary Button\", attributes: { accesskey: \"p\" } })\n          .withArgs({ string_id: \"secondary_button_id\" })\n          .resolves({\n            value: \"Secondary Button\",\n            attributes: { accesskey: \"s\" },\n          })\n          .withArgs({ string_id: \"secondary_button_id_2\" })\n          .resolves({\n            value: \"Secondary Button 2\",\n            attributes: { accesskey: \"a\" },\n          })\n          .withArgs({ string_id: \"secondary_button_id_3\" })\n          .resolves({\n            value: \"Secondary Button 3\",\n            attributes: { accesskey: \"g\" },\n          })\n          .withArgs(\n            sinon.match({\n              string_id: \"cfr-doorhanger-extension-learn-more-link\",\n            })\n          )\n          .resolves(\"Learn more\")\n          .withArgs(\n            sinon.match({ string_id: \"cfr-doorhanger-extension-total-users\" })\n          )\n          .callsFake(async ({ args }) => `${args.total} users`); // eslint-disable-line max-nested-callbacks\n\n        translateElementsStub = sandbox.stub().resolves();\n        setAttributesStub = sandbox.stub();\n        global.RemoteL10n.l10n.setAttributes = setAttributesStub;\n        global.RemoteL10n.l10n.translateElements = translateElementsStub;\n      });\n\n      it(\"should call `.hideAddressBarNotifier` and do nothing if there is no recommendation for the selected browser\", async () => {\n        sandbox.spy(pageAction, \"hideAddressBarNotifier\");\n        CFRPageActions.RecommendationMap.delete(fakeBrowser);\n        await pageAction._showPopupOnClick({});\n        assert.calledOnce(pageAction.hideAddressBarNotifier);\n        assert.notCalled(global.PopupNotifications.show);\n      });\n      it(\"should cancel any planned state changes\", async () => {\n        sandbox.spy(pageAction, \"_clearScheduledStateChanges\");\n        assert.notCalled(pageAction._clearScheduledStateChanges);\n        await pageAction._showPopupOnClick({});\n        assert.calledOnce(pageAction._clearScheduledStateChanges);\n      });\n      it(\"should set the right text values\", async () => {\n        await pageAction._showPopupOnClick({});\n        const headerLabel = elements[\"cfr-notification-header-label\"];\n        const headerLink = elements[\"cfr-notification-header-link\"];\n        const headerImage = elements[\"cfr-notification-header-image\"];\n        const footerText = elements[\"cfr-notification-footer-text\"];\n        const footerLink = elements[\"cfr-notification-footer-learn-more-link\"];\n        assert.equal(\n          headerLabel.value,\n          fakeRecommendation.content.heading_text\n        );\n        assert.isTrue(\n          headerLink\n            .getAttribute(\"href\")\n            .endsWith(fakeRecommendation.content.info_icon.sumo_path)\n        );\n        assert.equal(\n          headerImage.getAttribute(\"tooltiptext\"),\n          fakeRecommendation.content.info_icon.label\n        );\n        assert.equal(footerText.textContent, fakeRecommendation.content.text);\n        assert.equal(footerLink.value, \"Learn more\");\n        assert.equal(\n          footerLink.getAttribute(\"href\"),\n          fakeRecommendation.content.addon.amo_url\n        );\n      });\n      it(\"should add the rating correctly\", async () => {\n        await pageAction._showPopupOnClick();\n        const footerFilledStars =\n          elements[\"cfr-notification-footer-filled-stars\"];\n        const footerEmptyStars =\n          elements[\"cfr-notification-footer-empty-stars\"];\n        // .toFixed to sort out some floating precision errors\n        assert.equal(\n          footerFilledStars.style.width,\n          `${(4.2 * 17).toFixed(1)}px`\n        );\n        assert.equal(\n          footerEmptyStars.style.width,\n          `${(0.8 * 17).toFixed(1)}px`\n        );\n      });\n      it(\"should add the number of users correctly\", async () => {\n        await pageAction._showPopupOnClick();\n        const footerUsers = elements[\"cfr-notification-footer-users\"];\n        assert.isNull(footerUsers.getAttribute(\"hidden\"));\n        assert.equal(\n          footerUsers.getAttribute(\"value\"),\n          `${fakeRecommendation.content.addon.users} users`\n        );\n      });\n      it(\"should send the right telemetry\", async () => {\n        await pageAction._showPopupOnClick();\n        assert.calledWith(dispatchStub, {\n          type: \"DOORHANGER_TELEMETRY\",\n          data: {\n            action: \"cfr_user_event\",\n            source: \"CFR\",\n            message_id: fakeRecommendation.id,\n            bucket_id: fakeRecommendation.content.bucket_id,\n            event: \"CLICK_DOORHANGER\",\n          },\n        });\n      });\n      it(\"should send modelVersion if presented in the message\", async () => {\n        const recommendationWithModelVersion = {\n          ...fakeRecommendation,\n          personalizedModelVersion: \"model_version_1\",\n        };\n        CFRPageActions.clearRecommendations();\n        await CFRPageActions.addRecommendation(\n          fakeBrowser,\n          fakeHost,\n          recommendationWithModelVersion,\n          dispatchStub\n        );\n        await pageAction._showPopupOnClick();\n\n        assert.calledWith(dispatchStub, {\n          type: \"DOORHANGER_TELEMETRY\",\n          data: {\n            action: \"cfr_user_event\",\n            source: \"CFR\",\n            message_id: fakeRecommendation.id,\n            bucket_id: fakeRecommendation.content.bucket_id,\n            event: \"CLICK_DOORHANGER\",\n            event_context: { modelVersion: \"model_version_1\" },\n          },\n        });\n      });\n      it(\"should set the main action correctly\", async () => {\n        sinon\n          .stub(CFRPageActions, \"_fetchLatestAddonVersion\")\n          .resolves(\"latest-addon.xpi\");\n        await pageAction._showPopupOnClick();\n        const mainAction = global.PopupNotifications.show.firstCall.args[4]; // eslint-disable-line prefer-destructuring\n        assert.deepEqual(mainAction.label, {\n          value: \"Primary Button\",\n          attributes: { accesskey: \"p\" },\n        });\n        sandbox.spy(pageAction, \"hideAddressBarNotifier\");\n        await mainAction.callback();\n        assert.calledOnce(pageAction.hideAddressBarNotifier);\n        // Should block the message\n        assert.calledWith(dispatchStub, {\n          type: \"BLOCK_MESSAGE_BY_ID\",\n          data: { id: fakeRecommendation.id },\n        });\n        // Should trigger the action\n        assert.calledWith(\n          dispatchStub,\n          {\n            type: \"USER_ACTION\",\n            data: { id: \"primary_action\", data: { url: \"latest-addon.xpi\" } },\n          },\n          { browser: fakeBrowser }\n        );\n        // Should send telemetry\n        assert.calledWith(dispatchStub, {\n          type: \"DOORHANGER_TELEMETRY\",\n          data: {\n            action: \"cfr_user_event\",\n            source: \"CFR\",\n            message_id: fakeRecommendation.id,\n            bucket_id: fakeRecommendation.content.bucket_id,\n            event: \"INSTALL\",\n          },\n        });\n        // Should remove the recommendation\n        assert.isFalse(CFRPageActions.RecommendationMap.has(fakeBrowser));\n      });\n      it(\"should set the secondary action correctly\", async () => {\n        await pageAction._showPopupOnClick();\n        // eslint-disable-next-line prefer-destructuring\n        const [\n          secondaryAction,\n        ] = global.PopupNotifications.show.firstCall.args[5];\n\n        assert.deepEqual(secondaryAction.label, {\n          value: \"Secondary Button\",\n          attributes: { accesskey: \"s\" },\n        });\n        sandbox.spy(pageAction, \"hideAddressBarNotifier\");\n        CFRPageActions.RecommendationMap.set(fakeBrowser, {});\n        secondaryAction.callback();\n        // Should send telemetry\n        assert.calledWith(dispatchStub, {\n          type: \"DOORHANGER_TELEMETRY\",\n          data: {\n            action: \"cfr_user_event\",\n            source: \"CFR\",\n            message_id: fakeRecommendation.id,\n            bucket_id: fakeRecommendation.content.bucket_id,\n            event: \"DISMISS\",\n          },\n        });\n        // Don't remove the recommendation on `DISMISS` action\n        assert.isTrue(CFRPageActions.RecommendationMap.has(fakeBrowser));\n        assert.notCalled(pageAction.hideAddressBarNotifier);\n      });\n      it(\"should send right telemetry for BLOCK secondary action\", async () => {\n        await pageAction._showPopupOnClick();\n        // eslint-disable-next-line prefer-destructuring\n        const blockAction = global.PopupNotifications.show.firstCall.args[5][1];\n\n        assert.deepEqual(blockAction.label, {\n          value: \"Secondary Button 2\",\n          attributes: { accesskey: \"a\" },\n        });\n        sandbox.spy(pageAction, \"hideAddressBarNotifier\");\n        sandbox.spy(pageAction, \"_blockMessage\");\n        CFRPageActions.RecommendationMap.set(fakeBrowser, {});\n        blockAction.callback();\n        assert.calledOnce(pageAction.hideAddressBarNotifier);\n        assert.calledOnce(pageAction._blockMessage);\n        // Should send telemetry\n        assert.calledWith(dispatchStub, {\n          type: \"DOORHANGER_TELEMETRY\",\n          data: {\n            action: \"cfr_user_event\",\n            source: \"CFR\",\n            message_id: fakeRecommendation.id,\n            bucket_id: fakeRecommendation.content.bucket_id,\n            event: \"BLOCK\",\n          },\n        });\n        // Should remove the recommendation\n        assert.isFalse(CFRPageActions.RecommendationMap.has(fakeBrowser));\n      });\n      it(\"should send right telemetry for MANAGE secondary action\", async () => {\n        await pageAction._showPopupOnClick();\n        // eslint-disable-next-line prefer-destructuring\n        const manageAction =\n          global.PopupNotifications.show.firstCall.args[5][2];\n\n        assert.deepEqual(manageAction.label, {\n          value: \"Secondary Button 3\",\n          attributes: { accesskey: \"g\" },\n        });\n        sandbox.spy(pageAction, \"hideAddressBarNotifier\");\n        CFRPageActions.RecommendationMap.set(fakeBrowser, {});\n        manageAction.callback();\n        // Should send telemetry\n        assert.calledWith(dispatchStub, {\n          type: \"DOORHANGER_TELEMETRY\",\n          data: {\n            action: \"cfr_user_event\",\n            source: \"CFR\",\n            message_id: fakeRecommendation.id,\n            bucket_id: fakeRecommendation.content.bucket_id,\n            event: \"MANAGE\",\n          },\n        });\n        // Don't remove the recommendation on `MANAGE` action\n        assert.isTrue(CFRPageActions.RecommendationMap.has(fakeBrowser));\n        assert.notCalled(pageAction.hideAddressBarNotifier);\n      });\n      it(\"should call PopupNotifications.show with the right arguments\", async () => {\n        await pageAction._showPopupOnClick();\n        assert.calledWith(\n          global.PopupNotifications.show,\n          fakeBrowser,\n          \"contextual-feature-recommendation\",\n          fakeRecommendation.content.addon.title,\n          \"cfr\",\n          sinon.match.any, // Corresponds to the main action, tested above\n          sinon.match.any, // Corresponds to the secondary action, tested above\n          {\n            popupIconURL: fakeRecommendation.content.addon.icon,\n            hideClose: true,\n            eventCallback: pageAction._popupStateChange,\n          }\n        );\n      });\n      it(\"should show the bullet list details\", async () => {\n        fakeRecommendation.content.layout = \"message_and_animation\";\n        await pageAction._showPopupOnClick();\n\n        assert.calledOnce(translateElementsStub);\n      });\n      it(\"should set the data-l10n-id on the list element\", async () => {\n        fakeRecommendation.content.layout = \"message_and_animation\";\n        await pageAction._showPopupOnClick();\n\n        assert.calledOnce(setAttributesStub);\n        assert.calledWith(\n          setAttributesStub,\n          sinon.match.any,\n          fakeRecommendation.content.descriptionDetails.steps[0].string_id\n        );\n      });\n      it(\"should set the correct data-notification-category\", async () => {\n        fakeRecommendation.content.layout = \"message_and_animation\";\n        await pageAction._showPopupOnClick();\n\n        assert.equal(\n          elements[\"contextual-feature-recommendation-notification\"].dataset\n            .notificationCategory,\n          fakeRecommendation.content.layout\n        );\n      });\n      it(\"should send PIN event on primary action click\", async () => {\n        fakeRecommendation.content.layout = \"message_and_animation\";\n        sandbox.stub(pageAction, \"_sendTelemetry\");\n        await pageAction._showPopupOnClick();\n\n        const [\n          ,\n          ,\n          ,\n          ,\n          { callback },\n        ] = global.PopupNotifications.show.firstCall.args;\n        callback();\n\n        // First call is triggered by `_showPopupOnClick`\n        assert.propertyVal(\n          pageAction._sendTelemetry.secondCall.args[0],\n          \"event\",\n          \"PIN\"\n        );\n      });\n    });\n  });\n\n  describe(\"CFRPageActions\", () => {\n    beforeEach(() => {\n      // Spy on the prototype methods to inspect calls for any PageAction instance\n      sandbox.spy(PageAction.prototype, \"showAddressBarNotifier\");\n      sandbox.spy(PageAction.prototype, \"hideAddressBarNotifier\");\n    });\n\n    describe(\"updatePageActions\", () => {\n      let savedRec;\n\n      beforeEach(() => {\n        const win = fakeBrowser.ownerGlobal;\n        CFRPageActions.PageActionMap.set(\n          win,\n          new PageAction(win, dispatchStub)\n        );\n        const { id, content } = fakeRecommendation;\n        savedRec = {\n          id,\n          host: fakeHost,\n          content,\n        };\n        CFRPageActions.RecommendationMap.set(fakeBrowser, savedRec);\n      });\n\n      it(\"should do nothing if a pageAction doesn't exist for the window\", () => {\n        const win = fakeBrowser.ownerGlobal;\n        CFRPageActions.PageActionMap.delete(win);\n        CFRPageActions.updatePageActions(fakeBrowser);\n        assert.notCalled(PageAction.prototype.showAddressBarNotifier);\n        assert.notCalled(PageAction.prototype.hideAddressBarNotifier);\n      });\n      it(\"should do nothing if the browser is not the `selectedBrowser`\", () => {\n        const someOtherFakeBrowser = {};\n        CFRPageActions.updatePageActions(someOtherFakeBrowser);\n        assert.notCalled(PageAction.prototype.showAddressBarNotifier);\n        assert.notCalled(PageAction.prototype.hideAddressBarNotifier);\n      });\n      it(\"should hideAddressBarNotifier the pageAction if a recommendation doesn't exist for the given browser\", () => {\n        CFRPageActions.RecommendationMap.delete(fakeBrowser);\n        CFRPageActions.updatePageActions(fakeBrowser);\n        assert.calledOnce(PageAction.prototype.hideAddressBarNotifier);\n      });\n      it(\"should show the pageAction if a recommendation exists and the host matches\", () => {\n        CFRPageActions.updatePageActions(fakeBrowser);\n        assert.calledOnce(PageAction.prototype.showAddressBarNotifier);\n        assert.calledWith(\n          PageAction.prototype.showAddressBarNotifier,\n          savedRec\n        );\n      });\n      it(\"should show the pageAction if a recommendation exists and it doesn't have a host defined\", () => {\n        const recNoHost = { ...savedRec, host: undefined };\n        CFRPageActions.RecommendationMap.set(fakeBrowser, recNoHost);\n        CFRPageActions.updatePageActions(fakeBrowser);\n        assert.calledOnce(PageAction.prototype.showAddressBarNotifier);\n        assert.calledWith(\n          PageAction.prototype.showAddressBarNotifier,\n          recNoHost\n        );\n      });\n      it(\"should hideAddressBarNotifier the pageAction and delete the recommendation if the recommendation exists but the host doesn't match\", () => {\n        const someOtherFakeHost = \"subdomain.mozilla.com\";\n        fakeBrowser.documentURI.host = someOtherFakeHost;\n        assert.isTrue(CFRPageActions.RecommendationMap.has(fakeBrowser));\n        CFRPageActions.updatePageActions(fakeBrowser);\n        assert.calledOnce(PageAction.prototype.hideAddressBarNotifier);\n        assert.isFalse(CFRPageActions.RecommendationMap.has(fakeBrowser));\n      });\n      it(\"should not call `delete` if retain is true\", () => {\n        savedRec.retain = true;\n        fakeBrowser.documentURI.host = \"subdomain.mozilla.com\";\n        assert.isTrue(CFRPageActions.RecommendationMap.has(fakeBrowser));\n\n        CFRPageActions.updatePageActions(fakeBrowser);\n        assert.propertyVal(savedRec, \"retain\", false);\n        assert.calledOnce(PageAction.prototype.hideAddressBarNotifier);\n        assert.isTrue(CFRPageActions.RecommendationMap.has(fakeBrowser));\n      });\n      it(\"should call `delete` if retain is false\", () => {\n        savedRec.retain = false;\n        fakeBrowser.documentURI.host = \"subdomain.mozilla.com\";\n        assert.isTrue(CFRPageActions.RecommendationMap.has(fakeBrowser));\n\n        CFRPageActions.updatePageActions(fakeBrowser);\n        assert.propertyVal(savedRec, \"retain\", false);\n        assert.calledOnce(PageAction.prototype.hideAddressBarNotifier);\n        assert.isFalse(CFRPageActions.RecommendationMap.has(fakeBrowser));\n      });\n    });\n\n    describe(\"forceRecommendation\", () => {\n      it(\"should succeed and add an element to the RecommendationMap\", async () => {\n        assert.isTrue(\n          await CFRPageActions.forceRecommendation(\n            { browser: fakeBrowser },\n            fakeRecommendation,\n            dispatchStub\n          )\n        );\n        assert.deepInclude(CFRPageActions.RecommendationMap.get(fakeBrowser), {\n          id: fakeRecommendation.id,\n          content: fakeRecommendation.content,\n        });\n      });\n      it(\"should create a PageAction if one doesn't exist for the window, save it in the PageActionMap, and call `show`\", async () => {\n        const win = fakeBrowser.ownerGlobal;\n        assert.isFalse(CFRPageActions.PageActionMap.has(win));\n        await CFRPageActions.forceRecommendation(\n          { browser: fakeBrowser },\n          fakeRecommendation,\n          dispatchStub\n        );\n        const pageAction = CFRPageActions.PageActionMap.get(win);\n        assert.equal(win, pageAction.window);\n        assert.equal(dispatchStub, pageAction._dispatchToASRouter);\n        assert.calledOnce(PageAction.prototype.showAddressBarNotifier);\n      });\n    });\n\n    describe(\"addRecommendation\", () => {\n      it(\"should fail and not add a recommendation if the browser is part of a private window\", async () => {\n        global.PrivateBrowsingUtils.isWindowPrivate.returns(true);\n        assert.isFalse(\n          await CFRPageActions.addRecommendation(\n            fakeBrowser,\n            fakeHost,\n            fakeRecommendation,\n            dispatchStub\n          )\n        );\n        assert.isFalse(CFRPageActions.RecommendationMap.has(fakeBrowser));\n      });\n      it(\"should fail and not add a recommendation if the browser is not the selected browser\", async () => {\n        global.gBrowser.selectedBrowser = {}; // Some other browser\n        assert.isFalse(\n          await CFRPageActions.addRecommendation(\n            fakeBrowser,\n            fakeHost,\n            fakeRecommendation,\n            dispatchStub\n          )\n        );\n      });\n      it(\"should fail and not add a recommendation if the host doesn't match\", async () => {\n        const someOtherFakeHost = \"subdomain.mozilla.com\";\n        assert.isFalse(\n          await CFRPageActions.addRecommendation(\n            fakeBrowser,\n            someOtherFakeHost,\n            fakeRecommendation,\n            dispatchStub\n          )\n        );\n      });\n      it(\"should otherwise succeed and add an element to the RecommendationMap\", async () => {\n        assert.isTrue(\n          await CFRPageActions.addRecommendation(\n            fakeBrowser,\n            fakeHost,\n            fakeRecommendation,\n            dispatchStub\n          )\n        );\n        assert.deepInclude(CFRPageActions.RecommendationMap.get(fakeBrowser), {\n          id: fakeRecommendation.id,\n          host: fakeHost,\n          content: fakeRecommendation.content,\n        });\n      });\n      it(\"should create a PageAction if one doesn't exist for the window, save it in the PageActionMap, and call `show`\", async () => {\n        const win = fakeBrowser.ownerGlobal;\n        assert.isFalse(CFRPageActions.PageActionMap.has(win));\n        await CFRPageActions.addRecommendation(\n          fakeBrowser,\n          fakeHost,\n          fakeRecommendation,\n          dispatchStub\n        );\n        const pageAction = CFRPageActions.PageActionMap.get(win);\n        assert.equal(win, pageAction.window);\n        assert.equal(dispatchStub, pageAction._dispatchToASRouter);\n        assert.calledOnce(PageAction.prototype.showAddressBarNotifier);\n      });\n      it(\"should add the right url if we fetched and addon install URL\", async () => {\n        fakeRecommendation.template = \"cfr_doorhanger\";\n        await CFRPageActions.addRecommendation(\n          fakeBrowser,\n          fakeHost,\n          fakeRecommendation,\n          dispatchStub\n        );\n        const recommendation = CFRPageActions.RecommendationMap.get(\n          fakeBrowser\n        );\n\n        // sanity check - just go through some of the rest of the attributes to make sure they were untouched\n        assert.equal(recommendation.id, fakeRecommendation.id);\n        assert.equal(\n          recommendation.content.heading_text,\n          fakeRecommendation.content.heading_text\n        );\n        assert.equal(\n          recommendation.content.addon,\n          fakeRecommendation.content.addon\n        );\n        assert.equal(\n          recommendation.content.text,\n          fakeRecommendation.content.text\n        );\n        assert.equal(\n          recommendation.content.buttons.secondary,\n          fakeRecommendation.content.buttons.secondary\n        );\n        assert.equal(\n          recommendation.content.buttons.primary.action.id,\n          fakeRecommendation.content.buttons.primary.action.id\n        );\n\n        delete fakeRecommendation.template;\n      });\n      it(\"should prevent a second message if one is currently displayed\", async () => {\n        const secondMessage = { ...fakeRecommendation, id: \"second_message\" };\n        let messageAdded = await CFRPageActions.addRecommendation(\n          fakeBrowser,\n          fakeHost,\n          fakeRecommendation,\n          dispatchStub\n        );\n\n        assert.isTrue(messageAdded);\n        assert.deepInclude(CFRPageActions.RecommendationMap.get(fakeBrowser), {\n          id: fakeRecommendation.id,\n          host: fakeHost,\n          content: fakeRecommendation.content,\n        });\n\n        messageAdded = await CFRPageActions.addRecommendation(\n          fakeBrowser,\n          fakeHost,\n          secondMessage,\n          dispatchStub\n        );\n        // Adding failed\n        assert.isFalse(messageAdded);\n        // First message is still there\n        assert.deepInclude(CFRPageActions.RecommendationMap.get(fakeBrowser), {\n          id: fakeRecommendation.id,\n          host: fakeHost,\n          content: fakeRecommendation.content,\n        });\n      });\n      it(\"should send impressions just for the first message\", async () => {\n        const secondMessage = { ...fakeRecommendation, id: \"second_message\" };\n        await CFRPageActions.addRecommendation(\n          fakeBrowser,\n          fakeHost,\n          fakeRecommendation,\n          dispatchStub\n        );\n        await CFRPageActions.addRecommendation(\n          fakeBrowser,\n          fakeHost,\n          secondMessage,\n          dispatchStub\n        );\n\n        // Doorhanger telemetry + Impression for just 1 message\n        assert.calledTwice(dispatchStub);\n        const [firstArgs] = dispatchStub.firstCall.args;\n        const [secondArgs] = dispatchStub.secondCall.args;\n        assert.equal(firstArgs.data.id, secondArgs.data.message_id);\n      });\n    });\n\n    describe(\"clearRecommendations\", () => {\n      const createFakePageAction = () => ({\n        hideAddressBarNotifier: sandbox.stub(),\n      });\n      const windows = [{}, {}, { closed: true }];\n      const browsers = [{}, {}, {}, {}];\n\n      beforeEach(() => {\n        CFRPageActions.PageActionMap.set(windows[0], createFakePageAction());\n        CFRPageActions.PageActionMap.set(windows[2], createFakePageAction());\n        for (const browser of browsers) {\n          CFRPageActions.RecommendationMap.set(browser, {});\n        }\n        globals.set({ Services: { wm: { getEnumerator: () => windows } } });\n      });\n\n      it(\"should hideAddressBarNotifier the PageActions of any existing, non-closed windows\", () => {\n        const pageActions = windows.map(win =>\n          CFRPageActions.PageActionMap.get(win)\n        );\n        CFRPageActions.clearRecommendations();\n\n        // Only the first window had a PageAction and wasn't closed\n        assert.calledOnce(pageActions[0].hideAddressBarNotifier);\n        assert.isUndefined(pageActions[1]);\n        assert.notCalled(pageActions[2].hideAddressBarNotifier);\n      });\n      it(\"should clear the PageActionMap and the RecommendationMap\", () => {\n        CFRPageActions.clearRecommendations();\n\n        // Both are WeakMaps and so are not iterable, cannot be cleared, and\n        // cannot have their length queried directly, so we have to check\n        // whether previous elements still exist\n        assert.lengthOf(windows, 3);\n        for (const win of windows) {\n          assert.isFalse(CFRPageActions.PageActionMap.has(win));\n        }\n        assert.lengthOf(browsers, 4);\n        for (const browser of browsers) {\n          assert.isFalse(CFRPageActions.RecommendationMap.has(browser));\n        }\n      });\n    });\n\n    describe(\"reloadL10n\", () => {\n      const createFakePageAction = () => ({\n        hideAddressBarNotifier() {},\n        reloadL10n: sandbox.stub(),\n      });\n      const windows = [{}, {}, { closed: true }];\n\n      beforeEach(() => {\n        CFRPageActions.PageActionMap.set(windows[0], createFakePageAction());\n        CFRPageActions.PageActionMap.set(windows[2], createFakePageAction());\n        globals.set({ Services: { wm: { getEnumerator: () => windows } } });\n      });\n\n      it(\"should call reloadL10n for all the PageActions of any existing, non-closed windows\", () => {\n        const pageActions = windows.map(win =>\n          CFRPageActions.PageActionMap.get(win)\n        );\n        CFRPageActions.reloadL10n();\n\n        // Only the first window had a PageAction and wasn't closed\n        assert.calledOnce(pageActions[0].reloadL10n);\n        assert.isUndefined(pageActions[1]);\n        assert.notCalled(pageActions[2].reloadL10n);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/asrouter/MessageLoaderUtils.test.js",
    "content": "import { GlobalOverrider } from \"test/unit/utils\";\nimport { MessageLoaderUtils } from \"lib/ASRouter.jsm\";\nconst { STARTPAGE_VERSION } = MessageLoaderUtils;\n\nconst FAKE_OPTIONS = {\n  storage: {\n    set() {\n      return Promise.resolve();\n    },\n    get() {\n      return Promise.resolve();\n    },\n  },\n  dispatchToAS: () => {},\n};\nconst FAKE_RESPONSE_HEADERS = { get() {} };\n\ndescribe(\"MessageLoaderUtils\", () => {\n  let fetchStub;\n  let clock;\n  let sandbox;\n\n  beforeEach(() => {\n    sandbox = sinon.createSandbox();\n    clock = sinon.useFakeTimers();\n    fetchStub = sinon.stub(global, \"fetch\");\n  });\n  afterEach(() => {\n    sandbox.restore();\n    clock.restore();\n    fetchStub.restore();\n  });\n\n  describe(\"#loadMessagesForProvider\", () => {\n    it(\"should return messages for a local provider with hardcoded messages\", async () => {\n      const sourceMessage = { id: \"foo\" };\n      const provider = {\n        id: \"provider123\",\n        type: \"local\",\n        messages: [sourceMessage],\n      };\n\n      const result = await MessageLoaderUtils.loadMessagesForProvider(\n        provider,\n        FAKE_OPTIONS\n      );\n\n      assert.isArray(result.messages);\n      // Does the message have the right properties?\n      const [message] = result.messages;\n      assert.propertyVal(message, \"id\", \"foo\");\n      assert.propertyVal(message, \"provider\", \"provider123\");\n    });\n    it(\"should filter out local messages listed in the `exclude` field\", async () => {\n      const sourceMessage = { id: \"foo\" };\n      const provider = {\n        id: \"provider123\",\n        type: \"local\",\n        messages: [sourceMessage],\n        exclude: [\"foo\"],\n      };\n\n      const result = await MessageLoaderUtils.loadMessagesForProvider(\n        provider,\n        FAKE_OPTIONS\n      );\n\n      assert.lengthOf(result.messages, 0);\n    });\n    it(\"should return messages for remote provider\", async () => {\n      const sourceMessage = { id: \"foo\" };\n      fetchStub.resolves({\n        ok: true,\n        status: 200,\n        json: () => Promise.resolve({ messages: [sourceMessage] }),\n        headers: FAKE_RESPONSE_HEADERS,\n      });\n      const provider = {\n        id: \"provider123\",\n        type: \"remote\",\n        url: \"https://foo.com\",\n      };\n\n      const result = await MessageLoaderUtils.loadMessagesForProvider(\n        provider,\n        FAKE_OPTIONS\n      );\n      assert.isArray(result.messages);\n      // Does the message have the right properties?\n      const [message] = result.messages;\n      assert.propertyVal(message, \"id\", \"foo\");\n      assert.propertyVal(message, \"provider\", \"provider123\");\n      assert.propertyVal(message, \"provider_url\", \"https://foo.com\");\n    });\n    describe(\"remote provider HTTP codes\", () => {\n      const testMessage = { id: \"foo\" };\n      const provider = {\n        id: \"provider123\",\n        type: \"remote\",\n        url: \"https://foo.com\",\n        updateCycleInMs: 300,\n      };\n      const respJson = { messages: [testMessage] };\n\n      function assertReturnsCorrectMessages(actual) {\n        assert.isArray(actual.messages);\n        // Does the message have the right properties?\n        const [message] = actual.messages;\n        assert.propertyVal(message, \"id\", testMessage.id);\n        assert.propertyVal(message, \"provider\", provider.id);\n        assert.propertyVal(message, \"provider_url\", provider.url);\n      }\n\n      it(\"should return messages for 200 response\", async () => {\n        fetchStub.resolves({\n          ok: true,\n          status: 200,\n          json: () => Promise.resolve(respJson),\n          headers: FAKE_RESPONSE_HEADERS,\n        });\n        assertReturnsCorrectMessages(\n          await MessageLoaderUtils.loadMessagesForProvider(\n            provider,\n            FAKE_OPTIONS\n          )\n        );\n      });\n\n      it(\"should return messages for a 302 response with json\", async () => {\n        fetchStub.resolves({\n          ok: true,\n          status: 302,\n          json: () => Promise.resolve(respJson),\n          headers: FAKE_RESPONSE_HEADERS,\n        });\n        assertReturnsCorrectMessages(\n          await MessageLoaderUtils.loadMessagesForProvider(\n            provider,\n            FAKE_OPTIONS\n          )\n        );\n      });\n\n      it(\"should return an empty array for a 204 response\", async () => {\n        fetchStub.resolves({\n          ok: true,\n          status: 204,\n          json: () => \"\",\n          headers: FAKE_RESPONSE_HEADERS,\n        });\n        const result = await MessageLoaderUtils.loadMessagesForProvider(\n          provider,\n          FAKE_OPTIONS\n        );\n        assert.deepEqual(result.messages, []);\n      });\n\n      it(\"should return an empty array for a 500 response\", async () => {\n        fetchStub.resolves({\n          ok: false,\n          status: 500,\n          json: () => \"\",\n          headers: FAKE_RESPONSE_HEADERS,\n        });\n        const result = await MessageLoaderUtils.loadMessagesForProvider(\n          provider,\n          FAKE_OPTIONS\n        );\n        assert.deepEqual(result.messages, []);\n      });\n\n      it(\"should return cached messages for a 304 response\", async () => {\n        clock.tick(302);\n        const messages = [{ id: \"message-1\" }, { id: \"message-2\" }];\n        const fakeStorage = {\n          set() {\n            return Promise.resolve();\n          },\n          get() {\n            return Promise.resolve({\n              [provider.id]: {\n                version: STARTPAGE_VERSION,\n                url: provider.url,\n                messages,\n                etag: \"etag0987654321\",\n                lastFetched: 1,\n              },\n            });\n          },\n        };\n        fetchStub.resolves({\n          ok: true,\n          status: 304,\n          json: () => \"\",\n          headers: FAKE_RESPONSE_HEADERS,\n        });\n        const result = await MessageLoaderUtils.loadMessagesForProvider(\n          provider,\n          { ...FAKE_OPTIONS, storage: fakeStorage }\n        );\n        assert.equal(result.messages.length, messages.length);\n        messages.forEach(message => {\n          assert.ok(result.messages.find(m => m.id === message.id));\n        });\n      });\n\n      it(\"should return an empty array if json doesn't parse properly\", async () => {\n        fetchStub.resolves({\n          ok: false,\n          status: 200,\n          json: () => \"\",\n          headers: FAKE_RESPONSE_HEADERS,\n        });\n        const result = await MessageLoaderUtils.loadMessagesForProvider(\n          provider,\n          FAKE_OPTIONS\n        );\n        assert.deepEqual(result.messages, []);\n      });\n\n      it(\"should report response parsing errors with MessageLoaderUtils.reportError\", async () => {\n        const err = {};\n        sandbox.spy(MessageLoaderUtils, \"reportError\");\n        fetchStub.resolves({\n          ok: true,\n          status: 200,\n          json: sandbox.stub().rejects(err),\n          headers: FAKE_RESPONSE_HEADERS,\n        });\n        await MessageLoaderUtils.loadMessagesForProvider(\n          provider,\n          FAKE_OPTIONS\n        );\n\n        assert.calledOnce(MessageLoaderUtils.reportError);\n        // Report that json parsing failed\n        assert.calledWith(MessageLoaderUtils.reportError, err);\n      });\n\n      it(\"should report missing `messages` with MessageLoaderUtils.reportError\", async () => {\n        sandbox.spy(MessageLoaderUtils, \"reportError\");\n        fetchStub.resolves({\n          ok: true,\n          status: 200,\n          json: sandbox.stub().resolves({}),\n          headers: FAKE_RESPONSE_HEADERS,\n        });\n        await MessageLoaderUtils.loadMessagesForProvider(\n          provider,\n          FAKE_OPTIONS\n        );\n\n        assert.calledOnce(MessageLoaderUtils.reportError);\n        // Report no messages returned\n        assert.calledWith(\n          MessageLoaderUtils.reportError,\n          \"No messages returned from https://foo.com.\"\n        );\n      });\n\n      it(\"should report bad status responses with MessageLoaderUtils.reportError\", async () => {\n        sandbox.spy(MessageLoaderUtils, \"reportError\");\n        fetchStub.resolves({\n          ok: false,\n          status: 500,\n          json: sandbox.stub().resolves({}),\n          headers: FAKE_RESPONSE_HEADERS,\n        });\n        await MessageLoaderUtils.loadMessagesForProvider(\n          provider,\n          FAKE_OPTIONS\n        );\n\n        assert.calledOnce(MessageLoaderUtils.reportError);\n        // Report no messages returned\n        assert.calledWith(\n          MessageLoaderUtils.reportError,\n          \"Invalid response status 500 from https://foo.com.\"\n        );\n      });\n\n      it(\"should return an empty array if the request rejects\", async () => {\n        fetchStub.rejects(new Error(\"something went wrong\"));\n        const result = await MessageLoaderUtils.loadMessagesForProvider(\n          provider,\n          FAKE_OPTIONS\n        );\n        assert.deepEqual(result.messages, []);\n      });\n    });\n    describe(\"remote provider caching\", () => {\n      const provider = {\n        id: \"provider123\",\n        type: \"remote\",\n        url: \"https://foo.com\",\n        updateCycleInMs: 300,\n      };\n\n      it(\"should return cached results if they aren't expired\", async () => {\n        clock.tick(1);\n        const messages = [{ id: \"message-1\" }, { id: \"message-2\" }];\n        const fakeStorage = {\n          set() {\n            return Promise.resolve();\n          },\n          get() {\n            return Promise.resolve({\n              [provider.id]: {\n                version: STARTPAGE_VERSION,\n                url: provider.url,\n                messages,\n                etag: \"etag0987654321\",\n                lastFetched: Date.now(),\n              },\n            });\n          },\n        };\n        const result = await MessageLoaderUtils.loadMessagesForProvider(\n          provider,\n          { ...FAKE_OPTIONS, storage: fakeStorage }\n        );\n        assert.equal(result.messages.length, messages.length);\n        messages.forEach(message => {\n          assert.ok(result.messages.find(m => m.id === message.id));\n        });\n      });\n\n      it(\"should return fetch results if the cache messages are expired\", async () => {\n        clock.tick(302);\n        const testMessage = { id: \"foo\" };\n        const respJson = { messages: [testMessage] };\n        const fakeStorage = {\n          set() {\n            return Promise.resolve();\n          },\n          get() {\n            return Promise.resolve({\n              [provider.id]: {\n                version: STARTPAGE_VERSION,\n                url: provider.url,\n                messages: [{ id: \"message-1\" }, { id: \"message-2\" }],\n                etag: \"etag0987654321\",\n                lastFetched: 1,\n              },\n            });\n          },\n        };\n        fetchStub.resolves({\n          ok: true,\n          status: 200,\n          json: () => Promise.resolve(respJson),\n          headers: FAKE_RESPONSE_HEADERS,\n        });\n        const result = await MessageLoaderUtils.loadMessagesForProvider(\n          provider,\n          { ...FAKE_OPTIONS, storage: fakeStorage }\n        );\n        assert.equal(result.messages.length, 1);\n        assert.equal(result.messages[0].id, testMessage.id);\n      });\n    });\n    it(\"should return an empty array for a remote provider with a blank URL without attempting a request\", async () => {\n      const provider = { id: \"provider123\", type: \"remote\", url: \"\" };\n\n      const result = await MessageLoaderUtils.loadMessagesForProvider(\n        provider,\n        FAKE_OPTIONS\n      );\n\n      assert.notCalled(fetchStub);\n      assert.deepEqual(result.messages, []);\n    });\n    it(\"should return .lastUpdated with the time at which the messages were fetched\", async () => {\n      const sourceMessage = { id: \"foo\" };\n      const provider = {\n        id: \"provider123\",\n        type: \"remote\",\n        url: \"foo.com\",\n      };\n\n      fetchStub.resolves({\n        ok: true,\n        status: 200,\n        json: () =>\n          new Promise(resolve => {\n            clock.tick(42);\n            resolve({ messages: [sourceMessage] });\n          }),\n        headers: FAKE_RESPONSE_HEADERS,\n      });\n\n      const result = await MessageLoaderUtils.loadMessagesForProvider(\n        provider,\n        FAKE_OPTIONS\n      );\n\n      assert.propertyVal(result, \"lastUpdated\", 42);\n    });\n  });\n\n  describe(\"#shouldProviderUpdate\", () => {\n    it(\"should return true if the provider does not had a .lastUpdated property\", () => {\n      assert.isTrue(MessageLoaderUtils.shouldProviderUpdate({ id: \"foo\" }));\n    });\n    it(\"should return false if the provider does not had a .updateCycleInMs property and has a .lastUpdated\", () => {\n      clock.tick(1);\n      assert.isFalse(\n        MessageLoaderUtils.shouldProviderUpdate({ id: \"foo\", lastUpdated: 0 })\n      );\n    });\n    it(\"should return true if the time since .lastUpdated is greater than .updateCycleInMs\", () => {\n      clock.tick(301);\n      assert.isTrue(\n        MessageLoaderUtils.shouldProviderUpdate({\n          id: \"foo\",\n          lastUpdated: 0,\n          updateCycleInMs: 300,\n        })\n      );\n    });\n    it(\"should return false if the time since .lastUpdated is less than .updateCycleInMs\", () => {\n      clock.tick(299);\n      assert.isFalse(\n        MessageLoaderUtils.shouldProviderUpdate({\n          id: \"foo\",\n          lastUpdated: 0,\n          updateCycleInMs: 300,\n        })\n      );\n    });\n  });\n\n  describe(\"#_loadAddonIconInURLBar\", () => {\n    let notificationContainerEl;\n    let browser;\n    let getContainerStub;\n    beforeEach(() => {\n      notificationContainerEl = { style: {} };\n      browser = {\n        ownerDocument: {\n          getElementById() {\n            return {};\n          },\n        },\n      };\n      getContainerStub = sandbox.stub(browser.ownerDocument, \"getElementById\");\n    });\n    it(\"should return for empty args\", () => {\n      MessageLoaderUtils._loadAddonIconInURLBar();\n      assert.notCalled(getContainerStub);\n    });\n    it(\"should return if notification popup box not found\", () => {\n      getContainerStub.returns(null);\n      MessageLoaderUtils._loadAddonIconInURLBar(browser);\n      assert.calledOnce(getContainerStub);\n    });\n    it(\"should unhide notification popup box with display style as none\", () => {\n      getContainerStub.returns(notificationContainerEl);\n      notificationContainerEl.style.display = \"none\";\n      MessageLoaderUtils._loadAddonIconInURLBar(browser);\n      assert.calledWith(\n        browser.ownerDocument.getElementById,\n        \"notification-popup-box\"\n      );\n      assert.equal(notificationContainerEl.style.display, \"block\");\n    });\n    it(\"should unhide notification popup box with display style empty\", () => {\n      getContainerStub.returns(notificationContainerEl);\n      notificationContainerEl.style.display = \"\";\n      MessageLoaderUtils._loadAddonIconInURLBar(browser);\n      assert.calledWith(\n        browser.ownerDocument.getElementById,\n        \"notification-popup-box\"\n      );\n      assert.equal(notificationContainerEl.style.display, \"block\");\n    });\n  });\n\n  describe(\"#installAddonFromURL\", () => {\n    let globals;\n    let getInstallStub;\n    let installAddonStub;\n    beforeEach(() => {\n      globals = new GlobalOverrider();\n      getInstallStub = sandbox.stub();\n      installAddonStub = sandbox.stub();\n      sandbox.stub(MessageLoaderUtils, \"_loadAddonIconInURLBar\").returns(null);\n      globals.set(\"AddonManager\", {\n        getInstallForURL: getInstallStub,\n        installAddonFromWebpage: installAddonStub,\n      });\n    });\n    afterEach(() => {\n      globals.restore();\n    });\n    it(\"should call the Addons API when passed a valid URL\", async () => {\n      getInstallStub.resolves(null);\n      installAddonStub.resolves(null);\n\n      await MessageLoaderUtils.installAddonFromURL({}, \"foo.com\");\n\n      assert.calledOnce(getInstallStub);\n      assert.calledOnce(installAddonStub);\n\n      // Verify that the expected installation source has been passed to the getInstallForURL\n      // method (See Bug 1496167 for a rationale).\n      assert.calledWithExactly(getInstallStub, \"foo.com\", {\n        telemetryInfo: { source: \"amo\" },\n      });\n    });\n    it(\"should optionally pass a custom telemetrySource to the Addons API if specified\", async () => {\n      getInstallStub.resolves(null);\n      installAddonStub.resolves(null);\n\n      await MessageLoaderUtils.installAddonFromURL({}, \"foo.com\", \"foo\");\n\n      assert.calledOnce(getInstallStub);\n      assert.calledOnce(installAddonStub);\n\n      // Verify that a custom installation source can be passed to the getInstallForURL\n      // method (See Bug 1549770 for a rationale).\n      assert.calledWithExactly(getInstallStub, \"foo.com\", {\n        telemetryInfo: { source: \"foo\" },\n      });\n    });\n    it(\"should not call the Addons API on invalid URLs\", async () => {\n      sandbox\n        .stub(global.Services.scriptSecurityManager, \"getSystemPrincipal\")\n        .throws();\n\n      await MessageLoaderUtils.installAddonFromURL({}, \"https://foo.com\");\n\n      assert.notCalled(getInstallStub);\n      assert.notCalled(installAddonStub);\n    });\n  });\n\n  describe(\"#cleanupCache\", () => {\n    it(\"should remove data for providers no longer active\", async () => {\n      const fakeStorage = {\n        get: sinon.stub().returns(\n          Promise.resolve({\n            \"id-1\": {},\n            \"id-2\": {},\n            \"id-3\": {},\n          })\n        ),\n        set: sinon.stub().returns(Promise.resolve()),\n      };\n      const fakeProviders = [\n        { id: \"id-1\", type: \"remote\" },\n        { id: \"id-3\", type: \"remote\" },\n      ];\n\n      await MessageLoaderUtils.cleanupCache(fakeProviders, fakeStorage);\n\n      assert.calledOnce(fakeStorage.set);\n      assert.calledWith(\n        fakeStorage.set,\n        MessageLoaderUtils.REMOTE_LOADER_CACHE_KEY,\n        { \"id-1\": {}, \"id-3\": {} }\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/asrouter/ModalOverlay.test.jsx",
    "content": "import { ModalOverlayWrapper } from \"content-src/asrouter/components/ModalOverlay/ModalOverlay\";\nimport { mount } from \"enzyme\";\nimport React from \"react\";\n\ndescribe(\"ModalOverlayWrapper\", () => {\n  let fakeDoc;\n  let sandbox;\n  let header;\n  beforeEach(() => {\n    sandbox = sinon.createSandbox();\n    header = document.createElement(\"div\");\n\n    fakeDoc = {\n      addEventListener: sandbox.stub(),\n      removeEventListener: sandbox.stub(),\n      body: { classList: { add: sandbox.stub(), remove: sandbox.stub() } },\n      getElementById() {\n        return header;\n      },\n    };\n  });\n  afterEach(() => {\n    sandbox.restore();\n  });\n  it(\"should add eventListener and a class on mount\", async () => {\n    mount(<ModalOverlayWrapper document={fakeDoc} />);\n    assert.calledOnce(fakeDoc.addEventListener);\n    assert.calledWith(fakeDoc.body.classList.add, \"modal-open\");\n  });\n\n  it(\"should remove eventListener on unmount\", async () => {\n    const wrapper = mount(<ModalOverlayWrapper document={fakeDoc} />);\n    wrapper.unmount();\n    assert.calledOnce(fakeDoc.addEventListener);\n    assert.calledOnce(fakeDoc.removeEventListener);\n    assert.calledWith(fakeDoc.body.classList.remove, \"modal-open\");\n  });\n\n  it(\"should call props.onClose on an Escape key\", async () => {\n    const onClose = sandbox.stub();\n    mount(<ModalOverlayWrapper document={fakeDoc} onClose={onClose} />);\n\n    // Simulate onkeydown being called\n    const [, callback] = fakeDoc.addEventListener.firstCall.args;\n    callback({ key: \"Escape\" });\n\n    assert.calledOnce(onClose);\n  });\n\n  it(\"should not call props.onClose on other keys than Escape\", async () => {\n    const onClose = sandbox.stub();\n    mount(<ModalOverlayWrapper document={fakeDoc} onClose={onClose} />);\n\n    // Simulate onkeydown being called\n    const [, callback] = fakeDoc.addEventListener.firstCall.args;\n    callback({ key: \"Ctrl\" });\n\n    assert.notCalled(onClose);\n  });\n\n  it(\"should not call props.onClose when clicked outside dialog\", async () => {\n    const onClose = sandbox.stub();\n    const wrapper = mount(\n      <ModalOverlayWrapper document={fakeDoc} onClose={onClose} />\n    );\n    wrapper.find(\"div.modalOverlayOuter.active\").simulate(\"click\");\n    assert.notCalled(onClose);\n  });\n});\n"
  },
  {
    "path": "test/unit/asrouter/PanelTestProvider.test.js",
    "content": "import { PanelTestProvider } from \"lib/PanelTestProvider.jsm\";\nimport schema from \"content-src/asrouter/schemas/panel/cfr-fxa-bookmark.schema.json\";\nimport update_schema from \"content-src/asrouter/templates/OnboardingMessage/UpdateAction.schema.json\";\nimport whats_new_schema from \"content-src/asrouter/templates/OnboardingMessage/WhatsNewMessage.schema.json\";\nconst messages = PanelTestProvider.getMessages();\n\ndescribe(\"PanelTestProvider\", () => {\n  it(\"should have a message\", () => {\n    // Careful: when changing this number make sure that new messages also go\n    // through schema verifications.\n    assert.lengthOf(messages, 16);\n  });\n  it(\"should be a valid message\", () => {\n    const fxaMessages = messages.filter(\n      ({ template }) => template === \"fxa_bookmark_panel\"\n    );\n    for (let message of fxaMessages) {\n      assert.jsonSchema(message.content, schema);\n    }\n  });\n  it(\"should be a valid message\", () => {\n    const updateMessages = messages.filter(\n      ({ template }) => template === \"update_action\"\n    );\n    for (let message of updateMessages) {\n      assert.jsonSchema(message.content, update_schema);\n    }\n  });\n  it(\"should be a valid message\", () => {\n    const whatsNewMessages = messages.filter(\n      ({ template }) => template === \"whatsnew_panel_message\"\n    );\n    for (let message of whatsNewMessages) {\n      assert.jsonSchema(message.content, whats_new_schema);\n      // Not part of `message.content` so it can't be enforced through schema\n      assert.property(message, \"order\");\n    }\n  });\n});\n"
  },
  {
    "path": "test/unit/asrouter/RemoteL10n.test.js",
    "content": "import { RemoteL10n, _RemoteL10n } from \"lib/RemoteL10n.jsm\";\nimport { GlobalOverrider } from \"test/unit/utils\";\n\ndescribe(\"RemoteL10n\", () => {\n  let sandbox;\n  let globals;\n  let domL10nStub;\n  beforeEach(() => {\n    sandbox = sinon.createSandbox();\n    globals = new GlobalOverrider();\n    domL10nStub = sandbox.stub();\n    globals.set(\"DOMLocalization\", domL10nStub);\n  });\n  afterEach(() => {\n    sandbox.restore();\n    globals.restore();\n  });\n  describe(\"#RemoteL10n\", () => {\n    it(\"should create a new instance\", () => {\n      assert.ok(new _RemoteL10n());\n    });\n    it(\"should create a DOMLocalization instance\", () => {\n      domL10nStub.returns({ instance: true });\n      const instance = new _RemoteL10n();\n\n      assert.propertyVal(instance._createDOML10n(), \"instance\", true);\n      assert.calledOnce(domL10nStub);\n    });\n    it(\"should create a new instance\", () => {\n      domL10nStub.returns({ instance: true });\n      const instance = new _RemoteL10n();\n\n      assert.ok(instance.l10n);\n\n      instance.reloadL10n();\n\n      assert.ok(instance.l10n);\n\n      assert.calledTwice(domL10nStub);\n    });\n    it(\"should reuse the instance\", () => {\n      domL10nStub.returns({ instance: true });\n      const instance = new _RemoteL10n();\n\n      assert.ok(instance.l10n);\n      assert.ok(instance.l10n);\n\n      assert.calledOnce(domL10nStub);\n    });\n  });\n  describe(\"#_createDOML10n\", () => {\n    it(\"should load the remote Fluent file if USE_REMOTE_L10N_PREF is true\", async () => {\n      sandbox.stub(global.Services.prefs, \"getBoolPref\").returns(true);\n      RemoteL10n._createDOML10n();\n\n      assert.calledOnce(domL10nStub);\n      const { args } = domL10nStub.firstCall;\n      // The first arg is the resource array, and the second one is the bundle generator.\n      assert.equal(args.length, 2);\n      assert.deepEqual(args[0], [\n        \"browser/newtab/asrouter.ftl\",\n        \"browser/branding/brandings.ftl\",\n        \"browser/branding/sync-brand.ftl\",\n        \"branding/brand.ftl\",\n      ]);\n      assert.isFunction(args[1]);\n    });\n    it(\"should load the local Fluent file if USE_REMOTE_L10N_PREF is false\", () => {\n      sandbox.stub(global.Services.prefs, \"getBoolPref\").returns(false);\n      RemoteL10n._createDOML10n();\n\n      const { args } = domL10nStub.firstCall;\n      // The first arg is the resource array, and the second one should be null.\n      assert.equal(args.length, 2);\n      assert.deepEqual(args[0], [\n        \"browser/newtab/asrouter.ftl\",\n        \"browser/branding/brandings.ftl\",\n        \"browser/branding/sync-brand.ftl\",\n        \"branding/brand.ftl\",\n      ]);\n      assert.isUndefined(args[1]);\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/asrouter/RichText.test.jsx",
    "content": "import {\n  convertLinks,\n  RichText,\n} from \"content-src/asrouter/components/RichText/RichText\";\nimport { Localized } from \"fluent-react\";\nimport { mount } from \"enzyme\";\nimport React from \"react\";\n\ndescribe(\"convertLinks\", () => {\n  let sandbox;\n  beforeEach(() => {\n    sandbox = sinon.createSandbox();\n  });\n  afterEach(() => {\n    sandbox.restore();\n  });\n  it(\"should return an object with anchor elements\", () => {\n    const cta = {\n      url: \"https://foo.com\",\n      metric: \"foo\",\n    };\n    const stub = sandbox.stub();\n    const result = convertLinks({ cta }, stub);\n\n    assert.property(result, \"cta\");\n    assert.propertyVal(result.cta, \"type\", \"a\");\n    assert.propertyVal(result.cta.props, \"href\", cta.url);\n    assert.propertyVal(result.cta.props, \"data-metric\", cta.metric);\n    assert.propertyVal(result.cta.props, \"onClick\", stub);\n  });\n  it(\"should return an anchor element without href\", () => {\n    const cta = {\n      url: \"https://foo.com\",\n      metric: \"foo\",\n      action: \"OPEN_MENU\",\n      args: \"appMenu\",\n    };\n    const stub = sandbox.stub();\n    const result = convertLinks({ cta }, stub);\n\n    assert.property(result, \"cta\");\n    assert.propertyVal(result.cta, \"type\", \"a\");\n    assert.propertyVal(result.cta.props, \"href\", false);\n    assert.propertyVal(result.cta.props, \"data-metric\", cta.metric);\n    assert.propertyVal(result.cta.props, \"data-action\", cta.action);\n    assert.propertyVal(result.cta.props, \"data-args\", cta.args);\n    assert.propertyVal(result.cta.props, \"onClick\", stub);\n  });\n  it(\"should follow openNewWindow prop\", () => {\n    const cta = { url: \"https://foo.com\" };\n    const newWindow = convertLinks({ cta }, sandbox.stub(), false, true);\n    const sameWindow = convertLinks({ cta }, sandbox.stub(), false);\n\n    assert.propertyVal(newWindow.cta.props, \"target\", \"_blank\");\n    assert.propertyVal(sameWindow.cta.props, \"target\", \"\");\n  });\n  it(\"should allow for custom elements & styles\", () => {\n    const wrapper = mount(\n      <RichText\n        customElements={{ em: <em style={{ color: \"#f05\" }} /> }}\n        text=\"<em>foo</em>\"\n        localization_id=\"text\"\n      />\n    );\n\n    const localized = wrapper.find(Localized);\n    assert.propertyVal(localized.props().em.props.style, \"color\", \"#f05\");\n  });\n});\n"
  },
  {
    "path": "test/unit/asrouter/SnippetsTestMessageProvider.test.js",
    "content": "import EOYSnippetSchema from \"../../../content-src/asrouter/templates/EOYSnippet/EOYSnippet.schema.json\";\nimport SimpleBelowSearchSnippetSchema from \"../../../content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.schema.json\";\nimport SimpleSnippetSchema from \"../../../content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.schema.json\";\nimport { SnippetsTestMessageProvider } from \"../../../lib/SnippetsTestMessageProvider.jsm\";\nimport SubmitFormSnippetSchema from \"../../../content-src/asrouter/templates/SubmitFormSnippet/SubmitFormSnippet.schema.json\";\n\nconst schemas = {\n  simple_snippet: SimpleSnippetSchema,\n  newsletter_snippet: SubmitFormSnippetSchema,\n  fxa_signup_snippet: SubmitFormSnippetSchema,\n  send_to_device_snippet: SubmitFormSnippetSchema,\n  eoy_snippet: EOYSnippetSchema,\n  simple_below_search_snippet: SimpleBelowSearchSnippetSchema,\n};\n\ndescribe(\"SnippetsTestMessageProvider\", () => {\n  let messages = SnippetsTestMessageProvider.getMessages();\n\n  it(\"should return an array of messages\", () => {\n    assert.isArray(messages);\n  });\n\n  it(\"should have a valid example of each schema\", () => {\n    Object.keys(schemas).forEach(templateName => {\n      const example = messages.find(\n        message => message.template === templateName\n      );\n      assert.ok(example, `has a ${templateName} example`);\n    });\n  });\n\n  it(\"should have examples that are valid\", () => {\n    messages.forEach(example => {\n      assert.jsonSchema(\n        example.content,\n        schemas[example.template],\n        `${example.id} should be valid`\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/asrouter/TargetingDocs.test.js",
    "content": "import { ASRouterTargeting } from \"lib/ASRouterTargeting.jsm\";\nimport docs from \"content-src/asrouter/docs/targeting-attributes.md\";\n\n// The following targeting parameters are either deprecated or should not be included in the docs for some reason.\nconst SKIP_DOCS = [];\n// These are extra message context attributes via ASRouter.jsm\nconst MESSAGE_CONTEXT_ATTRIBUTES = [\n  \"previousSessionEnd\",\n  \"trailheadInterrupt\",\n  \"trailheadTriplet\",\n];\n\nfunction getHeadingsFromDocs() {\n  const re = /### `(\\w+)`/g;\n  const found = [];\n  let match = 1;\n  while (match) {\n    match = re.exec(docs);\n    if (match) {\n      found.push(match[1]);\n    }\n  }\n  return found;\n}\n\nfunction getTOCFromDocs() {\n  const re = /## Available attributes\\n+([^]+)\\n+## Detailed usage/;\n  const sectionMatch = docs.match(re);\n  if (!sectionMatch) {\n    return [];\n  }\n  const [, listText] = sectionMatch;\n  const re2 = /\\[(\\w+)\\]/g;\n  const found = [];\n  let match = 1;\n  while (match) {\n    match = re2.exec(listText);\n    if (match) {\n      found.push(match[1]);\n    }\n  }\n  return found;\n}\n\ndescribe(\"ASRTargeting docs\", () => {\n  const DOCS_TARGETING_HEADINGS = getHeadingsFromDocs();\n  const DOCS_TOC = getTOCFromDocs();\n  const ASRTargetingAttributes = [\n    ...Object.keys(ASRouterTargeting.Environment).filter(\n      attribute => !SKIP_DOCS.includes(attribute)\n    ),\n    ...MESSAGE_CONTEXT_ATTRIBUTES,\n  ];\n\n  describe(\"All targeting params documented in targeting-attributes.md\", () => {\n    for (const targetingParam of ASRTargetingAttributes) {\n      // If this test is failing, you probably forgot to add docs to content-src/asrouter/targeting-attributes.md\n      // for a new targeting attribute, or you forgot to put it in the table of contents up top.\n      it(`should have docs and table of contents entry for ${targetingParam}`, () => {\n        assert.include(\n          DOCS_TARGETING_HEADINGS,\n          targetingParam,\n          `Didn't find the heading: ### \\`${targetingParam}\\``\n        );\n        assert.include(\n          DOCS_TOC,\n          targetingParam,\n          `Didn't find a table of contents entry for ${targetingParam}`\n        );\n      });\n    }\n  });\n  describe(\"No extra attributes in targeting-attributes.md\", () => {\n    // whitelist includes targeting attributes that are not implemented by\n    // ASRTargetingAttributes. For example trigger context passed to the evaluation\n    // context in when a trigger runs or ASRouter state used in the evaluation.\n    const whitelist = [\n      \"personalizedCfrThreshold\",\n      \"personalizedCfrScores\",\n      \"messageImpressions\",\n    ];\n    for (const targetingParam of DOCS_TARGETING_HEADINGS.filter(\n      doc => !whitelist.includes(doc)\n    )) {\n      // If this test is failing, you might has spelled something wrong or removed a targeting param without\n      // removing its docs.\n      it(`should have an implementation for ${targetingParam} in ASRouterTargeting.Environment`, () => {\n        assert.include(\n          ASRTargetingAttributes,\n          targetingParam,\n          `Didn't find an implementation for ${targetingParam}`\n        );\n      });\n    }\n  });\n});\n"
  },
  {
    "path": "test/unit/asrouter/asrouter-content.test.jsx",
    "content": "import {\n  ASRouterUISurface,\n  ASRouterUtils,\n} from \"content-src/asrouter/asrouter-content\";\nimport { GlobalOverrider } from \"test/unit/utils\";\nimport { OUTGOING_MESSAGE_NAME as AS_GENERAL_OUTGOING_MESSAGE_NAME } from \"content-src/lib/init-store\";\nimport { FAKE_LOCAL_MESSAGES } from \"./constants\";\nimport { OnboardingMessageProvider } from \"lib/OnboardingMessageProvider.jsm\";\nimport React from \"react\";\nimport { mount } from \"enzyme\";\nimport { Trailhead } from \"../../../content-src/asrouter/templates/Trailhead/Trailhead\";\nimport { Triplets } from \"../../../content-src/asrouter/templates/FirstRun/Triplets\";\nimport { actionCreators as ac } from \"common/Actions.jsm\";\n\nlet [FAKE_MESSAGE] = FAKE_LOCAL_MESSAGES;\nconst FAKE_NEWSLETTER_SNIPPET = FAKE_LOCAL_MESSAGES.find(\n  msg => msg.id === \"newsletter\"\n);\nconst FAKE_FXA_SNIPPET = FAKE_LOCAL_MESSAGES.find(msg => msg.id === \"fxa\");\nconst FAKE_BELOW_SEARCH_SNIPPET = FAKE_LOCAL_MESSAGES.find(\n  msg => msg.id === \"belowsearch\"\n);\n\nFAKE_MESSAGE = Object.assign({}, FAKE_MESSAGE, { provider: \"fakeprovider\" });\n\ndescribe(\"ASRouterUtils\", () => {\n  let global;\n  let sandbox;\n  let fakeSendAsyncMessage;\n  beforeEach(() => {\n    global = new GlobalOverrider();\n    sandbox = sinon.createSandbox();\n    fakeSendAsyncMessage = sandbox.stub();\n    global.set({ RPMSendAsyncMessage: fakeSendAsyncMessage });\n  });\n  afterEach(() => {\n    sandbox.restore();\n    global.restore();\n  });\n  it(\"should send a message with the right payload data\", () => {\n    ASRouterUtils.sendTelemetry({ id: 1, event: \"CLICK\" });\n\n    assert.calledOnce(fakeSendAsyncMessage);\n    assert.calledWith(fakeSendAsyncMessage, AS_GENERAL_OUTGOING_MESSAGE_NAME);\n    const [, payload] = fakeSendAsyncMessage.firstCall.args;\n    assert.propertyVal(payload.data, \"id\", 1);\n    assert.propertyVal(payload.data, \"event\", \"CLICK\");\n  });\n});\n\ndescribe(\"ASRouterUISurface\", () => {\n  let wrapper;\n  let globalO;\n  let sandbox;\n  let headerPortal;\n  let footerPortal;\n  let fakeDocument;\n  let fetchStub;\n\n  beforeEach(() => {\n    sandbox = sinon.createSandbox();\n    headerPortal = document.createElement(\"div\");\n    footerPortal = document.createElement(\"div\");\n    sandbox.stub(footerPortal, \"querySelector\").returns(footerPortal);\n    fetchStub = sandbox.stub(global, \"fetch\").resolves({\n      ok: true,\n      status: 200,\n      json: () => Promise.resolve({}),\n    });\n    fakeDocument = {\n      location: { href: \"\" },\n      _listeners: new Set(),\n      _visibilityState: \"hidden\",\n      head: {\n        appendChild(el) {\n          return el;\n        },\n      },\n      get visibilityState() {\n        return this._visibilityState;\n      },\n      set visibilityState(value) {\n        if (this._visibilityState === value) {\n          return;\n        }\n        this._visibilityState = value;\n        this._listeners.forEach(l => l());\n      },\n      addEventListener(event, listener) {\n        this._listeners.add(listener);\n      },\n      removeEventListener(event, listener) {\n        this._listeners.delete(listener);\n      },\n      get body() {\n        return document.createElement(\"body\");\n      },\n      getElementById(id) {\n        switch (id) {\n          case \"header-asrouter-container\":\n            return headerPortal;\n          default:\n            return footerPortal;\n        }\n      },\n      createElement(tag) {\n        return document.createElement(tag);\n      },\n    };\n    globalO = new GlobalOverrider();\n    globalO.set({\n      RPMAddMessageListener: sandbox.stub(),\n      RPMRemoveMessageListener: sandbox.stub(),\n      RPMSendAsyncMessage: sandbox.stub(),\n    });\n\n    sandbox.stub(ASRouterUtils, \"sendTelemetry\");\n\n    wrapper = mount(<ASRouterUISurface document={fakeDocument} />);\n  });\n\n  afterEach(() => {\n    sandbox.restore();\n    globalO.restore();\n  });\n\n  it(\"should render the component if a message id is defined\", () => {\n    wrapper.setState({ message: FAKE_MESSAGE });\n    assert.isTrue(wrapper.exists());\n  });\n\n  it(\"should pass in the correct form_method for newsletter snippets\", () => {\n    wrapper.setState({ message: FAKE_NEWSLETTER_SNIPPET });\n\n    assert.isTrue(wrapper.find(\"SubmitFormSnippet\").exists());\n    assert.propertyVal(\n      wrapper.find(\"SubmitFormSnippet\").props(),\n      \"form_method\",\n      \"POST\"\n    );\n  });\n\n  it(\"should pass in the correct form_method for fxa snippets\", () => {\n    wrapper.setState({ message: FAKE_FXA_SNIPPET });\n\n    assert.isTrue(wrapper.find(\"SubmitFormSnippet\").exists());\n    assert.propertyVal(\n      wrapper.find(\"SubmitFormSnippet\").props(),\n      \"form_method\",\n      \"GET\"\n    );\n  });\n\n  it(\"should render a preview banner if message provider is preview\", () => {\n    wrapper.setState({ message: { ...FAKE_MESSAGE, provider: \"preview\" } });\n    assert.isTrue(wrapper.find(\".snippets-preview-banner\").exists());\n  });\n\n  it(\"should not render a preview banner if message provider is not preview\", () => {\n    wrapper.setState({ message: FAKE_MESSAGE });\n    assert.isFalse(wrapper.find(\".snippets-preview-banner\").exists());\n  });\n\n  it(\"should render a SimpleSnippet in the footer portal\", () => {\n    wrapper.setState({ message: FAKE_MESSAGE });\n    assert.isTrue(footerPortal.childElementCount > 0);\n    assert.equal(headerPortal.childElementCount, 0);\n  });\n\n  it(\"should not render a SimpleBelowSearchSnippet in a portal\", () => {\n    wrapper.setState({ message: FAKE_BELOW_SEARCH_SNIPPET });\n    assert.equal(headerPortal.childElementCount, 0);\n    assert.equal(footerPortal.childElementCount, 0);\n  });\n\n  it(\"should render a trailhead message in the header portal\", async () => {\n    // wrapper = shallow(<ASRouterUISurface document={fakeDocument} />);\n    const message = (await OnboardingMessageProvider.getUntranslatedMessages()).find(\n      msg => msg.template === \"trailhead\"\n    );\n\n    wrapper.setState({ message });\n\n    assert.isTrue(headerPortal.childElementCount > 0);\n    assert.equal(footerPortal.childElementCount, 0);\n  });\n\n  it(\"should dispatch an event to select the correct theme\", () => {\n    const stub = sandbox.stub(window, \"dispatchEvent\");\n    sandbox\n      .stub(ASRouterUtils, \"getPreviewEndpoint\")\n      .returns({ theme: \"dark\" });\n\n    wrapper = mount(<ASRouterUISurface document={fakeDocument} />);\n\n    assert.calledOnce(stub);\n    assert.property(stub.firstCall.args[0].detail.data, \"ntp_background\");\n    assert.property(stub.firstCall.args[0].detail.data, \"ntp_text\");\n    assert.property(stub.firstCall.args[0].detail.data, \"sidebar\");\n    assert.property(stub.firstCall.args[0].detail.data, \"sidebar_text\");\n  });\n\n  describe(\"snippets\", () => {\n    it(\"should send correct event and source when snippet is blocked\", () => {\n      wrapper.setState({ message: FAKE_MESSAGE });\n\n      wrapper.find(\".blockButton\").simulate(\"click\");\n      assert.propertyVal(\n        ASRouterUtils.sendTelemetry.firstCall.args[0],\n        \"event\",\n        \"BLOCK\"\n      );\n      assert.propertyVal(\n        ASRouterUtils.sendTelemetry.firstCall.args[0],\n        \"source\",\n        \"NEWTAB_FOOTER_BAR\"\n      );\n    });\n\n    it(\"should not send telemetry when a preview snippet is blocked\", () => {\n      wrapper.setState({ message: { ...FAKE_MESSAGE, provider: \"preview\" } });\n\n      wrapper.find(\".blockButton\").simulate(\"click\");\n      assert.notCalled(ASRouterUtils.sendTelemetry);\n    });\n  });\n\n  describe(\"trailhead\", () => {\n    it(\"should render trailhead if a trailhead message is received\", async () => {\n      const message = (await OnboardingMessageProvider.getUntranslatedMessages()).find(\n        msg => msg.template === \"trailhead\"\n      );\n      wrapper.setState({ message });\n      assert.lengthOf(wrapper.find(Trailhead), 1);\n    });\n\n    it(\"should render Triplets if a trailhead message with bundle is received\", async () => {\n      const FAKE_TRIPLETS_BUNDLE = [\n        {\n          id: \"test\",\n          content: {\n            title: { string_id: \"foo\" },\n            text: { string_id: \"text1\" },\n            icon: \"icon\",\n            primary_button: {\n              label: { string_id: \"button1\" },\n              action: {\n                type: \"OPEN_URL\",\n                data: { args: \"https://example.com/\" },\n              },\n            },\n          },\n        },\n      ];\n      const message = (await OnboardingMessageProvider.getUntranslatedMessages()).find(\n        msg => msg.template === \"trailhead\"\n      );\n      wrapper.setState({\n        message: { ...message, bundle: FAKE_TRIPLETS_BUNDLE },\n      });\n      assert.lengthOf(wrapper.find(Triplets), 1);\n    });\n\n    it(\"should send NEW_TAB_MESSAGE_REQUEST if a bundle card id is blocked or cleared\", async () => {\n      sandbox.stub(ASRouterUtils, \"sendMessage\");\n      const FAKE_TRIPLETS_BUNDLE_1 = [\n        {\n          id: \"CARD_1\",\n          content: {\n            title: { string_id: \"onboarding-private-browsing-title\" },\n            text: { string_id: \"onboarding-private-browsing-text\" },\n            icon: \"icon\",\n            primary_button: {\n              label: { string_id: \"onboarding-button-label-get-started\" },\n              action: {\n                type: \"OPEN_URL\",\n                data: { args: \"https://example.com/\" },\n              },\n            },\n          },\n        },\n      ];\n      const message = (await OnboardingMessageProvider.getUntranslatedMessages()).find(\n        msg => msg.id === \"TRAILHEAD_1\"\n      );\n      wrapper.setState({\n        message: { ...message, bundle: FAKE_TRIPLETS_BUNDLE_1 },\n      });\n\n      wrapper.instance().clearMessage(\"CARD_1\");\n      assert.calledOnce(ASRouterUtils.sendMessage);\n      assert.calledWithExactly(ASRouterUtils.sendMessage, {\n        type: \"NEWTAB_MESSAGE_REQUEST\",\n        data: { endpoint: undefined },\n      });\n    });\n  });\n\n  describe(\"impressions\", () => {\n    function simulateVisibilityChange(value) {\n      fakeDocument.visibilityState = value;\n    }\n\n    it(\"should call blockById after CTA link is clicked\", () => {\n      wrapper.setState({ message: FAKE_MESSAGE });\n      sandbox.stub(ASRouterUtils, \"blockById\");\n      wrapper.instance().sendClick({ target: { dataset: { metric: \"\" } } });\n\n      assert.calledOnce(ASRouterUtils.blockById);\n      assert.calledWithExactly(ASRouterUtils.blockById, FAKE_MESSAGE.id);\n    });\n\n    it(\"should executeAction if defined on the anchor\", () => {\n      wrapper.setState({ message: FAKE_MESSAGE });\n      sandbox.spy(ASRouterUtils, \"executeAction\");\n      wrapper.instance().sendClick({\n        target: { dataset: { action: \"OPEN_MENU\", args: \"appMenu\" } },\n      });\n\n      assert.calledOnce(ASRouterUtils.executeAction);\n      assert.calledWithExactly(ASRouterUtils.executeAction, {\n        type: \"OPEN_MENU\",\n        data: { args: \"appMenu\" },\n      });\n    });\n\n    it(\"should not call blockById if do_not_autoblock is true\", () => {\n      wrapper.setState({\n        message: {\n          ...FAKE_MESSAGE,\n          ...{ content: { ...FAKE_MESSAGE.content, do_not_autoblock: true } },\n        },\n      });\n      sandbox.stub(ASRouterUtils, \"blockById\");\n      wrapper.instance().sendClick({ target: { dataset: { metric: \"\" } } });\n\n      assert.notCalled(ASRouterUtils.blockById);\n    });\n\n    it(\"should not send an impression if no message exists\", () => {\n      simulateVisibilityChange(\"visible\");\n\n      assert.notCalled(ASRouterUtils.sendTelemetry);\n    });\n\n    it(\"should not send an impression if the page is not visible\", () => {\n      simulateVisibilityChange(\"hidden\");\n      wrapper.setState({ message: FAKE_MESSAGE });\n\n      assert.notCalled(ASRouterUtils.sendTelemetry);\n    });\n\n    it(\"should not send an impression for a preview message\", () => {\n      wrapper.setState({ message: { ...FAKE_MESSAGE, provider: \"preview\" } });\n      assert.notCalled(ASRouterUtils.sendTelemetry);\n\n      simulateVisibilityChange(\"visible\");\n      assert.notCalled(ASRouterUtils.sendTelemetry);\n    });\n\n    it(\"should send an impression ping when there is a message and the page becomes visible\", () => {\n      wrapper.setState({ message: FAKE_MESSAGE });\n      assert.notCalled(ASRouterUtils.sendTelemetry);\n\n      simulateVisibilityChange(\"visible\");\n      assert.calledOnce(ASRouterUtils.sendTelemetry);\n    });\n\n    it(\"should send the correct impression source\", () => {\n      wrapper.setState({ message: FAKE_MESSAGE });\n      simulateVisibilityChange(\"visible\");\n\n      assert.calledOnce(ASRouterUtils.sendTelemetry);\n      assert.propertyVal(\n        ASRouterUtils.sendTelemetry.firstCall.args[0],\n        \"event\",\n        \"IMPRESSION\"\n      );\n      assert.propertyVal(\n        ASRouterUtils.sendTelemetry.firstCall.args[0],\n        \"source\",\n        \"NEWTAB_FOOTER_BAR\"\n      );\n    });\n\n    it(\"should send an impression ping when the page is visible and a message gets loaded\", () => {\n      simulateVisibilityChange(\"visible\");\n      wrapper.setState({ message: {} });\n      assert.notCalled(ASRouterUtils.sendTelemetry);\n\n      wrapper.setState({ message: FAKE_MESSAGE });\n      assert.calledOnce(ASRouterUtils.sendTelemetry);\n    });\n\n    it(\"should send another impression ping if the message id changes\", () => {\n      simulateVisibilityChange(\"visible\");\n      wrapper.setState({ message: FAKE_MESSAGE });\n      assert.calledOnce(ASRouterUtils.sendTelemetry);\n\n      wrapper.setState({ message: FAKE_LOCAL_MESSAGES[1] });\n      assert.calledTwice(ASRouterUtils.sendTelemetry);\n    });\n\n    it(\"should not send another impression ping if the message id has not changed\", () => {\n      simulateVisibilityChange(\"visible\");\n      wrapper.setState({ message: FAKE_MESSAGE });\n      assert.calledOnce(ASRouterUtils.sendTelemetry);\n\n      wrapper.setState({ somethingElse: 123 });\n      assert.calledOnce(ASRouterUtils.sendTelemetry);\n    });\n\n    it(\"should not send another impression ping if the message is cleared\", () => {\n      simulateVisibilityChange(\"visible\");\n      wrapper.setState({ message: FAKE_MESSAGE });\n      assert.calledOnce(ASRouterUtils.sendTelemetry);\n\n      wrapper.setState({ message: {} });\n      assert.calledOnce(ASRouterUtils.sendTelemetry);\n    });\n\n    it(\"should call .sendTelemetry with the right message data\", () => {\n      simulateVisibilityChange(\"visible\");\n      wrapper.setState({ message: FAKE_MESSAGE });\n\n      assert.calledOnce(ASRouterUtils.sendTelemetry);\n      const [payload] = ASRouterUtils.sendTelemetry.firstCall.args;\n\n      assert.propertyVal(payload, \"message_id\", FAKE_MESSAGE.id);\n      assert.propertyVal(payload, \"event\", \"IMPRESSION\");\n      assert.propertyVal(\n        payload,\n        \"action\",\n        `${FAKE_MESSAGE.provider}_user_event`\n      );\n      assert.propertyVal(payload, \"source\", \"NEWTAB_FOOTER_BAR\");\n    });\n  });\n\n  describe(\".fetchFlowParams\", () => {\n    let dispatchStub;\n    const assertCalledWithURL = url =>\n      assert.calledWith(fetchStub, new URL(url).toString(), {\n        credentials: \"omit\",\n      });\n    beforeEach(() => {\n      dispatchStub = sandbox.stub();\n      wrapper = mount(\n        <ASRouterUISurface\n          dispatch={dispatchStub}\n          fxaEndpoint=\"https://accounts.firefox.com\"\n        />\n      );\n    });\n    it(\"should use the base url returned from the endpoint pref\", async () => {\n      wrapper = mount(\n        <ASRouterUISurface\n          dispatch={dispatchStub}\n          fxaEndpoint=\"https://foo.com\"\n        />\n      );\n      await wrapper.instance().fetchFlowParams();\n\n      assertCalledWithURL(\"https://foo.com/metrics-flow\");\n    });\n    it(\"should add given search params to the URL\", async () => {\n      const params = { foo: \"1\", bar: \"2\" };\n\n      await wrapper.instance().fetchFlowParams(params);\n\n      assertCalledWithURL(\n        \"https://accounts.firefox.com/metrics-flow?foo=1&bar=2\"\n      );\n    });\n    it(\"should return flowId, flowBeginTime, deviceId on a 200 response\", async () => {\n      const flowInfo = { flowId: \"foo\", flowBeginTime: 123, deviceId: \"bar\" };\n      fetchStub.withArgs(\"https://accounts.firefox.com/metrics-flow\").resolves({\n        ok: true,\n        status: 200,\n        json: () => Promise.resolve(flowInfo),\n      });\n\n      const result = await wrapper.instance().fetchFlowParams();\n      assert.deepEqual(result, flowInfo);\n    });\n    it(\"should return {} and dispatch a TELEMETRY_UNDESIRED_EVENT on a non-200 response\", async () => {\n      fetchStub.withArgs(\"https://accounts.firefox.com/metrics-flow\").resolves({\n        ok: false,\n        status: 400,\n        statusText: \"Client error\",\n        url: \"https://accounts.firefox.com/metrics-flow\",\n      });\n\n      const result = await wrapper.instance().fetchFlowParams();\n      assert.deepEqual(result, {});\n      assert.calledWith(\n        dispatchStub,\n        ac.OnlyToMain({\n          type: \"TELEMETRY_UNDESIRED_EVENT\",\n          data: {\n            event: \"FXA_METRICS_FETCH_ERROR\",\n            value: 400,\n          },\n        })\n      );\n    });\n    it(\"should return {} and dispatch a TELEMETRY_UNDESIRED_EVENT on a parsing erorr\", async () => {\n      fetchStub.withArgs(\"https://accounts.firefox.com/metrics-flow\").resolves({\n        ok: false,\n        status: 200,\n        // No json to parse, throws an error\n      });\n\n      const result = await wrapper.instance().fetchFlowParams();\n      assert.deepEqual(result, {});\n      assert.calledWith(\n        dispatchStub,\n        ac.OnlyToMain({\n          type: \"TELEMETRY_UNDESIRED_EVENT\",\n          data: { event: \"FXA_METRICS_ERROR\" },\n        })\n      );\n    });\n\n    describe(\".onUserAction\", () => {\n      it(\"if the action.type is ENABLE_FIREFOX_MONITOR, it should generate the right monitor URL given some flowParams\", async () => {\n        const flowInfo = { flowId: \"foo\", flowBeginTime: 123, deviceId: \"bar\" };\n        fetchStub\n          .withArgs(\n            \"https://accounts.firefox.com/metrics-flow?utm_term=avocado\"\n          )\n          .resolves({\n            ok: true,\n            status: 200,\n            json: () => Promise.resolve(flowInfo),\n          });\n\n        sandbox.spy(ASRouterUtils, \"executeAction\");\n\n        const msg = {\n          type: \"ENABLE_FIREFOX_MONITOR\",\n          data: {\n            args: {\n              url: \"https://monitor.firefox.com?foo=bar\",\n              flowRequestParams: {\n                utm_term: \"avocado\",\n              },\n            },\n          },\n        };\n\n        await wrapper.instance().onUserAction(msg);\n\n        assertCalledWithURL(\n          \"https://accounts.firefox.com/metrics-flow?utm_term=avocado\"\n        );\n        assert.calledWith(ASRouterUtils.executeAction, {\n          type: \"OPEN_URL\",\n          data: {\n            args: new URL(\n              \"https://monitor.firefox.com?foo=bar&deviceId=bar&flowId=foo&flowBeginTime=123\"\n            ).toString(),\n          },\n        });\n      });\n      it(\"if the action.type is not ENABLE_FIREFOX_MONITOR, it should just call ASRouterUtils.executeAction\", async () => {\n        const msg = {\n          type: \"FOO\",\n          data: {\n            args: \"bar\",\n          },\n        };\n        sandbox.spy(ASRouterUtils, \"executeAction\");\n        await wrapper.instance().onUserAction(msg);\n        assert.calledWith(ASRouterUtils.executeAction, msg);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/asrouter/compatibility-reference/fx57-compat.test.js",
    "content": "import EOYSnippetSchema from \"content-src/asrouter/templates/EOYSnippet/EOYSnippet.schema.json\";\nimport { expectedValues } from \"./snippets-fx57\";\nimport SimpleSnippetSchema from \"content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.schema.json\";\nimport SubmitFormSchema from \"content-src/asrouter/templates/SubmitFormSnippet/SubmitFormSnippet.schema.json\";\n\nexport const SnippetsSchemas = {\n  eoy_snippet: EOYSnippetSchema,\n  simple_snippet: SimpleSnippetSchema,\n  newsletter_snippet: SubmitFormSchema,\n  fxa_signup_snippet: SubmitFormSchema,\n  send_to_device_snippet: SubmitFormSchema,\n};\n\ndescribe(\"Firefox 57 compatibility test\", () => {\n  Object.keys(expectedValues).forEach(template => {\n    describe(template, () => {\n      const schema = SnippetsSchemas[template];\n      it(`should have a schema for ${template}`, () => {\n        assert.ok(schema);\n      });\n      it(`should validate with the schema for ${template}`, () => {\n        assert.jsonSchema(expectedValues[template], schema);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/asrouter/compatibility-reference/snippets-fx57.js",
    "content": "/**\n * IMPORTANT NOTE!!!\n *\n * Please DO NOT introduce breaking changes file without contacting snippets endpoint engineers\n * and changing the schema version to reflect a breaking change.\n *\n */\n\nconst DATA_URI_IMAGE =\n  \"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7\";\n\nexport const expectedValues = {\n  // Simple Snippet (https://github.com/mozmeao/snippets/blob/master/activity-stream/simple-snippet.html)\n  simple_snippet: {\n    icon: DATA_URI_IMAGE,\n    button_label: \"Click me\",\n    button_url: \"https://mozilla.org\",\n    button_background_color: \"#FF0000\",\n    button_color: \"#FFFFFF\",\n    text: \"Hello world\",\n    title: \"Hi!\",\n    title_icon: DATA_URI_IMAGE,\n    tall: true,\n  },\n\n  // FXA Snippet (https://github.com/mozmeao/snippets/blob/master/activity-stream/fxa.html)\n  fxa_signup_snippet: {\n    scene1_icon: DATA_URI_IMAGE,\n    scene1_button_label: \"Click me\",\n    scene1_button_background_color: \"#FF0000\",\n    scene1_button_color: \"#FFFFFF\",\n    scene1_text: \"Hello <em>world</em>\",\n    scene1_title: \"Hi!\",\n    scene1_title_icon: DATA_URI_IMAGE,\n\n    scene2_text: \"Second scene\",\n    scene2_title: \"Second scene title\",\n    scene2_email_placeholder_text: \"Email here\",\n    scene2_button_label: \"Sign Me Up\",\n    scene2_dismiss_button_text: \"Dismiss\",\n\n    utm_campaign: \"snippets123\",\n    utm_term: \"123term\",\n  },\n\n  // Send To Device Snippet (https://github.com/mozmeao/snippets/blob/master/activity-stream/send-to-device.html)\n  send_to_device_snippet: {\n    include_sms: true,\n    locale: \"de\",\n    country: \"DE\",\n    message_id_sms: \"foo\",\n    message_id_email: \"foo\",\n    scene1_button_background_color: \"#FF0000\",\n    scene1_button_color: \"#FFFFFF\",\n    scene1_button_label: \"Click me\",\n    scene1_icon: DATA_URI_IMAGE,\n    scene1_text: \"Hello world\",\n    scene1_title: \"Hi!\",\n    scene1_title_icon: DATA_URI_IMAGE,\n\n    scene2_button_label: \"Sign Me Up\",\n    scene2_disclaimer_html: \"Hello <em>world</em>\",\n    scene2_dismiss_button_text: \"Dismiss\",\n    scene2_icon: DATA_URI_IMAGE,\n    scene2_input_placeholder: \"Email here\",\n\n    scene2_text: \"Second scene\",\n    scene2_title: \"Second scene title\",\n\n    error_text: \"error\",\n    success_text: \"all good\",\n    success_title: \"Ok!\",\n  },\n\n  // Newsletter Snippet (https://github.com/mozmeao/snippets/blob/master/activity-stream/newsletter-subscribe.html)\n  newsletter_snippet: {\n    scene1_icon: DATA_URI_IMAGE,\n    scene1_button_label: \"Click me\",\n    scene1_button_background_color: \"#FF0000\",\n    scene1_button_color: \"#FFFFFF\",\n    scene1_text: \"Hello world\",\n    scene1_title: \"Hi!\",\n    scene1_title_icon: DATA_URI_IMAGE,\n\n    scene2_text: \"Second scene\",\n    scene2_title: \"Second scene title\",\n    scene2_newsletter: \"foo\",\n    scene2_email_placeholder_text: \"Email here\",\n    scene2_button_label: \"Sign Me Up\",\n    scene2_privacy_html: \"Hello <em>world</em>\",\n    scene2_dismiss_button_text: \"Dismiss\",\n\n    locale: \"de\",\n\n    error_text: \"error\",\n    success_text: \"all good\",\n  },\n\n  // EOY Snippet (https://github.com/mozmeao/snippets/blob/master/activity-stream/mofo-eoy-2017.html)\n  eoy_snippet: {\n    block_button_text: \"Block\",\n\n    donation_form_url: \"https://donate.mozilla.org/\",\n    text:\n      \"Big corporations want to restrict how we access the web. Fake news is making it harder for us to find the truth. Online bullies are silencing inspired voices. The not-for-profit Mozilla Foundation fights for a healthy internet with programs like our Tech Policy Fellowships and Internet Health Report; will you donate today?\",\n    icon: DATA_URI_IMAGE,\n    button_label: \"Donate\",\n    monthly_checkbox_label_text: \"Make my donation monthly\",\n    button_background_color: \"#0060DF\",\n    button_color: \"#FFFFFF\",\n    background_color: \"#FFFFFF\",\n    text_color: \"#000000\",\n    highlight_color: \"#FFE900\",\n\n    locale: \"en-US\",\n    currency_code: \"usd\",\n\n    donation_amount_first: 50,\n    donation_amount_second: 25,\n    donation_amount_third: 10,\n    donation_amount_fourth: 3,\n    selected_button: \"donation_amount_second\",\n\n    test: \"bold\",\n  },\n};\n"
  },
  {
    "path": "test/unit/asrouter/constants.js",
    "content": "export const CHILD_TO_PARENT_MESSAGE_NAME = \"ASRouter:child-to-parent\";\nexport const PARENT_TO_CHILD_MESSAGE_NAME = \"ASRouter:parent-to-child\";\n\nexport const FAKE_LOCAL_MESSAGES = [\n  {\n    id: \"foo\",\n    provider: \"snippets\",\n    template: \"simple_snippet\",\n    content: { title: \"Foo\", body: \"Foo123\" },\n  },\n  {\n    id: \"foo1\",\n    template: \"simple_snippet\",\n    provider: \"snippets\",\n    bundled: 2,\n    order: 1,\n    content: { title: \"Foo1\", body: \"Foo123-1\" },\n  },\n  {\n    id: \"foo2\",\n    template: \"simple_snippet\",\n    provider: \"snippets\",\n    bundled: 2,\n    order: 2,\n    content: { title: \"Foo2\", body: \"Foo123-2\" },\n  },\n  {\n    id: \"bar\",\n    template: \"fancy_template\",\n    content: { title: \"Foo\", body: \"Foo123\" },\n  },\n  { id: \"baz\", content: { title: \"Foo\", body: \"Foo123\" } },\n  {\n    id: \"newsletter\",\n    provider: \"snippets\",\n    template: \"newsletter_snippet\",\n    content: { title: \"Foo\", body: \"Foo123\" },\n  },\n  {\n    id: \"fxa\",\n    provider: \"snippets\",\n    template: \"fxa_signup_snippet\",\n    content: { title: \"Foo\", body: \"Foo123\" },\n  },\n  {\n    id: \"belowsearch\",\n    provider: \"snippets\",\n    template: \"simple_below_search_snippet\",\n    content: { text: \"Foo\" },\n  },\n];\nexport const FAKE_LOCAL_PROVIDER = {\n  id: \"onboarding\",\n  type: \"local\",\n  localProvider: \"FAKE_LOCAL_PROVIDER\",\n  enabled: true,\n  cohort: 0,\n};\nexport const FAKE_LOCAL_PROVIDERS = {\n  FAKE_LOCAL_PROVIDER: { getMessages: () => FAKE_LOCAL_MESSAGES },\n};\n\nexport const FAKE_REMOTE_MESSAGES = [\n  {\n    id: \"qux\",\n    template: \"simple_snippet\",\n    content: { title: \"Qux\", body: \"hello world\" },\n  },\n];\nexport const FAKE_REMOTE_PROVIDER = {\n  id: \"remotey\",\n  type: \"remote\",\n  url: \"http://fake.com/endpoint\",\n  enabled: true,\n};\n\nexport const FAKE_REMOTE_SETTINGS_PROVIDER = {\n  id: \"remotey-settingsy\",\n  type: \"remote-settings\",\n  bucket: \"bucketname\",\n  enabled: true,\n};\n\nconst notificationText = new String(\"Fake notification text\"); // eslint-disable-line\nnotificationText.attributes = { tooltiptext: \"Fake tooltip text\" };\n\nexport const FAKE_RECOMMENDATION = {\n  id: \"fake_id\",\n  template: \"cfr_doorhanger\",\n  content: {\n    category: \"cfrDummy\",\n    bucket_id: \"fake_bucket_id\",\n    notification_text: notificationText,\n    info_icon: {\n      label: \"Fake Info Icon Label\",\n      sumo_path: \"a_help_path_fragment\",\n    },\n    heading_text: \"Fake Heading Text\",\n    addon: {\n      title: \"Fake Addon Title\",\n      author: \"Fake Addon Author\",\n      icon: \"a_path_to_some_icon\",\n      rating: 4.2,\n      users: 1234,\n      amo_url: \"a_path_to_amo\",\n    },\n    descriptionDetails: {\n      steps: [{ string_id: \"cfr-features-step1\" }],\n    },\n    text: \"Here is the recommendation text body\",\n    buttons: {\n      primary: {\n        label: { string_id: \"primary_button_id\" },\n        action: {\n          id: \"primary_action\",\n          data: {},\n        },\n      },\n      secondary: [\n        {\n          label: { string_id: \"secondary_button_id\" },\n          action: { id: \"secondary_action\" },\n        },\n        {\n          label: { string_id: \"secondary_button_id_2\" },\n        },\n        {\n          label: { string_id: \"secondary_button_id_3\" },\n          action: { id: \"secondary_action\" },\n        },\n      ],\n    },\n  },\n};\n\n// Stubs methods on RemotePageManager\nexport class FakeRemotePageManager {\n  constructor() {\n    this.messagePorts = [];\n    this.addMessageListener = sinon.stub();\n    this.sendAsyncMessage = sinon.stub();\n    this.removeMessageListener = sinon.stub();\n    this.browser = {\n      ownerGlobal: {\n        openTrustedLinkIn: sinon.stub(),\n        openLinkIn: sinon.stub(),\n        OpenBrowserWindow: sinon.stub(),\n        openPreferences: sinon.stub(),\n        gBrowser: {\n          pinTab: sinon.stub(),\n          selectedTab: {},\n        },\n        ConfirmationHint: {\n          show: sinon.stub(),\n        },\n        gProtectionsHandler: {\n          showProtectionsPopup: sinon.stub(),\n          openProtections: sinon.stub(),\n        },\n      },\n    };\n    this.portID = \"6000:2\";\n  }\n}\n"
  },
  {
    "path": "test/unit/asrouter/schemas/panel/cfr-fxa-bookmark.schema.test.js",
    "content": "import schema from \"content-src/asrouter/schemas/panel/cfr-fxa-bookmark.schema.json\";\n\nconst DEFAULT_CONTENT = {\n  title: \"Sync your bookmarks everywhere\",\n  text: \"Great find! Now don't be left without this bookmark.\",\n  cta: \"Sync bookmarks now\",\n  info_icon: {\n    tooltiptext: \"Learn more\",\n  },\n};\n\nconst L10N_CONTENT = {\n  title: { string_id: \"cfr-bookmark-title\" },\n  text: { string_id: \"cfr-bookmark-body\" },\n  cta: { string_id: \"cfr-bookmark-link-text\" },\n  info_icon: {\n    tooltiptext: { string_id: \"cfr-bookmark-tooltip-text\" },\n  },\n};\n\ndescribe(\"CFR FxA Message Schema\", () => {\n  it(\"should validate DEFAULT_CONTENT\", () => {\n    assert.jsonSchema(DEFAULT_CONTENT, schema);\n  });\n  it(\"should validate L10N_CONTENT\", () => {\n    assert.jsonSchema(L10N_CONTENT, schema);\n  });\n});\n"
  },
  {
    "path": "test/unit/asrouter/template-utils.test.js",
    "content": "import { safeURI } from \"content-src/asrouter/template-utils\";\n\ndescribe(\"safeURI\", () => {\n  let warnStub;\n  beforeEach(() => {\n    warnStub = sinon.stub(console, \"warn\");\n  });\n  afterEach(() => {\n    warnStub.restore();\n  });\n  it(\"should allow http: URIs\", () => {\n    assert.equal(safeURI(\"http://foo.com\"), \"http://foo.com\");\n  });\n  it(\"should allow https: URIs\", () => {\n    assert.equal(safeURI(\"https://foo.com\"), \"https://foo.com\");\n  });\n  it(\"should allow data URIs\", () => {\n    assert.equal(\n      safeURI(\"data:image/png;base64,iVBO\"),\n      \"data:image/png;base64,iVBO\"\n    );\n  });\n  it(\"should not allow javascript: URIs\", () => {\n    assert.equal(safeURI(\"javascript:foo()\"), \"\"); // eslint-disable-line no-script-url\n    assert.calledOnce(warnStub);\n  });\n  it(\"should not warn if the URL is falsey \", () => {\n    assert.equal(safeURI(), \"\");\n    assert.notCalled(warnStub);\n  });\n});\n"
  },
  {
    "path": "test/unit/asrouter/templates/EOYSnippet.test.jsx",
    "content": "import { EOYSnippet } from \"content-src/asrouter/templates/EOYSnippet/EOYSnippet\";\nimport { GlobalOverrider } from \"test/unit/utils\";\nimport { mount } from \"enzyme\";\nimport React from \"react\";\nimport schema from \"content-src/asrouter/templates/EOYSnippet/EOYSnippet.schema.json\";\n\nconst DEFAULT_CONTENT = {\n  text: \"foo\",\n  donation_amount_first: 50,\n  donation_amount_second: 25,\n  donation_amount_third: 10,\n  donation_amount_fourth: 5,\n  donation_form_url: \"https://submit.form\",\n  button_label: \"Donate\",\n  currency_code: \"usd\",\n};\n\ndescribe(\"EOYSnippet\", () => {\n  let sandbox;\n  let wrapper;\n\n  /**\n   * mountAndCheckProps - Mounts a EOYSnippet with DEFAULT_CONTENT extended with any props\n   *                      passed in the content param and validates props against the schema.\n   * @param {obj} content Object containing custom message content (e.g. {text, icon, title})\n   * @returns enzyme wrapper for EOYSnippet\n   */\n  function mountAndCheckProps(content = {}, provider = \"test-provider\") {\n    const props = {\n      content: Object.assign({}, DEFAULT_CONTENT, content),\n      provider,\n      onAction: sandbox.stub(),\n      onBlock: sandbox.stub(),\n    };\n    const comp = mount(<EOYSnippet {...props} />);\n    // Check schema with the final props the component receives (including defaults)\n    assert.jsonSchema(comp.children().get(0).props.content, schema);\n    return comp;\n  }\n\n  beforeEach(() => {\n    sandbox = sinon.createSandbox();\n    wrapper = mountAndCheckProps();\n  });\n\n  afterEach(() => {\n    sandbox.restore();\n  });\n\n  it(\"should have the correct defaults\", () => {\n    wrapper = mountAndCheckProps();\n    // SendToDeviceSnippet is a wrapper around SubmitFormSnippet\n    const { props } = wrapper.children().get(0);\n\n    const defaultProperties = Object.keys(schema.properties).filter(\n      prop => schema.properties[prop].default\n    );\n    assert.lengthOf(defaultProperties, 4);\n    defaultProperties.forEach(prop =>\n      assert.propertyVal(props.content, prop, schema.properties[prop].default)\n    );\n  });\n\n  it(\"should render 4 donation options\", () => {\n    assert.lengthOf(wrapper.find(\"input[type='radio']\"), 4);\n  });\n\n  it(\"should select the second donation option\", () => {\n    wrapper = mountAndCheckProps({ selected_button: \"donation_amount_second\" });\n\n    assert.propertyVal(\n      wrapper.find(\"input[type='radio']\").get(1).props,\n      \"defaultChecked\",\n      true\n    );\n  });\n\n  it(\"should set frequency value to monthly\", () => {\n    const form = wrapper.find(\"form\").instance();\n    assert.equal(form.querySelector(\"[name='frequency']\").value, \"single\");\n\n    form.querySelector(\"#monthly-checkbox\").checked = true;\n    wrapper.find(\"form\").simulate(\"submit\");\n\n    assert.equal(form.querySelector(\"[name='frequency']\").value, \"monthly\");\n  });\n\n  it(\"should block after submitting the form\", () => {\n    const onBlockStub = sandbox.stub();\n    wrapper.setProps({ onBlock: onBlockStub });\n\n    wrapper.find(\"form\").simulate(\"submit\");\n\n    assert.calledOnce(onBlockStub);\n  });\n\n  it(\"should not block if do_not_autoblock is true\", () => {\n    const onBlockStub = sandbox.stub();\n    wrapper = mountAndCheckProps({ do_not_autoblock: true });\n    wrapper.setProps({ onBlock: onBlockStub });\n\n    wrapper.find(\"form\").simulate(\"submit\");\n\n    assert.notCalled(onBlockStub);\n  });\n\n  it(\"it should preserve URL GET params as hidden inputs\", () => {\n    wrapper = mountAndCheckProps({\n      donation_form_url:\n        \"https://donate.mozilla.org/pl/?utm_source=desktop-snippet&amp;utm_medium=snippet&amp;utm_campaign=donate&amp;utm_term=7556\",\n    });\n\n    const hiddenInputs = wrapper.find(\"input[type='hidden']\");\n\n    assert.propertyVal(\n      hiddenInputs.find(\"[name='utm_source']\").props(),\n      \"value\",\n      \"desktop-snippet\"\n    );\n    assert.propertyVal(\n      hiddenInputs.find(\"[name='amp;utm_medium']\").props(),\n      \"value\",\n      \"snippet\"\n    );\n    assert.propertyVal(\n      hiddenInputs.find(\"[name='amp;utm_campaign']\").props(),\n      \"value\",\n      \"donate\"\n    );\n    assert.propertyVal(\n      hiddenInputs.find(\"[name='amp;utm_term']\").props(),\n      \"value\",\n      \"7556\"\n    );\n  });\n\n  describe(\"locale\", () => {\n    let stub;\n    let globals;\n    beforeEach(() => {\n      globals = new GlobalOverrider();\n      stub = sandbox.stub().returns({ format: () => {} });\n\n      globals = new GlobalOverrider();\n      globals.set({ Intl: { NumberFormat: stub } });\n    });\n    afterEach(() => {\n      globals.restore();\n    });\n\n    it(\"should use content.locale for Intl\", () => {\n      // triggers component rendering and calls the function we're testing\n      wrapper.setProps({\n        content: {\n          locale: \"locale-foo\",\n          donation_form_url: DEFAULT_CONTENT.donation_form_url,\n        },\n      });\n\n      assert.calledOnce(stub);\n      assert.calledWithExactly(stub, \"locale-foo\", sinon.match.object);\n    });\n\n    it(\"should use navigator.language as locale fallback\", () => {\n      // triggers component rendering and calls the function we're testing\n      wrapper.setProps({\n        content: {\n          locale: null,\n          donation_form_url: DEFAULT_CONTENT.donation_form_url,\n        },\n      });\n\n      assert.calledOnce(stub);\n      assert.calledWithExactly(stub, navigator.language, sinon.match.object);\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/asrouter/templates/ExtensionDoorhanger.test.jsx",
    "content": "import { CFRMessageProvider } from \"lib/CFRMessageProvider.jsm\";\nimport schema from \"content-src/asrouter/templates/CFR/templates/ExtensionDoorhanger.schema.json\";\n\nconst DEFAULT_CONTENT = {\n  layout: \"addon_recommendation\",\n  category: \"dummyCategory\",\n  bucket_id: \"some_bucket_id\",\n  notification_text: \"Recommendation\",\n  heading_text: \"Recommended Extension\",\n  info_icon: {\n    label: { attributes: { tooltiptext: \"Why am I seeing this\" } },\n    sumo_path: \"extensionrecommendations\",\n  },\n  addon: {\n    id: \"1234\",\n    title: \"Addon name\",\n    icon: \"https://mozilla.org/icon\",\n    author: \"Author name\",\n    amo_url: \"https://example.com\",\n  },\n  text: \"Description of addon\",\n  buttons: {\n    primary: {\n      label: {\n        value: \"Add Now\",\n        attributes: { accesskey: \"A\" },\n      },\n      action: {\n        type: \"INSTALL_ADDON_FROM_URL\",\n        data: { url: null },\n      },\n    },\n    secondary: {\n      label: {\n        value: \"Not Now\",\n        attributes: { accesskey: \"N\" },\n      },\n      action: { type: \"CANCEL\" },\n    },\n  },\n};\n\nconst L10N_CONTENT = {\n  layout: \"addon_recommendation\",\n  category: \"dummyL10NCategory\",\n  bucket_id: \"some_bucket_id\",\n  notification_text: { string_id: \"notification_text_id\" },\n  heading_text: { string_id: \"heading_text_id\" },\n  info_icon: {\n    label: { string_id: \"why_seeing_this\" },\n    sumo_path: \"extensionrecommendations\",\n  },\n  addon: {\n    id: \"1234\",\n    title: \"Addon name\",\n    icon: \"https://mozilla.org/icon\",\n    author: \"Author name\",\n    amo_url: \"https://example.com\",\n  },\n  text: { string_id: \"text_id\" },\n  buttons: {\n    primary: {\n      label: { string_id: \"btn_ok_id\" },\n      action: {\n        type: \"INSTALL_ADDON_FROM_URL\",\n        data: { url: null },\n      },\n    },\n    secondary: {\n      label: { string_id: \"btn_cancel_id\" },\n      action: { type: \"CANCEL\" },\n    },\n  },\n};\n\ndescribe(\"ExtensionDoorhanger\", () => {\n  it(\"should validate DEFAULT_CONTENT\", () => {\n    assert.jsonSchema(DEFAULT_CONTENT, schema);\n  });\n  it(\"should validate L10N_CONTENT\", () => {\n    assert.jsonSchema(L10N_CONTENT, schema);\n  });\n  it(\"should validate all messages from CFRMessageProvider\", () => {\n    const messages = CFRMessageProvider.getMessages();\n    messages.forEach(msg => assert.jsonSchema(msg.content, schema));\n  });\n});\n"
  },
  {
    "path": "test/unit/asrouter/templates/FXASignupSnippet.test.jsx",
    "content": "import { FXASignupSnippet } from \"content-src/asrouter/templates/FXASignupSnippet/FXASignupSnippet\";\nimport { mount } from \"enzyme\";\nimport React from \"react\";\nimport schema from \"content-src/asrouter/templates/FXASignupSnippet/FXASignupSnippet.schema.json\";\nimport { SnippetsTestMessageProvider } from \"lib/SnippetsTestMessageProvider.jsm\";\n\nconst DEFAULT_CONTENT = SnippetsTestMessageProvider.getMessages().find(\n  msg => msg.template === \"fxa_signup_snippet\"\n).content;\n\ndescribe(\"FXASignupSnippet\", () => {\n  let sandbox;\n\n  function mountAndCheckProps(content = {}) {\n    const props = {\n      id: \"foo123\",\n      content: Object.assign(\n        { utm_campaign: \"foo\", utm_term: \"bar\" },\n        DEFAULT_CONTENT,\n        content\n      ),\n      onBlock() {},\n      onDismiss: sandbox.stub(),\n      sendUserActionTelemetry: sandbox.stub(),\n      onAction: sandbox.stub(),\n    };\n    const comp = mount(<FXASignupSnippet {...props} />);\n    // Check schema with the final props the component receives (including defaults)\n    assert.jsonSchema(comp.children().get(0).props.content, schema);\n    return comp;\n  }\n\n  beforeEach(() => {\n    sandbox = sinon.createSandbox();\n  });\n  afterEach(() => {\n    sandbox.restore();\n  });\n\n  it(\"should have the correct defaults\", () => {\n    const defaults = {\n      id: \"foo123\",\n      onBlock() {},\n      content: {},\n      onDismiss: sandbox.stub(),\n      sendUserActionTelemetry: sandbox.stub(),\n      onAction: sandbox.stub(),\n    };\n    const wrapper = mount(<FXASignupSnippet {...defaults} />);\n    // FXASignupSnippet is a wrapper around SubmitFormSnippet\n    const { props } = wrapper.children().get(0);\n\n    const defaultProperties = Object.keys(schema.properties).filter(\n      prop => schema.properties[prop].default\n    );\n    assert.lengthOf(defaultProperties, 5);\n    defaultProperties.forEach(prop =>\n      assert.propertyVal(props.content, prop, schema.properties[prop].default)\n    );\n\n    const defaultHiddenProperties = Object.keys(\n      schema.properties.hidden_inputs.properties\n    ).filter(prop => schema.properties.hidden_inputs.properties[prop].default);\n    assert.lengthOf(defaultHiddenProperties, 0);\n  });\n\n  it(\"should have a form_action\", () => {\n    const wrapper = mountAndCheckProps();\n\n    assert.propertyVal(\n      wrapper.children().get(0).props,\n      \"form_action\",\n      \"https://accounts.firefox.com/\"\n    );\n  });\n\n  it(\"should navigate to scene2\", () => {\n    const wrapper = mountAndCheckProps({});\n\n    wrapper.find(\".ASRouterButton\").simulate(\"click\");\n\n    assert.lengthOf(wrapper.find(\".mainInput\"), 1);\n  });\n});\n"
  },
  {
    "path": "test/unit/asrouter/templates/FirstRun.test.jsx",
    "content": "import {\n  FirstRun,\n  FLUENT_FILES,\n} from \"content-src/asrouter/templates/FirstRun/FirstRun\";\nimport { Interrupt } from \"content-src/asrouter/templates/FirstRun/Interrupt\";\nimport { Triplets } from \"content-src/asrouter/templates/FirstRun/Triplets\";\nimport { OnboardingMessageProvider } from \"lib/OnboardingMessageProvider.jsm\";\nimport { mount } from \"enzyme\";\nimport React from \"react\";\n\nconst FAKE_TRIPLETS_BUNDLE_1 = [\n  {\n    id: \"CARD_1\",\n    content: {\n      title: { string_id: \"onboarding-private-browsing-title\" },\n      text: { string_id: \"onboarding-private-browsing-text\" },\n      icon: \"icon\",\n      primary_button: {\n        label: { string_id: \"onboarding-button-label-get-started\" },\n        action: {\n          type: \"OPEN_URL\",\n          data: { args: \"https://example.com/\" },\n        },\n      },\n    },\n  },\n];\n\nconst FAKE_TRIPLETS_BUNDLE_2 = [\n  {\n    id: \"CARD_2\",\n    content: {\n      title: { string_id: \"onboarding-data-sync-title\" },\n      text: { string_id: \"onboarding-data-sync-text2\" },\n      icon: \"icon\",\n      primary_button: {\n        label: { string_id: \"onboarding-data-sync-button2\" },\n        action: {\n          type: \"OPEN_URL\",\n          data: { args: \"https://foo.com/\" },\n        },\n      },\n    },\n  },\n];\n\nconst FAKE_FLOW_PARAMS = {\n  deviceId: \"foo\",\n  flowId: \"abc1\",\n  flowBeginTime: 1234,\n};\n\nasync function getTestMessage(id, requestNewBundle) {\n  const message = (await OnboardingMessageProvider.getUntranslatedMessages()).find(\n    msg => msg.id === id\n  );\n\n  // Simulate dynamic triplets by returning a different bundle\n  if (requestNewBundle) {\n    return { ...message, bundle: FAKE_TRIPLETS_BUNDLE_2 };\n  }\n  return { ...message, bundle: FAKE_TRIPLETS_BUNDLE_1 };\n}\n\ndescribe(\"<FirstRun>\", () => {\n  let wrapper;\n  let message;\n  let fakeDoc;\n  let sandbox;\n  let clock;\n  let onBlockByIdStub;\n\n  async function setup() {\n    sandbox = sinon.createSandbox();\n    clock = sandbox.useFakeTimers();\n    message = await getTestMessage(\"TRAILHEAD_1\");\n    fakeDoc = {\n      body: document.createElement(\"body\"),\n      head: document.createElement(\"head\"),\n      createElement: type => document.createElement(type),\n      getElementById: () => document.createElement(\"div\"),\n      activeElement: document.createElement(\"div\"),\n    };\n    onBlockByIdStub = sandbox.stub();\n\n    sandbox\n      .stub(global, \"fetch\")\n      .withArgs(\"http://fake.com/endpoint\")\n      .resolves({\n        ok: true,\n        status: 200,\n        json: () => Promise.resolve(FAKE_FLOW_PARAMS),\n      });\n\n    wrapper = mount(\n      <FirstRun\n        message={message}\n        document={fakeDoc}\n        dispatch={() => {}}\n        sendUserActionTelemetry={() => {}}\n        onBlockById={onBlockByIdStub}\n      />\n    );\n  }\n\n  beforeEach(setup);\n  afterEach(() => {\n    sandbox.restore();\n  });\n\n  it(\"should render\", () => {\n    assert.ok(wrapper);\n  });\n  describe(\"with both interrupt and triplets\", () => {\n    it(\"should render interrupt and triplets\", () => {\n      assert.lengthOf(wrapper.find(Interrupt), 1, \"<Interrupt>\");\n      assert.lengthOf(wrapper.find(Triplets), 1, \"<Triplets>\");\n    });\n    it(\"should show the card panel and hide the content on the Triplets\", () => {\n      // This is so the container shows up in the background but we can fade in the content when intterupt is closed.\n      const tripletsProps = wrapper.find(Triplets).props();\n      assert.propertyVal(tripletsProps, \"showCardPanel\", true);\n      assert.propertyVal(tripletsProps, \"showContent\", false);\n    });\n    it(\"should set the UTM term to trailhead-join (for the traihead-join message)\", () => {\n      const iProps = wrapper.find(Interrupt).props();\n      const tProps = wrapper.find(Triplets).props();\n      assert.propertyVal(iProps, \"UTMTerm\", \"trailhead-join\");\n      assert.propertyVal(tProps, \"UTMTerm\", \"trailhead-join-card\");\n    });\n  });\n\n  describe(\"with an interrupt but no triplets\", () => {\n    beforeEach(() => {\n      message.bundle = []; // Empty triplets\n      wrapper = mount(<FirstRun message={message} document={fakeDoc} />);\n    });\n    it(\"should render interrupt but no triplets\", () => {\n      assert.lengthOf(wrapper.find(Interrupt), 1, \"<Interrupt>\");\n      assert.lengthOf(wrapper.find(Triplets), 0, \"<Triplets>\");\n    });\n  });\n\n  describe(\"with triplets but no interrupt\", () => {\n    it(\"should render interrupt but no triplets\", () => {\n      delete message.content; // Empty interrupt\n      wrapper = mount(<FirstRun message={message} document={fakeDoc} />);\n\n      assert.lengthOf(wrapper.find(Interrupt), 0, \"<Interrupt>\");\n      assert.lengthOf(wrapper.find(Triplets), 1, \"<Triplets>\");\n    });\n  });\n\n  describe(\"with no triplets or interrupt\", () => {\n    it(\"should render empty\", () => {\n      message = { type: \"FOO_123\" };\n      wrapper = mount(<FirstRun message={message} document={fakeDoc} />);\n\n      assert.isTrue(wrapper.isEmptyRender());\n    });\n  });\n\n  it(\"should pass along executeAction appropriately\", () => {\n    const stub = sandbox.stub();\n    wrapper = mount(\n      <FirstRun message={message} document={fakeDoc} executeAction={stub} />\n    );\n\n    assert.propertyVal(wrapper.find(Interrupt).props(), \"executeAction\", stub);\n    assert.propertyVal(wrapper.find(Triplets).props(), \"onAction\", stub);\n  });\n\n  it(\"should load flow params on mount if fxaEndpoint is defined\", () => {\n    const stub = sandbox.stub();\n    wrapper = mount(\n      <FirstRun\n        message={message}\n        document={fakeDoc}\n        dispatch={() => {}}\n        fetchFlowParams={stub}\n        fxaEndpoint=\"https://foo.com\"\n      />\n    );\n    assert.calledOnce(stub);\n  });\n\n  it(\"should load flow params onUpdate if fxaEndpoint is not defined on mount and then later defined\", () => {\n    const stub = sandbox.stub();\n    wrapper = mount(\n      <FirstRun\n        message={message}\n        document={fakeDoc}\n        fetchFlowParams={stub}\n        dispatch={() => {}}\n      />\n    );\n    assert.notCalled(stub);\n    wrapper.setProps({ fxaEndpoint: \"https://foo.com\" });\n    assert.calledOnce(stub);\n  });\n\n  it(\"should not load flow params again onUpdate if they were already set\", () => {\n    const stub = sandbox.stub();\n    wrapper = mount(\n      <FirstRun\n        message={message}\n        document={fakeDoc}\n        dispatch={() => {}}\n        fetchFlowParams={stub}\n        fxaEndpoint=\"https://foo.com\"\n      />\n    );\n    wrapper.setProps({ foo: \"bar\" });\n    wrapper.setProps({ foo: \"baz\" });\n    assert.calledOnce(stub);\n  });\n\n  it(\"should load fluent files on mount\", () => {\n    assert.lengthOf(fakeDoc.head.querySelectorAll(\"link\"), FLUENT_FILES.length);\n  });\n\n  it(\"should hide the interrupt and show the triplets when onNextScene is called\", () => {\n    // Simulate calling next scene\n    wrapper\n      .find(Interrupt)\n      .find(\".trailheadStart\")\n      .simulate(\"click\");\n\n    assert.lengthOf(wrapper.find(Interrupt), 0, \"Interrupt hidden\");\n    assert.isTrue(\n      wrapper\n        .find(Triplets)\n        .find(\".trailheadCardGrid\")\n        .hasClass(\"show\"),\n      \"Show triplet content\"\n    );\n  });\n\n  it(\"should hide the interrupt when props.interruptCleared changes to true\", () => {\n    assert.lengthOf(wrapper.find(Interrupt), 1, \"Interrupt shown\");\n    wrapper.setProps({ interruptCleared: true });\n\n    assert.lengthOf(wrapper.find(Interrupt), 0, \"Interrupt hidden\");\n  });\n\n  it(\"should hide triplets when closeTriplets is called and block extended triplets after 500ms\", () => {\n    // Simulate calling next scene\n    wrapper\n      .find(Triplets)\n      .find(\".icon-dismiss\")\n      .simulate(\"click\");\n\n    assert.isFalse(\n      wrapper\n        .find(Triplets)\n        .find(\".trailheadCardGrid\")\n        .hasClass(\"show\"),\n      \"Show triplet content\"\n    );\n\n    assert.notCalled(onBlockByIdStub);\n    clock.tick(500);\n    assert.calledWith(onBlockByIdStub, \"EXTENDED_TRIPLETS_1\");\n  });\n\n  it(\"should update triplets card when cards in message bundle changes\", async () => {\n    let tripletsProps = wrapper.find(Triplets).props();\n    assert.propertyVal(tripletsProps, \"cards\", FAKE_TRIPLETS_BUNDLE_1);\n\n    const messageWithNewBundle = await getTestMessage(\"TRAILHEAD_1\", true);\n    wrapper.setProps({ message: messageWithNewBundle });\n    tripletsProps = wrapper.find(Triplets).props();\n    assert.propertyVal(tripletsProps, \"cards\", FAKE_TRIPLETS_BUNDLE_2);\n  });\n});\n"
  },
  {
    "path": "test/unit/asrouter/templates/FullPageInterrupt.test.jsx",
    "content": "import { mount } from \"enzyme\";\nimport { OnboardingMessageProvider } from \"lib/OnboardingMessageProvider.jsm\";\nimport React from \"react\";\nimport {\n  FullPageInterrupt,\n  FxAccounts,\n  FxCards,\n} from \"content-src/asrouter/templates/FullPageInterrupt/FullPageInterrupt\";\nimport { FxASignupForm } from \"content-src/asrouter/components/FxASignupForm/FxASignupForm\";\nimport { OnboardingCard } from \"content-src/asrouter/templates/OnboardingMessage/OnboardingMessage\";\n\nconst CARDS = [\n  {\n    id: \"CARD_1\",\n    content: {\n      title: { string_id: \"onboarding-private-browsing-title\" },\n      text: { string_id: \"onboarding-private-browsing-text\" },\n      icon: \"icon\",\n      primary_button: {\n        label: { string_id: \"onboarding-button-label-get-started\" },\n        action: {\n          type: \"OPEN_URL\",\n          data: { args: \"https://example.com/\" },\n        },\n      },\n    },\n  },\n];\n\nconst FAKE_FLOW_PARAMS = {\n  deviceId: \"foo\",\n  flowId: \"abc1\",\n  flowBeginTime: 1234,\n};\n\ndescribe(\"<FullPageInterrupt>\", () => {\n  let wrapper;\n  let dummyNode;\n  let dispatch;\n  let onBlock;\n  let sandbox;\n  let onAction;\n  let onBlockById;\n  let sendTelemetryStub;\n\n  beforeEach(async () => {\n    sandbox = sinon.createSandbox();\n    dispatch = sandbox.stub();\n    onBlock = sandbox.stub();\n    onAction = sandbox.stub();\n    onBlockById = sandbox.stub();\n    sendTelemetryStub = sandbox.stub();\n\n    dummyNode = document.createElement(\"body\");\n    sandbox.stub(dummyNode, \"querySelector\").returns(dummyNode);\n    const fakeDocument = {\n      getElementById() {\n        return dummyNode;\n      },\n    };\n\n    const message = (await OnboardingMessageProvider.getUntranslatedMessages()).find(\n      msg => msg.id === \"FULL_PAGE_1\"\n    );\n\n    wrapper = mount(\n      <FullPageInterrupt\n        message={message}\n        UTMTerm={message.utm_term}\n        fxaEndpoint=\"https://accounts.firefox.com/endpoint/\"\n        dispatch={dispatch}\n        onBlock={onBlock}\n        onAction={onAction}\n        onBlockById={onBlockById}\n        cards={CARDS}\n        document={fakeDocument}\n        sendUserActionTelemetry={sendTelemetryStub}\n        flowParams={FAKE_FLOW_PARAMS}\n      />\n    );\n  });\n\n  afterEach(() => {\n    sandbox.restore();\n  });\n\n  it(\"should trigger onBlock on removeOverlay\", () => {\n    wrapper.instance().removeOverlay();\n    assert.calledOnce(onBlock);\n  });\n\n  it(\"should render Full Page interrupt with accounts and triplet cards section\", () => {\n    assert.lengthOf(wrapper.find(FxAccounts), 1);\n    assert.lengthOf(wrapper.find(FxCards), 1);\n  });\n\n  it(\"should render FxASignupForm inside FxAccounts\", () => {\n    assert.lengthOf(wrapper.find(FxASignupForm), 1);\n  });\n\n  it(\"should display learn more link on full page\", () => {\n    assert.ok(wrapper.find(\"a.fullpage-left-link\").exists());\n  });\n\n  it(\"should add utm_* query params to card actions and send the right ping when a card button is clicked\", () => {\n    wrapper\n      .find(OnboardingCard)\n      .find(\"button.onboardingButton\")\n      .simulate(\"click\");\n\n    assert.calledOnce(onAction);\n    const url = onAction.firstCall.args[0].data.args;\n    assert.equal(\n      url,\n      \"https://example.com/?utm_source=activity-stream&utm_campaign=firstrun&utm_medium=referral&utm_term=trailhead-full_page_d\"\n    );\n    assert.calledWith(sendTelemetryStub, {\n      event: \"CLICK_BUTTON\",\n      message_id: CARDS[0].id,\n      id: \"TRAILHEAD\",\n    });\n  });\n  it(\"should not call blockById by default when a card button is clicked\", () => {\n    wrapper\n      .find(OnboardingCard)\n      .find(\"button.onboardingButton\")\n      .simulate(\"click\");\n    assert.notCalled(onBlockById);\n  });\n  it(\"should call blockById when blockOnClick on message is true\", () => {\n    CARDS[0].blockOnClick = true;\n    wrapper\n      .find(OnboardingCard)\n      .find(\"button.onboardingButton\")\n      .simulate(\"click\");\n    assert.calledOnce(onBlockById);\n    assert.calledWith(onBlockById, CARDS[0].id);\n  });\n});\n"
  },
  {
    "path": "test/unit/asrouter/templates/FxASignupForm.test.jsx",
    "content": "import { actionCreators as ac, actionTypes as at } from \"common/Actions.jsm\";\nimport { FxASignupForm } from \"content-src/asrouter/components/FxASignupForm/FxASignupForm\";\nimport { mount } from \"enzyme\";\nimport React from \"react\";\n\ndescribe(\"<FxASignupForm>\", () => {\n  let wrapper;\n  let dummyNode;\n  let dispatch;\n  let onClose;\n  let sandbox;\n\n  const FAKE_FLOW_PARAMS = {\n    deviceId: \"foo\",\n    flowId: \"abc1\",\n    flowBeginTime: 1234,\n  };\n\n  const FAKE_MESSAGE_CONTENT = {\n    title: { string_id: \"onboarding-welcome-body\" },\n    learn: {\n      text: { string_id: \"onboarding-welcome-learn-more\" },\n      url: \"https://www.mozilla.org/firefox/accounts/\",\n    },\n    form: {\n      title: { string_id: \"onboarding-welcome-form-header\" },\n      text: { string_id: \"onboarding-join-form-body\" },\n      email: { string_id: \"onboarding-fullpage-form-email\" },\n      button: { string_id: \"onboarding-join-form-continue\" },\n    },\n  };\n\n  beforeEach(async () => {\n    sandbox = sinon.sandbox.create();\n    dispatch = sandbox.stub();\n    onClose = sandbox.stub();\n\n    dummyNode = document.createElement(\"body\");\n    sandbox.stub(dummyNode, \"querySelector\").returns(dummyNode);\n    const fakeDocument = {\n      getElementById() {\n        return dummyNode;\n      },\n    };\n\n    wrapper = mount(\n      <FxASignupForm\n        document={fakeDocument}\n        content={FAKE_MESSAGE_CONTENT}\n        dispatch={dispatch}\n        fxaEndpoint=\"https://accounts.firefox.com/endpoint/\"\n        UTMTerm=\"test-utm-term\"\n        flowParams={FAKE_FLOW_PARAMS}\n        onClose={onClose}\n      />\n    );\n  });\n\n  afterEach(() => {\n    sandbox.restore();\n  });\n\n  it(\"should prevent submissions with no email\", () => {\n    const form = wrapper.find(\"form\");\n    const preventDefault = sandbox.stub();\n\n    form.simulate(\"submit\", { preventDefault });\n\n    assert.calledOnce(preventDefault);\n    assert.notCalled(dispatch);\n  });\n\n  it(\"should not display signin link by default\", () => {\n    assert.notOk(\n      wrapper\n        .find(\"button[data-l10n-id='onboarding-join-form-signin']\")\n        .exists()\n    );\n  });\n\n  it(\"should display signin when showSignInLink is true\", () => {\n    wrapper.setProps({ showSignInLink: true });\n    let signIn = wrapper.find(\n      \"button[data-l10n-id='onboarding-join-form-signin']\"\n    );\n    assert.exists(signIn);\n  });\n\n  it(\"should emit UserEvent SUBMIT_EMAIL when you submit a valid email\", () => {\n    let form = wrapper.find(\"form\");\n    assert.ok(form.exists());\n    form.getDOMNode().elements.email.value = \"a@b.c\";\n\n    form.simulate(\"submit\");\n    assert.calledOnce(dispatch);\n    assert.isUserEventAction(dispatch.firstCall.args[0]);\n    assert.calledWith(\n      dispatch,\n      ac.UserEvent({\n        event: at.SUBMIT_EMAIL,\n        value: { has_flow_params: true },\n      })\n    );\n  });\n\n  it(\"should emit UserEvent SUBMIT_SIGNIN when submit with email disabled\", () => {\n    let form = wrapper.find(\"form\");\n    form.getDOMNode().elements.email.disabled = true;\n\n    form.simulate(\"submit\");\n    assert.calledOnce(dispatch);\n    assert.isUserEventAction(dispatch.firstCall.args[0]);\n    assert.calledWith(\n      dispatch,\n      ac.UserEvent({\n        event: at.SUBMIT_SIGNIN,\n        value: { has_flow_params: true },\n      })\n    );\n  });\n});\n"
  },
  {
    "path": "test/unit/asrouter/templates/Interrupt.test.jsx",
    "content": "import { FullPageInterrupt } from \"content-src/asrouter/templates/FullPageInterrupt/FullPageInterrupt\";\nimport { Interrupt } from \"content-src/asrouter/templates/FirstRun/Interrupt\";\nimport { ReturnToAMO } from \"content-src/asrouter/templates/ReturnToAMO/ReturnToAMO\";\nimport { Trailhead } from \"content-src/asrouter/templates//Trailhead/Trailhead\";\nimport { shallow } from \"enzyme\";\nimport React from \"react\";\n\ndescribe(\"<Interrupt>\", () => {\n  let wrapper;\n  it(\"should render Return TO AMO when the message has a template of return_to_amo_overlay\", () => {\n    wrapper = shallow(\n      <Interrupt\n        message={{ id: \"FOO\", content: {}, template: \"return_to_amo_overlay\" }}\n      />\n    );\n    assert.lengthOf(wrapper.find(ReturnToAMO), 1);\n  });\n  it(\"should render Trailhead when the message has a template of trailhead\", () => {\n    wrapper = shallow(\n      <Interrupt message={{ id: \"FOO\", content: {}, template: \"trailhead\" }} />\n    );\n    assert.lengthOf(wrapper.find(Trailhead), 1);\n  });\n  it(\"should render Full Page interrupt when the message has a template of full_page_interrupt\", () => {\n    wrapper = shallow(\n      <Interrupt\n        message={{ id: \"FOO\", content: {}, template: \"full_page_interrupt\" }}\n      />\n    );\n    assert.lengthOf(wrapper.find(FullPageInterrupt), 1);\n  });\n  it(\"should throw an error if another type of message is dispatched\", () => {\n    assert.throws(() => {\n      wrapper = shallow(\n        <Interrupt message={{ id: \"FOO\", template: \"something\" }} />\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/asrouter/templates/NewsletterSnippet.test.jsx",
    "content": "import { mount } from \"enzyme\";\nimport { NewsletterSnippet } from \"content-src/asrouter/templates/NewsletterSnippet/NewsletterSnippet\";\nimport React from \"react\";\nimport schema from \"content-src/asrouter/templates/NewsletterSnippet/NewsletterSnippet.schema.json\";\nimport { SnippetsTestMessageProvider } from \"lib/SnippetsTestMessageProvider.jsm\";\n\nconst DEFAULT_CONTENT = SnippetsTestMessageProvider.getMessages().find(\n  msg => msg.template === \"newsletter_snippet\"\n).content;\n\ndescribe(\"NewsletterSnippet\", () => {\n  let sandbox;\n\n  function mountAndCheckProps(content = {}) {\n    const props = {\n      id: \"foo123\",\n      content: Object.assign({}, DEFAULT_CONTENT, content),\n      onBlock() {},\n      onDismiss: sandbox.stub(),\n      sendUserActionTelemetry: sandbox.stub(),\n      onAction: sandbox.stub(),\n    };\n    const comp = mount(<NewsletterSnippet {...props} />);\n    // Check schema with the final props the component receives (including defaults)\n    assert.jsonSchema(comp.children().get(0).props.content, schema);\n    return comp;\n  }\n\n  beforeEach(() => {\n    sandbox = sinon.createSandbox();\n  });\n  afterEach(() => {\n    sandbox.restore();\n  });\n\n  describe(\"schema test\", () => {\n    it(\"should validate the schema and defaults\", () => {\n      const wrapper = mountAndCheckProps();\n      wrapper.find(\".ASRouterButton\").simulate(\"click\");\n      assert.equal(wrapper.find(\".mainInput\").instance().type, \"email\");\n    });\n\n    it(\"should have all of the default fields\", () => {\n      const defaults = {\n        id: \"foo123\",\n        content: {},\n        onBlock() {},\n        onDismiss: sandbox.stub(),\n        sendUserActionTelemetry: sandbox.stub(),\n        onAction: sandbox.stub(),\n      };\n      const wrapper = mount(<NewsletterSnippet {...defaults} />);\n      // NewsletterSnippet is a wrapper around SubmitFormSnippet\n      const { props } = wrapper.children().get(0);\n\n      // the `locale` properties gets used as part of hidden_fields so we\n      // check for it separately\n      const properties = { ...schema.properties };\n      const { locale } = properties;\n      delete properties.locale;\n\n      const defaultProperties = Object.keys(properties).filter(\n        prop => properties[prop].default\n      );\n      assert.lengthOf(defaultProperties, 6);\n      defaultProperties.forEach(prop =>\n        assert.propertyVal(props.content, prop, properties[prop].default)\n      );\n\n      const defaultHiddenProperties = Object.keys(\n        schema.properties.hidden_inputs.properties\n      ).filter(\n        prop => schema.properties.hidden_inputs.properties[prop].default\n      );\n      assert.lengthOf(defaultHiddenProperties, 1);\n      defaultHiddenProperties.forEach(prop =>\n        assert.propertyVal(\n          props.content.hidden_inputs,\n          prop,\n          schema.properties.hidden_inputs.properties[prop].default\n        )\n      );\n      assert.propertyVal(props.content.hidden_inputs, \"lang\", locale.default);\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/asrouter/templates/OnboardingMessage.test.jsx",
    "content": "import { GlobalOverrider } from \"test/unit/utils\";\nimport { OnboardingMessageProvider } from \"lib/OnboardingMessageProvider.jsm\";\nimport schema from \"content-src/asrouter/templates/OnboardingMessage/OnboardingMessage.schema.json\";\nimport badgeSchema from \"content-src/asrouter/templates/OnboardingMessage/ToolbarBadgeMessage.schema.json\";\nimport whatsNewSchema from \"content-src/asrouter/templates/OnboardingMessage/WhatsNewMessage.schema.json\";\n\nconst DEFAULT_CONTENT = {\n  title: \"A title\",\n  text: \"A description\",\n  icon: \"icon\",\n  primary_button: {\n    label: \"some_button_label\",\n    action: {\n      type: \"SOME_TYPE\",\n      data: { args: \"example.com\" },\n    },\n  },\n};\n\nconst L10N_CONTENT = {\n  title: { string_id: \"onboarding-private-browsing-title\" },\n  text: { string_id: \"onboarding-private-browsing-text\" },\n  icon: \"icon\",\n  primary_button: {\n    label: { string_id: \"onboarding-button-label-get-started\" },\n    action: { type: \"SOME_TYPE\" },\n  },\n};\n\ndescribe(\"OnboardingMessage\", () => {\n  let globals;\n  let sandbox;\n  beforeEach(() => {\n    globals = new GlobalOverrider();\n    sandbox = sinon.createSandbox();\n    globals.set(\"FxAccountsConfig\", {\n      promiseConnectAccountURI: sandbox.stub().resolves(\"some/url\"),\n    });\n    globals.set(\"AddonRepository\", {\n      getAddonsByIDs: ([content]) => [\n        {\n          name: content,\n          sourceURI: { spec: \"foo\", scheme: \"https\" },\n          icons: { 64: \"icon\" },\n        },\n      ],\n    });\n  });\n  afterEach(() => {\n    sandbox.restore();\n    globals.restore();\n  });\n  it(\"should validate DEFAULT_CONTENT\", () => {\n    assert.jsonSchema(DEFAULT_CONTENT, schema);\n  });\n  it(\"should validate L10N_CONTENT\", () => {\n    assert.jsonSchema(L10N_CONTENT, schema);\n  });\n  it(\"should validate all messages from OnboardingMessageProvider\", async () => {\n    const messages = await OnboardingMessageProvider.getUntranslatedMessages();\n    // FXA_1 doesn't have content - so filter it out\n    messages\n      .filter(msg => msg.template in [\"onboarding\", \"return_to_amo_overlay\"])\n      .forEach(msg => assert.jsonSchema(msg.content, schema));\n  });\n  it(\"should validate all badge template messages\", async () => {\n    const messages = await OnboardingMessageProvider.getUntranslatedMessages();\n\n    messages\n      .filter(msg => msg.template === \"toolbar_badge\")\n      .forEach(msg => assert.jsonSchema(msg.content, badgeSchema));\n  });\n  it(\"should validate all What's New template messages\", async () => {\n    const messages = await OnboardingMessageProvider.getUntranslatedMessages();\n\n    messages\n      .filter(msg => msg.template === \"whatsnew_panel_message\")\n      .forEach(msg => assert.jsonSchema(msg.content, whatsNewSchema));\n  });\n  it(\"should decode the content field (double decoding)\", async () => {\n    const fakeContent = \"foo%2540bar.org\";\n    globals.set(\"AttributionCode\", {\n      getAttrDataAsync: sandbox\n        .stub()\n        .resolves({ content: fakeContent, source: \"addons.mozilla.org\" }),\n    });\n\n    const msgs = (await OnboardingMessageProvider.getUntranslatedMessages()).filter(\n      ({ id }) => id === \"RETURN_TO_AMO_1\"\n    );\n    const [\n      translatedMessage,\n    ] = await OnboardingMessageProvider.translateMessages(msgs);\n    assert.propertyVal(\n      translatedMessage.content.text.args,\n      \"addon-name\",\n      \"foo@bar.org\"\n    );\n  });\n  it(\"should catch any decoding exceptions\", async () => {\n    const fakeContent = \"foo%bar.org\";\n    globals.set(\"AttributionCode\", {\n      getAttrDataAsync: sandbox\n        .stub()\n        .resolves({ content: fakeContent, source: \"addons.mozilla.org\" }),\n    });\n\n    const msgs = (await OnboardingMessageProvider.getUntranslatedMessages()).filter(\n      ({ id }) => id === \"RETURN_TO_AMO_1\"\n    );\n    const [\n      translatedMessage,\n    ] = await OnboardingMessageProvider.translateMessages(msgs);\n    assert.propertyVal(\n      translatedMessage.content.text.args,\n      \"addon-name\",\n      fakeContent\n    );\n  });\n  it(\"should ignore attribution from sources other than mozilla.org\", async () => {\n    const fakeContent = \"foo%bar.org\";\n    globals.set(\"AttributionCode\", {\n      getAttrDataAsync: sandbox\n        .stub()\n        .resolves({ content: fakeContent, source: \"addons.allizom.org\" }),\n    });\n\n    const [\n      returnToAMOMsg,\n    ] = (await OnboardingMessageProvider.getUntranslatedMessages()).filter(\n      ({ id }) => id === \"RETURN_TO_AMO_1\"\n    );\n    assert.propertyVal(returnToAMOMsg.content.text.args, \"addon-name\", null);\n  });\n  it(\"should correctly add all addon information to the message after translation\", async () => {\n    const fakeContent = \"foo%2540bar.org\";\n    globals.set(\"AttributionCode\", {\n      getAttrDataAsync: sandbox\n        .stub()\n        .resolves({ content: fakeContent, source: \"addons.mozilla.org\" }),\n    });\n\n    const msgs = (await OnboardingMessageProvider.getUntranslatedMessages()).filter(\n      ({ id }) => id === \"RETURN_TO_AMO_1\"\n    );\n    const [\n      translatedMessage,\n    ] = await OnboardingMessageProvider.translateMessages(msgs);\n    assert.propertyVal(\n      translatedMessage.content.text.args,\n      \"addon-name\",\n      \"foo@bar.org\"\n    );\n    assert.propertyVal(translatedMessage.content, \"addon_icon\", \"icon\");\n    assert.propertyVal(\n      translatedMessage.content.primary_button.action.data,\n      \"url\",\n      \"foo\"\n    );\n    assert.propertyVal(\n      translatedMessage.content.primary_button.action.data,\n      \"telemetrySource\",\n      \"rtamo\"\n    );\n  });\n  it(\"should skip return_to_amo_overlay if any addon fields are missing\", async () => {\n    const fakeContent = \"foo%bar.org\";\n    globals.set(\"AttributionCode\", {\n      getAttrDataAsync: sandbox\n        .stub()\n        .resolves({ content: fakeContent, source: \"addons.mozilla.org\" }),\n    });\n    globals.set(\"AddonRepository\", {\n      getAddonsByIDs: ([content]) => [\n        {\n          name: content,\n          sourceURI: { spec: \"foo\", scheme: \"https\" },\n          icons: { 64: null },\n        },\n      ],\n    });\n\n    const msgs = (await OnboardingMessageProvider.getUntranslatedMessages()).filter(\n      ({ id }) => id === \"RETURN_TO_AMO_1\"\n    );\n    const translatedMessages = await OnboardingMessageProvider.translateMessages(\n      msgs\n    );\n    assert.lengthOf(translatedMessages, 0);\n  });\n  it(\"should skip return_to_amo_overlay if any addon fields are missing\", async () => {\n    const fakeContent = \"foo%bar.org\";\n    globals.set(\"AttributionCode\", {\n      getAttrDataAsync: sandbox\n        .stub()\n        .resolves({ content: fakeContent, source: \"addons.mozilla.org\" }),\n    });\n    globals.set(\"AddonRepository\", {\n      getAddonsByIDs: ([content]) => [\n        {\n          name: content,\n          sourceURI: { spec: null, scheme: \"https\" },\n          icons: { 64: \"icon\" },\n        },\n      ],\n    });\n\n    const msgs = (await OnboardingMessageProvider.getUntranslatedMessages()).filter(\n      ({ id }) => id === \"RETURN_TO_AMO_1\"\n    );\n    const translatedMessages = await OnboardingMessageProvider.translateMessages(\n      msgs\n    );\n    assert.lengthOf(translatedMessages, 0);\n  });\n  it(\"should skip return_to_amo_overlay if any addon fields are missing\", async () => {\n    const fakeContent = \"foo%bar.org\";\n    globals.set(\"AttributionCode\", {\n      getAttrDataAsync: sandbox\n        .stub()\n        .resolves({ content: fakeContent, source: \"addons.mozilla.org\" }),\n    });\n    globals.set(\"AddonRepository\", {\n      getAddonsByIDs: ([content]) => [\n        {\n          name: null,\n          sourceURI: { spec: \"foo\", scheme: \"https\" },\n          icons: { 64: \"icon\" },\n        },\n      ],\n    });\n\n    const msgs = (await OnboardingMessageProvider.getUntranslatedMessages()).filter(\n      ({ id }) => id === \"RETURN_TO_AMO_1\"\n    );\n    const translatedMessages = await OnboardingMessageProvider.translateMessages(\n      msgs\n    );\n    assert.lengthOf(translatedMessages, 0);\n  });\n  it(\"should skip return_to_amo_overlay if addon scheme is not https\", async () => {\n    const fakeContent = \"foo%bar.org\";\n    globals.set(\"AttributionCode\", {\n      getAttrDataAsync: sandbox\n        .stub()\n        .resolves({ content: fakeContent, source: \"addons.mozilla.org\" }),\n    });\n    globals.set(\"AddonRepository\", {\n      getAddonsByIDs: ([content]) => [\n        {\n          name: content,\n          sourceURI: { spec: \"foo\", scheme: \"http\" },\n          icons: { 64: \"icon\" },\n        },\n      ],\n    });\n\n    const msgs = (await OnboardingMessageProvider.getUntranslatedMessages()).filter(\n      ({ id }) => id === \"RETURN_TO_AMO_1\"\n    );\n    const translatedMessages = await OnboardingMessageProvider.translateMessages(\n      msgs\n    );\n    assert.lengthOf(translatedMessages, 0);\n  });\n  it(\"should skip return_to_amo_overlay if getAddonInfo fails\", async () => {\n    globals.set(\"AttributionCode\", {\n      getAttrDataAsync: sandbox.stub().rejects(),\n    });\n\n    const msgs = (await OnboardingMessageProvider.getUntranslatedMessages()).filter(\n      ({ id }) => id === \"RETURN_TO_AMO_1\"\n    );\n    const translatedMessages = await OnboardingMessageProvider.translateMessages(\n      msgs\n    );\n    assert.lengthOf(translatedMessages, 0);\n  });\n  it(\"should catch any exceptions fetching the addon information\", async () => {\n    const fakeContent = \"foo%bar.org\";\n    globals.set(\"AttributionCode\", {\n      getAttrDataAsync: sandbox.stub().resolves({ content: fakeContent }),\n    });\n    globals.set(\"AddonRepository\", {\n      getAddonsByIDs: sandbox.stub().rejects(),\n    });\n\n    const msgs = await OnboardingMessageProvider.getUntranslatedMessages();\n    const translatedMessages = await OnboardingMessageProvider.translateMessages(\n      msgs\n    );\n    const returnToAMOMsgs = translatedMessages.filter(\n      ({ id }) => id === \"RETURN_TO_AMO_1\"\n    );\n    assert.lengthOf(translatedMessages, msgs.length - 1);\n    assert.lengthOf(returnToAMOMsgs, 0);\n  });\n});\n"
  },
  {
    "path": "test/unit/asrouter/templates/SendToDeviceSnippet.test.jsx",
    "content": "import { mount } from \"enzyme\";\nimport React from \"react\";\nimport schema from \"content-src/asrouter/templates/SendToDeviceSnippet/SendToDeviceSnippet.schema.json\";\nimport { SendToDeviceSnippet } from \"content-src/asrouter/templates/SendToDeviceSnippet/SendToDeviceSnippet\";\nimport { SnippetsTestMessageProvider } from \"lib/SnippetsTestMessageProvider.jsm\";\n\nconst DEFAULT_CONTENT = SnippetsTestMessageProvider.getMessages().find(\n  msg => msg.template === \"send_to_device_snippet\"\n).content;\n\nasync function testBodyContains(body, key, value) {\n  const regex = new RegExp(\n    `Content-Disposition: form-data; name=\"${key}\"${value}`\n  );\n  const match = regex.exec(body);\n  return match;\n}\n\n/**\n * Simulates opening the second panel (form view), filling in the input, and submitting\n * @param {EnzymeWrapper} wrapper A SendToDevice wrapper\n * @param {string} value Email or phone number\n * @param {function?} setCustomValidity setCustomValidity stub\n */\nfunction openFormAndSetValue(wrapper, value, setCustomValidity = () => {}) {\n  // expand\n  wrapper.find(\".ASRouterButton\").simulate(\"click\");\n  // Fill in email\n  const input = wrapper.find(\".mainInput\");\n  input.instance().value = value;\n  input.simulate(\"change\", { target: { value, setCustomValidity } });\n  wrapper.find(\"form\").simulate(\"submit\");\n}\n\ndescribe(\"SendToDeviceSnippet\", () => {\n  let sandbox;\n  let fetchStub;\n  let jsonResponse;\n\n  function mountAndCheckProps(content = {}) {\n    const props = {\n      id: \"foo123\",\n      content: Object.assign({}, DEFAULT_CONTENT, content),\n      onBlock() {},\n      onDismiss: sandbox.stub(),\n      sendUserActionTelemetry: sandbox.stub(),\n      onAction: sandbox.stub(),\n    };\n    const comp = mount(<SendToDeviceSnippet {...props} />);\n    // Check schema with the final props the component receives (including defaults)\n    assert.jsonSchema(comp.children().get(0).props.content, schema);\n    return comp;\n  }\n\n  beforeEach(() => {\n    sandbox = sinon.createSandbox();\n    jsonResponse = { status: \"ok\" };\n    fetchStub = sandbox\n      .stub(global, \"fetch\")\n      .returns(Promise.resolve({ json: () => Promise.resolve(jsonResponse) }));\n  });\n  afterEach(() => {\n    sandbox.restore();\n  });\n\n  it(\"should have the correct defaults\", () => {\n    const defaults = {\n      id: \"foo123\",\n      onBlock() {},\n      content: {},\n      onDismiss: sandbox.stub(),\n      sendUserActionTelemetry: sandbox.stub(),\n      onAction: sandbox.stub(),\n      form_method: \"POST\",\n    };\n    const wrapper = mount(<SendToDeviceSnippet {...defaults} />);\n    // SendToDeviceSnippet is a wrapper around SubmitFormSnippet\n    const { props } = wrapper.children().get(0);\n\n    const defaultProperties = Object.keys(schema.properties).filter(\n      prop => schema.properties[prop].default\n    );\n    assert.lengthOf(defaultProperties, 7);\n    defaultProperties.forEach(prop =>\n      assert.propertyVal(props.content, prop, schema.properties[prop].default)\n    );\n\n    const defaultHiddenProperties = Object.keys(\n      schema.properties.hidden_inputs.properties\n    ).filter(prop => schema.properties.hidden_inputs.properties[prop].default);\n    assert.lengthOf(defaultHiddenProperties, 0);\n  });\n\n  describe(\"form input\", () => {\n    it(\"should set the input type to text if content.include_sms is true\", () => {\n      const wrapper = mountAndCheckProps({ include_sms: true });\n      wrapper.find(\".ASRouterButton\").simulate(\"click\");\n      assert.equal(wrapper.find(\".mainInput\").instance().type, \"text\");\n    });\n    it(\"should set the input type to email if content.include_sms is false\", () => {\n      const wrapper = mountAndCheckProps({ include_sms: false });\n      wrapper.find(\".ASRouterButton\").simulate(\"click\");\n      assert.equal(wrapper.find(\".mainInput\").instance().type, \"email\");\n    });\n    it(\"should validate the input with isEmailOrPhoneNumber if include_sms is true\", () => {\n      const wrapper = mountAndCheckProps({ include_sms: true });\n      const setCustomValidity = sandbox.stub();\n      openFormAndSetValue(wrapper, \"foo\", setCustomValidity);\n      assert.calledWith(\n        setCustomValidity,\n        \"Must be an email or a phone number.\"\n      );\n    });\n    it(\"should not custom validate the input if include_sms is false\", () => {\n      const wrapper = mountAndCheckProps({ include_sms: false });\n      const setCustomValidity = sandbox.stub();\n      openFormAndSetValue(wrapper, \"foo\", setCustomValidity);\n      assert.notCalled(setCustomValidity);\n    });\n  });\n\n  describe(\"submitting\", () => {\n    it(\"should send the right information to basket.mozilla.org/news/subscribe for an email\", async () => {\n      const wrapper = mountAndCheckProps({\n        locale: \"fr-CA\",\n        include_sms: true,\n        message_id_email: \"foo\",\n      });\n\n      openFormAndSetValue(wrapper, \"foo@bar.com\");\n      wrapper.find(\"form\").simulate(\"submit\");\n\n      assert.calledOnce(fetchStub);\n      const [request] = fetchStub.firstCall.args;\n\n      assert.equal(request.url, \"https://basket.mozilla.org/news/subscribe/\");\n      const body = await request.text();\n      assert.ok(testBodyContains(body, \"email\", \"foo@bar.com\"), \"has email\");\n      assert.ok(testBodyContains(body, \"lang\", \"fr-CA\"), \"has lang\");\n      assert.ok(\n        testBodyContains(body, \"newsletters\", \"foo\"),\n        \"has newsletters\"\n      );\n      assert.ok(\n        testBodyContains(body, \"source_url\", \"foo\"),\n        \"https%3A%2F%2Fsnippets.mozilla.com%2Fshow%2Ffoo123\"\n      );\n    });\n    it(\"should send the right information for an sms\", async () => {\n      const wrapper = mountAndCheckProps({\n        locale: \"fr-CA\",\n        include_sms: true,\n        message_id_sms: \"foo\",\n        country: \"CA\",\n      });\n\n      openFormAndSetValue(wrapper, \"5371283767\");\n      wrapper.find(\"form\").simulate(\"submit\");\n\n      assert.calledOnce(fetchStub);\n      const [request] = fetchStub.firstCall.args;\n\n      assert.equal(\n        request.url,\n        \"https://basket.mozilla.org/news/subscribe_sms/\"\n      );\n      const body = await request.text();\n      assert.ok(\n        testBodyContains(body, \"mobile_number\", \"5371283767\"),\n        \"has number\"\n      );\n      assert.ok(testBodyContains(body, \"lang\", \"fr-CA\"), \"has lang\");\n      assert.ok(testBodyContains(body, \"country\", \"CA\"), \"CA\");\n      assert.ok(testBodyContains(body, \"msg_name\", \"foo\"), \"has msg_name\");\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/asrouter/templates/SimpleBelowSearchSnippet.test.jsx",
    "content": "import { mount } from \"enzyme\";\nimport React from \"react\";\nimport schema from \"content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.schema.json\";\nimport { SimpleBelowSearchSnippet } from \"content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.jsx\";\n\nconst DEFAULT_CONTENT = { text: \"foo\" };\n\ndescribe(\"SimpleBelowSearchSnippet\", () => {\n  let sandbox;\n  let sendUserActionTelemetryStub;\n\n  /**\n   * mountAndCheckProps - Mounts a SimpleBelowSearchSnippet with DEFAULT_CONTENT extended with any props\n   *                      passed in the content param and validates props against the schema.\n   * @param {obj} content Object containing custom message content (e.g. {text, icon})\n   * @returns enzyme wrapper for SimpleSnippet\n   */\n  function mountAndCheckProps(content = {}, provider = \"test-provider\") {\n    const props = {\n      content: { ...DEFAULT_CONTENT, ...content },\n      provider,\n      sendUserActionTelemetry: sendUserActionTelemetryStub,\n      onAction: sandbox.stub(),\n    };\n    assert.jsonSchema(props.content, schema);\n    return mount(<SimpleBelowSearchSnippet {...props} />);\n  }\n\n  beforeEach(() => {\n    sandbox = sinon.createSandbox();\n    sendUserActionTelemetryStub = sandbox.stub();\n  });\n\n  afterEach(() => {\n    sandbox.restore();\n  });\n\n  it(\"should render .text\", () => {\n    const wrapper = mountAndCheckProps({ text: \"bar\" });\n    assert.equal(wrapper.find(\".body\").text(), \"bar\");\n  });\n\n  it(\"should render .icon (light theme)\", () => {\n    const wrapper = mountAndCheckProps({\n      icon: \"data:image/gif;base64,R0lGODl\",\n    });\n    assert.equal(\n      wrapper.find(\".icon-light-theme\").prop(\"src\"),\n      \"data:image/gif;base64,R0lGODl\"\n    );\n  });\n\n  it(\"should render .icon (dark theme)\", () => {\n    const wrapper = mountAndCheckProps({\n      icon_dark_theme: \"data:image/gif;base64,R0lGODl\",\n    });\n    assert.equal(\n      wrapper.find(\".icon-dark-theme\").prop(\"src\"),\n      \"data:image/gif;base64,R0lGODl\"\n    );\n  });\n});\n"
  },
  {
    "path": "test/unit/asrouter/templates/SimpleSnippet.test.jsx",
    "content": "import { mount } from \"enzyme\";\nimport React from \"react\";\nimport schema from \"content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.schema.json\";\nimport { SimpleSnippet } from \"content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.jsx\";\n\nconst DEFAULT_CONTENT = { text: \"foo\" };\n\ndescribe(\"SimpleSnippet\", () => {\n  let sandbox;\n  let onBlockStub;\n  let sendUserActionTelemetryStub;\n\n  /**\n   * mountAndCheckProps - Mounts a SimpleSnippet with DEFAULT_CONTENT extended with any props\n   *                      passed in the content param and validates props against the schema.\n   * @param {obj} content Object containing custom message content (e.g. {text, icon, title})\n   * @returns enzyme wrapper for SimpleSnippet\n   */\n  function mountAndCheckProps(content = {}, provider = \"test-provider\") {\n    const props = {\n      content: Object.assign({}, DEFAULT_CONTENT, content),\n      provider,\n      onBlock: onBlockStub,\n      sendUserActionTelemetry: sendUserActionTelemetryStub,\n      onAction: sandbox.stub(),\n    };\n    assert.jsonSchema(props.content, schema);\n    return mount(<SimpleSnippet {...props} />);\n  }\n\n  beforeEach(() => {\n    sandbox = sinon.createSandbox();\n    onBlockStub = sandbox.stub();\n    sendUserActionTelemetryStub = sandbox.stub();\n  });\n\n  afterEach(() => {\n    sandbox.restore();\n  });\n\n  it(\"should render .text\", () => {\n    const wrapper = mountAndCheckProps({ text: \"bar\" });\n    assert.equal(wrapper.find(\".body\").text(), \"bar\");\n  });\n  it(\"should not render title element if no .title prop is supplied\", () => {\n    const wrapper = mountAndCheckProps();\n    assert.lengthOf(wrapper.find(\".title\"), 0);\n  });\n  it(\"should render .title\", () => {\n    const wrapper = mountAndCheckProps({ title: \"Foo\" });\n    assert.equal(\n      wrapper\n        .find(\".title\")\n        .text()\n        .trim(),\n      \"Foo\"\n    );\n  });\n  it(\"should render a light theme variant .icon\", () => {\n    const wrapper = mountAndCheckProps({\n      icon: \"data:image/gif;base64,R0lGODl\",\n    });\n    assert.equal(\n      wrapper.find(\".icon-light-theme\").prop(\"src\"),\n      \"data:image/gif;base64,R0lGODl\"\n    );\n  });\n  it(\"should render a dark theme variant .icon\", () => {\n    const wrapper = mountAndCheckProps({\n      icon_dark_theme: \"data:image/gif;base64,R0lGODl\",\n    });\n    assert.equal(\n      wrapper.find(\".icon-dark-theme\").prop(\"src\"),\n      \"data:image/gif;base64,R0lGODl\"\n    );\n  });\n  it(\"should render a light theme variant .icon as fallback\", () => {\n    const wrapper = mountAndCheckProps({\n      icon_dark_theme: \"\",\n      icon: \"data:image/gif;base64,R0lGODp\",\n    });\n    assert.equal(\n      wrapper.find(\".icon-dark-theme\").prop(\"src\"),\n      \"data:image/gif;base64,R0lGODp\"\n    );\n  });\n  it(\"should render .button_label and default className\", () => {\n    const wrapper = mountAndCheckProps({\n      button_label: \"Click here\",\n      button_action: \"OPEN_APPLICATIONS_MENU\",\n      button_action_args: \"appMenu\",\n    });\n\n    const button = wrapper.find(\"button.ASRouterButton\");\n    button.simulate(\"click\");\n\n    assert.equal(button.text(), \"Click here\");\n    assert.equal(button.prop(\"className\"), \"ASRouterButton secondary\");\n    assert.calledOnce(wrapper.props().onAction);\n    assert.calledWithExactly(wrapper.props().onAction, {\n      type: \"OPEN_APPLICATIONS_MENU\",\n      data: { args: \"appMenu\" },\n    });\n  });\n  it(\"should not wrap the main content if a section header is not present\", () => {\n    const wrapper = mountAndCheckProps({ text: \"bar\" });\n    assert.lengthOf(wrapper.find(\".innerContentWrapper\"), 0);\n  });\n  it(\"should wrap the main content if a section header is present\", () => {\n    const wrapper = mountAndCheckProps({\n      section_title_icon: \"data:image/gif;base64,R0lGODl\",\n      section_title_text: \"Messages from Mozilla\",\n    });\n\n    assert.lengthOf(wrapper.find(\".innerContentWrapper\"), 1);\n  });\n  it(\"should render a section header if text and icon (light-theme) are specified\", () => {\n    const wrapper = mountAndCheckProps({\n      section_title_icon: \"data:image/gif;base64,R0lGODl\",\n      section_title_text: \"Messages from Mozilla\",\n    });\n\n    assert.equal(\n      wrapper.find(\".section-title .icon-light-theme\").prop(\"style\")\n        .backgroundImage,\n      'url(\"data:image/gif;base64,R0lGODl\")'\n    );\n    assert.equal(\n      wrapper\n        .find(\".section-title-text\")\n        .text()\n        .trim(),\n      \"Messages from Mozilla\"\n    );\n    // ensure there is no <a> when a section_title_url is not specified\n    assert.lengthOf(wrapper.find(\".section-title a\"), 0);\n  });\n  it(\"should render a section header if text and icon (light-theme) are specified\", () => {\n    const wrapper = mountAndCheckProps({\n      section_title_icon: \"data:image/gif;base64,R0lGODl\",\n      section_title_icon_dark_theme: \"data:image/gif;base64,R0lGODl\",\n      section_title_text: \"Messages from Mozilla\",\n    });\n\n    assert.equal(\n      wrapper.find(\".section-title .icon-dark-theme\").prop(\"style\")\n        .backgroundImage,\n      'url(\"data:image/gif;base64,R0lGODl\")'\n    );\n    assert.equal(\n      wrapper\n        .find(\".section-title-text\")\n        .text()\n        .trim(),\n      \"Messages from Mozilla\"\n    );\n    // ensure there is no <a> when a section_title_url is not specified\n    assert.lengthOf(wrapper.find(\".section-title a\"), 0);\n  });\n  it(\"should render a section header wrapped in an <a> tag if a url is provided\", () => {\n    const wrapper = mountAndCheckProps({\n      section_title_icon: \"data:image/gif;base64,R0lGODl\",\n      section_title_text: \"Messages from Mozilla\",\n      section_title_url: \"https://www.mozilla.org\",\n    });\n\n    assert.equal(\n      wrapper.find(\".section-title a\").prop(\"href\"),\n      \"https://www.mozilla.org\"\n    );\n  });\n  it(\"should send an OPEN_URL action when button_url is defined and button is clicked\", () => {\n    const wrapper = mountAndCheckProps({\n      button_label: \"Button\",\n      button_url: \"https://mozilla.org\",\n    });\n\n    const button = wrapper.find(\"button.ASRouterButton\");\n    button.simulate(\"click\");\n\n    assert.calledOnce(wrapper.props().onAction);\n    assert.calledWithExactly(wrapper.props().onAction, {\n      type: \"OPEN_URL\",\n      data: { args: \"https://mozilla.org\" },\n    });\n  });\n  it(\"should call props.onBlock and sendUserActionTelemetry when CTA button is clicked\", () => {\n    const wrapper = mountAndCheckProps({ text: \"bar\" });\n\n    wrapper.instance().onButtonClick();\n\n    assert.calledOnce(onBlockStub);\n    assert.calledOnce(sendUserActionTelemetryStub);\n  });\n\n  it(\"should not call props.onBlock if do_not_autoblock is true\", () => {\n    const wrapper = mountAndCheckProps({ text: \"bar\", do_not_autoblock: true });\n\n    wrapper.instance().onButtonClick();\n\n    assert.notCalled(onBlockStub);\n  });\n\n  it(\"should not call sendUserActionTelemetry for preview message when CTA button is clicked\", () => {\n    const wrapper = mountAndCheckProps({ text: \"bar\" }, \"preview\");\n\n    wrapper.instance().onButtonClick();\n\n    assert.calledOnce(onBlockStub);\n    assert.notCalled(sendUserActionTelemetryStub);\n  });\n});\n"
  },
  {
    "path": "test/unit/asrouter/templates/SubmitFormSnippet.test.jsx",
    "content": "import { mount } from \"enzyme\";\nimport React from \"react\";\nimport { RichText } from \"content-src/asrouter/components/RichText/RichText.jsx\";\nimport schema from \"content-src/asrouter/templates/SubmitFormSnippet/SubmitFormSnippet.schema.json\";\nimport { SubmitFormSnippet } from \"content-src/asrouter/templates/SubmitFormSnippet/SubmitFormSnippet.jsx\";\n\nconst DEFAULT_CONTENT = {\n  scene1_text: \"foo\",\n  scene2_text: \"bar\",\n  scene1_button_label: \"Sign Up\",\n  retry_button_label: \"Try again\",\n  form_action: \"foo.com\",\n  hidden_inputs: { foo: \"foo\" },\n  error_text: \"error\",\n  success_text: \"success\",\n};\n\ndescribe(\"SubmitFormSnippet\", () => {\n  let sandbox;\n  let onBlockStub;\n\n  /**\n   * mountAndCheckProps - Mounts a SubmitFormSnippet with DEFAULT_CONTENT extended with any props\n   *                      passed in the content param and validates props against the schema.\n   * @param {obj} content Object containing custom message content (e.g. {text, icon, title})\n   * @returns enzyme wrapper for SubmitFormSnippet\n   */\n  function mountAndCheckProps(content = {}) {\n    const props = {\n      content: Object.assign({}, DEFAULT_CONTENT, content),\n      onBlock: onBlockStub,\n      onDismiss: sandbox.stub(),\n      sendUserActionTelemetry: sandbox.stub(),\n      onAction: sandbox.stub(),\n      form_method: \"POST\",\n    };\n    assert.jsonSchema(props.content, schema);\n    return mount(<SubmitFormSnippet {...props} />);\n  }\n\n  beforeEach(() => {\n    sandbox = sinon.createSandbox();\n    onBlockStub = sandbox.stub();\n  });\n\n  afterEach(() => {\n    sandbox.restore();\n  });\n\n  it(\"should render .text\", () => {\n    const wrapper = mountAndCheckProps({ scene1_text: \"bar\" });\n    assert.equal(wrapper.find(\".body\").text(), \"bar\");\n  });\n  it(\"should not render title element if no .title prop is supplied\", () => {\n    const wrapper = mountAndCheckProps();\n    assert.lengthOf(wrapper.find(\".title\"), 0);\n  });\n  it(\"should render .title\", () => {\n    const wrapper = mountAndCheckProps({ scene1_title: \"Foo\" });\n    assert.equal(\n      wrapper\n        .find(\".title\")\n        .text()\n        .trim(),\n      \"Foo\"\n    );\n  });\n  it(\"should render light-theme .icon\", () => {\n    const wrapper = mountAndCheckProps({\n      scene1_icon: \"data:image/gif;base64,R0lGODl\",\n    });\n    assert.equal(\n      wrapper.find(\".icon-light-theme\").prop(\"src\"),\n      \"data:image/gif;base64,R0lGODl\"\n    );\n  });\n  it(\"should render dark-theme .icon\", () => {\n    const wrapper = mountAndCheckProps({\n      scene1_icon_dark_theme: \"data:image/gif;base64,R0lGODl\",\n    });\n    assert.equal(\n      wrapper.find(\".icon-dark-theme\").prop(\"src\"),\n      \"data:image/gif;base64,R0lGODl\"\n    );\n  });\n  it(\"should render .button_label and default className\", () => {\n    const wrapper = mountAndCheckProps({ scene1_button_label: \"Click here\" });\n\n    const button = wrapper.find(\"button.ASRouterButton\");\n    assert.equal(button.text(), \"Click here\");\n    assert.equal(button.prop(\"className\"), \"ASRouterButton secondary\");\n  });\n\n  describe(\"#SignupView\", () => {\n    let wrapper;\n    const fetchOk = { json: () => Promise.resolve({ status: \"ok\" }) };\n    const fetchFail = { json: () => Promise.resolve({ status: \"fail\" }) };\n\n    beforeEach(() => {\n      wrapper = mountAndCheckProps({\n        scene1_text: \"bar\",\n        scene2_email_placeholder_text: \"Email\",\n        scene2_text: \"signup\",\n      });\n    });\n\n    it(\"should set the input type if provided through props.inputType\", () => {\n      wrapper.setProps({ inputType: \"number\" });\n      wrapper.setState({ expanded: true });\n      assert.equal(wrapper.find(\".mainInput\").instance().type, \"number\");\n    });\n\n    it(\"should validate via props.validateInput if provided\", () => {\n      function validateInput(value, content) {\n        if (content.country === \"CA\" && value === \"poutine\") {\n          return \"\";\n        }\n        return \"Must be poutine\";\n      }\n      const setCustomValidity = sandbox.stub();\n      wrapper.setProps({\n        validateInput,\n        content: { ...DEFAULT_CONTENT, country: \"CA\" },\n      });\n      wrapper.setState({ expanded: true });\n      const input = wrapper.find(\".mainInput\");\n      input.instance().value = \"poutine\";\n      input.simulate(\"change\", {\n        target: { value: \"poutine\", setCustomValidity },\n      });\n      assert.calledWith(setCustomValidity, \"\");\n\n      input.instance().value = \"fried chicken\";\n      input.simulate(\"change\", {\n        target: { value: \"fried chicken\", setCustomValidity },\n      });\n      assert.calledWith(setCustomValidity, \"Must be poutine\");\n    });\n\n    it(\"should show the signup form if state.expanded is true\", () => {\n      wrapper.setState({ expanded: true });\n\n      assert.isTrue(wrapper.find(\"form\").exists());\n    });\n    it(\"should dismiss the snippet\", () => {\n      wrapper.setState({ expanded: true });\n\n      wrapper.find(\".ASRouterButton.secondary\").simulate(\"click\");\n\n      assert.calledOnce(wrapper.props().onDismiss);\n    });\n    it(\"should send a DISMISS event ping\", () => {\n      wrapper.setState({ expanded: true });\n\n      wrapper.find(\".ASRouterButton.secondary\").simulate(\"click\");\n\n      assert.equal(\n        wrapper.props().sendUserActionTelemetry.firstCall.args[0].event,\n        \"DISMISS\"\n      );\n    });\n    it(\"should render hidden inputs + email input\", () => {\n      wrapper.setState({ expanded: true });\n\n      assert.lengthOf(wrapper.find(\"input[type='hidden']\"), 1);\n    });\n    it(\"should open the SignupView when the action button is clicked\", () => {\n      assert.isFalse(wrapper.find(\"form\").exists());\n\n      wrapper.find(\".ASRouterButton\").simulate(\"click\");\n\n      assert.isTrue(wrapper.state().expanded);\n      assert.isTrue(wrapper.find(\"form\").exists());\n    });\n    it(\"should submit telemetry when the action button is clicked\", () => {\n      assert.isFalse(wrapper.find(\"form\").exists());\n\n      wrapper.find(\".ASRouterButton\").simulate(\"click\");\n\n      assert.equal(\n        wrapper.props().sendUserActionTelemetry.firstCall.args[0].value,\n        \"scene1-button-learn-more\"\n      );\n    });\n    it(\"should submit form data when submitted\", () => {\n      sandbox.stub(window, \"fetch\").resolves(fetchOk);\n      wrapper.setState({ expanded: true });\n\n      wrapper.find(\"form\").simulate(\"submit\");\n      assert.calledOnce(window.fetch);\n    });\n    it(\"should send user telemetry when submitted\", () => {\n      wrapper.setState({ expanded: true });\n\n      wrapper.find(\"form\").simulate(\"submit\");\n\n      assert.equal(\n        wrapper.props().sendUserActionTelemetry.firstCall.args[0].value,\n        \"conversion-subscribe-activation\"\n      );\n    });\n    it(\"should set signupSuccess when submission status is ok\", async () => {\n      sandbox.stub(window, \"fetch\").resolves(fetchOk);\n      wrapper.setState({ expanded: true });\n      await wrapper.instance().handleSubmit({ preventDefault: sandbox.stub() });\n\n      assert.equal(wrapper.state().signupSuccess, true);\n      assert.equal(wrapper.state().signupSubmitted, true);\n      assert.calledOnce(onBlockStub);\n      assert.calledWithExactly(onBlockStub, { preventDismiss: true });\n    });\n    it(\"should send user telemetry when submission status is ok\", async () => {\n      sandbox.stub(window, \"fetch\").resolves(fetchOk);\n      wrapper.setState({ expanded: true });\n      await wrapper.instance().handleSubmit({ preventDefault: sandbox.stub() });\n\n      assert.equal(\n        wrapper.props().sendUserActionTelemetry.secondCall.args[0].value,\n        \"subscribe-success\"\n      );\n    });\n    it(\"should not block the snippet if submission failed\", async () => {\n      sandbox.stub(window, \"fetch\").resolves(fetchFail);\n      wrapper.setState({ expanded: true });\n      await wrapper.instance().handleSubmit({ preventDefault: sandbox.stub() });\n\n      assert.equal(wrapper.state().signupSuccess, false);\n      assert.equal(wrapper.state().signupSubmitted, true);\n      assert.notCalled(onBlockStub);\n    });\n    it(\"should not block if do_not_autoblock is true\", async () => {\n      sandbox.stub(window, \"fetch\").resolves(fetchOk);\n      wrapper = mountAndCheckProps({\n        scene1_text: \"bar\",\n        scene2_email_placeholder_text: \"Email\",\n        scene2_text: \"signup\",\n        do_not_autoblock: true,\n      });\n      wrapper.setState({ expanded: true });\n      await wrapper.instance().handleSubmit({ preventDefault: sandbox.stub() });\n\n      assert.equal(wrapper.state().signupSuccess, true);\n      assert.equal(wrapper.state().signupSubmitted, true);\n      assert.notCalled(onBlockStub);\n    });\n    it(\"should send user telemetry if submission failed\", async () => {\n      sandbox.stub(window, \"fetch\").resolves(fetchFail);\n      wrapper.setState({ expanded: true });\n      await wrapper.instance().handleSubmit({ preventDefault: sandbox.stub() });\n\n      assert.equal(\n        wrapper.props().sendUserActionTelemetry.secondCall.args[0].value,\n        \"subscribe-error\"\n      );\n    });\n    it(\"should render the signup success message\", () => {\n      wrapper.setProps({ content: { success_text: \"success\" } });\n      wrapper.setState({ signupSuccess: true, signupSubmitted: true });\n\n      assert.isTrue(wrapper.find(\".submissionStatus\").exists());\n      assert.propertyVal(\n        wrapper.find(RichText).props(),\n        \"localization_id\",\n        \"success_text\"\n      );\n      assert.propertyVal(\n        wrapper.find(RichText).props(),\n        \"success_text\",\n        \"success\"\n      );\n      assert.isFalse(wrapper.find(\".ASRouterButton\").exists());\n    });\n    it(\"should render the signup error message\", () => {\n      wrapper.setProps({ content: { error_text: \"trouble\" } });\n      wrapper.setState({ signupSuccess: false, signupSubmitted: true });\n\n      assert.isTrue(wrapper.find(\".submissionStatus\").exists());\n      assert.propertyVal(\n        wrapper.find(RichText).props(),\n        \"localization_id\",\n        \"error_text\"\n      );\n      assert.propertyVal(\n        wrapper.find(RichText).props(),\n        \"error_text\",\n        \"trouble\"\n      );\n      assert.isTrue(wrapper.find(\".ASRouterButton\").exists());\n    });\n    it(\"should render the button to return to the signup form if there was an error\", () => {\n      wrapper.setState({ signupSubmitted: true, signupSuccess: false });\n\n      const button = wrapper.find(\"button.ASRouterButton\");\n      assert.equal(button.text(), \"Try again\");\n      wrapper.find(\".ASRouterButton\").simulate(\"click\");\n\n      assert.equal(wrapper.state().signupSubmitted, false);\n    });\n    it(\"should not render the privacy notice checkbox if prop is missing\", () => {\n      wrapper.setState({ expanded: true });\n\n      assert.isFalse(wrapper.find(\".privacyNotice\").exists());\n    });\n    it(\"should render the privacy notice checkbox if prop is provided\", () => {\n      wrapper.setProps({\n        content: { ...DEFAULT_CONTENT, scene2_privacy_html: \"privacy notice\" },\n      });\n      wrapper.setState({ expanded: true });\n\n      assert.isTrue(wrapper.find(\".privacyNotice\").exists());\n    });\n    it(\"should not call fetch if form_method is GET\", async () => {\n      sandbox.stub(window, \"fetch\").resolves(fetchOk);\n      wrapper.setProps({ form_method: \"GET\" });\n      wrapper.setState({ expanded: true });\n\n      await wrapper.instance().handleSubmit({ preventDefault: sandbox.stub() });\n\n      assert.notCalled(window.fetch);\n    });\n    it(\"should block the snippet when form_method is GET\", () => {\n      wrapper.setProps({ form_method: \"GET\" });\n      wrapper.setState({ expanded: true });\n\n      wrapper.instance().handleSubmit({ preventDefault: sandbox.stub() });\n\n      assert.calledOnce(onBlockStub);\n      assert.calledWithExactly(onBlockStub, { preventDismiss: true });\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/asrouter/templates/Trailhead.test.jsx",
    "content": "import { actionCreators as ac, actionTypes as at } from \"common/Actions.jsm\";\nimport { FxASignupForm } from \"content-src/asrouter/components/FxASignupForm/FxASignupForm\";\nimport { mount } from \"enzyme\";\nimport { OnboardingMessageProvider } from \"lib/OnboardingMessageProvider.jsm\";\nimport React from \"react\";\nimport { Trailhead } from \"content-src/asrouter/templates/Trailhead/Trailhead\";\n\nexport const CARDS = [\n  {\n    content: {\n      title: { string_id: \"onboarding-private-browsing-title\" },\n      text: { string_id: \"onboarding-private-browsing-text\" },\n      icon: \"icon\",\n      primary_button: {\n        label: { string_id: \"onboarding-button-label-get-started\" },\n        action: {\n          type: \"OPEN_URL\",\n          data: { args: \"https://example.com/\" },\n        },\n      },\n    },\n  },\n];\n\ndescribe(\"<Trailhead>\", () => {\n  let wrapper;\n  let dummyNode;\n  let dispatch;\n  let onAction;\n  let sandbox;\n  let onNextScene;\n\n  beforeEach(async () => {\n    sandbox = sinon.sandbox.create();\n    dispatch = sandbox.stub();\n    onAction = sandbox.stub();\n    onNextScene = sandbox.stub();\n    sandbox.stub(global, \"fetch\").resolves({\n      ok: true,\n      status: 200,\n      json: () => Promise.resolve({ flowId: 123, flowBeginTime: 456 }),\n    });\n\n    dummyNode = document.createElement(\"body\");\n    sandbox.stub(dummyNode, \"querySelector\").returns(dummyNode);\n    const fakeDocument = {\n      get activeElement() {\n        return dummyNode;\n      },\n      get body() {\n        return dummyNode;\n      },\n      getElementById() {\n        return dummyNode;\n      },\n    };\n\n    const message = (await OnboardingMessageProvider.getUntranslatedMessages()).find(\n      msg => msg.id === \"TRAILHEAD_1\"\n    );\n    message.cards = CARDS;\n    wrapper = mount(\n      <Trailhead\n        message={message}\n        UTMTerm={message.utm_term}\n        fxaEndpoint=\"https://accounts.firefox.com/endpoint\"\n        dispatch={dispatch}\n        onAction={onAction}\n        document={fakeDocument}\n        onNextScene={onNextScene}\n      />\n    );\n  });\n\n  afterEach(() => {\n    sandbox.restore();\n  });\n\n  it(\"should render FxASignupForm with signup email\", () => {\n    assert.lengthOf(wrapper.find(FxASignupForm), 1);\n  });\n\n  it(\"should emit UserEvent SKIPPED_SIGNIN and call nextScene when you click the start browsing button\", () => {\n    let skipButton = wrapper.find(\".trailheadStart\");\n    assert.ok(skipButton.exists());\n    skipButton.simulate(\"click\");\n\n    assert.calledOnce(onNextScene);\n\n    assert.calledOnce(dispatch);\n    assert.isUserEventAction(dispatch.firstCall.args[0]);\n    assert.calledWith(\n      dispatch,\n      ac.UserEvent({\n        event: at.SKIPPED_SIGNIN,\n        value: { has_flow_params: false },\n      })\n    );\n  });\n\n  it(\"should NOT emit UserEvent SKIPPED_SIGNIN when closeModal is triggered by visibilitychange event\", () => {\n    wrapper.instance().closeModal({ type: \"visibilitychange\" });\n    assert.notCalled(dispatch);\n  });\n\n  it(\"should prevent submissions with no email\", () => {\n    const form = wrapper.find(\"form\");\n    const preventDefault = sandbox.stub();\n\n    form.simulate(\"submit\", { preventDefault });\n\n    assert.calledOnce(preventDefault);\n    assert.notCalled(dispatch);\n  });\n  it(\"should emit UserEvent SUBMIT_EMAIL when you submit a valid email\", () => {\n    let form = wrapper.find(\"form\");\n    assert.ok(form.exists());\n    form.getDOMNode().elements.email.value = \"a@b.c\";\n\n    form.simulate(\"submit\");\n\n    assert.calledOnce(dispatch);\n    assert.isUserEventAction(dispatch.firstCall.args[0]);\n    assert.calledWith(\n      dispatch,\n      ac.UserEvent({\n        event: at.SUBMIT_EMAIL,\n        value: { has_flow_params: false },\n      })\n    );\n  });\n\n  it(\"should keep focus in dialog when blurring start button\", () => {\n    const skipButton = wrapper.find(\".trailheadStart\");\n    sandbox.stub(dummyNode, \"focus\");\n\n    skipButton.simulate(\"blur\", { relatedTarget: dummyNode });\n\n    assert.calledOnce(dummyNode.focus);\n  });\n});\n"
  },
  {
    "path": "test/unit/asrouter/templates/Triplets.test.jsx",
    "content": "import { mount } from \"enzyme\";\nimport { Triplets } from \"content-src/asrouter/templates/FirstRun/Triplets\";\nimport { OnboardingCard } from \"content-src/asrouter/templates/OnboardingMessage/OnboardingMessage\";\nimport React from \"react\";\n\nconst CARDS = [\n  {\n    id: \"CARD_1\",\n    content: {\n      title: { string_id: \"onboarding-private-browsing-title\" },\n      text: { string_id: \"onboarding-private-browsing-text\" },\n      icon: \"icon\",\n      primary_button: {\n        label: { string_id: \"onboarding-button-label-get-started\" },\n        action: {\n          type: \"OPEN_URL\",\n          data: { args: \"https://example.com/\" },\n        },\n      },\n    },\n  },\n];\n\ndescribe(\"<Triplets>\", () => {\n  let wrapper;\n  let sandbox;\n  let sendTelemetryStub;\n  let onAction;\n  let onHide;\n  let onBlockById;\n\n  async function setup() {\n    sandbox = sinon.createSandbox();\n    sendTelemetryStub = sandbox.stub();\n    onAction = sandbox.stub();\n    onBlockById = sandbox.stub();\n    onHide = sandbox.stub();\n\n    wrapper = mount(\n      <Triplets\n        cards={CARDS}\n        showCardPanel={true}\n        showContent={true}\n        hideContainer={onHide}\n        onAction={onAction}\n        UTMTerm=\"trailhead-join-card\"\n        onBlockById={onBlockById}\n        sendUserActionTelemetry={sendTelemetryStub}\n      />\n    );\n  }\n\n  beforeEach(setup);\n  afterEach(() => {\n    sandbox.restore();\n  });\n\n  it(\"should add an expanded class to container if props.showCardPanel is true\", () => {\n    wrapper.setProps({ showCardPanel: true });\n    assert.isTrue(\n      wrapper.find(\".trailheadCards\").hasClass(\"expanded\"),\n      \"has .expanded)\"\n    );\n  });\n  it(\"should add a collapsed class to container if props.showCardPanel is true\", () => {\n    wrapper.setProps({ showCardPanel: false });\n    assert.isFalse(\n      wrapper.find(\".trailheadCards\").hasClass(\"expanded\"),\n      \"has .expanded)\"\n    );\n  });\n  it(\"should send telemetry and call props.hideContainer when the dismiss button is clicked\", () => {\n    wrapper.find(\"button.icon-dismiss\").simulate(\"click\");\n    assert.calledOnce(onHide);\n    assert.calledWith(sendTelemetryStub, {\n      event: \"DISMISS\",\n      message_id: CARDS[0].id,\n      id: \"onboarding-cards\",\n      action: \"onboarding_user_event\",\n    });\n  });\n  it(\"should add utm_* query params to card actions and send the right ping when a card button is clicked\", () => {\n    wrapper\n      .find(OnboardingCard)\n      .find(\"button.onboardingButton\")\n      .simulate(\"click\");\n    assert.calledOnce(onAction);\n    const url = onAction.firstCall.args[0].data.args;\n    assert.equal(\n      url,\n      \"https://example.com/?utm_source=activity-stream&utm_campaign=firstrun&utm_medium=referral&utm_term=trailhead-join-card\"\n    );\n    assert.calledWith(sendTelemetryStub, {\n      event: \"CLICK_BUTTON\",\n      message_id: CARDS[0].id,\n      id: \"TRAILHEAD\",\n    });\n  });\n  it(\"should not call blockById by default when a card button is clicked\", () => {\n    wrapper\n      .find(OnboardingCard)\n      .find(\"button.onboardingButton\")\n      .simulate(\"click\");\n    assert.notCalled(onBlockById);\n  });\n  it(\"should call blockById when blockOnClick on message is true\", () => {\n    CARDS[0].blockOnClick = true;\n    wrapper\n      .find(OnboardingCard)\n      .find(\"button.onboardingButton\")\n      .simulate(\"click\");\n    assert.calledOnce(onBlockById);\n    assert.calledWith(onBlockById, CARDS[0].id);\n  });\n});\n"
  },
  {
    "path": "test/unit/asrouter/templates/isEmailOrPhoneNumber.test.js",
    "content": "import { isEmailOrPhoneNumber } from \"content-src/asrouter/templates/SendToDeviceSnippet/isEmailOrPhoneNumber\";\n\nconst CONTENT = {};\n\ndescribe(\"isEmailOrPhoneNumber\", () => {\n  it(\"should return 'email' for emails\", () => {\n    assert.equal(isEmailOrPhoneNumber(\"foobar@asd.com\", CONTENT), \"email\");\n    assert.equal(isEmailOrPhoneNumber(\"foobar@asd.co.uk\", CONTENT), \"email\");\n  });\n  it(\"should return 'phone' for valid en-US/en-CA phone numbers\", () => {\n    assert.equal(\n      isEmailOrPhoneNumber(\"14582731273\", { locale: \"en-US\" }),\n      \"phone\"\n    );\n    assert.equal(\n      isEmailOrPhoneNumber(\"4582731273\", { locale: \"en-CA\" }),\n      \"phone\"\n    );\n  });\n  it(\"should return an empty string for invalid phone number lengths in en-US/en-CA\", () => {\n    // Not enough digits\n    assert.equal(isEmailOrPhoneNumber(\"4522\", { locale: \"en-US\" }), \"\");\n    assert.equal(isEmailOrPhoneNumber(\"4522\", { locale: \"en-CA\" }), \"\");\n  });\n  it(\"should return 'phone' for valid German phone numbers\", () => {\n    assert.equal(\n      isEmailOrPhoneNumber(\"145827312732\", { locale: \"de\" }),\n      \"phone\"\n    );\n  });\n  it(\"should return 'phone' for any number of digits in other locales\", () => {\n    assert.equal(isEmailOrPhoneNumber(\"4\", CONTENT), \"phone\");\n  });\n  it(\"should return an empty string for other invalid inputs\", () => {\n    assert.equal(\n      isEmailOrPhoneNumber(\"abc\", CONTENT),\n      \"\",\n      \"abc should be invalid\"\n    );\n    assert.equal(\n      isEmailOrPhoneNumber(\"abc@\", CONTENT),\n      \"\",\n      \"abc@ should be invalid\"\n    );\n    assert.equal(\n      isEmailOrPhoneNumber(\"abc@foo\", CONTENT),\n      \"\",\n      \"abc@foo should be invalid\"\n    );\n    assert.equal(\n      isEmailOrPhoneNumber(\"123d1232\", CONTENT),\n      \"\",\n      \"123d1232 should be invalid\"\n    );\n  });\n});\n"
  },
  {
    "path": "test/unit/common/Actions.test.js",
    "content": "import {\n  actionCreators as ac,\n  actionTypes as at,\n  actionUtils as au,\n  BACKGROUND_PROCESS,\n  CONTENT_MESSAGE_TYPE,\n  globalImportContext,\n  MAIN_MESSAGE_TYPE,\n  PRELOAD_MESSAGE_TYPE,\n  UI_CODE,\n} from \"common/Actions.jsm\";\n\ndescribe(\"Actions\", () => {\n  it(\"should set globalImportContext to UI_CODE\", () => {\n    assert.equal(globalImportContext, UI_CODE);\n  });\n});\n\ndescribe(\"ActionTypes\", () => {\n  it(\"should be in alpha order\", () => {\n    assert.equal(\n      Object.keys(at).join(\", \"),\n      Object.keys(at)\n        .sort()\n        .join(\", \")\n    );\n  });\n});\n\ndescribe(\"ActionCreators\", () => {\n  describe(\"_RouteMessage\", () => {\n    it(\"should throw if options are not passed as the second param\", () => {\n      assert.throws(() => {\n        au._RouteMessage({ type: \"FOO\" });\n      });\n    });\n    it(\"should set all defined options on the .meta property of the new action\", () => {\n      assert.deepEqual(\n        au._RouteMessage(\n          { type: \"FOO\", meta: { hello: \"world\" } },\n          { from: \"foo\", to: \"bar\" }\n        ),\n        { type: \"FOO\", meta: { hello: \"world\", from: \"foo\", to: \"bar\" } }\n      );\n    });\n    it(\"should remove any undefined options related to message routing\", () => {\n      const action = au._RouteMessage(\n        { type: \"FOO\", meta: { fromTarget: \"bar\" } },\n        { from: \"foo\", to: \"bar\" }\n      );\n      assert.isUndefined(action.meta.fromTarget);\n    });\n  });\n  describe(\"AlsoToMain\", () => {\n    it(\"should create the right action\", () => {\n      const action = { type: \"FOO\", data: \"BAR\" };\n      const newAction = ac.AlsoToMain(action);\n      assert.deepEqual(newAction, {\n        type: \"FOO\",\n        data: \"BAR\",\n        meta: { from: CONTENT_MESSAGE_TYPE, to: MAIN_MESSAGE_TYPE },\n      });\n    });\n    it(\"should add the fromTarget if it was supplied\", () => {\n      const action = { type: \"FOO\", data: \"BAR\" };\n      const newAction = ac.AlsoToMain(action, \"port123\");\n      assert.equal(newAction.meta.fromTarget, \"port123\");\n    });\n    describe(\"isSendToMain\", () => {\n      it(\"should return true if action is AlsoToMain\", () => {\n        const newAction = ac.AlsoToMain({ type: \"FOO\" });\n        assert.isTrue(au.isSendToMain(newAction));\n      });\n      it(\"should return false if action is not AlsoToMain\", () => {\n        assert.isFalse(au.isSendToMain({ type: \"FOO\" }));\n      });\n    });\n  });\n  describe(\"AlsoToOneContent\", () => {\n    it(\"should create the right action\", () => {\n      const action = { type: \"FOO\", data: \"BAR\" };\n      const targetId = \"abc123\";\n      const newAction = ac.AlsoToOneContent(action, targetId);\n      assert.deepEqual(newAction, {\n        type: \"FOO\",\n        data: \"BAR\",\n        meta: {\n          from: MAIN_MESSAGE_TYPE,\n          to: CONTENT_MESSAGE_TYPE,\n          toTarget: targetId,\n        },\n      });\n    });\n    it(\"should throw if no targetId is provided\", () => {\n      assert.throws(() => {\n        ac.AlsoToOneContent({ type: \"FOO\" });\n      });\n    });\n    describe(\"isSendToOneContent\", () => {\n      it(\"should return true if action is AlsoToOneContent\", () => {\n        const newAction = ac.AlsoToOneContent({ type: \"FOO\" }, \"foo123\");\n        assert.isTrue(au.isSendToOneContent(newAction));\n      });\n      it(\"should return false if action is not AlsoToMain\", () => {\n        assert.isFalse(au.isSendToOneContent({ type: \"FOO\" }));\n        assert.isFalse(\n          au.isSendToOneContent(ac.BroadcastToContent({ type: \"FOO\" }))\n        );\n      });\n    });\n    describe(\"isFromMain\", () => {\n      it(\"should return true if action is AlsoToOneContent\", () => {\n        const newAction = ac.AlsoToOneContent({ type: \"FOO\" }, \"foo123\");\n        assert.isTrue(au.isFromMain(newAction));\n      });\n      it(\"should return true if action is BroadcastToContent\", () => {\n        const newAction = ac.BroadcastToContent({ type: \"FOO\" });\n        assert.isTrue(au.isFromMain(newAction));\n      });\n      it(\"should return false if action is AlsoToMain\", () => {\n        const newAction = ac.AlsoToMain({ type: \"FOO\" });\n        assert.isFalse(au.isFromMain(newAction));\n      });\n    });\n  });\n  describe(\"BroadcastToContent\", () => {\n    it(\"should create the right action\", () => {\n      const action = { type: \"FOO\", data: \"BAR\" };\n      const newAction = ac.BroadcastToContent(action);\n      assert.deepEqual(newAction, {\n        type: \"FOO\",\n        data: \"BAR\",\n        meta: { from: MAIN_MESSAGE_TYPE, to: CONTENT_MESSAGE_TYPE },\n      });\n    });\n    describe(\"isBroadcastToContent\", () => {\n      it(\"should return true if action is BroadcastToContent\", () => {\n        assert.isTrue(\n          au.isBroadcastToContent(ac.BroadcastToContent({ type: \"FOO\" }))\n        );\n      });\n      it(\"should return false if action is not BroadcastToContent\", () => {\n        assert.isFalse(au.isBroadcastToContent({ type: \"FOO\" }));\n        assert.isFalse(\n          au.isBroadcastToContent(\n            ac.AlsoToOneContent({ type: \"FOO\" }, \"foo123\")\n          )\n        );\n      });\n    });\n  });\n  describe(\"AlsoToPreloaded\", () => {\n    it(\"should create the right action\", () => {\n      const action = { type: \"FOO\", data: \"BAR\" };\n      const newAction = ac.AlsoToPreloaded(action);\n      assert.deepEqual(newAction, {\n        type: \"FOO\",\n        data: \"BAR\",\n        meta: { from: MAIN_MESSAGE_TYPE, to: PRELOAD_MESSAGE_TYPE },\n      });\n    });\n  });\n  describe(\"isSendToPreloaded\", () => {\n    it(\"should return true if action is AlsoToPreloaded\", () => {\n      assert.isTrue(au.isSendToPreloaded(ac.AlsoToPreloaded({ type: \"FOO\" })));\n    });\n    it(\"should return false if action is not AlsoToPreloaded\", () => {\n      assert.isFalse(au.isSendToPreloaded({ type: \"FOO\" }));\n      assert.isFalse(\n        au.isSendToPreloaded(ac.BroadcastToContent({ type: \"FOO\" }))\n      );\n    });\n  });\n  describe(\"UserEvent\", () => {\n    it(\"should include the given data\", () => {\n      const data = { action: \"foo\" };\n      assert.equal(ac.UserEvent(data).data, data);\n    });\n    it(\"should wrap with AlsoToMain\", () => {\n      const action = ac.UserEvent({ action: \"foo\" });\n      assert.isTrue(au.isSendToMain(action), \"isSendToMain\");\n    });\n  });\n  describe(\"ASRouterUserEvent\", () => {\n    it(\"should include the given data\", () => {\n      const data = { action: \"foo\" };\n      assert.equal(ac.ASRouterUserEvent(data).data, data);\n    });\n    it(\"should wrap with AlsoToMain\", () => {\n      const action = ac.ASRouterUserEvent({ action: \"foo\" });\n      assert.isTrue(au.isSendToMain(action), \"isSendToMain\");\n    });\n  });\n  describe(\"UndesiredEvent\", () => {\n    it(\"should include the given data\", () => {\n      const data = { action: \"foo\" };\n      assert.equal(ac.UndesiredEvent(data).data, data);\n    });\n    it(\"should wrap with AlsoToMain if in UI code\", () => {\n      assert.isTrue(\n        au.isSendToMain(ac.UndesiredEvent({ action: \"foo\" })),\n        \"isSendToMain\"\n      );\n    });\n    it(\"should not wrap with AlsoToMain if not in UI code\", () => {\n      const action = ac.UndesiredEvent({ action: \"foo\" }, BACKGROUND_PROCESS);\n      assert.isFalse(au.isSendToMain(action), \"isSendToMain\");\n    });\n  });\n  describe(\"PerfEvent\", () => {\n    it(\"should include the right data\", () => {\n      const data = { action: \"foo\" };\n      assert.equal(ac.UndesiredEvent(data).data, data);\n    });\n    it(\"should wrap with AlsoToMain if in UI code\", () => {\n      assert.isTrue(\n        au.isSendToMain(ac.PerfEvent({ action: \"foo\" })),\n        \"isSendToMain\"\n      );\n    });\n    it(\"should not wrap with AlsoToMain if not in UI code\", () => {\n      const action = ac.PerfEvent({ action: \"foo\" }, BACKGROUND_PROCESS);\n      assert.isFalse(au.isSendToMain(action), \"isSendToMain\");\n    });\n  });\n  describe(\"ImpressionStats\", () => {\n    it(\"should include the right data\", () => {\n      const data = { action: \"foo\" };\n      assert.equal(ac.ImpressionStats(data).data, data);\n    });\n    it(\"should wrap with AlsoToMain if in UI code\", () => {\n      assert.isTrue(\n        au.isSendToMain(ac.ImpressionStats({ action: \"foo\" })),\n        \"isSendToMain\"\n      );\n    });\n    it(\"should not wrap with AlsoToMain if not in UI code\", () => {\n      const action = ac.ImpressionStats({ action: \"foo\" }, BACKGROUND_PROCESS);\n      assert.isFalse(au.isSendToMain(action), \"isSendToMain\");\n    });\n  });\n  describe(\"WebExtEvent\", () => {\n    it(\"should set the provided type\", () => {\n      const action = ac.WebExtEvent(at.WEBEXT_CLICK, {\n        source: \"MyExtension\",\n        url: \"foo.com\",\n      });\n      assert.equal(action.type, at.WEBEXT_CLICK);\n    });\n    it(\"should set the provided data\", () => {\n      const data = { source: \"MyExtension\", url: \"foo.com\" };\n      const action = ac.WebExtEvent(at.WEBEXT_CLICK, data);\n      assert.equal(action.data, data);\n    });\n    it(\"should throw if the 'source' property is missing\", () => {\n      assert.throws(() => {\n        ac.WebExtEvent(at.WEBEXT_CLICK, {});\n      });\n    });\n  });\n});\n\ndescribe(\"ActionUtils\", () => {\n  describe(\"getPortIdOfSender\", () => {\n    it(\"should return the PortID from a AlsoToMain action\", () => {\n      const portID = \"foo123\";\n      const result = au.getPortIdOfSender(\n        ac.AlsoToMain({ type: \"FOO\" }, portID)\n      );\n      assert.equal(result, portID);\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/common/Dedupe.test.js",
    "content": "import { Dedupe } from \"common/Dedupe.jsm\";\n\ndescribe(\"Dedupe\", () => {\n  let instance;\n  beforeEach(() => {\n    instance = new Dedupe();\n  });\n  describe(\"group\", () => {\n    it(\"should remove duplicates inside the groups\", () => {\n      const beforeItems = [[1, 1, 1], [2, 2, 2], [3, 3, 3]];\n      const afterItems = [[1], [2], [3]];\n      assert.deepEqual(instance.group(...beforeItems), afterItems);\n    });\n    it(\"should remove duplicates between groups, favouring earlier groups\", () => {\n      const beforeItems = [[1, 2, 3], [2, 3, 4], [3, 4, 5]];\n      const afterItems = [[1, 2, 3], [4], [5]];\n      assert.deepEqual(instance.group(...beforeItems), afterItems);\n    });\n    it(\"should remove duplicates from groups of objects\", () => {\n      instance = new Dedupe(item => item.id);\n      const beforeItems = [\n        [{ id: 1 }, { id: 1 }, { id: 2 }],\n        [{ id: 1 }, { id: 3 }, { id: 2 }],\n        [{ id: 1 }, { id: 2 }, { id: 5 }],\n      ];\n      const afterItems = [[{ id: 1 }, { id: 2 }], [{ id: 3 }], [{ id: 5 }]];\n      assert.deepEqual(instance.group(...beforeItems), afterItems);\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/common/PerfService.test.js",
    "content": "/* globals assert, beforeEach, describe, it */\nimport { _PerfService } from \"common/PerfService.jsm\";\nimport { FakePerformance } from \"test/unit/utils.js\";\n\nlet perfService;\n\ndescribe(\"_PerfService\", () => {\n  let sandbox;\n  let fakePerfObj;\n\n  beforeEach(() => {\n    sandbox = sinon.createSandbox();\n    fakePerfObj = new FakePerformance();\n    perfService = new _PerfService({ performanceObj: fakePerfObj });\n  });\n\n  afterEach(() => {\n    sandbox.restore();\n  });\n\n  describe(\"#absNow\", () => {\n    it(\"should return a number > the time origin\", () => {\n      const absNow = perfService.absNow();\n\n      assert.isAbove(absNow, perfService.timeOrigin);\n    });\n  });\n  describe(\"#getEntriesByName\", () => {\n    it(\"should call getEntriesByName on the appropriate Window.performance\", () => {\n      sandbox.spy(fakePerfObj, \"getEntriesByName\");\n\n      perfService.getEntriesByName(\"monkey\", \"mark\");\n\n      assert.calledOnce(fakePerfObj.getEntriesByName);\n      assert.calledWithExactly(fakePerfObj.getEntriesByName, \"monkey\", \"mark\");\n    });\n\n    it(\"should return entries with the given name\", () => {\n      sandbox.spy(fakePerfObj, \"getEntriesByName\");\n      perfService.mark(\"monkey\");\n      perfService.mark(\"dog\");\n\n      let marks = perfService.getEntriesByName(\"monkey\", \"mark\");\n\n      assert.isArray(marks);\n      assert.lengthOf(marks, 1);\n      assert.propertyVal(marks[0], \"name\", \"monkey\");\n    });\n  });\n\n  describe(\"#getMostRecentAbsMarkStartByName\", () => {\n    it(\"should throw an error if there is no mark with the given name\", () => {\n      function bogusGet() {\n        perfService.getMostRecentAbsMarkStartByName(\"rheeeet\");\n      }\n\n      assert.throws(bogusGet, Error, /No marks with the name/);\n    });\n\n    it(\"should return the Number from the most recent mark with the given name + the time origin\", () => {\n      perfService.mark(\"dog\");\n      perfService.mark(\"dog\");\n\n      let absMarkStart = perfService.getMostRecentAbsMarkStartByName(\"dog\");\n\n      // 2 because we want the result of the 2nd call to mark, and an instance\n      // of FakePerformance just returns the number of time mark has been\n      // called.\n      assert.equal(absMarkStart - perfService.timeOrigin, 2);\n    });\n  });\n\n  describe(\"#mark\", () => {\n    it(\"should call the wrapped version of mark\", () => {\n      sandbox.spy(fakePerfObj, \"mark\");\n\n      perfService.mark(\"monkey\");\n\n      assert.calledOnce(fakePerfObj.mark);\n      assert.calledWithExactly(fakePerfObj.mark, \"monkey\");\n    });\n  });\n\n  describe(\"#timeOrigin\", () => {\n    it(\"should get the origin of the wrapped performance object\", () => {\n      assert.equal(perfService.timeOrigin, fakePerfObj.timeOrigin);\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/common/Reducers.test.js",
    "content": "import { INITIAL_STATE, insertPinned, reducers } from \"common/Reducers.jsm\";\nconst {\n  TopSites,\n  App,\n  Snippets,\n  Prefs,\n  Dialog,\n  Sections,\n  Pocket,\n  DiscoveryStream,\n  Search,\n  ASRouter,\n} = reducers;\nimport { actionTypes as at } from \"common/Actions.jsm\";\n\ndescribe(\"Reducers\", () => {\n  describe(\"App\", () => {\n    it(\"should return the initial state\", () => {\n      const nextState = App(undefined, { type: \"FOO\" });\n      assert.equal(nextState, INITIAL_STATE.App);\n    });\n    it(\"should set initialized to true on INIT\", () => {\n      const nextState = App(undefined, { type: \"INIT\" });\n\n      assert.propertyVal(nextState, \"initialized\", true);\n    });\n  });\n  describe(\"TopSites\", () => {\n    it(\"should return the initial state\", () => {\n      const nextState = TopSites(undefined, { type: \"FOO\" });\n      assert.equal(nextState, INITIAL_STATE.TopSites);\n    });\n    it(\"should add top sites on TOP_SITES_UPDATED\", () => {\n      const newRows = [{ url: \"foo.com\" }, { url: \"bar.com\" }];\n      const nextState = TopSites(undefined, {\n        type: at.TOP_SITES_UPDATED,\n        data: { links: newRows },\n      });\n      assert.equal(nextState.rows, newRows);\n    });\n    it(\"should not update state for empty action.data on TOP_SITES_UPDATED\", () => {\n      const nextState = TopSites(undefined, { type: at.TOP_SITES_UPDATED });\n      assert.equal(nextState, INITIAL_STATE.TopSites);\n    });\n    it(\"should initialize prefs on TOP_SITES_UPDATED\", () => {\n      const nextState = TopSites(undefined, {\n        type: at.TOP_SITES_UPDATED,\n        data: { links: [], pref: \"foo\" },\n      });\n\n      assert.equal(nextState.pref, \"foo\");\n    });\n    it(\"should pass prevState.prefs if not present in TOP_SITES_UPDATED\", () => {\n      const nextState = TopSites(\n        { prefs: \"foo\" },\n        { type: at.TOP_SITES_UPDATED, data: { links: [] } }\n      );\n\n      assert.equal(nextState.prefs, \"foo\");\n    });\n    it(\"should set editForm.site to action.data on TOP_SITES_EDIT\", () => {\n      const data = { index: 7 };\n      const nextState = TopSites(undefined, { type: at.TOP_SITES_EDIT, data });\n      assert.equal(nextState.editForm.index, data.index);\n    });\n    it(\"should set editForm to null on TOP_SITES_CANCEL_EDIT\", () => {\n      const nextState = TopSites(undefined, { type: at.TOP_SITES_CANCEL_EDIT });\n      assert.isNull(nextState.editForm);\n    });\n    it(\"should preserve the editForm.index\", () => {\n      const actionTypes = [\n        at.PREVIEW_RESPONSE,\n        at.PREVIEW_REQUEST,\n        at.PREVIEW_REQUEST_CANCEL,\n      ];\n      actionTypes.forEach(type => {\n        const oldState = { editForm: { index: 0, previewUrl: \"foo\" } };\n        const action = { type, data: { url: \"foo\" } };\n        const nextState = TopSites(oldState, action);\n        assert.equal(nextState.editForm.index, 0);\n      });\n    });\n    it(\"should set previewResponse on PREVIEW_RESPONSE\", () => {\n      const oldState = { editForm: { previewUrl: \"url\" } };\n      const action = {\n        type: at.PREVIEW_RESPONSE,\n        data: { preview: \"data:123\", url: \"url\" },\n      };\n      const nextState = TopSites(oldState, action);\n      assert.propertyVal(nextState.editForm, \"previewResponse\", \"data:123\");\n    });\n    it(\"should return previous state if action url does not match expected\", () => {\n      const oldState = { editForm: { previewUrl: \"foo\" } };\n      const action = { type: at.PREVIEW_RESPONSE, data: { url: \"bar\" } };\n      const nextState = TopSites(oldState, action);\n      assert.equal(nextState, oldState);\n    });\n    it(\"should return previous state if editForm is not set\", () => {\n      const actionTypes = [\n        at.PREVIEW_RESPONSE,\n        at.PREVIEW_REQUEST,\n        at.PREVIEW_REQUEST_CANCEL,\n      ];\n      actionTypes.forEach(type => {\n        const oldState = { editForm: null };\n        const action = { type, data: { url: \"bar\" } };\n        const nextState = TopSites(oldState, action);\n        assert.equal(nextState, oldState, type);\n      });\n    });\n    it(\"should set previewResponse to null on PREVIEW_REQUEST\", () => {\n      const oldState = { editForm: { previewResponse: \"foo\" } };\n      const action = { type: at.PREVIEW_REQUEST, data: {} };\n      const nextState = TopSites(oldState, action);\n      assert.propertyVal(nextState.editForm, \"previewResponse\", null);\n    });\n    it(\"should set previewUrl on PREVIEW_REQUEST\", () => {\n      const oldState = { editForm: {} };\n      const action = { type: at.PREVIEW_REQUEST, data: { url: \"bar\" } };\n      const nextState = TopSites(oldState, action);\n      assert.propertyVal(nextState.editForm, \"previewUrl\", \"bar\");\n    });\n    it(\"should add screenshots for SCREENSHOT_UPDATED\", () => {\n      const oldState = { rows: [{ url: \"foo.com\" }, { url: \"bar.com\" }] };\n      const action = {\n        type: at.SCREENSHOT_UPDATED,\n        data: { url: \"bar.com\", screenshot: \"data:123\" },\n      };\n      const nextState = TopSites(oldState, action);\n      assert.deepEqual(nextState.rows, [\n        { url: \"foo.com\" },\n        { url: \"bar.com\", screenshot: \"data:123\" },\n      ]);\n    });\n    it(\"should not modify rows if nothing matches the url for SCREENSHOT_UPDATED\", () => {\n      const oldState = { rows: [{ url: \"foo.com\" }, { url: \"bar.com\" }] };\n      const action = {\n        type: at.SCREENSHOT_UPDATED,\n        data: { url: \"baz.com\", screenshot: \"data:123\" },\n      };\n      const nextState = TopSites(oldState, action);\n      assert.deepEqual(nextState, oldState);\n    });\n    it(\"should bookmark an item on PLACES_BOOKMARK_ADDED\", () => {\n      const oldState = { rows: [{ url: \"foo.com\" }, { url: \"bar.com\" }] };\n      const action = {\n        type: at.PLACES_BOOKMARK_ADDED,\n        data: {\n          url: \"bar.com\",\n          bookmarkGuid: \"bookmark123\",\n          bookmarkTitle: \"Title for bar.com\",\n          dateAdded: 1234567,\n        },\n      };\n      const nextState = TopSites(oldState, action);\n      const [, newRow] = nextState.rows;\n      // new row has bookmark data\n      assert.equal(newRow.url, action.data.url);\n      assert.equal(newRow.bookmarkGuid, action.data.bookmarkGuid);\n      assert.equal(newRow.bookmarkTitle, action.data.bookmarkTitle);\n      assert.equal(newRow.bookmarkDateCreated, action.data.dateAdded);\n\n      // old row is unchanged\n      assert.equal(nextState.rows[0], oldState.rows[0]);\n    });\n    it(\"should not update state for empty action.data on PLACES_BOOKMARK_ADDED\", () => {\n      const nextState = TopSites(undefined, { type: at.PLACES_BOOKMARK_ADDED });\n      assert.equal(nextState, INITIAL_STATE.TopSites);\n    });\n    it(\"should remove a bookmark on PLACES_BOOKMARK_REMOVED\", () => {\n      const oldState = {\n        rows: [\n          { url: \"foo.com\" },\n          {\n            url: \"bar.com\",\n            bookmarkGuid: \"bookmark123\",\n            bookmarkTitle: \"Title for bar.com\",\n            dateAdded: 123456,\n          },\n        ],\n      };\n      const action = {\n        type: at.PLACES_BOOKMARK_REMOVED,\n        data: { url: \"bar.com\" },\n      };\n      const nextState = TopSites(oldState, action);\n      const [, newRow] = nextState.rows;\n      // new row no longer has bookmark data\n      assert.equal(newRow.url, oldState.rows[1].url);\n      assert.isUndefined(newRow.bookmarkGuid);\n      assert.isUndefined(newRow.bookmarkTitle);\n      assert.isUndefined(newRow.bookmarkDateCreated);\n\n      // old row is unchanged\n      assert.deepEqual(nextState.rows[0], oldState.rows[0]);\n    });\n    it(\"should not update state for empty action.data on PLACES_BOOKMARK_REMOVED\", () => {\n      const nextState = TopSites(undefined, {\n        type: at.PLACES_BOOKMARK_REMOVED,\n      });\n      assert.equal(nextState, INITIAL_STATE.TopSites);\n    });\n    it(\"should update prefs on TOP_SITES_PREFS_UPDATED\", () => {\n      const state = TopSites(\n        {},\n        { type: at.TOP_SITES_PREFS_UPDATED, data: { pref: \"foo\" } }\n      );\n\n      assert.equal(state.pref, \"foo\");\n    });\n    it(\"should not update state for empty action.data on PLACES_LINK_DELETED\", () => {\n      const nextState = TopSites(undefined, { type: at.PLACES_LINK_DELETED });\n      assert.equal(nextState, INITIAL_STATE.TopSites);\n    });\n    it(\"should remove the site on PLACES_LINK_DELETED\", () => {\n      const oldState = { rows: [{ url: \"foo.com\" }, { url: \"bar.com\" }] };\n      const deleteAction = {\n        type: at.PLACES_LINK_DELETED,\n        data: { url: \"foo.com\" },\n      };\n      const nextState = TopSites(oldState, deleteAction);\n      assert.deepEqual(nextState.rows, [{ url: \"bar.com\" }]);\n    });\n    it(\"should set showSearchShortcutsForm to true on TOP_SITES_OPEN_SEARCH_SHORTCUTS_MODAL\", () => {\n      const data = { index: 7 };\n      const nextState = TopSites(undefined, {\n        type: at.TOP_SITES_OPEN_SEARCH_SHORTCUTS_MODAL,\n        data,\n      });\n      assert.isTrue(nextState.showSearchShortcutsForm);\n    });\n    it(\"should set showSearchShortcutsForm to false on TOP_SITES_CLOSE_SEARCH_SHORTCUTS_MODAL\", () => {\n      const nextState = TopSites(undefined, {\n        type: at.TOP_SITES_CLOSE_SEARCH_SHORTCUTS_MODAL,\n      });\n      assert.isFalse(nextState.showSearchShortcutsForm);\n    });\n    it(\"should update searchShortcuts on UPDATE_SEARCH_SHORTCUTS\", () => {\n      const shortcuts = [\n        {\n          keyword: \"@google\",\n          shortURL: \"google\",\n          url: \"https://google.com\",\n          searchIdentifier: /^google/,\n        },\n        {\n          keyword: \"@baidu\",\n          shortURL: \"baidu\",\n          url: \"https://baidu.com\",\n          searchIdentifier: /^baidu/,\n        },\n      ];\n      const nextState = TopSites(undefined, {\n        type: at.UPDATE_SEARCH_SHORTCUTS,\n        data: { searchShortcuts: shortcuts },\n      });\n      assert.deepEqual(shortcuts, nextState.searchShortcuts);\n    });\n    it(\"should remove all content on SNIPPETS_PREVIEW_MODE\", () => {\n      const oldState = { rows: [{ url: \"foo.com\" }, { url: \"bar.com\" }] };\n      const nextState = TopSites(oldState, { type: at.SNIPPETS_PREVIEW_MODE });\n      assert.lengthOf(nextState.rows, 0);\n    });\n  });\n  describe(\"Prefs\", () => {\n    function prevState(custom = {}) {\n      return Object.assign({}, INITIAL_STATE.Prefs, custom);\n    }\n    it(\"should have the correct initial state\", () => {\n      const state = Prefs(undefined, {});\n      assert.deepEqual(state, INITIAL_STATE.Prefs);\n    });\n    describe(\"PREFS_INITIAL_VALUES\", () => {\n      it(\"should return a new object\", () => {\n        const state = Prefs(undefined, {\n          type: at.PREFS_INITIAL_VALUES,\n          data: {},\n        });\n        assert.notEqual(\n          INITIAL_STATE.Prefs,\n          state,\n          \"should not modify INITIAL_STATE\"\n        );\n      });\n      it(\"should set initalized to true\", () => {\n        const state = Prefs(undefined, {\n          type: at.PREFS_INITIAL_VALUES,\n          data: {},\n        });\n        assert.isTrue(state.initialized);\n      });\n      it(\"should set .values\", () => {\n        const newValues = { foo: 1, bar: 2 };\n        const state = Prefs(undefined, {\n          type: at.PREFS_INITIAL_VALUES,\n          data: newValues,\n        });\n        assert.equal(state.values, newValues);\n      });\n    });\n    describe(\"PREF_CHANGED\", () => {\n      it(\"should return a new Prefs object\", () => {\n        const state = Prefs(undefined, {\n          type: at.PREF_CHANGED,\n          data: { name: \"foo\", value: 2 },\n        });\n        assert.notEqual(\n          INITIAL_STATE.Prefs,\n          state,\n          \"should not modify INITIAL_STATE\"\n        );\n      });\n      it(\"should set the changed pref\", () => {\n        const state = Prefs(prevState({ foo: 1 }), {\n          type: at.PREF_CHANGED,\n          data: { name: \"foo\", value: 2 },\n        });\n        assert.equal(state.values.foo, 2);\n      });\n      it(\"should return a new .pref object instead of mutating\", () => {\n        const oldState = prevState({ foo: 1 });\n        const state = Prefs(oldState, {\n          type: at.PREF_CHANGED,\n          data: { name: \"foo\", value: 2 },\n        });\n        assert.notEqual(oldState.values, state.values);\n      });\n    });\n  });\n  describe(\"Dialog\", () => {\n    it(\"should return INITIAL_STATE by default\", () => {\n      assert.equal(\n        INITIAL_STATE.Dialog,\n        Dialog(undefined, { type: \"non_existent\" })\n      );\n    });\n    it(\"should toggle visible to true on DIALOG_OPEN\", () => {\n      const action = { type: at.DIALOG_OPEN };\n      const nextState = Dialog(INITIAL_STATE.Dialog, action);\n      assert.isTrue(nextState.visible);\n    });\n    it(\"should pass url data on DIALOG_OPEN\", () => {\n      const action = { type: at.DIALOG_OPEN, data: \"some url\" };\n      const nextState = Dialog(INITIAL_STATE.Dialog, action);\n      assert.equal(nextState.data, action.data);\n    });\n    it(\"should toggle visible to false on DIALOG_CANCEL\", () => {\n      const action = { type: at.DIALOG_CANCEL, data: \"some url\" };\n      const nextState = Dialog(INITIAL_STATE.Dialog, action);\n      assert.isFalse(nextState.visible);\n    });\n    it(\"should return inital state on DELETE_HISTORY_URL\", () => {\n      const action = { type: at.DELETE_HISTORY_URL };\n      const nextState = Dialog(INITIAL_STATE.Dialog, action);\n\n      assert.deepEqual(INITIAL_STATE.Dialog, nextState);\n    });\n  });\n  describe(\"Sections\", () => {\n    let oldState;\n\n    beforeEach(() => {\n      oldState = new Array(5).fill(null).map((v, i) => ({\n        id: `foo_bar_${i}`,\n        title: `Foo Bar ${i}`,\n        initialized: false,\n        rows: [\n          { url: \"www.foo.bar\", pocket_id: 123 },\n          { url: \"www.other.url\" },\n        ],\n        order: i,\n        type: \"history\",\n      }));\n    });\n\n    it(\"should return INITIAL_STATE by default\", () => {\n      assert.equal(\n        INITIAL_STATE.Sections,\n        Sections(undefined, { type: \"non_existent\" })\n      );\n    });\n    it(\"should remove the correct section on SECTION_DEREGISTER\", () => {\n      const newState = Sections(oldState, {\n        type: at.SECTION_DEREGISTER,\n        data: \"foo_bar_2\",\n      });\n      assert.lengthOf(newState, 4);\n      const expectedNewState = oldState.splice(2, 1) && oldState;\n      assert.deepEqual(newState, expectedNewState);\n    });\n    it(\"should add a section on SECTION_REGISTER if it doesn't already exist\", () => {\n      const action = {\n        type: at.SECTION_REGISTER,\n        data: { id: \"foo_bar_5\", title: \"Foo Bar 5\" },\n      };\n      const newState = Sections(oldState, action);\n      assert.lengthOf(newState, 6);\n      const insertedSection = newState.find(\n        section => section.id === \"foo_bar_5\"\n      );\n      assert.propertyVal(insertedSection, \"title\", action.data.title);\n    });\n    it(\"should set newSection.rows === [] if no rows are provided on SECTION_REGISTER\", () => {\n      const action = {\n        type: at.SECTION_REGISTER,\n        data: { id: \"foo_bar_5\", title: \"Foo Bar 5\" },\n      };\n      const newState = Sections(oldState, action);\n      const insertedSection = newState.find(\n        section => section.id === \"foo_bar_5\"\n      );\n      assert.deepEqual(insertedSection.rows, []);\n    });\n    it(\"should update a section on SECTION_REGISTER if it already exists\", () => {\n      const NEW_TITLE = \"New Title\";\n      const action = {\n        type: at.SECTION_REGISTER,\n        data: { id: \"foo_bar_2\", title: NEW_TITLE },\n      };\n      const newState = Sections(oldState, action);\n      assert.lengthOf(newState, 5);\n      const updatedSection = newState.find(\n        section => section.id === \"foo_bar_2\"\n      );\n      assert.ok(updatedSection && updatedSection.title === NEW_TITLE);\n    });\n    it(\"should set initialized to false on SECTION_REGISTER if there are no rows\", () => {\n      const NEW_TITLE = \"New Title\";\n      const action = {\n        type: at.SECTION_REGISTER,\n        data: { id: \"bloop\", title: NEW_TITLE },\n      };\n      const newState = Sections(oldState, action);\n      const updatedSection = newState.find(section => section.id === \"bloop\");\n      assert.propertyVal(updatedSection, \"initialized\", false);\n    });\n    it(\"should set initialized to true on SECTION_REGISTER if there are rows\", () => {\n      const NEW_TITLE = \"New Title\";\n      const action = {\n        type: at.SECTION_REGISTER,\n        data: { id: \"bloop\", title: NEW_TITLE, rows: [{}, {}] },\n      };\n      const newState = Sections(oldState, action);\n      const updatedSection = newState.find(section => section.id === \"bloop\");\n      assert.propertyVal(updatedSection, \"initialized\", true);\n    });\n    it(\"should have no effect on SECTION_UPDATE if the id doesn't exist\", () => {\n      const action = {\n        type: at.SECTION_UPDATE,\n        data: { id: \"fake_id\", data: \"fake_data\" },\n      };\n      const newState = Sections(oldState, action);\n      assert.deepEqual(oldState, newState);\n    });\n    it(\"should update the section with the correct data on SECTION_UPDATE\", () => {\n      const FAKE_DATA = { rows: [\"some\", \"fake\", \"data\"], foo: \"bar\" };\n      const action = {\n        type: at.SECTION_UPDATE,\n        data: Object.assign(FAKE_DATA, { id: \"foo_bar_2\" }),\n      };\n      const newState = Sections(oldState, action);\n      const updatedSection = newState.find(\n        section => section.id === \"foo_bar_2\"\n      );\n      assert.include(updatedSection, FAKE_DATA);\n    });\n    it(\"should set initialized to true on SECTION_UPDATE if rows is defined on action.data\", () => {\n      const data = { rows: [], id: \"foo_bar_2\" };\n      const action = { type: at.SECTION_UPDATE, data };\n      const newState = Sections(oldState, action);\n      const updatedSection = newState.find(\n        section => section.id === \"foo_bar_2\"\n      );\n      assert.propertyVal(updatedSection, \"initialized\", true);\n    });\n    it(\"should retain pinned cards on SECTION_UPDATE\", () => {\n      const ROW = { id: \"row\" };\n      let newState = Sections(oldState, {\n        type: at.SECTION_UPDATE,\n        data: Object.assign({ rows: [ROW] }, { id: \"foo_bar_2\" }),\n      });\n      let updatedSection = newState.find(section => section.id === \"foo_bar_2\");\n      assert.deepEqual(updatedSection.rows, [ROW]);\n\n      const PINNED_ROW = { id: \"pinned\", pinned: true, guid: \"pinned\" };\n      newState = Sections(newState, {\n        type: at.SECTION_UPDATE,\n        data: Object.assign({ rows: [PINNED_ROW] }, { id: \"foo_bar_2\" }),\n      });\n      updatedSection = newState.find(section => section.id === \"foo_bar_2\");\n      assert.deepEqual(updatedSection.rows, [PINNED_ROW]);\n\n      // Updating the section again should not duplicate pinned cards\n      newState = Sections(newState, {\n        type: at.SECTION_UPDATE,\n        data: Object.assign({ rows: [PINNED_ROW] }, { id: \"foo_bar_2\" }),\n      });\n      updatedSection = newState.find(section => section.id === \"foo_bar_2\");\n      assert.deepEqual(updatedSection.rows, [PINNED_ROW]);\n\n      // Updating the section should retain pinned card at its index\n      newState = Sections(newState, {\n        type: at.SECTION_UPDATE,\n        data: Object.assign({ rows: [ROW] }, { id: \"foo_bar_2\" }),\n      });\n      updatedSection = newState.find(section => section.id === \"foo_bar_2\");\n      assert.deepEqual(updatedSection.rows, [PINNED_ROW, ROW]);\n\n      // Clearing/Resetting the section should clear pinned cards\n      newState = Sections(newState, {\n        type: at.SECTION_UPDATE,\n        data: Object.assign({ rows: [] }, { id: \"foo_bar_2\" }),\n      });\n      updatedSection = newState.find(section => section.id === \"foo_bar_2\");\n      assert.deepEqual(updatedSection.rows, []);\n    });\n    it(\"should have no effect on SECTION_UPDATE_CARD if the id or url doesn't exist\", () => {\n      const noIdAction = {\n        type: at.SECTION_UPDATE_CARD,\n        data: {\n          id: \"non-existent\",\n          url: \"www.foo.bar\",\n          options: { title: \"New title\" },\n        },\n      };\n      const noIdState = Sections(oldState, noIdAction);\n      const noUrlAction = {\n        type: at.SECTION_UPDATE_CARD,\n        data: {\n          id: \"foo_bar_2\",\n          url: \"www.non-existent.url\",\n          options: { title: \"New title\" },\n        },\n      };\n      const noUrlState = Sections(oldState, noUrlAction);\n      assert.deepEqual(noIdState, oldState);\n      assert.deepEqual(noUrlState, oldState);\n    });\n    it(\"should update the card with the correct data on SECTION_UPDATE_CARD\", () => {\n      const action = {\n        type: at.SECTION_UPDATE_CARD,\n        data: {\n          id: \"foo_bar_2\",\n          url: \"www.other.url\",\n          options: { title: \"Fake new title\" },\n        },\n      };\n      const newState = Sections(oldState, action);\n      const updatedSection = newState.find(\n        section => section.id === \"foo_bar_2\"\n      );\n      const updatedCard = updatedSection.rows.find(\n        card => card.url === \"www.other.url\"\n      );\n      assert.propertyVal(updatedCard, \"title\", \"Fake new title\");\n    });\n    it(\"should only update the cards belonging to the right section on SECTION_UPDATE_CARD\", () => {\n      const action = {\n        type: at.SECTION_UPDATE_CARD,\n        data: {\n          id: \"foo_bar_2\",\n          url: \"www.other.url\",\n          options: { title: \"Fake new title\" },\n        },\n      };\n      const newState = Sections(oldState, action);\n      newState.forEach((section, i) => {\n        if (section.id !== \"foo_bar_2\") {\n          assert.deepEqual(section, oldState[i]);\n        }\n      });\n    });\n    it(\"should allow action.data to set .initialized\", () => {\n      const data = { rows: [], initialized: false, id: \"foo_bar_2\" };\n      const action = { type: at.SECTION_UPDATE, data };\n      const newState = Sections(oldState, action);\n      const updatedSection = newState.find(\n        section => section.id === \"foo_bar_2\"\n      );\n      assert.propertyVal(updatedSection, \"initialized\", false);\n    });\n    it(\"should dedupe based on dedupeConfigurations\", () => {\n      const site = { url: \"foo.com\" };\n      const highlights = { rows: [site], id: \"highlights\" };\n      const topstories = { rows: [site], id: \"topstories\" };\n      const dedupeConfigurations = [\n        { id: \"topstories\", dedupeFrom: [\"highlights\"] },\n      ];\n      const action = { data: { dedupeConfigurations }, type: \"SECTION_UPDATE\" };\n      const state = [highlights, topstories];\n\n      const nextState = Sections(state, action);\n\n      assert.equal(nextState.find(s => s.id === \"highlights\").rows.length, 1);\n      assert.equal(nextState.find(s => s.id === \"topstories\").rows.length, 0);\n    });\n    it(\"should remove blocked and deleted urls from all rows in all sections\", () => {\n      const blockAction = {\n        type: at.PLACES_LINK_BLOCKED,\n        data: { url: \"www.foo.bar\" },\n      };\n      const deleteAction = {\n        type: at.PLACES_LINK_DELETED,\n        data: { url: \"www.foo.bar\" },\n      };\n      const newBlockState = Sections(oldState, blockAction);\n      const newDeleteState = Sections(oldState, deleteAction);\n      newBlockState.concat(newDeleteState).forEach(section => {\n        assert.deepEqual(section.rows, [{ url: \"www.other.url\" }]);\n      });\n    });\n    it(\"should not update state for empty action.data on PLACES_LINK_DELETED\", () => {\n      const nextState = Sections(undefined, { type: at.PLACES_LINK_DELETED });\n      assert.equal(nextState, INITIAL_STATE.Sections);\n    });\n    it(\"should remove all removed pocket urls\", () => {\n      const removeAction = {\n        type: at.DELETE_FROM_POCKET,\n        data: { pocket_id: 123 },\n      };\n      const newBlockState = Sections(oldState, removeAction);\n      newBlockState.forEach(section => {\n        assert.deepEqual(section.rows, [{ url: \"www.other.url\" }]);\n      });\n    });\n    it(\"should archive all archived pocket urls\", () => {\n      const removeAction = {\n        type: at.ARCHIVE_FROM_POCKET,\n        data: { pocket_id: 123 },\n      };\n      const newBlockState = Sections(oldState, removeAction);\n      newBlockState.forEach(section => {\n        assert.deepEqual(section.rows, [{ url: \"www.other.url\" }]);\n      });\n    });\n    it(\"should not update state for empty action.data on PLACES_BOOKMARK_ADDED\", () => {\n      const nextState = Sections(undefined, { type: at.PLACES_BOOKMARK_ADDED });\n      assert.equal(nextState, INITIAL_STATE.Sections);\n    });\n    it(\"should bookmark an item when PLACES_BOOKMARK_ADDED is received\", () => {\n      const action = {\n        type: at.PLACES_BOOKMARK_ADDED,\n        data: {\n          url: \"www.foo.bar\",\n          bookmarkGuid: \"bookmark123\",\n          bookmarkTitle: \"Title for bar.com\",\n          dateAdded: 1234567,\n        },\n      };\n      const nextState = Sections(oldState, action);\n      // check a section to ensure the correct url was bookmarked\n      const [newRow, oldRow] = nextState[0].rows;\n\n      // new row has bookmark data\n      assert.equal(newRow.url, action.data.url);\n      assert.equal(newRow.type, \"bookmark\");\n      assert.equal(newRow.bookmarkGuid, action.data.bookmarkGuid);\n      assert.equal(newRow.bookmarkTitle, action.data.bookmarkTitle);\n      assert.equal(newRow.bookmarkDateCreated, action.data.dateAdded);\n\n      // old row is unchanged\n      assert.equal(oldRow, oldState[0].rows[1]);\n    });\n    it(\"should not update state for empty action.data on PLACES_BOOKMARK_REMOVED\", () => {\n      const nextState = Sections(undefined, {\n        type: at.PLACES_BOOKMARK_REMOVED,\n      });\n      assert.equal(nextState, INITIAL_STATE.Sections);\n    });\n    it(\"should remove the bookmark when PLACES_BOOKMARK_REMOVED is received\", () => {\n      const action = {\n        type: at.PLACES_BOOKMARK_REMOVED,\n        data: {\n          url: \"www.foo.bar\",\n          bookmarkGuid: \"bookmark123\",\n        },\n      };\n      // add some bookmark data for the first url in rows\n      oldState.forEach(item => {\n        item.rows[0].bookmarkGuid = \"bookmark123\";\n        item.rows[0].bookmarkTitle = \"Title for bar.com\";\n        item.rows[0].bookmarkDateCreated = 1234567;\n        item.rows[0].type = \"bookmark\";\n      });\n      const nextState = Sections(oldState, action);\n      // check a section to ensure the correct bookmark was removed\n      const [newRow, oldRow] = nextState[0].rows;\n\n      // new row isn't a bookmark\n      assert.equal(newRow.url, action.data.url);\n      assert.equal(newRow.type, \"history\");\n      assert.isUndefined(newRow.bookmarkGuid);\n      assert.isUndefined(newRow.bookmarkTitle);\n      assert.isUndefined(newRow.bookmarkDateCreated);\n\n      // old row is unchanged\n      assert.equal(oldRow, oldState[0].rows[1]);\n    });\n    it(\"should not update state for empty action.data on PLACES_SAVED_TO_POCKET\", () => {\n      const nextState = Sections(undefined, {\n        type: at.PLACES_SAVED_TO_POCKET,\n      });\n      assert.equal(nextState, INITIAL_STATE.Sections);\n    });\n    it(\"should add a pocked item on PLACES_SAVED_TO_POCKET\", () => {\n      const action = {\n        type: at.PLACES_SAVED_TO_POCKET,\n        data: {\n          url: \"www.foo.bar\",\n          pocket_id: 1234,\n          title: \"Title for bar.com\",\n        },\n      };\n      const nextState = Sections(oldState, action);\n      // check a section to ensure the correct url was saved to pocket\n      const [newRow, oldRow] = nextState[0].rows;\n\n      // new row has pocket data\n      assert.equal(newRow.url, action.data.url);\n      assert.equal(newRow.type, \"pocket\");\n      assert.equal(newRow.pocket_id, action.data.pocket_id);\n      assert.equal(newRow.title, action.data.title);\n\n      // old row is unchanged\n      assert.equal(oldRow, oldState[0].rows[1]);\n    });\n    it(\"should remove all content on SNIPPETS_PREVIEW_MODE\", () => {\n      const previewMode = { type: at.SNIPPETS_PREVIEW_MODE };\n      const newState = Sections(oldState, previewMode);\n      newState.forEach(section => {\n        assert.lengthOf(section.rows, 0);\n      });\n    });\n  });\n  describe(\"#insertPinned\", () => {\n    let links;\n\n    beforeEach(() => {\n      links = new Array(12).fill(null).map((v, i) => ({ url: `site${i}.com` }));\n    });\n\n    it(\"should place pinned links where they belong\", () => {\n      const pinned = [\n        { url: \"http://github.com/mozilla/activity-stream\", title: \"moz/a-s\" },\n        { url: \"http://example.com\", title: \"example\" },\n      ];\n      const result = insertPinned(links, pinned);\n      for (let index of [0, 1]) {\n        assert.equal(result[index].url, pinned[index].url);\n        assert.ok(result[index].isPinned);\n        assert.equal(result[index].pinIndex, index);\n      }\n      assert.deepEqual(result.slice(2), links);\n    });\n    it(\"should handle empty slots in the pinned list\", () => {\n      const pinned = [\n        null,\n        { url: \"http://github.com/mozilla/activity-stream\", title: \"moz/a-s\" },\n        null,\n        null,\n        { url: \"http://example.com\", title: \"example\" },\n      ];\n      const result = insertPinned(links, pinned);\n      for (let index of [1, 4]) {\n        assert.equal(result[index].url, pinned[index].url);\n        assert.ok(result[index].isPinned);\n        assert.equal(result[index].pinIndex, index);\n      }\n      result.splice(4, 1);\n      result.splice(1, 1);\n      assert.deepEqual(result, links);\n    });\n    it(\"should handle a pinned site past the end of the list of links\", () => {\n      const pinned = [];\n      pinned[11] = {\n        url: \"http://github.com/mozilla/activity-stream\",\n        title: \"moz/a-s\",\n      };\n      const result = insertPinned([], pinned);\n      assert.equal(result[11].url, pinned[11].url);\n      assert.isTrue(result[11].isPinned);\n      assert.equal(result[11].pinIndex, 11);\n    });\n    it(\"should unpin previously pinned links no longer in the pinned list\", () => {\n      const pinned = [];\n      links[2].isPinned = true;\n      links[2].pinIndex = 2;\n      const result = insertPinned(links, pinned);\n      assert.notProperty(result[2], \"isPinned\");\n      assert.notProperty(result[2], \"pinIndex\");\n    });\n    it(\"should handle a link present in both the links and pinned list\", () => {\n      const pinned = [links[7]];\n      const result = insertPinned(links, pinned);\n      assert.equal(links.length, result.length);\n    });\n    it(\"should not modify the original data\", () => {\n      const pinned = [{ url: \"http://example.com\" }];\n\n      insertPinned(links, pinned);\n\n      assert.equal(typeof pinned[0].isPinned, \"undefined\");\n    });\n  });\n  describe(\"Snippets\", () => {\n    it(\"should return INITIAL_STATE by default\", () => {\n      assert.equal(\n        Snippets(undefined, { type: \"some_action\" }),\n        INITIAL_STATE.Snippets\n      );\n    });\n    it(\"should set initialized to true on a SNIPPETS_DATA action\", () => {\n      const state = Snippets(undefined, { type: at.SNIPPETS_DATA, data: {} });\n      assert.isTrue(state.initialized);\n    });\n    it(\"should set the snippet data on a SNIPPETS_DATA action\", () => {\n      const data = { snippetsURL: \"foo.com\", version: 4 };\n      const state = Snippets(undefined, { type: at.SNIPPETS_DATA, data });\n      assert.propertyVal(state, \"snippetsURL\", data.snippetsURL);\n      assert.propertyVal(state, \"version\", data.version);\n    });\n    it(\"should reset to the initial state on a SNIPPETS_RESET action\", () => {\n      const state = Snippets(\n        { initialized: true, foo: \"bar\" },\n        { type: at.SNIPPETS_RESET }\n      );\n      assert.equal(state, INITIAL_STATE.Snippets);\n    });\n    it(\"should set the new blocklist on SNIPPET_BLOCKED\", () => {\n      const state = Snippets(\n        { blockList: [] },\n        { type: at.SNIPPET_BLOCKED, data: 1 }\n      );\n      assert.deepEqual(state.blockList, [1]);\n    });\n    it(\"should clear the blocklist on SNIPPETS_BLOCKLIST_CLEARED\", () => {\n      const state = Snippets(\n        { blockList: [1, 2] },\n        { type: at.SNIPPETS_BLOCKLIST_CLEARED }\n      );\n      assert.deepEqual(state.blockList, []);\n    });\n  });\n  describe(\"Pocket\", () => {\n    it(\"should return INITIAL_STATE by default\", () => {\n      assert.equal(\n        Pocket(undefined, { type: \"some_action\" }),\n        INITIAL_STATE.Pocket\n      );\n    });\n    it(\"should set waitingForSpoc on a POCKET_WAITING_FOR_SPOC action\", () => {\n      const state = Pocket(undefined, {\n        type: at.POCKET_WAITING_FOR_SPOC,\n        data: false,\n      });\n      assert.isFalse(state.waitingForSpoc);\n    });\n    it(\"should have undefined for initial isUserLoggedIn state\", () => {\n      assert.isNull(Pocket(undefined, { type: \"some_action\" }).isUserLoggedIn);\n    });\n    it(\"should set isUserLoggedIn to false on a POCKET_LOGGED_IN with null\", () => {\n      const state = Pocket(undefined, {\n        type: at.POCKET_LOGGED_IN,\n        data: null,\n      });\n      assert.isFalse(state.isUserLoggedIn);\n    });\n    it(\"should set isUserLoggedIn to false on a POCKET_LOGGED_IN with false\", () => {\n      const state = Pocket(undefined, {\n        type: at.POCKET_LOGGED_IN,\n        data: false,\n      });\n      assert.isFalse(state.isUserLoggedIn);\n    });\n    it(\"should set isUserLoggedIn to true on a POCKET_LOGGED_IN with true\", () => {\n      const state = Pocket(undefined, {\n        type: at.POCKET_LOGGED_IN,\n        data: true,\n      });\n      assert.isTrue(state.isUserLoggedIn);\n    });\n    it(\"should set pocketCta with correct object on a POCKET_CTA\", () => {\n      const data = {\n        cta_button: \"cta button\",\n        cta_text: \"cta text\",\n        cta_url: \"https://cta-url.com\",\n        use_cta: true,\n      };\n      const state = Pocket(undefined, { type: at.POCKET_CTA, data });\n      assert.equal(state.pocketCta.ctaButton, data.cta_button);\n      assert.equal(state.pocketCta.ctaText, data.cta_text);\n      assert.equal(state.pocketCta.ctaUrl, data.cta_url);\n      assert.equal(state.pocketCta.useCta, data.use_cta);\n    });\n  });\n  describe(\"DiscoveryStream\", () => {\n    it(\"should return INITIAL_STATE by default\", () => {\n      assert.equal(\n        DiscoveryStream(undefined, { type: \"some_action\" }),\n        INITIAL_STATE.DiscoveryStream\n      );\n    });\n    it(\"should set isPrivacyInfoModalVisible to true with SHOW_PRIVACY_INFO\", () => {\n      const state = DiscoveryStream(undefined, {\n        type: at.SHOW_PRIVACY_INFO,\n      });\n      assert.equal(state.isPrivacyInfoModalVisible, true);\n    });\n    it(\"should set isPrivacyInfoModalVisible to false with HIDE_PRIVACY_INFO\", () => {\n      const state = DiscoveryStream(undefined, {\n        type: at.HIDE_PRIVACY_INFO,\n      });\n      assert.equal(state.isPrivacyInfoModalVisible, false);\n    });\n    it(\"should set layout data with DISCOVERY_STREAM_LAYOUT_UPDATE\", () => {\n      const state = DiscoveryStream(undefined, {\n        type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,\n        data: { layout: [\"test\"], lastUpdated: 123 },\n      });\n      assert.equal(state.layout[0], \"test\");\n      assert.equal(state.lastUpdated, 123);\n    });\n    it(\"should reset layout data with DISCOVERY_STREAM_LAYOUT_RESET\", () => {\n      const layoutData = { layout: [\"test\"], lastUpdated: 123 };\n      const feedsData = {\n        \"https://foo.com/feed1\": { lastUpdated: 123, data: [1, 2, 3] },\n      };\n      const spocsData = {\n        lastUpdated: 123,\n        spocs: [1, 2, 3],\n      };\n      let state = DiscoveryStream(undefined, {\n        type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,\n        data: layoutData,\n      });\n      state = DiscoveryStream(state, {\n        type: at.DISCOVERY_STREAM_FEEDS_UPDATE,\n        data: feedsData,\n      });\n      state = DiscoveryStream(state, {\n        type: at.DISCOVERY_STREAM_SPOCS_UPDATE,\n        data: spocsData,\n      });\n      state = DiscoveryStream(state, {\n        type: at.DISCOVERY_STREAM_LAYOUT_RESET,\n      });\n\n      assert.deepEqual(state, INITIAL_STATE.DiscoveryStream);\n    });\n    it(\"should set config data with DISCOVERY_STREAM_CONFIG_CHANGE\", () => {\n      const state = DiscoveryStream(undefined, {\n        type: at.DISCOVERY_STREAM_CONFIG_CHANGE,\n        data: { enabled: true },\n      });\n      assert.deepEqual(state.config, { enabled: true });\n    });\n    it(\"should set feeds as loaded with DISCOVERY_STREAM_FEEDS_UPDATE\", () => {\n      const state = DiscoveryStream(undefined, {\n        type: at.DISCOVERY_STREAM_FEEDS_UPDATE,\n      });\n      assert.isTrue(state.feeds.loaded);\n    });\n    it(\"should set spoc_endpoint and spocs_per_domain with DISCOVERY_STREAM_SPOCS_ENDPOINT\", () => {\n      const state = DiscoveryStream(undefined, {\n        type: at.DISCOVERY_STREAM_SPOCS_ENDPOINT,\n        data: { url: \"foo.com\", spocs_per_domain: 2 },\n      });\n      assert.equal(state.spocs.spocs_endpoint, \"foo.com\");\n      assert.equal(state.spocs.spocs_per_domain, 2);\n    });\n    it(\"should use initial state with DISCOVERY_STREAM_SPOCS_PLACEMENTS\", () => {\n      const state = DiscoveryStream(undefined, {\n        type: at.DISCOVERY_STREAM_SPOCS_PLACEMENTS,\n        data: {},\n      });\n      assert.deepEqual(state.spocs.placements, []);\n    });\n    it(\"should set placements with DISCOVERY_STREAM_SPOCS_PLACEMENTS\", () => {\n      const state = DiscoveryStream(undefined, {\n        type: at.DISCOVERY_STREAM_SPOCS_PLACEMENTS,\n        data: {\n          placements: [1, 2, 3],\n        },\n      });\n      assert.deepEqual(state.spocs.placements, [1, 2, 3]);\n    });\n    it(\"should set spocs with DISCOVERY_STREAM_SPOCS_UPDATE\", () => {\n      const data = {\n        lastUpdated: 123,\n        spocs: [1, 2, 3],\n      };\n      const state = DiscoveryStream(undefined, {\n        type: at.DISCOVERY_STREAM_SPOCS_UPDATE,\n        data,\n      });\n      assert.deepEqual(state.spocs, {\n        spocs_endpoint: \"\",\n        spocs_per_domain: 1,\n        data: [1, 2, 3],\n        lastUpdated: 123,\n        loaded: true,\n        frequency_caps: [],\n        blocked: [],\n        placements: [],\n      });\n    });\n    it(\"should default to a single spoc placement\", () => {\n      const deleteAction = {\n        type: at.DISCOVERY_STREAM_LINK_BLOCKED,\n        data: { url: \"https://foo.com\" },\n      };\n      const oldState = {\n        spocs: {\n          data: {\n            spocs: [{ url: \"test-spoc.com\" }],\n          },\n          loaded: true,\n        },\n        feeds: {\n          data: {},\n          loaded: true,\n        },\n      };\n\n      const newState = DiscoveryStream(oldState, deleteAction);\n\n      assert.equal(newState.spocs.data.spocs.length, 1);\n    });\n    it(\"should handle no data from DISCOVERY_STREAM_SPOCS_UPDATE\", () => {\n      const data = null;\n      const state = DiscoveryStream(undefined, {\n        type: at.DISCOVERY_STREAM_SPOCS_UPDATE,\n        data,\n      });\n      assert.deepEqual(state.spocs, INITIAL_STATE.DiscoveryStream.spocs);\n    });\n    it(\"should add blocked spocs to blocked array with DISCOVERY_STREAM_SPOC_BLOCKED\", () => {\n      const firstState = DiscoveryStream(undefined, {\n        type: at.DISCOVERY_STREAM_SPOC_BLOCKED,\n        data: { url: \"https://foo.com\" },\n      });\n      const secondState = DiscoveryStream(firstState, {\n        type: at.DISCOVERY_STREAM_SPOC_BLOCKED,\n        data: { url: \"https://bar.com\" },\n      });\n      assert.deepEqual(firstState.spocs.blocked, [\"https://foo.com\"]);\n      assert.deepEqual(secondState.spocs.blocked, [\n        \"https://foo.com\",\n        \"https://bar.com\",\n      ]);\n    });\n    it(\"should not update state for empty action.data on DISCOVERY_STREAM_LINK_BLOCKED\", () => {\n      const newState = DiscoveryStream(undefined, {\n        type: at.DISCOVERY_STREAM_LINK_BLOCKED,\n      });\n      assert.equal(newState, INITIAL_STATE.DiscoveryStream);\n    });\n    it(\"should not update state if feeds are not loaded\", () => {\n      const deleteAction = {\n        type: at.DISCOVERY_STREAM_LINK_BLOCKED,\n        data: { url: \"foo.com\" },\n      };\n      const newState = DiscoveryStream(undefined, deleteAction);\n      assert.equal(newState, INITIAL_STATE.DiscoveryStream);\n    });\n    it(\"should not update state if spocs and feeds data is undefined\", () => {\n      const deleteAction = {\n        type: at.DISCOVERY_STREAM_LINK_BLOCKED,\n        data: { url: \"foo.com\" },\n      };\n      const oldState = {\n        spocs: {\n          data: {},\n          loaded: true,\n          placements: [{ name: \"spocs\" }],\n        },\n        feeds: {\n          data: {},\n          loaded: true,\n        },\n      };\n      const newState = DiscoveryStream(oldState, deleteAction);\n      assert.deepEqual(newState, oldState);\n    });\n    it(\"should remove the site on DISCOVERY_STREAM_LINK_BLOCKED from spocs if feeds data is empty\", () => {\n      const deleteAction = {\n        type: at.DISCOVERY_STREAM_LINK_BLOCKED,\n        data: { url: \"https://foo.com\" },\n      };\n      const oldState = {\n        spocs: {\n          data: {\n            spocs: [{ url: \"https://foo.com\" }, { url: \"test-spoc.com\" }],\n          },\n          loaded: true,\n          placements: [{ name: \"spocs\" }],\n        },\n        feeds: {\n          data: {},\n          loaded: true,\n        },\n      };\n      const newState = DiscoveryStream(oldState, deleteAction);\n      assert.deepEqual(newState.spocs.data.spocs, [{ url: \"test-spoc.com\" }]);\n    });\n    it(\"should remove the site on DISCOVERY_STREAM_LINK_BLOCKED from feeds if spocs data is empty\", () => {\n      const deleteAction = {\n        type: at.DISCOVERY_STREAM_LINK_BLOCKED,\n        data: { url: \"https://foo.com\" },\n      };\n      const oldState = {\n        spocs: {\n          data: {},\n          loaded: true,\n          placements: [{ name: \"spocs\" }],\n        },\n        feeds: {\n          data: {\n            \"https://foo.com/feed1\": {\n              data: {\n                recommendations: [\n                  { url: \"https://foo.com\" },\n                  { url: \"test.com\" },\n                ],\n              },\n            },\n          },\n          loaded: true,\n        },\n      };\n      const newState = DiscoveryStream(oldState, deleteAction);\n      assert.deepEqual(\n        newState.feeds.data[\"https://foo.com/feed1\"].data.recommendations,\n        [{ url: \"test.com\" }]\n      );\n    });\n    it(\"should remove the site on DISCOVERY_STREAM_LINK_BLOCKED from both feeds and spocs\", () => {\n      const oldState = {\n        feeds: {\n          data: {\n            \"https://foo.com/feed1\": {\n              data: {\n                recommendations: [\n                  { url: \"https://foo.com\" },\n                  { url: \"test.com\" },\n                ],\n              },\n            },\n          },\n          loaded: true,\n        },\n        spocs: {\n          data: {\n            spocs: [{ url: \"https://foo.com\" }, { url: \"test-spoc.com\" }],\n          },\n          loaded: true,\n          placements: [{ name: \"spocs\" }],\n        },\n      };\n      const deleteAction = {\n        type: at.DISCOVERY_STREAM_LINK_BLOCKED,\n        data: { url: \"https://foo.com\" },\n      };\n      const newState = DiscoveryStream(oldState, deleteAction);\n      assert.deepEqual(newState.spocs.data.spocs, [{ url: \"test-spoc.com\" }]);\n      assert.deepEqual(\n        newState.feeds.data[\"https://foo.com/feed1\"].data.recommendations,\n        [{ url: \"test.com\" }]\n      );\n    });\n    it(\"should not update state for empty action.data on PLACES_SAVED_TO_POCKET\", () => {\n      const newState = DiscoveryStream(undefined, {\n        type: at.PLACES_SAVED_TO_POCKET,\n      });\n      assert.equal(newState, INITIAL_STATE.DiscoveryStream);\n    });\n    it(\"should add pocket_id on PLACES_SAVED_TO_POCKET in both feeds and spocs\", () => {\n      const oldState = {\n        feeds: {\n          data: {\n            \"https://foo.com/feed1\": {\n              data: {\n                recommendations: [\n                  { url: \"https://foo.com\" },\n                  { url: \"test.com\" },\n                ],\n              },\n            },\n          },\n          loaded: true,\n        },\n        spocs: {\n          data: {\n            spocs: [{ url: \"https://foo.com\" }, { url: \"test-spoc.com\" }],\n          },\n          placements: [{ name: \"spocs\" }],\n          loaded: true,\n        },\n      };\n      const action = {\n        type: at.PLACES_SAVED_TO_POCKET,\n        data: {\n          url: \"https://foo.com\",\n          pocket_id: 1234,\n          open_url: \"https://foo-1234\",\n        },\n      };\n\n      const newState = DiscoveryStream(oldState, action);\n\n      assert.lengthOf(newState.spocs.data.spocs, 2);\n      assert.equal(\n        newState.spocs.data.spocs[0].pocket_id,\n        action.data.pocket_id\n      );\n      assert.equal(newState.spocs.data.spocs[0].open_url, action.data.open_url);\n      assert.isUndefined(newState.spocs.data.spocs[1].pocket_id);\n\n      assert.lengthOf(\n        newState.feeds.data[\"https://foo.com/feed1\"].data.recommendations,\n        2\n      );\n      assert.equal(\n        newState.feeds.data[\"https://foo.com/feed1\"].data.recommendations[0]\n          .pocket_id,\n        action.data.pocket_id\n      );\n      assert.equal(\n        newState.feeds.data[\"https://foo.com/feed1\"].data.recommendations[0]\n          .open_url,\n        action.data.open_url\n      );\n      assert.isUndefined(\n        newState.feeds.data[\"https://foo.com/feed1\"].data.recommendations[1]\n          .pocket_id\n      );\n    });\n    it(\"should not update state for empty action.data on DELETE_FROM_POCKET\", () => {\n      const newState = DiscoveryStream(undefined, {\n        type: at.DELETE_FROM_POCKET,\n      });\n      assert.equal(newState, INITIAL_STATE.DiscoveryStream);\n    });\n    it(\"should remove site on DELETE_FROM_POCKET in both feeds and spocs\", () => {\n      const oldState = {\n        feeds: {\n          data: {\n            \"https://foo.com/feed1\": {\n              data: {\n                recommendations: [\n                  { url: \"https://foo.com\", pocket_id: 1234 },\n                  { url: \"test.com\" },\n                ],\n              },\n            },\n          },\n          loaded: true,\n        },\n        spocs: {\n          data: {\n            spocs: [\n              { url: \"https://foo.com\", pocket_id: 1234 },\n              { url: \"test-spoc.com\" },\n            ],\n          },\n          loaded: true,\n          placements: [{ name: \"spocs\" }],\n        },\n      };\n      const deleteAction = {\n        type: at.DELETE_FROM_POCKET,\n        data: {\n          pocket_id: 1234,\n        },\n      };\n\n      const newState = DiscoveryStream(oldState, deleteAction);\n      assert.deepEqual(newState.spocs.data.spocs, [{ url: \"test-spoc.com\" }]);\n      assert.deepEqual(\n        newState.feeds.data[\"https://foo.com/feed1\"].data.recommendations,\n        [{ url: \"test.com\" }]\n      );\n    });\n    it(\"should remove site on ARCHIVE_FROM_POCKET in both feeds and spocs\", () => {\n      const oldState = {\n        feeds: {\n          data: {\n            \"https://foo.com/feed1\": {\n              data: {\n                recommendations: [\n                  { url: \"https://foo.com\", pocket_id: 1234 },\n                  { url: \"test.com\" },\n                ],\n              },\n            },\n          },\n          loaded: true,\n        },\n        spocs: {\n          data: {\n            spocs: [\n              { url: \"https://foo.com\", pocket_id: 1234 },\n              { url: \"test-spoc.com\" },\n            ],\n          },\n          loaded: true,\n          placements: [{ name: \"spocs\" }],\n        },\n      };\n      const deleteAction = {\n        type: at.ARCHIVE_FROM_POCKET,\n        data: {\n          pocket_id: 1234,\n        },\n      };\n\n      const newState = DiscoveryStream(oldState, deleteAction);\n      assert.deepEqual(newState.spocs.data.spocs, [{ url: \"test-spoc.com\" }]);\n      assert.deepEqual(\n        newState.feeds.data[\"https://foo.com/feed1\"].data.recommendations,\n        [{ url: \"test.com\" }]\n      );\n    });\n    it(\"should add boookmark details on PLACES_BOOKMARK_ADDED in both feeds and spocs\", () => {\n      const oldState = {\n        feeds: {\n          data: {\n            \"https://foo.com/feed1\": {\n              data: {\n                recommendations: [\n                  { url: \"https://foo.com\" },\n                  { url: \"test.com\" },\n                ],\n              },\n            },\n          },\n          loaded: true,\n        },\n        spocs: {\n          data: {\n            spocs: [{ url: \"https://foo.com\" }, { url: \"test-spoc.com\" }],\n          },\n          loaded: true,\n          placements: [{ name: \"spocs\" }],\n        },\n      };\n      const bookmarkAction = {\n        type: at.PLACES_BOOKMARK_ADDED,\n        data: {\n          url: \"https://foo.com\",\n          bookmarkGuid: \"bookmark123\",\n          bookmarkTitle: \"Title for bar.com\",\n          dateAdded: 1234567,\n        },\n      };\n\n      const newState = DiscoveryStream(oldState, bookmarkAction);\n\n      assert.lengthOf(newState.spocs.data.spocs, 2);\n      assert.equal(\n        newState.spocs.data.spocs[0].bookmarkGuid,\n        bookmarkAction.data.bookmarkGuid\n      );\n      assert.equal(\n        newState.spocs.data.spocs[0].bookmarkTitle,\n        bookmarkAction.data.bookmarkTitle\n      );\n      assert.isUndefined(newState.spocs.data.spocs[1].bookmarkGuid);\n\n      assert.lengthOf(\n        newState.feeds.data[\"https://foo.com/feed1\"].data.recommendations,\n        2\n      );\n      assert.equal(\n        newState.feeds.data[\"https://foo.com/feed1\"].data.recommendations[0]\n          .bookmarkGuid,\n        bookmarkAction.data.bookmarkGuid\n      );\n      assert.equal(\n        newState.feeds.data[\"https://foo.com/feed1\"].data.recommendations[0]\n          .bookmarkTitle,\n        bookmarkAction.data.bookmarkTitle\n      );\n      assert.isUndefined(\n        newState.feeds.data[\"https://foo.com/feed1\"].data.recommendations[1]\n          .bookmarkGuid\n      );\n    });\n\n    it(\"should remove boookmark details on PLACES_BOOKMARK_REMOVED in both feeds and spocs\", () => {\n      const oldState = {\n        feeds: {\n          data: {\n            \"https://foo.com/feed1\": {\n              data: {\n                recommendations: [\n                  {\n                    url: \"https://foo.com\",\n                    bookmarkGuid: \"bookmark123\",\n                    bookmarkTitle: \"Title for bar.com\",\n                  },\n                  { url: \"test.com\" },\n                ],\n              },\n            },\n          },\n          loaded: true,\n        },\n        spocs: {\n          data: {\n            spocs: [\n              {\n                url: \"https://foo.com\",\n                bookmarkGuid: \"bookmark123\",\n                bookmarkTitle: \"Title for bar.com\",\n              },\n              { url: \"test-spoc.com\" },\n            ],\n          },\n          loaded: true,\n          placements: [{ name: \"spocs\" }],\n        },\n      };\n      const action = {\n        type: at.PLACES_BOOKMARK_REMOVED,\n        data: {\n          url: \"https://foo.com\",\n        },\n      };\n\n      const newState = DiscoveryStream(oldState, action);\n\n      assert.lengthOf(newState.spocs.data.spocs, 2);\n      assert.isUndefined(newState.spocs.data.spocs[0].bookmarkGuid);\n      assert.isUndefined(newState.spocs.data.spocs[0].bookmarkTitle);\n\n      assert.lengthOf(\n        newState.feeds.data[\"https://foo.com/feed1\"].data.recommendations,\n        2\n      );\n      assert.isUndefined(\n        newState.feeds.data[\"https://foo.com/feed1\"].data.recommendations[0]\n          .bookmarkGuid\n      );\n      assert.isUndefined(\n        newState.feeds.data[\"https://foo.com/feed1\"].data.recommendations[0]\n          .bookmarkTitle\n      );\n    });\n  });\n  describe(\"Search\", () => {\n    it(\"should return INITIAL_STATE by default\", () => {\n      assert.equal(\n        Search(undefined, { type: \"some_action\" }),\n        INITIAL_STATE.Search\n      );\n    });\n    it(\"should set hide to true on HIDE_SEARCH\", () => {\n      const nextState = Search(undefined, { type: \"HIDE_SEARCH\" });\n      assert.propertyVal(nextState, \"hide\", true);\n    });\n    it(\"should set focus to true on FAKE_FOCUS_SEARCH\", () => {\n      const nextState = Search(undefined, { type: \"FAKE_FOCUS_SEARCH\" });\n      assert.propertyVal(nextState, \"fakeFocus\", true);\n    });\n    it(\"should set focus and hide to false on SHOW_SEARCH\", () => {\n      const nextState = Search(undefined, { type: \"SHOW_SEARCH\" });\n      assert.propertyVal(nextState, \"fakeFocus\", false);\n      assert.propertyVal(nextState, \"hide\", false);\n    });\n  });\n  it(\"should set initialized to true on AS_ROUTER_INITIALIZED\", () => {\n    const nextState = ASRouter(undefined, { type: \"AS_ROUTER_INITIALIZED\" });\n    assert.propertyVal(nextState, \"initialized\", true);\n  });\n});\n"
  },
  {
    "path": "test/unit/content-src/components/ASRouterAdmin.test.jsx",
    "content": "import {\n  ASRouterAdminInner,\n  CollapseToggle,\n  DiscoveryStreamAdmin,\n  ToggleStoryButton,\n} from \"content-src/components/ASRouterAdmin/ASRouterAdmin\";\nimport { GlobalOverrider } from \"test/unit/utils\";\nimport React from \"react\";\nimport { shallow } from \"enzyme\";\n\ndescribe(\"ASRouterAdmin\", () => {\n  let globals;\n  let sandbox;\n  let sendMessageStub;\n  let addListenerStub;\n  let removeListenerStub;\n  let wrapper;\n  let FAKE_PROVIDER_PREF = [\n    {\n      enabled: true,\n      id: \"snippets_local_testing\",\n      localProvider: \"SnippetsProvider\",\n      type: \"local\",\n    },\n  ];\n  let FAKE_PROVIDER = [\n    {\n      enabled: true,\n      id: \"snippets_local_testing\",\n      localProvider: \"SnippetsProvider\",\n      messages: [],\n      type: \"local\",\n    },\n  ];\n  beforeEach(() => {\n    globals = new GlobalOverrider();\n    sandbox = sinon.createSandbox();\n    sendMessageStub = sandbox.stub();\n    addListenerStub = sandbox.stub();\n    removeListenerStub = sandbox.stub();\n\n    globals.set(\"RPMSendAsyncMessage\", sendMessageStub);\n    globals.set(\"RPMAddMessageListener\", addListenerStub);\n    globals.set(\"RPMRemoveMessageListener\", removeListenerStub);\n\n    wrapper = shallow(\n      <ASRouterAdminInner collapsed={false} location={{ routes: [\"\"] }} />\n    );\n  });\n  afterEach(() => {\n    sandbox.restore();\n    globals.restore();\n  });\n  it(\"should render ASRouterAdmin component\", () => {\n    assert.ok(wrapper.exists());\n  });\n  it(\"should send ADMIN_CONNECT_STATE on mount\", () => {\n    assert.calledOnce(sendMessageStub);\n    assert.propertyVal(\n      sendMessageStub.firstCall.args[1],\n      \"type\",\n      \"ADMIN_CONNECT_STATE\"\n    );\n  });\n  it(\"should set a listener on mount\", () => {\n    assert.calledOnce(addListenerStub);\n    assert.calledWithExactly(\n      addListenerStub,\n      sinon.match.string,\n      wrapper.instance().onMessage\n    );\n  });\n  it(\"should remove listener on unmount\", () => {\n    wrapper.unmount();\n    assert.calledOnce(removeListenerStub);\n  });\n  it(\"should set a .collapsed class on the outer div if props.collapsed is true\", () => {\n    wrapper.setProps({ collapsed: true });\n    assert.isTrue(wrapper.find(\".asrouter-admin\").hasClass(\"collapsed\"));\n  });\n  it(\"should set a .expanded class on the outer div if props.collapsed is false\", () => {\n    wrapper.setProps({ collapsed: false });\n    assert.isTrue(wrapper.find(\".asrouter-admin\").hasClass(\"expanded\"));\n    assert.isFalse(wrapper.find(\".asrouter-admin\").hasClass(\"collapsed\"));\n  });\n  describe(\"#getSection\", () => {\n    it(\"should render a message provider section by default\", () => {\n      assert.equal(\n        wrapper\n          .find(\"h2\")\n          .at(2)\n          .text(),\n        \"Messages\"\n      );\n    });\n    it(\"should render a targeting section for targeting route\", () => {\n      wrapper = shallow(\n        <ASRouterAdminInner location={{ routes: [\"targeting\"] }} />\n      );\n      assert.equal(\n        wrapper\n          .find(\"h2\")\n          .at(0)\n          .text(),\n        \"Targeting Utilities\"\n      );\n    });\n    it(\"should render a pocket section for pocket route\", () => {\n      wrapper = shallow(\n        <ASRouterAdminInner location={{ routes: [\"pocket\"] }} Sections={[]} />\n      );\n      assert.equal(\n        wrapper\n          .find(\"h2\")\n          .at(0)\n          .text(),\n        \"Pocket\"\n      );\n    });\n    it(\"should render a DS section for DS route\", () => {\n      wrapper = shallow(\n        <ASRouterAdminInner\n          location={{ routes: [\"ds\"] }}\n          Sections={[]}\n          Prefs={{}}\n        />\n      );\n      assert.equal(\n        wrapper\n          .find(\"h2\")\n          .at(0)\n          .text(),\n        \"Discovery Stream\"\n      );\n    });\n    it(\"should render two error messages\", () => {\n      wrapper = shallow(\n        <ASRouterAdminInner location={{ routes: [\"errors\"] }} Sections={[]} />\n      );\n      const firstError = {\n        timestamp: Date.now() + 100,\n        error: { message: \"first\" },\n      };\n      const secondError = {\n        timestamp: Date.now(),\n        error: { message: \"second\" },\n      };\n      wrapper.setState({\n        providers: [{ id: \"foo\", errors: [firstError, secondError] }],\n      });\n\n      assert.equal(\n        wrapper\n          .find(\"tbody tr\")\n          .at(0)\n          .find(\"td\")\n          .at(0)\n          .text(),\n        \"foo\"\n      );\n      assert.lengthOf(wrapper.find(\"tbody tr\"), 2);\n      assert.equal(\n        wrapper\n          .find(\"tbody tr\")\n          .at(0)\n          .find(\"td\")\n          .at(1)\n          .text(),\n        secondError.error.message\n      );\n    });\n  });\n  describe(\"#render\", () => {\n    beforeEach(() => {\n      wrapper.setState({\n        providerPrefs: [],\n        providers: [],\n        userPrefs: {},\n      });\n    });\n    describe(\"#renderProviders\", () => {\n      it(\"should render the provider\", () => {\n        wrapper.setState({\n          providerPrefs: FAKE_PROVIDER_PREF,\n          providers: FAKE_PROVIDER,\n        });\n\n        // Header + 1 item\n        assert.lengthOf(wrapper.find(\".message-item\"), 2);\n      });\n    });\n    describe(\"#renderMessages\", () => {\n      beforeEach(() => {\n        wrapper.setState({\n          messageFilter: \"all\",\n          messageBlockList: [],\n          messageImpressions: { foo: 2 },\n        });\n      });\n      it(\"should render a message when no filtering is applied\", () => {\n        wrapper.setState({\n          messages: [{ id: \"foo\" }],\n        });\n\n        assert.lengthOf(wrapper.find(\".message-id\"), 1);\n        wrapper.find(\".message-item button.primary\").simulate(\"click\");\n        // first call is ADMIN_CONNECT_STATE\n        assert.propertyVal(\n          sendMessageStub.secondCall.args[1],\n          \"type\",\n          \"BLOCK_MESSAGE_BY_ID\"\n        );\n        assert.propertyVal(\n          sendMessageStub.secondCall.args[1].data,\n          \"id\",\n          \"foo\"\n        );\n      });\n      it(\"should render a blocked message\", () => {\n        wrapper.setState({\n          messages: [{ id: \"foo\" }],\n          messageBlockList: [\"foo\"],\n        });\n        assert.lengthOf(wrapper.find(\".message-item.blocked\"), 1);\n        wrapper.find(\".message-item.blocked button\").simulate(\"click\");\n        // first call is ADMIN_CONNECT_STATE\n        assert.propertyVal(\n          sendMessageStub.secondCall.args[1],\n          \"type\",\n          \"UNBLOCK_MESSAGE_BY_ID\"\n        );\n        assert.propertyVal(\n          sendMessageStub.secondCall.args[1].data,\n          \"id\",\n          \"foo\"\n        );\n      });\n      it(\"should render a message if provider matches filter\", () => {\n        wrapper.setState({\n          messageFilter: \"messageProvider\",\n          messages: [{ id: \"foo\", provider: \"messageProvider\" }],\n        });\n\n        assert.lengthOf(wrapper.find(\".message-id\"), 1);\n      });\n      it(\"should override with the selected message\", () => {\n        wrapper.setState({\n          messageFilter: \"messageProvider\",\n          messages: [{ id: \"foo\", provider: \"messageProvider\" }],\n        });\n\n        assert.lengthOf(wrapper.find(\".message-id\"), 1);\n        wrapper.find(\".message-item button:not(.primary)\").simulate(\"click\");\n        // first call is ADMIN_CONNECT_STATE\n        assert.propertyVal(\n          sendMessageStub.secondCall.args[1],\n          \"type\",\n          \"OVERRIDE_MESSAGE\"\n        );\n        assert.propertyVal(\n          sendMessageStub.secondCall.args[1].data,\n          \"id\",\n          \"foo\"\n        );\n      });\n      it(\"should hide message if provider filter changes\", () => {\n        wrapper.setState({\n          messageFilter: \"messageProvider\",\n          messages: [{ id: \"foo\", provider: \"messageProvider\" }],\n        });\n\n        assert.lengthOf(wrapper.find(\".message-id\"), 1);\n\n        wrapper.find(\"select\").simulate(\"change\", { target: { value: \"bar\" } });\n\n        assert.lengthOf(wrapper.find(\".message-id\"), 0);\n      });\n    });\n  });\n  describe(\"#DiscoveryStream\", () => {\n    it(\"should render a DiscoveryStreamAdmin component\", () => {\n      wrapper = shallow(\n        <DiscoveryStreamAdmin\n          otherPrefs={{}}\n          state={{\n            config: {\n              enabled: true,\n              layout_endpint: \"\",\n            },\n            layout: [],\n            spocs: {\n              frequency_caps: [],\n            },\n            feeds: {\n              data: {},\n            },\n          }}\n        />\n      );\n      assert.equal(\n        wrapper\n          .find(\"h3\")\n          .at(0)\n          .text(),\n        \"Endpoint variant\"\n      );\n    });\n    it(\"should render a spoc in DiscoveryStreamAdmin component\", () => {\n      wrapper = shallow(\n        <DiscoveryStreamAdmin\n          otherPrefs={{}}\n          state={{\n            config: {\n              enabled: true,\n              layout_endpint: \"\",\n            },\n            layout: [],\n            spocs: {\n              frequency_caps: [],\n              data: {\n                spocs: [\n                  {\n                    id: 12345,\n                  },\n                ],\n              },\n            },\n            feeds: {\n              data: {},\n            },\n          }}\n        />\n      );\n      wrapper.instance().onStoryToggle({ id: 12345 });\n      const messageSummary = wrapper.find(\".message-summary\").at(0);\n      const pre = messageSummary.find(\"pre\").at(0);\n      const spocText = pre.text();\n      assert.equal(spocText, '{\\n  \"id\": 12345\\n}');\n    });\n  });\n  describe(\"#ToggleStoryButton\", () => {\n    it(\"should fire onClick in toggle button\", async () => {\n      let result = \"\";\n      function onClick(spoc) {\n        result = spoc;\n      }\n\n      wrapper = shallow(<ToggleStoryButton story=\"spoc\" onClick={onClick} />);\n      wrapper.find(\"button\").simulate(\"click\");\n\n      assert.equal(result, \"spoc\");\n    });\n  });\n});\n\ndescribe(\"CollapseToggle\", () => {\n  let wrapper;\n  beforeEach(() => {\n    wrapper = shallow(<CollapseToggle location={{ routes: [\"\"] }} />);\n  });\n\n  describe(\"rendering inner content\", () => {\n    it(\"should not render ASRouterAdminInner for about:newtab (no hash)\", () => {\n      wrapper.setProps({ location: { hash: \"\", routes: [\"\"] } });\n      assert.lengthOf(wrapper.find(ASRouterAdminInner), 0);\n    });\n\n    it(\"should render ASRouterAdminInner for about:newtab#asrouter and subroutes\", () => {\n      wrapper.setProps({ location: { hash: \"#asrouter\", routes: [\"\"] } });\n      assert.lengthOf(wrapper.find(ASRouterAdminInner), 1);\n\n      wrapper.setProps({ location: { hash: \"#asrouter-foo\", routes: [\"\"] } });\n      assert.lengthOf(wrapper.find(ASRouterAdminInner), 1);\n    });\n\n    it(\"should render ASRouterAdminInner for about:newtab#devtools and subroutes\", () => {\n      wrapper.setProps({ location: { hash: \"#devtools\", routes: [\"\"] } });\n      assert.lengthOf(wrapper.find(ASRouterAdminInner), 1);\n\n      wrapper.setProps({ location: { hash: \"#devtools-foo\", routes: [\"\"] } });\n      assert.lengthOf(wrapper.find(ASRouterAdminInner), 1);\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/content-src/components/Base.test.jsx",
    "content": "import { _Base as Base, BaseContent } from \"content-src/components/Base/Base\";\nimport { ASRouterAdmin } from \"content-src/components/ASRouterAdmin/ASRouterAdmin\";\nimport { ErrorBoundary } from \"content-src/components/ErrorBoundary/ErrorBoundary\";\nimport React from \"react\";\nimport { Search } from \"content-src/components/Search/Search\";\nimport { shallow } from \"enzyme\";\n\ndescribe(\"<Base>\", () => {\n  let DEFAULT_PROPS = {\n    store: { getState: () => {} },\n    App: { initialized: true },\n    Prefs: { values: {} },\n    Sections: [],\n    DiscoveryStream: { config: { enabled: false } },\n    dispatch: () => {},\n  };\n\n  it(\"should render Base component\", () => {\n    const wrapper = shallow(<Base {...DEFAULT_PROPS} />);\n    assert.ok(wrapper.exists());\n  });\n\n  it(\"should render the BaseContent component, passing through all props\", () => {\n    const wrapper = shallow(<Base {...DEFAULT_PROPS} />);\n\n    assert.deepEqual(wrapper.find(BaseContent).props(), DEFAULT_PROPS);\n  });\n\n  it(\"should render an ErrorBoundary with class base-content-fallback\", () => {\n    const wrapper = shallow(<Base {...DEFAULT_PROPS} />);\n\n    assert.equal(\n      wrapper\n        .find(ErrorBoundary)\n        .first()\n        .prop(\"className\"),\n      \"base-content-fallback\"\n    );\n  });\n\n  it(\"should render an ASRouterAdmin if the devtools pref is true\", () => {\n    const wrapper = shallow(\n      <Base\n        {...DEFAULT_PROPS}\n        Prefs={{ values: { \"asrouter.devtoolsEnabled\": true } }}\n      />\n    );\n    assert.lengthOf(wrapper.find(ASRouterAdmin), 1);\n  });\n\n  it(\"should not render an ASRouterAdmin if the devtools pref is false\", () => {\n    const wrapper = shallow(\n      <Base\n        {...DEFAULT_PROPS}\n        Prefs={{ values: { \"asrouter.devtoolsEnabled\": false } }}\n      />\n    );\n    assert.lengthOf(wrapper.find(ASRouterAdmin), 0);\n  });\n});\n\ndescribe(\"<BaseContent>\", () => {\n  let DEFAULT_PROPS = {\n    store: { getState: () => {} },\n    App: { initialized: true },\n    Prefs: { values: {} },\n    Sections: [],\n    DiscoveryStream: { config: { enabled: false } },\n    dispatch: () => {},\n  };\n\n  it(\"should render an ErrorBoundary with a Search child\", () => {\n    const searchEnabledProps = Object.assign({}, DEFAULT_PROPS, {\n      Prefs: { values: { showSearch: true } },\n    });\n\n    const wrapper = shallow(<BaseContent {...searchEnabledProps} />);\n\n    assert.isTrue(\n      wrapper\n        .find(Search)\n        .parent()\n        .is(ErrorBoundary)\n    );\n  });\n\n  it(\"should render only search if no Sections are enabled\", () => {\n    const onlySearchProps = Object.assign({}, DEFAULT_PROPS, {\n      Sections: [{ id: \"highlights\", enabled: false }],\n      Prefs: { values: { showSearch: true } },\n    });\n\n    const wrapper = shallow(<BaseContent {...onlySearchProps} />);\n    assert.lengthOf(wrapper.find(\".only-search\"), 1);\n  });\n\n  it(\"should render only search if only highlights is available in DS\", () => {\n    const onlySearchProps = Object.assign({}, DEFAULT_PROPS, {\n      Sections: [{ id: \"highlights\", enabled: true }],\n      DiscoveryStream: { config: { enabled: true } },\n      Prefs: { values: { showSearch: true } },\n    });\n\n    const wrapper = shallow(<BaseContent {...onlySearchProps} />);\n    assert.lengthOf(wrapper.find(\".only-search\"), 1);\n  });\n});\n"
  },
  {
    "path": "test/unit/content-src/components/Card.test.jsx",
    "content": "import { actionCreators as ac, actionTypes as at } from \"common/Actions.jsm\";\nimport {\n  _Card as Card,\n  PlaceholderCard,\n} from \"content-src/components/Card/Card\";\nimport { combineReducers, createStore } from \"redux\";\nimport { GlobalOverrider } from \"test/unit/utils\";\nimport { INITIAL_STATE, reducers } from \"common/Reducers.jsm\";\nimport { cardContextTypes } from \"content-src/components/Card/types\";\nimport { ContextMenuButton } from \"content-src/components/ContextMenu/ContextMenuButton\";\nimport { LinkMenu } from \"content-src/components/LinkMenu/LinkMenu\";\nimport { Provider } from \"react-redux\";\nimport React from \"react\";\nimport { shallow, mount } from \"enzyme\";\n\nlet DEFAULT_PROPS = {\n  dispatch: sinon.stub(),\n  index: 0,\n  link: {\n    hostname: \"foo\",\n    title: \"A title for foo\",\n    url: \"http://www.foo.com\",\n    type: \"history\",\n    description: \"A description for foo\",\n    image: \"http://www.foo.com/img.png\",\n    guid: 1,\n  },\n  eventSource: \"TOP_STORIES\",\n  shouldSendImpressionStats: true,\n  contextMenuOptions: [\"Separator\"],\n};\n\nlet DEFAULT_BLOB_IMAGE = {\n  path: \"/testpath\",\n  data: new Blob([0]),\n};\n\nfunction mountCardWithProps(props) {\n  const store = createStore(combineReducers(reducers), INITIAL_STATE);\n  return mount(\n    <Provider store={store}>\n      <Card {...props} />\n    </Provider>\n  );\n}\n\ndescribe(\"<Card>\", () => {\n  let globals;\n  let wrapper;\n  beforeEach(() => {\n    globals = new GlobalOverrider();\n    wrapper = mountCardWithProps(DEFAULT_PROPS);\n  });\n  afterEach(() => {\n    DEFAULT_PROPS.dispatch.reset();\n    globals.restore();\n  });\n  it(\"should render a Card component\", () => assert.ok(wrapper.exists()));\n  it(\"should add the right url\", () => {\n    assert.propertyVal(\n      wrapper.find(\"a\").props(),\n      \"href\",\n      DEFAULT_PROPS.link.url\n    );\n\n    // test that pocket cards get a special open_url href\n    const pocketLink = Object.assign({}, DEFAULT_PROPS.link, {\n      open_url: \"getpocket.com/foo\",\n      type: \"pocket\",\n    });\n    wrapper = mount(\n      <Card {...Object.assign({}, DEFAULT_PROPS, { link: pocketLink })} />\n    );\n    assert.propertyVal(wrapper.find(\"a\").props(), \"href\", pocketLink.open_url);\n  });\n  it(\"should display a title\", () =>\n    assert.equal(wrapper.find(\".card-title\").text(), DEFAULT_PROPS.link.title));\n  it(\"should display a description\", () =>\n    assert.equal(\n      wrapper.find(\".card-description\").text(),\n      DEFAULT_PROPS.link.description\n    ));\n  it(\"should display a host name\", () =>\n    assert.equal(wrapper.find(\".card-host-name\").text(), \"foo\"));\n  it(\"should have a link menu button\", () =>\n    assert.ok(wrapper.find(\".context-menu-button\").exists()));\n  it(\"should render a link menu when button is clicked\", () => {\n    const button = wrapper.find(\".context-menu-button\");\n    assert.equal(wrapper.find(LinkMenu).length, 0);\n    button.simulate(\"click\", { preventDefault: () => {} });\n    assert.equal(wrapper.find(LinkMenu).length, 1);\n  });\n  it(\"should pass dispatch, source, onUpdate, site, options, and index to LinkMenu\", () => {\n    wrapper\n      .find(\".context-menu-button\")\n      .simulate(\"click\", { preventDefault: () => {} });\n    const { dispatch, source, onUpdate, site, options, index } = wrapper\n      .find(LinkMenu)\n      .props();\n    assert.equal(dispatch, DEFAULT_PROPS.dispatch);\n    assert.equal(source, DEFAULT_PROPS.eventSource);\n    assert.ok(onUpdate);\n    assert.equal(site, DEFAULT_PROPS.link);\n    assert.equal(options, DEFAULT_PROPS.contextMenuOptions);\n    assert.equal(index, DEFAULT_PROPS.index);\n  });\n  it(\"should pass through the correct menu options to LinkMenu if overridden by individual card\", () => {\n    const link = Object.assign({}, DEFAULT_PROPS.link);\n    link.contextMenuOptions = [\"CheckBookmark\"];\n\n    wrapper = mountCardWithProps(Object.assign({}, DEFAULT_PROPS, { link }));\n    wrapper\n      .find(\".context-menu-button\")\n      .simulate(\"click\", { preventDefault: () => {} });\n    const { options } = wrapper.find(LinkMenu).props();\n    assert.equal(options, link.contextMenuOptions);\n  });\n  it(\"should have a context based on type\", () => {\n    wrapper = shallow(<Card {...DEFAULT_PROPS} />);\n    const context = wrapper.find(\".card-context\");\n    const { icon, fluentID } = cardContextTypes[DEFAULT_PROPS.link.type];\n    assert.isTrue(context.childAt(0).hasClass(`icon-${icon}`));\n    assert.isTrue(context.childAt(1).hasClass(\"card-context-label\"));\n    assert.equal(context.childAt(1).prop(\"data-l10n-id\"), fluentID);\n  });\n  it(\"should support setting custom context\", () => {\n    const linkWithCustomContext = {\n      type: \"history\",\n      context: \"Custom\",\n      icon: \"icon-url\",\n    };\n\n    wrapper = shallow(\n      <Card\n        {...Object.assign({}, DEFAULT_PROPS, { link: linkWithCustomContext })}\n      />\n    );\n    const context = wrapper.find(\".card-context\");\n    const { icon } = cardContextTypes[DEFAULT_PROPS.link.type];\n    assert.isFalse(context.childAt(0).hasClass(`icon-${icon}`));\n    assert.equal(\n      context.childAt(0).props().style.backgroundImage,\n      \"url('icon-url')\"\n    );\n\n    assert.isTrue(context.childAt(1).hasClass(\"card-context-label\"));\n    assert.equal(context.childAt(1).text(), linkWithCustomContext.context);\n  });\n  it(\"should parse args for fluent correctly\", () => {\n    const title = '\"fluent\"';\n    const link = { ...DEFAULT_PROPS.link, title };\n\n    wrapper = mountCardWithProps({ ...DEFAULT_PROPS, link });\n    let button = wrapper.find(ContextMenuButton).find(\"button\");\n\n    assert.equal(button.prop(\"data-l10n-args\"), JSON.stringify({ title }));\n  });\n  it(\"should have .active class, on card-outer if context menu is open\", () => {\n    const button = wrapper.find(ContextMenuButton);\n    assert.isFalse(\n      wrapper.find(\".card-outer\").hasClass(\"active\"),\n      \"does not have active class\"\n    );\n    button.simulate(\"click\", { preventDefault: () => {} });\n    assert.isTrue(\n      wrapper.find(\".card-outer\").hasClass(\"active\"),\n      \"has active class\"\n    );\n  });\n  it(\"should send SHOW_DOWNLOAD_FILE if we clicked on a download\", () => {\n    const downloadLink = {\n      type: \"download\",\n      url: \"download.mov\",\n    };\n    wrapper = mountCardWithProps(\n      Object.assign({}, DEFAULT_PROPS, { link: downloadLink })\n    );\n    const card = wrapper.find(\".card\");\n    card.simulate(\"click\", { preventDefault: () => {} });\n    assert.calledThrice(DEFAULT_PROPS.dispatch);\n\n    assert.equal(\n      DEFAULT_PROPS.dispatch.firstCall.args[0].type,\n      at.SHOW_DOWNLOAD_FILE\n    );\n    assert.deepEqual(\n      DEFAULT_PROPS.dispatch.firstCall.args[0].data,\n      downloadLink\n    );\n  });\n  it(\"should send OPEN_LINK if we clicked on anything other than a download\", () => {\n    const nonDownloadLink = {\n      type: \"history\",\n      url: \"download.mov\",\n    };\n    wrapper = mountCardWithProps(\n      Object.assign({}, DEFAULT_PROPS, { link: nonDownloadLink })\n    );\n    const card = wrapper.find(\".card\");\n    const event = {\n      altKey: \"1\",\n      button: \"2\",\n      ctrlKey: \"3\",\n      metaKey: \"4\",\n      shiftKey: \"5\",\n    };\n    card.simulate(\n      \"click\",\n      Object.assign({}, event, { preventDefault: () => {} })\n    );\n    assert.calledThrice(DEFAULT_PROPS.dispatch);\n\n    assert.equal(DEFAULT_PROPS.dispatch.firstCall.args[0].type, at.OPEN_LINK);\n  });\n  describe(\"card image display\", () => {\n    const DEFAULT_BLOB_URL = \"blob://test\";\n    let url;\n    beforeEach(() => {\n      url = {\n        createObjectURL: globals.sandbox.stub().returns(DEFAULT_BLOB_URL),\n        revokeObjectURL: globals.sandbox.spy(),\n      };\n      globals.set(\"URL\", url);\n    });\n    afterEach(() => {\n      globals.restore();\n    });\n    it(\"should display a regular image correctly and not call revokeObjectURL when unmounted\", () => {\n      wrapper = shallow(<Card {...DEFAULT_PROPS} />);\n\n      assert.isUndefined(wrapper.state(\"cardImage\").path);\n      assert.equal(wrapper.state(\"cardImage\").url, DEFAULT_PROPS.link.image);\n      assert.equal(\n        wrapper.find(\".card-preview-image\").props().style.backgroundImage,\n        `url(${wrapper.state(\"cardImage\").url})`\n      );\n\n      wrapper.unmount();\n      assert.notCalled(url.revokeObjectURL);\n    });\n    it(\"should display a blob image correctly and revoke blob url when unmounted\", () => {\n      const link = Object.assign({}, DEFAULT_PROPS.link, {\n        image: DEFAULT_BLOB_IMAGE,\n      });\n      wrapper = shallow(<Card {...DEFAULT_PROPS} link={link} />);\n\n      assert.equal(wrapper.state(\"cardImage\").path, DEFAULT_BLOB_IMAGE.path);\n      assert.equal(wrapper.state(\"cardImage\").url, DEFAULT_BLOB_URL);\n      assert.equal(\n        wrapper.find(\".card-preview-image\").props().style.backgroundImage,\n        `url(${wrapper.state(\"cardImage\").url})`\n      );\n\n      wrapper.unmount();\n      assert.calledOnce(url.revokeObjectURL);\n    });\n    it(\"should not show an image if there isn't one and not call revokeObjectURL when unmounted\", () => {\n      const link = Object.assign({}, DEFAULT_PROPS.link);\n      delete link.image;\n\n      wrapper = shallow(<Card {...DEFAULT_PROPS} link={link} />);\n\n      assert.isNull(wrapper.state(\"cardImage\"));\n      assert.lengthOf(wrapper.find(\".card-preview-image\"), 0);\n\n      wrapper.unmount();\n      assert.notCalled(url.revokeObjectURL);\n    });\n    it(\"should remove current card image if new image is not present\", () => {\n      wrapper = shallow(<Card {...DEFAULT_PROPS} />);\n\n      const otherLink = Object.assign({}, DEFAULT_PROPS.link);\n      delete otherLink.image;\n      wrapper.setProps(Object.assign({}, DEFAULT_PROPS, { link: otherLink }));\n\n      assert.isNull(wrapper.state(\"cardImage\"));\n    });\n    it(\"should not create or revoke urls if normal image is already in state\", () => {\n      wrapper = shallow(<Card {...DEFAULT_PROPS} />);\n\n      wrapper.setProps(DEFAULT_PROPS);\n\n      assert.notCalled(url.createObjectURL);\n      assert.notCalled(url.revokeObjectURL);\n    });\n    it(\"should not create or revoke more urls if blob image is already in state\", () => {\n      const link = Object.assign({}, DEFAULT_PROPS.link, {\n        image: DEFAULT_BLOB_IMAGE,\n      });\n      wrapper = shallow(<Card {...DEFAULT_PROPS} link={link} />);\n\n      assert.calledOnce(url.createObjectURL);\n      assert.notCalled(url.revokeObjectURL);\n\n      wrapper.setProps(Object.assign({}, DEFAULT_PROPS, { link }));\n\n      assert.calledOnce(url.createObjectURL);\n      assert.notCalled(url.revokeObjectURL);\n    });\n    it(\"should create blob urls for new blobs and revoke existing ones\", () => {\n      const link = Object.assign({}, DEFAULT_PROPS.link, {\n        image: DEFAULT_BLOB_IMAGE,\n      });\n      wrapper = shallow(<Card {...DEFAULT_PROPS} link={link} />);\n\n      assert.calledOnce(url.createObjectURL);\n      assert.notCalled(url.revokeObjectURL);\n\n      const otherLink = Object.assign({}, DEFAULT_PROPS.link, {\n        image: { path: \"/newpath\", data: new Blob([0]) },\n      });\n      wrapper.setProps(Object.assign({}, DEFAULT_PROPS, { link: otherLink }));\n\n      assert.calledTwice(url.createObjectURL);\n      assert.calledOnce(url.revokeObjectURL);\n    });\n    it(\"should not call createObjectURL and revokeObjectURL for normal images\", () => {\n      wrapper = shallow(<Card {...DEFAULT_PROPS} />);\n\n      assert.notCalled(url.createObjectURL);\n      assert.notCalled(url.revokeObjectURL);\n\n      const otherLink = Object.assign({}, DEFAULT_PROPS.link, {\n        image: \"https://other/image\",\n      });\n      wrapper.setProps(Object.assign({}, DEFAULT_PROPS, { link: otherLink }));\n\n      assert.notCalled(url.createObjectURL);\n      assert.notCalled(url.revokeObjectURL);\n    });\n  });\n  describe(\"image loading\", () => {\n    let link;\n    let triggerImage = {};\n    let uniqueLink = 0;\n    beforeEach(() => {\n      global.Image.prototype = {\n        addEventListener(event, callback) {\n          triggerImage[event] = () => Promise.resolve(callback());\n        },\n      };\n\n      link = Object.assign({}, DEFAULT_PROPS.link);\n      link.image += uniqueLink++;\n      wrapper = shallow(<Card {...DEFAULT_PROPS} link={link} />);\n    });\n    it(\"should have a loaded preview image when the image is loaded\", () => {\n      assert.isFalse(wrapper.find(\".card-preview-image\").hasClass(\"loaded\"));\n\n      wrapper.setState({ imageLoaded: true });\n\n      assert.isTrue(wrapper.find(\".card-preview-image\").hasClass(\"loaded\"));\n    });\n    it(\"should start not loaded\", () => {\n      assert.isFalse(wrapper.state(\"imageLoaded\"));\n    });\n    it(\"should be loaded after load\", async () => {\n      await triggerImage.load();\n\n      assert.isTrue(wrapper.state(\"imageLoaded\"));\n    });\n    it(\"should be not be loaded after error \", async () => {\n      await triggerImage.error();\n\n      assert.isFalse(wrapper.state(\"imageLoaded\"));\n    });\n    it(\"should be not be loaded if image changes\", async () => {\n      await triggerImage.load();\n      const otherLink = Object.assign({}, link, {\n        image: \"https://other/image\",\n      });\n\n      wrapper.setProps(Object.assign({}, DEFAULT_PROPS, { link: otherLink }));\n\n      assert.isFalse(wrapper.state(\"imageLoaded\"));\n    });\n  });\n  describe(\"placeholder=true\", () => {\n    beforeEach(() => {\n      wrapper = mount(<Card placeholder={true} />);\n    });\n    it(\"should render when placeholder=true\", () => {\n      assert.ok(wrapper.exists());\n    });\n    it(\"should add a placeholder class to the outer element\", () => {\n      assert.isTrue(wrapper.find(\".card-outer\").hasClass(\"placeholder\"));\n    });\n    it(\"should not have a context menu button or LinkMenu\", () => {\n      assert.isFalse(\n        wrapper.find(ContextMenuButton).exists(),\n        \"context menu button\"\n      );\n      assert.isFalse(wrapper.find(LinkMenu).exists(), \"LinkMenu\");\n    });\n    it(\"should not call onLinkClick when the link is clicked\", () => {\n      const spy = sinon.spy(wrapper.instance(), \"onLinkClick\");\n      const card = wrapper.find(\".card\");\n      card.simulate(\"click\");\n      assert.notCalled(spy);\n    });\n  });\n  describe(\"#trackClick\", () => {\n    it(\"should call dispatch when the link is clicked with the right data\", () => {\n      const card = wrapper.find(\".card\");\n      const event = {\n        altKey: \"1\",\n        button: \"2\",\n        ctrlKey: \"3\",\n        metaKey: \"4\",\n        shiftKey: \"5\",\n      };\n      card.simulate(\n        \"click\",\n        Object.assign({}, event, { preventDefault: () => {} })\n      );\n      assert.calledThrice(DEFAULT_PROPS.dispatch);\n\n      // first dispatch call is the AlsoToMain message which will open a link in a window, and send some event data\n      assert.equal(DEFAULT_PROPS.dispatch.firstCall.args[0].type, at.OPEN_LINK);\n      assert.deepEqual(\n        DEFAULT_PROPS.dispatch.firstCall.args[0].data.event,\n        event\n      );\n\n      // second dispatch call is a UserEvent action for telemetry\n      assert.isUserEventAction(DEFAULT_PROPS.dispatch.secondCall.args[0]);\n      assert.calledWith(\n        DEFAULT_PROPS.dispatch.secondCall,\n        ac.UserEvent({\n          event: \"CLICK\",\n          source: DEFAULT_PROPS.eventSource,\n          action_position: DEFAULT_PROPS.index,\n        })\n      );\n\n      // third dispatch call is to send impression stats\n      assert.calledWith(\n        DEFAULT_PROPS.dispatch.thirdCall,\n        ac.ImpressionStats({\n          source: DEFAULT_PROPS.eventSource,\n          click: 0,\n          tiles: [{ id: DEFAULT_PROPS.link.guid, pos: DEFAULT_PROPS.index }],\n        })\n      );\n    });\n    it(\"should provide card_type to telemetry info if type is not history\", () => {\n      const link = Object.assign({}, DEFAULT_PROPS.link);\n      link.type = \"bookmark\";\n      wrapper = mount(<Card {...Object.assign({}, DEFAULT_PROPS, { link })} />);\n      const card = wrapper.find(\".card\");\n      const event = {\n        altKey: \"1\",\n        button: \"2\",\n        ctrlKey: \"3\",\n        metaKey: \"4\",\n        shiftKey: \"5\",\n      };\n\n      card.simulate(\n        \"click\",\n        Object.assign({}, event, { preventDefault: () => {} })\n      );\n\n      assert.isUserEventAction(DEFAULT_PROPS.dispatch.secondCall.args[0]);\n      assert.calledWith(\n        DEFAULT_PROPS.dispatch.secondCall,\n        ac.UserEvent({\n          event: \"CLICK\",\n          source: DEFAULT_PROPS.eventSource,\n          action_position: DEFAULT_PROPS.index,\n          value: { card_type: link.type },\n        })\n      );\n    });\n    it(\"should notify Web Extensions with WEBEXT_CLICK if props.isWebExtension is true\", () => {\n      wrapper = mountCardWithProps(\n        Object.assign({}, DEFAULT_PROPS, {\n          isWebExtension: true,\n          eventSource: \"MyExtension\",\n          index: 3,\n        })\n      );\n      const card = wrapper.find(\".card\");\n      const event = { preventDefault() {} };\n      card.simulate(\"click\", event);\n      assert.calledWith(\n        DEFAULT_PROPS.dispatch,\n        ac.WebExtEvent(at.WEBEXT_CLICK, {\n          source: \"MyExtension\",\n          url: DEFAULT_PROPS.link.url,\n          action_position: 3,\n        })\n      );\n    });\n  });\n});\n\ndescribe(\"<PlaceholderCard />\", () => {\n  it(\"should render a Card with placeholder=true\", () => {\n    const wrapper = mount(\n      <Provider store={createStore(combineReducers(reducers), INITIAL_STATE)}>\n        <PlaceholderCard />\n      </Provider>\n    );\n    assert.isTrue(wrapper.find(Card).props().placeholder);\n  });\n});\n"
  },
  {
    "path": "test/unit/content-src/components/CollapsibleSection.test.jsx",
    "content": "import { actionTypes as at } from \"common/Actions.jsm\";\nimport { CollapsibleSection } from \"content-src/components/CollapsibleSection/CollapsibleSection\";\nimport { ErrorBoundary } from \"content-src/components/ErrorBoundary/ErrorBoundary\";\nimport { mount } from \"enzyme\";\nimport React from \"react\";\n\nconst DEFAULT_PROPS = {\n  id: \"cool\",\n  className: \"cool-section\",\n  title: \"Cool Section\",\n  prefName: \"collapseSection\",\n  collapsed: false,\n  document: {\n    addEventListener: () => {},\n    removeEventListener: () => {},\n    visibilityState: \"visible\",\n  },\n  dispatch: () => {},\n};\n\ndescribe(\"CollapsibleSection\", () => {\n  let wrapper;\n\n  function setup(props = {}) {\n    const customProps = Object.assign({}, DEFAULT_PROPS, props);\n    wrapper = mount(\n      <CollapsibleSection {...customProps}>foo</CollapsibleSection>\n    );\n  }\n\n  beforeEach(() => setup());\n\n  it(\"should render the component\", () => {\n    assert.ok(wrapper.exists());\n  });\n\n  it(\"should render an ErrorBoundary with class section-body-fallback\", () => {\n    assert.equal(\n      wrapper\n        .find(ErrorBoundary)\n        .first()\n        .prop(\"className\"),\n      \"section-body-fallback\"\n    );\n  });\n\n  it(\"should have collapsed class if 'prefName' pref is true\", () => {\n    setup({ collapsed: true });\n    assert.ok(\n      wrapper\n        .find(\".collapsible-section\")\n        .first()\n        .hasClass(\"collapsed\")\n    );\n  });\n\n  it(\"should fire a pref change event when section title is clicked\", done => {\n    function dispatch(a) {\n      if (a.type === at.UPDATE_SECTION_PREFS) {\n        assert.equal(a.data.id, DEFAULT_PROPS.id);\n        assert.equal(a.data.value.collapsed, true);\n        done();\n      }\n    }\n    setup({ dispatch });\n    wrapper\n      .find(\".click-target\")\n      .at(0)\n      .simulate(\"click\");\n  });\n\n  it(\"should not fire a pref change when section title is clicked if sectionBody is falsy\", () => {\n    const dispatch = sinon.spy();\n    setup({ dispatch });\n    delete wrapper.find(CollapsibleSection).instance().sectionBody;\n\n    wrapper\n      .find(\".click-target\")\n      .at(0)\n      .simulate(\"click\");\n\n    assert.notCalled(dispatch);\n  });\n\n  it(\"should enable animations if the tab is visible\", () => {\n    wrapper.instance().enableOrDisableAnimation();\n    assert.ok(wrapper.instance().state.enableAnimation);\n  });\n\n  it(\"should disable animations if the tab is in the background\", () => {\n    const doc = Object.assign({}, DEFAULT_PROPS.document, {\n      visibilityState: \"hidden\",\n    });\n    setup({ document: doc });\n    wrapper.instance().enableOrDisableAnimation();\n    assert.isFalse(wrapper.instance().state.enableAnimation);\n  });\n\n  describe(\"without collapsible pref\", () => {\n    let dispatch;\n    beforeEach(() => {\n      dispatch = sinon.stub();\n      setup({ collapsed: undefined, dispatch });\n    });\n    it(\"should render the section uncollapsed\", () => {\n      assert.isFalse(\n        wrapper\n          .find(\".collapsible-section\")\n          .first()\n          .hasClass(\"collapsed\")\n      );\n    });\n\n    it(\"should not render the arrow if no collapsible pref exists for the section\", () => {\n      assert.lengthOf(wrapper.find(\".click-target .collapsible-arrow\"), 0);\n    });\n\n    it(\"should not trigger a dispatch when the section title is clicked \", () => {\n      wrapper\n        .find(\".click-target\")\n        .at(0)\n        .simulate(\"click\");\n\n      assert.notCalled(dispatch);\n    });\n  });\n\n  describe(\"icon\", () => {\n    it(\"should use the icon prop value as the url if it starts with `moz-extension://`\", () => {\n      const icon = \"moz-extension://some/extension/path\";\n      setup({ icon });\n      const props = wrapper\n        .find(\".icon\")\n        .first()\n        .props();\n      assert.equal(props.style.backgroundImage, `url('${icon}')`);\n    });\n    it(\"should use set the icon-* class if a string that doesn't start with `moz-extension://` is provided\", () => {\n      setup({ icon: \"cool\" });\n      assert.ok(\n        wrapper\n          .find(\".icon\")\n          .first()\n          .hasClass(\"icon-cool\")\n      );\n    });\n    it(\"should use the icon `webextension` if no other is provided\", () => {\n      setup({ icon: undefined });\n      assert.ok(\n        wrapper\n          .find(\".icon\")\n          .first()\n          .hasClass(\"icon-webextension\")\n      );\n    });\n  });\n\n  describe(\"maxHeight\", () => {\n    const maxHeight = \"123px\";\n    const setState = state =>\n      wrapper.setState(Object.assign({ maxHeight }, state || {}));\n    const checkHeight = val =>\n      assert.equal(\n        wrapper.find(\".section-body\").instance().style.maxHeight,\n        val\n      );\n\n    it(\"should have no max-height normally to avoid unexpected cropping\", () => {\n      setState();\n\n      checkHeight(\"\");\n    });\n    it(\"should have a max-height when animating open to a target height\", () => {\n      setState({ isAnimating: true });\n\n      checkHeight(maxHeight);\n    });\n    it(\"should not have a max-height when already collapsed\", () => {\n      setup({ collapsed: true });\n\n      checkHeight(\"\");\n    });\n    it(\"should not have a max-height when animating closed to a css-set 0\", () => {\n      setup({ collapsed: true });\n      setState({ isAnimating: true });\n\n      checkHeight(\"\");\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/content-src/components/ComponentPerfTimer.test.jsx",
    "content": "import { actionCreators as ac, actionTypes as at } from \"common/Actions.jsm\";\nimport { ComponentPerfTimer } from \"content-src/components/ComponentPerfTimer/ComponentPerfTimer\";\nimport createMockRaf from \"mock-raf\";\nimport React from \"react\";\n\nimport { shallow } from \"enzyme\";\n\nconst perfSvc = {\n  mark() {},\n  getMostRecentAbsMarkStartByName() {},\n};\n\nlet DEFAULT_PROPS = {\n  initialized: true,\n  rows: [],\n  id: \"highlights\",\n  dispatch() {},\n  perfSvc,\n};\n\ndescribe(\"<ComponentPerfTimer>\", () => {\n  let mockRaf;\n  let sandbox;\n  let wrapper;\n\n  const InnerEl = () => <div>Inner Element</div>;\n\n  beforeEach(() => {\n    mockRaf = createMockRaf();\n    sandbox = sinon.createSandbox();\n    sandbox.stub(window, \"requestAnimationFrame\").callsFake(mockRaf.raf);\n    wrapper = shallow(\n      <ComponentPerfTimer {...DEFAULT_PROPS}>\n        <InnerEl />\n      </ComponentPerfTimer>\n    );\n  });\n  afterEach(() => {\n    sandbox.restore();\n  });\n\n  it(\"should render props.children\", () => {\n    assert.ok(wrapper.contains(<InnerEl />));\n  });\n\n  describe(\"#constructor\", () => {\n    beforeEach(() => {\n      sandbox.stub(ComponentPerfTimer.prototype, \"_maybeSendBadStateEvent\");\n      sandbox.stub(\n        ComponentPerfTimer.prototype,\n        \"_ensureFirstRenderTsRecorded\"\n      );\n      wrapper = shallow(\n        <ComponentPerfTimer {...DEFAULT_PROPS}>\n          <InnerEl />\n        </ComponentPerfTimer>,\n        { disableLifecycleMethods: true }\n      );\n    });\n\n    it(\"should have the correct defaults\", () => {\n      const instance = wrapper.instance();\n\n      assert.isFalse(instance._reportMissingData);\n      assert.isFalse(instance._timestampHandled);\n      assert.isFalse(instance._recordedFirstRender);\n    });\n  });\n\n  describe(\"#render\", () => {\n    beforeEach(() => {\n      sandbox.stub(DEFAULT_PROPS, \"id\").value(\"fake_section\");\n      sandbox.stub(ComponentPerfTimer.prototype, \"_maybeSendBadStateEvent\");\n      sandbox.stub(\n        ComponentPerfTimer.prototype,\n        \"_ensureFirstRenderTsRecorded\"\n      );\n      wrapper = shallow(\n        <ComponentPerfTimer {...DEFAULT_PROPS}>\n          <InnerEl />\n        </ComponentPerfTimer>\n      );\n    });\n\n    it(\"should not call telemetry on sections that we don't want to record\", () => {\n      const instance = wrapper.instance();\n\n      assert.notCalled(instance._maybeSendBadStateEvent);\n      assert.notCalled(instance._ensureFirstRenderTsRecorded);\n    });\n  });\n\n  describe(\"#_componentDidMount\", () => {\n    it(\"should call _maybeSendPaintedEvent\", () => {\n      const instance = wrapper.instance();\n      const stub = sandbox.stub(instance, \"_maybeSendPaintedEvent\");\n\n      instance.componentDidMount();\n\n      assert.calledOnce(stub);\n    });\n\n    it(\"should not call _maybeSendPaintedEvent if id not in RECORDED_SECTIONS\", () => {\n      sandbox.stub(DEFAULT_PROPS, \"id\").value(\"topstories\");\n      wrapper = shallow(\n        <ComponentPerfTimer {...DEFAULT_PROPS}>\n          <InnerEl />\n        </ComponentPerfTimer>\n      );\n      const instance = wrapper.instance();\n      const stub = sandbox.stub(instance, \"_maybeSendPaintedEvent\");\n\n      instance.componentDidMount();\n\n      assert.notCalled(stub);\n    });\n  });\n\n  describe(\"#_componentDidUpdate\", () => {\n    it(\"should call _maybeSendPaintedEvent\", () => {\n      const instance = wrapper.instance();\n      const maybeSendPaintStub = sandbox.stub(\n        instance,\n        \"_maybeSendPaintedEvent\"\n      );\n\n      instance.componentDidUpdate();\n\n      assert.calledOnce(maybeSendPaintStub);\n    });\n\n    it(\"should not call _maybeSendPaintedEvent if id not in RECORDED_SECTIONS\", () => {\n      sandbox.stub(DEFAULT_PROPS, \"id\").value(\"topstories\");\n      wrapper = shallow(\n        <ComponentPerfTimer {...DEFAULT_PROPS}>\n          <InnerEl />\n        </ComponentPerfTimer>\n      );\n      const instance = wrapper.instance();\n      const stub = sandbox.stub(instance, \"_maybeSendPaintedEvent\");\n\n      instance.componentDidUpdate();\n\n      assert.notCalled(stub);\n    });\n  });\n\n  describe(\"_ensureFirstRenderTsRecorded\", () => {\n    let recordFirstRenderStub;\n    beforeEach(() => {\n      sandbox.stub(ComponentPerfTimer.prototype, \"_maybeSendBadStateEvent\");\n      recordFirstRenderStub = sandbox.stub(\n        ComponentPerfTimer.prototype,\n        \"_ensureFirstRenderTsRecorded\"\n      );\n    });\n\n    it(\"should set _recordedFirstRender\", () => {\n      sandbox.stub(DEFAULT_PROPS, \"initialized\").value(false);\n      wrapper = shallow(\n        <ComponentPerfTimer {...DEFAULT_PROPS}>\n          <InnerEl />\n        </ComponentPerfTimer>\n      );\n      const instance = wrapper.instance();\n\n      assert.isFalse(instance._recordedFirstRender);\n\n      recordFirstRenderStub.callThrough();\n      instance._ensureFirstRenderTsRecorded();\n\n      assert.isTrue(instance._recordedFirstRender);\n    });\n\n    it(\"should mark first_render_ts\", () => {\n      sandbox.stub(DEFAULT_PROPS, \"initialized\").value(false);\n      wrapper = shallow(\n        <ComponentPerfTimer {...DEFAULT_PROPS}>\n          <InnerEl />\n        </ComponentPerfTimer>\n      );\n      const instance = wrapper.instance();\n      const stub = sandbox.stub(perfSvc, \"mark\");\n\n      recordFirstRenderStub.callThrough();\n      instance._ensureFirstRenderTsRecorded();\n\n      assert.calledOnce(stub);\n      assert.calledWithExactly(stub, `${DEFAULT_PROPS.id}_first_render_ts`);\n    });\n  });\n\n  describe(\"#_maybeSendBadStateEvent\", () => {\n    let sendBadStateStub;\n    beforeEach(() => {\n      sendBadStateStub = sandbox.stub(\n        ComponentPerfTimer.prototype,\n        \"_maybeSendBadStateEvent\"\n      );\n      sandbox.stub(\n        ComponentPerfTimer.prototype,\n        \"_ensureFirstRenderTsRecorded\"\n      );\n    });\n\n    it(\"should set this._reportMissingData=true when called with initialized === false\", () => {\n      sandbox.stub(DEFAULT_PROPS, \"initialized\").value(false);\n      wrapper = shallow(\n        <ComponentPerfTimer {...DEFAULT_PROPS}>\n          <InnerEl />\n        </ComponentPerfTimer>\n      );\n      const instance = wrapper.instance();\n\n      assert.isFalse(instance._reportMissingData);\n\n      sendBadStateStub.callThrough();\n      instance._maybeSendBadStateEvent();\n\n      assert.isTrue(instance._reportMissingData);\n    });\n\n    it(\"should call _sendBadStateEvent if initialized & other metrics have been recorded\", () => {\n      const instance = wrapper.instance();\n      const stub = sandbox.stub(instance, \"_sendBadStateEvent\");\n      instance._reportMissingData = true;\n      instance._timestampHandled = true;\n      instance._recordedFirstRender = true;\n\n      sendBadStateStub.callThrough();\n      instance._maybeSendBadStateEvent();\n\n      assert.calledOnce(stub);\n      assert.isFalse(instance._reportMissingData);\n    });\n  });\n\n  describe(\"#_maybeSendPaintedEvent\", () => {\n    it(\"should call _sendPaintedEvent if props.initialized is true\", () => {\n      sandbox.stub(DEFAULT_PROPS, \"initialized\").value(true);\n      wrapper = shallow(\n        <ComponentPerfTimer {...DEFAULT_PROPS}>\n          <InnerEl />\n        </ComponentPerfTimer>,\n        { disableLifecycleMethods: true }\n      );\n      const instance = wrapper.instance();\n      const stub = sandbox.stub(instance, \"_afterFramePaint\");\n\n      assert.isFalse(instance._timestampHandled);\n\n      instance._maybeSendPaintedEvent();\n\n      assert.calledOnce(stub);\n      assert.calledWithExactly(stub, instance._sendPaintedEvent);\n      assert.isTrue(wrapper.instance()._timestampHandled);\n    });\n    it(\"should not call _sendPaintedEvent if this._timestampHandled is true\", () => {\n      const instance = wrapper.instance();\n      const spy = sinon.spy(instance, \"_afterFramePaint\");\n      instance._timestampHandled = true;\n\n      instance._maybeSendPaintedEvent();\n      spy.neverCalledWith(instance._sendPaintedEvent);\n    });\n    it(\"should not call _sendPaintedEvent if component not initialized\", () => {\n      sandbox.stub(DEFAULT_PROPS, \"initialized\").value(false);\n      wrapper = shallow(\n        <ComponentPerfTimer {...DEFAULT_PROPS}>\n          <InnerEl />\n        </ComponentPerfTimer>\n      );\n      const instance = wrapper.instance();\n      const spy = sinon.spy(instance, \"_afterFramePaint\");\n\n      instance._maybeSendPaintedEvent();\n\n      spy.neverCalledWith(instance._sendPaintedEvent);\n    });\n  });\n\n  describe(\"#_afterFramePaint\", () => {\n    it(\"should call callback after the requestAnimationFrame callback returns\", () =>\n      new Promise(resolve => {\n        // Setting the callback to resolve is the test that it does finally get\n        // called at the correct time, after the event loop ticks again.\n        // If it doesn't get called, this test will time out.\n        const callback = sandbox.spy(resolve);\n\n        const instance = wrapper.instance();\n\n        instance._afterFramePaint(callback);\n\n        assert.notCalled(callback);\n        mockRaf.step({ count: 1 });\n      }));\n  });\n\n  describe(\"#_sendBadStateEvent\", () => {\n    it(\"should call perfSvc.mark\", () => {\n      sandbox.spy(perfSvc, \"mark\");\n      const key = `${DEFAULT_PROPS.id}_data_ready_ts`;\n\n      wrapper.instance()._sendBadStateEvent();\n\n      assert.calledOnce(perfSvc.mark);\n      assert.calledWithExactly(perfSvc.mark, key);\n    });\n\n    it(\"should call compute the delta from first render to data ready\", () => {\n      sandbox.stub(perfSvc, \"getMostRecentAbsMarkStartByName\");\n\n      wrapper\n        .instance()\n        ._sendBadStateEvent(`${DEFAULT_PROPS.id}_data_ready_ts`);\n\n      assert.calledTwice(perfSvc.getMostRecentAbsMarkStartByName);\n      assert.calledWithExactly(\n        perfSvc.getMostRecentAbsMarkStartByName,\n        `${DEFAULT_PROPS.id}_data_ready_ts`\n      );\n      assert.calledWithExactly(\n        perfSvc.getMostRecentAbsMarkStartByName,\n        `${DEFAULT_PROPS.id}_first_render_ts`\n      );\n    });\n\n    it(\"should call dispatch SAVE_SESSION_PERF_DATA\", () => {\n      sandbox\n        .stub(perfSvc, \"getMostRecentAbsMarkStartByName\")\n        .withArgs(\"highlights_first_render_ts\")\n        .returns(0.5)\n        .withArgs(\"highlights_data_ready_ts\")\n        .returns(3.2);\n\n      const dispatch = sandbox.spy(DEFAULT_PROPS, \"dispatch\");\n      wrapper = shallow(\n        <ComponentPerfTimer {...DEFAULT_PROPS}>\n          <InnerEl />\n        </ComponentPerfTimer>\n      );\n\n      wrapper.instance()._sendBadStateEvent();\n\n      assert.calledOnce(dispatch);\n      assert.calledWithExactly(\n        dispatch,\n        ac.OnlyToMain({\n          type: at.SAVE_SESSION_PERF_DATA,\n          data: { [`${DEFAULT_PROPS.id}_data_late_by_ms`]: 2 },\n        })\n      );\n    });\n  });\n\n  describe(\"#_sendPaintedEvent\", () => {\n    beforeEach(() => {\n      sandbox.stub(ComponentPerfTimer.prototype, \"_maybeSendBadStateEvent\");\n      sandbox.stub(\n        ComponentPerfTimer.prototype,\n        \"_ensureFirstRenderTsRecorded\"\n      );\n    });\n\n    it(\"should not call mark with the wrong id\", () => {\n      sandbox.stub(perfSvc, \"mark\");\n      sandbox.stub(DEFAULT_PROPS, \"id\").value(\"fake_id\");\n      wrapper = shallow(\n        <ComponentPerfTimer {...DEFAULT_PROPS}>\n          <InnerEl />\n        </ComponentPerfTimer>\n      );\n\n      wrapper.instance()._sendPaintedEvent();\n\n      assert.notCalled(perfSvc.mark);\n    });\n    it(\"should call mark with the correct topsites\", () => {\n      sandbox.stub(perfSvc, \"mark\");\n      sandbox.stub(DEFAULT_PROPS, \"id\").value(\"topsites\");\n      wrapper = shallow(\n        <ComponentPerfTimer {...DEFAULT_PROPS}>\n          <InnerEl />\n        </ComponentPerfTimer>\n      );\n\n      wrapper.instance()._sendPaintedEvent();\n\n      assert.calledOnce(perfSvc.mark);\n      assert.calledWithExactly(perfSvc.mark, \"topsites_first_painted_ts\");\n    });\n    it(\"should not call getMostRecentAbsMarkStartByName if id!=topsites\", () => {\n      sandbox.stub(perfSvc, \"getMostRecentAbsMarkStartByName\");\n      sandbox.stub(DEFAULT_PROPS, \"id\").value(\"fake_id\");\n      wrapper = shallow(\n        <ComponentPerfTimer {...DEFAULT_PROPS}>\n          <InnerEl />\n        </ComponentPerfTimer>\n      );\n\n      wrapper.instance()._sendPaintedEvent();\n\n      assert.notCalled(perfSvc.getMostRecentAbsMarkStartByName);\n    });\n    it(\"should call getMostRecentAbsMarkStartByName for topsites\", () => {\n      sandbox.stub(perfSvc, \"getMostRecentAbsMarkStartByName\");\n      sandbox.stub(DEFAULT_PROPS, \"id\").value(\"topsites\");\n      wrapper = shallow(\n        <ComponentPerfTimer {...DEFAULT_PROPS}>\n          <InnerEl />\n        </ComponentPerfTimer>\n      );\n\n      wrapper.instance()._sendPaintedEvent();\n\n      assert.calledOnce(perfSvc.getMostRecentAbsMarkStartByName);\n      assert.calledWithExactly(\n        perfSvc.getMostRecentAbsMarkStartByName,\n        \"topsites_first_painted_ts\"\n      );\n    });\n    it(\"should dispatch SAVE_SESSION_PERF_DATA\", () => {\n      sandbox.stub(perfSvc, \"getMostRecentAbsMarkStartByName\").returns(42);\n      sandbox.stub(DEFAULT_PROPS, \"id\").value(\"topsites\");\n      const dispatch = sandbox.spy(DEFAULT_PROPS, \"dispatch\");\n      wrapper = shallow(\n        <ComponentPerfTimer {...DEFAULT_PROPS}>\n          <InnerEl />\n        </ComponentPerfTimer>\n      );\n\n      wrapper.instance()._sendPaintedEvent();\n\n      assert.calledOnce(dispatch);\n      assert.calledWithExactly(\n        dispatch,\n        ac.OnlyToMain({\n          type: at.SAVE_SESSION_PERF_DATA,\n          data: { topsites_first_painted_ts: 42 },\n        })\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/content-src/components/ConfirmDialog.test.jsx",
    "content": "import { actionCreators as ac, actionTypes as at } from \"common/Actions.jsm\";\nimport { _ConfirmDialog as ConfirmDialog } from \"content-src/components/ConfirmDialog/ConfirmDialog\";\nimport React from \"react\";\nimport { shallow } from \"enzyme\";\n\ndescribe(\"<ConfirmDialog>\", () => {\n  let wrapper;\n  let dispatch;\n  let ConfirmDialogProps;\n  beforeEach(() => {\n    dispatch = sinon.stub();\n    ConfirmDialogProps = {\n      visible: true,\n      data: {\n        onConfirm: [],\n        cancel_button_string_id: \"newtab-topsites-delete-history-button\",\n        confirm_button_string_id: \"newtab-topsites-cancel-button\",\n        eventSource: \"HIGHLIGHTS\",\n      },\n    };\n    wrapper = shallow(\n      <ConfirmDialog dispatch={dispatch} {...ConfirmDialogProps} />\n    );\n  });\n  it(\"should render an overlay\", () => {\n    assert.ok(wrapper.find(\".modal-overlay\").exists());\n  });\n  it(\"should render a modal\", () => {\n    assert.ok(wrapper.find(\".confirmation-dialog\").exists());\n  });\n  it(\"should not render if visible is false\", () => {\n    ConfirmDialogProps.visible = false;\n    wrapper = shallow(\n      <ConfirmDialog dispatch={dispatch} {...ConfirmDialogProps} />\n    );\n\n    assert.lengthOf(wrapper.find(\".confirmation-dialog\"), 0);\n  });\n  it(\"should display an icon if we provide one in props\", () => {\n    const iconName = \"modal-icon\";\n    // If there is no icon in the props, we shouldn't display an icon\n    assert.lengthOf(wrapper.find(`.icon-${iconName}`), 0);\n\n    ConfirmDialogProps.data.icon = iconName;\n    wrapper = shallow(\n      <ConfirmDialog dispatch={dispatch} {...ConfirmDialogProps} />\n    );\n\n    // But if we do provide an icon - we should show it\n    assert.lengthOf(wrapper.find(`.icon-${iconName}`), 1);\n  });\n  describe(\"fluent message check\", () => {\n    it(\"should render the message body sent via props\", () => {\n      Object.assign(ConfirmDialogProps.data, {\n        body_string_id: [\"foo\", \"bar\"],\n      });\n      wrapper = shallow(\n        <ConfirmDialog dispatch={dispatch} {...ConfirmDialogProps} />\n      );\n      let msgs = wrapper.find(\".modal-message\").find(\"p\");\n      assert.equal(msgs.length, ConfirmDialogProps.data.body_string_id.length);\n      msgs.forEach((fm, i) =>\n        assert.equal(\n          fm.prop(\"data-l10n-id\"),\n          ConfirmDialogProps.data.body_string_id[i]\n        )\n      );\n    });\n    it(\"should render the correct primary button text\", () => {\n      Object.assign(ConfirmDialogProps.data, {\n        confirm_button_string_id: \"primary_foo\",\n      });\n      wrapper = shallow(\n        <ConfirmDialog dispatch={dispatch} {...ConfirmDialogProps} />\n      );\n\n      let doneLabel = wrapper.find(\".actions\").childAt(1);\n      assert.ok(doneLabel.exists());\n      assert.equal(\n        doneLabel.prop(\"data-l10n-id\"),\n        ConfirmDialogProps.data.confirm_button_string_id\n      );\n    });\n  });\n  describe(\"click events\", () => {\n    it(\"should emit AlsoToMain DIALOG_CANCEL when you click the overlay\", () => {\n      let overlay = wrapper.find(\".modal-overlay\");\n\n      assert.ok(overlay.exists());\n      overlay.simulate(\"click\");\n\n      // Two events are emitted: UserEvent+AlsoToMain.\n      assert.calledTwice(dispatch);\n      assert.propertyVal(dispatch.firstCall.args[0], \"type\", at.DIALOG_CANCEL);\n      assert.calledWith(dispatch, { type: at.DIALOG_CANCEL });\n    });\n    it(\"should emit UserEvent DIALOG_CANCEL when you click the overlay\", () => {\n      let overlay = wrapper.find(\".modal-overlay\");\n\n      assert.ok(overlay);\n      overlay.simulate(\"click\");\n\n      // Two events are emitted: UserEvent+AlsoToMain.\n      assert.calledTwice(dispatch);\n      assert.isUserEventAction(dispatch.secondCall.args[0]);\n      assert.calledWith(\n        dispatch,\n        ac.UserEvent({ event: at.DIALOG_CANCEL, source: \"HIGHLIGHTS\" })\n      );\n    });\n    it(\"should emit AlsoToMain DIALOG_CANCEL on cancel\", () => {\n      let cancelButton = wrapper.find(\".actions\").childAt(0);\n\n      assert.ok(cancelButton);\n      cancelButton.simulate(\"click\");\n\n      // Two events are emitted: UserEvent+AlsoToMain.\n      assert.calledTwice(dispatch);\n      assert.propertyVal(dispatch.firstCall.args[0], \"type\", at.DIALOG_CANCEL);\n      assert.calledWith(dispatch, { type: at.DIALOG_CANCEL });\n    });\n    it(\"should emit UserEvent DIALOG_CANCEL on cancel\", () => {\n      let cancelButton = wrapper.find(\".actions\").childAt(0);\n\n      assert.ok(cancelButton);\n      cancelButton.simulate(\"click\");\n\n      // Two events are emitted: UserEvent+AlsoToMain.\n      assert.calledTwice(dispatch);\n      assert.isUserEventAction(dispatch.secondCall.args[0]);\n      assert.calledWith(\n        dispatch,\n        ac.UserEvent({ event: at.DIALOG_CANCEL, source: \"HIGHLIGHTS\" })\n      );\n    });\n    it(\"should emit UserEvent on primary button\", () => {\n      Object.assign(ConfirmDialogProps.data, {\n        body_string_id: [\"foo\", \"bar\"],\n        onConfirm: [\n          ac.AlsoToMain({ type: at.DELETE_URL, data: \"foo.bar\" }),\n          ac.UserEvent({ event: \"DELETE\" }),\n        ],\n      });\n      wrapper = shallow(\n        <ConfirmDialog dispatch={dispatch} {...ConfirmDialogProps} />\n      );\n      let doneButton = wrapper.find(\".actions\").childAt(1);\n\n      assert.ok(doneButton);\n      doneButton.simulate(\"click\");\n\n      // Two events are emitted: UserEvent+AlsoToMain.\n      assert.isUserEventAction(dispatch.secondCall.args[0]);\n\n      assert.calledTwice(dispatch);\n      assert.calledWith(dispatch, ConfirmDialogProps.data.onConfirm[1]);\n    });\n    it(\"should emit AlsoToMain on primary button\", () => {\n      Object.assign(ConfirmDialogProps.data, {\n        body_string_id: [\"foo\", \"bar\"],\n        onConfirm: [\n          ac.AlsoToMain({ type: at.DELETE_URL, data: \"foo.bar\" }),\n          ac.UserEvent({ event: \"DELETE\" }),\n        ],\n      });\n      wrapper = shallow(\n        <ConfirmDialog dispatch={dispatch} {...ConfirmDialogProps} />\n      );\n      let doneButton = wrapper.find(\".actions\").childAt(1);\n\n      assert.ok(doneButton);\n      doneButton.simulate(\"click\");\n\n      // Two events are emitted: UserEvent+AlsoToMain.\n      assert.calledTwice(dispatch);\n      assert.calledWith(dispatch, ConfirmDialogProps.data.onConfirm[0]);\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/content-src/components/ContextMenu.test.jsx",
    "content": "import {\n  ContextMenu,\n  ContextMenuItem,\n} from \"content-src/components/ContextMenu/ContextMenu\";\nimport { ContextMenuButton } from \"content-src/components/ContextMenu/ContextMenuButton\";\nimport { mount, shallow } from \"enzyme\";\nimport React from \"react\";\n\nconst DEFAULT_PROPS = {\n  onUpdate: () => {},\n  options: [],\n  tabbableOptionsLength: 0,\n};\n\nconst DEFAULT_MENU_OPTIONS = [\n  \"MoveUp\",\n  \"MoveDown\",\n  \"Separator\",\n  \"RemoveSection\",\n  \"CheckCollapsed\",\n  \"Separator\",\n  \"ManageSection\",\n];\n\nconst FakeMenu = props => {\n  return <div>{props.children}</div>;\n};\n\ndescribe(\"<ContextMenuButton>\", () => {\n  let sandbox;\n  beforeEach(() => {\n    sandbox = sinon.createSandbox();\n  });\n  afterEach(() => {\n    sandbox.restore();\n  });\n  it(\"should call onUpdate when clicked\", () => {\n    const onUpdate = sandbox.spy();\n    const wrapper = mount(\n      <ContextMenuButton onUpdate={onUpdate}>\n        <FakeMenu />\n      </ContextMenuButton>\n    );\n    wrapper.find(\".context-menu-button\").simulate(\"click\");\n    assert.calledOnce(onUpdate);\n  });\n  it(\"should call onUpdate when activated with Enter\", () => {\n    const onUpdate = sandbox.spy();\n    const wrapper = mount(\n      <ContextMenuButton onUpdate={onUpdate}>\n        <FakeMenu />\n      </ContextMenuButton>\n    );\n    wrapper.find(\".context-menu-button\").simulate(\"keydown\", { key: \"Enter\" });\n    assert.calledOnce(onUpdate);\n  });\n  it(\"should call onClick\", () => {\n    const onClick = sandbox.spy(ContextMenuButton.prototype, \"onClick\");\n    const wrapper = mount(\n      <ContextMenuButton>\n        <FakeMenu />\n      </ContextMenuButton>\n    );\n    wrapper.find(\"button\").simulate(\"click\");\n    assert.calledOnce(onClick);\n  });\n  it(\"should have a default keyboardAccess prop of false\", () => {\n    const wrapper = mount(\n      <ContextMenuButton>\n        <ContextMenu options={DEFAULT_MENU_OPTIONS} />\n      </ContextMenuButton>\n    );\n    wrapper.setState({ showContextMenu: true });\n    assert.equal(wrapper.find(ContextMenu).prop(\"keyboardAccess\"), false);\n  });\n  it(\"should pass the keyboardAccess prop down to ContextMenu\", () => {\n    const wrapper = mount(\n      <ContextMenuButton>\n        <ContextMenu options={DEFAULT_MENU_OPTIONS} />\n      </ContextMenuButton>\n    );\n    wrapper.setState({ showContextMenu: true, contextMenuKeyboard: true });\n    assert.equal(wrapper.find(ContextMenu).prop(\"keyboardAccess\"), true);\n  });\n  it(\"should call focusFirst when keyboardAccess is true\", () => {\n    const wrapper = mount(\n      <ContextMenuButton>\n        <ContextMenu options={[{ label: \"item1\", first: true }]} />\n      </ContextMenuButton>\n    );\n    const focusFirst = sandbox.spy(ContextMenuItem.prototype, \"focusFirst\");\n    wrapper.setState({ showContextMenu: true, contextMenuKeyboard: true });\n    assert.calledOnce(focusFirst);\n  });\n});\n\ndescribe(\"<ContextMenu>\", () => {\n  it(\"should render all the options provided\", () => {\n    const options = [\n      { label: \"item1\" },\n      { type: \"separator\" },\n      { label: \"item2\" },\n    ];\n    const wrapper = shallow(\n      <ContextMenu {...DEFAULT_PROPS} options={options} />\n    );\n    assert.lengthOf(wrapper.find(\".context-menu-list\").children(), 3);\n  });\n  it(\"should not add a link for a separator\", () => {\n    const options = [{ label: \"item1\" }, { type: \"separator\" }];\n    const wrapper = shallow(\n      <ContextMenu {...DEFAULT_PROPS} options={options} />\n    );\n    assert.lengthOf(wrapper.find(\".separator\"), 1);\n  });\n  it(\"should add a link for all types that are not separators\", () => {\n    const options = [{ label: \"item1\" }, { type: \"separator\" }];\n    const wrapper = shallow(\n      <ContextMenu {...DEFAULT_PROPS} options={options} />\n    );\n    assert.lengthOf(wrapper.find(ContextMenuItem), 1);\n  });\n  it(\"should add an icon to items that need icons\", () => {\n    const options = [{ label: \"item1\", icon: \"icon1\" }, { type: \"separator\" }];\n    const wrapper = mount(<ContextMenu {...DEFAULT_PROPS} options={options} />);\n    assert.lengthOf(wrapper.find(\".icon-icon1\"), 1);\n  });\n  it(\"should be tabbable\", () => {\n    const options = [{ label: \"item1\", icon: \"icon1\" }, { type: \"separator\" }];\n    const wrapper = mount(<ContextMenu {...DEFAULT_PROPS} options={options} />);\n    assert.equal(\n      wrapper.find(\".context-menu-item\").props().role,\n      \"presentation\"\n    );\n  });\n  it(\"should call onUpdate with false when an option is clicked\", () => {\n    const onUpdate = sinon.spy();\n    const onClick = sinon.spy();\n    const wrapper = mount(\n      <ContextMenu\n        {...DEFAULT_PROPS}\n        onUpdate={onUpdate}\n        options={[{ label: \"item1\", onClick }]}\n      />\n    );\n    wrapper.find(\".context-menu-item button\").simulate(\"click\");\n    assert.calledOnce(onUpdate);\n    assert.calledOnce(onClick);\n  });\n  it(\"should not have disabled className by default\", () => {\n    const options = [{ label: \"item1\", icon: \"icon1\" }, { type: \"separator\" }];\n    const wrapper = mount(<ContextMenu {...DEFAULT_PROPS} options={options} />);\n    assert.lengthOf(wrapper.find(\".context-menu-item a.disabled\"), 0);\n  });\n  it(\"should add disabled className to any disabled options\", () => {\n    const options = [\n      { label: \"item1\", icon: \"icon1\", disabled: true },\n      { type: \"separator\" },\n    ];\n    const wrapper = mount(<ContextMenu {...DEFAULT_PROPS} options={options} />);\n    assert.lengthOf(wrapper.find(\".context-menu-item button.disabled\"), 1);\n  });\n  it(\"should have the context-menu-item class\", () => {\n    const options = [{ label: \"item1\", icon: \"icon1\" }];\n    const wrapper = mount(<ContextMenu {...DEFAULT_PROPS} options={options} />);\n    assert.lengthOf(wrapper.find(\".context-menu-item\"), 1);\n  });\n  it(\"should call onClick when onKeyDown is called with Enter\", () => {\n    const onClick = sinon.spy();\n    const wrapper = mount(\n      <ContextMenu {...DEFAULT_PROPS} options={[{ label: \"item1\", onClick }]} />\n    );\n    wrapper\n      .find(\".context-menu-item button\")\n      .simulate(\"keydown\", { key: \"Enter\" });\n    assert.calledOnce(onClick);\n  });\n  it(\"should call focusSibling when onKeyDown is called with ArrowUp\", () => {\n    const wrapper = mount(\n      <ContextMenu {...DEFAULT_PROPS} options={[{ label: \"item1\" }]} />\n    );\n    const focusSibling = sinon.stub(\n      wrapper.find(ContextMenuItem).instance(),\n      \"focusSibling\"\n    );\n    wrapper\n      .find(\".context-menu-item button\")\n      .simulate(\"keydown\", { key: \"ArrowUp\" });\n    assert.calledOnce(focusSibling);\n  });\n  it(\"should call focusSibling when onKeyDown is called with ArrowDown\", () => {\n    const wrapper = mount(\n      <ContextMenu {...DEFAULT_PROPS} options={[{ label: \"item1\" }]} />\n    );\n    const focusSibling = sinon.stub(\n      wrapper.find(ContextMenuItem).instance(),\n      \"focusSibling\"\n    );\n    wrapper\n      .find(\".context-menu-item button\")\n      .simulate(\"keydown\", { key: \"ArrowDown\" });\n    assert.calledOnce(focusSibling);\n  });\n});\n"
  },
  {
    "path": "test/unit/content-src/components/DiscoveryStreamBase.test.jsx",
    "content": "import {\n  _DiscoveryStreamBase as DiscoveryStreamBase,\n  isAllowedCSS,\n} from \"content-src/components/DiscoveryStreamBase/DiscoveryStreamBase\";\nimport { GlobalOverrider } from \"test/unit/utils\";\nimport { CardGrid } from \"content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid\";\nimport { CollapsibleSection } from \"content-src/components/CollapsibleSection/CollapsibleSection\";\nimport { DSMessage } from \"content-src/components/DiscoveryStreamComponents/DSMessage/DSMessage\";\nimport { Hero } from \"content-src/components/DiscoveryStreamComponents/Hero/Hero\";\nimport { HorizontalRule } from \"content-src/components/DiscoveryStreamComponents/HorizontalRule/HorizontalRule\";\nimport { List } from \"content-src/components/DiscoveryStreamComponents/List/List\";\nimport { Navigation } from \"content-src/components/DiscoveryStreamComponents/Navigation/Navigation\";\nimport React from \"react\";\nimport { shallow } from \"enzyme\";\nimport { SectionTitle } from \"content-src/components/DiscoveryStreamComponents/SectionTitle/SectionTitle\";\nimport { TopSites } from \"content-src/components/DiscoveryStreamComponents/TopSites/TopSites\";\n\ndescribe(\"<isAllowedCSS>\", () => {\n  it(\"should allow colors\", () => {\n    assert.isTrue(isAllowedCSS(\"color\", \"red\"));\n  });\n\n  it(\"should allow resource urls\", () => {\n    assert.isTrue(\n      isAllowedCSS(\n        \"background-image\",\n        `url(\"resource://activity-stream/data/content/assets/glyph-info-16.svg\")`\n      )\n    );\n  });\n\n  it(\"should allow chrome urls\", () => {\n    assert.isTrue(\n      isAllowedCSS(\n        \"background-image\",\n        `url(\"chrome://browser/skin/history.svg\")`\n      )\n    );\n  });\n\n  it(\"should allow allowed https urls\", () => {\n    assert.isTrue(\n      isAllowedCSS(\n        \"background-image\",\n        `url(\"https://img-getpocket.cdn.mozilla.net/media/image.png\")`\n      )\n    );\n  });\n\n  it(\"should disallow other https urls\", () => {\n    assert.isFalse(\n      isAllowedCSS(\n        \"background-image\",\n        `url(\"https://mozilla.org/media/image.png\")`\n      )\n    );\n  });\n\n  it(\"should disallow other protocols\", () => {\n    assert.isFalse(\n      isAllowedCSS(\n        \"background-image\",\n        `url(\"ftp://mozilla.org/media/image.png\")`\n      )\n    );\n  });\n\n  it(\"should allow allowed multiple valid urls\", () => {\n    assert.isTrue(\n      isAllowedCSS(\n        \"background-image\",\n        `url(\"https://img-getpocket.cdn.mozilla.net/media/image.png\"), url(\"chrome://browser/skin/history.svg\")`\n      )\n    );\n  });\n\n  it(\"should disallow if any invaild\", () => {\n    assert.isFalse(\n      isAllowedCSS(\n        \"background-image\",\n        `url(\"chrome://browser/skin/history.svg\"), url(\"ftp://mozilla.org/media/image.png\")`\n      )\n    );\n  });\n});\n\ndescribe(\"<DiscoveryStreamBase>\", () => {\n  let wrapper;\n  let globals;\n  let sandbox;\n\n  function mountComponent(props = {}) {\n    const defaultProps = {\n      config: { collapsible: true },\n      layout: [],\n      feeds: { loaded: true },\n      spocs: {\n        loaded: true,\n        data: { spocs: null },\n      },\n      ...props,\n    };\n    return shallow(\n      <DiscoveryStreamBase\n        DiscoveryStream={defaultProps}\n        Prefs={{\n          values: {\n            \"feeds.section.topstories\": true,\n            \"feeds.topsites\": true,\n          },\n        }}\n        document={{\n          documentElement: { lang: \"en-US\" },\n        }}\n        Sections={[\n          {\n            id: \"topstories\",\n            learnMore: { link: {} },\n            pref: {},\n          },\n        ]}\n      />\n    );\n  }\n\n  beforeEach(() => {\n    globals = new GlobalOverrider();\n    sandbox = sinon.createSandbox();\n    wrapper = mountComponent();\n  });\n\n  afterEach(() => {\n    sandbox.restore();\n    globals.restore();\n  });\n\n  it(\"should render something if spocs are not loaded\", () => {\n    wrapper = mountComponent({\n      spocs: { loaded: false, data: { spocs: null } },\n    });\n\n    assert.notEqual(wrapper.type(), null);\n  });\n\n  it(\"should render something if feeds are not loaded\", () => {\n    wrapper = mountComponent({ feeds: { loaded: false } });\n\n    assert.notEqual(wrapper.type(), null);\n  });\n\n  it(\"should render nothing with no layout\", () => {\n    assert.ok(wrapper.exists());\n    assert.isEmpty(wrapper.children());\n  });\n\n  it(\"should render a HorizontalRule component\", () => {\n    wrapper = mountComponent({\n      layout: [{ components: [{ type: \"HorizontalRule\" }] }],\n    });\n\n    assert.equal(\n      wrapper\n        .find(\".ds-column-grid div\")\n        .children()\n        .at(0)\n        .type(),\n      HorizontalRule\n    );\n  });\n\n  it(\"should render a List component\", () => {\n    wrapper = mountComponent({\n      layout: [{ components: [{ properties: {}, type: \"List\" }] }],\n    });\n\n    assert.equal(\n      wrapper\n        .find(\".ds-column-grid div\")\n        .children()\n        .at(0)\n        .type(),\n      List\n    );\n  });\n\n  it(\"should render a Hero component\", () => {\n    wrapper = mountComponent({\n      layout: [{ components: [{ properties: {}, type: \"Hero\" }] }],\n    });\n\n    assert.equal(\n      wrapper\n        .find(\".ds-column-grid div\")\n        .children()\n        .at(0)\n        .type(),\n      Hero\n    );\n  });\n\n  it(\"should render a CardGrid component\", () => {\n    wrapper = mountComponent({\n      layout: [{ components: [{ properties: {}, type: \"CardGrid\" }] }],\n    });\n\n    assert.equal(\n      wrapper\n        .find(\".ds-column-grid div\")\n        .children()\n        .at(0)\n        .type(),\n      CardGrid\n    );\n  });\n\n  it(\"should render a Navigation component\", () => {\n    wrapper = mountComponent({\n      layout: [{ components: [{ properties: {}, type: \"Navigation\" }] }],\n    });\n\n    assert.equal(\n      wrapper\n        .find(\".ds-column-grid div\")\n        .children()\n        .at(0)\n        .type(),\n      Navigation\n    );\n  });\n\n  it(\"should render nothing if there was only a Message\", () => {\n    wrapper = mountComponent({\n      layout: [\n        { components: [{ header: {}, properties: {}, type: \"Message\" }] },\n      ],\n    });\n\n    assert.isEmpty(wrapper.children());\n  });\n\n  it(\"should render a regular Message when not collapsible\", () => {\n    wrapper = mountComponent({\n      config: { collapsible: false },\n      layout: [\n        { components: [{ header: {}, properties: {}, type: \"Message\" }] },\n      ],\n    });\n\n    assert.equal(\n      wrapper\n        .find(\".ds-column-grid div\")\n        .children()\n        .at(0)\n        .type(),\n      DSMessage\n    );\n  });\n\n  it(\"should convert first Message component to CollapsibleSection\", () => {\n    wrapper = mountComponent({\n      layout: [\n        {\n          components: [\n            { header: {}, properties: {}, type: \"Message\" },\n            { type: \"HorizontalRule\" },\n          ],\n        },\n      ],\n    });\n\n    assert.equal(\n      wrapper\n        .children()\n        .at(0)\n        .type(),\n      CollapsibleSection\n    );\n  });\n\n  it(\"should render a Message component\", () => {\n    wrapper = mountComponent({\n      layout: [\n        {\n          components: [\n            { header: {}, type: \"Message\" },\n            { properties: {}, type: \"Message\" },\n          ],\n        },\n      ],\n    });\n\n    assert.equal(\n      wrapper\n        .find(\".ds-column-grid div\")\n        .children()\n        .at(0)\n        .type(),\n      DSMessage\n    );\n  });\n\n  it(\"should render a SectionTitle component\", () => {\n    wrapper = mountComponent({\n      layout: [{ components: [{ properties: {}, type: \"SectionTitle\" }] }],\n    });\n\n    assert.equal(\n      wrapper\n        .find(\".ds-column-grid div\")\n        .children()\n        .at(0)\n        .type(),\n      SectionTitle\n    );\n  });\n\n  it(\"should render TopSites\", () => {\n    wrapper = mountComponent({\n      layout: [{ components: [{ properties: {}, type: \"TopSites\" }] }],\n    });\n\n    assert.equal(\n      wrapper\n        .find(\".ds-column-grid div\")\n        .children()\n        .at(0)\n        .type(),\n      TopSites\n    );\n  });\n\n  describe(\"#onStyleMount\", () => {\n    let parseStub;\n\n    beforeEach(() => {\n      parseStub = sandbox.stub();\n      globals.set(\"JSON\", { parse: parseStub });\n    });\n\n    afterEach(() => {\n      sandbox.restore();\n      globals.restore();\n    });\n\n    it(\"should return if no style\", () => {\n      assert.isUndefined(wrapper.instance().onStyleMount());\n      assert.notCalled(parseStub);\n    });\n\n    it(\"should insert rules\", () => {\n      const sheetStub = { insertRule: sandbox.stub(), cssRules: [{}] };\n      parseStub.returns([\n        [\n          null,\n          {\n            \".ds-message\": \"margin-bottom: -20px\",\n          },\n          null,\n          null,\n        ],\n      ]);\n      wrapper.instance().onStyleMount({ sheet: sheetStub, dataset: {} });\n\n      assert.calledOnce(sheetStub.insertRule);\n      assert.calledWithExactly(sheetStub.insertRule, \"DUMMY#CSS.SELECTOR {}\");\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/content-src/components/DiscoveryStreamComponents/CardGrid.test.jsx",
    "content": "import { CardGrid } from \"content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid\";\nimport { DSCard } from \"content-src/components/DiscoveryStreamComponents/DSCard/DSCard\";\nimport React from \"react\";\nimport { shallow } from \"enzyme\";\n\ndescribe(\"<CardGrid>\", () => {\n  let wrapper;\n\n  beforeEach(() => {\n    wrapper = shallow(<CardGrid />);\n  });\n\n  it(\"should render an empty div\", () => {\n    assert.ok(wrapper.exists());\n    assert.lengthOf(wrapper.children(), 0);\n  });\n\n  it(\"should render DSCards\", () => {\n    wrapper.setProps({ items: 2, data: { recommendations: [{}, {}] } });\n\n    assert.lengthOf(wrapper.find(\".ds-card-grid\").children(), 2);\n    assert.equal(\n      wrapper\n        .find(\".ds-card-grid\")\n        .children()\n        .at(0)\n        .type(),\n      DSCard\n    );\n  });\n\n  it(\"should add divisible-by-4 to the grid\", () => {\n    wrapper.setProps({ items: 4, data: { recommendations: [{}, {}] } });\n\n    assert.ok(wrapper.find(\".ds-card-grid-divisible-by-4\").exists());\n  });\n\n  it(\"should add divisible-by-3 to the grid\", () => {\n    wrapper.setProps({ items: 3, data: { recommendations: [{}, {}] } });\n\n    assert.ok(wrapper.find(\".ds-card-grid-divisible-by-3\").exists());\n  });\n});\n"
  },
  {
    "path": "test/unit/content-src/components/DiscoveryStreamComponents/DSCard.test.jsx",
    "content": "import {\n  DSCard,\n  DefaultMeta,\n  PlaceholderDSCard,\n  CTAButtonMeta,\n} from \"content-src/components/DiscoveryStreamComponents/DSCard/DSCard\";\nimport {\n  DSContextFooter,\n  StatusMessage,\n} from \"content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter\";\nimport { actionCreators as ac } from \"common/Actions.jsm\";\nimport { DSLinkMenu } from \"content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu\";\nimport React from \"react\";\nimport { SafeAnchor } from \"content-src/components/DiscoveryStreamComponents/SafeAnchor/SafeAnchor\";\nimport { shallow, mount } from \"enzyme\";\n\ndescribe(\"<DSCard>\", () => {\n  let wrapper;\n  let sandbox;\n\n  beforeEach(() => {\n    wrapper = shallow(<DSCard />);\n    wrapper.setState({ isSeen: true });\n    sandbox = sinon.createSandbox();\n  });\n\n  afterEach(() => {\n    sandbox.restore();\n  });\n\n  it(\"should render\", () => {\n    assert.ok(wrapper.exists());\n    assert.ok(wrapper.find(\".ds-card\"));\n  });\n\n  it(\"should render a SafeAnchor\", () => {\n    wrapper.setProps({ url: \"https://foo.com\" });\n\n    assert.equal(\n      wrapper\n        .children()\n        .at(0)\n        .type(),\n      SafeAnchor\n    );\n    assert.propertyVal(\n      wrapper\n        .children()\n        .at(0)\n        .props(),\n      \"url\",\n      \"https://foo.com\"\n    );\n  });\n\n  it(\"should pass onLinkClick prop\", () => {\n    assert.propertyVal(\n      wrapper\n        .children()\n        .at(0)\n        .props(),\n      \"onLinkClick\",\n      wrapper.instance().onLinkClick\n    );\n  });\n\n  it(\"should render DSLinkMenu\", () => {\n    assert.equal(\n      wrapper\n        .children()\n        .at(1)\n        .type(),\n      DSLinkMenu\n    );\n  });\n\n  it(\"should start with no .active class\", () => {\n    assert.equal(wrapper.find(\".active\").length, 0);\n  });\n\n  it(\"should render badges for pocket, bookmark when not a spoc element \", () => {\n    wrapper = mount(<DSCard context_type=\"bookmark\" />);\n    wrapper.setState({ isSeen: true });\n    const contextFooter = wrapper.find(DSContextFooter);\n\n    assert.lengthOf(contextFooter.find(StatusMessage), 1);\n  });\n\n  it(\"should render Sponsored Context for a spoc element\", () => {\n    const context = \"Sponsored by Foo\";\n    wrapper = mount(<DSCard context_type=\"bookmark\" context={context} />);\n    wrapper.setState({ isSeen: true });\n    const contextFooter = wrapper.find(DSContextFooter);\n\n    assert.lengthOf(contextFooter.find(StatusMessage), 0);\n    assert.equal(contextFooter.find(\".story-sponsored-label\").text(), context);\n  });\n\n  describe(\"onLinkClick\", () => {\n    let dispatch;\n\n    beforeEach(() => {\n      dispatch = sandbox.stub();\n      wrapper = shallow(<DSCard dispatch={dispatch} />);\n      wrapper.setState({ isSeen: true });\n    });\n\n    it(\"should call dispatch with the correct events\", () => {\n      wrapper.setProps({ id: \"fooidx\", pos: 1, type: \"foo\" });\n\n      wrapper.instance().onLinkClick();\n\n      assert.calledTwice(dispatch);\n      assert.calledWith(\n        dispatch,\n        ac.UserEvent({\n          event: \"CLICK\",\n          source: \"FOO\",\n          action_position: 1,\n          value: { card_type: \"organic\" },\n        })\n      );\n      assert.calledWith(\n        dispatch,\n        ac.ImpressionStats({\n          click: 0,\n          source: \"FOO\",\n          tiles: [{ id: \"fooidx\", pos: 1 }],\n        })\n      );\n    });\n\n    it(\"should set the right card_type on spocs\", () => {\n      wrapper.setProps({ id: \"fooidx\", pos: 1, type: \"foo\", flightId: 12345 });\n\n      wrapper.instance().onLinkClick();\n\n      assert.calledTwice(dispatch);\n      assert.calledWith(\n        dispatch,\n        ac.UserEvent({\n          event: \"CLICK\",\n          source: \"FOO\",\n          action_position: 1,\n          value: { card_type: \"spoc\" },\n        })\n      );\n      assert.calledWith(\n        dispatch,\n        ac.ImpressionStats({\n          click: 0,\n          source: \"FOO\",\n          tiles: [{ id: \"fooidx\", pos: 1 }],\n        })\n      );\n    });\n\n    it(\"should call dispatch with a shim\", () => {\n      wrapper.setProps({\n        id: \"fooidx\",\n        pos: 1,\n        type: \"foo\",\n        shim: {\n          click: \"click shim\",\n        },\n      });\n\n      wrapper.instance().onLinkClick();\n\n      assert.calledTwice(dispatch);\n      assert.calledWith(\n        dispatch,\n        ac.UserEvent({\n          event: \"CLICK\",\n          source: \"FOO\",\n          action_position: 1,\n          value: { card_type: \"organic\" },\n        })\n      );\n      assert.calledWith(\n        dispatch,\n        ac.ImpressionStats({\n          click: 0,\n          source: \"FOO\",\n          tiles: [{ id: \"fooidx\", pos: 1, shim: \"click shim\" }],\n        })\n      );\n    });\n  });\n\n  describe(\"DSCard with CTA\", () => {\n    beforeEach(() => {\n      wrapper = mount(<DSCard />);\n      wrapper.setState({ isSeen: true });\n    });\n\n    it(\"should render Default Meta\", () => {\n      const default_meta = wrapper.find(DefaultMeta);\n      assert.ok(default_meta.exists());\n    });\n\n    it(\"should not render cta-link for item with no cta\", () => {\n      const meta = wrapper.find(DefaultMeta);\n      assert.notOk(meta.find(\".cta-link\").exists());\n    });\n\n    it(\"should not render cta-link by default when item has cta and cta_variant not link\", () => {\n      wrapper.setProps({ cta: \"test\" });\n      const meta = wrapper.find(DefaultMeta);\n      assert.notOk(meta.find(\".cta-link\").exists());\n    });\n\n    it(\"should render cta-link by default when item has cta and cta_variant as link\", () => {\n      wrapper.setProps({ cta: \"test\", cta_variant: \"link\" });\n      const meta = wrapper.find(DefaultMeta);\n      assert.equal(meta.find(\".cta-link\").text(), \"test\");\n    });\n\n    it(\"should not render cta-button for non spoc content\", () => {\n      wrapper.setProps({ cta: \"test\", cta_variant: \"button\" });\n      const meta = wrapper.find(CTAButtonMeta);\n      assert.lengthOf(meta.find(\".cta-button\"), 0);\n    });\n\n    it(\"should render cta-button when item has cta and cta_variant is button and is spoc\", () => {\n      wrapper.setProps({\n        cta: \"test\",\n        cta_variant: \"button\",\n        context: \"Sponsored by Foo\",\n      });\n      const meta = wrapper.find(CTAButtonMeta);\n      assert.equal(meta.find(\".cta-button\").text(), \"test\");\n    });\n\n    it(\"should not render Sponsored by label in footer for spoc item with cta_variant button\", () => {\n      wrapper.setProps({\n        cta: \"test\",\n        context: \"Sponsored by test\",\n        cta_variant: \"button\",\n      });\n\n      assert.ok(wrapper.find(CTAButtonMeta).exists());\n      assert.notOk(wrapper.find(DSContextFooter).exists());\n    });\n\n    it(\"should render sponsor text on top for spoc item and cta button variant\", () => {\n      wrapper.setProps({\n        sponsor: \"Test\",\n        context: \"Sponsored by test\",\n        cta_variant: \"button\",\n      });\n\n      assert.ok(wrapper.find(CTAButtonMeta).exists());\n      const meta = wrapper.find(CTAButtonMeta);\n      assert.equal(meta.find(\".source\").text(), \"Test · Sponsored\");\n    });\n  });\n  describe(\"DSCard with Intersection Observer\", () => {\n    beforeEach(() => {\n      wrapper = shallow(<DSCard />);\n    });\n\n    it(\"should render card when seen\", () => {\n      let card = wrapper.find(\"div.ds-card.placeholder\");\n      assert.lengthOf(card, 1);\n\n      wrapper.instance().observer = {\n        unobserve: sandbox.stub(),\n      };\n      wrapper.instance().placeholderElement = \"element\";\n\n      wrapper.instance().onSeen([\n        {\n          isIntersecting: true,\n        },\n      ]);\n\n      assert.isTrue(wrapper.instance().state.isSeen);\n      card = wrapper.find(\"div.ds-card.placeholder\");\n      assert.lengthOf(card, 0);\n      assert.lengthOf(wrapper.find(SafeAnchor), 1);\n      assert.calledOnce(wrapper.instance().observer.unobserve);\n      assert.calledWith(wrapper.instance().observer.unobserve, \"element\");\n    });\n\n    it(\"should setup proper placholder ref for isSeen\", () => {\n      wrapper.instance().setPlaceholderRef(\"element\");\n      assert.equal(wrapper.instance().placeholderElement, \"element\");\n    });\n\n    it(\"should setup observer on componentDidMount\", () => {\n      wrapper = mount(<DSCard />);\n      assert.isTrue(!!wrapper.instance().observer);\n    });\n  });\n  describe(\"DSCard with Idle Callback\", () => {\n    let windowStub = {\n      requestIdleCallback: sinon.stub().returns(1),\n      cancelIdleCallback: sinon.stub(),\n    };\n    beforeEach(() => {\n      wrapper = shallow(<DSCard windowObj={windowStub} />);\n    });\n\n    it(\"should call requestIdleCallback on componentDidMount\", () => {\n      assert.calledOnce(windowStub.requestIdleCallback);\n    });\n\n    it(\"should call cancelIdleCallback on componentWillUnmount\", () => {\n      wrapper.instance().componentWillUnmount();\n      assert.calledOnce(windowStub.cancelIdleCallback);\n    });\n  });\n});\n\ndescribe(\"<PlaceholderDSCard> component\", () => {\n  it(\"should have placeholder prop\", () => {\n    const wrapper = shallow(<PlaceholderDSCard />);\n    const card = wrapper.find(DSCard);\n    assert.lengthOf(card, 1);\n\n    const placeholder = wrapper.find(DSCard).prop(\"placeholder\");\n    assert.isTrue(placeholder);\n  });\n\n  it(\"should contain placeholder div\", () => {\n    const wrapper = shallow(<DSCard placeholder={true} />);\n    wrapper.setState({ isSeen: true });\n    const card = wrapper.find(\"div.ds-card.placeholder\");\n    assert.lengthOf(card, 1);\n  });\n\n  it(\"should not be clickable\", () => {\n    const wrapper = shallow(<DSCard placeholder={true} />);\n    wrapper.setState({ isSeen: true });\n    const anchor = wrapper.find(\"SafeAnchor.ds-card-link\");\n    assert.lengthOf(anchor, 0);\n  });\n\n  it(\"should not have context menu\", () => {\n    const wrapper = shallow(<DSCard placeholder={true} />);\n    wrapper.setState({ isSeen: true });\n    const linkMenu = wrapper.find(DSLinkMenu);\n    assert.lengthOf(linkMenu, 0);\n  });\n});\n"
  },
  {
    "path": "test/unit/content-src/components/DiscoveryStreamComponents/DSContextFooter.test.jsx",
    "content": "import {\n  DSContextFooter,\n  StatusMessage,\n} from \"content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter\";\nimport React from \"react\";\nimport { mount } from \"enzyme\";\nimport { cardContextTypes } from \"content-src/components/Card/types.js\";\n\ndescribe(\"<DSContextFooter>\", () => {\n  let wrapper;\n  let sandbox;\n  const bookmarkBadge = \"bookmark\";\n  const removeBookmarkBadge = \"removedBookmark\";\n  const context = \"Sponsored by Babel\";\n  const engagement = \"Popular\";\n\n  beforeEach(() => {\n    wrapper = mount(<DSContextFooter />);\n    sandbox = sinon.createSandbox();\n  });\n\n  afterEach(() => {\n    sandbox.restore();\n  });\n\n  it(\"should render\", () => {\n    assert.isTrue(wrapper.exists());\n    assert.isOk(wrapper.find(\".story-footer\"));\n  });\n  it(\"should not render an engagement status if display_engagement_labels is false\", () => {\n    wrapper = mount(\n      <DSContextFooter\n        display_engagement_labels={false}\n        engagement={engagement}\n      />\n    );\n\n    const engagementLabel = wrapper.find(\".story-view-count\");\n    assert.equal(engagementLabel.length, 0);\n  });\n  it(\"should render an engagement status if no badge and spoc passed\", () => {\n    wrapper = mount(\n      <DSContextFooter\n        display_engagement_labels={true}\n        engagement={engagement}\n      />\n    );\n\n    const engagementLabel = wrapper.find(\".story-view-count\");\n    assert.equal(engagementLabel.text(), engagement);\n  });\n  it(\"should render a badge if a proper badge prop is passed\", () => {\n    wrapper = mount(\n      <DSContextFooter context_type={bookmarkBadge} engagement={engagement} />\n    );\n    const { fluentID } = cardContextTypes[bookmarkBadge];\n\n    assert.lengthOf(wrapper.find(\".story-view-count\"), 0);\n    const statusLabel = wrapper.find(\".story-context-label\");\n    assert.equal(statusLabel.prop(\"data-l10n-id\"), fluentID);\n  });\n  it(\"should only render a sponsored context if pass a sponsored context\", async () => {\n    wrapper = mount(\n      <DSContextFooter\n        context_type={bookmarkBadge}\n        context={context}\n        engagement={engagement}\n      />\n    );\n\n    assert.lengthOf(wrapper.find(\".story-view-count\"), 0);\n    assert.lengthOf(wrapper.find(StatusMessage), 0);\n    assert.equal(wrapper.find(\".story-sponsored-label\").text(), context);\n  });\n  it(\"should render a new badge if props change from an old badge to a new one\", async () => {\n    wrapper = mount(<DSContextFooter context_type={bookmarkBadge} />);\n\n    const { fluentID: bookmarkFluentID } = cardContextTypes[bookmarkBadge];\n    const bookmarkStatusMessage = wrapper.find(\n      `div[data-l10n-id='${bookmarkFluentID}']`\n    );\n    assert.isTrue(bookmarkStatusMessage.exists());\n\n    const { fluentID: removeBookmarkFluentID } = cardContextTypes[\n      removeBookmarkBadge\n    ];\n\n    wrapper.setProps({ context_type: removeBookmarkBadge });\n    await wrapper.update();\n\n    assert.isEmpty(bookmarkStatusMessage);\n    const removedBookmarkStatusMessage = wrapper.find(\n      `div[data-l10n-id='${removeBookmarkFluentID}']`\n    );\n    assert.isTrue(removedBookmarkStatusMessage.exists());\n  });\n});\n"
  },
  {
    "path": "test/unit/content-src/components/DiscoveryStreamComponents/DSDismiss.test.jsx",
    "content": "import { DSDismiss } from \"content-src/components/DiscoveryStreamComponents/DSDismiss/DSDismiss\";\nimport React from \"react\";\nimport { shallow } from \"enzyme\";\n\ndescribe(\"<DSDismiss>\", () => {\n  const fakeSpoc = {\n    url: \"https://foo.com\",\n    guid: \"1234\",\n  };\n  let wrapper;\n  let sandbox;\n  let dispatchStub;\n\n  beforeEach(() => {\n    sandbox = sinon.createSandbox();\n    dispatchStub = sandbox.stub();\n    wrapper = shallow(\n      <DSDismiss\n        data={fakeSpoc}\n        dispatch={dispatchStub}\n        shouldSendImpressionStats={true}\n      />\n    );\n  });\n\n  afterEach(() => {\n    sandbox.restore();\n  });\n\n  it(\"should render\", () => {\n    assert.ok(wrapper.exists());\n    assert.ok(wrapper.find(\".ds-dismiss\").exists());\n  });\n\n  it(\"should render proper hover state\", () => {\n    wrapper.instance().onHover();\n    assert.ok(wrapper.find(\".hovering\").exists());\n    wrapper.instance().offHover();\n    assert.ok(!wrapper.find(\".hovering\").exists());\n  });\n\n  it(\"should dispatch a BlockUrl event on click\", () => {\n    wrapper.instance().onDismissClick();\n\n    assert.calledThrice(dispatchStub);\n    assert.deepEqual(dispatchStub.firstCall.args[0].data, {\n      url: \"https://foo.com\",\n      pocket_id: undefined,\n    });\n    assert.deepEqual(dispatchStub.secondCall.args[0].data, {\n      event: \"BLOCK\",\n      source: \"DISCOVERY_STREAM\",\n      action_position: 0,\n      url: \"https://foo.com\",\n      guid: \"1234\",\n    });\n    assert.deepEqual(dispatchStub.thirdCall.args[0].data, {\n      source: \"DISCOVERY_STREAM\",\n      block: 0,\n      tiles: [\n        {\n          id: \"1234\",\n          pos: 0,\n        },\n      ],\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/content-src/components/DiscoveryStreamComponents/DSEmptyState.test.jsx",
    "content": "import { DSEmptyState } from \"content-src/components/DiscoveryStreamComponents/DSEmptyState/DSEmptyState\";\nimport React from \"react\";\nimport { shallow } from \"enzyme\";\n\ndescribe(\"<DSEmptyState>\", () => {\n  let wrapper;\n\n  beforeEach(() => {\n    wrapper = shallow(<DSEmptyState />);\n  });\n\n  it(\"should render\", () => {\n    assert.ok(wrapper.exists());\n    assert.ok(wrapper.find(\".section-empty-state\").exists());\n  });\n\n  it(\"should render defaultempty state message\", () => {\n    assert.ok(wrapper.find(\".empty-state-message\").exists());\n    const header = wrapper.find(\n      \"h2[data-l10n-id='newtab-discovery-empty-section-topstories-header']\"\n    );\n    const paragraph = wrapper.find(\n      \"p[data-l10n-id='newtab-discovery-empty-section-topstories-content']\"\n    );\n\n    assert.ok(header.exists());\n    assert.ok(paragraph.exists());\n  });\n\n  it(\"should render failed state message\", () => {\n    wrapper = shallow(<DSEmptyState status=\"failed\" />);\n    const button = wrapper.find(\n      \"button[data-l10n-id='newtab-discovery-empty-section-topstories-try-again-button']\"\n    );\n\n    assert.ok(button.exists());\n  });\n\n  it(\"should render waiting state message\", () => {\n    wrapper = shallow(<DSEmptyState status=\"waiting\" />);\n    const button = wrapper.find(\n      \"button[data-l10n-id='newtab-discovery-empty-section-topstories-loading']\"\n    );\n\n    assert.ok(button.exists());\n  });\n\n  it(\"should dispatch DISCOVERY_STREAM_RETRY_FEED on failed state button click\", () => {\n    const dispatch = sinon.spy();\n\n    wrapper = shallow(\n      <DSEmptyState\n        status=\"failed\"\n        dispatch={dispatch}\n        feed={{ url: \"https://foo.com\", data: {} }}\n      />\n    );\n    wrapper.find(\"button.try-again-button\").simulate(\"click\");\n\n    assert.calledTwice(dispatch);\n    let [action] = dispatch.firstCall.args;\n    assert.equal(action.type, \"DISCOVERY_STREAM_FEED_UPDATE\");\n    assert.deepEqual(action.data.feed, {\n      url: \"https://foo.com\",\n      data: { status: \"waiting\" },\n    });\n\n    [action] = dispatch.secondCall.args;\n\n    assert.equal(action.type, \"DISCOVERY_STREAM_RETRY_FEED\");\n    assert.deepEqual(action.data.feed, { url: \"https://foo.com\", data: {} });\n  });\n});\n"
  },
  {
    "path": "test/unit/content-src/components/DiscoveryStreamComponents/DSImage.test.jsx",
    "content": "import { DSImage } from \"content-src/components/DiscoveryStreamComponents/DSImage/DSImage\";\nimport { mount } from \"enzyme\";\nimport React from \"react\";\n\ndescribe(\"Discovery Stream <DSImage>\", () => {\n  let sandbox;\n  beforeEach(() => {\n    sandbox = sinon.createSandbox();\n  });\n\n  it(\"should have a child with class ds-image\", () => {\n    const img = mount(<DSImage />);\n    const child = img.find(\".ds-image\");\n\n    assert.lengthOf(child, 1);\n  });\n\n  it(\"should set proper sources if only `source` is available\", () => {\n    const img = mount(<DSImage source=\"https://placekitten.com/g/640/480\" />);\n\n    img.setState({\n      isSeen: true,\n      containerWidth: 640,\n    });\n\n    assert.equal(\n      img.find(\"img\").prop(\"src\"),\n      \"https://placekitten.com/g/640/480\"\n    );\n  });\n\n  it(\"should set proper sources if `rawSource` is available\", () => {\n    const img = mount(\n      <DSImage rawSource=\"https://placekitten.com/g/640/480\" />\n    );\n\n    img.setState({\n      isSeen: true,\n      containerWidth: 640,\n      containerHeight: 480,\n    });\n\n    assert.equal(\n      img.find(\"img\").prop(\"src\"),\n      \"https://img-getpocket.cdn.mozilla.net/640x480/filters:format(jpeg):quality(60):no_upscale():strip_exif()/https%3A%2F%2Fplacekitten.com%2Fg%2F640%2F480\"\n    );\n    assert.equal(\n      img.find(\"img\").prop(\"srcSet\"),\n      \"https://img-getpocket.cdn.mozilla.net/1280x960/filters:format(jpeg):quality(60):no_upscale():strip_exif()/https%3A%2F%2Fplacekitten.com%2Fg%2F640%2F480 2x\"\n    );\n  });\n\n  it(\"should fall back to unoptimized when optimized failed\", () => {\n    const img = mount(\n      <DSImage\n        source=\"https://placekitten.com/g/640/480\"\n        rawSource=\"https://placekitten.com/g/640/480\"\n      />\n    );\n    img.setState({\n      isSeen: true,\n      containerWidth: 640,\n      containerHeight: 480,\n    });\n\n    img.instance().onOptimizedImageError();\n    img.update();\n\n    assert.equal(\n      img.find(\"img\").prop(\"src\"),\n      \"https://placekitten.com/g/640/480\"\n    );\n  });\n\n  it(\"should render a placeholder broken image when image failed\", () => {\n    const img = mount(<DSImage />);\n    img.setState({ isSeen: true });\n\n    img.instance().onNonOptimizedImageError();\n    img.update();\n\n    assert.equal(img.find(\"div\").prop(\"className\"), \"broken-image\");\n  });\n\n  it(\"should update state when seen\", () => {\n    const img = mount(\n      <DSImage rawSource=\"https://placekitten.com/g/640/480\" />\n    );\n\n    img.instance().onSeen([\n      {\n        isIntersecting: true,\n        boundingClientRect: {\n          width: 640,\n          height: 480,\n        },\n      },\n    ]);\n\n    assert.equal(img.state().containerWidth, 640);\n    assert.equal(img.state().containerHeight, 480);\n    assert.propertyVal(img.state(), \"isSeen\", true);\n  });\n\n  it(\"should stop observing when removed\", () => {\n    const img = mount(<DSImage />);\n    const { observer } = img.instance();\n    sandbox.stub(observer, \"unobserve\");\n\n    img.unmount();\n\n    assert.calledOnce(observer.unobserve);\n  });\n  describe(\"DSImage with Idle Callback\", () => {\n    let wrapper;\n    let windowStub = {\n      requestIdleCallback: sinon.stub().returns(1),\n      cancelIdleCallback: sinon.stub(),\n    };\n    beforeEach(() => {\n      wrapper = mount(<DSImage windowObj={windowStub} />);\n    });\n\n    it(\"should call requestIdleCallback on componentDidMount\", () => {\n      assert.calledOnce(windowStub.requestIdleCallback);\n    });\n\n    it(\"should call cancelIdleCallback on componentWillUnmount\", () => {\n      wrapper.instance().componentWillUnmount();\n      assert.calledOnce(windowStub.cancelIdleCallback);\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/content-src/components/DiscoveryStreamComponents/DSLinkMenu.test.jsx",
    "content": "import { mount, shallow } from \"enzyme\";\nimport { DSLinkMenu } from \"content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu\";\nimport { ContextMenuButton } from \"content-src/components/ContextMenu/ContextMenuButton\";\nimport { LinkMenu } from \"content-src/components/LinkMenu/LinkMenu\";\nimport React from \"react\";\n\ndescribe(\"<DSLinkMenu>\", () => {\n  let wrapper;\n  let parentNode;\n\n  describe(\"DS link menu actions\", () => {\n    beforeEach(() => {\n      wrapper = mount(<DSLinkMenu />);\n      parentNode = wrapper.getDOMNode().parentNode;\n    });\n\n    afterEach(() => {\n      wrapper.unmount();\n    });\n\n    it(\"Should remove active on Menu Update\", () => {\n      // Add active class name to DSLinkMenu parent node\n      // to simulate menu open state\n      parentNode.classList.add(\"active\");\n      assert.equal(parentNode.className, \"active\");\n\n      wrapper.instance().onMenuUpdate(false);\n      wrapper.update();\n\n      assert.isEmpty(parentNode.className);\n    });\n\n    it(\"Should add active on Menu Show\", async () => {\n      wrapper.instance().nextAnimationFrame = () => {};\n      await wrapper.instance().onMenuShow();\n      wrapper.update();\n      assert.equal(parentNode.className, \"active\");\n    });\n\n    it(\"Should add last-item to support resized window\", async () => {\n      const fakeWindow = { scrollMaxX: \"20\" };\n      wrapper = mount(<DSLinkMenu windowObj={fakeWindow} />);\n      parentNode = wrapper.getDOMNode().parentNode;\n      wrapper.instance().nextAnimationFrame = () => {};\n      await wrapper.instance().onMenuShow();\n      wrapper.update();\n      assert.equal(parentNode.className, \"last-item active\");\n    });\n\n    it(\"Should call rAF from nextAnimationFrame\", () => {\n      const fakeWindow = { requestAnimationFrame: sinon.stub() };\n      wrapper = mount(<DSLinkMenu windowObj={fakeWindow} />);\n\n      wrapper.instance().nextAnimationFrame();\n      assert.calledOnce(fakeWindow.requestAnimationFrame);\n    });\n\n    it(\"should remove .active and .last-item classes from the parent component\", () => {\n      const instance = wrapper.instance();\n      const remove = sinon.stub();\n      instance.contextMenuButtonRef = {\n        current: {\n          parentElement: { parentElement: { classList: { remove } } },\n        },\n      };\n      instance.onMenuUpdate();\n      assert.calledOnce(remove);\n    });\n\n    it(\"should add .active and .last-item classes to the parent component\", async () => {\n      const instance = wrapper.instance();\n      const add = sinon.stub();\n      instance.nextAnimationFrame = () => {};\n      instance.contextMenuButtonRef = {\n        current: { parentElement: { parentElement: { classList: { add } } } },\n      };\n      await instance.onMenuShow();\n      assert.calledOnce(add);\n    });\n\n    it(\"should parse args for fluent correctly \", () => {\n      const title = '\"fluent\"';\n      wrapper = mount(<DSLinkMenu title={title} />);\n\n      const button = wrapper.find(\n        \"button[data-l10n-id='newtab-menu-content-tooltip']\"\n      );\n      assert.equal(button.prop(\"data-l10n-args\"), JSON.stringify({ title }));\n    });\n  });\n\n  describe(\"DS context menu options\", () => {\n    const ValidDSLinkMenuProps = {\n      site: {},\n    };\n\n    beforeEach(() => {\n      wrapper = shallow(<DSLinkMenu {...ValidDSLinkMenuProps} />);\n    });\n\n    it(\"should render a context menu button\", () => {\n      assert.ok(wrapper.exists());\n      assert.ok(\n        wrapper.find(ContextMenuButton).exists(),\n        \"context menu button exists\"\n      );\n    });\n\n    it(\"should render LinkMenu when context menu button is clicked\", () => {\n      let button = wrapper.find(ContextMenuButton);\n      button.simulate(\"click\", { preventDefault: () => {} });\n      assert.equal(wrapper.find(LinkMenu).length, 1);\n    });\n\n    it(\"should pass dispatch, onShow, site, options, shouldSendImpressionStats, source and index to LinkMenu\", () => {\n      wrapper\n        .find(ContextMenuButton)\n        .simulate(\"click\", { preventDefault: () => {} });\n      const linkMenuProps = wrapper.find(LinkMenu).props();\n      [\n        \"dispatch\",\n        \"onShow\",\n        \"site\",\n        \"index\",\n        \"options\",\n        \"source\",\n        \"shouldSendImpressionStats\",\n      ].forEach(prop => assert.property(linkMenuProps, prop));\n    });\n\n    it(\"should pass through the correct menu options to LinkMenu\", () => {\n      wrapper\n        .find(ContextMenuButton)\n        .simulate(\"click\", { preventDefault: () => {} });\n      const linkMenuProps = wrapper.find(LinkMenu).props();\n      assert.deepEqual(linkMenuProps.options, [\n        \"CheckBookmarkOrArchive\",\n        \"CheckSavedToPocket\",\n        \"Separator\",\n        \"OpenInNewWindow\",\n        \"OpenInPrivateWindow\",\n        \"Separator\",\n        \"BlockUrl\",\n      ]);\n    });\n\n    it(\"should pass through the correct menu options to LinkMenu for spocs\", () => {\n      wrapper = shallow(\n        <DSLinkMenu {...ValidDSLinkMenuProps} flightId=\"1234\" />\n      );\n      wrapper\n        .find(ContextMenuButton)\n        .simulate(\"click\", { preventDefault: () => {} });\n      const linkMenuProps = wrapper.find(LinkMenu).props();\n      assert.deepEqual(linkMenuProps.options, [\n        \"CheckBookmarkOrArchive\",\n        \"CheckSavedToPocket\",\n        \"Separator\",\n        \"OpenInNewWindow\",\n        \"OpenInPrivateWindow\",\n        \"Separator\",\n        \"BlockUrl\",\n        \"ShowPrivacyInfo\",\n      ]);\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/content-src/components/DiscoveryStreamComponents/DSMessage.test.jsx",
    "content": "import { DSMessage } from \"content-src/components/DiscoveryStreamComponents/DSMessage/DSMessage\";\nimport React from \"react\";\nimport { SafeAnchor } from \"content-src/components/DiscoveryStreamComponents/SafeAnchor/SafeAnchor\";\nimport { FluentOrText } from \"content-src/components/FluentOrText/FluentOrText\";\nimport { mount } from \"enzyme\";\n\ndescribe(\"<DSMessage>\", () => {\n  let wrapper;\n\n  beforeEach(() => {\n    wrapper = mount(<DSMessage />);\n  });\n\n  it(\"should render\", () => {\n    assert.ok(wrapper.exists());\n    assert.ok(wrapper.find(\".ds-message\").exists());\n  });\n\n  it(\"should render an icon\", () => {\n    wrapper.setProps({ icon: \"foo\" });\n\n    assert.ok(wrapper.find(\".glyph\").exists());\n    assert.propertyVal(\n      wrapper.find(\".glyph\").props().style,\n      \"backgroundImage\",\n      `url(foo)`\n    );\n  });\n\n  it(\"should render a title\", () => {\n    wrapper.setProps({ title: \"foo\" });\n\n    assert.ok(wrapper.find(\".title-text\").exists());\n    assert.equal(wrapper.find(\".title-text\").text(), \"foo\");\n  });\n\n  it(\"should render a SafeAnchor\", () => {\n    wrapper.setProps({ link_text: \"foo\", link_url: \"https://foo.com\" });\n\n    assert.equal(\n      wrapper\n        .find(\".title\")\n        .children()\n        .at(0)\n        .type(),\n      SafeAnchor\n    );\n  });\n\n  it(\"should render a FluentOrText\", () => {\n    wrapper.setProps({\n      link_text: \"link_text\",\n      title: \"title\",\n      link_url: \"https://link_url.com\",\n    });\n\n    assert.equal(\n      wrapper\n        .find(\".title-text\")\n        .children()\n        .at(0)\n        .type(),\n      FluentOrText\n    );\n\n    assert.equal(\n      wrapper\n        .find(\".link a\")\n        .children()\n        .at(0)\n        .type(),\n      FluentOrText\n    );\n  });\n});\n"
  },
  {
    "path": "test/unit/content-src/components/DiscoveryStreamComponents/DSPrivacyModal.test.jsx",
    "content": "import { DSPrivacyModal } from \"content-src/components/DiscoveryStreamComponents/DSPrivacyModal/DSPrivacyModal\";\nimport { shallow, mount } from \"enzyme\";\nimport { actionCreators as ac } from \"common/Actions.jsm\";\nimport React from \"react\";\n\ndescribe(\"Discovery Stream <DSPrivacyModal>\", () => {\n  let sandbox;\n  let dispatch;\n  let wrapper;\n  beforeEach(() => {\n    sandbox = sinon.createSandbox();\n    dispatch = sandbox.stub();\n    wrapper = shallow(<DSPrivacyModal dispatch={dispatch} />);\n  });\n\n  afterEach(() => {\n    sandbox.restore();\n  });\n\n  it(\"should contain a privacy notice\", () => {\n    const modal = mount(<DSPrivacyModal />);\n    const child = modal.find(\".privacy-notice\");\n\n    assert.lengthOf(child, 1);\n  });\n\n  it(\"should call dispatch when modal is closed\", () => {\n    wrapper.instance().closeModal();\n    assert.calledOnce(dispatch);\n  });\n\n  it(\"should call dispatch with the correct events\", () => {\n    wrapper.instance().onLinkClick();\n\n    assert.calledOnce(dispatch);\n    assert.calledWith(\n      dispatch,\n      ac.UserEvent({\n        event: \"CLICK_PRIVACY_INFO\",\n        source: \"DS_PRIVACY_MODAL\",\n      })\n    );\n  });\n});\n"
  },
  {
    "path": "test/unit/content-src/components/DiscoveryStreamComponents/DSTextPromo.test.jsx",
    "content": "import { DSTextPromo } from \"content-src/components/DiscoveryStreamComponents/DSTextPromo/DSTextPromo\";\nimport React from \"react\";\nimport { shallow } from \"enzyme\";\n\ndescribe(\"<DSTextPromo>\", () => {\n  let wrapper;\n  let sandbox;\n  let dispatchStub;\n\n  beforeEach(() => {\n    sandbox = sinon.createSandbox();\n    dispatchStub = sandbox.stub();\n    wrapper = shallow(\n      <DSTextPromo\n        shim={{ impression: \"1234\" }}\n        type=\"TEXTPROMO\"\n        pos={0}\n        dispatch={dispatchStub}\n        id=\"1234\"\n      />\n    );\n  });\n\n  afterEach(() => {\n    sandbox.restore();\n  });\n\n  it(\"should render\", () => {\n    assert.ok(wrapper.exists());\n    assert.ok(wrapper.find(\".ds-text-promo\").exists());\n  });\n\n  it(\"should render a header\", () => {\n    wrapper.setProps({ header: \"foo\" });\n    assert.ok(wrapper.find(\".text\").exists());\n  });\n\n  it(\"should render a subtitle\", () => {\n    wrapper.setProps({ subtitle: \"foo\" });\n    assert.ok(wrapper.find(\".subtitle\").exists());\n  });\n\n  it(\"should dispatch a click event on click\", () => {\n    wrapper.instance().onLinkClick();\n\n    assert.calledTwice(dispatchStub);\n    assert.deepEqual(dispatchStub.firstCall.args[0].data, {\n      event: \"CLICK\",\n      source: \"TEXTPROMO\",\n      action_position: 0,\n    });\n    assert.deepEqual(dispatchStub.secondCall.args[0].data, {\n      source: \"TEXTPROMO\",\n      click: 0,\n      tiles: [{ id: \"1234\", pos: 0 }],\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/content-src/components/DiscoveryStreamComponents/Hero.test.jsx",
    "content": "import {\n  DSCard,\n  PlaceholderDSCard,\n} from \"content-src/components/DiscoveryStreamComponents/DSCard/DSCard\";\nimport {\n  DSContextFooter,\n  StatusMessage,\n} from \"content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter\";\nimport { actionCreators as ac } from \"common/Actions.jsm\";\nimport { DSEmptyState } from \"content-src/components/DiscoveryStreamComponents/DSEmptyState/DSEmptyState\";\nimport { Hero } from \"content-src/components/DiscoveryStreamComponents/Hero/Hero\";\nimport { List } from \"content-src/components/DiscoveryStreamComponents/List/List\";\nimport React from \"react\";\nimport { shallow } from \"enzyme\";\n\ndescribe(\"<Hero>\", () => {\n  let DEFAULT_PROPS;\n  beforeEach(() => {\n    DEFAULT_PROPS = {\n      data: {\n        recommendations: [{ url: 1 }, { url: 2 }, { url: 3 }],\n      },\n    };\n  });\n\n  it(\"should render with nothing\", () => {\n    const wrapper = shallow(<Hero />);\n\n    assert.lengthOf(wrapper.find(\"a\"), 0);\n  });\n\n  it(\"should return Empty State for no recommendations\", () => {\n    const heroProps = {\n      data: { recommendations: [] },\n      header: { title: \"headerTitle\" },\n    };\n\n    const wrapper = shallow(<Hero {...heroProps} />);\n    const dsEmptyState = wrapper.find(DSEmptyState);\n    const dsHeader = wrapper.find(\".ds-header\");\n    const dsHero = wrapper.find(\".ds-hero.empty\");\n\n    assert.ok(wrapper.exists());\n    assert.lengthOf(dsEmptyState, 1);\n    assert.lengthOf(dsHeader, 1);\n    assert.lengthOf(dsHero, 1);\n  });\n\n  it(\"should render a hero link with expected url\", () => {\n    const wrapper = shallow(<Hero {...DEFAULT_PROPS} />);\n\n    assert.equal(\n      wrapper.find(\"SafeAnchor\").prop(\"url\"),\n      DEFAULT_PROPS.data.recommendations[0].url\n    );\n  });\n\n  it(\"should render badges for pocket, bookmark when not a spoc element \", () => {\n    const heroProps = {\n      data: { recommendations: [{ context_type: \"bookmark\" }] },\n      header: { title: \"headerTitle\" },\n    };\n\n    const wrapper = shallow(<Hero {...heroProps} />);\n    const contextFooter = wrapper.find(DSContextFooter).shallow();\n    assert.lengthOf(contextFooter.find(StatusMessage), 1);\n  });\n\n  it(\"should render Sponsored Context for a spoc element\", () => {\n    const heroProps = {\n      data: {\n        recommendations: [\n          { context_type: \"bookmark\", context: \"Sponsored by Foo\" },\n        ],\n      },\n      header: { title: \"headerTitle\" },\n    };\n    const wrapper = shallow(<Hero {...heroProps} />);\n    const contextFooter = wrapper.find(DSContextFooter).shallow();\n\n    assert.lengthOf(contextFooter.find(StatusMessage), 0);\n    assert.equal(\n      contextFooter.find(\".story-sponsored-label\").text(),\n      heroProps.data.recommendations[0].context\n    );\n  });\n\n  describe(\"subComponent: cards\", () => {\n    beforeEach(() => {\n      DEFAULT_PROPS.subComponentType = \"cards\";\n    });\n\n    it(\"should render no cards for 1 hero item\", () => {\n      const wrapper = shallow(<Hero {...DEFAULT_PROPS} items={1} />);\n\n      assert.lengthOf(wrapper.find(DSCard), 0);\n    });\n\n    it(\"should render 1 card with expected url for 2 hero items\", () => {\n      const wrapper = shallow(<Hero {...DEFAULT_PROPS} items={2} />);\n\n      assert.equal(\n        wrapper.find(DSCard).prop(\"url\"),\n        DEFAULT_PROPS.data.recommendations[1].url\n      );\n    });\n\n    it(\"should return PlaceholderDSCard for recommendations less than items\", () => {\n      const wrapper = shallow(<Hero {...DEFAULT_PROPS} items={4} />);\n\n      const dsCard = wrapper.find(DSCard);\n      assert.lengthOf(dsCard, 2);\n\n      const placeholderDSCard = wrapper.find(PlaceholderDSCard);\n      assert.lengthOf(placeholderDSCard, 1);\n    });\n  });\n\n  describe(\"subComponent: list\", () => {\n    beforeEach(() => {\n      DEFAULT_PROPS.subComponentType = \"list\";\n    });\n\n    it(\"should render list with no items for 1 hero item\", () => {\n      const wrapper = shallow(<Hero {...DEFAULT_PROPS} items={1} />);\n\n      assert.equal(wrapper.find(List).prop(\"items\"), 0);\n    });\n\n    it(\"should render list with 1 item for 2 hero items\", () => {\n      const wrapper = shallow(<Hero {...DEFAULT_PROPS} items={2} />);\n\n      assert.equal(wrapper.find(List).prop(\"items\"), 1);\n    });\n  });\n\n  describe(\"onLinkClick\", () => {\n    let dispatch;\n    let sandbox;\n    let wrapper;\n    const heroProps = {\n      data: { recommendations: [{ url: 1, id: \"foo-id\", pos: 1 }] },\n      type: \"foo\",\n      items: 1,\n    };\n\n    beforeEach(() => {\n      sandbox = sinon.createSandbox();\n      dispatch = sandbox.stub();\n      wrapper = shallow(<Hero dispatch={dispatch} {...heroProps} />);\n    });\n\n    afterEach(() => {\n      sandbox.restore();\n    });\n\n    it(\"should call dispatch with the correct events\", () => {\n      wrapper.instance().onLinkClick();\n\n      assert.calledTwice(dispatch);\n      assert.calledWith(\n        dispatch,\n        ac.UserEvent({\n          event: \"CLICK\",\n          source: \"FOO\",\n          action_position: 1,\n        })\n      );\n      assert.calledWith(\n        dispatch,\n        ac.ImpressionStats({\n          click: 0,\n          source: \"FOO\",\n          tiles: [{ id: \"foo-id\", pos: 1 }],\n        })\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/content-src/components/DiscoveryStreamComponents/Highlights.test.jsx",
    "content": "import { combineReducers, createStore } from \"redux\";\nimport { INITIAL_STATE, reducers } from \"common/Reducers.jsm\";\nimport { Highlights } from \"content-src/components/DiscoveryStreamComponents/Highlights/Highlights\";\nimport { mount } from \"enzyme\";\nimport { Provider } from \"react-redux\";\nimport React from \"react\";\n\ndescribe(\"Discovery Stream <Highlights>\", () => {\n  let wrapper;\n\n  afterEach(() => {\n    wrapper.unmount();\n  });\n\n  it(\"should render nothing with no highlights data\", () => {\n    const store = createStore(combineReducers(reducers), { ...INITIAL_STATE });\n\n    wrapper = mount(\n      <Provider store={store}>\n        <Highlights />\n      </Provider>\n    );\n\n    assert.ok(wrapper.isEmptyRender());\n  });\n\n  it(\"should render highlights\", () => {\n    const store = createStore(combineReducers(reducers), {\n      ...INITIAL_STATE,\n      Sections: [{ id: \"highlights\", enabled: true }],\n    });\n\n    wrapper = mount(\n      <Provider store={store}>\n        <Highlights />\n      </Provider>\n    );\n\n    assert.lengthOf(wrapper.find(\".ds-highlights\"), 1);\n  });\n});\n"
  },
  {
    "path": "test/unit/content-src/components/DiscoveryStreamComponents/HorizontalRule.test.jsx",
    "content": "import { HorizontalRule } from \"content-src/components/DiscoveryStreamComponents/HorizontalRule/HorizontalRule\";\nimport React from \"react\";\nimport { shallow } from \"enzyme\";\n\ndescribe(\"<HorizontalRule>\", () => {\n  let wrapper;\n\n  beforeEach(() => {\n    wrapper = shallow(<HorizontalRule />);\n  });\n\n  it(\"should render\", () => {\n    assert.ok(wrapper.exists());\n    assert.ok(wrapper.find(\".ds-hr\").exists());\n  });\n});\n"
  },
  {
    "path": "test/unit/content-src/components/DiscoveryStreamComponents/ImpressionStats.test.jsx",
    "content": "\"use strict\";\n\nimport {\n  ImpressionStats,\n  INTERSECTION_RATIO,\n} from \"content-src/components/DiscoveryStreamImpressionStats/ImpressionStats\";\nimport { actionTypes as at } from \"common/Actions.jsm\";\nimport React from \"react\";\nimport { shallow } from \"enzyme\";\n\ndescribe(\"<ImpressionStats>\", () => {\n  const SOURCE = \"TEST_SOURCE\";\n  const FullIntersectEntries = [\n    { isIntersecting: true, intersectionRatio: INTERSECTION_RATIO },\n  ];\n  const ZeroIntersectEntries = [\n    { isIntersecting: false, intersectionRatio: 0 },\n  ];\n  const PartialIntersectEntries = [\n    { isIntersecting: true, intersectionRatio: INTERSECTION_RATIO / 2 },\n  ];\n\n  // Build IntersectionObserver class with the arg `entries` for the intersect callback.\n  function buildIntersectionObserver(entries) {\n    return class {\n      constructor(callback) {\n        this.callback = callback;\n      }\n\n      observe() {\n        this.callback(entries);\n      }\n\n      unobserve() {}\n    };\n  }\n\n  const DEFAULT_PROPS = {\n    rows: [{ id: 1, pos: 0 }, { id: 2, pos: 1 }, { id: 3, pos: 2 }],\n    source: SOURCE,\n    IntersectionObserver: buildIntersectionObserver(FullIntersectEntries),\n    document: {\n      visibilityState: \"visible\",\n      addEventListener: sinon.stub(),\n      removeEventListener: sinon.stub(),\n    },\n  };\n\n  const InnerEl = () => <div>Inner Element</div>;\n\n  function renderImpressionStats(props = {}) {\n    return shallow(\n      <ImpressionStats {...DEFAULT_PROPS} {...props}>\n        <InnerEl />\n      </ImpressionStats>\n    );\n  }\n\n  it(\"should render props.children\", () => {\n    const wrapper = renderImpressionStats();\n    assert.ok(wrapper.contains(<InnerEl />));\n  });\n  it(\"should not send loaded content nor impression when the page is not visible\", () => {\n    const dispatch = sinon.spy();\n    const props = {\n      dispatch,\n      document: {\n        visibilityState: \"hidden\",\n        addEventListener: sinon.spy(),\n        removeEventListener: sinon.spy(),\n      },\n    };\n    renderImpressionStats(props);\n\n    assert.notCalled(dispatch);\n  });\n  it(\"should noly send loaded content but not impression when the wrapped item is not visbible\", () => {\n    const dispatch = sinon.spy();\n    const props = {\n      dispatch,\n      IntersectionObserver: buildIntersectionObserver(ZeroIntersectEntries),\n    };\n    renderImpressionStats(props);\n\n    // This one is for loaded content.\n    assert.calledOnce(dispatch);\n    const [action] = dispatch.firstCall.args;\n    assert.equal(action.type, at.DISCOVERY_STREAM_LOADED_CONTENT);\n    assert.equal(action.data.source, SOURCE);\n    assert.deepEqual(action.data.tiles, [\n      { id: 1, pos: 0 },\n      { id: 2, pos: 1 },\n      { id: 3, pos: 2 },\n    ]);\n  });\n  it(\"should not send impression when the wrapped item is visbible but below the ratio\", () => {\n    const dispatch = sinon.spy();\n    const props = {\n      dispatch,\n      IntersectionObserver: buildIntersectionObserver(PartialIntersectEntries),\n    };\n    renderImpressionStats(props);\n\n    // This one is for loaded content.\n    assert.calledOnce(dispatch);\n  });\n  it(\"should send a loaded content and an impression when the page is visible and the wrapped item meets the visibility ratio\", () => {\n    const dispatch = sinon.spy();\n    const props = {\n      dispatch,\n      IntersectionObserver: buildIntersectionObserver(FullIntersectEntries),\n    };\n    renderImpressionStats(props);\n\n    assert.calledTwice(dispatch);\n\n    let [action] = dispatch.firstCall.args;\n    assert.equal(action.type, at.DISCOVERY_STREAM_LOADED_CONTENT);\n    assert.equal(action.data.source, SOURCE);\n    assert.deepEqual(action.data.tiles, [\n      { id: 1, pos: 0 },\n      { id: 2, pos: 1 },\n      { id: 3, pos: 2 },\n    ]);\n\n    [action] = dispatch.secondCall.args;\n    assert.equal(action.type, at.DISCOVERY_STREAM_IMPRESSION_STATS);\n    assert.equal(action.data.source, SOURCE);\n    assert.deepEqual(action.data.tiles, [\n      { id: 1, pos: 0 },\n      { id: 2, pos: 1 },\n      { id: 3, pos: 2 },\n    ]);\n  });\n  it(\"should send a DISCOVERY_STREAM_SPOC_IMPRESSION when the wrapped item has a flightId\", () => {\n    const dispatch = sinon.spy();\n    const flightId = \"a_flight_id\";\n    const props = {\n      dispatch,\n      flightId,\n      IntersectionObserver: buildIntersectionObserver(FullIntersectEntries),\n    };\n    renderImpressionStats(props);\n\n    // Loaded content + DISCOVERY_STREAM_SPOC_IMPRESSION + impression\n    assert.calledThrice(dispatch);\n\n    const [action] = dispatch.secondCall.args;\n    assert.equal(action.type, at.DISCOVERY_STREAM_SPOC_IMPRESSION);\n    assert.deepEqual(action.data, { flightId });\n  });\n  it(\"should send an impression when the wrapped item transiting from invisible to visible\", () => {\n    const dispatch = sinon.spy();\n    const props = {\n      dispatch,\n      IntersectionObserver: buildIntersectionObserver(ZeroIntersectEntries),\n    };\n    const wrapper = renderImpressionStats(props);\n\n    // For the loaded content\n    assert.calledOnce(dispatch);\n\n    let [action] = dispatch.firstCall.args;\n    assert.equal(action.type, at.DISCOVERY_STREAM_LOADED_CONTENT);\n    assert.equal(action.data.source, SOURCE);\n    assert.deepEqual(action.data.tiles, [\n      { id: 1, pos: 0 },\n      { id: 2, pos: 1 },\n      { id: 3, pos: 2 },\n    ]);\n\n    dispatch.resetHistory();\n    wrapper.instance().impressionObserver.callback(FullIntersectEntries);\n\n    // For the impression\n    assert.calledOnce(dispatch);\n\n    [action] = dispatch.firstCall.args;\n    assert.equal(action.type, at.DISCOVERY_STREAM_IMPRESSION_STATS);\n    assert.deepEqual(action.data.tiles, [\n      { id: 1, pos: 0 },\n      { id: 2, pos: 1 },\n      { id: 3, pos: 2 },\n    ]);\n  });\n  it(\"should remove visibility change listener when the wrapper is removed\", () => {\n    const props = {\n      dispatch: sinon.spy(),\n      document: {\n        visibilityState: \"hidden\",\n        addEventListener: sinon.spy(),\n        removeEventListener: sinon.spy(),\n      },\n      IntersectionObserver,\n    };\n\n    const wrapper = renderImpressionStats(props);\n    assert.calledWith(props.document.addEventListener, \"visibilitychange\");\n    const [, listener] = props.document.addEventListener.firstCall.args;\n\n    wrapper.unmount();\n    assert.calledWith(\n      props.document.removeEventListener,\n      \"visibilitychange\",\n      listener\n    );\n  });\n  it(\"should unobserve the intersection observer when the wrapper is removed\", () => {\n    const IntersectionObserver = buildIntersectionObserver(\n      ZeroIntersectEntries\n    );\n    const spy = sinon.spy(IntersectionObserver.prototype, \"unobserve\");\n    const props = { dispatch: sinon.spy(), IntersectionObserver };\n\n    const wrapper = renderImpressionStats(props);\n    wrapper.unmount();\n\n    assert.calledOnce(spy);\n  });\n  it(\"should only send the latest impression on a visibility change\", () => {\n    const listeners = new Set();\n    const props = {\n      dispatch: sinon.spy(),\n      document: {\n        visibilityState: \"hidden\",\n        addEventListener: (ev, cb) => listeners.add(cb),\n        removeEventListener: (ev, cb) => listeners.delete(cb),\n      },\n    };\n\n    const wrapper = renderImpressionStats(props);\n\n    // Update twice\n    wrapper.setProps({ ...props, ...{ rows: [{ id: 123, pos: 4 }] } });\n    wrapper.setProps({ ...props, ...{ rows: [{ id: 2432, pos: 5 }] } });\n\n    assert.notCalled(props.dispatch);\n\n    // Simulate listeners getting called\n    props.document.visibilityState = \"visible\";\n    listeners.forEach(l => l());\n\n    // Make sure we only sent the latest event\n    assert.calledTwice(props.dispatch);\n    const [action] = props.dispatch.firstCall.args;\n    assert.deepEqual(action.data.tiles, [{ id: 2432, pos: 5 }]);\n  });\n});\n"
  },
  {
    "path": "test/unit/content-src/components/DiscoveryStreamComponents/List.test.jsx",
    "content": "import {\n  _List as List,\n  ListItem,\n  PlaceholderListItem,\n} from \"content-src/components/DiscoveryStreamComponents/List/List\";\nimport { actionCreators as ac } from \"common/Actions.jsm\";\nimport {\n  DSContextFooter,\n  StatusMessage,\n} from \"content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter\";\nimport { DSEmptyState } from \"content-src/components/DiscoveryStreamComponents/DSEmptyState/DSEmptyState\";\nimport { DSLinkMenu } from \"content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu\";\nimport { GlobalOverrider } from \"test/unit/utils\";\nimport React from \"react\";\nimport { shallow } from \"enzyme\";\n\ndescribe(\"<List> presentation component\", () => {\n  const ValidRecommendations = [\n    { url: 1 },\n    { url: 2 },\n    { context: \"test spoc\", url: 3 },\n  ];\n  const ValidListProps = {\n    data: {\n      recommendations: ValidRecommendations,\n    },\n    feed: {\n      url: \"fakeFeedUrl\",\n    },\n    header: {\n      title: \"fakeFeedTitle\",\n    },\n  };\n\n  it(\"should return null if feed.data is falsy\", () => {\n    const ListProps = {\n      data: { feeds: { a: \"stuff\" } },\n    };\n\n    const wrapper = shallow(<List {...ListProps} />);\n    assert.isNull(wrapper.getElement());\n  });\n\n  it(\"should return Empty State for no recommendations\", () => {\n    const ListProps = {\n      data: { recommendations: [] },\n      header: { title: \"headerTitle\" },\n    };\n\n    const wrapper = shallow(<List {...ListProps} />);\n    const dsEmptyState = wrapper.find(DSEmptyState);\n    const dsHeader = wrapper.find(\".ds-header\");\n    const dsList = wrapper.find(\".ds-list.empty\");\n\n    assert.ok(wrapper.exists());\n    assert.lengthOf(dsEmptyState, 1);\n    assert.lengthOf(dsHeader, 1);\n    assert.lengthOf(dsList, 1);\n  });\n\n  it(\"should return something containing a <ul> if props are valid\", () => {\n    const wrapper = shallow(<List {...ValidListProps} />);\n\n    const list = wrapper.find(\"ul\");\n    assert.ok(wrapper.exists());\n    assert.lengthOf(list, 1);\n  });\n\n  it(\"should return the right number of ListItems if props are valid\", () => {\n    const wrapper = shallow(<List {...ValidListProps} />);\n\n    const listItem = wrapper.find(ListItem);\n    assert.lengthOf(listItem, ValidRecommendations.length);\n  });\n\n  it(\"should return fewer ListItems for fewer items\", () => {\n    const wrapper = shallow(<List {...ValidListProps} items={1} />);\n\n    const listItem = wrapper.find(ListItem);\n    assert.lengthOf(listItem, 1);\n  });\n\n  it(\"should return PlaceHolderListItem for recommendations less than items\", () => {\n    const wrapper = shallow(<List {...ValidListProps} items={4} />);\n\n    const listItem = wrapper.find(ListItem);\n    assert.lengthOf(listItem, 3);\n\n    const placeholderListItem = wrapper.find(PlaceholderListItem);\n    assert.lengthOf(placeholderListItem, 1);\n  });\n\n  it(\"should return fewer ListItems for starting point\", () => {\n    const wrapper = shallow(<List {...ValidListProps} recStartingPoint={1} />);\n\n    const listItem = wrapper.find(ListItem);\n    assert.lengthOf(listItem, ValidRecommendations.length - 1);\n  });\n\n  it(\"should return expected ListItems when offset\", () => {\n    const wrapper = shallow(\n      <List {...ValidListProps} items={2} recStartingPoint={1} />\n    );\n\n    const listItemUrls = wrapper.find(ListItem).map(i => i.prop(\"url\"));\n    assert.sameOrderedMembers(listItemUrls, [\n      ValidRecommendations[1].url,\n      ValidRecommendations[2].url,\n    ]);\n  });\n\n  it(\"should return expected spoc ListItem\", () => {\n    const wrapper = shallow(\n      <List {...ValidListProps} items={3} recStartingPoint={0} />\n    );\n\n    const listItemContext = wrapper.find(ListItem).map(i => i.prop(\"context\"));\n    assert.sameOrderedMembers(listItemContext, [\n      undefined,\n      undefined,\n      ValidRecommendations[2].context,\n    ]);\n  });\n});\n\ndescribe(\"<ListItem> presentation component\", () => {\n  const ValidListItemProps = {\n    url: \"FAKE_URL\",\n    title: \"FAKE_TITLE\",\n    domain: \"example.com\",\n    image_src: \"FAKE_IMAGE_SRC\",\n    context_type: \"pocket\",\n  };\n  const ValidSpocListItemProps = {\n    url: \"FAKE_URL\",\n    title: \"FAKE_TITLE\",\n    domain: \"example.com\",\n    image_src: \"FAKE_IMAGE_SRC\",\n    context_type: \"pocket\",\n    context: \"FAKE_CONTEXT\",\n  };\n  let globals;\n\n  beforeEach(() => {\n    globals = new GlobalOverrider();\n  });\n\n  afterEach(() => {\n    globals.sandbox.restore();\n  });\n\n  it(\"should contain 'a.ds-list-item-link' with the props.url set\", () => {\n    const wrapper = shallow(<ListItem {...ValidListItemProps} />);\n\n    const anchors = wrapper.find(\n      `SafeAnchor.ds-list-item-link[url=\"${ValidListItemProps.url}\"]`\n    );\n    assert.lengthOf(anchors, 1);\n  });\n\n  it(\"should render badges for pocket, bookmark when not a spoc element \", () => {\n    const wrapper = shallow(<ListItem {...ValidListItemProps} />);\n    const contextFooter = wrapper.find(DSContextFooter).shallow();\n\n    assert.lengthOf(contextFooter.find(StatusMessage), 1);\n  });\n\n  it(\"should render Sponsored Context for a spoc element\", () => {\n    const wrapper = shallow(<ListItem {...ValidSpocListItemProps} />);\n    const contextFooter = wrapper.find(DSContextFooter).shallow();\n\n    assert.lengthOf(contextFooter.find(StatusMessage), 0);\n    assert.equal(\n      contextFooter.find(\".story-sponsored-label\").text(),\n      ValidSpocListItemProps.context\n    );\n  });\n\n  describe(\"onLinkClick\", () => {\n    let dispatch;\n    let sandbox;\n    let wrapper;\n\n    beforeEach(() => {\n      sandbox = sinon.createSandbox();\n      dispatch = sandbox.stub();\n      wrapper = shallow(\n        <ListItem dispatch={dispatch} {...ValidListItemProps} />\n      );\n    });\n\n    afterEach(() => {\n      sandbox.restore();\n    });\n\n    it(\"should call dispatch with the correct events\", () => {\n      wrapper.setProps({ id: \"foo-id\", pos: 1, type: \"foo\" });\n\n      wrapper.instance().onLinkClick();\n\n      assert.calledTwice(dispatch);\n      assert.calledWith(\n        dispatch,\n        ac.UserEvent({\n          event: \"CLICK\",\n          source: \"FOO\",\n          action_position: 1,\n          value: { card_type: \"organic\" },\n        })\n      );\n      assert.calledWith(\n        dispatch,\n        ac.ImpressionStats({\n          click: 0,\n          source: \"FOO\",\n          tiles: [{ id: \"foo-id\", pos: 1 }],\n        })\n      );\n    });\n\n    it(\"should set the right card_type on spocs\", () => {\n      wrapper.setProps({ id: \"foo-id\", pos: 1, type: \"foo\", flightId: 12345 });\n\n      wrapper.instance().onLinkClick();\n\n      assert.calledTwice(dispatch);\n      assert.calledWith(\n        dispatch,\n        ac.UserEvent({\n          event: \"CLICK\",\n          source: \"FOO\",\n          action_position: 1,\n          value: { card_type: \"spoc\" },\n        })\n      );\n      assert.calledWith(\n        dispatch,\n        ac.ImpressionStats({\n          click: 0,\n          source: \"FOO\",\n          tiles: [{ id: \"foo-id\", pos: 1 }],\n        })\n      );\n    });\n  });\n});\n\ndescribe(\"<PlaceholderListItem> component\", () => {\n  it(\"should have placeholder prop\", () => {\n    const wrapper = shallow(<PlaceholderListItem />);\n    const listItem = wrapper.find(ListItem);\n    assert.lengthOf(listItem, 1);\n\n    const placeholder = wrapper.find(ListItem).prop(\"placeholder\");\n    assert.isTrue(placeholder);\n  });\n\n  it(\"should contain placeholder listitem\", () => {\n    const wrapper = shallow(<ListItem placeholder={true} />);\n    const listItem = wrapper.find(\"li.ds-list-item.placeholder\");\n    assert.lengthOf(listItem, 1);\n  });\n\n  it(\"should not be clickable\", () => {\n    const wrapper = shallow(<ListItem placeholder={true} />);\n    const anchor = wrapper.find(\"SafeAnchor.ds-list-item-link\");\n    assert.lengthOf(anchor, 1);\n\n    const linkClick = anchor.prop(\"onLinkClick\");\n    assert.isUndefined(linkClick);\n  });\n\n  it(\"should not have context menu\", () => {\n    const wrapper = shallow(<ListItem placeholder={true} />);\n    const linkMenu = wrapper.find(DSLinkMenu);\n    assert.lengthOf(linkMenu, 0);\n  });\n});\n"
  },
  {
    "path": "test/unit/content-src/components/DiscoveryStreamComponents/Navigation.test.jsx",
    "content": "import {\n  Navigation,\n  Topic,\n} from \"content-src/components/DiscoveryStreamComponents/Navigation/Navigation\";\nimport React from \"react\";\nimport { SafeAnchor } from \"content-src/components/DiscoveryStreamComponents/SafeAnchor/SafeAnchor\";\nimport { FluentOrText } from \"content-src/components/FluentOrText/FluentOrText\";\nimport { shallow, mount } from \"enzyme\";\n\ndescribe(\"<Navigation>\", () => {\n  let wrapper;\n\n  beforeEach(() => {\n    wrapper = mount(<Navigation header={{}} />);\n  });\n\n  it(\"should render\", () => {\n    assert.ok(wrapper.exists());\n  });\n\n  it(\"should render a title\", () => {\n    wrapper.setProps({ header: { title: \"Foo\" } });\n\n    assert.equal(wrapper.find(\".ds-header\").text(), \"Foo\");\n  });\n\n  it(\"should render a FluentOrText\", () => {\n    wrapper.setProps({ header: { title: \"Foo\" } });\n\n    assert.equal(\n      wrapper\n        .find(\".ds-navigation\")\n        .children()\n        .at(0)\n        .type(),\n      FluentOrText\n    );\n  });\n\n  it(\"should render 2 Topics\", () => {\n    wrapper.setProps({\n      links: [\n        { url: \"https://foo.com\", name: \"foo\" },\n        { url: \"https://bar.com\", name: \"bar\" },\n      ],\n    });\n\n    assert.lengthOf(wrapper.find(\"ul\").children(), 2);\n  });\n});\n\ndescribe(\"<Topic>\", () => {\n  let wrapper;\n\n  beforeEach(() => {\n    wrapper = shallow(<Topic url=\"https://foo.com\" name=\"foo\" />);\n  });\n\n  it(\"should render\", () => {\n    assert.ok(wrapper.exists());\n    assert.equal(wrapper.type(), \"li\");\n    assert.equal(\n      wrapper\n        .children()\n        .at(0)\n        .type(),\n      SafeAnchor\n    );\n  });\n});\n"
  },
  {
    "path": "test/unit/content-src/components/DiscoveryStreamComponents/SafeAnchor.test.jsx",
    "content": "import React from \"react\";\nimport { SafeAnchor } from \"content-src/components/DiscoveryStreamComponents/SafeAnchor/SafeAnchor\";\nimport { shallow } from \"enzyme\";\n\ndescribe(\"Discovery Stream <SafeAnchor>\", () => {\n  let warnStub;\n  let sandbox;\n  beforeEach(() => {\n    warnStub = sinon.stub(console, \"warn\");\n    sandbox = sinon.createSandbox();\n  });\n  afterEach(() => {\n    warnStub.restore();\n    sandbox.restore();\n  });\n  it(\"should render with anchor\", () => {\n    const wrapper = shallow(<SafeAnchor />);\n    assert.lengthOf(wrapper.find(\"a\"), 1);\n  });\n  it(\"should render with anchor target for http\", () => {\n    const wrapper = shallow(<SafeAnchor url=\"http://example.com\" />);\n    assert.equal(wrapper.find(\"a\").prop(\"href\"), \"http://example.com\");\n  });\n  it(\"should render with anchor target for https\", () => {\n    const wrapper = shallow(<SafeAnchor url=\"https://example.com\" />);\n    assert.equal(wrapper.find(\"a\").prop(\"href\"), \"https://example.com\");\n  });\n  it(\"should not allow javascript: URIs\", () => {\n    const wrapper = shallow(<SafeAnchor url=\"javascript:foo()\" />); // eslint-disable-line no-script-url\n    assert.equal(wrapper.find(\"a\").prop(\"href\"), \"\");\n    assert.calledOnce(warnStub);\n  });\n  it(\"should not warn if the URL is falsey \", () => {\n    const wrapper = shallow(<SafeAnchor url=\"\" />);\n    assert.equal(wrapper.find(\"a\").prop(\"href\"), \"\");\n    assert.notCalled(warnStub);\n  });\n  it(\"should dispatch an event on click\", () => {\n    const dispatchStub = sandbox.stub();\n    const fakeEvent = { preventDefault: sandbox.stub(), currentTarget: {} };\n    const wrapper = shallow(<SafeAnchor dispatch={dispatchStub} />);\n\n    wrapper.find(\"a\").simulate(\"click\", fakeEvent);\n\n    assert.calledOnce(dispatchStub);\n    assert.calledOnce(fakeEvent.preventDefault);\n  });\n  it(\"should call onLinkClick if provided\", () => {\n    const onLinkClickStub = sandbox.stub();\n    const wrapper = shallow(<SafeAnchor onLinkClick={onLinkClickStub} />);\n\n    wrapper.find(\"a\").simulate(\"click\");\n\n    assert.calledOnce(onLinkClickStub);\n  });\n});\n"
  },
  {
    "path": "test/unit/content-src/components/DiscoveryStreamComponents/SectionTitle.test.jsx",
    "content": "import React from \"react\";\nimport { SectionTitle } from \"content-src/components/DiscoveryStreamComponents/SectionTitle/SectionTitle\";\nimport { shallow } from \"enzyme\";\n\ndescribe(\"<SectionTitle>\", () => {\n  let wrapper;\n\n  beforeEach(() => {\n    wrapper = shallow(<SectionTitle header={{}} />);\n  });\n\n  it(\"should render\", () => {\n    assert.ok(wrapper.exists());\n    assert.ok(wrapper.find(\".ds-section-title\").exists());\n  });\n\n  it(\"should render a subtitle\", () => {\n    wrapper.setProps({ header: { title: \"Foo\", subtitle: \"Bar\" } });\n\n    assert.equal(wrapper.find(\".subtitle\").text(), \"Bar\");\n  });\n});\n"
  },
  {
    "path": "test/unit/content-src/components/DiscoveryStreamComponents/TopSites.test.jsx",
    "content": "import { combineReducers, createStore } from \"redux\";\nimport {\n  INITIAL_STATE,\n  reducers,\n  TOP_SITES_DEFAULT_ROWS,\n} from \"common/Reducers.jsm\";\nimport { mount } from \"enzyme\";\nimport { TopSites as OldTopSites } from \"content-src/components/TopSites/TopSites\";\nimport { Provider } from \"react-redux\";\nimport React from \"react\";\nimport {\n  TopSites as TopSitesContainer,\n  _TopSites as TopSites,\n} from \"content-src/components/DiscoveryStreamComponents/TopSites/TopSites\";\n\ndescribe(\"Discovery Stream <TopSites>\", () => {\n  let wrapper;\n  let store;\n  const defaultTopSiteRows = [\n    { label: \"facebook\" },\n    { label: \"amazon\" },\n    { label: \"google\" },\n    { label: \"apple\" },\n  ];\n  const defaultTopSites = {\n    rows: defaultTopSiteRows,\n  };\n\n  beforeEach(() => {\n    INITIAL_STATE.Prefs.values.topSitesRows = TOP_SITES_DEFAULT_ROWS;\n    store = createStore(combineReducers(reducers), INITIAL_STATE);\n    wrapper = mount(\n      <Provider store={store}>\n        <TopSitesContainer TopSites={defaultTopSites} />\n      </Provider>\n    );\n  });\n\n  afterEach(() => {\n    wrapper.unmount();\n  });\n\n  it(\"should return a wrapper around old TopSites\", () => {\n    const oldTopSites = wrapper.find(OldTopSites);\n    const dsTopSitesWrapper = wrapper.find(\".ds-top-sites\");\n\n    assert.ok(wrapper.exists());\n    assert.lengthOf(oldTopSites, 1);\n    assert.lengthOf(dsTopSitesWrapper, 1);\n  });\n\n  describe(\"TopSites header\", () => {\n    it(\"should have header title undefined by default\", () => {\n      const oldTopSites = wrapper.find(OldTopSites);\n      assert.isUndefined(oldTopSites.props().title);\n    });\n\n    it(\"should set header title on old TopSites\", () => {\n      let DEFAULT_PROPS = {\n        header: { title: \"test\" },\n      };\n      wrapper = mount(\n        <Provider store={store}>\n          <TopSitesContainer {...DEFAULT_PROPS} />\n        </Provider>\n      );\n      const oldTopSites = wrapper.find(OldTopSites);\n      assert.equal(oldTopSites.props().title, \"test\");\n    });\n  });\n\n  describe(\"insertSpocContent\", () => {\n    let insertSpocContent;\n    const topSiteSpoc = {\n      url: \"foo\",\n      sponsor: \"bar\",\n      image_src: \"foobar\",\n      flight_id: \"1234\",\n      id: \"5678\",\n      shim: { impression: \"1011\" },\n    };\n    const data = { spocs: [topSiteSpoc] };\n    const resultSpocLeft = {\n      customScreenshotURL: \"foobar\",\n      type: \"SPOC\",\n      label: \"bar\",\n      title: \"bar\",\n      url: \"foo\",\n      flightId: \"1234\",\n      id: \"5678\",\n      guid: \"5678\",\n      shim: {\n        impression: \"1011\",\n      },\n      pos: 0,\n    };\n    const resultSpocRight = {\n      customScreenshotURL: \"foobar\",\n      type: \"SPOC\",\n      label: \"bar\",\n      title: \"bar\",\n      url: \"foo\",\n      flightId: \"1234\",\n      id: \"5678\",\n      guid: \"5678\",\n      shim: {\n        impression: \"1011\",\n      },\n      pos: 7,\n    };\n    const pinnedSite = {\n      label: \"pinnedSite\",\n      isPinned: true,\n    };\n\n    beforeEach(() => {\n      const instance = wrapper.find(TopSites).instance();\n      insertSpocContent = instance.insertSpocContent.bind(instance);\n    });\n\n    it(\"Should return null if no data or no TopSites\", () => {\n      assert.isNull(insertSpocContent(defaultTopSites, {}, \"right\"));\n      assert.isNull(insertSpocContent({}, data, \"right\"));\n    });\n\n    it(\"Should return null if an organic SPOC topsite exists\", () => {\n      const topSitesWithOrganicSpoc = {\n        rows: [...defaultTopSiteRows, topSiteSpoc],\n      };\n\n      assert.isNull(insertSpocContent(topSitesWithOrganicSpoc, data, \"right\"));\n    });\n\n    it(\"Should return next spoc if the first SPOC is an existing organic top site\", () => {\n      const topSitesWithOrganicSpoc = {\n        rows: [...defaultTopSiteRows, topSiteSpoc],\n      };\n      const extraSpocData = {\n        spocs: [\n          topSiteSpoc,\n          {\n            url: \"foo2\",\n            sponsor: \"bar2\",\n            image_src: \"foobar2\",\n            flight_id: \"1234\",\n            id: \"5678\",\n            shim: { impression: \"1011\" },\n          },\n        ],\n      };\n\n      const result = insertSpocContent(\n        topSitesWithOrganicSpoc,\n        extraSpocData,\n        \"right\"\n      );\n\n      const availableSpoc = {\n        customScreenshotURL: \"foobar2\",\n        type: \"SPOC\",\n        label: \"bar2\",\n        title: \"bar2\",\n        url: \"foo2\",\n        flightId: \"1234\",\n        id: \"5678\",\n        guid: \"5678\",\n        shim: {\n          impression: \"1011\",\n        },\n        pos: 7,\n      };\n      const expectedResult = {\n        rows: [...topSitesWithOrganicSpoc.rows, availableSpoc],\n      };\n\n      assert.deepEqual(result, expectedResult);\n    });\n\n    it(\"should add to end of row if the row is not full and alignment is right\", () => {\n      const result = insertSpocContent(defaultTopSites, data, \"right\");\n\n      const expectedResult = {\n        rows: [...defaultTopSiteRows, resultSpocRight],\n      };\n      assert.deepEqual(result, expectedResult);\n    });\n\n    it(\"should add to front of row if the row is not full and alignment is left\", () => {\n      const result = insertSpocContent(defaultTopSites, data, \"left\");\n      assert.deepEqual(result, {\n        rows: [resultSpocLeft, ...defaultTopSiteRows],\n      });\n    });\n\n    it(\"should add to first available in the front row if alignment is left and there are pins\", () => {\n      const topSiteRowsWithPins = [\n        pinnedSite,\n        pinnedSite,\n        ...defaultTopSiteRows,\n      ];\n\n      const result = insertSpocContent(\n        { rows: topSiteRowsWithPins },\n        data,\n        \"left\"\n      );\n\n      assert.deepEqual(result, {\n        rows: [pinnedSite, pinnedSite, resultSpocLeft, ...defaultTopSiteRows],\n      });\n    });\n\n    it(\"should add to first available in the next row if alignment is right and there are all pins in the front row\", () => {\n      const pinnedArray = new Array(8).fill(pinnedSite);\n      const result = insertSpocContent({ rows: pinnedArray }, data, \"right\");\n\n      assert.deepEqual(result, {\n        rows: [...pinnedArray, resultSpocRight],\n      });\n    });\n\n    it(\"should add to first available in the current row if alignment is right and there are some pins in the front row\", () => {\n      const pinnedArray = new Array(6).fill(pinnedSite);\n      const topSite = { label: \"foo\" };\n\n      const rowsWithPins = [topSite, topSite, ...pinnedArray];\n\n      const result = insertSpocContent({ rows: rowsWithPins }, data, \"right\");\n\n      assert.deepEqual(result, {\n        rows: [topSite, resultSpocRight, ...pinnedArray, topSite],\n      });\n    });\n\n    it(\"should preserve the indices of pinned items\", () => {\n      const topSite = { label: \"foo\" };\n      const rowsWithPins = [pinnedSite, topSite, topSite, pinnedSite];\n\n      const result = insertSpocContent({ rows: rowsWithPins }, data, \"left\");\n\n      // Pinned items should retain in Index 0 and Index 3 like defined in rowsWithPins\n      assert.deepEqual(result, {\n        rows: [pinnedSite, resultSpocLeft, topSite, pinnedSite, topSite],\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/content-src/components/ErrorBoundary.test.jsx",
    "content": "import { A11yLinkButton } from \"content-src/components/A11yLinkButton/A11yLinkButton\";\nimport {\n  ErrorBoundary,\n  ErrorBoundaryFallback,\n} from \"content-src/components/ErrorBoundary/ErrorBoundary\";\nimport React from \"react\";\nimport { shallow } from \"enzyme\";\n\ndescribe(\"<ErrorBoundary>\", () => {\n  it(\"should render its children if componentDidCatch wasn't called\", () => {\n    const wrapper = shallow(\n      <ErrorBoundary>\n        <div className=\"kids\" />\n      </ErrorBoundary>\n    );\n\n    assert.lengthOf(wrapper.find(\".kids\"), 1);\n  });\n\n  it(\"should render ErrorBoundaryFallback if componentDidCatch called\", () => {\n    const wrapper = shallow(<ErrorBoundary />);\n\n    wrapper.instance().componentDidCatch();\n    // since shallow wrappers don't automatically manage lifecycle semantics:\n    wrapper.update();\n\n    assert.lengthOf(wrapper.find(ErrorBoundaryFallback), 1);\n  });\n\n  it(\"should render the given FallbackComponent if componentDidCatch called\", () => {\n    class TestFallback extends React.PureComponent {\n      render() {\n        return <div className=\"my-fallback\">doh!</div>;\n      }\n    }\n\n    const wrapper = shallow(<ErrorBoundary FallbackComponent={TestFallback} />);\n    wrapper.instance().componentDidCatch();\n    // since shallow wrappers don't automatically manage lifecycle semantics:\n    wrapper.update();\n\n    assert.lengthOf(wrapper.find(TestFallback), 1);\n  });\n\n  it(\"should pass the given className prop to the FallbackComponent\", () => {\n    class TestFallback extends React.PureComponent {\n      render() {\n        return <div className={this.props.className}>doh!</div>;\n      }\n    }\n\n    const wrapper = shallow(\n      <ErrorBoundary FallbackComponent={TestFallback} className=\"sheep\" />\n    );\n    wrapper.instance().componentDidCatch();\n    // since shallow wrappers don't automatically manage lifecycle semantics:\n    wrapper.update();\n\n    assert.lengthOf(wrapper.find(\".sheep\"), 1);\n  });\n});\n\ndescribe(\"ErrorBoundaryFallback\", () => {\n  it(\"should render a <div> with a class of as-error-fallback\", () => {\n    const wrapper = shallow(<ErrorBoundaryFallback />);\n\n    assert.lengthOf(wrapper.find(\"div.as-error-fallback\"), 1);\n  });\n\n  it(\"should render a <div> with the props.className and .as-error-fallback\", () => {\n    const wrapper = shallow(<ErrorBoundaryFallback className=\"monkeys\" />);\n\n    assert.lengthOf(wrapper.find(\"div.monkeys.as-error-fallback\"), 1);\n  });\n\n  it(\"should call window.location.reload(true) if .reload-button clicked\", () => {\n    const fakeWindow = { location: { reload: sinon.spy() } };\n    const wrapper = shallow(<ErrorBoundaryFallback windowObj={fakeWindow} />);\n\n    wrapper.find(\".reload-button\").simulate(\"click\");\n\n    assert.calledOnce(fakeWindow.location.reload);\n    assert.calledWithExactly(fakeWindow.location.reload, true);\n  });\n\n  it(\"should render .reload-button as an <A11yLinkButton>\", () => {\n    const wrapper = shallow(<ErrorBoundaryFallback />);\n\n    assert.lengthOf(wrapper.find(\"A11yLinkButton.reload-button\"), 1);\n  });\n\n  it(\"should render newtab-error-fallback-refresh-link node\", () => {\n    const wrapper = shallow(<ErrorBoundaryFallback />);\n\n    const msgWrapper = wrapper.find(\n      '[data-l10n-id=\"newtab-error-fallback-refresh-link\"]'\n    );\n    assert.lengthOf(msgWrapper, 1);\n    assert.isTrue(msgWrapper.is(A11yLinkButton));\n  });\n\n  it(\"should render newtab-error-fallback-info node\", () => {\n    const wrapper = shallow(<ErrorBoundaryFallback />);\n\n    const msgWrapper = wrapper.find(\n      '[data-l10n-id=\"newtab-error-fallback-info\"]'\n    );\n    assert.lengthOf(msgWrapper, 1);\n  });\n});\n"
  },
  {
    "path": "test/unit/content-src/components/FluentOrText.test.jsx",
    "content": "import { FluentOrText } from \"content-src/components/FluentOrText/FluentOrText\";\nimport React from \"react\";\nimport { shallow } from \"enzyme\";\n\ndescribe(\"<FluentOrText>\", () => {\n  it(\"should create span with no children\", () => {\n    const wrapper = shallow(<FluentOrText />);\n\n    assert.ok(wrapper.find(\"span\"));\n  });\n  it(\"should set plain text\", () => {\n    const wrapper = shallow(<FluentOrText message={\"hello\"} />);\n\n    assert.equal(wrapper.text(), \"hello\");\n  });\n  it(\"should use fluent id on automatic span\", () => {\n    const wrapper = shallow(<FluentOrText message={{ id: \"fluent\" }} />);\n\n    assert.ok(wrapper.find(\"span[data-l10n-id='fluent']\"));\n  });\n  it(\"should also allow string_id\", () => {\n    const wrapper = shallow(<FluentOrText message={{ string_id: \"fluent\" }} />);\n\n    assert.ok(wrapper.find(\"span[data-l10n-id='fluent']\"));\n  });\n  it(\"should use fluent id on child\", () => {\n    const wrapper = shallow(\n      <FluentOrText message={{ id: \"fluent\" }}>\n        <p />\n      </FluentOrText>\n    );\n\n    assert.ok(wrapper.find(\"p[data-l10n-id='fluent']\"));\n  });\n  it(\"should set args for fluent\", () => {\n    const wrapper = shallow(<FluentOrText message={{ args: { num: 5 } }} />);\n\n    assert.ok(wrapper.find(\"span[data-l10n-args='{num: 5}']\"));\n  });\n  it(\"should also allow values\", () => {\n    const wrapper = shallow(<FluentOrText message={{ values: { num: 5 } }} />);\n\n    assert.ok(wrapper.find(\"span[data-l10n-args='{num: 5}']\"));\n  });\n  it(\"should preserve original children with fluent\", () => {\n    const wrapper = shallow(\n      <FluentOrText message={{ id: \"fluent\" }}>\n        <p>\n          <b data-l10n-name=\"bold\" />\n        </p>\n      </FluentOrText>\n    );\n\n    assert.ok(wrapper.find(\"b[data-l10n-name='bold']\"));\n  });\n  it(\"should only allow a single child\", () => {\n    assert.throws(() =>\n      shallow(\n        <FluentOrText>\n          <p />\n          <p />\n        </FluentOrText>\n      )\n    );\n  });\n});\n"
  },
  {
    "path": "test/unit/content-src/components/LinkMenu.test.jsx",
    "content": "import { ContextMenu } from \"content-src/components/ContextMenu/ContextMenu\";\nimport { _LinkMenu as LinkMenu } from \"content-src/components/LinkMenu/LinkMenu\";\nimport React from \"react\";\nimport { shallow } from \"enzyme\";\n\ndescribe(\"<LinkMenu>\", () => {\n  let wrapper;\n  beforeEach(() => {\n    wrapper = shallow(\n      <LinkMenu\n        site={{ url: \"\" }}\n        options={[\"CheckPinTopSite\", \"CheckBookmark\", \"OpenInNewWindow\"]}\n        dispatch={() => {}}\n      />\n    );\n  });\n  it(\"should render a ContextMenu element\", () => {\n    assert.ok(wrapper.find(ContextMenu).exists());\n  });\n  it(\"should pass onUpdate, and options to ContextMenu\", () => {\n    assert.ok(wrapper.find(ContextMenu).exists());\n    const contextMenuProps = wrapper.find(ContextMenu).props();\n    [\"onUpdate\", \"options\"].forEach(prop =>\n      assert.property(contextMenuProps, prop)\n    );\n  });\n  it(\"should give ContextMenu the correct tabbable options length for a11y\", () => {\n    const { options } = wrapper.find(ContextMenu).props();\n    const [firstItem] = options;\n    const lastItem = options[options.length - 1];\n\n    // first item should have {first: true}\n    assert.isTrue(firstItem.first);\n    assert.ok(!firstItem.last);\n\n    // last item should have {last: true}\n    assert.isTrue(lastItem.last);\n    assert.ok(!lastItem.first);\n\n    // middle items should have neither\n    for (let i = 1; i < options.length - 1; i++) {\n      assert.ok(!options[i].first && !options[i].last);\n    }\n  });\n  it(\"should show the correct options for default sites\", () => {\n    wrapper = shallow(\n      <LinkMenu\n        site={{ url: \"\", isDefault: true }}\n        options={[\"CheckBookmark\"]}\n        source={\"TOP_SITES\"}\n        isPrivateBrowsingEnabled={true}\n        dispatch={() => {}}\n      />\n    );\n    const { options } = wrapper.find(ContextMenu).props();\n    let i = 0;\n    assert.propertyVal(options[i++], \"id\", \"newtab-menu-pin\");\n    assert.propertyVal(options[i++], \"id\", \"newtab-menu-edit-topsites\");\n    assert.propertyVal(options[i++], \"type\", \"separator\");\n    assert.propertyVal(options[i++], \"id\", \"newtab-menu-open-new-window\");\n    assert.propertyVal(\n      options[i++],\n      \"id\",\n      \"newtab-menu-open-new-private-window\"\n    );\n    assert.propertyVal(options[i++], \"type\", \"separator\");\n    assert.propertyVal(options[i++], \"id\", \"newtab-menu-dismiss\");\n    assert.propertyVal(options, \"length\", i);\n    // Double check that delete options are not included for default top sites\n    options\n      .filter(o => o.type !== \"separator\")\n      .forEach(o => {\n        assert.notInclude([\"newtab-menu-delete-history\"], o.id);\n      });\n  });\n  it(\"should show Unpin option for a pinned site if CheckPinTopSite in options list\", () => {\n    wrapper = shallow(\n      <LinkMenu\n        site={{ url: \"\", isPinned: true }}\n        source={\"TOP_SITES\"}\n        options={[\"CheckPinTopSite\"]}\n        dispatch={() => {}}\n      />\n    );\n    const { options } = wrapper.find(ContextMenu).props();\n    assert.isDefined(options.find(o => o.id && o.id === \"newtab-menu-unpin\"));\n  });\n  it(\"should show Pin option for an unpinned site if CheckPinTopSite in options list\", () => {\n    wrapper = shallow(\n      <LinkMenu\n        site={{ url: \"\", isPinned: false }}\n        source={\"TOP_SITES\"}\n        options={[\"CheckPinTopSite\"]}\n        dispatch={() => {}}\n      />\n    );\n    const { options } = wrapper.find(ContextMenu).props();\n    assert.isDefined(options.find(o => o.id && o.id === \"newtab-menu-pin\"));\n  });\n  it(\"should show Unbookmark option for a bookmarked site if CheckBookmark in options list\", () => {\n    wrapper = shallow(\n      <LinkMenu\n        site={{ url: \"\", bookmarkGuid: 1234 }}\n        source={\"TOP_SITES\"}\n        options={[\"CheckBookmark\"]}\n        dispatch={() => {}}\n      />\n    );\n    const { options } = wrapper.find(ContextMenu).props();\n    assert.isDefined(\n      options.find(o => o.id && o.id === \"newtab-menu-remove-bookmark\")\n    );\n  });\n  it(\"should show Bookmark option for an unbookmarked site if CheckBookmark in options list\", () => {\n    wrapper = shallow(\n      <LinkMenu\n        site={{ url: \"\", bookmarkGuid: 0 }}\n        source={\"TOP_SITES\"}\n        options={[\"CheckBookmark\"]}\n        dispatch={() => {}}\n      />\n    );\n    const { options } = wrapper.find(ContextMenu).props();\n    assert.isDefined(\n      options.find(o => o.id && o.id === \"newtab-menu-bookmark\")\n    );\n  });\n  it(\"should show Save to Pocket option for an unsaved Pocket item if CheckSavedToPocket in options list\", () => {\n    wrapper = shallow(\n      <LinkMenu\n        site={{ url: \"\", bookmarkGuid: 0 }}\n        source={\"HIGHLIGHTS\"}\n        options={[\"CheckSavedToPocket\"]}\n        dispatch={() => {}}\n      />\n    );\n    const { options } = wrapper.find(ContextMenu).props();\n    assert.isDefined(\n      options.find(o => o.id && o.id === \"newtab-menu-save-to-pocket\")\n    );\n  });\n  it(\"should show Delete from Pocket option for a saved Pocket item if CheckSavedToPocket in options list\", () => {\n    wrapper = shallow(\n      <LinkMenu\n        site={{ url: \"\", pocket_id: 1234 }}\n        source={\"HIGHLIGHTS\"}\n        options={[\"CheckSavedToPocket\"]}\n        dispatch={() => {}}\n      />\n    );\n    const { options } = wrapper.find(ContextMenu).props();\n    assert.isDefined(\n      options.find(o => o.id && o.id === \"newtab-menu-delete-pocket\")\n    );\n  });\n  it(\"should show Archive from Pocket option for a saved Pocket item if CheckBookmarkOrArchive\", () => {\n    wrapper = shallow(\n      <LinkMenu\n        site={{ url: \"\", pocket_id: 1234 }}\n        source={\"HIGHLIGHTS\"}\n        options={[\"CheckBookmarkOrArchive\"]}\n        dispatch={() => {}}\n      />\n    );\n    const { options } = wrapper.find(ContextMenu).props();\n    assert.isDefined(\n      options.find(o => o.id && o.id === \"newtab-menu-archive-pocket\")\n    );\n  });\n  it(\"should show Bookmark option for an unbookmarked site if CheckBookmarkOrArchive in options list and no pocket_id\", () => {\n    wrapper = shallow(\n      <LinkMenu\n        site={{ url: \"\" }}\n        source={\"HIGHLIGHTS\"}\n        options={[\"CheckBookmarkOrArchive\"]}\n        dispatch={() => {}}\n      />\n    );\n    const { options } = wrapper.find(ContextMenu).props();\n    assert.isDefined(\n      options.find(o => o.id && o.id === \"newtab-menu-bookmark\")\n    );\n  });\n  it(\"should show Unbookmark option for a bookmarked site if CheckBookmarkOrArchive in options list and no pocket_id\", () => {\n    wrapper = shallow(\n      <LinkMenu\n        site={{ url: \"\", bookmarkGuid: 1234 }}\n        source={\"HIGHLIGHTS\"}\n        options={[\"CheckBookmarkOrArchive\"]}\n        dispatch={() => {}}\n      />\n    );\n    const { options } = wrapper.find(ContextMenu).props();\n    assert.isDefined(\n      options.find(o => o.id && o.id === \"newtab-menu-remove-bookmark\")\n    );\n  });\n  it(\"should show Open File option for a downloaded item\", () => {\n    wrapper = shallow(\n      <LinkMenu\n        site={{ url: \"\", type: \"download\", path: \"foo\" }}\n        source={\"HIGHLIGHTS\"}\n        options={[\"OpenFile\"]}\n        dispatch={() => {}}\n      />\n    );\n    const { options } = wrapper.find(ContextMenu).props();\n    assert.isDefined(\n      options.find(o => o.id && o.id === \"newtab-menu-open-file\")\n    );\n  });\n  it(\"should show Show File option for a downloaded item on a default platform\", () => {\n    wrapper = shallow(\n      <LinkMenu\n        site={{ url: \"\", type: \"download\", path: \"foo\" }}\n        source={\"HIGHLIGHTS\"}\n        options={[\"ShowFile\"]}\n        platform={\"default\"}\n        dispatch={() => {}}\n      />\n    );\n    const { options } = wrapper.find(ContextMenu).props();\n    assert.isDefined(\n      options.find(o => o.id && o.id === \"newtab-menu-show-file\")\n    );\n  });\n  it(\"should show Copy Downlad Link option for a downloaded item when CopyDownloadLink\", () => {\n    wrapper = shallow(\n      <LinkMenu\n        site={{ url: \"\", type: \"download\" }}\n        source={\"HIGHLIGHTS\"}\n        options={[\"CopyDownloadLink\"]}\n        dispatch={() => {}}\n      />\n    );\n    const { options } = wrapper.find(ContextMenu).props();\n    assert.isDefined(\n      options.find(o => o.id && o.id === \"newtab-menu-copy-download-link\")\n    );\n  });\n  it(\"should show Go To Download Page option for a downloaded item when GoToDownloadPage\", () => {\n    wrapper = shallow(\n      <LinkMenu\n        site={{ url: \"\", type: \"download\", referrer: \"foo\" }}\n        source={\"HIGHLIGHTS\"}\n        options={[\"GoToDownloadPage\"]}\n        dispatch={() => {}}\n      />\n    );\n    const { options } = wrapper.find(ContextMenu).props();\n    assert.isDefined(\n      options.find(o => o.id && o.id === \"newtab-menu-go-to-download-page\")\n    );\n    assert.isFalse(options[0].disabled);\n  });\n  it(\"should show Go To Download Page option as disabled for a downloaded item when GoToDownloadPage if no referrer exists\", () => {\n    wrapper = shallow(\n      <LinkMenu\n        site={{ url: \"\", type: \"download\", referrer: null }}\n        source={\"HIGHLIGHTS\"}\n        options={[\"GoToDownloadPage\"]}\n        dispatch={() => {}}\n      />\n    );\n    const { options } = wrapper.find(ContextMenu).props();\n    assert.isDefined(\n      options.find(o => o.id && o.id === \"newtab-menu-go-to-download-page\")\n    );\n    assert.isTrue(options[0].disabled);\n  });\n  it(\"should show Remove Download Link option for a downloaded item when RemoveDownload\", () => {\n    wrapper = shallow(\n      <LinkMenu\n        site={{ url: \"\", type: \"download\" }}\n        source={\"HIGHLIGHTS\"}\n        options={[\"RemoveDownload\"]}\n        dispatch={() => {}}\n      />\n    );\n    const { options } = wrapper.find(ContextMenu).props();\n    assert.isDefined(\n      options.find(o => o.id && o.id === \"newtab-menu-remove-download\")\n    );\n  });\n  it(\"should show Edit option\", () => {\n    const props = { url: \"foo\", label: \"label\" };\n    const index = 5;\n    wrapper = shallow(\n      <LinkMenu\n        site={props}\n        index={5}\n        source={\"TOP_SITES\"}\n        options={[\"EditTopSite\"]}\n        dispatch={() => {}}\n      />\n    );\n    const { options } = wrapper.find(ContextMenu).props();\n    const option = options.find(\n      o => o.id && o.id === \"newtab-menu-edit-topsites\"\n    );\n    assert.isDefined(option);\n    assert.equal(option.action.data.index, index);\n  });\n  describe(\".onClick\", () => {\n    const FAKE_INDEX = 3;\n    const FAKE_SOURCE = \"TOP_SITES\";\n    const FAKE_SITE = {\n      bookmarkGuid: 1234,\n      hostname: \"foo\",\n      path: \"foo\",\n      pocket_id: \"1234\",\n      referrer: \"https://foo.com/ref\",\n      title: \"bar\",\n      type: \"bookmark\",\n      typedBonus: true,\n      url: \"https://foo.com\",\n    };\n    const dispatch = sinon.stub();\n    const propOptions = [\n      \"ShowFile\",\n      \"CopyDownloadLink\",\n      \"GoToDownloadPage\",\n      \"RemoveDownload\",\n      \"Separator\",\n      \"ShowPrivacyInfo\",\n      \"RemoveBookmark\",\n      \"AddBookmark\",\n      \"OpenInNewWindow\",\n      \"OpenInPrivateWindow\",\n      \"BlockUrl\",\n      \"DeleteUrl\",\n      \"PinTopSite\",\n      \"UnpinTopSite\",\n      \"SaveToPocket\",\n      \"DeleteFromPocket\",\n      \"ArchiveFromPocket\",\n      \"WebExtDismiss\",\n    ];\n    const expectedActionData = {\n      \"newtab-menu-remove-bookmark\": FAKE_SITE.bookmarkGuid,\n      \"newtab-menu-bookmark\": {\n        url: FAKE_SITE.url,\n        title: FAKE_SITE.title,\n        type: FAKE_SITE.type,\n      },\n      \"newtab-menu-open-new-window\": {\n        url: FAKE_SITE.url,\n        referrer: FAKE_SITE.referrer,\n        typedBonus: FAKE_SITE.typedBonus,\n      },\n      \"newtab-menu-open-new-private-window\": {\n        url: FAKE_SITE.url,\n        referrer: FAKE_SITE.referrer,\n      },\n      \"newtab-menu-dismiss\": {\n        url: FAKE_SITE.url,\n        pocket_id: FAKE_SITE.pocket_id,\n      },\n      menu_action_webext_dismiss: {\n        source: \"TOP_SITES\",\n        url: FAKE_SITE.url,\n        action_position: 3,\n      },\n      \"newtab-menu-delete-history\": {\n        url: FAKE_SITE.url,\n        pocket_id: FAKE_SITE.pocket_id,\n        forceBlock: FAKE_SITE.bookmarkGuid,\n      },\n      \"newtab-menu-pin\": { site: { url: FAKE_SITE.url }, index: FAKE_INDEX },\n      \"newtab-menu-unpin\": { site: { url: FAKE_SITE.url } },\n      \"newtab-menu-save-to-pocket\": {\n        site: { url: FAKE_SITE.url, title: FAKE_SITE.title },\n      },\n      \"newtab-menu-delete-pocket\": { pocket_id: \"1234\" },\n      \"newtab-menu-archive-pocket\": { pocket_id: \"1234\" },\n      \"newtab-menu-show-file\": { url: FAKE_SITE.url },\n      \"newtab-menu-copy-download-link\": { url: FAKE_SITE.url },\n      \"newtab-menu-go-to-download-page\": { url: FAKE_SITE.referrer },\n      \"newtab-menu-remove-download\": { url: FAKE_SITE.url },\n    };\n    const { options } = shallow(\n      <LinkMenu\n        site={FAKE_SITE}\n        siteInfo={{ value: { card_type: FAKE_SITE.type } }}\n        dispatch={dispatch}\n        index={FAKE_INDEX}\n        isPrivateBrowsingEnabled={true}\n        platform={\"default\"}\n        options={propOptions}\n        source={FAKE_SOURCE}\n        shouldSendImpressionStats={true}\n      />\n    )\n      .find(ContextMenu)\n      .props();\n    afterEach(() => dispatch.reset());\n    options\n      .filter(o => o.type !== \"separator\")\n      .forEach(option => {\n        it(`should fire a ${option.action.type} action for ${\n          option.id\n        } with the expected data`, () => {\n          option.onClick();\n\n          if (option.impression && option.userEvent) {\n            assert.calledThrice(dispatch);\n          } else if (option.impression || option.userEvent) {\n            assert.calledTwice(dispatch);\n          } else {\n            assert.calledOnce(dispatch);\n          }\n\n          // option.action is dispatched\n          assert.ok(dispatch.firstCall.calledWith(option.action));\n\n          // option.action has correct data\n          // (delete is a special case as it dispatches a nested DIALOG_OPEN-type action)\n          // in the case of this FAKE_SITE, we send a bookmarkGuid therefore we also want\n          // to block this if we delete it\n          if (option.id === \"newtab-menu-delete-history\") {\n            assert.deepEqual(\n              option.action.data.onConfirm[0].data,\n              expectedActionData[option.id]\n            );\n            // Test UserEvent send correct meta about item deleted\n            assert.propertyVal(\n              option.action.data.onConfirm[1].data,\n              \"action_position\",\n              FAKE_INDEX\n            );\n            assert.propertyVal(\n              option.action.data.onConfirm[1].data,\n              \"source\",\n              FAKE_SOURCE\n            );\n          } else {\n            assert.deepEqual(option.action.data, expectedActionData[option.id]);\n          }\n        });\n        it(`should fire a UserEvent action for ${\n          option.id\n        } if configured`, () => {\n          if (option.userEvent) {\n            option.onClick();\n            const [action] = dispatch.secondCall.args;\n            assert.isUserEventAction(action);\n            assert.propertyVal(action.data, \"source\", FAKE_SOURCE);\n            assert.propertyVal(action.data, \"action_position\", FAKE_INDEX);\n            assert.propertyVal(action.data.value, \"card_type\", FAKE_SITE.type);\n          }\n        });\n        it(`should send impression stats for ${option.id}`, () => {\n          if (option.impression) {\n            option.onClick();\n            const [action] = dispatch.thirdCall.args;\n            assert.deepEqual(action, option.impression);\n          }\n        });\n      });\n    it(`should not send impression stats if not configured`, () => {\n      const fakeOptions = shallow(\n        <LinkMenu\n          site={FAKE_SITE}\n          dispatch={dispatch}\n          index={FAKE_INDEX}\n          options={propOptions}\n          source={FAKE_SOURCE}\n          shouldSendImpressionStats={false}\n        />\n      )\n        .find(ContextMenu)\n        .props().options;\n\n      fakeOptions\n        .filter(o => o.type !== \"separator\")\n        .forEach(option => {\n          if (option.impression) {\n            option.onClick();\n            assert.calledTwice(dispatch);\n            assert.notEqual(dispatch.firstCall.args[0], option.impression);\n            assert.notEqual(dispatch.secondCall.args[0], option.impression);\n            dispatch.reset();\n          }\n        });\n    });\n    it(`should pin a SPOC with all of the site details sent`, () => {\n      const pinSpocTopSite = \"PinSpocTopSite\";\n      const { options: spocOptions } = shallow(\n        <LinkMenu\n          site={FAKE_SITE}\n          siteInfo={{ value: { card_type: FAKE_SITE.type } }}\n          dispatch={dispatch}\n          index={FAKE_INDEX}\n          isPrivateBrowsingEnabled={true}\n          platform={\"default\"}\n          options={[pinSpocTopSite]}\n          source={FAKE_SOURCE}\n          shouldSendImpressionStats={true}\n        />\n      )\n        .find(ContextMenu)\n        .props();\n\n      const [pinSpocOption] = spocOptions;\n      pinSpocOption.onClick();\n\n      if (pinSpocOption.impression && pinSpocOption.userEvent) {\n        assert.calledThrice(dispatch);\n      } else if (pinSpocOption.impression || pinSpocOption.userEvent) {\n        assert.calledTwice(dispatch);\n      } else {\n        assert.calledOnce(dispatch);\n      }\n\n      // option.action is dispatched\n      assert.ok(dispatch.firstCall.calledWith(pinSpocOption.action));\n\n      assert.deepEqual(pinSpocOption.action.data, {\n        site: FAKE_SITE,\n        index: FAKE_INDEX,\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/content-src/components/MoreRecommendations.test.jsx",
    "content": "import { MoreRecommendations } from \"content-src/components/MoreRecommendations/MoreRecommendations\";\nimport React from \"react\";\nimport { shallow } from \"enzyme\";\n\ndescribe(\"<MoreRecommendations>\", () => {\n  it(\"should render a MoreRecommendations element\", () => {\n    const wrapper = shallow(<MoreRecommendations />);\n    assert.ok(wrapper.exists());\n  });\n  it(\"should render a link when provided with read_more_endpoint prop\", () => {\n    const wrapper = shallow(\n      <MoreRecommendations read_more_endpoint=\"https://endpoint.com\" />\n    );\n\n    const link = wrapper.find(\".more-recommendations\");\n    assert.lengthOf(link, 1);\n  });\n  it(\"should not render a link when provided with read_more_endpoint prop\", () => {\n    const wrapper = shallow(<MoreRecommendations read_more_endpoint=\"\" />);\n\n    const link = wrapper.find(\".more-recommendations\");\n    assert.lengthOf(link, 0);\n  });\n});\n"
  },
  {
    "path": "test/unit/content-src/components/PocketLoggedInCta.test.jsx",
    "content": "import { combineReducers, createStore } from \"redux\";\nimport { INITIAL_STATE, reducers } from \"common/Reducers.jsm\";\nimport { mount, shallow } from \"enzyme\";\nimport {\n  PocketLoggedInCta,\n  _PocketLoggedInCta as PocketLoggedInCtaRaw,\n} from \"content-src/components/PocketLoggedInCta/PocketLoggedInCta\";\nimport { Provider } from \"react-redux\";\nimport React from \"react\";\n\nfunction mountSectionWithProps(props) {\n  const store = createStore(combineReducers(reducers), INITIAL_STATE);\n  return mount(\n    <Provider store={store}>\n      <PocketLoggedInCta {...props} />\n    </Provider>\n  );\n}\n\ndescribe(\"<PocketLoggedInCta>\", () => {\n  it(\"should render a PocketLoggedInCta element\", () => {\n    const wrapper = mountSectionWithProps({});\n    assert.ok(wrapper.exists());\n  });\n  it(\"should render Fluent spans when rendered without props\", () => {\n    const wrapper = mountSectionWithProps({});\n\n    const message = wrapper.find(\"span[data-l10n-id]\");\n    assert.lengthOf(message, 2);\n  });\n  it(\"should not render Fluent spans when rendered with props\", () => {\n    const wrapper = shallow(\n      <PocketLoggedInCtaRaw\n        Pocket={{\n          pocketCta: {\n            ctaButton: \"button\",\n            ctaText: \"text\",\n          },\n        }}\n      />\n    );\n\n    const message = wrapper.find(\"span[data-l10n-id]\");\n    assert.lengthOf(message, 0);\n  });\n});\n"
  },
  {
    "path": "test/unit/content-src/components/ReturnToAMO.test.jsx",
    "content": "import { mount } from \"enzyme\";\nimport React from \"react\";\nimport { ReturnToAMO } from \"content-src/asrouter/templates/ReturnToAMO/ReturnToAMO\";\n\ndescribe(\"<ReturnToAMO>\", () => {\n  let dispatch;\n  let onReady;\n  let sandbox;\n  let wrapper;\n  let dummyNode;\n  let fakeDocument;\n  let sendUserActionTelemetryStub;\n  let content;\n  beforeEach(() => {\n    sandbox = sinon.createSandbox();\n    dispatch = sandbox.stub();\n    onReady = sandbox.stub();\n    sendUserActionTelemetryStub = sandbox.stub();\n    content = {\n      primary_button: {},\n      secondary_button: {},\n    };\n    dummyNode = document.createElement(\"body\");\n    sandbox.stub(dummyNode, \"querySelector\").returns(dummyNode);\n    fakeDocument = {\n      get activeElement() {\n        return dummyNode;\n      },\n      get body() {\n        return dummyNode;\n      },\n      getElementById() {\n        return dummyNode;\n      },\n    };\n  });\n\n  afterEach(() => {\n    sandbox.restore();\n  });\n\n  describe(\"not mounted\", () => {\n    it(\"should send an IMPRESSION on mount\", () => {\n      assert.notCalled(sendUserActionTelemetryStub);\n\n      wrapper = mount(\n        <ReturnToAMO\n          document={fakeDocument}\n          onReady={onReady}\n          dispatch={dispatch}\n          content={content}\n          onBlock={sandbox.stub()}\n          onAction={sandbox.stub()}\n          UISurface=\"NEWTAB_OVERLAY\"\n          sendUserActionTelemetry={sendUserActionTelemetryStub}\n        />\n      );\n\n      assert.calledOnce(sendUserActionTelemetryStub);\n      assert.calledWithExactly(sendUserActionTelemetryStub, {\n        event: \"IMPRESSION\",\n        id: wrapper.instance().props.UISurface,\n      });\n    });\n  });\n\n  describe(\"mounted\", () => {\n    beforeEach(() => {\n      wrapper = mount(\n        <ReturnToAMO\n          document={fakeDocument}\n          onReady={onReady}\n          dispatch={dispatch}\n          content={content}\n          onBlock={sandbox.stub()}\n          onAction={sandbox.stub()}\n          UISurface=\"NEWTAB_OVERLAY\"\n          sendUserActionTelemetry={sendUserActionTelemetryStub}\n        />\n      );\n\n      // Clear the IMPRESSION ping\n      sendUserActionTelemetryStub.reset();\n    });\n\n    it(\"should send telemetry on block\", () => {\n      wrapper.instance().onBlockButton();\n\n      assert.calledOnce(sendUserActionTelemetryStub);\n      assert.calledWithExactly(sendUserActionTelemetryStub, {\n        event: \"BLOCK\",\n        id: wrapper.instance().props.UISurface,\n      });\n    });\n\n    it(\"should send telemetry on install\", () => {\n      wrapper.instance().onClickAddExtension();\n\n      assert.calledWithExactly(sendUserActionTelemetryStub, {\n        event: \"INSTALL\",\n        id: wrapper.instance().props.UISurface,\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/content-src/components/Search.test.jsx",
    "content": "import { GlobalOverrider } from \"test/unit/utils\";\nimport { mount, shallow } from \"enzyme\";\nimport React from \"react\";\nimport { _Search as Search } from \"content-src/components/Search/Search\";\n\nconst DEFAULT_PROPS = { dispatch() {} };\n\ndescribe(\"<Search>\", () => {\n  let globals;\n  let sandbox;\n  beforeEach(() => {\n    globals = new GlobalOverrider();\n    sandbox = globals.sandbox;\n\n    global.ContentSearchUIController.prototype = { search: sandbox.spy() };\n  });\n  afterEach(() => {\n    globals.restore();\n  });\n\n  it(\"should render a Search element\", () => {\n    const wrapper = shallow(<Search {...DEFAULT_PROPS} />);\n    assert.ok(wrapper.exists());\n  });\n  it(\"should not use a <form> element\", () => {\n    const wrapper = mount(<Search {...DEFAULT_PROPS} />);\n\n    assert.equal(wrapper.find(\"form\").length, 0);\n  });\n  it(\"should listen for ContentSearchClient on render\", () => {\n    const spy = globals.set(\"addEventListener\", sandbox.spy());\n\n    const wrapper = mount(<Search {...DEFAULT_PROPS} />);\n\n    assert.calledOnce(spy.withArgs(\"ContentSearchClient\", wrapper.instance()));\n  });\n  it(\"should stop listening for ContentSearchClient on unmount\", () => {\n    const spy = globals.set(\"removeEventListener\", sandbox.spy());\n    const wrapper = mount(<Search {...DEFAULT_PROPS} />);\n    // cache the instance as we can't call this method after unmount is called\n    const instance = wrapper.instance();\n\n    wrapper.unmount();\n\n    assert.calledOnce(spy.withArgs(\"ContentSearchClient\", instance));\n  });\n  it(\"should add gContentSearchController as a global\", () => {\n    // current about:home tests need gContentSearchController to exist as a global\n    // so let's test it here too to ensure we don't break this behaviour\n    mount(<Search {...DEFAULT_PROPS} />);\n    assert.property(window, \"gContentSearchController\");\n    assert.ok(window.gContentSearchController);\n  });\n  it(\"should pass along search when clicking the search button\", () => {\n    const wrapper = mount(<Search {...DEFAULT_PROPS} />);\n\n    wrapper.find(\".search-button\").simulate(\"click\");\n\n    const { search } = window.gContentSearchController;\n    assert.calledOnce(search);\n    assert.propertyVal(search.firstCall.args[0], \"type\", \"click\");\n  });\n  it(\"should send a UserEvent action\", () => {\n    global.ContentSearchUIController.prototype.search = () => {\n      dispatchEvent(\n        new CustomEvent(\"ContentSearchClient\", { detail: { type: \"Search\" } })\n      );\n    };\n    const dispatch = sinon.spy();\n    const wrapper = mount(<Search {...DEFAULT_PROPS} dispatch={dispatch} />);\n\n    wrapper.find(\".search-button\").simulate(\"click\");\n\n    assert.calledOnce(dispatch);\n    const [action] = dispatch.firstCall.args;\n    assert.isUserEventAction(action);\n    assert.propertyVal(action.data, \"event\", \"SEARCH\");\n  });\n\n  describe(\"Search Hand-off\", () => {\n    it(\"should render a Search element when hand-off is enabled\", () => {\n      const wrapper = shallow(\n        <Search {...DEFAULT_PROPS} handoffEnabled={true} />\n      );\n      assert.ok(wrapper.exists());\n      assert.equal(wrapper.find(\".search-handoff-button\").length, 1);\n    });\n    it(\"should hand-off search when button is clicked\", () => {\n      const dispatch = sinon.spy();\n      const wrapper = shallow(\n        <Search {...DEFAULT_PROPS} handoffEnabled={true} dispatch={dispatch} />\n      );\n      wrapper\n        .find(\".search-handoff-button\")\n        .simulate(\"click\", { preventDefault: () => {} });\n      assert.calledThrice(dispatch);\n      assert.calledWith(dispatch, {\n        data: { text: undefined },\n        meta: {\n          from: \"ActivityStream:Content\",\n          skipLocal: true,\n          to: \"ActivityStream:Main\",\n        },\n        type: \"HANDOFF_SEARCH_TO_AWESOMEBAR\",\n      });\n      assert.calledWith(dispatch, { type: \"FAKE_FOCUS_SEARCH\" });\n      const [action] = dispatch.thirdCall.args;\n      assert.isUserEventAction(action);\n      assert.propertyVal(action.data, \"event\", \"SEARCH_HANDOFF\");\n    });\n    it(\"should hand-off search on paste\", () => {\n      const dispatch = sinon.spy();\n      const wrapper = mount(\n        <Search {...DEFAULT_PROPS} handoffEnabled={true} dispatch={dispatch} />\n      );\n      wrapper.instance()._searchHandoffButton = { contains: () => true };\n      wrapper.instance().onSearchHandoffPaste({\n        clipboardData: {\n          getData: () => \"some copied text\",\n        },\n        preventDefault: () => {},\n      });\n      assert.equal(dispatch.callCount, 4);\n      assert.calledWith(dispatch, {\n        data: { text: \"some copied text\" },\n        meta: {\n          from: \"ActivityStream:Content\",\n          skipLocal: true,\n          to: \"ActivityStream:Main\",\n        },\n        type: \"HANDOFF_SEARCH_TO_AWESOMEBAR\",\n      });\n      assert.calledWith(dispatch, { type: \"HIDE_SEARCH\" });\n      const [action] = dispatch.thirdCall.args;\n      assert.isUserEventAction(action);\n      assert.propertyVal(action.data, \"event\", \"SEARCH_HANDOFF\");\n    });\n    it(\"should properly handle drop events\", () => {\n      const dispatch = sinon.spy();\n      const wrapper = mount(\n        <Search {...DEFAULT_PROPS} handoffEnabled={true} dispatch={dispatch} />\n      );\n      const preventDefault = sinon.spy();\n      wrapper.find(\".fake-editable\").simulate(\"drop\", {\n        dataTransfer: { getData: () => \"dropped text\" },\n        preventDefault,\n      });\n      assert.equal(dispatch.callCount, 4);\n      assert.calledWith(dispatch, {\n        data: { text: \"dropped text\" },\n        meta: {\n          from: \"ActivityStream:Content\",\n          skipLocal: true,\n          to: \"ActivityStream:Main\",\n        },\n        type: \"HANDOFF_SEARCH_TO_AWESOMEBAR\",\n      });\n      assert.calledWith(dispatch, { type: \"HIDE_SEARCH\" });\n      const [action] = dispatch.thirdCall.args;\n      assert.isUserEventAction(action);\n      assert.propertyVal(action.data, \"event\", \"SEARCH_HANDOFF\");\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/content-src/components/SectionMenu.test.jsx",
    "content": "import { ContextMenu } from \"content-src/components/ContextMenu/ContextMenu\";\nimport React from \"react\";\nimport { _SectionMenu as SectionMenu } from \"content-src/components/SectionMenu/SectionMenu\";\nimport { shallow } from \"enzyme\";\n\nconst DEFAULT_PROPS = {\n  name: \"Section Name\",\n  id: \"sectionId\",\n  source: \"TOP_SITES\",\n  showPrefName: \"showSection\",\n  collapsePrefName: \"collapseSection\",\n  collapsed: false,\n  onUpdate: () => {},\n  visible: false,\n  dispatch: () => {},\n};\n\ndescribe(\"<SectionMenu>\", () => {\n  let wrapper;\n  beforeEach(() => {\n    wrapper = shallow(<SectionMenu {...DEFAULT_PROPS} />);\n  });\n  it(\"should render a ContextMenu element\", () => {\n    assert.ok(wrapper.find(ContextMenu).exists());\n  });\n  it(\"should pass onUpdate, and options to ContextMenu\", () => {\n    assert.ok(wrapper.find(ContextMenu).exists());\n    const contextMenuProps = wrapper.find(ContextMenu).props();\n    [\"onUpdate\", \"options\"].forEach(prop =>\n      assert.property(contextMenuProps, prop)\n    );\n  });\n  it(\"should give ContextMenu the correct tabbable options length for a11y\", () => {\n    const { options } = wrapper.find(ContextMenu).props();\n    const [firstItem] = options;\n    const lastItem = options[options.length - 1];\n\n    // first item should have {first: true}\n    assert.isTrue(firstItem.first);\n    assert.ok(!firstItem.last);\n\n    // last item should have {last: true}\n    assert.isTrue(lastItem.last);\n    assert.ok(!lastItem.first);\n\n    // middle items should have neither\n    for (let i = 1; i < options.length - 1; i++) {\n      assert.ok(!options[i].first && !options[i].last);\n    }\n  });\n  it(\"should show the correct default options\", () => {\n    wrapper = shallow(<SectionMenu {...DEFAULT_PROPS} />);\n    const { options } = wrapper.find(ContextMenu).props();\n    let i = 0;\n    assert.propertyVal(options[i++], \"id\", \"newtab-section-menu-move-up\");\n    assert.propertyVal(options[i++], \"id\", \"newtab-section-menu-move-down\");\n    assert.propertyVal(options[i++], \"type\", \"separator\");\n    assert.propertyVal(\n      options[i++],\n      \"id\",\n      \"newtab-section-menu-remove-section\"\n    );\n    assert.propertyVal(\n      options[i++],\n      \"id\",\n      \"newtab-section-menu-collapse-section\"\n    );\n    assert.propertyVal(options[i++], \"type\", \"separator\");\n    assert.propertyVal(\n      options[i++],\n      \"id\",\n      \"newtab-section-menu-manage-section\"\n    );\n    assert.propertyVal(options, \"length\", i);\n  });\n  it(\"should show the correct default options for a web extension\", () => {\n    wrapper = shallow(<SectionMenu {...DEFAULT_PROPS} isWebExtension={true} />);\n    const { options } = wrapper.find(ContextMenu).props();\n    let i = 0;\n    assert.propertyVal(options[i++], \"id\", \"newtab-section-menu-move-up\");\n    assert.propertyVal(options[i++], \"id\", \"newtab-section-menu-move-down\");\n    assert.propertyVal(options[i++], \"type\", \"separator\");\n    assert.propertyVal(\n      options[i++],\n      \"id\",\n      \"newtab-section-menu-collapse-section\"\n    );\n    assert.propertyVal(options[i++], \"type\", \"separator\");\n    assert.propertyVal(options[i++], \"id\", \"newtab-section-menu-manage-webext\");\n    assert.propertyVal(options, \"length\", i);\n  });\n  it(\"should show Collapse option for an expanded section if CheckCollapsed in options list\", () => {\n    wrapper = shallow(<SectionMenu {...DEFAULT_PROPS} collapsed={false} />);\n    const { options } = wrapper.find(ContextMenu).props();\n    assert.isDefined(\n      options.find(o => o.id && o.id === \"newtab-section-menu-collapse-section\")\n    );\n  });\n  it(\"should show Expand option for a collapsed section if CheckCollapsed in options list\", () => {\n    wrapper = shallow(<SectionMenu {...DEFAULT_PROPS} collapsed={true} />);\n    const { options } = wrapper.find(ContextMenu).props();\n    assert.isDefined(\n      options.find(o => o.id && o.id === \"newtab-section-menu-expand-section\")\n    );\n  });\n  it(\"should show Add Top Site option\", () => {\n    wrapper = shallow(\n      <SectionMenu {...DEFAULT_PROPS} extraOptions={[\"AddTopSite\"]} />\n    );\n    const { options } = wrapper.find(ContextMenu).props();\n    assert.equal(options[0].id, \"newtab-section-menu-add-topsite\");\n  });\n  it(\"should show Add Search Engine option\", () => {\n    wrapper = shallow(\n      <SectionMenu {...DEFAULT_PROPS} extraOptions={[\"AddSearchShortcut\"]} />\n    );\n    const { options } = wrapper.find(ContextMenu).props();\n    assert.equal(options[0].id, \"newtab-section-menu-add-search-engine\");\n  });\n  it(\"should show Privacy Notice option if privacyNoticeURL is passed\", () => {\n    wrapper = shallow(\n      <SectionMenu\n        {...DEFAULT_PROPS}\n        privacyNoticeURL=\"https://mozilla.org/privacy\"\n      />\n    );\n    const { options } = wrapper.find(ContextMenu).props();\n    let i = 0;\n    assert.propertyVal(options[i++], \"id\", \"newtab-section-menu-move-up\");\n    assert.propertyVal(options[i++], \"id\", \"newtab-section-menu-move-down\");\n    assert.propertyVal(options[i++], \"type\", \"separator\");\n    assert.propertyVal(\n      options[i++],\n      \"id\",\n      \"newtab-section-menu-remove-section\"\n    );\n    assert.propertyVal(\n      options[i++],\n      \"id\",\n      \"newtab-section-menu-collapse-section\"\n    );\n    assert.propertyVal(options[i++], \"type\", \"separator\");\n    assert.propertyVal(\n      options[i++],\n      \"id\",\n      \"newtab-section-menu-privacy-notice\"\n    );\n    assert.propertyVal(\n      options[i++],\n      \"id\",\n      \"newtab-section-menu-manage-section\"\n    );\n    assert.propertyVal(options, \"length\", i);\n  });\n  it(\"should disable Move Up on first section\", () => {\n    wrapper = shallow(<SectionMenu {...DEFAULT_PROPS} isFirst={true} />);\n    const { options } = wrapper.find(ContextMenu).props();\n    assert.ok(options[0].disabled);\n  });\n  it(\"should disable Move Down on last section\", () => {\n    wrapper = shallow(<SectionMenu {...DEFAULT_PROPS} isLast={true} />);\n    const { options } = wrapper.find(ContextMenu).props();\n    assert.ok(options[1].disabled);\n  });\n  describe(\".onClick\", () => {\n    const dispatch = sinon.stub();\n    const expectedActionData = {\n      \"newtab-section-menu-move-up\": { id: \"sectionId\", direction: -1 },\n      \"newtab-section-menu-move-down\": { id: \"sectionId\", direction: +1 },\n      \"newtab-section-menu-remove-section\": {\n        name: \"showSection\",\n        value: false,\n      },\n      \"newtab-section-menu-collapse-section\": {\n        id: DEFAULT_PROPS.id,\n        value: { collapsed: true },\n      },\n      \"newtab-section-menu-expand-section\": {\n        id: DEFAULT_PROPS.id,\n        value: { collapsed: false },\n      },\n      \"newtab-section-menu-manage-section\": undefined,\n      \"newtab-section-menu-add-topsite\": { index: -1 },\n      \"newtab-section-menu-privacy-notice\": {\n        url: DEFAULT_PROPS.privacyNoticeURL,\n      },\n    };\n    const { options } = shallow(\n      <SectionMenu {...DEFAULT_PROPS} dispatch={dispatch} />\n    )\n      .find(ContextMenu)\n      .props();\n    afterEach(() => dispatch.reset());\n    options\n      .filter(o => o.type !== \"separator\")\n      .forEach(option => {\n        it(`should fire a ${option.action.type} action for ${\n          option.id\n        } with the expected data`, () => {\n          option.onClick();\n\n          if (option.userEvent && option.action) {\n            assert.calledTwice(dispatch);\n          } else if (option.userEvent || option.action) {\n            assert.calledOnce(dispatch);\n          } else {\n            assert.notCalled(dispatch);\n          }\n\n          // option.action is dispatched\n          assert.ok(dispatch.firstCall.calledWith(option.action));\n          assert.deepEqual(option.action.data, expectedActionData[option.id]);\n        });\n        it(`should fire a UserEvent action for ${\n          option.id\n        } if configured`, () => {\n          if (option.userEvent) {\n            option.onClick();\n            const [action] = dispatch.secondCall.args;\n            assert.isUserEventAction(action);\n            assert.propertyVal(action.data, \"source\", DEFAULT_PROPS.source);\n          }\n        });\n      });\n  });\n  describe(\"dispatch expand section if section is collapsed and adding top site\", () => {\n    const dispatch = sinon.stub();\n    const expectedExpandData = {\n      id: DEFAULT_PROPS.id,\n      value: { collapsed: false },\n    };\n    const expectedAddData = { index: -1 };\n    const { options } = shallow(\n      <SectionMenu\n        {...DEFAULT_PROPS}\n        collapsed={true}\n        dispatch={dispatch}\n        extraOptions={[\"AddTopSite\"]}\n      />\n    )\n      .find(ContextMenu)\n      .props();\n    afterEach(() => dispatch.reset());\n\n    assert.equal(options[0].id, \"newtab-section-menu-add-topsite\");\n    options\n      .filter(o => o.id === \"newtab-section-menu-add-topsite\")\n      .forEach(option => {\n        it(`should dispatch an action to expand the section and to add a topsite after expanding`, () => {\n          option.onClick();\n\n          const [expandAction] = dispatch.firstCall.args;\n          assert.deepEqual(expandAction.data, expectedExpandData);\n\n          const [addAction] = dispatch.thirdCall.args;\n          assert.deepEqual(addAction.data, expectedAddData);\n        });\n        it(`should dispatch the expand userEvent and add topsite userEvent after expanding`, () => {\n          option.onClick();\n          assert.ok(dispatch.thirdCall.calledWith(option.action));\n\n          const [expandUserEvent] = dispatch.secondCall.args;\n          assert.isUserEventAction(expandUserEvent);\n          assert.propertyVal(\n            expandUserEvent.data,\n            \"source\",\n            DEFAULT_PROPS.source\n          );\n\n          const [addUserEvent] = dispatch.lastCall.args;\n          assert.isUserEventAction(addUserEvent);\n          assert.propertyVal(addUserEvent.data, \"source\", DEFAULT_PROPS.source);\n        });\n      });\n  });\n});\n"
  },
  {
    "path": "test/unit/content-src/components/Sections.test.jsx",
    "content": "import { combineReducers, createStore } from \"redux\";\nimport { INITIAL_STATE, reducers } from \"common/Reducers.jsm\";\nimport {\n  Section,\n  SectionIntl,\n  _Sections as Sections,\n} from \"content-src/components/Sections/Sections\";\nimport { actionTypes as at } from \"common/Actions.jsm\";\nimport { mount, shallow } from \"enzyme\";\nimport { PlaceholderCard } from \"content-src/components/Card/Card\";\nimport { PocketLoggedInCta } from \"content-src/components/PocketLoggedInCta/PocketLoggedInCta\";\nimport { Provider } from \"react-redux\";\nimport React from \"react\";\nimport { SectionMenu } from \"content-src/components/SectionMenu/SectionMenu\";\nimport { Topics } from \"content-src/components/Topics/Topics\";\nimport { TopSites } from \"content-src/components/TopSites/TopSites\";\n\nfunction mountSectionWithProps(props) {\n  const store = createStore(combineReducers(reducers), INITIAL_STATE);\n  return mount(\n    <Provider store={store}>\n      <Section {...props} />\n    </Provider>\n  );\n}\n\nfunction mountSectionIntlWithProps(props) {\n  const store = createStore(combineReducers(reducers), INITIAL_STATE);\n  return mount(\n    <Provider store={store}>\n      <SectionIntl {...props} />\n    </Provider>\n  );\n}\n\ndescribe(\"<Sections>\", () => {\n  let wrapper;\n  let FAKE_SECTIONS;\n  beforeEach(() => {\n    FAKE_SECTIONS = new Array(5).fill(null).map((v, i) => ({\n      id: `foo_bar_${i}`,\n      title: `Foo Bar ${i}`,\n      enabled: !!(i % 2),\n      rows: [],\n    }));\n    wrapper = shallow(\n      <Sections\n        Sections={FAKE_SECTIONS}\n        Prefs={{\n          values: { sectionOrder: FAKE_SECTIONS.map(i => i.id).join(\",\") },\n        }}\n      />\n    );\n  });\n  it(\"should render a Sections element\", () => {\n    assert.ok(wrapper.exists());\n  });\n  it(\"should render a Section for each one passed in props.Sections with .enabled === true\", () => {\n    const sectionElems = wrapper.find(SectionIntl);\n    assert.lengthOf(sectionElems, 2);\n    sectionElems.forEach((section, i) => {\n      assert.equal(section.props().id, FAKE_SECTIONS[2 * i + 1].id);\n      assert.equal(section.props().enabled, true);\n    });\n  });\n  it(\"should render Top Sites if feeds.topsites pref is true\", () => {\n    wrapper = shallow(\n      <Sections\n        Sections={FAKE_SECTIONS}\n        Prefs={{\n          values: {\n            \"feeds.topsites\": true,\n            sectionOrder: \"topsites,topstories,highlights\",\n          },\n        }}\n      />\n    );\n    assert.equal(wrapper.find(TopSites).length, 1);\n  });\n  it(\"should NOT render Top Sites if feeds.topsites pref is false\", () => {\n    wrapper = shallow(\n      <Sections\n        Sections={FAKE_SECTIONS}\n        Prefs={{\n          values: {\n            \"feeds.topsites\": false,\n            sectionOrder: \"topsites,topstories,highlights\",\n          },\n        }}\n      />\n    );\n    assert.equal(wrapper.find(TopSites).length, 0);\n  });\n  it(\"should render the sections in the order specifed by sectionOrder pref\", () => {\n    wrapper = shallow(\n      <Sections\n        Sections={FAKE_SECTIONS}\n        Prefs={{ values: { sectionOrder: \"foo_bar_1,foo_bar_3\" } }}\n      />\n    );\n    let sections = wrapper.find(SectionIntl);\n    assert.lengthOf(sections, 2);\n    assert.equal(sections.first().props().id, \"foo_bar_1\");\n    assert.equal(sections.last().props().id, \"foo_bar_3\");\n    wrapper = shallow(\n      <Sections\n        Sections={FAKE_SECTIONS}\n        Prefs={{ values: { sectionOrder: \"foo_bar_3,foo_bar_1\" } }}\n      />\n    );\n    sections = wrapper.find(SectionIntl);\n    assert.lengthOf(sections, 2);\n    assert.equal(sections.first().props().id, \"foo_bar_3\");\n    assert.equal(sections.last().props().id, \"foo_bar_1\");\n  });\n});\n\ndescribe(\"<Section>\", () => {\n  let wrapper;\n  let FAKE_SECTION;\n\n  beforeEach(() => {\n    FAKE_SECTION = {\n      id: `foo_bar_1`,\n      pref: { collapsed: false },\n      title: `Foo Bar 1`,\n      rows: [{ link: \"http://localhost\", index: 0 }],\n      emptyState: {\n        icon: \"check\",\n        message: \"Some message\",\n      },\n      rowsPref: \"section.rows\",\n      maxRows: 4,\n      Prefs: { values: { \"section.rows\": 2 } },\n    };\n    wrapper = mountSectionIntlWithProps(FAKE_SECTION);\n  });\n\n  describe(\"context menu\", () => {\n    it(\"should render a context menu button\", () => {\n      wrapper = mountSectionIntlWithProps(FAKE_SECTION);\n\n      assert.equal(\n        wrapper.find(\".section-top-bar .context-menu-button\").length,\n        1\n      );\n    });\n    it(\"should render a section menu when button is clicked\", () => {\n      wrapper = mountSectionIntlWithProps(FAKE_SECTION);\n\n      const button = wrapper.find(\".section-top-bar .context-menu-button\");\n      assert.equal(wrapper.find(SectionMenu).length, 0);\n      button.simulate(\"click\", { preventDefault: () => {} });\n      assert.equal(wrapper.find(SectionMenu).length, 1);\n    });\n    it(\"should not render a section menu by default\", () => {\n      wrapper = shallow(<Section {...FAKE_SECTION} />);\n      assert.equal(wrapper.find(SectionMenu).length, 0);\n    });\n  });\n\n  describe(\"placeholders\", () => {\n    const CARDS_PER_ROW = 3;\n    const fakeSite = { link: \"http://localhost\" };\n    function renderWithSites(rows) {\n      const store = createStore(combineReducers(reducers), INITIAL_STATE);\n      return mount(\n        <Provider store={store}>\n          <Section {...FAKE_SECTION} rows={rows} />\n        </Provider>\n      );\n    }\n\n    it(\"should return 2 row of placeholders if realRows is 0\", () => {\n      wrapper = renderWithSites([]);\n      assert.lengthOf(wrapper.find(PlaceholderCard), 6);\n    });\n    it(\"should fill in the rest of the rows\", () => {\n      wrapper = renderWithSites(new Array(CARDS_PER_ROW).fill(fakeSite));\n      assert.lengthOf(\n        wrapper.find(PlaceholderCard),\n        CARDS_PER_ROW,\n        \"CARDS_PER_ROW\"\n      );\n\n      wrapper = renderWithSites(new Array(CARDS_PER_ROW + 1).fill(fakeSite));\n      assert.lengthOf(wrapper.find(PlaceholderCard), 2, \"CARDS_PER_ROW + 1\");\n\n      wrapper = renderWithSites(new Array(CARDS_PER_ROW + 2).fill(fakeSite));\n      assert.lengthOf(wrapper.find(PlaceholderCard), 1, \"CARDS_PER_ROW + 2\");\n\n      wrapper = renderWithSites(\n        new Array(2 * CARDS_PER_ROW - 1).fill(fakeSite)\n      );\n      assert.lengthOf(wrapper.find(PlaceholderCard), 1, \"CARDS_PER_ROW - 1\");\n    });\n    it(\"should not add placeholders all the rows are full\", () => {\n      wrapper = renderWithSites(new Array(2 * CARDS_PER_ROW).fill(fakeSite));\n      assert.lengthOf(wrapper.find(PlaceholderCard), 0, \"2 rows\");\n    });\n  });\n\n  describe(\"empty state\", () => {\n    beforeEach(() => {\n      Object.assign(FAKE_SECTION, {\n        initialized: true,\n        dispatch: () => {},\n        rows: [],\n        emptyState: {\n          message: \"Some message\",\n          icon: \"moz-extension://some/extension/path\",\n        },\n      });\n      wrapper = shallow(<Section {...FAKE_SECTION} />);\n    });\n    it(\"should be shown when rows is empty and initialized is true\", () => {\n      assert.ok(wrapper.find(\".empty-state\").exists());\n    });\n    it(\"should not be shown in initialized is false\", () => {\n      Object.assign(FAKE_SECTION, {\n        initialized: false,\n        rows: [],\n        emptyState: {\n          message: \"Some message\",\n          icon: \"moz-extension://some/extension/path\",\n        },\n      });\n      wrapper = shallow(<Section {...FAKE_SECTION} />);\n      assert.isFalse(wrapper.find(\".empty-state\").exists());\n    });\n    it(\"should use the icon prop as the icon url if it starts with `moz-extension://`\", () => {\n      const props = wrapper\n        .find(\".empty-state-icon\")\n        .first()\n        .props();\n      assert.equal(\n        props.style[\"background-image\"],\n        `url('${FAKE_SECTION.emptyState.icon}')`\n      );\n    });\n  });\n\n  describe(\"topics component\", () => {\n    let TOP_STORIES_SECTION;\n    beforeEach(() => {\n      TOP_STORIES_SECTION = {\n        id: \"topstories\",\n        title: \"TopStories\",\n        pref: { collapsed: false },\n        rows: [{ guid: 1, link: \"http://localhost\", isDefault: true }],\n        topics: [],\n        read_more_endpoint: \"http://localhost/read-more\",\n        maxRows: 1,\n        eventSource: \"TOP_STORIES\",\n      };\n    });\n    it(\"should not render for empty topics\", () => {\n      wrapper = mountSectionIntlWithProps(TOP_STORIES_SECTION);\n\n      assert.lengthOf(wrapper.find(\".topic\"), 0);\n    });\n    it(\"should render for non-empty topics\", () => {\n      TOP_STORIES_SECTION.topics = [{ name: \"topic1\", url: \"topic-url1\" }];\n      wrapper = shallow(\n        <Section\n          Pocket={{ pocketCta: { useCta: true }, isUserLoggedIn: true }}\n          {...TOP_STORIES_SECTION}\n        />\n      );\n\n      assert.lengthOf(wrapper.find(Topics), 1);\n      assert.lengthOf(wrapper.find(PocketLoggedInCta), 0);\n    });\n    it(\"should delay render of third rec to give time for potential spoc\", async () => {\n      TOP_STORIES_SECTION.rows = [\n        { guid: 1, link: \"http://localhost\" },\n        { guid: 2, link: \"http://localhost\" },\n        { guid: 3, link: \"http://localhost\" },\n      ];\n      wrapper = shallow(\n        <Section\n          Pocket={{ waitingForSpoc: true, pocketCta: {} }}\n          {...TOP_STORIES_SECTION}\n        />\n      );\n      assert.lengthOf(wrapper.find(PlaceholderCard), 1);\n\n      wrapper.setProps({\n        Pocket: {\n          waitingForSpoc: false,\n          pocketCta: {},\n        },\n      });\n      assert.lengthOf(wrapper.find(PlaceholderCard), 0);\n    });\n    it(\"should render container for uninitialized topics to ensure content doesn't shift\", () => {\n      delete TOP_STORIES_SECTION.topics;\n\n      wrapper = mountSectionIntlWithProps(TOP_STORIES_SECTION);\n\n      assert.lengthOf(wrapper.find(\".top-stories-bottom-container\"), 1);\n      assert.lengthOf(wrapper.find(Topics), 0);\n      assert.lengthOf(wrapper.find(PocketLoggedInCta), 0);\n    });\n\n    it(\"should render a pocket cta if not logged in and set to display cta\", () => {\n      TOP_STORIES_SECTION.topics = [{ name: \"topic1\", url: \"topic-url1\" }];\n      wrapper = shallow(\n        <Section\n          Pocket={{ pocketCta: { useCta: true }, isUserLoggedIn: false }}\n          {...TOP_STORIES_SECTION}\n        />\n      );\n\n      assert.lengthOf(wrapper.find(Topics), 0);\n      assert.lengthOf(wrapper.find(PocketLoggedInCta), 1);\n    });\n    it(\"should render nothing while loading to avoid a flicker of log in state\", () => {\n      TOP_STORIES_SECTION.topics = [{ name: \"topic1\", url: \"topic-url1\" }];\n      wrapper = shallow(\n        <Section\n          Pocket={{ pocketCta: { useCta: false } }}\n          {...TOP_STORIES_SECTION}\n        />\n      );\n\n      assert.lengthOf(wrapper.find(Topics), 0);\n      assert.lengthOf(wrapper.find(PocketLoggedInCta), 0);\n    });\n    it(\"should render a topics list if set to not display cta with either logged or out\", () => {\n      TOP_STORIES_SECTION.topics = [{ name: \"topic1\", url: \"topic-url1\" }];\n      wrapper = shallow(\n        <Section\n          Pocket={{ pocketCta: { useCta: false }, isUserLoggedIn: false }}\n          {...TOP_STORIES_SECTION}\n        />\n      );\n\n      assert.lengthOf(wrapper.find(Topics), 1);\n      assert.lengthOf(wrapper.find(PocketLoggedInCta), 0);\n\n      wrapper = shallow(\n        <Section\n          Pocket={{ pocketCta: { useCta: false }, isUserLoggedIn: true }}\n          {...TOP_STORIES_SECTION}\n        />\n      );\n\n      assert.lengthOf(wrapper.find(Topics), 1);\n      assert.lengthOf(wrapper.find(PocketLoggedInCta), 0);\n    });\n    it(\"should render nothing if set to display a cta and not logged in or out (waiting for state)\", () => {\n      TOP_STORIES_SECTION.topics = [{ name: \"topic1\", url: \"topic-url1\" }];\n      wrapper = shallow(\n        <Section\n          Pocket={{ pocketCta: { useCta: true } }}\n          {...TOP_STORIES_SECTION}\n        />\n      );\n\n      assert.lengthOf(wrapper.find(Topics), 0);\n      assert.lengthOf(wrapper.find(PocketLoggedInCta), 0);\n    });\n  });\n\n  describe(\"impression stats\", () => {\n    const FAKE_TOPSTORIES_SECTION_PROPS = {\n      id: \"TopStories\",\n      title: \"Foo Bar 1\",\n      pref: { collapsed: false },\n      maxRows: 1,\n      rows: [{ guid: 1 }, { guid: 2 }],\n      shouldSendImpressionStats: true,\n\n      document: {\n        visibilityState: \"visible\",\n        addEventListener: sinon.stub(),\n        removeEventListener: sinon.stub(),\n      },\n      eventSource: \"TOP_STORIES\",\n      options: { personalized: false },\n    };\n\n    function renderSection(props = {}) {\n      return shallow(<Section {...FAKE_TOPSTORIES_SECTION_PROPS} {...props} />);\n    }\n\n    it(\"should send impression with the right stats when the page loads\", () => {\n      const dispatch = sinon.spy();\n      renderSection({ dispatch });\n\n      assert.calledOnce(dispatch);\n\n      const [action] = dispatch.firstCall.args;\n      assert.equal(action.type, at.TELEMETRY_IMPRESSION_STATS);\n      assert.equal(action.data.source, \"TOP_STORIES\");\n      assert.deepEqual(action.data.tiles, [{ id: 1 }, { id: 2 }]);\n    });\n    it(\"should not send impression stats if not configured\", () => {\n      const dispatch = sinon.spy();\n      const props = Object.assign({}, FAKE_TOPSTORIES_SECTION_PROPS, {\n        shouldSendImpressionStats: false,\n        dispatch,\n      });\n      renderSection(props);\n      assert.notCalled(dispatch);\n    });\n    it(\"should not send impression stats if the section is collapsed\", () => {\n      const dispatch = sinon.spy();\n      const props = Object.assign({}, FAKE_TOPSTORIES_SECTION_PROPS, {\n        pref: { collapsed: true },\n      });\n      renderSection(props);\n      assert.notCalled(dispatch);\n    });\n    it(\"should send 1 impression when the page becomes visibile after loading\", () => {\n      const props = {\n        dispatch: sinon.spy(),\n        document: {\n          visibilityState: \"hidden\",\n          addEventListener: sinon.spy(),\n          removeEventListener: sinon.spy(),\n        },\n      };\n\n      renderSection(props);\n\n      // Was the event listener added?\n      assert.calledWith(props.document.addEventListener, \"visibilitychange\");\n\n      // Make sure dispatch wasn't called yet\n      assert.notCalled(props.dispatch);\n\n      // Simulate a visibilityChange event\n      const [, listener] = props.document.addEventListener.firstCall.args;\n      props.document.visibilityState = \"visible\";\n      listener();\n\n      // Did we actually dispatch an event?\n      assert.calledOnce(props.dispatch);\n      assert.equal(\n        props.dispatch.firstCall.args[0].type,\n        at.TELEMETRY_IMPRESSION_STATS\n      );\n\n      // Did we remove the event listener?\n      assert.calledWith(\n        props.document.removeEventListener,\n        \"visibilitychange\",\n        listener\n      );\n    });\n    it(\"should remove visibility change listener when section is removed\", () => {\n      const props = {\n        dispatch: sinon.spy(),\n        document: {\n          visibilityState: \"hidden\",\n          addEventListener: sinon.spy(),\n          removeEventListener: sinon.spy(),\n        },\n      };\n\n      const section = renderSection(props);\n      assert.calledWith(props.document.addEventListener, \"visibilitychange\");\n      const [, listener] = props.document.addEventListener.firstCall.args;\n\n      section.unmount();\n      assert.calledWith(\n        props.document.removeEventListener,\n        \"visibilitychange\",\n        listener\n      );\n    });\n    it(\"should send an impression if props are updated and props.rows are different\", () => {\n      const props = { dispatch: sinon.spy() };\n      wrapper = renderSection(props);\n      props.dispatch.resetHistory();\n\n      // New rows\n      wrapper.setProps(\n        Object.assign({}, FAKE_TOPSTORIES_SECTION_PROPS, {\n          rows: [{ guid: 123 }],\n        })\n      );\n\n      assert.calledOnce(props.dispatch);\n    });\n    it(\"should not send an impression if props are updated but props.rows are the same\", () => {\n      const props = { dispatch: sinon.spy() };\n      wrapper = renderSection(props);\n      props.dispatch.resetHistory();\n\n      // Only update the disclaimer prop\n      wrapper.setProps(\n        Object.assign({}, FAKE_TOPSTORIES_SECTION_PROPS, {\n          disclaimer: { id: \"bar\" },\n        })\n      );\n\n      assert.notCalled(props.dispatch);\n    });\n    it(\"should not send an impression if props are updated and props.rows are the same but section is collapsed\", () => {\n      const props = { dispatch: sinon.spy() };\n      wrapper = renderSection(props);\n      props.dispatch.resetHistory();\n\n      // New rows and collapsed\n      wrapper.setProps(\n        Object.assign({}, FAKE_TOPSTORIES_SECTION_PROPS, {\n          rows: [{ guid: 123 }],\n          pref: { collapsed: true },\n        })\n      );\n\n      assert.notCalled(props.dispatch);\n\n      // Expand the section. Now the impression stats should be sent\n      wrapper.setProps(\n        Object.assign({}, FAKE_TOPSTORIES_SECTION_PROPS, {\n          rows: [{ guid: 123 }],\n          pref: { collapsed: false },\n        })\n      );\n\n      assert.calledOnce(props.dispatch);\n    });\n    it(\"should not send an impression if props are updated but GUIDs are the same\", () => {\n      const props = { dispatch: sinon.spy() };\n      wrapper = renderSection(props);\n      props.dispatch.resetHistory();\n\n      wrapper.setProps(\n        Object.assign({}, FAKE_TOPSTORIES_SECTION_PROPS, {\n          rows: [{ guid: 1 }, { guid: 2 }],\n        })\n      );\n\n      assert.notCalled(props.dispatch);\n    });\n    it(\"should only send the latest impression on a visibility change\", () => {\n      const listeners = new Set();\n      const props = {\n        dispatch: sinon.spy(),\n        document: {\n          visibilityState: \"hidden\",\n          addEventListener: (ev, cb) => listeners.add(cb),\n          removeEventListener: (ev, cb) => listeners.delete(cb),\n        },\n      };\n\n      wrapper = renderSection(props);\n\n      // Update twice\n      wrapper.setProps(Object.assign({}, props, { rows: [{ guid: 123 }] }));\n      wrapper.setProps(Object.assign({}, props, { rows: [{ guid: 2432 }] }));\n\n      assert.notCalled(props.dispatch);\n\n      // Simulate listeners getting called\n      props.document.visibilityState = \"visible\";\n      listeners.forEach(l => l());\n\n      // Make sure we only sent the latest event\n      assert.calledOnce(props.dispatch);\n      const [action] = props.dispatch.firstCall.args;\n      assert.deepEqual(action.data.tiles, [{ id: 2432 }]);\n    });\n  });\n\n  describe(\"tab rehydrated\", () => {\n    it(\"should fire NEW_TAB_REHYDRATED event\", () => {\n      const dispatch = sinon.spy();\n      const TOP_STORIES_SECTION = {\n        id: \"topstories\",\n        title: \"TopStories\",\n        pref: { collapsed: false },\n        initialized: false,\n        rows: [{ guid: 1, link: \"http://localhost\", isDefault: true }],\n        topics: [],\n        read_more_endpoint: \"http://localhost/read-more\",\n        maxRows: 1,\n        eventSource: \"TOP_STORIES\",\n      };\n      wrapper = shallow(\n        <Section\n          Pocket={{ waitingForSpoc: true, pocketCta: {} }}\n          {...TOP_STORIES_SECTION}\n          dispatch={dispatch}\n        />\n      );\n      assert.notCalled(dispatch);\n\n      wrapper.setProps({ initialized: true });\n\n      assert.calledOnce(dispatch);\n      const [action] = dispatch.firstCall.args;\n      assert.equal(\"NEW_TAB_REHYDRATED\", action.type);\n    });\n  });\n\n  describe(\"#numRows\", () => {\n    it(\"should return maxRows if there is no rowsPref set\", () => {\n      delete FAKE_SECTION.rowsPref;\n      wrapper = mountSectionIntlWithProps(FAKE_SECTION);\n      assert.equal(\n        wrapper.find(Section).instance().numRows,\n        FAKE_SECTION.maxRows\n      );\n    });\n\n    it(\"should return number of rows set in Pref if rowsPref is set\", () => {\n      const numRows = 2;\n      Object.assign(FAKE_SECTION, {\n        rowsPref: \"section.rows\",\n        maxRows: 4,\n        Prefs: { values: { \"section.rows\": numRows } },\n      });\n      wrapper = mountSectionWithProps(FAKE_SECTION);\n      assert.equal(wrapper.find(Section).instance().numRows, numRows);\n    });\n\n    it(\"should return number of rows set in Pref even if higher than maxRows value\", () => {\n      const numRows = 10;\n      Object.assign(FAKE_SECTION, {\n        rowsPref: \"section.rows\",\n        maxRows: 4,\n        Prefs: { values: { \"section.rows\": numRows } },\n      });\n      wrapper = mountSectionWithProps(FAKE_SECTION);\n      assert.equal(wrapper.find(Section).instance().numRows, numRows);\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/content-src/components/TopSites/SearchShortcutsForm.test.jsx",
    "content": "import {\n  SearchShortcutsForm,\n  SelectableSearchShortcut,\n} from \"content-src/components/TopSites/SearchShortcutsForm\";\nimport React from \"react\";\nimport { shallow } from \"enzyme\";\n\ndescribe(\"<SearchShortcutsForm>\", () => {\n  let wrapper;\n  let sandbox;\n  let dispatchStub;\n\n  beforeEach(() => {\n    sandbox = sinon.createSandbox();\n    dispatchStub = sandbox.stub();\n    const defaultProps = { rows: [], searchShortcuts: [] };\n    wrapper = shallow(\n      <SearchShortcutsForm TopSites={defaultProps} dispatch={dispatchStub} />\n    );\n  });\n\n  afterEach(() => {\n    sandbox.restore();\n  });\n\n  it(\"should render\", () => {\n    assert.ok(wrapper.exists());\n    assert.ok(wrapper.find(\".topsite-form\").exists());\n  });\n\n  it(\"should render SelectableSearchShortcut components\", () => {\n    wrapper.setState({ shortcuts: [{}, {}] });\n\n    assert.lengthOf(\n      wrapper.find(\".search-shortcuts-container div\").children(),\n      2\n    );\n    assert.equal(\n      wrapper\n        .find(\".search-shortcuts-container div\")\n        .children()\n        .at(0)\n        .type(),\n      SelectableSearchShortcut\n    );\n  });\n\n  it(\"should render SelectableSearchShortcut components\", () => {\n    const onCloseStub = sandbox.stub();\n    const fakeEvent = { preventDefault: sandbox.stub() };\n    wrapper.setState({ shortcuts: [{}, {}] });\n    wrapper.setProps({ onClose: onCloseStub });\n\n    wrapper.find(\".done\").simulate(\"click\", fakeEvent);\n\n    assert.calledOnce(dispatchStub);\n    assert.calledOnce(fakeEvent.preventDefault);\n    assert.calledOnce(onCloseStub);\n  });\n});\n"
  },
  {
    "path": "test/unit/content-src/components/TopSites.test.jsx",
    "content": "import { actionCreators as ac, actionTypes as at } from \"common/Actions.jsm\";\nimport { GlobalOverrider } from \"test/unit/utils\";\nimport {\n  MIN_CORNER_FAVICON_SIZE,\n  MIN_RICH_FAVICON_SIZE,\n} from \"content-src/components/TopSites/TopSitesConstants\";\nimport {\n  TOP_SITES_DEFAULT_ROWS,\n  TOP_SITES_MAX_SITES_PER_ROW,\n} from \"common/Reducers.jsm\";\nimport {\n  TopSite,\n  TopSiteLink,\n  TopSiteList,\n  TopSitePlaceholder,\n} from \"content-src/components/TopSites/TopSite\";\nimport { A11yLinkButton } from \"content-src/components/A11yLinkButton/A11yLinkButton\";\nimport { LinkMenu } from \"content-src/components/LinkMenu/LinkMenu\";\nimport React from \"react\";\nimport { SectionMenu } from \"content-src/components/SectionMenu/SectionMenu\";\nimport { mount, shallow } from \"enzyme\";\nimport { TopSiteForm } from \"content-src/components/TopSites/TopSiteForm\";\nimport { TopSiteFormInput } from \"content-src/components/TopSites/TopSiteFormInput\";\nimport { _TopSites as TopSites } from \"content-src/components/TopSites/TopSites\";\nimport { ContextMenuButton } from \"content-src/components/ContextMenu/ContextMenuButton\";\n\nconst perfSvc = {\n  mark() {},\n  getMostRecentAbsMarkStartByName() {},\n};\n\nconst DEFAULT_PROPS = {\n  Prefs: { values: {} },\n  TopSites: { initialized: true, rows: [] },\n  TopSitesRows: TOP_SITES_DEFAULT_ROWS,\n  topSiteIconType: () => \"no_image\",\n  dispatch() {},\n  perfSvc,\n};\n\nconst DEFAULT_BLOB_URL = \"blob://test\";\n\ndescribe(\"<TopSites>\", () => {\n  let sandbox;\n\n  beforeEach(() => {\n    sandbox = sinon.createSandbox();\n  });\n\n  afterEach(() => {\n    sandbox.restore();\n  });\n\n  it(\"should render a TopSites element\", () => {\n    const wrapper = shallow(<TopSites {...DEFAULT_PROPS} />);\n    assert.ok(wrapper.exists());\n  });\n  describe(\"context menu\", () => {\n    it(\"should render a context menu button\", () => {\n      const wrapper = mount(<TopSites {...DEFAULT_PROPS} />);\n      assert.equal(\n        wrapper.find(\".section-top-bar .context-menu-button\").length,\n        1\n      );\n    });\n    it(\"should render a section menu when button is clicked\", () => {\n      const wrapper = mount(<TopSites {...DEFAULT_PROPS} />);\n      const button = wrapper.find(\".section-top-bar .context-menu-button\");\n      assert.equal(wrapper.find(SectionMenu).length, 0);\n      button.simulate(\"click\", { preventDefault: () => {} });\n      assert.equal(wrapper.find(SectionMenu).length, 1);\n    });\n    it(\"should not render a section menu by default\", () => {\n      const wrapper = mount(<TopSites {...DEFAULT_PROPS} />);\n      assert.equal(wrapper.find(SectionMenu).length, 0);\n    });\n    it(\"should pass through the correct menu extraOptions to SectionMenu\", () => {\n      const wrapper = mount(<TopSites {...DEFAULT_PROPS} />);\n      wrapper\n        .find(\".section-top-bar .context-menu-button\")\n        .simulate(\"click\", { preventDefault: () => {} });\n      const sectionMenuProps = wrapper.find(SectionMenu).props();\n      assert.deepEqual(sectionMenuProps.extraOptions, [\"AddTopSite\"]);\n    });\n  });\n  describe(\"#_dispatchTopSitesStats\", () => {\n    let globals;\n    let wrapper;\n    let dispatchStatsSpy;\n\n    beforeEach(() => {\n      globals = new GlobalOverrider();\n      sandbox.stub(DEFAULT_PROPS, \"dispatch\");\n      wrapper = shallow(<TopSites {...DEFAULT_PROPS} />, {\n        disableLifecycleMethods: true,\n      });\n      dispatchStatsSpy = sandbox.spy(\n        wrapper.instance(),\n        \"_dispatchTopSitesStats\"\n      );\n    });\n    afterEach(() => {\n      globals.restore();\n      sandbox.restore();\n    });\n    it(\"should call _dispatchTopSitesStats on componentDidMount\", () => {\n      wrapper.instance().componentDidMount();\n\n      assert.calledOnce(dispatchStatsSpy);\n    });\n    it(\"should call _dispatchTopSitesStats on componentDidUpdate\", () => {\n      wrapper.instance().componentDidUpdate();\n\n      assert.calledOnce(dispatchStatsSpy);\n    });\n    it(\"should dispatch SAVE_SESSION_PERF_DATA\", () => {\n      wrapper.instance()._dispatchTopSitesStats();\n\n      assert.calledOnce(DEFAULT_PROPS.dispatch);\n      assert.calledWithExactly(\n        DEFAULT_PROPS.dispatch,\n        ac.AlsoToMain({\n          type: at.SAVE_SESSION_PERF_DATA,\n          data: {\n            topsites_icon_stats: {\n              custom_screenshot: 0,\n              screenshot_with_icon: 0,\n              screenshot: 0,\n              tippytop: 0,\n              rich_icon: 0,\n              no_image: 0,\n            },\n            topsites_pinned: 0,\n            topsites_search_shortcuts: 0,\n          },\n        })\n      );\n    });\n    it(\"should correctly count TopSite images - just screenshot\", () => {\n      const rows = [{ screenshot: true }];\n      sandbox.stub(DEFAULT_PROPS.TopSites, \"rows\").value(rows);\n      wrapper.instance()._dispatchTopSitesStats();\n\n      assert.calledOnce(DEFAULT_PROPS.dispatch);\n      assert.calledWithExactly(\n        DEFAULT_PROPS.dispatch,\n        ac.AlsoToMain({\n          type: at.SAVE_SESSION_PERF_DATA,\n          data: {\n            topsites_icon_stats: {\n              custom_screenshot: 0,\n              screenshot_with_icon: 0,\n              screenshot: 1,\n              tippytop: 0,\n              rich_icon: 0,\n              no_image: 0,\n            },\n            topsites_pinned: 0,\n            topsites_search_shortcuts: 0,\n          },\n        })\n      );\n    });\n    it(\"should correctly count TopSite images - custom_screenshot\", () => {\n      const rows = [{ customScreenshotURL: true }];\n      sandbox.stub(DEFAULT_PROPS.TopSites, \"rows\").value(rows);\n      wrapper.instance()._dispatchTopSitesStats();\n\n      assert.calledOnce(DEFAULT_PROPS.dispatch);\n      assert.calledWithExactly(\n        DEFAULT_PROPS.dispatch,\n        ac.AlsoToMain({\n          type: at.SAVE_SESSION_PERF_DATA,\n          data: {\n            topsites_icon_stats: {\n              custom_screenshot: 1,\n              screenshot_with_icon: 0,\n              screenshot: 0,\n              tippytop: 0,\n              rich_icon: 0,\n              no_image: 0,\n            },\n            topsites_pinned: 0,\n            topsites_search_shortcuts: 0,\n          },\n        })\n      );\n    });\n    it(\"should correctly count TopSite images - screenshot + favicon\", () => {\n      const rows = [{ screenshot: true, faviconSize: MIN_CORNER_FAVICON_SIZE }];\n      sandbox.stub(DEFAULT_PROPS.TopSites, \"rows\").value(rows);\n      wrapper.instance()._dispatchTopSitesStats();\n\n      assert.calledOnce(DEFAULT_PROPS.dispatch);\n      assert.calledWithExactly(\n        DEFAULT_PROPS.dispatch,\n        ac.AlsoToMain({\n          type: at.SAVE_SESSION_PERF_DATA,\n          data: {\n            topsites_icon_stats: {\n              custom_screenshot: 0,\n              screenshot_with_icon: 1,\n              screenshot: 0,\n              tippytop: 0,\n              rich_icon: 0,\n              no_image: 0,\n            },\n            topsites_pinned: 0,\n            topsites_search_shortcuts: 0,\n          },\n        })\n      );\n    });\n    it(\"should correctly count TopSite images - rich_icon\", () => {\n      const rows = [{ faviconSize: MIN_RICH_FAVICON_SIZE }];\n      sandbox.stub(DEFAULT_PROPS.TopSites, \"rows\").value(rows);\n      wrapper.instance()._dispatchTopSitesStats();\n\n      assert.calledOnce(DEFAULT_PROPS.dispatch);\n      assert.calledWithExactly(\n        DEFAULT_PROPS.dispatch,\n        ac.AlsoToMain({\n          type: at.SAVE_SESSION_PERF_DATA,\n          data: {\n            topsites_icon_stats: {\n              custom_screenshot: 0,\n              screenshot_with_icon: 0,\n              screenshot: 0,\n              tippytop: 0,\n              rich_icon: 1,\n              no_image: 0,\n            },\n            topsites_pinned: 0,\n            topsites_search_shortcuts: 0,\n          },\n        })\n      );\n    });\n    it(\"should correctly count TopSite images - tippytop\", () => {\n      const rows = [\n        { tippyTopIcon: \"foo\" },\n        { faviconRef: \"tippytop\" },\n        { faviconRef: \"foobar\" },\n      ];\n      sandbox.stub(DEFAULT_PROPS.TopSites, \"rows\").value(rows);\n      wrapper.instance()._dispatchTopSitesStats();\n\n      assert.calledOnce(DEFAULT_PROPS.dispatch);\n      assert.calledWithExactly(\n        DEFAULT_PROPS.dispatch,\n        ac.AlsoToMain({\n          type: at.SAVE_SESSION_PERF_DATA,\n          data: {\n            topsites_icon_stats: {\n              custom_screenshot: 0,\n              screenshot_with_icon: 0,\n              screenshot: 0,\n              tippytop: 2,\n              rich_icon: 0,\n              no_image: 1,\n            },\n            topsites_pinned: 0,\n            topsites_search_shortcuts: 0,\n          },\n        })\n      );\n    });\n    it(\"should correctly count TopSite images - no image\", () => {\n      const rows = [{}];\n      sandbox.stub(DEFAULT_PROPS.TopSites, \"rows\").value(rows);\n      wrapper.instance()._dispatchTopSitesStats();\n\n      assert.calledOnce(DEFAULT_PROPS.dispatch);\n      assert.calledWithExactly(\n        DEFAULT_PROPS.dispatch,\n        ac.AlsoToMain({\n          type: at.SAVE_SESSION_PERF_DATA,\n          data: {\n            topsites_icon_stats: {\n              custom_screenshot: 0,\n              screenshot_with_icon: 0,\n              screenshot: 0,\n              tippytop: 0,\n              rich_icon: 0,\n              no_image: 1,\n            },\n            topsites_pinned: 0,\n            topsites_search_shortcuts: 0,\n          },\n        })\n      );\n    });\n    it(\"should correctly count pinned Top Sites\", () => {\n      const rows = [\n        { isPinned: true },\n        { isPinned: false },\n        { isPinned: true },\n      ];\n      sandbox.stub(DEFAULT_PROPS.TopSites, \"rows\").value(rows);\n      wrapper.instance()._dispatchTopSitesStats();\n\n      assert.calledOnce(DEFAULT_PROPS.dispatch);\n      assert.calledWithExactly(\n        DEFAULT_PROPS.dispatch,\n        ac.AlsoToMain({\n          type: at.SAVE_SESSION_PERF_DATA,\n          data: {\n            topsites_icon_stats: {\n              custom_screenshot: 0,\n              screenshot_with_icon: 0,\n              screenshot: 0,\n              tippytop: 0,\n              rich_icon: 0,\n              no_image: 3,\n            },\n            topsites_pinned: 2,\n            topsites_search_shortcuts: 0,\n          },\n        })\n      );\n    });\n    it(\"should correctly count search shortcut Top Sites\", () => {\n      const rows = [{ searchTopSite: true }, { searchTopSite: true }];\n      sandbox.stub(DEFAULT_PROPS.TopSites, \"rows\").value(rows);\n      wrapper.instance()._dispatchTopSitesStats();\n\n      assert.calledOnce(DEFAULT_PROPS.dispatch);\n      assert.calledWithExactly(\n        DEFAULT_PROPS.dispatch,\n        ac.AlsoToMain({\n          type: at.SAVE_SESSION_PERF_DATA,\n          data: {\n            topsites_icon_stats: {\n              custom_screenshot: 0,\n              screenshot_with_icon: 0,\n              screenshot: 0,\n              tippytop: 0,\n              rich_icon: 0,\n              no_image: 2,\n            },\n            topsites_pinned: 0,\n            topsites_search_shortcuts: 2,\n          },\n        })\n      );\n    });\n    it(\"should only count visible top sites on wide layout\", () => {\n      globals.set(\"matchMedia\", () => ({ matches: true }));\n      const rows = [\n        {},\n        {},\n        {},\n        {},\n        {},\n        {},\n        {},\n        {},\n        {},\n        {},\n        {},\n        {},\n        {},\n        {},\n        {},\n        {},\n      ];\n      sandbox.stub(DEFAULT_PROPS.TopSites, \"rows\").value(rows);\n\n      wrapper.instance()._dispatchTopSitesStats();\n      assert.calledOnce(DEFAULT_PROPS.dispatch);\n      assert.calledWithExactly(\n        DEFAULT_PROPS.dispatch,\n        ac.AlsoToMain({\n          type: at.SAVE_SESSION_PERF_DATA,\n          data: {\n            topsites_icon_stats: {\n              custom_screenshot: 0,\n              screenshot_with_icon: 0,\n              screenshot: 0,\n              tippytop: 0,\n              rich_icon: 0,\n              no_image: 8,\n            },\n            topsites_pinned: 0,\n            topsites_search_shortcuts: 0,\n          },\n        })\n      );\n    });\n    it(\"should only count visible top sites on normal layout\", () => {\n      globals.set(\"matchMedia\", () => ({ matches: false }));\n      const rows = [\n        {},\n        {},\n        {},\n        {},\n        {},\n        {},\n        {},\n        {},\n        {},\n        {},\n        {},\n        {},\n        {},\n        {},\n        {},\n        {},\n      ];\n      sandbox.stub(DEFAULT_PROPS.TopSites, \"rows\").value(rows);\n      wrapper.instance()._dispatchTopSitesStats();\n      assert.calledOnce(DEFAULT_PROPS.dispatch);\n      assert.calledWithExactly(\n        DEFAULT_PROPS.dispatch,\n        ac.AlsoToMain({\n          type: at.SAVE_SESSION_PERF_DATA,\n          data: {\n            topsites_icon_stats: {\n              custom_screenshot: 0,\n              screenshot_with_icon: 0,\n              screenshot: 0,\n              tippytop: 0,\n              rich_icon: 0,\n              no_image: 6,\n            },\n            topsites_pinned: 0,\n            topsites_search_shortcuts: 0,\n          },\n        })\n      );\n    });\n  });\n});\n\ndescribe(\"<TopSiteLink>\", () => {\n  let globals;\n  let link;\n  let url;\n  beforeEach(() => {\n    globals = new GlobalOverrider();\n    url = {\n      createObjectURL: globals.sandbox.stub().returns(DEFAULT_BLOB_URL),\n      revokeObjectURL: globals.sandbox.spy(),\n    };\n    globals.set(\"URL\", url);\n    link = { url: \"https://foo.com\", screenshot: \"foo.jpg\", hostname: \"foo\" };\n  });\n  afterEach(() => globals.restore());\n  it(\"should add the right url\", () => {\n    link.url = \"https://www.foobar.org\";\n    const wrapper = shallow(<TopSiteLink link={link} />);\n    assert.propertyVal(\n      wrapper.find(\"a\").props(),\n      \"href\",\n      \"https://www.foobar.org\"\n    );\n  });\n  it(\"should not add the url to the href if it a search shortcut\", () => {\n    link.searchTopSite = true;\n    const wrapper = shallow(<TopSiteLink link={link} />);\n    assert.isUndefined(wrapper.find(\"a\").props().href);\n  });\n  it(\"should have rtl direction automatically set for text\", () => {\n    const wrapper = shallow(<TopSiteLink link={link} />);\n\n    assert.isTrue(!!wrapper.find(\"[dir='auto']\").length);\n  });\n  it(\"should render a title\", () => {\n    const wrapper = shallow(<TopSiteLink link={link} title=\"foobar\" />);\n    const titleEl = wrapper.find(\".title\");\n\n    assert.equal(titleEl.text(), \"foobar\");\n  });\n  it(\"should have only the title as the text of the link\", () => {\n    const wrapper = shallow(<TopSiteLink link={link} title=\"foobar\" />);\n\n    assert.equal(wrapper.find(\"a\").text(), \"foobar\");\n  });\n  it(\"should render the pin icon for pinned links\", () => {\n    link.isPinned = true;\n    link.pinnedIndex = 7;\n    const wrapper = shallow(<TopSiteLink link={link} />);\n    assert.equal(wrapper.find(\".icon-pin-small\").length, 1);\n  });\n  it(\"should not render the pin icon for non pinned links\", () => {\n    link.isPinned = false;\n    const wrapper = shallow(<TopSiteLink link={link} />);\n    assert.equal(wrapper.find(\".icon-pin-small\").length, 0);\n  });\n  it(\"should render the first letter of the title as a fallback for missing screenshots\", () => {\n    const wrapper = shallow(<TopSiteLink link={link} title={\"foo\"} />);\n    assert.equal(wrapper.find(\".tile\").prop(\"data-fallback\"), \"f\");\n  });\n  it(\"should render a normal image screenshot with the .active class, if it is provided\", () => {\n    const wrapper = shallow(<TopSiteLink link={link} />);\n    const screenshotEl = wrapper.find(\".screenshot\");\n\n    assert.propertyVal(\n      screenshotEl.props().style,\n      \"backgroundImage\",\n      \"url(foo.jpg)\"\n    );\n    assert.isTrue(screenshotEl.hasClass(\"active\"));\n  });\n  it(\"should render a blob image screenshot with the .active class, if it is provided\", () => {\n    link.screenshot = { path: \"/test_path\", data: new Blob([0]) };\n\n    const wrapper = shallow(<TopSiteLink link={link} />);\n    const screenshotEl = wrapper.find(\".screenshot\");\n\n    assert.propertyVal(\n      screenshotEl.props().style,\n      \"backgroundImage\",\n      `url(${DEFAULT_BLOB_URL})`\n    );\n    assert.isTrue(screenshotEl.hasClass(\"active\"));\n  });\n  it(\"should render a small icon with fallback letter with the screenshot if the icon is smaller than 16x16\", () => {\n    link.favicon = \"too-small-icon.png\";\n    link.faviconSize = 10;\n    const wrapper = shallow(<TopSiteLink link={link} title=\"foo\" />);\n    const screenshotEl = wrapper.find(\".screenshot\");\n    const defaultIconEl = wrapper.find(\".default-icon\");\n\n    assert.propertyVal(\n      screenshotEl.props().style,\n      \"backgroundImage\",\n      \"url(foo.jpg)\"\n    );\n    assert.isTrue(screenshotEl.hasClass(\"active\"));\n    assert.lengthOf(defaultIconEl, 1);\n    assert.equal(defaultIconEl.prop(\"data-fallback\"), \"f\");\n  });\n  it(\"should render a small icon with fallback letter with the screenshot if the icon is missing\", () => {\n    const wrapper = shallow(<TopSiteLink link={link} title=\"foo\" />);\n    const screenshotEl = wrapper.find(\".screenshot\");\n    const defaultIconEl = wrapper.find(\".default-icon\");\n\n    assert.propertyVal(\n      screenshotEl.props().style,\n      \"backgroundImage\",\n      \"url(foo.jpg)\"\n    );\n    assert.isTrue(screenshotEl.hasClass(\"active\"));\n    assert.lengthOf(defaultIconEl, 1);\n    assert.equal(defaultIconEl.prop(\"data-fallback\"), \"f\");\n  });\n  it(\"should render a small icon with the screenshot if the icon is bigger than 32x32\", () => {\n    link.favicon = \"small-icon.png\";\n    link.faviconSize = 32;\n\n    const wrapper = shallow(<TopSiteLink link={link} />);\n    const screenshotEl = wrapper.find(\".screenshot\");\n    const defaultIconEl = wrapper.find(\".default-icon\");\n\n    assert.propertyVal(\n      screenshotEl.props().style,\n      \"backgroundImage\",\n      \"url(foo.jpg)\"\n    );\n    assert.isTrue(screenshotEl.hasClass(\"active\"));\n    assert.propertyVal(\n      defaultIconEl.props().style,\n      \"backgroundImage\",\n      `url(${link.favicon})`\n    );\n    assert.lengthOf(wrapper.find(\".rich-icon\"), 0);\n  });\n  it(\"should not add the .active class to the screenshot element if no screenshot prop is provided\", () => {\n    link.screenshot = null;\n    const wrapper = shallow(<TopSiteLink link={link} />);\n    assert.isFalse(wrapper.find(\".screenshot\").hasClass(\"active\"));\n  });\n  it(\"should render the tippy top icon if provided and not a small icon\", () => {\n    link.tippyTopIcon = \"foo.png\";\n    link.backgroundColor = \"#FFFFFF\";\n    const wrapper = shallow(<TopSiteLink link={link} />);\n    assert.lengthOf(wrapper.find(\".screenshot\"), 0);\n    assert.lengthOf(wrapper.find(\".default-icon\"), 0);\n    const tippyTop = wrapper.find(\".rich-icon\");\n    assert.propertyVal(\n      tippyTop.props().style,\n      \"backgroundImage\",\n      \"url(foo.png)\"\n    );\n    assert.propertyVal(tippyTop.props().style, \"backgroundColor\", \"#FFFFFF\");\n  });\n  it(\"should render a rich icon if provided and not a small icon\", () => {\n    link.favicon = \"foo.png\";\n    link.faviconSize = 196;\n    link.backgroundColor = \"#FFFFFF\";\n    const wrapper = shallow(<TopSiteLink link={link} />);\n    assert.lengthOf(wrapper.find(\".screenshot\"), 0);\n    assert.lengthOf(wrapper.find(\".default-icon\"), 0);\n    const richIcon = wrapper.find(\".rich-icon\");\n    assert.propertyVal(\n      richIcon.props().style,\n      \"backgroundImage\",\n      \"url(foo.png)\"\n    );\n    assert.propertyVal(richIcon.props().style, \"backgroundColor\", \"#FFFFFF\");\n  });\n  it(\"should not render a rich icon if it is smaller than 96x96\", () => {\n    link.favicon = \"foo.png\";\n    link.faviconSize = 48;\n    link.backgroundColor = \"#FFFFFF\";\n    const wrapper = shallow(<TopSiteLink link={link} />);\n    assert.equal(wrapper.find(\".screenshot\").length, 1);\n    assert.equal(wrapper.find(\".rich-icon\").length, 0);\n  });\n  it(\"should apply just the default class name to the outer link if props.className is falsey\", () => {\n    const wrapper = shallow(<TopSiteLink className={false} />);\n    assert.ok(wrapper.find(\"li\").hasClass(\"top-site-outer\"));\n  });\n  it(\"should add props.className to the outer link element\", () => {\n    const wrapper = shallow(<TopSiteLink className=\"foo bar\" />);\n    assert.ok(wrapper.find(\"li\").hasClass(\"top-site-outer foo bar\"));\n  });\n  describe(\"#onDragEvent\", () => {\n    let simulate;\n    let wrapper;\n    beforeEach(() => {\n      wrapper = shallow(\n        <TopSiteLink isDraggable={true} onDragEvent={() => {}} />\n      );\n      simulate = type => {\n        const event = {\n          dataTransfer: { setData() {}, types: { includes() {} } },\n          preventDefault() {\n            this.prevented = true;\n          },\n          target: { blur() {} },\n          type,\n        };\n        wrapper.simulate(type, event);\n        return event;\n      };\n    });\n    it(\"should allow clicks without dragging\", () => {\n      simulate(\"mousedown\");\n      simulate(\"mouseup\");\n\n      const event = simulate(\"click\");\n\n      assert.notOk(event.prevented);\n    });\n    it(\"should prevent clicks after dragging\", () => {\n      simulate(\"mousedown\");\n      simulate(\"dragstart\");\n      simulate(\"dragenter\");\n      simulate(\"drop\");\n      simulate(\"dragend\");\n      simulate(\"mouseup\");\n\n      const event = simulate(\"click\");\n\n      assert.ok(event.prevented);\n    });\n    it(\"should allow clicks after dragging then clicking\", () => {\n      simulate(\"mousedown\");\n      simulate(\"dragstart\");\n      simulate(\"dragenter\");\n      simulate(\"drop\");\n      simulate(\"dragend\");\n      simulate(\"mouseup\");\n      simulate(\"click\");\n\n      simulate(\"mousedown\");\n      simulate(\"mouseup\");\n\n      const event = simulate(\"click\");\n\n      assert.notOk(event.prevented);\n    });\n  });\n});\n\ndescribe(\"<TopSite>\", () => {\n  let link;\n  beforeEach(() => {\n    link = { url: \"https://foo.com\", screenshot: \"foo.jpg\", hostname: \"foo\" };\n  });\n\n  it(\"should render a TopSite\", () => {\n    const wrapper = shallow(<TopSite link={link} />);\n    assert.ok(wrapper.exists());\n  });\n\n  it(\"should render a shortened title based off the url\", () => {\n    link.url = \"https://www.foobar.org\";\n    link.hostname = \"foobar\";\n    link.eTLD = \"org\";\n    const wrapper = shallow(<TopSite link={link} />);\n\n    assert.equal(wrapper.find(TopSiteLink).props().title, \"foobar\");\n  });\n\n  it(\"should parse args for fluent correctly\", () => {\n    const title = '\"fluent\"';\n    link.hostname = title;\n\n    const wrapper = mount(<TopSite link={link} />);\n    const button = wrapper.find(\n      \"button[data-l10n-id='newtab-menu-content-tooltip']\"\n    );\n    assert.equal(button.prop(\"data-l10n-args\"), JSON.stringify({ title }));\n  });\n\n  it(\"should have .active class, on top-site-outer if context menu is open\", () => {\n    const wrapper = shallow(<TopSite link={link} index={1} activeIndex={1} />);\n    wrapper.setState({ showContextMenu: true });\n\n    assert.equal(\n      wrapper\n        .find(TopSiteLink)\n        .props()\n        .className.trim(),\n      \"active\"\n    );\n  });\n  it(\"should not add .active class, on top-site-outer if context menu is closed\", () => {\n    const wrapper = shallow(<TopSite link={link} index={1} />);\n    wrapper.setState({ showContextMenu: false, activeTile: 1 });\n    assert.equal(wrapper.find(TopSiteLink).props().className, \"\");\n  });\n  it(\"should render a context menu button\", () => {\n    const wrapper = shallow(<TopSite link={link} />);\n    assert.equal(wrapper.find(ContextMenuButton).length, 1);\n  });\n  it(\"should render a link menu\", () => {\n    const wrapper = shallow(<TopSite link={link} />);\n    assert.equal(wrapper.find(LinkMenu).length, 1);\n  });\n  it(\"should pass onUpdate, site, options, and index to LinkMenu\", () => {\n    const wrapper = shallow(<TopSite link={link} />);\n    const linkMenuProps = wrapper.find(LinkMenu).props();\n    [\"onUpdate\", \"site\", \"index\", \"options\"].forEach(prop =>\n      assert.property(linkMenuProps, prop)\n    );\n  });\n  it(\"should pass through the correct menu options to LinkMenu\", () => {\n    const wrapper = shallow(<TopSite link={link} />);\n    const linkMenuProps = wrapper.find(LinkMenu).props();\n    assert.deepEqual(linkMenuProps.options, [\n      \"CheckPinTopSite\",\n      \"EditTopSite\",\n      \"Separator\",\n      \"OpenInNewWindow\",\n      \"OpenInPrivateWindow\",\n      \"Separator\",\n      \"BlockUrl\",\n      \"DeleteUrl\",\n    ]);\n  });\n\n  describe(\"#onLinkClick\", () => {\n    it(\"should call dispatch when the link is clicked\", () => {\n      const dispatch = sinon.stub();\n      const wrapper = shallow(\n        <TopSite link={link} index={3} dispatch={dispatch} />\n      );\n\n      wrapper.find(TopSiteLink).simulate(\"click\", { preventDefault() {} });\n\n      assert.calledTwice(dispatch);\n    });\n    it(\"should dispatch a UserEventAction with the right data\", () => {\n      const dispatch = sinon.stub();\n      const wrapper = shallow(\n        <TopSite\n          link={Object.assign({}, link, {\n            iconType: \"rich_icon\",\n            isPinned: true,\n          })}\n          index={3}\n          dispatch={dispatch}\n        />\n      );\n\n      wrapper.find(TopSiteLink).simulate(\"click\", { preventDefault() {} });\n\n      const [action] = dispatch.firstCall.args;\n      assert.isUserEventAction(action);\n\n      assert.propertyVal(action.data, \"event\", \"CLICK\");\n      assert.propertyVal(action.data, \"source\", \"TOP_SITES\");\n      assert.propertyVal(action.data, \"action_position\", 3);\n      assert.propertyVal(action.data.value, \"card_type\", \"pinned\");\n      assert.propertyVal(action.data.value, \"icon_type\", \"rich_icon\");\n    });\n    it(\"should dispatch a UserEventAction with the right data for search top site\", () => {\n      const dispatch = sinon.stub();\n      const siteInfo = {\n        iconType: \"tippytop\",\n        isPinned: true,\n        searchTopSite: true,\n        hostname: \"google\",\n        label: \"@google\",\n      };\n      const wrapper = shallow(\n        <TopSite\n          link={Object.assign({}, link, siteInfo)}\n          index={3}\n          dispatch={dispatch}\n        />\n      );\n\n      wrapper.find(TopSiteLink).simulate(\"click\", { preventDefault() {} });\n\n      const [action] = dispatch.firstCall.args;\n      assert.isUserEventAction(action);\n\n      assert.propertyVal(action.data, \"event\", \"CLICK\");\n      assert.propertyVal(action.data, \"source\", \"TOP_SITES\");\n      assert.propertyVal(action.data, \"action_position\", 3);\n      assert.propertyVal(action.data.value, \"card_type\", \"search\");\n      assert.propertyVal(action.data.value, \"icon_type\", \"tippytop\");\n      assert.propertyVal(action.data.value, \"search_vendor\", \"google\");\n    });\n    it(\"should dispatch a UserEventAction with the right data for SPOC top site\", () => {\n      const dispatch = sinon.stub();\n      const siteInfo = {\n        iconType: \"custom_screenshot\",\n        type: \"SPOC\",\n      };\n      const wrapper = shallow(\n        <TopSite\n          link={Object.assign({}, link, siteInfo)}\n          index={0}\n          dispatch={dispatch}\n        />\n      );\n\n      wrapper.find(TopSiteLink).simulate(\"click\", { preventDefault() {} });\n\n      const [action] = dispatch.firstCall.args;\n      assert.isUserEventAction(action);\n\n      assert.propertyVal(action.data, \"event\", \"CLICK\");\n      assert.propertyVal(action.data, \"source\", \"TOP_SITES\");\n      assert.propertyVal(action.data, \"action_position\", 0);\n      assert.propertyVal(action.data.value, \"card_type\", \"spoc\");\n      assert.propertyVal(action.data.value, \"icon_type\", \"custom_screenshot\");\n    });\n    it(\"should dispatch OPEN_LINK with the right data\", () => {\n      const dispatch = sinon.stub();\n      const wrapper = shallow(\n        <TopSite\n          link={Object.assign({}, link, { typedBonus: true })}\n          index={3}\n          dispatch={dispatch}\n        />\n      );\n\n      wrapper.find(TopSiteLink).simulate(\"click\", { preventDefault() {} });\n\n      const [action] = dispatch.secondCall.args;\n      assert.propertyVal(action, \"type\", at.OPEN_LINK);\n      assert.propertyVal(action.data, \"typedBonus\", true);\n    });\n  });\n});\n\ndescribe(\"<TopSiteForm>\", () => {\n  let wrapper;\n  let sandbox;\n\n  function setup(props = {}) {\n    sandbox = sinon.createSandbox();\n    const customProps = Object.assign(\n      {},\n      { onClose: sandbox.spy(), dispatch: sandbox.spy() },\n      props\n    );\n    wrapper = mount(<TopSiteForm {...customProps} />);\n  }\n\n  describe(\"validateForm\", () => {\n    beforeEach(() => setup({ site: { url: \"http://foo\" } }));\n\n    it(\"should return true for a correct URL\", () => {\n      wrapper.setState({ url: \"foo\" });\n\n      assert.isTrue(wrapper.instance().validateForm());\n    });\n\n    it(\"should return false for a incorrect URL\", () => {\n      wrapper.setState({ url: \" \" });\n\n      assert.isNull(wrapper.instance().validateForm());\n      assert.isTrue(wrapper.state().validationError);\n    });\n\n    it(\"should return true for a correct custom screenshot URL\", () => {\n      wrapper.setState({ customScreenshotUrl: \"foo\" });\n\n      assert.isTrue(wrapper.instance().validateForm());\n    });\n\n    it(\"should return false for a incorrect custom screenshot URL\", () => {\n      wrapper.setState({ customScreenshotUrl: \" \" });\n\n      assert.isNull(wrapper.instance().validateForm());\n    });\n\n    it(\"should return true for an empty custom screenshot URL\", () => {\n      wrapper.setState({ customScreenshotUrl: \"\" });\n\n      assert.isTrue(wrapper.instance().validateForm());\n    });\n\n    it(\"should return false for file: protocol\", () => {\n      wrapper.setState({ customScreenshotUrl: \"file:///C:/Users/foo\" });\n\n      assert.isFalse(wrapper.instance().validateForm());\n    });\n  });\n\n  describe(\"#previewButton\", () => {\n    beforeEach(() =>\n      setup({\n        site: { customScreenshotURL: \"http://foo.com\" },\n        previewResponse: null,\n      })\n    );\n\n    it(\"should render the preview button on invalid urls\", () => {\n      assert.equal(0, wrapper.find(\".preview\").length);\n\n      wrapper.setState({ customScreenshotUrl: \" \" });\n\n      assert.equal(1, wrapper.find(\".preview\").length);\n    });\n\n    it(\"should render the preview button when input value updated\", () => {\n      assert.equal(0, wrapper.find(\".preview\").length);\n\n      wrapper.setState({\n        customScreenshotUrl: \"http://baz.com\",\n        screenshotPreview: null,\n      });\n\n      assert.equal(1, wrapper.find(\".preview\").length);\n    });\n  });\n\n  describe(\"preview request\", () => {\n    beforeEach(() => {\n      setup({\n        site: { customScreenshotURL: \"http://foo.com\", url: \"http://foo.com\" },\n        previewResponse: null,\n      });\n    });\n\n    it(\"shouldn't dispatch a request for invalid urls\", () => {\n      wrapper.setState({ customScreenshotUrl: \" \", url: \"foo\" });\n\n      wrapper.find(\".preview\").simulate(\"click\");\n\n      assert.notCalled(wrapper.props().dispatch);\n    });\n\n    it(\"should dispatch a PREVIEW_REQUEST\", () => {\n      wrapper.setState({ customScreenshotUrl: \"screenshot\" });\n      wrapper.find(\".preview\").simulate(\"submit\");\n\n      assert.calledTwice(wrapper.props().dispatch);\n      assert.calledWith(\n        wrapper.props().dispatch,\n        ac.AlsoToMain({\n          type: at.PREVIEW_REQUEST,\n          data: { url: \"http://screenshot\" },\n        })\n      );\n      assert.calledWith(\n        wrapper.props().dispatch,\n        ac.UserEvent({\n          event: \"PREVIEW_REQUEST\",\n          source: \"TOP_SITES\",\n        })\n      );\n    });\n  });\n\n  describe(\"#TopSiteLink\", () => {\n    beforeEach(() => {\n      setup();\n    });\n\n    it(\"should display a TopSiteLink preview\", () => {\n      assert.equal(wrapper.find(TopSiteLink).length, 1);\n    });\n\n    it(\"should display the preview screenshot\", () => {\n      wrapper.setProps({ site: { tippyTopIcon: \"bar\" } });\n\n      assert.equal(\n        wrapper.find(\".top-site-icon\").getDOMNode().style[\"background-image\"],\n        'url(\"bar\")'\n      );\n\n      wrapper.setProps({ previewResponse: \"foo\", previewUrl: \"foo\" });\n\n      assert.equal(\n        wrapper.find(\".top-site-icon\").getDOMNode().style[\"background-image\"],\n        'url(\"foo\")'\n      );\n    });\n\n    it(\"should not render any icon on error\", () => {\n      wrapper.setProps({ previewResponse: \"\" });\n\n      assert.equal(wrapper.find(\".top-site-icon\").length, 0);\n    });\n\n    it(\"should render the search icon when searchTopSite is true\", () => {\n      wrapper.setProps({ site: { tippyTopIcon: \"bar\", searchTopSite: true } });\n\n      assert.equal(\n        wrapper.find(\".rich-icon\").getDOMNode().style[\"background-image\"],\n        'url(\"bar\")'\n      );\n      assert.isTrue(wrapper.find(\".search-topsite\").exists());\n    });\n  });\n\n  describe(\"#addMode\", () => {\n    beforeEach(() => setup());\n\n    it(\"should render the component\", () => {\n      assert.ok(wrapper.find(TopSiteForm).exists());\n    });\n    it(\"should have the correct header\", () => {\n      assert.equal(\n        wrapper.findWhere(\n          n =>\n            n.length &&\n            n.prop(\"data-l10n-id\") === \"newtab-topsites-add-topsites-header\"\n        ).length,\n        1\n      );\n    });\n    it(\"should have the correct button text\", () => {\n      assert.equal(\n        wrapper.findWhere(\n          n =>\n            n.length && n.prop(\"data-l10n-id\") === \"newtab-topsites-save-button\"\n        ).length,\n        0\n      );\n      assert.equal(\n        wrapper.findWhere(\n          n =>\n            n.length && n.prop(\"data-l10n-id\") === \"newtab-topsites-add-button\"\n        ).length,\n        1\n      );\n    });\n    it(\"should not render a preview button\", () => {\n      assert.equal(0, wrapper.find(\".custom-image-input-container\").length);\n    });\n    it(\"should call onClose if Cancel button is clicked\", () => {\n      wrapper.find(\".cancel\").simulate(\"click\");\n      assert.calledOnce(wrapper.instance().props.onClose);\n    });\n    it(\"should set validationError if url is empty\", () => {\n      assert.equal(wrapper.state().validationError, false);\n      wrapper.find(\".done\").simulate(\"submit\");\n      assert.equal(wrapper.state().validationError, true);\n    });\n    it(\"should set validationError if url is invalid\", () => {\n      wrapper.setState({ url: \"not valid\" });\n      assert.equal(wrapper.state().validationError, false);\n      wrapper.find(\".done\").simulate(\"submit\");\n      assert.equal(wrapper.state().validationError, true);\n    });\n    it(\"should call onClose and dispatch with right args if URL is valid\", () => {\n      wrapper.setState({ url: \"valid.com\", label: \"a label\" });\n      wrapper.find(\".done\").simulate(\"submit\");\n      assert.calledOnce(wrapper.instance().props.onClose);\n      assert.calledWith(wrapper.instance().props.dispatch, {\n        data: {\n          site: { label: \"a label\", url: \"http://valid.com\" },\n          index: -1,\n        },\n        meta: { from: \"ActivityStream:Content\", to: \"ActivityStream:Main\" },\n        type: at.TOP_SITES_PIN,\n      });\n      assert.calledWith(wrapper.instance().props.dispatch, {\n        data: {\n          action_position: -1,\n          source: \"TOP_SITES\",\n          event: \"TOP_SITES_EDIT\",\n        },\n        meta: { from: \"ActivityStream:Content\", to: \"ActivityStream:Main\" },\n        type: at.TELEMETRY_USER_EVENT,\n      });\n    });\n    it(\"should not pass empty string label in dispatch data\", () => {\n      wrapper.setState({ url: \"valid.com\", label: \"\" });\n      wrapper.find(\".done\").simulate(\"submit\");\n      assert.calledWith(wrapper.instance().props.dispatch, {\n        data: { site: { url: \"http://valid.com\" }, index: -1 },\n        meta: { from: \"ActivityStream:Content\", to: \"ActivityStream:Main\" },\n        type: at.TOP_SITES_PIN,\n      });\n    });\n    it(\"should open the custom screenshot input\", () => {\n      assert.isFalse(wrapper.state().showCustomScreenshotForm);\n\n      wrapper.find(A11yLinkButton).simulate(\"click\");\n\n      assert.isTrue(wrapper.state().showCustomScreenshotForm);\n    });\n  });\n\n  describe(\"edit existing Topsite\", () => {\n    beforeEach(() =>\n      setup({\n        site: {\n          url: \"https://foo.bar\",\n          label: \"baz\",\n          customScreenshotURL: \"http://foo\",\n        },\n        index: 7,\n      })\n    );\n\n    it(\"should render the component\", () => {\n      assert.ok(wrapper.find(TopSiteForm).exists());\n    });\n    it(\"should have the correct header\", () => {\n      assert.equal(\n        wrapper.findWhere(\n          n => n.prop(\"data-l10n-id\") === \"newtab-topsites-edit-topsites-header\"\n        ).length,\n        1\n      );\n    });\n    it(\"should have the correct button text\", () => {\n      assert.equal(\n        wrapper.findWhere(\n          n => n.prop(\"data-l10n-id\") === \"newtab-topsites-add-button\"\n        ).length,\n        0\n      );\n      assert.equal(\n        wrapper.findWhere(\n          n => n.prop(\"data-l10n-id\") === \"newtab-topsites-save-button\"\n        ).length,\n        1\n      );\n    });\n    it(\"should call onClose if Cancel button is clicked\", () => {\n      wrapper.find(\".cancel\").simulate(\"click\");\n      assert.calledOnce(wrapper.instance().props.onClose);\n    });\n    it(\"should show error and not call onClose or dispatch if URL is empty\", () => {\n      wrapper.setState({ url: \"\" });\n      assert.equal(wrapper.state().validationError, false);\n      wrapper.find(\".done\").simulate(\"submit\");\n      assert.equal(wrapper.state().validationError, true);\n      assert.notCalled(wrapper.instance().props.onClose);\n      assert.notCalled(wrapper.instance().props.dispatch);\n    });\n    it(\"should show error and not call onClose or dispatch if URL is invalid\", () => {\n      wrapper.setState({ url: \"not valid\" });\n      assert.equal(wrapper.state().validationError, false);\n      wrapper.find(\".done\").simulate(\"submit\");\n      assert.equal(wrapper.state().validationError, true);\n      assert.notCalled(wrapper.instance().props.onClose);\n      assert.notCalled(wrapper.instance().props.dispatch);\n    });\n    it(\"should call onClose and dispatch with right args if URL is valid\", () => {\n      wrapper.find(\".done\").simulate(\"submit\");\n      assert.calledOnce(wrapper.instance().props.onClose);\n      assert.calledTwice(wrapper.instance().props.dispatch);\n      assert.calledWith(wrapper.instance().props.dispatch, {\n        data: {\n          site: {\n            label: \"baz\",\n            url: \"https://foo.bar\",\n            customScreenshotURL: \"http://foo\",\n          },\n          index: 7,\n        },\n        meta: { from: \"ActivityStream:Content\", to: \"ActivityStream:Main\" },\n        type: at.TOP_SITES_PIN,\n      });\n      assert.calledWith(wrapper.instance().props.dispatch, {\n        data: {\n          action_position: 7,\n          source: \"TOP_SITES\",\n          event: \"TOP_SITES_EDIT\",\n        },\n        meta: { from: \"ActivityStream:Content\", to: \"ActivityStream:Main\" },\n        type: at.TELEMETRY_USER_EVENT,\n      });\n    });\n    it(\"should set customScreenshotURL to null if it was removed\", () => {\n      wrapper.setState({ customScreenshotUrl: \"\" });\n\n      wrapper.find(\".done\").simulate(\"submit\");\n\n      assert.calledWith(wrapper.instance().props.dispatch, {\n        data: {\n          site: {\n            label: \"baz\",\n            url: \"https://foo.bar\",\n            customScreenshotURL: null,\n          },\n          index: 7,\n        },\n        meta: { from: \"ActivityStream:Content\", to: \"ActivityStream:Main\" },\n        type: at.TOP_SITES_PIN,\n      });\n    });\n    it(\"should call onClose and dispatch with right args if URL is valid (negative index)\", () => {\n      wrapper.setProps({ index: -1 });\n      wrapper.find(\".done\").simulate(\"submit\");\n      assert.calledOnce(wrapper.instance().props.onClose);\n      assert.calledTwice(wrapper.instance().props.dispatch);\n      assert.calledWith(wrapper.instance().props.dispatch, {\n        data: {\n          site: {\n            label: \"baz\",\n            url: \"https://foo.bar\",\n            customScreenshotURL: \"http://foo\",\n          },\n          index: -1,\n        },\n        meta: { from: \"ActivityStream:Content\", to: \"ActivityStream:Main\" },\n        type: at.TOP_SITES_PIN,\n      });\n    });\n    it(\"should not pass empty string label in dispatch data\", () => {\n      wrapper.setState({ label: \"\" });\n      wrapper.find(\".done\").simulate(\"submit\");\n      assert.calledWith(wrapper.instance().props.dispatch, {\n        data: {\n          site: { url: \"https://foo.bar\", customScreenshotURL: \"http://foo\" },\n          index: 7,\n        },\n        meta: { from: \"ActivityStream:Content\", to: \"ActivityStream:Main\" },\n        type: at.TOP_SITES_PIN,\n      });\n    });\n    it(\"should render the save button if custom screenshot request finished\", () => {\n      wrapper.setState({\n        customScreenshotUrl: \"foo\",\n        screenshotPreview: \"custom\",\n      });\n      assert.equal(0, wrapper.find(\".preview\").length);\n      assert.equal(1, wrapper.find(\".done\").length);\n    });\n    it(\"should render the save button if custom screenshot url was cleared\", () => {\n      wrapper.setState({ customScreenshotUrl: \"\" });\n      wrapper.setProps({ site: { customScreenshotURL: \"foo\" } });\n      assert.equal(0, wrapper.find(\".preview\").length);\n      assert.equal(1, wrapper.find(\".done\").length);\n    });\n  });\n\n  describe(\"#previewMode\", () => {\n    beforeEach(() => setup({ previewResponse: null }));\n\n    it(\"should transition from save to preview\", () => {\n      wrapper.setProps({\n        site: { url: \"https://foo.bar\", customScreenshotURL: \"baz\" },\n        index: 7,\n      });\n\n      assert.equal(\n        wrapper.findWhere(\n          n =>\n            n.length && n.prop(\"data-l10n-id\") === \"newtab-topsites-save-button\"\n        ).length,\n        1\n      );\n\n      wrapper.setState({ customScreenshotUrl: \"foo\" });\n\n      assert.equal(\n        wrapper.findWhere(\n          n =>\n            n.length &&\n            n.prop(\"data-l10n-id\") === \"newtab-topsites-preview-button\"\n        ).length,\n        1\n      );\n    });\n\n    it(\"should transition from add to preview\", () => {\n      assert.equal(\n        wrapper.findWhere(\n          n =>\n            n.length && n.prop(\"data-l10n-id\") === \"newtab-topsites-add-button\"\n        ).length,\n        1\n      );\n\n      wrapper.setState({ customScreenshotUrl: \"foo\" });\n\n      assert.equal(\n        wrapper.findWhere(\n          n =>\n            n.length &&\n            n.prop(\"data-l10n-id\") === \"newtab-topsites-preview-button\"\n        ).length,\n        1\n      );\n    });\n  });\n\n  describe(\"#validateUrl\", () => {\n    it(\"should properly validate URLs\", () => {\n      setup();\n      assert.ok(wrapper.instance().validateUrl(\"mozilla.org\"));\n      assert.ok(wrapper.instance().validateUrl(\"https://mozilla.org\"));\n      assert.ok(wrapper.instance().validateUrl(\"http://mozilla.org\"));\n      assert.ok(\n        wrapper\n          .instance()\n          .validateUrl(\n            \"https://mozilla.invisionapp.com/d/main/#/projects/prototypes\"\n          )\n      );\n      assert.ok(wrapper.instance().validateUrl(\"httpfoobar\"));\n      assert.ok(wrapper.instance().validateUrl(\"httpsfoo.bar\"));\n      assert.isNull(wrapper.instance().validateUrl(\"mozilla org\"));\n      assert.isNull(wrapper.instance().validateUrl(\"\"));\n    });\n  });\n\n  describe(\"#cleanUrl\", () => {\n    it(\"should properly prepend http:// to URLs when required\", () => {\n      setup();\n      assert.equal(\n        \"http://mozilla.org\",\n        wrapper.instance().cleanUrl(\"mozilla.org\")\n      );\n      assert.equal(\n        \"http://https.org\",\n        wrapper.instance().cleanUrl(\"https.org\")\n      );\n      assert.equal(\"http://httpcom\", wrapper.instance().cleanUrl(\"httpcom\"));\n      assert.equal(\n        \"http://mozilla.org\",\n        wrapper.instance().cleanUrl(\"http://mozilla.org\")\n      );\n      assert.equal(\n        \"https://firefox.com\",\n        wrapper.instance().cleanUrl(\"https://firefox.com\")\n      );\n    });\n  });\n});\n\ndescribe(\"<TopSiteList>\", () => {\n  it(\"should render a TopSiteList element\", () => {\n    const wrapper = shallow(<TopSiteList {...DEFAULT_PROPS} />);\n    assert.ok(wrapper.exists());\n  });\n  it(\"should render a TopSite for each link with the right url\", () => {\n    const rows = [{ url: \"https://foo.com\" }, { url: \"https://bar.com\" }];\n    const wrapper = shallow(\n      <TopSiteList {...DEFAULT_PROPS} TopSites={{ rows }} />\n    );\n    const links = wrapper.find(TopSite);\n    assert.lengthOf(links, 2);\n    rows.forEach((row, i) =>\n      assert.equal(links.get(i).props.link.url, row.url)\n    );\n  });\n  it(\"should slice the TopSite rows to the TopSitesRows pref\", () => {\n    const rows = [];\n    for (\n      let i = 0;\n      i < TOP_SITES_DEFAULT_ROWS * TOP_SITES_MAX_SITES_PER_ROW + 3;\n      i++\n    ) {\n      rows.push({ url: `https://foo${i}.com` });\n    }\n    const wrapper = shallow(\n      <TopSiteList\n        {...DEFAULT_PROPS}\n        TopSites={{ rows }}\n        TopSitesRows={TOP_SITES_DEFAULT_ROWS}\n      />\n    );\n    const links = wrapper.find(TopSite);\n    assert.lengthOf(\n      links,\n      TOP_SITES_DEFAULT_ROWS * TOP_SITES_MAX_SITES_PER_ROW\n    );\n  });\n  it(\"should fill with placeholders if TopSites rows is less than TopSitesRows\", () => {\n    const rows = [{ url: \"https://foo.com\" }, { url: \"https://bar.com\" }];\n    const wrapper = shallow(\n      <TopSiteList {...DEFAULT_PROPS} TopSites={{ rows }} TopSitesRows={1} />\n    );\n    assert.lengthOf(wrapper.find(TopSite), 2, \"topSites\");\n    assert.lengthOf(\n      wrapper.find(TopSitePlaceholder),\n      TOP_SITES_MAX_SITES_PER_ROW - 2,\n      \"placeholders\"\n    );\n  });\n  it(\"should fill any holes in TopSites with placeholders\", () => {\n    const rows = [{ url: \"https://foo.com\" }];\n    rows[3] = { url: \"https://bar.com\" };\n    const wrapper = shallow(\n      <TopSiteList {...DEFAULT_PROPS} TopSites={{ rows }} TopSitesRows={1} />\n    );\n    assert.lengthOf(wrapper.find(TopSite), 2, \"topSites\");\n    assert.lengthOf(\n      wrapper.find(TopSitePlaceholder),\n      TOP_SITES_MAX_SITES_PER_ROW - 2,\n      \"placeholders\"\n    );\n  });\n  it(\"should update state onDragStart and clear it onDragEnd\", () => {\n    const wrapper = shallow(<TopSiteList {...DEFAULT_PROPS} />);\n    const instance = wrapper.instance();\n    const index = 7;\n    const link = { url: \"https://foo.com\" };\n    const title = \"foo\";\n    instance.onDragEvent({ type: \"dragstart\" }, index, link, title);\n    assert.equal(instance.state.draggedIndex, index);\n    assert.equal(instance.state.draggedSite, link);\n    assert.equal(instance.state.draggedTitle, title);\n    instance.onDragEvent({ type: \"dragend\" });\n    assert.deepEqual(instance.state, TopSiteList.DEFAULT_STATE);\n  });\n  it(\"should clear state when new props arrive after a drop\", () => {\n    const site1 = { url: \"https://foo.com\" };\n    const site2 = { url: \"https://bar.com\" };\n    const rows = [site1, site2];\n    const wrapper = shallow(\n      <TopSiteList {...DEFAULT_PROPS} TopSites={{ rows }} />\n    );\n    const instance = wrapper.instance();\n    instance.setState({\n      draggedIndex: 1,\n      draggedSite: site2,\n      draggedTitle: \"bar\",\n      topSitesPreview: [],\n    });\n    wrapper.setProps({ TopSites: { rows: [site2, site1] } });\n    assert.deepEqual(instance.state, TopSiteList.DEFAULT_STATE);\n  });\n  it(\"should dispatch events on drop\", () => {\n    const dispatch = sinon.spy();\n    const wrapper = shallow(\n      <TopSiteList {...DEFAULT_PROPS} dispatch={dispatch} />\n    );\n    const instance = wrapper.instance();\n    const index = 7;\n    const link = { url: \"https://foo.com\", customScreenshotURL: \"foo\" };\n    const title = \"foo\";\n    instance.onDragEvent({ type: \"dragstart\" }, index, link, title);\n    dispatch.resetHistory();\n    instance.onDragEvent({ type: \"drop\" }, 3);\n    assert.calledTwice(dispatch);\n    assert.calledWith(dispatch, {\n      data: {\n        draggedFromIndex: 7,\n        index: 3,\n        site: {\n          label: \"foo\",\n          url: \"https://foo.com\",\n          customScreenshotURL: \"foo\",\n        },\n      },\n      meta: { from: \"ActivityStream:Content\", to: \"ActivityStream:Main\" },\n      type: \"TOP_SITES_INSERT\",\n    });\n    assert.calledWith(dispatch, {\n      data: { action_position: 3, event: \"DROP\", source: \"TOP_SITES\" },\n      meta: { from: \"ActivityStream:Content\", to: \"ActivityStream:Main\" },\n      type: \"TELEMETRY_USER_EVENT\",\n    });\n  });\n  it(\"should make a topSitesPreview onDragEnter\", () => {\n    const wrapper = shallow(<TopSiteList {...DEFAULT_PROPS} />);\n    const instance = wrapper.instance();\n    const site = { url: \"https://foo.com\" };\n    instance.setState({\n      draggedIndex: 4,\n      draggedSite: site,\n      draggedTitle: \"foo\",\n    });\n    const draggedSite = Object.assign({}, site, {\n      isPinned: true,\n      isDragged: true,\n    });\n    instance.onDragEvent({ type: \"dragenter\" }, 2);\n    assert.ok(instance.state.topSitesPreview);\n    assert.deepEqual(instance.state.topSitesPreview[2], draggedSite);\n  });\n  it(\"should _makeTopSitesPreview correctly\", () => {\n    const site1 = { url: \"https://foo.com\" };\n    const site2 = { url: \"https://bar.com\" };\n    const site3 = { url: \"https://baz.com\" };\n    const rows = [site1, site2, site3];\n    let wrapper = shallow(\n      <TopSiteList {...DEFAULT_PROPS} TopSites={{ rows }} TopSitesRows={1} />\n    );\n    let instance = wrapper.instance();\n    instance.setState({\n      draggedIndex: 0,\n      draggedSite: site1,\n      draggedTitle: \"foo\",\n    });\n    let draggedSite = Object.assign({}, site1, {\n      isPinned: true,\n      isDragged: true,\n    });\n    assert.deepEqual(instance._makeTopSitesPreview(1), [\n      site2,\n      draggedSite,\n      site3,\n      null,\n      null,\n      null,\n      null,\n      null,\n    ]);\n    assert.deepEqual(instance._makeTopSitesPreview(2), [\n      site2,\n      site3,\n      draggedSite,\n      null,\n      null,\n      null,\n      null,\n      null,\n    ]);\n    assert.deepEqual(instance._makeTopSitesPreview(3), [\n      site2,\n      site3,\n      null,\n      draggedSite,\n      null,\n      null,\n      null,\n      null,\n    ]);\n    site2.isPinned = true;\n    assert.deepEqual(instance._makeTopSitesPreview(1), [\n      site2,\n      draggedSite,\n      site3,\n      null,\n      null,\n      null,\n      null,\n      null,\n    ]);\n    assert.deepEqual(instance._makeTopSitesPreview(2), [\n      site3,\n      site2,\n      draggedSite,\n      null,\n      null,\n      null,\n      null,\n      null,\n    ]);\n    site3.isPinned = true;\n    assert.deepEqual(instance._makeTopSitesPreview(1), [\n      site2,\n      draggedSite,\n      site3,\n      null,\n      null,\n      null,\n      null,\n      null,\n    ]);\n    assert.deepEqual(instance._makeTopSitesPreview(2), [\n      site2,\n      site3,\n      draggedSite,\n      null,\n      null,\n      null,\n      null,\n      null,\n    ]);\n    site2.isPinned = false;\n    assert.deepEqual(instance._makeTopSitesPreview(1), [\n      site2,\n      draggedSite,\n      site3,\n      null,\n      null,\n      null,\n      null,\n      null,\n    ]);\n    assert.deepEqual(instance._makeTopSitesPreview(2), [\n      site2,\n      site3,\n      draggedSite,\n      null,\n      null,\n      null,\n      null,\n      null,\n    ]);\n    site3.isPinned = false;\n    instance.setState({\n      draggedIndex: 1,\n      draggedSite: site2,\n      draggedTitle: \"bar\",\n    });\n    draggedSite = Object.assign({}, site2, { isPinned: true, isDragged: true });\n    assert.deepEqual(instance._makeTopSitesPreview(0), [\n      draggedSite,\n      site1,\n      site3,\n      null,\n      null,\n      null,\n      null,\n      null,\n    ]);\n    assert.deepEqual(instance._makeTopSitesPreview(2), [\n      site1,\n      site3,\n      draggedSite,\n      null,\n      null,\n      null,\n      null,\n      null,\n    ]);\n  });\n  it(\"should add a className hide-for-narrow to sites after 6/row\", () => {\n    const rows = [];\n    for (let i = 0; i < TOP_SITES_MAX_SITES_PER_ROW; i++) {\n      rows.push({ url: `https://foo${i}.com` });\n    }\n    const wrapper = mount(\n      <TopSiteList {...DEFAULT_PROPS} TopSites={{ rows }} TopSitesRows={1} />\n    );\n    assert.lengthOf(wrapper.find(\"li.hide-for-narrow\"), 2);\n  });\n});\n\ndescribe(\"TopSitePlaceholder\", () => {\n  it(\"should dispatch a TOP_SITES_EDIT action when edit-button is clicked\", () => {\n    const dispatch = sinon.spy();\n    const wrapper = shallow(\n      <TopSitePlaceholder dispatch={dispatch} index={7} />\n    );\n\n    wrapper\n      .find(\".edit-button\")\n      .first()\n      .simulate(\"click\");\n\n    assert.calledOnce(dispatch);\n    assert.calledWithExactly(dispatch, {\n      type: at.TOP_SITES_EDIT,\n      data: { index: 7 },\n    });\n  });\n});\n\ndescribe(\"#TopSiteFormInput\", () => {\n  let wrapper;\n  let onChangeStub;\n\n  describe(\"no errors\", () => {\n    beforeEach(() => {\n      onChangeStub = sinon.stub();\n\n      wrapper = mount(\n        <TopSiteFormInput\n          titleId=\"newtab-topsites-title-label\"\n          placeholderId=\"newtab-topsites-title-input\"\n          errorMessageId=\"newtab-topsites-url-validation\"\n          onChange={onChangeStub}\n          value=\"foo\"\n        />\n      );\n    });\n\n    it(\"should render the provided title\", () => {\n      const title = wrapper.find(\"span\");\n      assert.propertyVal(\n        title.props(),\n        \"data-l10n-id\",\n        \"newtab-topsites-title-label\"\n      );\n    });\n\n    it(\"should render the provided value\", () => {\n      const input = wrapper.find(\"input\");\n\n      assert.equal(input.getDOMNode().value, \"foo\");\n    });\n\n    it(\"should render the clear button if cb is provided\", () => {\n      assert.equal(wrapper.find(\".icon-clear-input\").length, 0);\n\n      wrapper.setProps({ onClear: sinon.stub() });\n\n      assert.equal(wrapper.find(\".icon-clear-input\").length, 1);\n    });\n\n    it(\"should show the loading indicator\", () => {\n      assert.equal(wrapper.find(\".loading-container\").length, 0);\n\n      wrapper.setProps({ loading: true });\n\n      assert.equal(wrapper.find(\".loading-container\").length, 1);\n    });\n    it(\"should disable the input when loading indicator is present\", () => {\n      assert.isFalse(wrapper.find(\"input\").getDOMNode().disabled);\n\n      wrapper.setProps({ loading: true });\n\n      assert.isTrue(wrapper.find(\"input\").getDOMNode().disabled);\n    });\n  });\n\n  describe(\"with error\", () => {\n    beforeEach(() => {\n      onChangeStub = sinon.stub();\n\n      wrapper = mount(\n        <TopSiteFormInput\n          titleId=\"newtab-topsites-title-label\"\n          placeholderId=\"newtab-topsites-title-input\"\n          onChange={onChangeStub}\n          validationError={true}\n          errorMessageId=\"newtab-topsites-url-validation\"\n          value=\"foo\"\n        />\n      );\n    });\n\n    it(\"should render the error message\", () => {\n      assert.equal(\n        wrapper.findWhere(\n          n => n.prop(\"data-l10n-id\") === \"newtab-topsites-url-validation\"\n        ).length,\n        1\n      );\n    });\n\n    it(\"should reset the error state on value change\", () => {\n      wrapper.find(\"input\").simulate(\"change\", { target: { value: \"bar\" } });\n\n      assert.isFalse(wrapper.state().validationError);\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/content-src/components/Topics.test.jsx",
    "content": "import { Topic, Topics } from \"content-src/components/Topics/Topics\";\nimport React from \"react\";\nimport { shallow } from \"enzyme\";\n\ndescribe(\"<Topics>\", () => {\n  it(\"should render a Topics element\", () => {\n    const wrapper = shallow(<Topics topics={[]} />);\n    assert.ok(wrapper.exists());\n  });\n  it(\"should render a Topic element for each topic with the right url\", () => {\n    const data = [\n      { name: \"topic1\", url: \"https://topic1.com\" },\n      { name: \"topic2\", url: \"https://topic2.com\" },\n    ];\n\n    const wrapper = shallow(<Topics topics={data} />);\n\n    const topics = wrapper.find(Topic);\n    assert.lengthOf(topics, 2);\n    topics.forEach((topic, i) => assert.equal(topic.props().url, data[i].url));\n  });\n});\n"
  },
  {
    "path": "test/unit/content-src/components/addUtmParams.test.js",
    "content": "import {\n  addUtmParams,\n  BASE_PARAMS,\n} from \"content-src/asrouter/templates/FirstRun/addUtmParams\";\n\ndescribe(\"addUtmParams\", () => {\n  it(\"should convert a string URL\", () => {\n    const result = addUtmParams(\"https://foo.com\", \"foo\");\n    assert.equal(result.hostname, \"foo.com\");\n  });\n  it(\"should add all base params\", () => {\n    assert.match(\n      addUtmParams(new URL(\"https://foo.com\"), \"foo\").toString(),\n      /utm_source=activity-stream&utm_campaign=firstrun&utm_medium=referral/\n    );\n  });\n  it(\"should allow updating base params utm values\", () => {\n    BASE_PARAMS.utm_campaign = \"firstrun-default\";\n    assert.match(\n      addUtmParams(new URL(\"https://foo.com\"), \"foo\", \"default\").toString(),\n      /utm_source=activity-stream&utm_campaign=firstrun-default&utm_medium=referral/\n    );\n  });\n  it(\"should add utm_term\", () => {\n    const params = addUtmParams(new URL(\"https://foo.com\"), \"foo\").searchParams;\n    assert.equal(params.get(\"utm_term\"), \"foo\", \"utm_term\");\n  });\n});\n"
  },
  {
    "path": "test/unit/content-src/lib/detect-user-session-start.test.js",
    "content": "import { actionCreators as ac, actionTypes as at } from \"common/Actions.jsm\";\nimport { DetectUserSessionStart } from \"content-src/lib/detect-user-session-start\";\n\ndescribe(\"detectUserSessionStart\", () => {\n  let store;\n  class PerfService {\n    getMostRecentAbsMarkStartByName() {\n      return 1234;\n    }\n    mark() {}\n  }\n\n  beforeEach(() => {\n    store = { dispatch: () => {} };\n  });\n  describe(\"#sendEventOrAddListener\", () => {\n    it(\"should call ._sendEvent immediately if the document is visible\", () => {\n      const mockDocument = { visibilityState: \"visible\" };\n      const instance = new DetectUserSessionStart(store, {\n        document: mockDocument,\n      });\n      sinon.stub(instance, \"_sendEvent\");\n\n      instance.sendEventOrAddListener();\n\n      assert.calledOnce(instance._sendEvent);\n    });\n    it(\"should add an event listener on visibility changes the document is not visible\", () => {\n      const mockDocument = {\n        visibilityState: \"hidden\",\n        addEventListener: sinon.spy(),\n      };\n      const instance = new DetectUserSessionStart(store, {\n        document: mockDocument,\n      });\n      sinon.stub(instance, \"_sendEvent\");\n\n      instance.sendEventOrAddListener();\n\n      assert.notCalled(instance._sendEvent);\n      assert.calledWith(\n        mockDocument.addEventListener,\n        \"visibilitychange\",\n        instance._onVisibilityChange\n      );\n    });\n  });\n  describe(\"#_sendEvent\", () => {\n    it(\"should dispatch an action with the SAVE_SESSION_PERF_DATA\", () => {\n      const dispatch = sinon.spy(store, \"dispatch\");\n      const instance = new DetectUserSessionStart(store);\n\n      instance._sendEvent();\n\n      assert.calledWith(\n        dispatch,\n        ac.AlsoToMain({\n          type: at.SAVE_SESSION_PERF_DATA,\n          data: { visibility_event_rcvd_ts: sinon.match.number },\n        })\n      );\n    });\n\n    it(\"shouldn't send a message if getMostRecentAbsMarkStartByName throws\", () => {\n      let perfService = new PerfService();\n      sinon.stub(perfService, \"getMostRecentAbsMarkStartByName\").throws();\n      const dispatch = sinon.spy(store, \"dispatch\");\n      const instance = new DetectUserSessionStart(store, { perfService });\n\n      instance._sendEvent();\n\n      assert.notCalled(dispatch);\n    });\n\n    it('should call perfService.mark(\"visibility_event_rcvd_ts\")', () => {\n      let perfService = new PerfService();\n      sinon.stub(perfService, \"mark\");\n      const instance = new DetectUserSessionStart(store, { perfService });\n\n      instance._sendEvent();\n\n      assert.calledWith(perfService.mark, \"visibility_event_rcvd_ts\");\n    });\n  });\n\n  describe(\"_onVisibilityChange\", () => {\n    it(\"should not send an event if visiblity is not visible\", () => {\n      const instance = new DetectUserSessionStart(store, {\n        document: { visibilityState: \"hidden\" },\n      });\n      sinon.stub(instance, \"_sendEvent\");\n\n      instance._onVisibilityChange();\n\n      assert.notCalled(instance._sendEvent);\n    });\n    it(\"should send an event and remove the event listener if visibility is visible\", () => {\n      const mockDocument = {\n        visibilityState: \"visible\",\n        removeEventListener: sinon.spy(),\n      };\n      const instance = new DetectUserSessionStart(store, {\n        document: mockDocument,\n      });\n      sinon.stub(instance, \"_sendEvent\");\n\n      instance._onVisibilityChange();\n\n      assert.calledOnce(instance._sendEvent);\n      assert.calledWith(\n        mockDocument.removeEventListener,\n        \"visibilitychange\",\n        instance._onVisibilityChange\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/content-src/lib/init-store.test.js",
    "content": "import { actionCreators as ac, actionTypes as at } from \"common/Actions.jsm\";\nimport { addNumberReducer, GlobalOverrider } from \"test/unit/utils\";\nimport {\n  EARLY_QUEUED_ACTIONS,\n  INCOMING_MESSAGE_NAME,\n  initStore,\n  MERGE_STORE_ACTION,\n  OUTGOING_MESSAGE_NAME,\n  queueEarlyMessageMiddleware,\n  rehydrationMiddleware,\n} from \"content-src/lib/init-store\";\n\ndescribe(\"initStore\", () => {\n  let globals;\n  let store;\n  beforeEach(() => {\n    globals = new GlobalOverrider();\n    globals.set(\"RPMSendAsyncMessage\", globals.sandbox.spy());\n    globals.set(\"RPMAddMessageListener\", globals.sandbox.spy());\n    store = initStore({ number: addNumberReducer });\n  });\n  afterEach(() => globals.restore());\n  it(\"should create a store with the provided reducers\", () => {\n    assert.ok(store);\n    assert.property(store.getState(), \"number\");\n  });\n  it(\"should add a listener that dispatches actions\", () => {\n    assert.calledWith(global.RPMAddMessageListener, INCOMING_MESSAGE_NAME);\n    const [, listener] = global.RPMAddMessageListener.firstCall.args;\n    globals.sandbox.spy(store, \"dispatch\");\n    const message = { name: INCOMING_MESSAGE_NAME, data: { type: \"FOO\" } };\n\n    listener(message);\n\n    assert.calledWith(store.dispatch, message.data);\n  });\n  it(\"should not throw if RPMAddMessageListener is not defined\", () => {\n    // Note: this is being set/restored by GlobalOverrider\n    delete global.RPMAddMessageListener;\n\n    assert.doesNotThrow(() => initStore({ number: addNumberReducer }));\n  });\n  it(\"should log errors from failed messages\", () => {\n    const [, callback] = global.RPMAddMessageListener.firstCall.args;\n    globals.sandbox.stub(global.console, \"error\");\n    globals.sandbox.stub(store, \"dispatch\").throws(Error(\"failed\"));\n\n    const message = {\n      name: INCOMING_MESSAGE_NAME,\n      data: { type: MERGE_STORE_ACTION },\n    };\n    callback(message);\n\n    assert.calledOnce(global.console.error);\n  });\n  it(\"should replace the state if a MERGE_STORE_ACTION is dispatched\", () => {\n    store.dispatch({ type: MERGE_STORE_ACTION, data: { number: 42 } });\n    assert.deepEqual(store.getState(), { number: 42 });\n  });\n  it(\"should call .send and update the local store if an AlsoToMain action is dispatched\", () => {\n    const subscriber = sinon.spy();\n    const action = ac.AlsoToMain({ type: \"FOO\" });\n\n    store.subscribe(subscriber);\n    store.dispatch(action);\n\n    assert.calledWith(\n      global.RPMSendAsyncMessage,\n      OUTGOING_MESSAGE_NAME,\n      action\n    );\n    assert.calledOnce(subscriber);\n  });\n  it(\"should call .send but not update the local store if an OnlyToMain action is dispatched\", () => {\n    const subscriber = sinon.spy();\n    const action = ac.OnlyToMain({ type: \"FOO\" });\n\n    store.subscribe(subscriber);\n    store.dispatch(action);\n\n    assert.calledWith(\n      global.RPMSendAsyncMessage,\n      OUTGOING_MESSAGE_NAME,\n      action\n    );\n    assert.notCalled(subscriber);\n  });\n  it(\"should not send out other types of actions\", () => {\n    store.dispatch({ type: \"FOO\" });\n    assert.notCalled(global.RPMSendAsyncMessage);\n  });\n  describe(\"rehydrationMiddleware\", () => {\n    it(\"should allow NEW_TAB_STATE_REQUEST to go through\", () => {\n      const action = ac.AlsoToMain({ type: at.NEW_TAB_STATE_REQUEST });\n      const next = sinon.spy();\n      rehydrationMiddleware(store)(next)(action);\n      assert.calledWith(next, action);\n    });\n    it(\"should dispatch an additional NEW_TAB_STATE_REQUEST if INIT was received after a request\", () => {\n      const requestAction = ac.AlsoToMain({ type: at.NEW_TAB_STATE_REQUEST });\n      const next = sinon.spy();\n\n      rehydrationMiddleware(store)(next)(requestAction);\n\n      next.resetHistory();\n      rehydrationMiddleware(store)(next)({ type: at.INIT });\n      assert.calledWith(next, requestAction);\n    });\n    it(\"should allow MERGE_STORE_ACTION to go through\", () => {\n      const action = { type: MERGE_STORE_ACTION };\n      const next = sinon.spy();\n      rehydrationMiddleware(store)(next)(action);\n      assert.calledWith(next, action);\n    });\n    it(\"should not allow actions from main to go through before MERGE_STORE_ACTION was received\", () => {\n      const next = sinon.spy();\n\n      rehydrationMiddleware(store)(next)(\n        ac.BroadcastToContent({ type: \"FOO\" })\n      );\n      rehydrationMiddleware(store)(next)(\n        ac.AlsoToOneContent({ type: \"FOO\" }, 123)\n      );\n\n      assert.notCalled(next);\n    });\n    it(\"should allow all local actions to go through\", () => {\n      const action = { type: \"FOO\" };\n      const next = sinon.spy();\n      rehydrationMiddleware(store)(next)(action);\n      assert.calledWith(next, action);\n    });\n    it(\"should allow actions from main to go through after MERGE_STORE_ACTION has been received\", () => {\n      const next = sinon.spy();\n      rehydrationMiddleware(store)(next)({ type: MERGE_STORE_ACTION });\n      next.resetHistory();\n\n      const action = ac.AlsoToOneContent({ type: \"FOO\" }, 123);\n      rehydrationMiddleware(store)(next)(action);\n      assert.calledWith(next, action);\n    });\n  });\n  describe(\"queueEarlyMessageMiddleware\", () => {\n    it(\"should allow all local actions to go through\", () => {\n      const action = { type: \"FOO\" };\n      const next = sinon.spy();\n\n      queueEarlyMessageMiddleware(store)(next)(action);\n\n      assert.calledWith(next, action);\n    });\n    it(\"should allow action to main that does not belong to EARLY_QUEUED_ACTIONS to go through\", () => {\n      const action = ac.AlsoToMain({ type: \"FOO\" });\n      const next = sinon.spy();\n\n      queueEarlyMessageMiddleware(store)(next)(action);\n\n      assert.calledWith(next, action);\n    });\n    it(`should line up EARLY_QUEUED_ACTIONS only let them go through after it receives the action from main`, () => {\n      EARLY_QUEUED_ACTIONS.forEach(actionType => {\n        const testStore = initStore({ number: addNumberReducer });\n        const next = sinon.spy();\n        const action = ac.AlsoToMain({ type: actionType });\n        const fromMainAction = ac.AlsoToOneContent({ type: \"FOO\" }, 123);\n\n        // Early actions should be added to the queue\n        queueEarlyMessageMiddleware(testStore)(next)(action);\n        queueEarlyMessageMiddleware(testStore)(next)(action);\n\n        assert.notCalled(next);\n        assert.equal(testStore._earlyActionQueue.length, 2);\n        next.resetHistory();\n\n        // Receiving action from main would empty the queue\n        queueEarlyMessageMiddleware(testStore)(next)(fromMainAction);\n\n        assert.calledThrice(next);\n        assert.equal(next.firstCall.args[0], fromMainAction);\n        assert.equal(next.secondCall.args[0], action);\n        assert.equal(next.thirdCall.args[0], action);\n        assert.equal(testStore._earlyActionQueue.length, 0);\n        next.resetHistory();\n\n        // New action should go through immediately\n        queueEarlyMessageMiddleware(testStore)(next)(action);\n        assert.calledOnce(next);\n        assert.calledWith(next, action);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/content-src/lib/screenshot-utils.test.js",
    "content": "import { GlobalOverrider } from \"test/unit/utils\";\nimport { ScreenshotUtils } from \"content-src/lib/screenshot-utils\";\n\nconst DEFAULT_BLOB_URL = \"blob://test\";\n\ndescribe(\"ScreenshotUtils\", () => {\n  let globals;\n  let url;\n  beforeEach(() => {\n    globals = new GlobalOverrider();\n    url = {\n      createObjectURL: globals.sandbox.stub().returns(DEFAULT_BLOB_URL),\n      revokeObjectURL: globals.sandbox.spy(),\n    };\n    globals.set(\"URL\", url);\n  });\n  afterEach(() => globals.restore());\n  describe(\"#createLocalImageObject\", () => {\n    it(\"should return null if no remoteImage is supplied\", () => {\n      let localImageObject = ScreenshotUtils.createLocalImageObject(null);\n\n      assert.notCalled(url.createObjectURL);\n      assert.equal(localImageObject, null);\n    });\n    it(\"should create a local image object with the correct properties if remoteImage is a blob\", () => {\n      let localImageObject = ScreenshotUtils.createLocalImageObject({\n        path: \"/path1\",\n        data: new Blob([0]),\n      });\n\n      assert.calledOnce(url.createObjectURL);\n      assert.deepEqual(localImageObject, {\n        path: \"/path1\",\n        url: DEFAULT_BLOB_URL,\n      });\n    });\n    it(\"should create a local image object with the correct properties if remoteImage is a normal image\", () => {\n      const imageUrl = \"https://test-url\";\n      let localImageObject = ScreenshotUtils.createLocalImageObject(imageUrl);\n\n      assert.notCalled(url.createObjectURL);\n      assert.deepEqual(localImageObject, { url: imageUrl });\n    });\n  });\n  describe(\"#maybeRevokeBlobObjectURL\", () => {\n    // Note that we should also ensure that all the tests for #isBlob are green.\n    it(\"should call revokeObjectURL if image is a blob\", () => {\n      ScreenshotUtils.maybeRevokeBlobObjectURL({\n        path: \"/path1\",\n        url: \"blob://test\",\n      });\n\n      assert.calledOnce(url.revokeObjectURL);\n    });\n    it(\"should not call revokeObjectURL if image is not a blob\", () => {\n      ScreenshotUtils.maybeRevokeBlobObjectURL({ url: \"https://test-url\" });\n\n      assert.notCalled(url.revokeObjectURL);\n    });\n  });\n  describe(\"#isRemoteImageLocal\", () => {\n    it(\"should return true if both propsImage and stateImage are not present\", () => {\n      assert.isTrue(ScreenshotUtils.isRemoteImageLocal(null, null));\n    });\n    it(\"should return false if propsImage is present and stateImage is not present\", () => {\n      assert.isFalse(ScreenshotUtils.isRemoteImageLocal(null, {}));\n    });\n    it(\"should return false if propsImage is not present and stateImage is present\", () => {\n      assert.isFalse(ScreenshotUtils.isRemoteImageLocal({}, null));\n    });\n    it(\"should return true if both propsImage and stateImage are equal blobs\", () => {\n      const blobPath = \"/test-blob-path/test.png\";\n      assert.isTrue(\n        ScreenshotUtils.isRemoteImageLocal(\n          { path: blobPath, url: \"blob://test\" }, // state\n          { path: blobPath, data: new Blob([0]) } // props\n        )\n      );\n    });\n    it(\"should return false if both propsImage and stateImage are different blobs\", () => {\n      assert.isFalse(\n        ScreenshotUtils.isRemoteImageLocal(\n          { path: \"/path1\", url: \"blob://test\" }, // state\n          { path: \"/path2\", data: new Blob([0]) } // props\n        )\n      );\n    });\n    it(\"should return true if both propsImage and stateImage are equal normal images\", () => {\n      assert.isTrue(\n        ScreenshotUtils.isRemoteImageLocal(\n          { url: \"test url\" }, // state\n          \"test url\" // props\n        )\n      );\n    });\n    it(\"should return false if both propsImage and stateImage are different normal images\", () => {\n      assert.isFalse(\n        ScreenshotUtils.isRemoteImageLocal(\n          { url: \"test url 1\" }, // state\n          \"test url 2\" // props\n        )\n      );\n    });\n    it(\"should return false if both propsImage and stateImage are different type of images\", () => {\n      assert.isFalse(\n        ScreenshotUtils.isRemoteImageLocal(\n          { path: \"/path1\", url: \"blob://test\" }, // state\n          \"test url 2\" // props\n        )\n      );\n      assert.isFalse(\n        ScreenshotUtils.isRemoteImageLocal(\n          { url: \"https://test-url\" }, // state\n          { path: \"/path1\", data: new Blob([0]) } // props\n        )\n      );\n    });\n  });\n  describe(\"#isBlob\", () => {\n    let state = {\n      blobImage: { path: \"/test\", url: \"blob://test\" },\n      normalImage: { url: \"https://test-url\" },\n    };\n    let props = {\n      blobImage: { path: \"/test\", data: new Blob([0]) },\n      normalImage: \"https://test-url\",\n    };\n    it(\"should return false if image is null\", () => {\n      assert.isFalse(ScreenshotUtils.isBlob(true, null));\n      assert.isFalse(ScreenshotUtils.isBlob(false, null));\n    });\n    it(\"should return true if image is a blob and type matches\", () => {\n      assert.isTrue(ScreenshotUtils.isBlob(true, state.blobImage));\n      assert.isTrue(ScreenshotUtils.isBlob(false, props.blobImage));\n    });\n    it(\"should return false if image is not a blob and type matches\", () => {\n      assert.isFalse(ScreenshotUtils.isBlob(true, state.normalImage));\n      assert.isFalse(ScreenshotUtils.isBlob(false, props.normalImage));\n    });\n    it(\"should return false if type does not match\", () => {\n      assert.isFalse(ScreenshotUtils.isBlob(false, state.blobImage));\n      assert.isFalse(ScreenshotUtils.isBlob(false, state.normalImage));\n      assert.isFalse(ScreenshotUtils.isBlob(true, props.blobImage));\n      assert.isFalse(ScreenshotUtils.isBlob(true, props.normalImage));\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/content-src/lib/selectLayoutRender.test.js",
    "content": "import { combineReducers, createStore } from \"redux\";\nimport { actionTypes as at } from \"common/Actions.jsm\";\nimport { GlobalOverrider } from \"test/unit/utils\";\nimport { reducers } from \"common/Reducers.jsm\";\nimport { selectLayoutRender } from \"content-src/lib/selectLayoutRender\";\nconst FAKE_LAYOUT = [\n  {\n    width: 3,\n    components: [\n      { type: \"foo\", feed: { url: \"foo.com\" }, properties: { items: 2 } },\n    ],\n  },\n];\nconst FAKE_FEEDS = {\n  \"foo.com\": { data: { recommendations: [{ id: \"foo\" }, { id: \"bar\" }] } },\n};\n\ndescribe(\"selectLayoutRender\", () => {\n  let store;\n  let globals;\n\n  beforeEach(() => {\n    globals = new GlobalOverrider();\n    store = createStore(combineReducers(reducers));\n  });\n\n  afterEach(() => {\n    globals.restore();\n  });\n\n  it(\"should return an empty array given initial state\", () => {\n    const { layoutRender } = selectLayoutRender({\n      state: store.getState().DiscoveryStream,\n      prefs: {},\n      rollCache: [],\n    });\n    assert.deepEqual(layoutRender, []);\n  });\n\n  it(\"should return an empty SPOCS fill array given initial state\", () => {\n    const { spocsFill } = selectLayoutRender({\n      state: store.getState().DiscoveryStream,\n    });\n    assert.deepEqual(spocsFill, []);\n  });\n\n  it(\"should add .data property from feeds to each compontent in .layout\", () => {\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,\n      data: { layout: FAKE_LAYOUT },\n    });\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_FEED_UPDATE,\n      data: { feed: FAKE_FEEDS[\"foo.com\"], url: \"foo.com\" },\n    });\n    store.dispatch({ type: at.DISCOVERY_STREAM_FEEDS_UPDATE });\n\n    const { layoutRender } = selectLayoutRender({\n      state: store.getState().DiscoveryStream,\n    });\n\n    assert.lengthOf(layoutRender, 1);\n    assert.propertyVal(layoutRender[0], \"width\", 3);\n    assert.deepEqual(layoutRender[0].components[0], {\n      type: \"foo\",\n      feed: { url: \"foo.com\" },\n      properties: { items: 2 },\n      data: { recommendations: [{ id: \"foo\", pos: 0 }, { id: \"bar\", pos: 1 }] },\n    });\n  });\n\n  it(\"should return layout with placeholder data if feed doesn't have data\", () => {\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,\n      data: { layout: FAKE_LAYOUT },\n    });\n    store.dispatch({ type: at.DISCOVERY_STREAM_FEEDS_UPDATE });\n\n    const { layoutRender } = selectLayoutRender({\n      state: store.getState().DiscoveryStream,\n    });\n\n    assert.lengthOf(layoutRender, 1);\n    assert.propertyVal(layoutRender[0], \"width\", 3);\n    assert.deepEqual(layoutRender[0].components[0].data.recommendations, [\n      { placeholder: true },\n      { placeholder: true },\n    ]);\n  });\n\n  it(\"should return layout with empty spocs data if feed isn't defined but spocs is\", () => {\n    const fakeLayout = [\n      {\n        width: 3,\n        components: [{ type: \"foo\", spocs: { positions: [{ index: 2 }] } }],\n      },\n    ];\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,\n      data: { layout: fakeLayout },\n    });\n    store.dispatch({ type: at.DISCOVERY_STREAM_FEEDS_UPDATE });\n\n    const { layoutRender } = selectLayoutRender({\n      state: store.getState().DiscoveryStream,\n    });\n\n    assert.lengthOf(layoutRender, 1);\n    assert.propertyVal(layoutRender[0], \"width\", 3);\n    assert.deepEqual(layoutRender[0].components[0].data.spocs, []);\n  });\n\n  it(\"should return layout with spocs data if feed isn't defined but spocs is\", () => {\n    const fakeLayout = [\n      {\n        width: 3,\n        components: [\n          { type: \"foo\", spocs: { probability: 1, positions: [{ index: 0 }] } },\n        ],\n      },\n    ];\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,\n      data: { layout: fakeLayout },\n    });\n    store.dispatch({ type: at.DISCOVERY_STREAM_FEEDS_UPDATE });\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_SPOCS_UPDATE,\n      data: { lastUpdated: 0, spocs: { spocs: [1, 2, 3] } },\n    });\n\n    const { layoutRender } = selectLayoutRender({\n      state: store.getState().DiscoveryStream,\n    });\n\n    assert.lengthOf(layoutRender, 1);\n    assert.propertyVal(layoutRender[0], \"width\", 3);\n    assert.deepEqual(layoutRender[0].components[0].data.spocs, [1]);\n  });\n\n  it(\"should return feed data offset by layout set prop\", () => {\n    const fakeLayout = [\n      {\n        width: 3,\n        components: [\n          { type: \"foo\", properties: { offset: 1 }, feed: { url: \"foo.com\" } },\n        ],\n      },\n    ];\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,\n      data: { layout: fakeLayout },\n    });\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_FEED_UPDATE,\n      data: { feed: FAKE_FEEDS[\"foo.com\"], url: \"foo.com\" },\n    });\n    store.dispatch({ type: at.DISCOVERY_STREAM_FEEDS_UPDATE });\n\n    const { layoutRender } = selectLayoutRender({\n      state: store.getState().DiscoveryStream,\n    });\n\n    assert.deepEqual(layoutRender[0].components[0].data, {\n      recommendations: [{ id: \"bar\" }],\n    });\n  });\n\n  it(\"should return spoc result and spocs fill for rolls below the probability\", () => {\n    const fakeSpocConfig = {\n      positions: [{ index: 0 }, { index: 1 }],\n      probability: 0.5,\n    };\n    const fakeLayout = [\n      {\n        width: 3,\n        components: [\n          { type: \"foo\", feed: { url: \"foo.com\" }, spocs: fakeSpocConfig },\n        ],\n      },\n    ];\n    const fakeSpocsData = {\n      lastUpdated: 0,\n      spocs: { spocs: [\"fooSpoc\", \"barSpoc\"] },\n    };\n\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,\n      data: { layout: fakeLayout },\n    });\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_FEED_UPDATE,\n      data: { feed: FAKE_FEEDS[\"foo.com\"], url: \"foo.com\" },\n    });\n    store.dispatch({ type: at.DISCOVERY_STREAM_FEEDS_UPDATE });\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_SPOCS_UPDATE,\n      data: fakeSpocsData,\n    });\n    const randomStub = globals.sandbox.stub(global.Math, \"random\").returns(0.1);\n\n    const { spocsFill, layoutRender } = selectLayoutRender({\n      state: store.getState().DiscoveryStream,\n    });\n\n    assert.calledTwice(randomStub);\n    assert.lengthOf(layoutRender, 1);\n    assert.deepEqual(\n      layoutRender[0].components[0].data.recommendations[0],\n      \"fooSpoc\"\n    );\n    assert.deepEqual(\n      layoutRender[0].components[0].data.recommendations[1],\n      \"barSpoc\"\n    );\n    assert.deepEqual(layoutRender[0].components[0].data.recommendations[2], {\n      id: \"foo\",\n    });\n    assert.deepEqual(layoutRender[0].components[0].data.recommendations[3], {\n      id: \"bar\",\n    });\n\n    assert.deepEqual(spocsFill, [\n      { id: undefined, reason: \"n/a\", displayed: 1, full_recalc: 0 },\n      { id: undefined, reason: \"n/a\", displayed: 1, full_recalc: 0 },\n    ]);\n  });\n\n  it(\"should return spoc result and spocs fill when there are more positions than spocs\", () => {\n    const fakeSpocConfig = {\n      positions: [{ index: 0 }, { index: 1 }, { index: 2 }],\n      probability: 0.5,\n    };\n    const fakeLayout = [\n      {\n        width: 3,\n        components: [\n          { type: \"foo\", feed: { url: \"foo.com\" }, spocs: fakeSpocConfig },\n        ],\n      },\n    ];\n    const fakeSpocsData = {\n      lastUpdated: 0,\n      spocs: { spocs: [\"fooSpoc\", \"barSpoc\"] },\n    };\n\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,\n      data: { layout: fakeLayout },\n    });\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_FEED_UPDATE,\n      data: { feed: FAKE_FEEDS[\"foo.com\"], url: \"foo.com\" },\n    });\n    store.dispatch({ type: at.DISCOVERY_STREAM_FEEDS_UPDATE });\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_SPOCS_UPDATE,\n      data: fakeSpocsData,\n    });\n    const randomStub = globals.sandbox.stub(global.Math, \"random\").returns(0.1);\n\n    const { spocsFill, layoutRender } = selectLayoutRender({\n      state: store.getState().DiscoveryStream,\n    });\n\n    assert.calledTwice(randomStub);\n    assert.lengthOf(layoutRender, 1);\n    assert.deepEqual(\n      layoutRender[0].components[0].data.recommendations[0],\n      \"fooSpoc\"\n    );\n    assert.deepEqual(\n      layoutRender[0].components[0].data.recommendations[1],\n      \"barSpoc\"\n    );\n    assert.deepEqual(layoutRender[0].components[0].data.recommendations[2], {\n      id: \"foo\",\n    });\n    assert.deepEqual(layoutRender[0].components[0].data.recommendations[3], {\n      id: \"bar\",\n    });\n\n    assert.deepEqual(spocsFill, [\n      { id: undefined, reason: \"n/a\", displayed: 1, full_recalc: 0 },\n      { id: undefined, reason: \"n/a\", displayed: 1, full_recalc: 0 },\n    ]);\n  });\n\n  it(\"should report non-displayed spocs with reason as probability_selection and out_of_position\", () => {\n    const fakeSpocConfig = {\n      positions: [{ index: 0 }, { index: 1 }, { index: 2 }],\n      probability: 0.5,\n    };\n    const fakeLayout = [\n      {\n        width: 3,\n        components: [\n          { type: \"foo\", feed: { url: \"foo.com\" }, spocs: fakeSpocConfig },\n        ],\n      },\n    ];\n    const fakeSpocsData = {\n      lastUpdated: 0,\n      spocs: { spocs: [\"fooSpoc\", \"barSpoc\", \"lastSpoc\"] },\n    };\n\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,\n      data: { layout: fakeLayout },\n    });\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_FEED_UPDATE,\n      data: { feed: FAKE_FEEDS[\"foo.com\"], url: \"foo.com\" },\n    });\n    store.dispatch({ type: at.DISCOVERY_STREAM_FEEDS_UPDATE });\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_SPOCS_UPDATE,\n      data: fakeSpocsData,\n    });\n    const randomStub = globals.sandbox.stub(global.Math, \"random\");\n\n    const { spocsFill, layoutRender } = selectLayoutRender({\n      state: store.getState().DiscoveryStream,\n      rollCache: [0.7, 0.3, 0.8],\n    });\n\n    assert.notCalled(randomStub);\n    assert.lengthOf(layoutRender, 1);\n    assert.deepEqual(layoutRender[0].components[0].data.recommendations[0], {\n      id: \"foo\",\n    });\n    assert.deepEqual(\n      layoutRender[0].components[0].data.recommendations[1],\n      \"fooSpoc\"\n    );\n    assert.deepEqual(layoutRender[0].components[0].data.recommendations[2], {\n      id: \"bar\",\n    });\n\n    assert.deepEqual(spocsFill, [\n      { id: undefined, reason: \"n/a\", displayed: 1, full_recalc: 0 },\n      {\n        id: undefined,\n        reason: \"probability_selection\",\n        displayed: 0,\n        full_recalc: 0,\n      },\n      {\n        id: undefined,\n        reason: \"out_of_position\",\n        displayed: 0,\n        full_recalc: 0,\n      },\n    ]);\n  });\n\n  it(\"should not return spoc result for rolls above the probability\", () => {\n    const fakeSpocConfig = {\n      positions: [{ index: 0 }, { index: 1 }],\n      probability: 0.5,\n    };\n    const fakeLayout = [\n      {\n        width: 3,\n        components: [\n          { type: \"foo\", feed: { url: \"foo.com\" }, spocs: fakeSpocConfig },\n        ],\n      },\n    ];\n    const fakeSpocsData = {\n      lastUpdated: 0,\n      spocs: { spocs: [\"fooSpoc\", \"barSpoc\"] },\n    };\n\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,\n      data: { layout: fakeLayout },\n    });\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_FEED_UPDATE,\n      data: { feed: FAKE_FEEDS[\"foo.com\"], url: \"foo.com\" },\n    });\n    store.dispatch({ type: at.DISCOVERY_STREAM_FEEDS_UPDATE });\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_SPOCS_UPDATE,\n      data: fakeSpocsData,\n    });\n    const randomStub = globals.sandbox.stub(global.Math, \"random\").returns(0.6);\n\n    const { spocsFill, layoutRender } = selectLayoutRender({\n      state: store.getState().DiscoveryStream,\n    });\n\n    assert.calledTwice(randomStub);\n    assert.lengthOf(layoutRender, 1);\n    assert.deepEqual(layoutRender[0].components[0].data.recommendations[0], {\n      id: \"foo\",\n    });\n    assert.deepEqual(layoutRender[0].components[0].data.recommendations[1], {\n      id: \"bar\",\n    });\n\n    assert.deepEqual(spocsFill, [\n      {\n        id: undefined,\n        reason: \"probability_selection\",\n        displayed: 0,\n        full_recalc: 0,\n      },\n      {\n        id: undefined,\n        reason: \"out_of_position\",\n        displayed: 0,\n        full_recalc: 0,\n      },\n    ]);\n  });\n\n  it(\"Subsequent render should return spoc result for cached rolls below the probability\", () => {\n    const fakeSpocConfig = {\n      positions: [{ index: 0 }, { index: 1 }],\n      probability: 0.5,\n    };\n    const fakeLayout = [\n      {\n        width: 3,\n        components: [\n          { type: \"foo\", feed: { url: \"foo.com\" }, spocs: fakeSpocConfig },\n        ],\n      },\n    ];\n    const fakeSpocsData = {\n      lastUpdated: 0,\n      spocs: { spocs: [\"fooSpoc\", \"barSpoc\"] },\n    };\n\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,\n      data: { layout: fakeLayout },\n    });\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_FEED_UPDATE,\n      data: { feed: FAKE_FEEDS[\"foo.com\"], url: \"foo.com\" },\n    });\n    store.dispatch({ type: at.DISCOVERY_STREAM_FEEDS_UPDATE });\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_SPOCS_UPDATE,\n      data: fakeSpocsData,\n    });\n    const randomStub = globals.sandbox.stub(global.Math, \"random\");\n\n    const { spocsFill, layoutRender } = selectLayoutRender({\n      state: store.getState().DiscoveryStream,\n      rollCache: [0.4, 0.3],\n    });\n\n    assert.notCalled(randomStub);\n    assert.lengthOf(layoutRender, 1);\n    assert.deepEqual(\n      layoutRender[0].components[0].data.recommendations[0],\n      \"fooSpoc\"\n    );\n    assert.deepEqual(\n      layoutRender[0].components[0].data.recommendations[1],\n      \"barSpoc\"\n    );\n    assert.deepEqual(layoutRender[0].components[0].data.recommendations[2], {\n      id: \"foo\",\n    });\n    assert.deepEqual(layoutRender[0].components[0].data.recommendations[3], {\n      id: \"bar\",\n    });\n\n    assert.deepEqual(spocsFill, [\n      { id: undefined, reason: \"n/a\", displayed: 1, full_recalc: 0 },\n      { id: undefined, reason: \"n/a\", displayed: 1, full_recalc: 0 },\n    ]);\n  });\n\n  it(\"Subsequent render should not return spoc result for cached rolls above the probability\", () => {\n    const fakeSpocConfig = {\n      positions: [{ index: 0 }, { index: 1 }],\n      probability: 0.5,\n    };\n    const fakeLayout = [\n      {\n        width: 3,\n        components: [\n          { type: \"foo\", feed: { url: \"foo.com\" }, spocs: fakeSpocConfig },\n        ],\n      },\n    ];\n    const fakeSpocsData = {\n      lastUpdated: 0,\n      spocs: { spocs: [\"fooSpoc\", \"barSpoc\"] },\n    };\n\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,\n      data: { layout: fakeLayout },\n    });\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_FEED_UPDATE,\n      data: { feed: FAKE_FEEDS[\"foo.com\"], url: \"foo.com\" },\n    });\n    store.dispatch({ type: at.DISCOVERY_STREAM_FEEDS_UPDATE });\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_SPOCS_UPDATE,\n      data: fakeSpocsData,\n    });\n    const randomStub = globals.sandbox.stub(global.Math, \"random\");\n\n    const { spocsFill, layoutRender } = selectLayoutRender({\n      state: store.getState().DiscoveryStream,\n      rollCache: [0.6, 0.7],\n    });\n\n    assert.notCalled(randomStub);\n    assert.lengthOf(layoutRender, 1);\n    assert.deepEqual(layoutRender[0].components[0].data.recommendations[0], {\n      id: \"foo\",\n    });\n    assert.deepEqual(layoutRender[0].components[0].data.recommendations[1], {\n      id: \"bar\",\n    });\n\n    assert.deepEqual(spocsFill, [\n      {\n        id: undefined,\n        reason: \"probability_selection\",\n        displayed: 0,\n        full_recalc: 0,\n      },\n      {\n        id: undefined,\n        reason: \"out_of_position\",\n        displayed: 0,\n        full_recalc: 0,\n      },\n    ]);\n  });\n\n  it(\"Subsequent render should return spoc result by cached rolls probability\", () => {\n    const fakeSpocConfig = {\n      positions: [{ index: 0 }, { index: 1 }],\n      probability: 0.5,\n    };\n    const fakeLayout = [\n      {\n        width: 3,\n        components: [\n          { type: \"foo\", feed: { url: \"foo.com\" }, spocs: fakeSpocConfig },\n        ],\n      },\n    ];\n    const fakeSpocsData = {\n      lastUpdated: 0,\n      spocs: { spocs: [\"fooSpoc\", \"barSpoc\"] },\n    };\n\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,\n      data: { layout: fakeLayout },\n    });\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_FEED_UPDATE,\n      data: { feed: FAKE_FEEDS[\"foo.com\"], url: \"foo.com\" },\n    });\n    store.dispatch({ type: at.DISCOVERY_STREAM_FEEDS_UPDATE });\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_SPOCS_UPDATE,\n      data: fakeSpocsData,\n    });\n    const randomStub = globals.sandbox.stub(global.Math, \"random\");\n\n    const { spocsFill, layoutRender } = selectLayoutRender({\n      state: store.getState().DiscoveryStream,\n      rollCache: [0.7, 0.2],\n    });\n\n    assert.notCalled(randomStub);\n    assert.lengthOf(layoutRender, 1);\n    assert.deepEqual(layoutRender[0].components[0].data.recommendations[0], {\n      id: \"foo\",\n    });\n    assert.deepEqual(\n      layoutRender[0].components[0].data.recommendations[1],\n      \"fooSpoc\"\n    );\n    assert.deepEqual(layoutRender[0].components[0].data.recommendations[2], {\n      id: \"bar\",\n    });\n\n    assert.deepEqual(spocsFill, [\n      { id: undefined, reason: \"n/a\", displayed: 1, full_recalc: 0 },\n      {\n        id: undefined,\n        reason: \"out_of_position\",\n        displayed: 0,\n        full_recalc: 0,\n      },\n    ]);\n  });\n\n  it(\"should return a layout with feeds of items length with positions\", () => {\n    const fakeLayout = [\n      {\n        width: 3,\n        components: [\n          { type: \"foo\", properties: { items: 3 }, feed: { url: \"foo.com\" } },\n        ],\n      },\n    ];\n    const fakeRecommendations = [\n      { name: \"item1\" },\n      { name: \"item2\" },\n      { name: \"item3\" },\n      { name: \"item4\" },\n    ];\n    const fakeFeeds = {\n      \"foo.com\": { data: { recommendations: fakeRecommendations } },\n    };\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,\n      data: { layout: fakeLayout },\n    });\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_FEED_UPDATE,\n      data: { feed: fakeFeeds[\"foo.com\"], url: \"foo.com\" },\n    });\n    store.dispatch({ type: at.DISCOVERY_STREAM_FEEDS_UPDATE });\n\n    const { spocsFill, layoutRender } = selectLayoutRender({\n      state: store.getState().DiscoveryStream,\n    });\n\n    const { recommendations } = layoutRender[0].components[0].data;\n    assert.equal(recommendations.length, 4);\n    assert.equal(recommendations[0].pos, 0);\n    assert.equal(recommendations[1].pos, 1);\n    assert.equal(recommendations[2].pos, 2);\n    assert.equal(recommendations[3].pos, undefined);\n\n    assert.lengthOf(spocsFill, 0);\n  });\n  it(\"should stop rendering feeds if we hit one that's not ready\", () => {\n    const fakeLayout = [\n      {\n        width: 3,\n        components: [\n          { type: \"foo1\" },\n          { type: \"foo2\", properties: { items: 3 }, feed: { url: \"foo2.com\" } },\n          { type: \"foo3\", properties: { items: 3 }, feed: { url: \"foo3.com\" } },\n          { type: \"foo4\", properties: { items: 3 }, feed: { url: \"foo4.com\" } },\n          { type: \"foo5\" },\n        ],\n      },\n    ];\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,\n      data: { layout: fakeLayout },\n    });\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_FEED_UPDATE,\n      data: { feed: { data: { recommendations: [] } }, url: \"foo2.com\" },\n    });\n\n    const { layoutRender } = selectLayoutRender({\n      state: store.getState().DiscoveryStream,\n    });\n\n    assert.equal(layoutRender[0].components[0].type, \"foo1\");\n    assert.equal(layoutRender[0].components[1].type, \"foo2\");\n    assert.isTrue(\n      layoutRender[0].components[2].data.recommendations[0].placeholder\n    );\n    assert.lengthOf(layoutRender[0].components, 3);\n    assert.isUndefined(layoutRender[0].components[3]);\n  });\n  it(\"should render everything if everything is ready\", () => {\n    const fakeLayout = [\n      {\n        width: 3,\n        components: [\n          { type: \"foo1\" },\n          { type: \"foo2\", properties: { items: 3 }, feed: { url: \"foo2.com\" } },\n          { type: \"foo3\", properties: { items: 3 }, feed: { url: \"foo3.com\" } },\n          { type: \"foo4\", properties: { items: 3 }, feed: { url: \"foo4.com\" } },\n          { type: \"foo5\" },\n        ],\n      },\n    ];\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,\n      data: { layout: fakeLayout },\n    });\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_FEED_UPDATE,\n      data: { feed: { data: { recommendations: [] } }, url: \"foo2.com\" },\n    });\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_FEED_UPDATE,\n      data: { feed: { data: { recommendations: [] } }, url: \"foo3.com\" },\n    });\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_FEED_UPDATE,\n      data: { feed: { data: { recommendations: [] } }, url: \"foo4.com\" },\n    });\n\n    const { layoutRender } = selectLayoutRender({\n      state: store.getState().DiscoveryStream,\n    });\n\n    assert.equal(layoutRender[0].components[0].type, \"foo1\");\n    assert.equal(layoutRender[0].components[1].type, \"foo2\");\n    assert.equal(layoutRender[0].components[2].type, \"foo3\");\n    assert.equal(layoutRender[0].components[3].type, \"foo4\");\n    assert.equal(layoutRender[0].components[4].type, \"foo5\");\n  });\n  it(\"should stop rendering feeds if we hit a not ready spoc\", () => {\n    const fakeLayout = [\n      {\n        width: 3,\n        components: [\n          { type: \"foo1\" },\n          { type: \"foo2\", properties: { items: 3 }, feed: { url: \"foo2.com\" } },\n          {\n            type: \"foo3\",\n            properties: { items: 3 },\n            feed: { url: \"foo3.com\" },\n            spocs: { positions: [{ index: 0 }], probability: 1 },\n          },\n          { type: \"foo4\", properties: { items: 3 }, feed: { url: \"foo4.com\" } },\n          { type: \"foo5\" },\n        ],\n      },\n    ];\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,\n      data: { layout: fakeLayout },\n    });\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_FEED_UPDATE,\n      data: { feed: { data: { recommendations: [] } }, url: \"foo2.com\" },\n    });\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_FEED_UPDATE,\n      data: { feed: { data: { recommendations: [] } }, url: \"foo3.com\" },\n    });\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_FEED_UPDATE,\n      data: { feed: { data: { recommendations: [] } }, url: \"foo4.com\" },\n    });\n\n    const { layoutRender } = selectLayoutRender({\n      state: store.getState().DiscoveryStream,\n    });\n\n    assert.equal(layoutRender[0].components[0].type, \"foo1\");\n    assert.equal(layoutRender[0].components[1].type, \"foo2\");\n    assert.deepEqual(layoutRender[0].components[2].data.recommendations, [\n      { placeholder: true },\n      { placeholder: true },\n      { placeholder: true },\n    ]);\n  });\n  it(\"should not render a spoc if there are no available spocs\", () => {\n    const fakeLayout = [\n      {\n        width: 3,\n        components: [\n          { type: \"foo1\" },\n          { type: \"foo2\", properties: { items: 3 }, feed: { url: \"foo2.com\" } },\n          {\n            type: \"foo3\",\n            properties: { items: 3 },\n            feed: { url: \"foo3.com\" },\n            spocs: { positions: [{ index: 0 }], probability: 1 },\n          },\n          { type: \"foo4\", properties: { items: 3 }, feed: { url: \"foo4.com\" } },\n          { type: \"foo5\" },\n        ],\n      },\n    ];\n    const fakeSpocsData = { lastUpdated: 0, spocs: { spocs: [] } };\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,\n      data: { layout: fakeLayout },\n    });\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_FEED_UPDATE,\n      data: { feed: { data: { recommendations: [] } }, url: \"foo2.com\" },\n    });\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_FEED_UPDATE,\n      data: {\n        feed: { data: { recommendations: [{ name: \"rec\" }] } },\n        url: \"foo3.com\",\n      },\n    });\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_FEED_UPDATE,\n      data: { feed: { data: { recommendations: [] } }, url: \"foo4.com\" },\n    });\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_SPOCS_UPDATE,\n      data: fakeSpocsData,\n    });\n\n    const { layoutRender } = selectLayoutRender({\n      state: store.getState().DiscoveryStream,\n    });\n\n    assert.deepEqual(layoutRender[0].components[2].data.recommendations[0], {\n      name: \"rec\",\n      pos: 0,\n    });\n  });\n  it(\"should not render a row if no components exist after filter in that row\", () => {\n    const fakeLayout = [\n      {\n        width: 3,\n        components: [{ type: \"TopSites\" }],\n      },\n      {\n        width: 3,\n        components: [{ type: \"Message\" }],\n      },\n    ];\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,\n      data: { layout: fakeLayout },\n    });\n\n    const { layoutRender } = selectLayoutRender({\n      state: store.getState().DiscoveryStream,\n      prefs: { \"feeds.topsites\": true },\n    });\n\n    assert.equal(layoutRender[0].components[0].type, \"TopSites\");\n    assert.equal(layoutRender[1], undefined);\n  });\n  it(\"should not render a component if filtered\", () => {\n    const fakeLayout = [\n      {\n        width: 3,\n        components: [{ type: \"Message\" }, { type: \"TopSites\" }],\n      },\n    ];\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,\n      data: { layout: fakeLayout },\n    });\n\n    const { layoutRender } = selectLayoutRender({\n      state: store.getState().DiscoveryStream,\n      prefs: { \"feeds.topsites\": true },\n    });\n\n    assert.equal(layoutRender[0].components[0].type, \"TopSites\");\n    assert.equal(layoutRender[0].components[1], undefined);\n  });\n  it(\"should not render a Navigation if not en-*\", () => {\n    const fakeLayout = [\n      {\n        width: 3,\n        components: [\n          { type: \"Navigation\" },\n          { type: \"Message\" },\n          { type: \"TopSites\" },\n        ],\n      },\n    ];\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,\n      data: { layout: fakeLayout },\n    });\n\n    const { layoutRender } = selectLayoutRender({\n      state: store.getState().DiscoveryStream,\n      prefs: {\n        \"feeds.topsites\": true,\n        \"feeds.section.topstories\": true,\n      },\n    });\n\n    assert.equal(layoutRender[0].components[0].type, \"Message\");\n    assert.equal(layoutRender[0].components[1].type, \"TopSites\");\n    assert.equal(layoutRender[0].components[2], undefined);\n  });\n  it(\"should skip rendering a spoc in position if that spoc is blocked for that session\", () => {\n    const fakeLayout = [\n      {\n        width: 3,\n        components: [\n          {\n            type: \"foo1\",\n            properties: { items: 3 },\n            feed: { url: \"foo1.com\" },\n            spocs: { positions: [{ index: 0 }], probability: 1 },\n          },\n        ],\n      },\n    ];\n    const fakeSpocsData = {\n      lastUpdated: 0,\n      spocs: {\n        spocs: [{ name: \"spoc\", url: \"https://foo.com\" }],\n      },\n    };\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_LAYOUT_UPDATE,\n      data: { layout: fakeLayout },\n    });\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_FEED_UPDATE,\n      data: {\n        feed: { data: { recommendations: [{ name: \"rec\" }] } },\n        url: \"foo1.com\",\n      },\n    });\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_SPOCS_UPDATE,\n      data: fakeSpocsData,\n    });\n\n    const { layoutRender: layout1 } = selectLayoutRender({\n      state: store.getState().DiscoveryStream,\n    });\n\n    store.dispatch({\n      type: at.DISCOVERY_STREAM_SPOC_BLOCKED,\n      data: { url: \"https://foo.com\" },\n    });\n\n    const { layoutRender: layout2 } = selectLayoutRender({\n      state: store.getState().DiscoveryStream,\n    });\n\n    assert.deepEqual(layout1[0].components[0].data.recommendations[0], {\n      name: \"spoc\",\n      url: \"https://foo.com\",\n      pos: 0,\n    });\n    assert.deepEqual(layout2[0].components[0].data.recommendations[0], {\n      name: \"rec\",\n      pos: 0,\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/lib/AboutPreferences.test.js",
    "content": "/* global Services */\nimport {\n  AboutPreferences,\n  PREFERENCES_LOADED_EVENT,\n} from \"lib/AboutPreferences.jsm\";\nimport { actionTypes as at } from \"common/Actions.jsm\";\nimport { GlobalOverrider } from \"test/unit/utils\";\n\ndescribe(\"AboutPreferences Feed\", () => {\n  let globals;\n  let sandbox;\n  let Sections;\n  let DiscoveryStream;\n  let instance;\n\n  beforeEach(() => {\n    globals = new GlobalOverrider();\n    sandbox = globals.sandbox;\n    Sections = [];\n    DiscoveryStream = { config: { enabled: false } };\n    instance = new AboutPreferences();\n    instance.store = {\n      dispatch: sandbox.stub(),\n      getState: () => ({ Sections, DiscoveryStream }),\n    };\n  });\n  afterEach(() => {\n    globals.restore();\n  });\n\n  describe(\"#onAction\", () => {\n    it(\"should call .init() on an INIT action\", () => {\n      const stub = sandbox.stub(instance, \"init\");\n\n      instance.onAction({ type: at.INIT });\n\n      assert.calledOnce(stub);\n    });\n    it(\"should call .uninit() on an UNINIT action\", () => {\n      const stub = sandbox.stub(instance, \"uninit\");\n\n      instance.onAction({ type: at.UNINIT });\n\n      assert.calledOnce(stub);\n    });\n    it(\"should call .openPreferences on SETTINGS_OPEN\", () => {\n      const action = {\n        type: at.SETTINGS_OPEN,\n        _target: { browser: { ownerGlobal: { openPreferences: sinon.spy() } } },\n      };\n      instance.onAction(action);\n      assert.calledOnce(action._target.browser.ownerGlobal.openPreferences);\n    });\n    it(\"should call .BrowserOpenAddonsMgr with the extension id on OPEN_WEBEXT_SETTINGS\", () => {\n      const action = {\n        type: at.OPEN_WEBEXT_SETTINGS,\n        data: \"foo\",\n        _target: {\n          browser: { ownerGlobal: { BrowserOpenAddonsMgr: sinon.spy() } },\n        },\n      };\n      instance.onAction(action);\n      assert.calledWith(\n        action._target.browser.ownerGlobal.BrowserOpenAddonsMgr,\n        \"addons://detail/foo\"\n      );\n    });\n  });\n  describe(\"#observe\", () => {\n    it(\"should watch for about:preferences loading\", () => {\n      sandbox.stub(Services.obs, \"addObserver\");\n\n      instance.init();\n\n      assert.calledOnce(Services.obs.addObserver);\n      assert.calledWith(\n        Services.obs.addObserver,\n        instance,\n        PREFERENCES_LOADED_EVENT\n      );\n    });\n    it(\"should stop watching on uninit\", () => {\n      sandbox.stub(Services.obs, \"removeObserver\");\n\n      instance.uninit();\n\n      assert.calledOnce(Services.obs.removeObserver);\n      assert.calledWith(\n        Services.obs.removeObserver,\n        instance,\n        PREFERENCES_LOADED_EVENT\n      );\n    });\n    it(\"should try to render on event\", async () => {\n      const stub = sandbox.stub(instance, \"renderPreferences\");\n      Sections.push({});\n\n      await instance.observe(window, PREFERENCES_LOADED_EVENT);\n\n      assert.calledOnce(stub);\n      assert.equal(stub.firstCall.args[0], window);\n      assert.include(stub.firstCall.args[1], Sections[0]);\n    });\n    it(\"Hide topstories rows select in sections if discovery stream is enabled\", async () => {\n      const stub = sandbox.stub(instance, \"renderPreferences\");\n\n      Sections.push({\n        rowsPref: \"row_pref\",\n        maxRows: 3,\n        pref: { descString: \"foo\" },\n        learnMore: { link: \"https://foo.com\" },\n        id: \"topstories\",\n      });\n      DiscoveryStream = { config: { enabled: true } };\n\n      await instance.observe(window, PREFERENCES_LOADED_EVENT);\n\n      assert.calledOnce(stub);\n      const [, structure] = stub.firstCall.args;\n      assert.equal(structure[0].id, \"search\");\n      assert.equal(structure[1].id, \"topsites\");\n      assert.equal(structure[2].id, \"topstories\");\n      assert.isEmpty(structure[2].rowsPref);\n    });\n  });\n  describe(\"#renderPreferences\", () => {\n    let node;\n    let prefStructure;\n    let Preferences;\n    let gHomePane;\n    const testRender = () =>\n      instance.renderPreferences(\n        {\n          document: {\n            createXULElement: sandbox.stub().returns(node),\n            l10n: {\n              setAttributes(el, id, args) {\n                el.setAttribute(\"data-l10n-id\", id);\n                el.setAttribute(\"data-l10n-args\", JSON.stringify(args));\n              },\n            },\n            createProcessingInstruction: sandbox.stub(),\n            createElementNS: sandbox.stub().callsFake((NS, el) => node),\n            getElementById: sandbox.stub().returns(node),\n            insertBefore: sandbox.stub().returnsArg(0),\n            querySelector: sandbox\n              .stub()\n              .returns({ appendChild: sandbox.stub() }),\n          },\n          Preferences,\n          gHomePane,\n        },\n        prefStructure,\n        DiscoveryStream.config\n      );\n    beforeEach(() => {\n      node = {\n        appendChild: sandbox.stub().returnsArg(0),\n        addEventListener: sandbox.stub(),\n        classList: { add: sandbox.stub(), remove: sandbox.stub() },\n        cloneNode: sandbox.stub().returnsThis(),\n        insertAdjacentElement: sandbox.stub().returnsArg(1),\n        setAttribute: sandbox.stub(),\n        remove: sandbox.stub(),\n        style: {},\n      };\n      prefStructure = [];\n      Preferences = {\n        add: sandbox.stub(),\n        get: sandbox.stub().returns({\n          on: sandbox.stub(),\n        }),\n      };\n      gHomePane = { toggleRestoreDefaultsBtn: sandbox.stub() };\n    });\n    describe(\"#getString\", () => {\n      it(\"should not fail if titleString is not provided\", () => {\n        prefStructure = [{ pref: {} }];\n\n        testRender();\n        assert.calledWith(\n          node.setAttribute,\n          \"data-l10n-id\",\n          sinon.match.typeOf(\"undefined\")\n        );\n      });\n      it(\"should return the string id if titleString is just a string\", () => {\n        const titleString = \"foo\";\n        prefStructure = [{ pref: { titleString } }];\n\n        testRender();\n        assert.calledWith(node.setAttribute, \"data-l10n-id\", titleString);\n      });\n      it(\"should set id and args if titleString is an object with id and values\", () => {\n        const titleString = { id: \"foo\", values: { provider: \"bar\" } };\n        prefStructure = [{ pref: { titleString } }];\n\n        testRender();\n        assert.calledWith(node.setAttribute, \"data-l10n-id\", titleString.id);\n        assert.calledWith(\n          node.setAttribute,\n          \"data-l10n-args\",\n          JSON.stringify(titleString.values)\n        );\n      });\n    });\n    describe(\"#linkPref\", () => {\n      it(\"should add a pref to the global\", () => {\n        prefStructure = [{ pref: { feed: \"feed\" } }];\n\n        testRender();\n\n        assert.calledOnce(Preferences.add);\n      });\n      it(\"should skip adding if not shown\", () => {\n        prefStructure = [{ shouldHidePref: true }];\n\n        testRender();\n\n        assert.notCalled(Preferences.add);\n      });\n    });\n    describe(\"pref icon\", () => {\n      it(\"should default to webextension icon\", () => {\n        prefStructure = [{ pref: { feed: \"feed\" } }];\n\n        testRender();\n\n        assert.calledWith(\n          node.setAttribute,\n          \"src\",\n          \"resource://activity-stream/data/content/assets/glyph-webextension-16.svg\"\n        );\n      });\n      it(\"should use desired glyph icon\", () => {\n        prefStructure = [{ icon: \"highlights\", pref: { feed: \"feed\" } }];\n\n        testRender();\n\n        assert.calledWith(\n          node.setAttribute,\n          \"src\",\n          \"resource://activity-stream/data/content/assets/glyph-highlights-16.svg\"\n        );\n      });\n      it(\"should use specified chrome icon\", () => {\n        const icon = \"chrome://the/icon.svg\";\n        prefStructure = [{ icon, pref: { feed: \"feed\" } }];\n\n        testRender();\n\n        assert.calledWith(node.setAttribute, \"src\", icon);\n      });\n    });\n    describe(\"title line\", () => {\n      it(\"should render a title\", () => {\n        const titleString = \"the_title\";\n        prefStructure = [{ pref: { titleString } }];\n\n        testRender();\n\n        assert.calledWith(node.setAttribute, \"data-l10n-id\", titleString);\n      });\n      it(\"should add a link for top stories\", () => {\n        const href = \"https://disclaimer/\";\n        prefStructure = [\n          {\n            id: \"topstories\",\n            pref: { feed: \"feed\", learnMore: { link: { href } } },\n          },\n        ];\n\n        testRender();\n        assert.calledWith(node.setAttribute, \"href\", href);\n      });\n    });\n    describe(\"description line\", () => {\n      it(\"should render a description\", () => {\n        const descString = \"the_desc\";\n        prefStructure = [{ pref: { descString } }];\n\n        testRender();\n\n        assert.calledWith(node.setAttribute, \"data-l10n-id\", descString);\n      });\n      it(\"should render rows dropdown with appropriate number\", () => {\n        prefStructure = [\n          { rowsPref: \"row_pref\", maxRows: 3, pref: { descString: \"foo\" } },\n        ];\n\n        testRender();\n\n        assert.calledWith(node.setAttribute, \"value\", 1);\n        assert.calledWith(node.setAttribute, \"value\", 2);\n        assert.calledWith(node.setAttribute, \"value\", 3);\n      });\n    });\n    describe(\"nested prefs\", () => {\n      const titleString = \"im_nested\";\n      beforeEach(() => {\n        prefStructure = [{ pref: { nestedPrefs: [{ titleString }] } }];\n      });\n      it(\"should render a nested pref\", () => {\n        testRender();\n\n        assert.calledWith(node.setAttribute, \"data-l10n-id\", titleString);\n      });\n      it(\"should add a change event\", () => {\n        testRender();\n\n        assert.calledOnce(Preferences.get().on);\n        assert.calledWith(Preferences.get().on, \"change\");\n      });\n      it(\"should default node disabled to false\", async () => {\n        Preferences.get = sandbox.stub().returns({\n          on: sandbox.stub(),\n          _value: true,\n        });\n\n        testRender();\n\n        assert.isFalse(node.disabled);\n      });\n      it(\"should default node disabled to true\", async () => {\n        testRender();\n\n        assert.isTrue(node.disabled);\n      });\n      it(\"should set node disabled to true\", async () => {\n        const pref = {\n          on: sandbox.stub(),\n          _value: true,\n        };\n        Preferences.get = sandbox.stub().returns(pref);\n\n        testRender();\n        pref._value = !pref._value;\n        await Preferences.get().on.firstCall.args[1]();\n\n        assert.isTrue(node.disabled);\n      });\n      it(\"should set node disabled to false\", async () => {\n        const pref = {\n          on: sandbox.stub(),\n          _value: false,\n        };\n        Preferences.get = sandbox.stub().returns(pref);\n\n        testRender();\n        pref._value = !pref._value;\n        await Preferences.get().on.firstCall.args[1]();\n\n        assert.isFalse(node.disabled);\n      });\n    });\n    describe(\"restore defaults btn\", () => {\n      it(\"should call toggleRestoreDefaultsBtn\", () => {\n        testRender();\n\n        assert.calledOnce(gHomePane.toggleRestoreDefaultsBtn);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/lib/ActivityStream.test.js",
    "content": "import { CONTENT_MESSAGE_TYPE } from \"common/Actions.jsm\";\nimport injector from \"inject!lib/ActivityStream.jsm\";\n\ndescribe(\"ActivityStream\", () => {\n  let sandbox;\n  let as;\n  let ActivityStream;\n  let PREFS_CONFIG;\n  function Fake() {}\n  function FakeStore() {\n    return { init: () => {}, uninit: () => {}, feeds: { get: () => {} } };\n  }\n\n  beforeEach(() => {\n    sandbox = sinon.createSandbox();\n    ({ ActivityStream, PREFS_CONFIG } = injector({\n      \"lib/Store.jsm\": { Store: FakeStore },\n      \"lib/AboutPreferences.jsm\": { AboutPreferences: Fake },\n      \"lib/NewTabInit.jsm\": { NewTabInit: Fake },\n      \"lib/PlacesFeed.jsm\": { PlacesFeed: Fake },\n      \"lib/PrefsFeed.jsm\": { PrefsFeed: Fake },\n      \"lib/SectionsManager.jsm\": { SectionsFeed: Fake },\n      \"lib/SystemTickFeed.jsm\": { SystemTickFeed: Fake },\n      \"lib/TelemetryFeed.jsm\": { TelemetryFeed: Fake },\n      \"lib/FaviconFeed.jsm\": { FaviconFeed: Fake },\n      \"lib/TopSitesFeed.jsm\": { TopSitesFeed: Fake },\n      \"lib/TopStoriesFeed.jsm\": { TopStoriesFeed: Fake },\n      \"lib/HighlightsFeed.jsm\": { HighlightsFeed: Fake },\n      \"lib/ASRouterFeed.jsm\": { ASRouterFeed: Fake },\n      \"lib/DiscoveryStreamFeed.jsm\": { DiscoveryStreamFeed: Fake },\n    }));\n    as = new ActivityStream();\n    sandbox.stub(as.store, \"init\");\n    sandbox.stub(as.store, \"uninit\");\n    sandbox.stub(as._defaultPrefs, \"init\");\n  });\n\n  afterEach(() => sandbox.restore());\n\n  it(\"should exist\", () => {\n    assert.ok(ActivityStream);\n  });\n  it(\"should initialize with .initialized=false\", () => {\n    assert.isFalse(as.initialized, \".initialized\");\n  });\n  describe(\"#init\", () => {\n    beforeEach(() => {\n      as.init();\n    });\n    it(\"should initialize default prefs\", () => {\n      assert.calledOnce(as._defaultPrefs.init);\n    });\n    it(\"should set .initialized to true\", () => {\n      assert.isTrue(as.initialized, \".initialized\");\n    });\n    it(\"should call .store.init\", () => {\n      assert.calledOnce(as.store.init);\n    });\n    it(\"should pass to Store an INIT event for content\", () => {\n      as.init();\n\n      const [, action] = as.store.init.firstCall.args;\n      assert.equal(action.meta.to, CONTENT_MESSAGE_TYPE);\n    });\n    it(\"should pass to Store an UNINIT event\", () => {\n      as.init();\n\n      const [, , action] = as.store.init.firstCall.args;\n      assert.equal(action.type, \"UNINIT\");\n    });\n    it(\"should clear old default discoverystream config pref\", () => {\n      sandbox.stub(global.Services.prefs, \"prefHasUserValue\").returns(true);\n      sandbox\n        .stub(global.Services.prefs, \"getStringPref\")\n        .returns(\n          `{\"api_key_pref\":\"extensions.pocket.oAuthConsumerKey\",\"enabled\":false,\"show_spocs\":true,\"layout_endpoint\":\"https://getpocket.cdn.mozilla.net/v3/newtab/layout?version=1&consumer_key=$apiKey&layout_variant=basic\"}`\n        );\n      sandbox.stub(global.Services.prefs, \"clearUserPref\");\n\n      as.init();\n\n      assert.calledWith(\n        global.Services.prefs.clearUserPref,\n        \"browser.newtabpage.activity-stream.discoverystream.config\"\n      );\n    });\n  });\n  describe(\"#uninit\", () => {\n    beforeEach(() => {\n      as.init();\n      as.uninit();\n    });\n    it(\"should set .initialized to false\", () => {\n      assert.isFalse(as.initialized, \".initialized\");\n    });\n    it(\"should call .store.uninit\", () => {\n      assert.calledOnce(as.store.uninit);\n    });\n  });\n  describe(\"feeds\", () => {\n    it(\"should create a NewTabInit feed\", () => {\n      const feed = as.feeds.get(\"feeds.newtabinit\")();\n      assert.instanceOf(feed, Fake);\n    });\n    it(\"should create a Places feed\", () => {\n      const feed = as.feeds.get(\"feeds.places\")();\n      assert.instanceOf(feed, Fake);\n    });\n    it(\"should create a TopSites feed\", () => {\n      const feed = as.feeds.get(\"feeds.topsites\")();\n      assert.instanceOf(feed, Fake);\n    });\n    it(\"should create a Telemetry feed\", () => {\n      const feed = as.feeds.get(\"feeds.telemetry\")();\n      assert.instanceOf(feed, Fake);\n    });\n    it(\"should create a Prefs feed\", () => {\n      const feed = as.feeds.get(\"feeds.prefs\")();\n      assert.instanceOf(feed, Fake);\n    });\n    it(\"should create a section feed for each section in PREFS_CONFIG\", () => {\n      // If new sections are added, their feeds will have to be added to the\n      // list of injected feeds above for this test to pass\n      let feedCount = 0;\n      for (const pref of PREFS_CONFIG.keys()) {\n        if (pref.search(/^feeds\\.section\\.[^.]+$/) === 0) {\n          const feed = as.feeds.get(pref)();\n          assert.instanceOf(feed, Fake);\n          feedCount++;\n        }\n      }\n      assert.isAbove(feedCount, 0);\n    });\n    it(\"should create a AboutPreferences feed\", () => {\n      const feed = as.feeds.get(\"feeds.aboutpreferences\")();\n      assert.instanceOf(feed, Fake);\n    });\n    it(\"should create a SectionsFeed\", () => {\n      const feed = as.feeds.get(\"feeds.sections\")();\n      assert.instanceOf(feed, Fake);\n    });\n    it(\"should create a SystemTick feed\", () => {\n      const feed = as.feeds.get(\"feeds.systemtick\")();\n      assert.instanceOf(feed, Fake);\n    });\n    it(\"should create a Favicon feed\", () => {\n      const feed = as.feeds.get(\"feeds.favicon\")();\n      assert.instanceOf(feed, Fake);\n    });\n    it(\"should create a ASRouter feed\", () => {\n      const feed = as.feeds.get(\"feeds.asrouterfeed\")();\n      assert.instanceOf(feed, Fake);\n    });\n    it(\"should create a DiscoveryStreamFeed feed\", () => {\n      const feed = as.feeds.get(\"feeds.discoverystreamfeed\")();\n      assert.instanceOf(feed, Fake);\n    });\n  });\n  describe(\"_migratePref\", () => {\n    it(\"should migrate a pref if the user has set a custom value\", () => {\n      sandbox.stub(global.Services.prefs, \"prefHasUserValue\").returns(true);\n      sandbox.stub(global.Services.prefs, \"getPrefType\").returns(\"integer\");\n      sandbox.stub(global.Services.prefs, \"getIntPref\").returns(10);\n      as._migratePref(\"oldPrefName\", result => assert.equal(10, result));\n    });\n    it(\"should not migrate a pref if the user has not set a custom value\", () => {\n      // we bailed out early so we don't check the pref type later\n      sandbox.stub(global.Services.prefs, \"prefHasUserValue\").returns(false);\n      sandbox.stub(global.Services.prefs, \"getPrefType\");\n      as._migratePref(\"oldPrefName\");\n      assert.notCalled(global.Services.prefs.getPrefType);\n    });\n    it(\"should use the proper pref getter for each type\", () => {\n      sandbox.stub(global.Services.prefs, \"prefHasUserValue\").returns(true);\n\n      // Integer\n      sandbox.stub(global.Services.prefs, \"getIntPref\");\n      sandbox.stub(global.Services.prefs, \"getPrefType\").returns(\"integer\");\n      as._migratePref(\"oldPrefName\", () => {});\n      assert.calledWith(global.Services.prefs.getIntPref, \"oldPrefName\");\n\n      // Boolean\n      sandbox.stub(global.Services.prefs, \"getBoolPref\");\n      global.Services.prefs.getPrefType.returns(\"boolean\");\n      as._migratePref(\"oldPrefName\", () => {});\n      assert.calledWith(global.Services.prefs.getBoolPref, \"oldPrefName\");\n\n      // String\n      sandbox.stub(global.Services.prefs, \"getStringPref\");\n      global.Services.prefs.getPrefType.returns(\"string\");\n      as._migratePref(\"oldPrefName\", () => {});\n      assert.calledWith(global.Services.prefs.getStringPref, \"oldPrefName\");\n    });\n    it(\"should clear the old pref after setting the new one\", () => {\n      sandbox.stub(global.Services.prefs, \"prefHasUserValue\").returns(true);\n      sandbox.stub(global.Services.prefs, \"clearUserPref\");\n      sandbox.stub(global.Services.prefs, \"getPrefType\").returns(\"integer\");\n      as._migratePref(\"oldPrefName\", () => {});\n      assert.calledWith(global.Services.prefs.clearUserPref, \"oldPrefName\");\n    });\n  });\n  describe(\"_updateDynamicPrefs Discovery Stream\", () => {\n    it(\"should be true with expected en-US geo and locale\", () => {\n      sandbox.stub(global.Services.prefs, \"prefHasUserValue\").returns(true);\n      sandbox.stub(global.Services.prefs, \"getStringPref\").returns(\"US\");\n      sandbox\n        .stub(global.Services.locale, \"appLocaleAsLangTag\")\n        .get(() => \"en-US\");\n\n      as._updateDynamicPrefs();\n\n      assert.isTrue(\n        JSON.parse(PREFS_CONFIG.get(\"discoverystream.config\").value).enabled\n      );\n    });\n    it(\"should be true with expected en-CA geo and locale\", () => {\n      sandbox.stub(global.Services.prefs, \"prefHasUserValue\").returns(true);\n      sandbox.stub(global.Services.prefs, \"getStringPref\").returns(\"CA\");\n      sandbox\n        .stub(global.Services.locale, \"appLocaleAsLangTag\")\n        .get(() => \"en-CA\");\n\n      as._updateDynamicPrefs();\n\n      assert.isTrue(\n        JSON.parse(PREFS_CONFIG.get(\"discoverystream.config\").value).enabled\n      );\n    });\n    it(\"should be true with expected de geo and locale\", () => {\n      sandbox.stub(global.Services.prefs, \"prefHasUserValue\").returns(true);\n      sandbox.stub(global.Services.prefs, \"getStringPref\").returns(\"DE\");\n      sandbox\n        .stub(global.Services.locale, \"appLocaleAsLangTag\")\n        .get(() => \"de-DE\");\n\n      as._updateDynamicPrefs();\n\n      assert.isTrue(\n        JSON.parse(PREFS_CONFIG.get(\"discoverystream.config\").value).enabled\n      );\n    });\n    it(\"should be false with no geo and locale\", () => {\n      sandbox.stub(global.Services.prefs, \"prefHasUserValue\").returns(true);\n      sandbox.stub(global.Services.prefs, \"getStringPref\").returns(\"NOGEO\");\n\n      as._updateDynamicPrefs();\n\n      assert.isFalse(\n        JSON.parse(PREFS_CONFIG.get(\"discoverystream.config\").value).enabled\n      );\n    });\n    it(\"should be false with weird geo and locale combination\", () => {\n      sandbox.stub(global.Services.prefs, \"prefHasUserValue\").returns(true);\n      sandbox.stub(global.Services.prefs, \"getStringPref\").returns(\"DE\");\n      sandbox\n        .stub(global.Services.locale, \"appLocaleAsLangTag\")\n        .get(() => \"en-US\");\n\n      as._updateDynamicPrefs();\n\n      assert.isFalse(\n        JSON.parse(PREFS_CONFIG.get(\"discoverystream.config\").value).enabled\n      );\n    });\n  });\n  describe(\"_updateDynamicPrefs topstories default value\", () => {\n    it(\"should be false with no geo/locale\", () => {\n      as._updateDynamicPrefs();\n\n      assert.isFalse(PREFS_CONFIG.get(\"feeds.section.topstories\").value);\n    });\n    it(\"should be false with unexpected geo\", () => {\n      sandbox.stub(global.Services.prefs, \"prefHasUserValue\").returns(true);\n      sandbox.stub(global.Services.prefs, \"getStringPref\").returns(\"NOGEO\");\n\n      as._updateDynamicPrefs();\n\n      assert.isFalse(PREFS_CONFIG.get(\"feeds.section.topstories\").value);\n    });\n    it(\"should be false with expected geo and unexpected locale\", () => {\n      sandbox.stub(global.Services.prefs, \"prefHasUserValue\").returns(true);\n      sandbox.stub(global.Services.prefs, \"getStringPref\").returns(\"US\");\n      sandbox\n        .stub(global.Services.locale, \"appLocaleAsLangTag\")\n        .get(() => \"no-LOCALE\");\n\n      as._updateDynamicPrefs();\n\n      assert.isFalse(PREFS_CONFIG.get(\"feeds.section.topstories\").value);\n    });\n    it(\"should be true with expected geo and locale\", () => {\n      sandbox.stub(global.Services.prefs, \"prefHasUserValue\").returns(true);\n      sandbox.stub(global.Services.prefs, \"getStringPref\").returns(\"US\");\n      sandbox\n        .stub(global.Services.locale, \"appLocaleAsLangTag\")\n        .get(() => \"en-US\");\n\n      as._updateDynamicPrefs();\n\n      assert.isTrue(PREFS_CONFIG.get(\"feeds.section.topstories\").value);\n    });\n    it(\"should be false after expected geo and locale then unexpected\", () => {\n      sandbox.stub(global.Services.prefs, \"prefHasUserValue\").returns(true);\n      sandbox\n        .stub(global.Services.prefs, \"getStringPref\")\n        .onFirstCall()\n        .returns(\"US\")\n        .onSecondCall()\n        .returns(\"NOGEO\");\n      sandbox\n        .stub(global.Services.locale, \"appLocaleAsLangTag\")\n        .get(() => \"en-US\");\n\n      as._updateDynamicPrefs();\n      as._updateDynamicPrefs();\n\n      assert.isFalse(PREFS_CONFIG.get(\"feeds.section.topstories\").value);\n    });\n  });\n  describe(\"_updateDynamicPrefs topstories delayed default value\", () => {\n    let clock;\n    beforeEach(() => {\n      clock = sinon.useFakeTimers();\n\n      // Have addObserver cause prefHasUserValue to now return true then observe\n      sandbox\n        .stub(global.Services.prefs, \"addObserver\")\n        .callsFake((pref, obs) => {\n          sandbox.stub(global.Services.prefs, \"prefHasUserValue\").returns(true);\n          setTimeout(() => obs.observe(null, \"nsPref:changed\", pref)); // eslint-disable-line max-nested-callbacks\n        });\n    });\n    afterEach(() => clock.restore());\n\n    it(\"should set false with unexpected geo\", () => {\n      sandbox.stub(global.Services.prefs, \"getStringPref\").returns(\"NOGEO\");\n\n      as._updateDynamicPrefs();\n      clock.tick(1);\n\n      assert.isFalse(PREFS_CONFIG.get(\"feeds.section.topstories\").value);\n    });\n    it(\"should set true with expected geo and locale\", () => {\n      sandbox.stub(global.Services.prefs, \"getStringPref\").returns(\"US\");\n      sandbox\n        .stub(global.Services.locale, \"appLocaleAsLangTag\")\n        .get(() => \"en-US\");\n\n      as._updateDynamicPrefs();\n      clock.tick(1);\n\n      assert.isTrue(PREFS_CONFIG.get(\"feeds.section.topstories\").value);\n    });\n    it(\"should not change default even with expected geo and locale\", () => {\n      as._defaultPrefs.set(\"feeds.section.topstories\", false);\n      sandbox.stub(global.Services.prefs, \"getStringPref\").returns(\"US\");\n      sandbox\n        .stub(global.Services.locale, \"appLocaleAsLangTag\")\n        .get(() => \"en-US\");\n\n      as._updateDynamicPrefs();\n      clock.tick(1);\n\n      assert.isFalse(PREFS_CONFIG.get(\"feeds.section.topstories\").value);\n    });\n  });\n  describe(\"telemetry reporting on init failure\", () => {\n    it(\"should send a ping on init error\", () => {\n      as = new ActivityStream();\n      const telemetry = { handleUndesiredEvent: sandbox.spy() };\n      sandbox.stub(as.store, \"init\").throws();\n      sandbox.stub(as.store.feeds, \"get\").returns(telemetry);\n      try {\n        as.init();\n      } catch (e) {}\n      assert.calledOnce(telemetry.handleUndesiredEvent);\n    });\n  });\n\n  describe(\"searchs shortcuts shouldPin pref\", () => {\n    const SEARCH_SHORTCUTS_SEARCH_ENGINES_PREF =\n      \"improvesearch.topSiteSearchShortcuts.searchEngines\";\n    let stub;\n\n    beforeEach(() => {\n      sandbox.stub(global.Services.prefs, \"prefHasUserValue\").returns(true);\n      stub = sandbox.stub(global.Services.prefs, \"getStringPref\");\n    });\n\n    it(\"should be an empty string when no geo is available\", () => {\n      as._updateDynamicPrefs();\n      assert.equal(\n        PREFS_CONFIG.get(SEARCH_SHORTCUTS_SEARCH_ENGINES_PREF).value,\n        \"\"\n      );\n    });\n\n    it(\"should be 'baidu' in China\", () => {\n      stub.returns(\"CN\");\n      as._updateDynamicPrefs();\n      assert.equal(\n        PREFS_CONFIG.get(SEARCH_SHORTCUTS_SEARCH_ENGINES_PREF).value,\n        \"baidu\"\n      );\n    });\n\n    it(\"should be 'yandex' in Russia, Belarus, Kazakhstan, and Turkey\", () => {\n      const geos = [\"BY\", \"KZ\", \"RU\", \"TR\"];\n      for (const geo of geos) {\n        stub.returns(geo);\n        as._updateDynamicPrefs();\n        assert.equal(\n          PREFS_CONFIG.get(SEARCH_SHORTCUTS_SEARCH_ENGINES_PREF).value,\n          \"yandex\"\n        );\n      }\n    });\n\n    it(\"should be 'google,amazon' in Germany, France, the UK, Japan, Italy, and the US\", () => {\n      const geos = [\"DE\", \"FR\", \"GB\", \"IT\", \"JP\", \"US\"];\n      for (const geo of geos) {\n        stub.returns(geo);\n        as._updateDynamicPrefs();\n        assert.equal(\n          PREFS_CONFIG.get(SEARCH_SHORTCUTS_SEARCH_ENGINES_PREF).value,\n          \"google,amazon\"\n        );\n      }\n    });\n\n    it(\"should be 'google' elsewhere\", () => {\n      // A selection of other geos\n      const geos = [\"BR\", \"CA\", \"ES\", \"ID\", \"IN\"];\n      for (const geo of geos) {\n        stub.returns(geo);\n        as._updateDynamicPrefs();\n        assert.equal(\n          PREFS_CONFIG.get(SEARCH_SHORTCUTS_SEARCH_ENGINES_PREF).value,\n          \"google\"\n        );\n      }\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/lib/ActivityStreamMessageChannel.test.js",
    "content": "import { actionCreators as ac, actionTypes as at } from \"common/Actions.jsm\";\nimport {\n  ActivityStreamMessageChannel,\n  DEFAULT_OPTIONS,\n} from \"lib/ActivityStreamMessageChannel.jsm\";\nimport { addNumberReducer, GlobalOverrider } from \"test/unit/utils\";\nimport { applyMiddleware, createStore } from \"redux\";\n\nconst OPTIONS = [\n  \"pageURL, outgoingMessageName\",\n  \"incomingMessageName\",\n  \"dispatch\",\n];\n\ndescribe(\"ActivityStreamMessageChannel\", () => {\n  let globals;\n  let dispatch;\n  let mm;\n  let RPmessagePorts;\n  beforeEach(() => {\n    RPmessagePorts = [];\n    function RP(url, isFromAboutNewTab = false) {\n      this.url = url;\n      this.messagePorts = RPmessagePorts;\n      this.addMessageListener = globals.sandbox.spy();\n      this.removeMessageListener = globals.sandbox.spy();\n      this.sendAsyncMessage = globals.sandbox.spy();\n      this.destroy = globals.sandbox.spy();\n      this.isFromAboutNewTab = isFromAboutNewTab;\n    }\n    globals = new GlobalOverrider();\n    const override = globals.sandbox.stub();\n    override.withArgs(true).returns(new RP(\"about:newtab\", true));\n    override.withArgs(false).returns(null);\n    globals.set(\"AboutNewTab\", {\n      override,\n      reset: globals.sandbox.spy(),\n    });\n    globals.set(\"RemotePages\", RP);\n    dispatch = globals.sandbox.spy();\n    mm = new ActivityStreamMessageChannel({ dispatch });\n  });\n\n  afterEach(() => globals.restore());\n\n  describe(\"portID validation\", () => {\n    let sandbox;\n    beforeEach(() => {\n      sandbox = sinon.createSandbox();\n      sandbox.spy(global.Cu, \"reportError\");\n    });\n    afterEach(() => {\n      sandbox.restore();\n    });\n    it(\"should log errors for an invalid portID\", () => {\n      mm.validatePortID({});\n      mm.validatePortID({});\n      mm.validatePortID({});\n\n      assert.equal(global.Cu.reportError.callCount, 3);\n    });\n  });\n\n  it(\"should exist\", () => {\n    assert.ok(ActivityStreamMessageChannel);\n  });\n  it(\"should apply default options\", () => {\n    mm = new ActivityStreamMessageChannel();\n    OPTIONS.forEach(o => assert.equal(mm[o], DEFAULT_OPTIONS[o], o));\n  });\n  it(\"should add options\", () => {\n    const options = {\n      dispatch: () => {},\n      pageURL: \"FOO.html\",\n      outgoingMessageName: \"OUT\",\n      incomingMessageName: \"IN\",\n    };\n    mm = new ActivityStreamMessageChannel(options);\n    OPTIONS.forEach(o => assert.equal(mm[o], options[o], o));\n  });\n  it(\"should throw an error if no dispatcher was provided\", () => {\n    mm = new ActivityStreamMessageChannel();\n    assert.throws(() => mm.dispatch({ type: \"FOO\" }));\n  });\n  describe(\"Creating/destroying the channel\", () => {\n    describe(\"#createChannel\", () => {\n      it(\"should create .channel with the correct URL\", () => {\n        mm.createChannel();\n        assert.ok(mm.channel);\n        assert.equal(mm.channel.url, mm.pageURL);\n      });\n      it(\"should add 4 message listeners\", () => {\n        mm.createChannel();\n        assert.callCount(mm.channel.addMessageListener, 4);\n      });\n      it(\"should add the custom message listener to the channel\", () => {\n        mm.createChannel();\n        assert.calledWith(\n          mm.channel.addMessageListener,\n          mm.incomingMessageName,\n          mm.onMessage\n        );\n      });\n      it(\"should override AboutNewTab\", () => {\n        mm.createChannel();\n        assert.calledOnce(global.AboutNewTab.override);\n      });\n      it(\"should use the channel passed by AboutNewTab on override\", () => {\n        mm.createChannel();\n        assert.ok(mm.channel.isFromAboutNewTab);\n      });\n      it(\"should not override AboutNewTab if the pageURL is not about:newtab\", () => {\n        mm = new ActivityStreamMessageChannel({ pageURL: \"foo.html\" });\n        mm.createChannel();\n        assert.notCalled(global.AboutNewTab.override);\n      });\n    });\n    describe(\"#simulateMessagesForExistingTabs\", () => {\n      beforeEach(() => {\n        sinon.stub(mm, \"onActionFromContent\");\n        mm.createChannel();\n      });\n      it(\"should simulate init for existing ports\", () => {\n        RPmessagePorts.push({\n          url: \"about:monkeys\",\n          loaded: false,\n          portID: \"inited\",\n          simulated: true,\n          browser: { getAttribute: () => \"preloaded\" },\n        });\n        RPmessagePorts.push({\n          url: \"about:sheep\",\n          loaded: true,\n          portID: \"loaded\",\n          simulated: true,\n          browser: { getAttribute: () => \"preloaded\" },\n        });\n\n        mm.simulateMessagesForExistingTabs();\n\n        assert.calledWith(mm.onActionFromContent.firstCall, {\n          type: at.NEW_TAB_INIT,\n          data: RPmessagePorts[0],\n        });\n        assert.calledWith(mm.onActionFromContent.secondCall, {\n          type: at.NEW_TAB_INIT,\n          data: RPmessagePorts[1],\n        });\n      });\n      it(\"should simulate load for loaded ports\", () => {\n        RPmessagePorts.push({\n          loaded: true,\n          portID: \"foo\",\n          browser: { getAttribute: () => \"preloaded\" },\n        });\n\n        mm.simulateMessagesForExistingTabs();\n\n        assert.calledWith(\n          mm.onActionFromContent,\n          { type: at.NEW_TAB_LOAD },\n          \"foo\"\n        );\n      });\n      it(\"should set renderLayers on preloaded browsers after load\", () => {\n        RPmessagePorts.push({\n          loaded: true,\n          portID: \"foo\",\n          browser: { getAttribute: () => \"preloaded\" },\n        });\n        mm.simulateMessagesForExistingTabs();\n        assert.equal(RPmessagePorts[0].browser.renderLayers, true);\n      });\n    });\n    describe(\"#destroyChannel\", () => {\n      let channel;\n      beforeEach(() => {\n        mm.createChannel();\n        channel = mm.channel;\n      });\n      it(\"should set .channel to null\", () => {\n        mm.destroyChannel();\n        assert.isNull(mm.channel);\n      });\n      it(\"should reset AboutNewTab, and pass back its channel\", () => {\n        mm.destroyChannel();\n        assert.calledOnce(global.AboutNewTab.reset);\n        assert.calledWith(global.AboutNewTab.reset, channel);\n      });\n      it(\"should not reset AboutNewTab if the pageURL is not about:newtab\", () => {\n        mm = new ActivityStreamMessageChannel({ pageURL: \"foo.html\" });\n        mm.createChannel();\n        mm.destroyChannel();\n        assert.notCalled(global.AboutNewTab.reset);\n      });\n      it(\"should call channel.destroy() if pageURL is not about:newtab\", () => {\n        mm = new ActivityStreamMessageChannel({ pageURL: \"foo.html\" });\n        mm.createChannel();\n        channel = mm.channel;\n        mm.destroyChannel();\n        assert.calledOnce(channel.destroy);\n      });\n    });\n  });\n  describe(\"Message handling\", () => {\n    describe(\"#getTargetById\", () => {\n      it(\"should get an id if it exists\", () => {\n        const t = { portID: \"foo:1\" };\n        mm.createChannel();\n        mm.channel.messagePorts.push(t);\n        assert.equal(mm.getTargetById(\"foo:1\"), t);\n      });\n      it(\"should return null if the target doesn't exist\", () => {\n        const t = { portID: \"foo:2\" };\n        mm.createChannel();\n        mm.channel.messagePorts.push(t);\n        assert.equal(mm.getTargetById(\"bar:3\"), null);\n      });\n    });\n    describe(\"#getPreloadedBrowser\", () => {\n      it(\"should get a preloaded browser if it exists\", () => {\n        const port = {\n          browser: {\n            getAttribute() {\n              return \"preloaded\";\n            },\n          },\n        };\n        mm.createChannel();\n        mm.channel.messagePorts.push(port);\n        assert.equal(mm.getPreloadedBrowser()[0], port);\n      });\n      it(\"should get all the preloaded browsers across windows if they exist\", () => {\n        const port = {\n          browser: {\n            getAttribute() {\n              return \"preloaded\";\n            },\n          },\n        };\n        mm.createChannel();\n        mm.channel.messagePorts.push(port);\n        mm.channel.messagePorts.push(port);\n        assert.equal(mm.getPreloadedBrowser().length, 2);\n      });\n      it(\"should return null if there is no preloaded browser\", () => {\n        const port = {\n          browser: {\n            getAttribute() {\n              return \"consumed\";\n            },\n          },\n        };\n        mm.createChannel();\n        mm.channel.messagePorts.push(port);\n        assert.equal(mm.getPreloadedBrowser(), null);\n      });\n    });\n    describe(\"#onNewTabInit\", () => {\n      it(\"should dispatch a NEW_TAB_INIT action\", () => {\n        const t = { portID: \"foo\", url: \"about:monkeys\" };\n        sinon.stub(mm, \"onActionFromContent\");\n\n        mm.onNewTabInit({ target: t });\n\n        assert.calledWith(mm.onActionFromContent, {\n          type: at.NEW_TAB_INIT,\n          data: t,\n        });\n      });\n    });\n    describe(\"#onNewTabLoad\", () => {\n      it(\"should dispatch a NEW_TAB_LOAD action\", () => {\n        const t = {\n          portID: \"foo\",\n          browser: { getAttribute: () => \"preloaded\" },\n        };\n        sinon.stub(mm, \"onActionFromContent\");\n        mm.onNewTabLoad({ target: t });\n        assert.calledWith(\n          mm.onActionFromContent,\n          { type: at.NEW_TAB_LOAD },\n          \"foo\"\n        );\n      });\n    });\n    describe(\"#onNewTabUnload\", () => {\n      it(\"should dispatch a NEW_TAB_UNLOAD action\", () => {\n        const t = { portID: \"foo\" };\n        sinon.stub(mm, \"onActionFromContent\");\n        mm.onNewTabUnload({ target: t });\n        assert.calledWith(\n          mm.onActionFromContent,\n          { type: at.NEW_TAB_UNLOAD },\n          \"foo\"\n        );\n      });\n    });\n    describe(\"#onMessage\", () => {\n      let sandbox;\n      beforeEach(() => {\n        sandbox = sinon.createSandbox();\n        sandbox.spy(global.Cu, \"reportError\");\n      });\n      afterEach(() => sandbox.restore());\n      it(\"should report an error if the msg.data is missing\", () => {\n        mm.onMessage({ target: { portID: \"foo\" } });\n        assert.calledOnce(global.Cu.reportError);\n      });\n      it(\"should report an error if the msg.data.type is missing\", () => {\n        mm.onMessage({ target: { portID: \"foo\" }, data: \"foo\" });\n        assert.calledOnce(global.Cu.reportError);\n      });\n      it(\"should call onActionFromContent\", () => {\n        sinon.stub(mm, \"onActionFromContent\");\n        const action = {\n          data: { data: {}, type: \"FOO\" },\n          target: { portID: \"foo\" },\n        };\n        const expectedAction = {\n          type: action.data.type,\n          data: action.data.data,\n          _target: { portID: \"foo\" },\n        };\n        mm.onMessage(action);\n        assert.calledWith(mm.onActionFromContent, expectedAction, \"foo\");\n      });\n    });\n  });\n  describe(\"Sending and broadcasting\", () => {\n    describe(\"#send\", () => {\n      it(\"should send a message on the right port\", () => {\n        const t = { portID: \"foo:3\", sendAsyncMessage: sinon.spy() };\n        mm.createChannel();\n        mm.channel.messagePorts = [t];\n        const action = ac.AlsoToOneContent({ type: \"HELLO\" }, \"foo:3\");\n        mm.send(action);\n        assert.calledWith(\n          t.sendAsyncMessage,\n          DEFAULT_OPTIONS.outgoingMessageName,\n          action\n        );\n      });\n      it(\"should not throw if the target isn't around\", () => {\n        mm.createChannel();\n        // port is not added to the channel\n        const action = ac.AlsoToOneContent({ type: \"HELLO\" }, \"foo:4\");\n\n        assert.doesNotThrow(() => mm.send(action));\n      });\n    });\n    describe(\"#broadcast\", () => {\n      it(\"should send a message on the channel\", () => {\n        mm.createChannel();\n        const action = ac.BroadcastToContent({ type: \"HELLO\" });\n        mm.broadcast(action);\n        assert.calledWith(\n          mm.channel.sendAsyncMessage,\n          DEFAULT_OPTIONS.outgoingMessageName,\n          action\n        );\n      });\n    });\n    describe(\"#preloaded browser\", () => {\n      it(\"should send the message to the preloaded browser if there's data and a preloaded browser exists\", () => {\n        const port = {\n          browser: {\n            getAttribute() {\n              return \"preloaded\";\n            },\n          },\n          sendAsyncMessage: sinon.spy(),\n        };\n        mm.createChannel();\n        mm.channel.messagePorts.push(port);\n        const action = ac.AlsoToPreloaded({ type: \"HELLO\", data: 10 });\n        mm.sendToPreloaded(action);\n        assert.calledWith(\n          port.sendAsyncMessage,\n          DEFAULT_OPTIONS.outgoingMessageName,\n          action\n        );\n      });\n      it(\"should send the message to all the preloaded browsers if there's data and they exist\", () => {\n        const port = {\n          browser: {\n            getAttribute() {\n              return \"preloaded\";\n            },\n          },\n          sendAsyncMessage: sinon.spy(),\n        };\n        mm.createChannel();\n        mm.channel.messagePorts.push(port);\n        mm.channel.messagePorts.push(port);\n        mm.sendToPreloaded(ac.AlsoToPreloaded({ type: \"HELLO\", data: 10 }));\n        assert.calledTwice(port.sendAsyncMessage);\n      });\n      it(\"should not send the message to the preloaded browser if there's no data and a preloaded browser does not exists\", () => {\n        const port = {\n          browser: {\n            getAttribute() {\n              return \"consumed\";\n            },\n          },\n          sendAsyncMessage: sinon.spy(),\n        };\n        mm.createChannel();\n        mm.channel.messagePorts.push(port);\n        const action = ac.AlsoToPreloaded({ type: \"HELLO\" });\n        mm.sendToPreloaded(action);\n        assert.notCalled(port.sendAsyncMessage);\n      });\n    });\n  });\n  describe(\"Handling actions\", () => {\n    describe(\"#onActionFromContent\", () => {\n      beforeEach(() => mm.onActionFromContent({ type: \"FOO\" }, \"foo:5\"));\n      it(\"should dispatch a AlsoToMain action\", () => {\n        assert.calledOnce(dispatch);\n        const [action] = dispatch.firstCall.args;\n        assert.equal(action.type, \"FOO\", \"action.type\");\n      });\n      it(\"should have the right fromTarget\", () => {\n        const [action] = dispatch.firstCall.args;\n        assert.equal(action.meta.fromTarget, \"foo:5\", \"meta.fromTarget\");\n      });\n    });\n    describe(\"#middleware\", () => {\n      let store;\n      beforeEach(() => {\n        store = createStore(addNumberReducer, applyMiddleware(mm.middleware));\n      });\n      it(\"should just call next if no channel is found\", () => {\n        store.dispatch({ type: \"ADD\", data: 10 });\n        assert.equal(store.getState(), 10);\n      });\n      it(\"should call .send but not affect the main store if an OnlyToOneContent action is dispatched\", () => {\n        sinon.stub(mm, \"send\");\n        const action = ac.OnlyToOneContent({ type: \"ADD\", data: 10 }, \"foo\");\n        mm.createChannel();\n\n        store.dispatch(action);\n\n        assert.calledWith(mm.send, action);\n        assert.equal(store.getState(), 0);\n      });\n      it(\"should call .send and update the main store if an AlsoToOneContent action is dispatched\", () => {\n        sinon.stub(mm, \"send\");\n        const action = ac.AlsoToOneContent({ type: \"ADD\", data: 10 }, \"foo\");\n        mm.createChannel();\n\n        store.dispatch(action);\n\n        assert.calledWith(mm.send, action);\n        assert.equal(store.getState(), 10);\n      });\n      it(\"should call .broadcast if the action is BroadcastToContent\", () => {\n        sinon.stub(mm, \"broadcast\");\n        const action = ac.BroadcastToContent({ type: \"FOO\" });\n\n        mm.createChannel();\n        store.dispatch(action);\n\n        assert.calledWith(mm.broadcast, action);\n      });\n      it(\"should call .sendToPreloaded if the action is AlsoToPreloaded\", () => {\n        sinon.stub(mm, \"sendToPreloaded\");\n        const action = ac.AlsoToPreloaded({ type: \"FOO\" });\n\n        mm.createChannel();\n        store.dispatch(action);\n\n        assert.calledWith(mm.sendToPreloaded, action);\n      });\n      it(\"should dispatch other actions normally\", () => {\n        sinon.stub(mm, \"send\");\n        sinon.stub(mm, \"broadcast\");\n        sinon.stub(mm, \"sendToPreloaded\");\n\n        mm.createChannel();\n        store.dispatch({ type: \"ADD\", data: 1 });\n\n        assert.equal(store.getState(), 1);\n        assert.notCalled(mm.send);\n        assert.notCalled(mm.broadcast);\n        assert.notCalled(mm.sendToPreloaded);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/lib/ActivityStreamPrefs.test.js",
    "content": "import { DefaultPrefs, Prefs } from \"lib/ActivityStreamPrefs.jsm\";\n\nconst TEST_PREF_CONFIG = new Map([\n  [\"foo\", { value: true }],\n  [\"bar\", { value: \"BAR\" }],\n  [\"baz\", { value: 1 }],\n  [\"qux\", { value: \"foo\", value_local_dev: \"foofoo\" }],\n]);\n\ndescribe(\"ActivityStreamPrefs\", () => {\n  describe(\"Prefs\", () => {\n    let p;\n    beforeEach(() => {\n      p = new Prefs();\n    });\n    it(\"should have get, set, and observe methods\", () => {\n      assert.property(p, \"get\");\n      assert.property(p, \"set\");\n      assert.property(p, \"observe\");\n    });\n    describe(\"#observeBranch\", () => {\n      let listener;\n      beforeEach(() => {\n        p._prefBranch = { addObserver: sinon.stub() };\n        listener = { onPrefChanged: sinon.stub() };\n        p.observeBranch(listener);\n      });\n      it(\"should add an observer\", () => {\n        assert.calledOnce(p._prefBranch.addObserver);\n        assert.calledWith(p._prefBranch.addObserver, \"\");\n      });\n      it(\"should store the listener\", () => {\n        assert.equal(p._branchObservers.size, 1);\n        assert.ok(p._branchObservers.has(listener));\n      });\n      it(\"should call listener's onPrefChanged\", () => {\n        p._branchObservers.get(listener)();\n\n        assert.calledOnce(listener.onPrefChanged);\n      });\n    });\n    describe(\"#ignoreBranch\", () => {\n      let listener;\n      beforeEach(() => {\n        p._prefBranch = {\n          addObserver: sinon.stub(),\n          removeObserver: sinon.stub(),\n        };\n        listener = {};\n        p.observeBranch(listener);\n      });\n      it(\"should remove the observer\", () => {\n        p.ignoreBranch(listener);\n\n        assert.calledOnce(p._prefBranch.removeObserver);\n        assert.calledWith(\n          p._prefBranch.removeObserver,\n          p._prefBranch.addObserver.firstCall.args[0]\n        );\n      });\n      it(\"should remove the listener\", () => {\n        assert.equal(p._branchObservers.size, 1);\n\n        p.ignoreBranch(listener);\n\n        assert.equal(p._branchObservers.size, 0);\n      });\n    });\n  });\n\n  describe(\"DefaultPrefs\", () => {\n    describe(\"#init\", () => {\n      let defaultPrefs;\n      let sandbox;\n      beforeEach(() => {\n        sandbox = sinon.createSandbox();\n        defaultPrefs = new DefaultPrefs(TEST_PREF_CONFIG);\n        sinon.stub(defaultPrefs, \"set\");\n      });\n      afterEach(() => {\n        sandbox.restore();\n      });\n      it(\"should initialize a boolean pref\", () => {\n        defaultPrefs.init();\n        assert.calledWith(defaultPrefs.set, \"foo\", true);\n      });\n      it(\"should not initialize a pref if a default exists\", () => {\n        defaultPrefs.prefs.foo = false;\n\n        defaultPrefs.init();\n\n        assert.neverCalledWith(defaultPrefs.set, \"foo\", true);\n      });\n      it(\"should initialize a string pref\", () => {\n        defaultPrefs.init();\n        assert.calledWith(defaultPrefs.set, \"bar\", \"BAR\");\n      });\n      it(\"should initialize a integer pref\", () => {\n        defaultPrefs.init();\n        assert.calledWith(defaultPrefs.set, \"baz\", 1);\n      });\n      it(\"should initialize a pref with value if Firefox is not a local build\", () => {\n        defaultPrefs.init();\n        assert.calledWith(defaultPrefs.set, \"qux\", \"foo\");\n      });\n      it(\"should initialize a pref with value_local_dev if Firefox is a local build\", () => {\n        sandbox.stub(global.AppConstants, \"MOZILLA_OFFICIAL\").value(false);\n        defaultPrefs.init();\n        assert.calledWith(defaultPrefs.set, \"qux\", \"foofoo\");\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/lib/ActivityStreamStorage.test.js",
    "content": "import { ActivityStreamStorage } from \"lib/ActivityStreamStorage.jsm\";\nimport { GlobalOverrider } from \"test/unit/utils\";\n\nlet overrider = new GlobalOverrider();\n\ndescribe(\"ActivityStreamStorage\", () => {\n  let sandbox;\n  let indexedDB;\n  let storage;\n  beforeEach(() => {\n    sandbox = sinon.createSandbox();\n    indexedDB = {\n      open: sandbox.stub().resolves({}),\n      deleteDatabase: sandbox.stub().resolves(),\n    };\n    overrider.set({ IndexedDB: indexedDB });\n    storage = new ActivityStreamStorage({\n      storeNames: [\"storage_test\"],\n      telemetry: { handleUndesiredEvent: sandbox.stub() },\n    });\n  });\n  afterEach(() => {\n    sandbox.restore();\n  });\n  it(\"should throw if required arguments not provided\", () => {\n    assert.throws(() => new ActivityStreamStorage({ telemetry: true }));\n  });\n  describe(\".db\", () => {\n    it(\"should not throw an error when accessing db\", async () => {\n      assert.ok(storage.db);\n    });\n\n    it(\"should delete and recreate the db if opening db fails\", async () => {\n      const newDb = {};\n      indexedDB.open.onFirstCall().rejects(new Error(\"fake error\"));\n      indexedDB.open.onSecondCall().resolves(newDb);\n\n      const db = await storage.db;\n      assert.calledOnce(indexedDB.deleteDatabase);\n      assert.calledTwice(indexedDB.open);\n      assert.equal(db, newDb);\n    });\n  });\n  describe(\"#getDbTable\", () => {\n    let testStorage;\n    let storeStub;\n    beforeEach(() => {\n      storeStub = {\n        getAll: sandbox.stub().resolves(),\n        get: sandbox.stub().resolves(),\n        put: sandbox.stub().resolves(),\n      };\n      sandbox.stub(storage, \"_getStore\").resolves(storeStub);\n      testStorage = storage.getDbTable(\"storage_test\");\n    });\n    it(\"should reverse key value parameters for put\", async () => {\n      await testStorage.set(\"key\", \"value\");\n\n      assert.calledOnce(storeStub.put);\n      assert.calledWith(storeStub.put, \"value\", \"key\");\n    });\n    it(\"should return the correct value for get\", async () => {\n      storeStub.get.withArgs(\"foo\").resolves(\"foo\");\n\n      const result = await testStorage.get(\"foo\");\n\n      assert.calledOnce(storeStub.get);\n      assert.equal(result, \"foo\");\n    });\n    it(\"should return the correct value for getAll\", async () => {\n      storeStub.getAll.resolves([\"bar\"]);\n\n      const result = await testStorage.getAll();\n\n      assert.calledOnce(storeStub.getAll);\n      assert.deepEqual(result, [\"bar\"]);\n    });\n    it(\"should query the correct object store\", async () => {\n      await testStorage.get();\n\n      assert.calledOnce(storage._getStore);\n      assert.calledWithExactly(storage._getStore, \"storage_test\");\n    });\n    it(\"should throw if table is not found\", () => {\n      assert.throws(() => storage.getDbTable(\"undefined_store\"));\n    });\n  });\n  it(\"should get the correct objectStore when calling _getStore\", async () => {\n    const objectStoreStub = sandbox.stub();\n    indexedDB.open.resolves({ objectStore: objectStoreStub });\n\n    await storage._getStore(\"foo\");\n\n    assert.calledOnce(objectStoreStub);\n    assert.calledWithExactly(objectStoreStub, \"foo\", \"readwrite\");\n  });\n  it(\"should create a db with the correct store name\", async () => {\n    const dbStub = {\n      createObjectStore: sandbox.stub(),\n      objectStoreNames: { contains: sandbox.stub().returns(false) },\n    };\n    await storage.db;\n\n    // call the cb with a stub\n    indexedDB.open.args[0][2](dbStub);\n\n    assert.calledOnce(dbStub.createObjectStore);\n    assert.calledWithExactly(dbStub.createObjectStore, \"storage_test\");\n  });\n  it(\"should handle an array of object store names\", async () => {\n    storage = new ActivityStreamStorage({\n      storeNames: [\"store1\", \"store2\"],\n      telemetry: {},\n    });\n    const dbStub = {\n      createObjectStore: sandbox.stub(),\n      objectStoreNames: { contains: sandbox.stub().returns(false) },\n    };\n    await storage.db;\n\n    // call the cb with a stub\n    indexedDB.open.args[0][2](dbStub);\n\n    assert.calledTwice(dbStub.createObjectStore);\n    assert.calledWith(dbStub.createObjectStore, \"store1\");\n    assert.calledWith(dbStub.createObjectStore, \"store2\");\n  });\n  it(\"should skip creating existing stores\", async () => {\n    storage = new ActivityStreamStorage({\n      storeNames: [\"store1\", \"store2\"],\n      telemetry: {},\n    });\n    const dbStub = {\n      createObjectStore: sandbox.stub(),\n      objectStoreNames: { contains: sandbox.stub().returns(true) },\n    };\n    await storage.db;\n\n    // call the cb with a stub\n    indexedDB.open.args[0][2](dbStub);\n\n    assert.notCalled(dbStub.createObjectStore);\n  });\n  describe(\"#_requestWrapper\", () => {\n    it(\"should return a successful result\", async () => {\n      const result = await storage._requestWrapper(() =>\n        Promise.resolve(\"foo\")\n      );\n\n      assert.equal(result, \"foo\");\n      assert.notCalled(storage.telemetry.handleUndesiredEvent);\n    });\n    it(\"should report failures\", async () => {\n      try {\n        await storage._requestWrapper(() => Promise.reject(new Error()));\n      } catch (e) {\n        assert.calledOnce(storage.telemetry.handleUndesiredEvent);\n      }\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/lib/BookmarkPanelHub.test.js",
    "content": "import { _BookmarkPanelHub } from \"lib/BookmarkPanelHub.jsm\";\nimport { GlobalOverrider } from \"test/unit/utils\";\nimport { PanelTestProvider } from \"lib/PanelTestProvider.jsm\";\n\ndescribe(\"BookmarkPanelHub\", () => {\n  let globals;\n  let sandbox;\n  let instance;\n  let fakeAddImpression;\n  let fakeHandleMessageRequest;\n  let fakeL10n;\n  let fakeMessage;\n  let fakeMessageFluent;\n  let fakeTarget;\n  let fakeContainer;\n  let fakeDispatch;\n  let fakeWindow;\n  let isBrowserPrivateStub;\n  beforeEach(() => {\n    sandbox = sinon.createSandbox();\n    globals = new GlobalOverrider();\n\n    fakeL10n = {\n      setAttributes: sandbox.stub(),\n      translateElements: sandbox.stub().resolves(),\n    };\n    globals.set(\"DOMLocalization\", function() {\n      return fakeL10n;\n    }); // eslint-disable-line prefer-arrow-callback\n    globals.set(\"FxAccounts\", {\n      config: { promiseConnectAccountURI: sandbox.stub() },\n    });\n    isBrowserPrivateStub = sandbox.stub().returns(false);\n    globals.set(\"PrivateBrowsingUtils\", {\n      isBrowserPrivate: isBrowserPrivateStub,\n    });\n\n    instance = new _BookmarkPanelHub();\n    fakeAddImpression = sandbox.stub();\n    fakeHandleMessageRequest = sandbox.stub();\n    [\n      { content: fakeMessageFluent },\n      { content: fakeMessage },\n    ] = PanelTestProvider.getMessages();\n    fakeContainer = {\n      addEventListener: sandbox.stub(),\n      setAttribute: sandbox.stub(),\n      removeAttribute: sandbox.stub(),\n      classList: { add: sandbox.stub() },\n      appendChild: sandbox.stub(),\n      querySelector: sandbox.stub(),\n      children: [],\n      style: {},\n      getBoundingClientRect: sandbox.stub(),\n    };\n    const document = {\n      createElementNS: sandbox.stub().returns(fakeContainer),\n      getElementById: sandbox.stub().returns(fakeContainer),\n      l10n: fakeL10n,\n    };\n    fakeWindow = {\n      ownerGlobal: {\n        openLinkIn: sandbox.stub(),\n        gBrowser: { selectedBrowser: \"browser\" },\n      },\n      MozXULElement: { insertFTLIfNeeded: sandbox.stub() },\n      document,\n      requestAnimationFrame: x => x(),\n    };\n    fakeTarget = {\n      document,\n      container: {\n        querySelector: sandbox.stub(),\n        appendChild: sandbox.stub(),\n        setAttribute: sandbox.stub(),\n        removeAttribute: sandbox.stub(),\n      },\n      hidePopup: sandbox.stub(),\n      infoButton: {},\n      close: sandbox.stub(),\n      browser: {\n        ownerGlobal: {\n          gBrowser: { ownerDocument: document },\n          window: fakeWindow,\n        },\n      },\n    };\n    fakeDispatch = sandbox.stub();\n  });\n  afterEach(() => {\n    instance.uninit();\n    sandbox.restore();\n    globals.restore();\n  });\n  it(\"should create an instance\", () => {\n    assert.ok(instance);\n  });\n  it(\"should uninit\", () => {\n    instance.uninit();\n\n    assert.isFalse(instance._initialized);\n    assert.isNull(instance._addImpression);\n    assert.isNull(instance._handleMessageRequest);\n  });\n  it(\"should instantiate handleMessageRequest and addImpression and l10n\", () => {\n    instance.init(fakeHandleMessageRequest, fakeAddImpression, fakeDispatch);\n\n    assert.equal(instance._addImpression, fakeAddImpression);\n    assert.equal(instance._handleMessageRequest, fakeHandleMessageRequest);\n    assert.equal(instance._dispatch, fakeDispatch);\n    assert.ok(instance._l10n);\n    assert.isTrue(instance._initialized);\n  });\n  it(\"should return early if not initialized\", async () => {\n    assert.isFalse(await instance.messageRequest());\n  });\n  describe(\"#messageRequest\", () => {\n    beforeEach(() => {\n      sandbox.stub(instance, \"onResponse\");\n      instance.init(fakeHandleMessageRequest, fakeAddImpression, fakeDispatch);\n    });\n    afterEach(() => {\n      sandbox.restore();\n    });\n    it(\"should not re-request messages for the same URL\", async () => {\n      instance._response = { url: \"foo.com\", content: true };\n      fakeTarget.url = \"foo.com\";\n      sandbox.stub(instance, \"showMessage\");\n\n      await instance.messageRequest(fakeTarget);\n\n      assert.notCalled(fakeHandleMessageRequest);\n      assert.calledOnce(instance.showMessage);\n    });\n    it(\"should call handleMessageRequest\", async () => {\n      fakeHandleMessageRequest.resolves(fakeMessage);\n\n      await instance.messageRequest(fakeTarget, {});\n\n      assert.calledOnce(fakeHandleMessageRequest);\n      assert.calledWithExactly(fakeHandleMessageRequest, {\n        triggerId: instance._trigger.id,\n      });\n    });\n    it(\"should call onResponse\", async () => {\n      fakeHandleMessageRequest.resolves(fakeMessage);\n\n      await instance.messageRequest(fakeTarget, {});\n\n      assert.calledOnce(instance.onResponse);\n      assert.calledWithExactly(\n        instance.onResponse,\n        fakeMessage,\n        fakeTarget,\n        {}\n      );\n    });\n  });\n  describe(\"#onResponse\", () => {\n    beforeEach(() => {\n      instance.init(fakeHandleMessageRequest, fakeAddImpression, fakeDispatch);\n      sandbox.stub(instance, \"showMessage\");\n      sandbox.stub(instance, \"sendImpression\");\n      sandbox.stub(instance, \"hideMessage\");\n      fakeTarget = { infoButton: { disabled: true } };\n    });\n    it(\"should show a message when called with a response\", () => {\n      instance.onResponse({ content: \"content\" }, fakeTarget, fakeWindow);\n\n      assert.calledOnce(instance.showMessage);\n      assert.calledWithExactly(\n        instance.showMessage,\n        \"content\",\n        fakeTarget,\n        fakeWindow\n      );\n      assert.calledOnce(instance.sendImpression);\n    });\n    it(\"should insert the appropriate ftl files with translations\", () => {\n      instance.onResponse({ content: \"content\" }, fakeTarget, fakeWindow);\n\n      assert.calledTwice(fakeWindow.MozXULElement.insertFTLIfNeeded);\n      assert.calledWith(\n        fakeWindow.MozXULElement.insertFTLIfNeeded,\n        \"browser/newtab/asrouter.ftl\"\n      );\n      assert.calledWith(\n        fakeWindow.MozXULElement.insertFTLIfNeeded,\n        \"browser/branding/sync-brand.ftl\"\n      );\n    });\n    it(\"should dispatch a user impression\", () => {\n      sandbox.spy(instance, \"sendUserEventTelemetry\");\n\n      instance.onResponse({ content: \"content\" }, fakeTarget, fakeWindow);\n\n      assert.calledOnce(instance.sendUserEventTelemetry);\n      assert.calledWithExactly(\n        instance.sendUserEventTelemetry,\n        \"IMPRESSION\",\n        fakeWindow\n      );\n      assert.calledOnce(fakeDispatch);\n\n      const [ping] = fakeDispatch.firstCall.args;\n\n      assert.equal(ping.type, \"DOORHANGER_TELEMETRY\");\n      assert.equal(ping.data.event, \"IMPRESSION\");\n    });\n    it(\"should not dispatch a user impression if the window is private\", () => {\n      isBrowserPrivateStub.returns(true);\n      sandbox.spy(instance, \"sendUserEventTelemetry\");\n\n      instance.onResponse({ content: \"content\" }, fakeTarget, fakeWindow);\n\n      assert.calledOnce(instance.sendUserEventTelemetry);\n      assert.calledWithExactly(\n        instance.sendUserEventTelemetry,\n        \"IMPRESSION\",\n        fakeWindow\n      );\n      assert.notCalled(fakeDispatch);\n    });\n    it(\"should hide existing messages if no response is provided\", () => {\n      instance.onResponse(null, fakeTarget);\n\n      assert.calledOnce(instance.hideMessage);\n      assert.calledWithExactly(instance.hideMessage, fakeTarget);\n    });\n  });\n  describe(\"#showMessage.collapsed=false\", () => {\n    beforeEach(() => {\n      instance.init(fakeHandleMessageRequest, fakeAddImpression, fakeDispatch);\n      sandbox.stub(instance, \"toggleRecommendation\");\n      sandbox.stub(instance, \"_response\").value({ collapsed: false });\n    });\n    it(\"should create a container\", () => {\n      fakeTarget.container.querySelector.returns(false);\n\n      instance.showMessage(fakeMessage, fakeTarget);\n\n      assert.equal(fakeTarget.document.createElementNS.callCount, 6);\n      assert.calledOnce(fakeTarget.container.appendChild);\n      assert.notCalled(fakeL10n.setAttributes);\n    });\n    it(\"should create a container (fluent message)\", () => {\n      fakeTarget.container.querySelector.returns(false);\n\n      instance.showMessage(fakeMessageFluent, fakeTarget);\n\n      assert.equal(fakeTarget.document.createElementNS.callCount, 6);\n      assert.calledOnce(fakeTarget.container.appendChild);\n    });\n    it(\"should set l10n attributes\", () => {\n      fakeTarget.container.querySelector.returns(false);\n\n      instance.showMessage(fakeMessageFluent, fakeTarget);\n\n      assert.equal(fakeL10n.setAttributes.callCount, 4);\n    });\n    it(\"call adjust panel height when height is > 150px\", async () => {\n      fakeTarget.container.querySelector.returns(false);\n      fakeContainer.getBoundingClientRect.returns({ height: 160 });\n\n      await instance._adjustPanelHeight(fakeWindow, fakeContainer);\n\n      assert.calledOnce(fakeWindow.document.l10n.translateElements);\n      assert.calledTwice(fakeContainer.getBoundingClientRect);\n      assert.calledWithExactly(\n        fakeContainer.classList.add,\n        \"longMessagePadding\"\n      );\n    });\n    it(\"should reuse the container\", () => {\n      fakeTarget.container.querySelector.returns(true);\n\n      instance.showMessage(fakeMessage, fakeTarget);\n\n      assert.notCalled(fakeTarget.container.appendChild);\n    });\n    it(\"should open a tab with FxA signup\", async () => {\n      fakeTarget.container.querySelector.returns(false);\n\n      instance.showMessage(fakeMessage, fakeTarget, fakeWindow);\n      // Call the event listener cb\n      await fakeContainer.addEventListener.firstCall.args[1]();\n\n      assert.calledOnce(fakeWindow.ownerGlobal.openLinkIn);\n    });\n    it(\"should send a click event\", async () => {\n      sandbox.stub(instance, \"sendUserEventTelemetry\");\n      fakeTarget.container.querySelector.returns(false);\n\n      instance.showMessage(fakeMessage, fakeTarget, fakeWindow);\n      // Call the event listener cb\n      await fakeContainer.addEventListener.firstCall.args[1]();\n\n      assert.calledOnce(instance.sendUserEventTelemetry);\n      assert.calledWithExactly(\n        instance.sendUserEventTelemetry,\n        \"CLICK\",\n        fakeWindow\n      );\n    });\n    it(\"should send a click event\", async () => {\n      sandbox.stub(instance, \"sendUserEventTelemetry\");\n      fakeTarget.container.querySelector.returns(false);\n\n      instance.showMessage(fakeMessage, fakeTarget, fakeWindow);\n      // Call the event listener cb\n      await fakeContainer.addEventListener.firstCall.args[1]();\n\n      assert.calledOnce(instance.sendUserEventTelemetry);\n      assert.calledWithExactly(\n        instance.sendUserEventTelemetry,\n        \"CLICK\",\n        fakeWindow\n      );\n    });\n    it(\"should collapse the message\", () => {\n      fakeTarget.container.querySelector.returns(false);\n      sandbox.spy(instance, \"collapseMessage\");\n      instance._response.collapsed = false;\n\n      instance.showMessage(fakeMessage, fakeTarget, fakeWindow);\n      // Show message calls it once so we need to reset\n      instance.toggleRecommendation.reset();\n      // Call the event listener cb\n      fakeContainer.addEventListener.secondCall.args[1]();\n\n      assert.calledOnce(instance.collapseMessage);\n      assert.calledOnce(fakeTarget.close);\n      assert.isTrue(instance._response.collapsed);\n      assert.calledOnce(instance.toggleRecommendation);\n    });\n    it(\"should send a dismiss event\", () => {\n      sandbox.stub(instance, \"sendUserEventTelemetry\");\n      sandbox.spy(instance, \"collapseMessage\");\n      instance._response.collapsed = false;\n\n      instance.showMessage(fakeMessage, fakeTarget, fakeWindow);\n      // Call the event listener cb\n      fakeContainer.addEventListener.secondCall.args[1]();\n\n      assert.calledOnce(instance.sendUserEventTelemetry);\n      assert.calledWithExactly(\n        instance.sendUserEventTelemetry,\n        \"DISMISS\",\n        fakeWindow\n      );\n    });\n    it(\"should call toggleRecommendation `true`\", () => {\n      instance.showMessage(fakeMessage, fakeTarget, fakeWindow);\n\n      assert.calledOnce(instance.toggleRecommendation);\n      assert.calledWithExactly(instance.toggleRecommendation, true);\n    });\n  });\n  describe(\"#showMessage.collapsed=true\", () => {\n    beforeEach(() => {\n      sandbox\n        .stub(instance, \"_response\")\n        .value({ collapsed: true, target: fakeTarget });\n      sandbox.stub(instance, \"toggleRecommendation\");\n    });\n    it(\"should return early if the message is collapsed\", () => {\n      instance.showMessage();\n\n      assert.calledOnce(instance.toggleRecommendation);\n      assert.calledWithExactly(instance.toggleRecommendation, false);\n    });\n  });\n  describe(\"#hideMessage\", () => {\n    let removeStub;\n    beforeEach(() => {\n      sandbox.stub(instance, \"toggleRecommendation\");\n      removeStub = sandbox.stub();\n      fakeTarget = {\n        container: {\n          querySelector: sandbox.stub().returns({ remove: removeStub }),\n        },\n      };\n      instance._response = { win: fakeWindow };\n    });\n    it(\"should remove the message\", () => {\n      instance.hideMessage(fakeTarget);\n\n      assert.calledOnce(removeStub);\n    });\n    it(\"should call toggleRecommendation `false`\", () => {\n      instance.hideMessage(fakeTarget);\n\n      assert.calledOnce(instance.toggleRecommendation);\n      assert.calledWithExactly(instance.toggleRecommendation, false);\n    });\n  });\n  describe(\"#toggleRecommendation\", () => {\n    let target;\n    beforeEach(() => {\n      target = {\n        infoButton: {},\n        container: {\n          setAttribute: sandbox.stub(),\n          removeAttribute: sandbox.stub(),\n        },\n      };\n      sandbox.stub(instance, \"_response\").value({ target });\n    });\n    it(\"should check infoButton\", () => {\n      instance.toggleRecommendation(true);\n\n      assert.isTrue(target.infoButton.checked);\n    });\n    it(\"should uncheck infoButton\", () => {\n      instance.toggleRecommendation(false);\n\n      assert.isFalse(target.infoButton.checked);\n    });\n    it(\"should uncheck infoButton\", () => {\n      target.infoButton.checked = true;\n\n      instance.toggleRecommendation();\n\n      assert.isFalse(target.infoButton.checked);\n    });\n    it(\"should disable the container\", () => {\n      target.infoButton.checked = true;\n\n      instance.toggleRecommendation();\n\n      assert.calledOnce(target.container.setAttribute);\n    });\n    it(\"should enable container\", () => {\n      target.infoButton.checked = false;\n\n      instance.toggleRecommendation();\n\n      assert.calledOnce(target.container.removeAttribute);\n    });\n  });\n  describe(\"#_forceShowMessage\", () => {\n    it(\"should call showMessage with the correct args\", () => {\n      sandbox.spy(instance, \"showMessage\");\n      sandbox.stub(instance, \"hideMessage\");\n\n      instance._forceShowMessage(fakeTarget, { content: fakeMessage });\n\n      assert.calledOnce(instance.showMessage);\n      assert.calledOnce(instance.hideMessage);\n      assert.calledWithExactly(\n        instance.showMessage,\n        fakeMessage,\n        sinon.match.object,\n        fakeWindow\n      );\n    });\n    it(\"should insert required fluent files\", () => {\n      sandbox.stub(instance, \"showMessage\");\n\n      instance._forceShowMessage(fakeTarget, { content: fakeMessage });\n\n      assert.calledTwice(fakeWindow.MozXULElement.insertFTLIfNeeded);\n    });\n    it(\"should insert a message you can collapse\", () => {\n      sandbox.spy(instance, \"showMessage\");\n      sandbox.stub(instance, \"toggleRecommendation\");\n      sandbox.stub(instance, \"sendUserEventTelemetry\");\n\n      instance._forceShowMessage(fakeTarget, { content: fakeMessage });\n\n      const [\n        ,\n        eventListenerCb,\n      ] = fakeContainer.addEventListener.secondCall.args;\n      // Called with `true` to show the message\n      instance.toggleRecommendation.reset();\n      eventListenerCb({ stopPropagation: sandbox.stub() });\n\n      assert.calledWithExactly(instance.toggleRecommendation, false);\n    });\n  });\n  describe(\"#sendImpression\", () => {\n    beforeEach(() => {\n      instance.init(fakeHandleMessageRequest, fakeAddImpression, fakeDispatch);\n      instance._response = \"foo\";\n    });\n    it(\"should dispatch an impression\", () => {\n      instance.sendImpression();\n\n      assert.calledOnce(fakeAddImpression);\n      assert.equal(fakeAddImpression.firstCall.args[0], \"foo\");\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/lib/DiscoveryStreamFeed.test.js",
    "content": "import {\n  actionCreators as ac,\n  actionTypes as at,\n  actionUtils as au,\n} from \"common/Actions.jsm\";\nimport { combineReducers, createStore } from \"redux\";\nimport { GlobalOverrider } from \"test/unit/utils\";\nimport injector from \"inject!lib/DiscoveryStreamFeed.jsm\";\nimport { reducers } from \"common/Reducers.jsm\";\n\nconst CONFIG_PREF_NAME = \"discoverystream.config\";\nconst DUMMY_ENDPOINT = \"https://getpocket.cdn.mozilla.net/dummy\";\nconst ENDPOINTS_PREF_NAME = \"discoverystream.endpoints\";\nconst SPOC_IMPRESSION_TRACKING_PREF = \"discoverystream.spoc.impressions\";\nconst REC_IMPRESSION_TRACKING_PREF = \"discoverystream.rec.impressions\";\nconst THIRTY_MINUTES = 30 * 60 * 1000;\nconst ONE_WEEK = 7 * 24 * 60 * 60 * 1000; // 1 week\n\nconst FAKE_UUID = \"{foo-123-foo}\";\n\n// eslint-disable-next-line max-statements\ndescribe(\"DiscoveryStreamFeed\", () => {\n  let DiscoveryStreamFeed;\n  let feed;\n  let sandbox;\n  let fetchStub;\n  let clock;\n  let fakeNewTabUtils;\n  let globals;\n\n  const setPref = (name, value) => {\n    const action = {\n      type: at.PREF_CHANGED,\n      data: {\n        name,\n        value: typeof value === \"object\" ? JSON.stringify(value) : value,\n      },\n    };\n    feed.store.dispatch(action);\n    feed.onAction(action);\n  };\n\n  beforeEach(() => {\n    sandbox = sinon.createSandbox();\n\n    class FakeUserDomainAffinityProvider {\n      constructor(\n        timeSegments,\n        parameterSets,\n        maxHistoryQueryResults,\n        version,\n        scores\n      ) {\n        this.timeSegments = timeSegments;\n        this.parameterSets = parameterSets;\n        this.maxHistoryQueryResults = maxHistoryQueryResults;\n        this.version = version;\n        this.scores = scores;\n      }\n\n      getAffinities() {\n        return {};\n      }\n    }\n\n    // Fetch\n    fetchStub = sandbox.stub(global, \"fetch\");\n\n    // Time\n    clock = sinon.useFakeTimers();\n\n    // Injector\n    ({ DiscoveryStreamFeed } = injector({\n      \"lib/UserDomainAffinityProvider.jsm\": {\n        UserDomainAffinityProvider: FakeUserDomainAffinityProvider,\n      },\n    }));\n\n    globals = new GlobalOverrider();\n    globals.set(\"gUUIDGenerator\", { generateUUID: () => FAKE_UUID });\n\n    sandbox\n      .stub(global.Services.prefs, \"getBoolPref\")\n      .withArgs(\"browser.newtabpage.activity-stream.discoverystream.enabled\")\n      .returns(true);\n\n    // Feed\n    feed = new DiscoveryStreamFeed();\n    feed.store = createStore(combineReducers(reducers), {\n      Prefs: {\n        values: {\n          [CONFIG_PREF_NAME]: JSON.stringify({\n            enabled: false,\n            show_spocs: false,\n            layout_endpoint: DUMMY_ENDPOINT,\n          }),\n          [ENDPOINTS_PREF_NAME]: DUMMY_ENDPOINT,\n          \"discoverystream.enabled\": true,\n        },\n      },\n    });\n    global.fetch.resetHistory();\n\n    sandbox.stub(feed, \"_maybeUpdateCachedData\").resolves();\n\n    globals.set(\"setTimeout\", callback => {\n      callback();\n    });\n\n    fakeNewTabUtils = {\n      blockedLinks: {\n        links: [],\n        isBlocked: () => false,\n      },\n    };\n    globals.set(\"NewTabUtils\", fakeNewTabUtils);\n  });\n\n  afterEach(() => {\n    clock.restore();\n    sandbox.restore();\n    globals.restore();\n  });\n\n  describe(\"#fetchFromEndpoint\", () => {\n    beforeEach(() => {\n      feed._prefCache = {\n        config: {\n          api_key_pref: \"\",\n        },\n      };\n      fetchStub.resolves({\n        json: () => Promise.resolve(\"hi\"),\n        ok: true,\n      });\n    });\n    it(\"should get a response\", async () => {\n      const response = await feed.fetchFromEndpoint(DUMMY_ENDPOINT);\n\n      assert.equal(response, \"hi\");\n    });\n    it(\"should not send cookies\", async () => {\n      await feed.fetchFromEndpoint(DUMMY_ENDPOINT);\n\n      assert.propertyVal(fetchStub.firstCall.args[1], \"credentials\", \"omit\");\n    });\n    it(\"should allow unexpected response\", async () => {\n      fetchStub.resolves({ ok: false });\n\n      const response = await feed.fetchFromEndpoint(DUMMY_ENDPOINT);\n\n      assert.equal(response, null);\n    });\n    it(\"should disallow unexpected endpoints\", async () => {\n      feed.store.getState = () => ({\n        Prefs: { values: { [ENDPOINTS_PREF_NAME]: \"https://other.site\" } },\n      });\n\n      const response = await feed.fetchFromEndpoint(DUMMY_ENDPOINT);\n\n      assert.equal(response, null);\n    });\n    it(\"should allow multiple endpoints\", async () => {\n      feed.store.getState = () => ({\n        Prefs: {\n          values: {\n            [ENDPOINTS_PREF_NAME]: `https://other.site,${DUMMY_ENDPOINT}`,\n          },\n        },\n      });\n\n      const response = await feed.fetchFromEndpoint(DUMMY_ENDPOINT);\n\n      assert.equal(response, \"hi\");\n    });\n    it(\"should replace urls with $apiKey\", async () => {\n      sandbox.stub(global.Services.prefs, \"getCharPref\").returns(\"replaced\");\n\n      await feed.fetchFromEndpoint(\n        \"https://getpocket.cdn.mozilla.net/dummy?consumer_key=$apiKey\"\n      );\n\n      assert.calledWithMatch(\n        fetchStub,\n        \"https://getpocket.cdn.mozilla.net/dummy?consumer_key=replaced\",\n        { credentials: \"omit\" }\n      );\n    });\n    it(\"should replace locales with $locale\", async () => {\n      feed.locale = \"replaced\";\n      await feed.fetchFromEndpoint(\n        \"https://getpocket.cdn.mozilla.net/dummy?locale_lang=$locale\"\n      );\n\n      assert.calledWithMatch(\n        fetchStub,\n        \"https://getpocket.cdn.mozilla.net/dummy?locale_lang=replaced\",\n        { credentials: \"omit\" }\n      );\n    });\n    it(\"should allow POST and with other options\", async () => {\n      await feed.fetchFromEndpoint(\"https://getpocket.cdn.mozilla.net/dummy\", {\n        method: \"POST\",\n        body: \"{}\",\n      });\n\n      assert.calledWithMatch(\n        fetchStub,\n        \"https://getpocket.cdn.mozilla.net/dummy\",\n        {\n          credentials: \"omit\",\n          method: \"POST\",\n          body: \"{}\",\n        }\n      );\n    });\n  });\n\n  describe(\"#getOrCreateImpressionId\", () => {\n    it(\"should create impression id in constructor\", async () => {\n      assert.equal(feed._impressionId, FAKE_UUID);\n    });\n    it(\"should create impression id if none exists\", async () => {\n      sandbox.stub(global.Services.prefs, \"setCharPref\").returns();\n\n      const result = feed.getOrCreateImpressionId();\n\n      assert.equal(result, FAKE_UUID);\n      assert.calledOnce(global.Services.prefs.setCharPref);\n    });\n    it(\"should use impression id if exists\", async () => {\n      sandbox.stub(global.Services.prefs, \"getCharPref\").returns(\"from get\");\n\n      const result = feed.getOrCreateImpressionId();\n\n      assert.equal(result, \"from get\");\n      assert.calledOnce(global.Services.prefs.getCharPref);\n    });\n  });\n\n  describe(\"#loadLayout\", () => {\n    it(\"should fetch data and populate the cache if it is empty\", async () => {\n      const resp = { layout: [\"foo\", \"bar\"] };\n      const fakeCache = {};\n      sandbox.stub(feed.cache, \"get\").returns(Promise.resolve(fakeCache));\n      sandbox.stub(feed.cache, \"set\").returns(Promise.resolve());\n\n      fetchStub.resolves({ ok: true, json: () => Promise.resolve(resp) });\n\n      await feed.loadLayout(feed.store.dispatch);\n\n      assert.calledOnce(fetchStub);\n      assert.equal(feed.cache.set.firstCall.args[0], \"layout\");\n      assert.deepEqual(feed.cache.set.firstCall.args[1].layout, resp.layout);\n    });\n    it(\"should fetch data and populate the cache if the cached data is older than 30 mins\", async () => {\n      const resp = { layout: [\"foo\", \"bar\"] };\n      const fakeCache = {\n        layout: { layout: [\"hello\"], lastUpdated: Date.now() },\n      };\n\n      sandbox.stub(feed.cache, \"get\").returns(Promise.resolve(fakeCache));\n      sandbox.stub(feed.cache, \"set\").returns(Promise.resolve());\n\n      fetchStub.resolves({ ok: true, json: () => Promise.resolve(resp) });\n\n      clock.tick(THIRTY_MINUTES + 1);\n      await feed.loadLayout(feed.store.dispatch);\n\n      assert.calledOnce(fetchStub);\n      assert.equal(feed.cache.set.firstCall.args[0], \"layout\");\n      assert.deepEqual(feed.cache.set.firstCall.args[1].layout, resp.layout);\n    });\n    it(\"should use the cached data and not fetch if the cached data is less than 30 mins old\", async () => {\n      const fakeCache = {\n        layout: { layout: [\"hello\"], lastUpdated: Date.now() },\n      };\n\n      sandbox.stub(feed.cache, \"get\").returns(Promise.resolve(fakeCache));\n      sandbox.stub(feed.cache, \"set\").returns(Promise.resolve());\n\n      clock.tick(THIRTY_MINUTES - 1);\n      await feed.loadLayout(feed.store.dispatch);\n\n      assert.notCalled(fetchStub);\n      assert.notCalled(feed.cache.set);\n    });\n    it(\"should set spocs_endpoint from layout\", async () => {\n      const resp = { layout: [\"foo\", \"bar\"], spocs: { url: \"foo.com\" } };\n      const fakeCache = {};\n      sandbox.stub(feed.cache, \"get\").returns(Promise.resolve(fakeCache));\n      sandbox.stub(feed.cache, \"set\").returns(Promise.resolve());\n\n      fetchStub.resolves({ ok: true, json: () => Promise.resolve(resp) });\n\n      await feed.loadLayout(feed.store.dispatch);\n\n      assert.equal(\n        feed.store.getState().DiscoveryStream.spocs.spocs_endpoint,\n        \"foo.com\"\n      );\n    });\n    it(\"should use local layout with hardcoded_layout being true\", async () => {\n      feed.config.hardcoded_layout = true;\n      sandbox.stub(feed, \"fetchLayout\").returns(Promise.resolve(\"\"));\n\n      await feed.loadLayout(feed.store.dispatch);\n\n      assert.notCalled(feed.fetchLayout);\n      assert.equal(\n        feed.store.getState().DiscoveryStream.spocs.spocs_endpoint,\n        \"https://spocs.getpocket.com/spocs\"\n      );\n    });\n    it(\"should use local basic layout with hardcoded_layout and hardcoded_basic_layout being true\", async () => {\n      feed.config.hardcoded_layout = true;\n      feed.config.hardcoded_basic_layout = true;\n      sandbox.stub(feed, \"fetchLayout\").returns(Promise.resolve(\"\"));\n\n      await feed.loadLayout(feed.store.dispatch);\n\n      assert.notCalled(feed.fetchLayout);\n      assert.equal(\n        feed.store.getState().DiscoveryStream.spocs.spocs_endpoint,\n        \"https://spocs.getpocket.com/spocs\"\n      );\n      const { layout } = feed.store.getState().DiscoveryStream;\n      assert.equal(layout[0].components[2].properties.items, 3);\n    });\n    it(\"should use 1 row layout if locale lang doesn't support 7 row layout\", async () => {\n      feed.config.hardcoded_layout = true;\n      feed.store = createStore(combineReducers(reducers), {\n        Prefs: {\n          values: {\n            [CONFIG_PREF_NAME]: JSON.stringify({\n              enabled: true,\n              show_spocs: false,\n              layout_endpoint: DUMMY_ENDPOINT,\n            }),\n            [ENDPOINTS_PREF_NAME]: DUMMY_ENDPOINT,\n            \"discoverystream.enabled\": true,\n            \"discoverystream.lang-layout-config\": \"en\",\n          },\n        },\n      });\n      feed.locale = \"de-DE\";\n      sandbox.stub(feed, \"fetchLayout\").returns(Promise.resolve(\"\"));\n\n      await feed.loadLayout(feed.store.dispatch);\n\n      const { layout } = feed.store.getState().DiscoveryStream;\n      assert.equal(layout[0].components[2].properties.items, 3);\n    });\n    it(\"should use 7 row layout if locale lang supports it\", async () => {\n      feed.config.hardcoded_layout = true;\n      feed.store = createStore(combineReducers(reducers), {\n        Prefs: {\n          values: {\n            [CONFIG_PREF_NAME]: JSON.stringify({\n              enabled: true,\n              show_spocs: false,\n              layout_endpoint: DUMMY_ENDPOINT,\n            }),\n            [ENDPOINTS_PREF_NAME]: DUMMY_ENDPOINT,\n            \"discoverystream.enabled\": true,\n            \"discoverystream.lang-layout-config\": \"en,de\",\n          },\n        },\n      });\n      feed.locale = \"de-DE\";\n      sandbox.stub(feed, \"fetchLayout\").returns(Promise.resolve(\"\"));\n\n      await feed.loadLayout(feed.store.dispatch);\n\n      const { layout } = feed.store.getState().DiscoveryStream;\n      assert.equal(layout[2].components[0].properties.items, 21);\n    });\n    it(\"should use new spocs endpoint if in the config\", async () => {\n      feed.config.spocs_endpoint = \"https://spocs.getpocket.com/spocs2\";\n\n      await feed.loadLayout(feed.store.dispatch);\n\n      assert.equal(\n        feed.store.getState().DiscoveryStream.spocs.spocs_endpoint,\n        \"https://spocs.getpocket.com/spocs2\"\n      );\n    });\n    it(\"should use local basic layout with hardcoded_layout and FF pref hardcoded_basic_layout\", async () => {\n      feed.config.hardcoded_layout = true;\n      feed.store = createStore(combineReducers(reducers), {\n        Prefs: {\n          values: {\n            [CONFIG_PREF_NAME]: JSON.stringify({\n              enabled: false,\n              show_spocs: false,\n              layout_endpoint: DUMMY_ENDPOINT,\n            }),\n            [ENDPOINTS_PREF_NAME]: DUMMY_ENDPOINT,\n            \"discoverystream.enabled\": true,\n            \"discoverystream.hardcoded-basic-layout\": true,\n          },\n        },\n      });\n\n      sandbox.stub(feed, \"fetchLayout\").returns(Promise.resolve(\"\"));\n\n      await feed.loadLayout(feed.store.dispatch);\n\n      assert.notCalled(feed.fetchLayout);\n      assert.equal(\n        feed.store.getState().DiscoveryStream.spocs.spocs_endpoint,\n        \"https://spocs.getpocket.com/spocs\"\n      );\n      const { layout } = feed.store.getState().DiscoveryStream;\n      assert.equal(layout[0].components[2].properties.items, 3);\n    });\n    it(\"should use new spocs endpoint if in a FF pref\", async () => {\n      feed.store = createStore(combineReducers(reducers), {\n        Prefs: {\n          values: {\n            [CONFIG_PREF_NAME]: JSON.stringify({\n              enabled: false,\n              show_spocs: false,\n              layout_endpoint: DUMMY_ENDPOINT,\n            }),\n            [ENDPOINTS_PREF_NAME]: DUMMY_ENDPOINT,\n            \"discoverystream.enabled\": true,\n            \"discoverystream.spocs-endpoint\":\n              \"https://spocs.getpocket.com/spocs2\",\n          },\n        },\n      });\n\n      await feed.loadLayout(feed.store.dispatch);\n\n      assert.equal(\n        feed.store.getState().DiscoveryStream.spocs.spocs_endpoint,\n        \"https://spocs.getpocket.com/spocs2\"\n      );\n    });\n    it(\"should fetch local layout for invalid layout endpoint or when fetch layout fails\", async () => {\n      feed.config.hardcoded_layout = false;\n      fetchStub.resolves({ ok: false });\n\n      await feed.loadLayout(feed.store.dispatch, true);\n\n      assert.calledOnce(fetchStub);\n      assert.equal(\n        feed.store.getState().DiscoveryStream.spocs.spocs_endpoint,\n        \"https://spocs.getpocket.com/spocs\"\n      );\n    });\n  });\n\n  describe(\"#updatePlacements\", () => {\n    it(\"should fire update placements without dupes with updatePlacements\", () => {\n      sandbox.spy(feed.store, \"dispatch\");\n      const fakeComponents = {\n        components: [\n          { placement: { name: \"first\" } },\n          { placement: { name: \"second\" } },\n        ],\n      };\n      const fakeLayout = [fakeComponents];\n\n      feed.updatePlacements(feed.store.dispatch, fakeLayout);\n\n      assert.calledOnce(feed.store.dispatch);\n      assert.calledWith(feed.store.dispatch, {\n        type: \"DISCOVERY_STREAM_SPOCS_PLACEMENTS\",\n        data: { placements: [{ name: \"first\" }, { name: \"second\" }] },\n      });\n    });\n    it(\"should fire update placements from loadLayout\", async () => {\n      sandbox.spy(feed, \"updatePlacements\");\n\n      await feed.loadLayout(feed.store.dispatch);\n\n      assert.calledOnce(feed.updatePlacements);\n    });\n  });\n\n  describe(\"#placementsForEach\", () => {\n    it(\"should forEach through placements\", () => {\n      const fakeComponents = {\n        components: [\n          { placement: { name: \"first\" } },\n          { placement: { name: \"second\" } },\n        ],\n      };\n      const fakeLayout = [fakeComponents];\n      feed.updatePlacements(feed.store.dispatch, fakeLayout);\n      let items = [];\n\n      feed.placementsForEach(item => items.push(item.name));\n\n      assert.deepEqual(items, [\"first\", \"second\"]);\n    });\n    it(\"should forEach through placements for just spocs if no placements exist\", () => {\n      let items = [];\n\n      feed.placementsForEach(item => items.push(item.name));\n\n      assert.deepEqual(items, [\"spocs\"]);\n    });\n  });\n\n  describe(\"#loadLayoutEndPointUsingPref\", () => {\n    it(\"should return endpoint if valid key\", async () => {\n      const endpoint = feed.finalLayoutEndpoint(\n        \"https://somedomain.org/stories?consumer_key=$apiKey\",\n        \"test_key_val\"\n      );\n      assert.equal(\n        \"https://somedomain.org/stories?consumer_key=test_key_val\",\n        endpoint\n      );\n    });\n\n    it(\"should throw error if key is empty\", async () => {\n      assert.throws(() => {\n        feed.finalLayoutEndpoint(\n          \"https://somedomain.org/stories?consumer_key=$apiKey\",\n          \"\"\n        );\n      });\n    });\n\n    it(\"should return url if $apiKey is missing in layout_endpoint\", async () => {\n      const endpoint = feed.finalLayoutEndpoint(\n        \"https://somedomain.org/stories?consumer_key=\",\n        \"test_key_val\"\n      );\n      assert.equal(\"https://somedomain.org/stories?consumer_key=\", endpoint);\n    });\n\n    it(\"should update config layout_endpoint based on api_key_pref value\", async () => {\n      feed.store.getState = () => ({\n        Prefs: {\n          values: {\n            [CONFIG_PREF_NAME]: JSON.stringify({\n              api_key_pref: \"test_api_key_pref\",\n              enabled: true,\n              layout_endpoint:\n                \"https://somedomain.org/stories?consumer_key=$apiKey\",\n            }),\n          },\n        },\n      });\n      sandbox\n        .stub(global.Services.prefs, \"getCharPref\")\n        .returns(\"test_api_key_val\");\n      assert.equal(\n        \"https://somedomain.org/stories?consumer_key=test_api_key_val\",\n        feed.config.layout_endpoint\n      );\n    });\n\n    it(\"should not update config layout_endpoint if api_key_pref missing\", async () => {\n      feed.store.getState = () => ({\n        Prefs: {\n          values: {\n            [CONFIG_PREF_NAME]: JSON.stringify({\n              enabled: true,\n              layout_endpoint:\n                \"https://somedomain.org/stories?consumer_key=1234\",\n            }),\n          },\n        },\n      });\n      sandbox\n        .stub(global.Services.prefs, \"getCharPref\")\n        .returns(\"test_api_key_val\");\n      assert.notCalled(global.Services.prefs.getCharPref);\n      assert.equal(\n        \"https://somedomain.org/stories?consumer_key=1234\",\n        feed.config.layout_endpoint\n      );\n    });\n\n    it(\"should not set config layout_endpoint if layout_endpoint missing in prefs\", async () => {\n      feed.store.getState = () => ({\n        Prefs: {\n          values: {\n            [CONFIG_PREF_NAME]: JSON.stringify({\n              enabled: true,\n            }),\n          },\n        },\n      });\n      sandbox\n        .stub(global.Services.prefs, \"getCharPref\")\n        .returns(\"test_api_key_val\");\n      assert.notCalled(global.Services.prefs.getCharPref);\n      assert.isUndefined(feed.config.layout_endpoint);\n    });\n  });\n\n  describe(\"#loadComponentFeeds\", () => {\n    let fakeCache;\n    let fakeDiscoveryStream;\n    beforeEach(() => {\n      fakeDiscoveryStream = {\n        DiscoveryStream: {\n          layout: [\n            { components: [{ feed: { url: \"foo.com\" } }] },\n            { components: [{}] },\n            {},\n          ],\n        },\n      };\n      fakeCache = {};\n      sandbox.stub(feed.store, \"getState\").returns(fakeDiscoveryStream);\n      sandbox.stub(feed.cache, \"set\").returns(Promise.resolve());\n    });\n\n    afterEach(() => {\n      sandbox.restore();\n    });\n\n    it(\"should not dispatch updates when layout is not defined\", async () => {\n      fakeDiscoveryStream = {\n        DiscoveryStream: {},\n      };\n      feed.store.getState.returns(fakeDiscoveryStream);\n      sandbox.spy(feed.store, \"dispatch\");\n\n      await feed.loadComponentFeeds(feed.store.dispatch);\n\n      assert.notCalled(feed.store.dispatch);\n    });\n\n    it(\"should populate feeds cache\", async () => {\n      fakeCache = {\n        feeds: { \"foo.com\": { lastUpdated: Date.now(), data: \"data\" } },\n      };\n      sandbox.stub(feed.cache, \"get\").returns(Promise.resolve(fakeCache));\n\n      await feed.loadComponentFeeds(feed.store.dispatch);\n\n      assert.calledWith(feed.cache.set, \"feeds\", {\n        \"foo.com\": { data: \"data\", lastUpdated: 0 },\n      });\n    });\n\n    it(\"should send feed update events with new feed data\", async () => {\n      sandbox.stub(feed.cache, \"get\").returns(Promise.resolve(fakeCache));\n      sandbox.spy(feed.store, \"dispatch\");\n      feed._prefCache = {\n        config: {\n          api_key_pref: \"\",\n        },\n      };\n\n      await feed.loadComponentFeeds(feed.store.dispatch);\n\n      assert.calledWith(feed.store.dispatch.firstCall, {\n        type: at.DISCOVERY_STREAM_FEED_UPDATE,\n        data: { feed: { data: { status: \"failed\" } }, url: \"foo.com\" },\n      });\n      assert.calledWith(feed.store.dispatch.secondCall, {\n        type: at.DISCOVERY_STREAM_FEEDS_UPDATE,\n      });\n    });\n\n    it(\"should return number of promises equal to unique urls\", async () => {\n      sandbox.stub(feed.cache, \"get\").returns(Promise.resolve(fakeCache));\n      sandbox.stub(global.Promise, \"all\").resolves();\n      fakeDiscoveryStream = {\n        DiscoveryStream: {\n          layout: [\n            {\n              components: [\n                { feed: { url: \"foo.com\" } },\n                { feed: { url: \"bar.com\" } },\n              ],\n            },\n            { components: [{ feed: { url: \"foo.com\" } }] },\n            {},\n            { components: [{ feed: { url: \"baz.com\" } }] },\n          ],\n        },\n      };\n      feed.store.getState.returns(fakeDiscoveryStream);\n\n      await feed.loadComponentFeeds(feed.store.dispatch);\n\n      assert.calledOnce(global.Promise.all);\n      const { args } = global.Promise.all.firstCall;\n      assert.equal(args[0].length, 3);\n    });\n\n    it(\"should not record the request time if no fetch request was issued\", async () => {\n      const fakeComponents = { components: [] };\n      const fakeLayout = [fakeComponents, { components: [{}] }, {}];\n      fakeDiscoveryStream = { DiscoveryStream: { layout: fakeLayout } };\n      fakeCache = {\n        feeds: { \"foo.com\": { lastUpdated: Date.now(), data: \"data\" } },\n      };\n      sandbox.stub(feed.cache, \"get\").returns(Promise.resolve(fakeCache));\n      feed.componentFeedRequestTime = undefined;\n\n      await feed.loadComponentFeeds(feed.store.dispatch);\n\n      assert.isUndefined(feed.componentFeedRequestTime);\n    });\n  });\n\n  describe(\"#getComponentFeed\", () => {\n    it(\"should fetch fresh feed data if cache is empty\", async () => {\n      const fakeCache = {};\n      sandbox.stub(feed.cache, \"get\").returns(Promise.resolve(fakeCache));\n      sandbox.stub(feed, \"rotate\").callsFake(val => val);\n      sandbox\n        .stub(feed, \"scoreItems\")\n        .callsFake(val => ({ data: val, filtered: [] }));\n      sandbox.stub(feed, \"fetchFromEndpoint\").resolves({\n        recommendations: \"data\",\n        settings: {\n          recsExpireTime: 1,\n        },\n      });\n\n      const feedResp = await feed.getComponentFeed(\"foo.com\");\n\n      assert.equal(feedResp.data.recommendations, \"data\");\n    });\n    it(\"should fetch fresh feed data if cache is old\", async () => {\n      const fakeCache = { feeds: { \"foo.com\": { lastUpdated: Date.now() } } };\n      sandbox.stub(feed.cache, \"get\").returns(Promise.resolve(fakeCache));\n      sandbox.stub(feed, \"fetchFromEndpoint\").resolves({\n        recommendations: \"data\",\n        settings: {\n          recsExpireTime: 1,\n        },\n      });\n      sandbox.stub(feed, \"rotate\").callsFake(val => val);\n      sandbox\n        .stub(feed, \"scoreItems\")\n        .callsFake(val => ({ data: val, filtered: [] }));\n      clock.tick(THIRTY_MINUTES + 1);\n\n      const feedResp = await feed.getComponentFeed(\"foo.com\");\n\n      assert.equal(feedResp.data.recommendations, \"data\");\n    });\n    it(\"should return feed data from cache if it is fresh\", async () => {\n      const fakeCache = {\n        feeds: { \"foo.com\": { lastUpdated: Date.now(), data: \"data\" } },\n      };\n      sandbox.stub(feed.cache, \"get\").resolves(fakeCache);\n      sandbox.stub(feed, \"fetchFromEndpoint\").resolves(\"old data\");\n      clock.tick(THIRTY_MINUTES - 1);\n\n      const feedResp = await feed.getComponentFeed(\"foo.com\");\n\n      assert.equal(feedResp.data, \"data\");\n    });\n    it(\"should return null if no response was received\", async () => {\n      sandbox.stub(feed, \"fetchFromEndpoint\").resolves(null);\n\n      const feedResp = await feed.getComponentFeed(\"foo.com\");\n\n      assert.deepEqual(feedResp, { data: { status: \"failed\" } });\n    });\n  });\n\n  describe(\"#loadSpocs\", () => {\n    beforeEach(() => {\n      feed._prefCache = {\n        config: {\n          api_key_pref: \"\",\n        },\n      };\n      Object.defineProperty(feed, \"showSpocs\", { get: () => true });\n    });\n    it(\"should not fetch or update cache if no spocs endpoint is defined\", async () => {\n      feed.store.dispatch(\n        ac.BroadcastToContent({\n          type: at.DISCOVERY_STREAM_SPOCS_ENDPOINT,\n          data: \"\",\n        })\n      );\n\n      sandbox.spy(feed.cache, \"set\");\n\n      await feed.loadSpocs(feed.store.dispatch);\n\n      assert.notCalled(global.fetch);\n      assert.notCalled(feed.cache.set);\n    });\n    it(\"should fetch fresh spocs data if cache is empty\", async () => {\n      sandbox.stub(feed.cache, \"get\").returns(Promise.resolve());\n      sandbox.stub(feed, \"fetchFromEndpoint\").resolves({ placement: \"data\" });\n      sandbox.stub(feed.cache, \"set\").returns(Promise.resolve());\n\n      await feed.loadSpocs(feed.store.dispatch);\n\n      assert.calledWith(feed.cache.set, \"spocs\", {\n        spocs: { placement: \"data\" },\n        lastUpdated: 0,\n      });\n      assert.equal(\n        feed.store.getState().DiscoveryStream.spocs.data.placement,\n        \"data\"\n      );\n    });\n    it(\"should fetch fresh data if cache is old\", async () => {\n      const cachedSpoc = {\n        spocs: { placement: \"old\" },\n        lastUpdated: Date.now(),\n      };\n      const cachedData = { spocs: cachedSpoc };\n      sandbox.stub(feed.cache, \"get\").returns(Promise.resolve(cachedData));\n      sandbox.stub(feed, \"fetchFromEndpoint\").resolves({ placement: \"new\" });\n      sandbox.stub(feed.cache, \"set\").returns(Promise.resolve());\n      clock.tick(THIRTY_MINUTES + 1);\n\n      await feed.loadSpocs(feed.store.dispatch);\n\n      assert.equal(\n        feed.store.getState().DiscoveryStream.spocs.data.placement,\n        \"new\"\n      );\n    });\n    it(\"should return spoc data from cache if it is fresh\", async () => {\n      const cachedSpoc = {\n        spocs: { placement: \"old\" },\n        lastUpdated: Date.now(),\n      };\n      const cachedData = { spocs: cachedSpoc };\n      sandbox.stub(feed.cache, \"get\").returns(Promise.resolve(cachedData));\n      sandbox.stub(feed, \"fetchFromEndpoint\").resolves({ placement: \"new\" });\n      sandbox.stub(feed.cache, \"set\").returns(Promise.resolve());\n      clock.tick(THIRTY_MINUTES - 1);\n\n      await feed.loadSpocs(feed.store.dispatch);\n\n      assert.equal(\n        feed.store.getState().DiscoveryStream.spocs.data.placement,\n        \"old\"\n      );\n    });\n    it(\"should properly transform spocs using placements\", async () => {\n      sandbox.stub(feed.cache, \"get\").returns(Promise.resolve());\n      sandbox\n        .stub(feed, \"fetchFromEndpoint\")\n        .resolves({ spocs: [{ id: \"data\" }] });\n      sandbox.stub(feed.cache, \"set\").returns(Promise.resolve());\n\n      await feed.loadSpocs(feed.store.dispatch);\n\n      assert.calledWith(feed.cache.set, \"spocs\", {\n        spocs: { spocs: [{ id: \"data\", min_score: 0, score: 1 }] },\n        lastUpdated: 0,\n      });\n\n      assert.deepEqual(\n        feed.store.getState().DiscoveryStream.spocs.data.spocs[0],\n        { id: \"data\", min_score: 0, score: 1 }\n      );\n    });\n  });\n\n  describe(\"#showSpocs\", () => {\n    it(\"should return false from showSpocs if user pref showSponsored is false\", async () => {\n      feed.store.getState = () => ({\n        Prefs: { values: { showSponsored: false } },\n      });\n      Object.defineProperty(feed, \"config\", {\n        get: () => ({ show_spocs: true }),\n      });\n\n      assert.isFalse(feed.showSpocs);\n    });\n    it(\"should return false from showSpocs if DiscoveryStream pref show_spocs is false\", async () => {\n      feed.store.getState = () => ({\n        Prefs: { values: { showSponsored: true } },\n      });\n      Object.defineProperty(feed, \"config\", {\n        get: () => ({ show_spocs: false }),\n      });\n\n      assert.isFalse(feed.showSpocs);\n    });\n    it(\"should return true from showSpocs if both prefs are true\", async () => {\n      feed.store.getState = () => ({\n        Prefs: { values: { showSponsored: true } },\n      });\n      Object.defineProperty(feed, \"config\", {\n        get: () => ({ show_spocs: true }),\n      });\n\n      assert.isTrue(feed.showSpocs);\n    });\n  });\n\n  describe(\"#clearSpocs\", () => {\n    it(\"should not fail with no endpoint\", async () => {\n      sandbox.stub(feed.store, \"getState\").returns({\n        Prefs: {\n          values: { \"discoverystream.endpointSpocsClear\": null },\n        },\n      });\n      sandbox.stub(feed, \"fetchFromEndpoint\").resolves(null);\n\n      await feed.clearSpocs();\n\n      assert.notCalled(feed.fetchFromEndpoint);\n    });\n    it(\"should call DELETE with endpoint\", async () => {\n      sandbox.stub(feed.store, \"getState\").returns({\n        Prefs: {\n          values: {\n            \"discoverystream.endpointSpocsClear\": \"https://spocs/user\",\n          },\n        },\n      });\n      sandbox.stub(feed, \"fetchFromEndpoint\").resolves(null);\n      feed._impressionId = \"1234\";\n\n      await feed.clearSpocs();\n\n      assert.equal(\n        feed.fetchFromEndpoint.firstCall.args[0],\n        \"https://spocs/user\"\n      );\n      assert.equal(feed.fetchFromEndpoint.firstCall.args[1].method, \"DELETE\");\n      assert.equal(\n        feed.fetchFromEndpoint.firstCall.args[1].body,\n        '{\"pocket_id\":\"1234\"}'\n      );\n    });\n  });\n\n  describe(\"#rotate\", () => {\n    it(\"should move seen first story to the back of the response\", () => {\n      const recsExpireTime = 5600;\n      const feedResponse = {\n        recommendations: [\n          {\n            id: \"first\",\n          },\n          {\n            id: \"second\",\n          },\n          {\n            id: \"third\",\n          },\n          {\n            id: \"fourth\",\n          },\n        ],\n        settings: {\n          recsExpireTime,\n        },\n      };\n      const fakeImpressions = {\n        first: Date.now() - recsExpireTime * 1000,\n        third: Date.now(),\n      };\n      sandbox.stub(feed, \"readDataPref\").returns(fakeImpressions);\n\n      const result = feed.rotate(\n        feedResponse.recommendations,\n        feedResponse.settings.recsExpireTime\n      );\n\n      assert.equal(result[3].id, \"first\");\n    });\n  });\n\n  describe(\"#resetCache\", () => {\n    it(\"should set .layout, .feeds .spocs and .affinities to {\", async () => {\n      sandbox.stub(feed.cache, \"set\").returns(Promise.resolve());\n\n      await feed.resetCache();\n\n      assert.callCount(feed.cache.set, 4);\n      const firstCall = feed.cache.set.getCall(0);\n      const secondCall = feed.cache.set.getCall(1);\n      const thirdCall = feed.cache.set.getCall(2);\n      const fourthCall = feed.cache.set.getCall(3);\n      assert.deepEqual(firstCall.args, [\"layout\", {}]);\n      assert.deepEqual(secondCall.args, [\"feeds\", {}]);\n      assert.deepEqual(thirdCall.args, [\"spocs\", {}]);\n      assert.deepEqual(fourthCall.args, [\"affinities\", {}]);\n    });\n  });\n\n  describe(\"#transform\", () => {\n    it(\"should return initial data if spocs are empty\", () => {\n      const { data: result } = feed.transform({ spocs: [] });\n\n      assert.equal(result.spocs.length, 0);\n    });\n    it(\"should sort based on item_score\", () => {\n      const { data: result } = feed.transform([\n        { id: 2, flight_id: 2, item_score: 0.8, min_score: 0.1 },\n        { id: 3, flight_id: 3, item_score: 0.7, min_score: 0.1 },\n        { id: 1, flight_id: 1, item_score: 0.9, min_score: 0.1 },\n      ]);\n\n      assert.deepEqual(result, [\n        { id: 1, flight_id: 1, item_score: 0.9, score: 0.9, min_score: 0.1 },\n        { id: 2, flight_id: 2, item_score: 0.8, score: 0.8, min_score: 0.1 },\n        { id: 3, flight_id: 3, item_score: 0.7, score: 0.7, min_score: 0.1 },\n      ]);\n    });\n    it(\"should remove items with scores lower than min_score\", () => {\n      const { data: result, filtered } = feed.transform([\n        { id: 2, flight_id: 2, item_score: 0.8, min_score: 0.9 },\n        { id: 3, flight_id: 3, item_score: 0.7, min_score: 0.7 },\n        { id: 1, flight_id: 1, item_score: 0.9, min_score: 0.8 },\n      ]);\n\n      assert.deepEqual(result, [\n        { id: 1, flight_id: 1, item_score: 0.9, score: 0.9, min_score: 0.8 },\n        { id: 3, flight_id: 3, item_score: 0.7, score: 0.7, min_score: 0.7 },\n      ]);\n\n      assert.deepEqual(filtered.below_min_score, [\n        { id: 2, flight_id: 2, item_score: 0.8, min_score: 0.9, score: 0.8 },\n      ]);\n    });\n    it(\"should add a score prop to spocs\", () => {\n      const { data: result } = feed.transform([\n        { flight_id: 1, item_score: 0.9, min_score: 0.1 },\n      ]);\n\n      assert.equal(result[0].score, 0.9);\n    });\n    it(\"should filter out duplicate flights\", () => {\n      const { data: result, filtered } = feed.transform([\n        { id: 1, flight_id: 2, item_score: 0.8, min_score: 0.1 },\n        { id: 2, flight_id: 3, item_score: 0.6, min_score: 0.1 },\n        { id: 3, flight_id: 1, item_score: 0.9, min_score: 0.1 },\n        { id: 4, flight_id: 3, item_score: 0.7, min_score: 0.1 },\n        { id: 5, flight_id: 1, item_score: 0.9, min_score: 0.1 },\n      ]);\n\n      assert.deepEqual(result, [\n        { id: 3, flight_id: 1, item_score: 0.9, score: 0.9, min_score: 0.1 },\n        { id: 1, flight_id: 2, item_score: 0.8, score: 0.8, min_score: 0.1 },\n        { id: 4, flight_id: 3, item_score: 0.7, score: 0.7, min_score: 0.1 },\n      ]);\n\n      assert.deepEqual(filtered.flight_duplicate, [\n        { id: 5, flight_id: 1, item_score: 0.9, min_score: 0.1, score: 0.9 },\n        { id: 2, flight_id: 3, item_score: 0.6, min_score: 0.1, score: 0.6 },\n      ]);\n    });\n    it(\"should filter out duplicate flight while using spocs_per_domain\", () => {\n      sandbox.stub(feed.store, \"getState\").returns({\n        DiscoveryStream: {\n          spocs: { spocs_per_domain: 2 },\n        },\n      });\n\n      const { data: result, filtered } = feed.transform([\n        { id: 1, flight_id: 2, item_score: 0.8, min_score: 0.1 },\n        { id: 2, flight_id: 3, item_score: 0.6, min_score: 0.1 },\n        { id: 3, flight_id: 1, item_score: 0.6, min_score: 0.1 },\n        { id: 4, flight_id: 3, item_score: 0.7, min_score: 0.1 },\n        { id: 5, flight_id: 1, item_score: 0.9, min_score: 0.1 },\n        { id: 6, flight_id: 2, item_score: 0.6, min_score: 0.1 },\n        { id: 7, flight_id: 3, item_score: 0.7, min_score: 0.1 },\n        { id: 8, flight_id: 1, item_score: 0.8, min_score: 0.1 },\n        { id: 9, flight_id: 3, item_score: 0.7, min_score: 0.1 },\n        { id: 10, flight_id: 1, item_score: 0.8, min_score: 0.1 },\n      ]);\n\n      assert.deepEqual(result, [\n        { id: 5, flight_id: 1, item_score: 0.9, score: 0.9, min_score: 0.1 },\n        { id: 1, flight_id: 2, item_score: 0.8, score: 0.8, min_score: 0.1 },\n        { id: 8, flight_id: 1, item_score: 0.8, score: 0.8, min_score: 0.1 },\n        { id: 4, flight_id: 3, item_score: 0.7, score: 0.7, min_score: 0.1 },\n        { id: 7, flight_id: 3, item_score: 0.7, score: 0.7, min_score: 0.1 },\n        { id: 6, flight_id: 2, item_score: 0.6, score: 0.6, min_score: 0.1 },\n      ]);\n\n      assert.deepEqual(filtered.flight_duplicate, [\n        { id: 10, flight_id: 1, item_score: 0.8, min_score: 0.1, score: 0.8 },\n        { id: 9, flight_id: 3, item_score: 0.7, min_score: 0.1, score: 0.7 },\n        { id: 2, flight_id: 3, item_score: 0.6, min_score: 0.1, score: 0.6 },\n        { id: 3, flight_id: 1, item_score: 0.6, min_score: 0.1, score: 0.6 },\n      ]);\n    });\n  });\n\n  describe(\"#filterBlocked\", () => {\n    it(\"should return initial data if spocs are empty\", () => {\n      const { data: result } = feed.filterBlocked([]);\n\n      assert.equal(result.length, 0);\n    });\n    it(\"should return initial data if links are not blocked\", () => {\n      const { data: result } = feed.filterBlocked([\n        { url: \"https://foo.com\" },\n        { url: \"test.com\" },\n      ]);\n      assert.equal(result.length, 2);\n    });\n    it(\"should return filtered out based on blockedlist\", () => {\n      fakeNewTabUtils.blockedLinks.links = [{ url: \"https://foo.com\" }];\n      fakeNewTabUtils.blockedLinks.isBlocked = site =>\n        fakeNewTabUtils.blockedLinks.links[0].url === site.url;\n\n      const { data: result, filtered } = feed.filterBlocked([\n        { id: 1, url: \"https://foo.com\" },\n        { id: 2, url: \"test.com\" },\n      ]);\n\n      assert.lengthOf(result, 1);\n      assert.equal(result[0].url, \"test.com\");\n      assert.notInclude(result, fakeNewTabUtils.blockedLinks.links[0]);\n      assert.deepEqual(filtered, [{ id: 1, url: \"https://foo.com\" }]);\n    });\n    it(\"should return initial recommendations data if links are not blocked\", () => {\n      const { data: result } = feed.filterBlocked([\n        { url: \"https://foo.com\" },\n        { url: \"test.com\" },\n      ]);\n      assert.equal(result.length, 2);\n    });\n    it(\"filterRecommendations based on blockedlist by passing feed data\", () => {\n      fakeNewTabUtils.blockedLinks.links = [{ url: \"https://foo.com\" }];\n      fakeNewTabUtils.blockedLinks.isBlocked = site =>\n        fakeNewTabUtils.blockedLinks.links[0].url === site.url;\n\n      const result = feed.filterRecommendations({\n        lastUpdated: 4,\n        data: {\n          recommendations: [{ url: \"https://foo.com\" }, { url: \"test.com\" }],\n        },\n      });\n\n      assert.equal(result.lastUpdated, 4);\n      assert.lengthOf(result.data.recommendations, 1);\n      assert.equal(result.data.recommendations[0].url, \"test.com\");\n      assert.notInclude(\n        result.data.recommendations,\n        fakeNewTabUtils.blockedLinks.links[0]\n      );\n    });\n  });\n\n  describe(\"#frequencyCapSpocs\", () => {\n    it(\"should return filtered out spocs based on frequency caps\", () => {\n      const fakeSpocs = [\n        {\n          id: 1,\n          flight_id: \"seen\",\n          caps: {\n            lifetime: 3,\n            flight: {\n              count: 1,\n              period: 1,\n            },\n          },\n        },\n        {\n          id: 2,\n          flight_id: \"not-seen\",\n          caps: {\n            lifetime: 3,\n            flight: {\n              count: 1,\n              period: 1,\n            },\n          },\n        },\n      ];\n      const fakeImpressions = {\n        seen: [Date.now() - 1],\n      };\n      sandbox.stub(feed, \"readDataPref\").returns(fakeImpressions);\n\n      const { data: result, filtered } = feed.frequencyCapSpocs(fakeSpocs);\n\n      assert.equal(result.length, 1);\n      assert.equal(result[0].flight_id, \"not-seen\");\n      assert.deepEqual(filtered, [fakeSpocs[0]]);\n    });\n    it(\"should return simple structure and do nothing with no spocs\", () => {\n      const { data: result, filtered } = feed.frequencyCapSpocs([]);\n\n      assert.equal(result.length, 0);\n      assert.equal(filtered.length, 0);\n    });\n  });\n\n  describe(\"#migrateFlightId\", () => {\n    it(\"should migrate campaign to flight if no flight exists\", () => {\n      const fakeSpocs = [\n        {\n          id: 1,\n          campaign_id: \"campaign\",\n          caps: {\n            lifetime: 3,\n            campaign: {\n              count: 1,\n              period: 1,\n            },\n          },\n        },\n      ];\n      const { data: result } = feed.migrateFlightId(fakeSpocs);\n\n      assert.deepEqual(result[0], {\n        id: 1,\n        flight_id: \"campaign\",\n        campaign_id: \"campaign\",\n        caps: {\n          lifetime: 3,\n          flight: {\n            count: 1,\n            period: 1,\n          },\n          campaign: {\n            count: 1,\n            period: 1,\n          },\n        },\n      });\n    });\n    it(\"should not migrate campaign to flight if caps or id don't exist\", () => {\n      const fakeSpocs = [{ id: 1 }];\n      const { data: result } = feed.migrateFlightId(fakeSpocs);\n\n      assert.deepEqual(result[0], { id: 1 });\n    });\n    it(\"should return simple structure and do nothing with no spocs\", () => {\n      const { data: result } = feed.migrateFlightId([]);\n\n      assert.equal(result.length, 0);\n    });\n  });\n\n  describe(\"#isBelowFrequencyCap\", () => {\n    it(\"should return true if there are no flight impressions\", () => {\n      const fakeImpressions = {\n        seen: [Date.now() - 1],\n      };\n      const fakeSpoc = {\n        flight_id: \"not-seen\",\n        caps: {\n          lifetime: 3,\n          flight: {\n            count: 1,\n            period: 1,\n          },\n        },\n      };\n\n      const result = feed.isBelowFrequencyCap(fakeImpressions, fakeSpoc);\n\n      assert.isTrue(result);\n    });\n    it(\"should return true if there are no flight caps\", () => {\n      const fakeImpressions = {\n        seen: [Date.now() - 1],\n      };\n      const fakeSpoc = {\n        flight_id: \"seen\",\n        caps: {\n          lifetime: 3,\n        },\n      };\n\n      const result = feed.isBelowFrequencyCap(fakeImpressions, fakeSpoc);\n\n      assert.isTrue(result);\n    });\n\n    it(\"should return false if lifetime cap is hit\", () => {\n      const fakeImpressions = {\n        seen: [Date.now() - 1],\n      };\n      const fakeSpoc = {\n        flight_id: \"seen\",\n        caps: {\n          lifetime: 1,\n          flight: {\n            count: 3,\n            period: 1,\n          },\n        },\n      };\n\n      const result = feed.isBelowFrequencyCap(fakeImpressions, fakeSpoc);\n\n      assert.isFalse(result);\n    });\n\n    it(\"should return false if time based cap is hit\", () => {\n      const fakeImpressions = {\n        seen: [Date.now() - 1],\n      };\n      const fakeSpoc = {\n        flight_id: \"seen\",\n        caps: {\n          lifetime: 3,\n          flight: {\n            count: 1,\n            period: 1,\n          },\n        },\n      };\n\n      const result = feed.isBelowFrequencyCap(fakeImpressions, fakeSpoc);\n\n      assert.isFalse(result);\n    });\n  });\n\n  describe(\"#retryFeed\", () => {\n    it(\"should retry a feed fetch\", async () => {\n      sandbox.stub(feed, \"getComponentFeed\").returns(Promise.resolve({}));\n      sandbox.stub(feed, \"filterRecommendations\").returns({});\n      sandbox.spy(feed.store, \"dispatch\");\n\n      await feed.retryFeed({ url: \"https://feed.com\" });\n\n      assert.calledOnce(feed.getComponentFeed);\n      assert.calledOnce(feed.filterRecommendations);\n      assert.calledOnce(feed.store.dispatch);\n      assert.equal(\n        feed.store.dispatch.firstCall.args[0].type,\n        \"DISCOVERY_STREAM_FEED_UPDATE\"\n      );\n      assert.deepEqual(feed.store.dispatch.firstCall.args[0].data, {\n        feed: {},\n        url: \"https://feed.com\",\n      });\n    });\n  });\n\n  describe(\"#recordFlightImpression\", () => {\n    it(\"should return false if time based cap is hit\", () => {\n      sandbox.stub(feed, \"readDataPref\").returns({});\n      sandbox.stub(feed, \"writeDataPref\").returns();\n\n      feed.recordFlightImpression(\"seen\");\n\n      assert.calledWith(feed.writeDataPref, SPOC_IMPRESSION_TRACKING_PREF, {\n        seen: [0],\n      });\n    });\n  });\n\n  describe(\"#recordBlockFlightId\", () => {\n    it(\"should call writeDataPref with new flight id added\", () => {\n      sandbox.stub(feed, \"readDataPref\").returns({ \"1234\": 1 });\n      sandbox.stub(feed, \"writeDataPref\").returns();\n\n      feed.recordBlockFlightId(\"5678\");\n\n      assert.calledOnce(feed.readDataPref);\n      assert.calledWith(feed.writeDataPref, \"discoverystream.flight.blocks\", {\n        \"1234\": 1,\n        \"5678\": 1,\n      });\n    });\n  });\n\n  describe(\"#cleanUpFlightImpressionPref\", () => {\n    it(\"should remove flight-3 because it is no longer being used\", async () => {\n      const fakeSpocs = {\n        spocs: [\n          {\n            flight_id: \"flight-1\",\n            caps: {\n              lifetime: 3,\n              flight: {\n                count: 1,\n                period: 1,\n              },\n            },\n          },\n          {\n            flight_id: \"flight-2\",\n            caps: {\n              lifetime: 3,\n              flight: {\n                count: 1,\n                period: 1,\n              },\n            },\n          },\n        ],\n      };\n      const fakeImpressions = {\n        \"flight-2\": [Date.now() - 1],\n        \"flight-3\": [Date.now() - 1],\n      };\n      sandbox.stub(feed, \"readDataPref\").returns(fakeImpressions);\n      sandbox.stub(feed, \"writeDataPref\").returns();\n\n      feed.cleanUpFlightImpressionPref(fakeSpocs);\n\n      assert.calledWith(feed.writeDataPref, SPOC_IMPRESSION_TRACKING_PREF, {\n        \"flight-2\": [-1],\n      });\n    });\n  });\n\n  describe(\"#recordTopRecImpressions\", () => {\n    it(\"should add a rec id to the rec impression pref\", () => {\n      sandbox.stub(feed, \"readDataPref\").returns({});\n      sandbox.stub(feed, \"writeDataPref\");\n\n      feed.recordTopRecImpressions(\"rec\");\n\n      assert.calledWith(feed.writeDataPref, REC_IMPRESSION_TRACKING_PREF, {\n        rec: 0,\n      });\n    });\n    it(\"should not add an impression if it already exists\", () => {\n      sandbox.stub(feed, \"readDataPref\").returns({ rec: 4 });\n      sandbox.stub(feed, \"writeDataPref\");\n\n      feed.recordTopRecImpressions(\"rec\");\n\n      assert.notCalled(feed.writeDataPref);\n    });\n  });\n\n  describe(\"#cleanUpTopRecImpressionPref\", () => {\n    it(\"should remove recs no longer being used\", () => {\n      const newFeeds = {\n        \"https://foo.com\": {\n          data: {\n            recommendations: [\n              {\n                id: \"rec1\",\n              },\n              {\n                id: \"rec2\",\n              },\n            ],\n          },\n        },\n        \"https://bar.com\": {\n          data: {\n            recommendations: [\n              {\n                id: \"rec3\",\n              },\n              {\n                id: \"rec4\",\n              },\n            ],\n          },\n        },\n      };\n      const fakeImpressions = {\n        rec2: Date.now() - 1,\n        rec3: Date.now() - 1,\n        rec5: Date.now() - 1,\n      };\n      sandbox.stub(feed, \"readDataPref\").returns(fakeImpressions);\n      sandbox.stub(feed, \"writeDataPref\").returns();\n\n      feed.cleanUpTopRecImpressionPref(newFeeds);\n\n      assert.calledWith(feed.writeDataPref, REC_IMPRESSION_TRACKING_PREF, {\n        rec2: -1,\n        rec3: -1,\n      });\n    });\n  });\n\n  describe(\"#writeDataPref\", () => {\n    it(\"should call Services.prefs.setStringPref\", () => {\n      sandbox.spy(feed.store, \"dispatch\");\n      const fakeImpressions = {\n        foo: [Date.now() - 1],\n        bar: [Date.now() - 1],\n      };\n\n      feed.writeDataPref(SPOC_IMPRESSION_TRACKING_PREF, fakeImpressions);\n\n      assert.calledWithMatch(feed.store.dispatch, {\n        data: {\n          name: SPOC_IMPRESSION_TRACKING_PREF,\n          value: JSON.stringify(fakeImpressions),\n        },\n        type: at.SET_PREF,\n      });\n    });\n  });\n\n  describe(\"#readDataPref\", () => {\n    it(\"should return what's in Services.prefs.getStringPref\", () => {\n      const fakeImpressions = {\n        foo: [Date.now() - 1],\n        bar: [Date.now() - 1],\n      };\n      setPref(SPOC_IMPRESSION_TRACKING_PREF, fakeImpressions);\n\n      const result = feed.readDataPref(SPOC_IMPRESSION_TRACKING_PREF);\n\n      assert.deepEqual(result, fakeImpressions);\n    });\n  });\n\n  describe(\"#onAction: DISCOVERY_STREAM_IMPRESSION_STATS\", () => {\n    it(\"should call recordTopRecImpressions from DISCOVERY_STREAM_IMPRESSION_STATS\", async () => {\n      sandbox.stub(feed, \"recordTopRecImpressions\").returns();\n      await feed.onAction({\n        type: at.DISCOVERY_STREAM_IMPRESSION_STATS,\n        data: { tiles: [{ id: \"seen\" }] },\n      });\n\n      assert.calledWith(feed.recordTopRecImpressions, \"seen\");\n    });\n  });\n\n  describe(\"#onAction: DISCOVERY_STREAM_SPOC_IMPRESSION\", () => {\n    beforeEach(() => {\n      const data = {\n        spocs: [\n          {\n            id: 1,\n            flight_id: \"seen\",\n            caps: {\n              lifetime: 3,\n              flight: {\n                count: 1,\n                period: 1,\n              },\n            },\n          },\n          {\n            id: 2,\n            flight_id: \"not-seen\",\n            caps: {\n              lifetime: 3,\n              flight: {\n                count: 1,\n                period: 1,\n              },\n            },\n          },\n        ],\n      };\n      sandbox.stub(feed.store, \"getState\").returns({\n        DiscoveryStream: {\n          spocs: {\n            data,\n            spocs_per_domain: 2,\n          },\n        },\n      });\n    });\n\n    it(\"should call dispatch to ac.AlsoToPreloaded with filtered spoc data\", async () => {\n      Object.defineProperty(feed, \"showSpocs\", { get: () => true });\n      const fakeImpressions = {\n        seen: [Date.now() - 1],\n      };\n      const result = {\n        spocs: [\n          {\n            id: 2,\n            flight_id: \"not-seen\",\n            caps: {\n              lifetime: 3,\n              flight: {\n                count: 1,\n                period: 1,\n              },\n            },\n          },\n        ],\n      };\n      const spocFillResult = [\n        {\n          id: 1,\n          reason: \"frequency_cap\",\n          displayed: 0,\n          full_recalc: 0,\n        },\n      ];\n\n      sandbox.stub(feed, \"recordFlightImpression\").returns();\n      sandbox.stub(feed, \"readDataPref\").returns(fakeImpressions);\n      sandbox.spy(feed.store, \"dispatch\");\n\n      await feed.onAction({\n        type: at.DISCOVERY_STREAM_SPOC_IMPRESSION,\n        data: { flight_id: \"seen\" },\n      });\n\n      assert.deepEqual(\n        feed.store.dispatch.secondCall.args[0].data.spocs,\n        result\n      );\n      assert.deepEqual(\n        feed.store.dispatch.thirdCall.args[0].data.spoc_fills,\n        spocFillResult\n      );\n    });\n    it(\"should not call dispatch to ac.AlsoToPreloaded if spocs were not changed by frequency capping\", async () => {\n      Object.defineProperty(feed, \"showSpocs\", { get: () => true });\n      const fakeImpressions = {};\n      sandbox.stub(feed, \"recordFlightImpression\").returns();\n      sandbox.stub(feed, \"readDataPref\").returns(fakeImpressions);\n      sandbox.spy(feed.store, \"dispatch\");\n\n      await feed.onAction({\n        type: at.DISCOVERY_STREAM_SPOC_IMPRESSION,\n        data: { flight_id: \"seen\" },\n      });\n\n      assert.notCalled(feed.store.dispatch);\n    });\n    it(\"should attempt feq cap on valid spocs with placements on impression\", async () => {\n      sandbox.restore();\n      Object.defineProperty(feed, \"showSpocs\", { get: () => true });\n      const fakeImpressions = {};\n      sandbox.stub(feed, \"recordFlightImpression\").returns();\n      sandbox.stub(feed, \"readDataPref\").returns(fakeImpressions);\n      sandbox.spy(feed.store, \"dispatch\");\n      sandbox.spy(feed, \"frequencyCapSpocs\");\n\n      const data = {\n        spocs: [\n          {\n            id: 2,\n            flight_id: \"seen-2\",\n            caps: {\n              lifetime: 3,\n              flight: {\n                count: 1,\n                period: 1,\n              },\n            },\n          },\n        ],\n      };\n      sandbox.stub(feed.store, \"getState\").returns({\n        DiscoveryStream: {\n          spocs: {\n            data,\n            placements: [{ name: \"spocs\" }, { name: \"notSpocs\" }],\n            spocs_per_domain: 1,\n          },\n        },\n      });\n\n      await feed.onAction({\n        type: at.DISCOVERY_STREAM_SPOC_IMPRESSION,\n        data: { flight_id: \"doesn't matter\" },\n      });\n\n      assert.calledOnce(feed.frequencyCapSpocs);\n      assert.calledWith(feed.frequencyCapSpocs, data.spocs);\n    });\n  });\n\n  describe(\"#onAction: PLACES_LINK_BLOCKED\", () => {\n    beforeEach(() => {\n      const data = {\n        spocs: [\n          {\n            id: 1,\n            flight_id: \"foo\",\n            url: \"foo.com\",\n          },\n          {\n            id: 2,\n            flight_id: \"bar\",\n            url: \"bar.com\",\n          },\n        ],\n      };\n      sandbox.stub(feed.store, \"getState\").returns({\n        DiscoveryStream: {\n          spocs: {\n            data,\n            placements: [{ name: \"spocs\" }],\n          },\n        },\n      });\n    });\n\n    it(\"should call dispatch with the SPOCS Fill if found a blocked spoc\", async () => {\n      Object.defineProperty(feed, \"showSpocs\", { get: () => true });\n      const spocFillResult = [\n        {\n          id: 1,\n          reason: \"blocked_by_user\",\n          displayed: 0,\n          full_recalc: 0,\n        },\n      ];\n\n      sandbox.spy(feed.store, \"dispatch\");\n\n      await feed.onAction({\n        type: at.PLACES_LINK_BLOCKED,\n        data: { url: \"foo.com\" },\n      });\n\n      assert.deepEqual(\n        feed.store.dispatch.firstCall.args[0].data.spoc_fills,\n        spocFillResult\n      );\n      assert.deepEqual(\n        feed.store.dispatch.secondCall.args[0].data.url,\n        \"foo.com\"\n      );\n    });\n    it(\"should not call dispatch with the SPOCS Fill if the blocked is not a SPOC\", async () => {\n      Object.defineProperty(feed, \"showSpocs\", { get: () => true });\n      sandbox.spy(feed.store, \"dispatch\");\n\n      await feed.onAction({\n        type: at.PLACES_LINK_BLOCKED,\n        data: { url: \"not_a_spoc.com\" },\n      });\n\n      assert.calledOnce(feed.store.dispatch);\n      assert.deepEqual(\n        feed.store.dispatch.firstCall.args[0].data.url,\n        \"not_a_spoc.com\"\n      );\n    });\n    it(\"should dispatch a DISCOVERY_STREAM_SPOC_BLOCKED for a blocked spoc\", async () => {\n      Object.defineProperty(feed, \"showSpocs\", { get: () => true });\n      sandbox.spy(feed.store, \"dispatch\");\n\n      await feed.onAction({\n        type: at.PLACES_LINK_BLOCKED,\n        data: { url: \"foo.com\" },\n      });\n\n      assert.equal(\n        feed.store.dispatch.thirdCall.args[0].type,\n        \"DISCOVERY_STREAM_SPOC_BLOCKED\"\n      );\n    });\n  });\n\n  describe(\"#onAction: BLOCK_URL\", () => {\n    it(\"should call recordBlockFlightId whith BLOCK_URL\", async () => {\n      sandbox.stub(feed, \"recordBlockFlightId\").returns();\n\n      await feed.onAction({\n        type: at.BLOCK_URL,\n        data: {\n          flight_id: \"1234\",\n        },\n      });\n\n      assert.calledWith(feed.recordBlockFlightId, \"1234\");\n    });\n  });\n\n  describe(\"#onAction: INIT\", () => {\n    it(\"should be .loaded=false before initialization\", () => {\n      assert.isFalse(feed.loaded);\n    });\n    it(\"should load data and set .loaded=true if config.enabled is true\", async () => {\n      sandbox.stub(feed.cache, \"set\").returns(Promise.resolve());\n      setPref(CONFIG_PREF_NAME, { enabled: true });\n      sandbox.stub(feed, \"loadLayout\").returns(Promise.resolve());\n      sandbox.stub(feed, \"reportCacheAge\").resolves();\n      sandbox.spy(feed, \"reportRequestTime\");\n\n      await feed.onAction({ type: at.INIT });\n\n      assert.calledOnce(feed.loadLayout);\n      assert.calledOnce(feed.reportCacheAge);\n      assert.calledOnce(feed.reportRequestTime);\n      assert.isTrue(feed.loaded);\n    });\n  });\n\n  describe(\"#onAction: DISCOVERY_STREAM_CONFIG_SET_VALUE\", async () => {\n    it(\"should add the new value to the pref without changing the existing values\", async () => {\n      sandbox.spy(feed.store, \"dispatch\");\n      setPref(CONFIG_PREF_NAME, { enabled: true, other: \"value\" });\n\n      await feed.onAction({\n        type: at.DISCOVERY_STREAM_CONFIG_SET_VALUE,\n        data: { name: \"layout_endpoint\", value: \"foo.com\" },\n      });\n\n      assert.calledWithMatch(feed.store.dispatch, {\n        data: {\n          name: CONFIG_PREF_NAME,\n          value: JSON.stringify({\n            enabled: true,\n            other: \"value\",\n            layout_endpoint: \"foo.com\",\n          }),\n        },\n        type: at.SET_PREF,\n      });\n    });\n  });\n\n  describe(\"#onAction: DISCOVERY_STREAM_CONFIG_RESET_DEFAULTS\", async () => {\n    it(\"Should dispatch CLEAR_PREF with pref name\", async () => {\n      sandbox.spy(feed.store, \"dispatch\");\n      await feed.onAction({\n        type: at.DISCOVERY_STREAM_CONFIG_RESET_DEFAULTS,\n      });\n\n      assert.calledWithMatch(feed.store.dispatch, {\n        data: {\n          name: CONFIG_PREF_NAME,\n        },\n        type: at.CLEAR_PREF,\n      });\n    });\n  });\n\n  describe(\"#onAction: DISCOVERY_STREAM_RETRY_FEED\", async () => {\n    it(\"should call retryFeed\", async () => {\n      sandbox.spy(feed, \"retryFeed\");\n      feed.onAction({\n        type: at.DISCOVERY_STREAM_RETRY_FEED,\n        data: { feed: { url: \"https://feed.com\" } },\n      });\n      assert.calledOnce(feed.retryFeed);\n      assert.calledWith(feed.retryFeed, { url: \"https://feed.com\" });\n    });\n  });\n\n  describe(\"#onAction: DISCOVERY_STREAM_CONFIG_CHANGE\", () => {\n    it(\"should call this.loadLayout if config.enabled changes to true \", async () => {\n      sandbox.stub(feed.cache, \"set\").returns(Promise.resolve());\n      // First initialize\n      await feed.onAction({ type: at.INIT });\n      assert.isFalse(feed.loaded);\n\n      // force clear cached pref value\n      feed._prefCache = {};\n      setPref(CONFIG_PREF_NAME, { enabled: true });\n\n      sandbox.stub(feed, \"resetCache\").returns(Promise.resolve());\n      sandbox.stub(feed, \"loadLayout\").returns(Promise.resolve());\n      await feed.onAction({ type: at.DISCOVERY_STREAM_CONFIG_CHANGE });\n\n      assert.calledOnce(feed.loadLayout);\n      assert.calledOnce(feed.resetCache);\n      assert.isTrue(feed.loaded);\n    });\n    it(\"should clear the cache if a config change happens and config.enabled is true\", async () => {\n      sandbox.stub(feed.cache, \"set\").returns(Promise.resolve());\n      // force clear cached pref value\n      feed._prefCache = {};\n      setPref(CONFIG_PREF_NAME, { enabled: true });\n\n      sandbox.stub(feed, \"resetCache\").returns(Promise.resolve());\n      await feed.onAction({ type: at.DISCOVERY_STREAM_CONFIG_CHANGE });\n\n      assert.calledOnce(feed.resetCache);\n    });\n    it(\"should dispatch DISCOVERY_STREAM_LAYOUT_RESET from DISCOVERY_STREAM_CONFIG_CHANGE\", async () => {\n      sandbox.stub(feed, \"resetDataPrefs\");\n      sandbox.stub(feed, \"resetCache\").resolves();\n      sandbox.stub(feed, \"enable\").resolves();\n      setPref(CONFIG_PREF_NAME, { enabled: true });\n      sandbox.spy(feed.store, \"dispatch\");\n\n      await feed.onAction({ type: at.DISCOVERY_STREAM_CONFIG_CHANGE });\n\n      assert.calledWithMatch(feed.store.dispatch, {\n        type: at.DISCOVERY_STREAM_LAYOUT_RESET,\n      });\n    });\n    it(\"should not call this.loadLayout if config.enabled changes to false\", async () => {\n      sandbox.stub(feed.cache, \"set\").returns(Promise.resolve());\n      // force clear cached pref value\n      feed._prefCache = {};\n      setPref(CONFIG_PREF_NAME, { enabled: true });\n\n      await feed.onAction({ type: at.INIT });\n      assert.isTrue(feed.loaded);\n\n      feed._prefCache = {};\n      setPref(CONFIG_PREF_NAME, { enabled: false });\n      sandbox.stub(feed, \"resetCache\").returns(Promise.resolve());\n      sandbox.stub(feed, \"loadLayout\").returns(Promise.resolve());\n      await feed.onAction({ type: at.DISCOVERY_STREAM_CONFIG_CHANGE });\n\n      assert.notCalled(feed.loadLayout);\n      assert.calledOnce(feed.resetCache);\n      assert.isFalse(feed.loaded);\n    });\n  });\n\n  describe(\"#onAction: UNINIT\", () => {\n    it(\"should reset pref cache\", async () => {\n      feed._prefCache = { cached: \"value\" };\n\n      await feed.onAction({ type: at.UNINIT });\n\n      assert.deepEqual(feed._prefCache, {});\n    });\n  });\n\n  describe(\"#onAction: PREF_CHANGED\", () => {\n    it(\"should update state.DiscoveryStream.config when the pref changes\", async () => {\n      setPref(CONFIG_PREF_NAME, {\n        enabled: true,\n        show_spocs: false,\n        layout_endpoint: \"foo\",\n      });\n\n      assert.deepEqual(feed.store.getState().DiscoveryStream.config, {\n        enabled: true,\n        show_spocs: false,\n        layout_endpoint: \"foo\",\n      });\n    });\n    it(\"should fire loadSpocs is showSponsored pref changes\", async () => {\n      sandbox.stub(feed, \"loadSpocs\").returns(Promise.resolve());\n\n      await feed.onAction({\n        type: at.PREF_CHANGED,\n        data: { name: \"showSponsored\" },\n      });\n\n      assert.calledOnce(feed.loadSpocs);\n    });\n    it(\"should call clearSpocs when sponsored content is turned off\", async () => {\n      sandbox.stub(feed, \"clearSpocs\").returns(Promise.resolve());\n\n      await feed.onAction({\n        type: at.PREF_CHANGED,\n        data: { name: \"showSponsored\", value: false },\n      });\n\n      assert.calledOnce(feed.clearSpocs);\n    });\n    it(\"should call clearSpocs when top stories is turned off\", async () => {\n      sandbox.stub(feed, \"clearSpocs\").returns(Promise.resolve());\n\n      await feed.onAction({\n        type: at.PREF_CHANGED,\n        data: { name: \"feeds.section.topstories\", value: false },\n      });\n\n      assert.calledOnce(feed.clearSpocs);\n    });\n  });\n\n  describe(\"#onAction: SYSTEM_TICK\", () => {\n    it(\"should not refresh if DiscoveryStream has not been loaded\", async () => {\n      sandbox.stub(feed, \"refreshAll\").resolves();\n      setPref(CONFIG_PREF_NAME, { enabled: true });\n\n      await feed.onAction({ type: at.SYSTEM_TICK });\n      assert.notCalled(feed.refreshAll);\n    });\n\n    it(\"should not refresh if no caches are expired\", async () => {\n      sandbox.stub(feed.cache, \"set\").resolves();\n      setPref(CONFIG_PREF_NAME, { enabled: true });\n\n      await feed.onAction({ type: at.INIT });\n\n      sandbox.stub(feed, \"checkIfAnyCacheExpired\").resolves(false);\n      sandbox.stub(feed, \"refreshAll\").resolves();\n\n      await feed.onAction({ type: at.SYSTEM_TICK });\n      assert.notCalled(feed.refreshAll);\n    });\n\n    it(\"should refresh if DiscoveryStream has been loaded at least once and a cache has expired\", async () => {\n      sandbox.stub(feed.cache, \"set\").resolves();\n      setPref(CONFIG_PREF_NAME, { enabled: true });\n\n      await feed.onAction({ type: at.INIT });\n\n      sandbox.stub(feed, \"checkIfAnyCacheExpired\").resolves(true);\n      sandbox.stub(feed, \"refreshAll\").resolves();\n\n      await feed.onAction({ type: at.SYSTEM_TICK });\n      assert.calledOnce(feed.refreshAll);\n    });\n\n    it(\"should refresh and not update open tabs if DiscoveryStream has been loaded at least once\", async () => {\n      sandbox.stub(feed.cache, \"set\").resolves();\n      setPref(CONFIG_PREF_NAME, { enabled: true });\n\n      await feed.onAction({ type: at.INIT });\n\n      sandbox.stub(feed, \"checkIfAnyCacheExpired\").resolves(true);\n      sandbox.stub(feed, \"refreshAll\").resolves();\n\n      await feed.onAction({ type: at.SYSTEM_TICK });\n      assert.calledWith(feed.refreshAll, { updateOpenTabs: false });\n    });\n  });\n\n  describe(\"#onAction: PREF_SHOW_SPONSORED\", () => {\n    it(\"should call loadSpocs when preference changes\", async () => {\n      sandbox.stub(feed, \"loadSpocs\").resolves();\n      sandbox.stub(feed.store, \"dispatch\");\n\n      await feed.onAction({\n        type: at.PREF_CHANGED,\n        data: { name: \"showSponsored\" },\n      });\n\n      assert.calledOnce(feed.loadSpocs);\n      const [dispatchFn] = feed.loadSpocs.firstCall.args;\n      dispatchFn({});\n      assert.calledWith(feed.store.dispatch, ac.BroadcastToContent({}));\n    });\n  });\n\n  describe(\"#isExpired\", () => {\n    it(\"should throw if the key is not valid\", () => {\n      assert.throws(() => {\n        feed.isExpired({}, \"foo\");\n      });\n    });\n    it(\"should return false for layout on startup for content under 1 week\", () => {\n      const layout = { lastUpdated: Date.now() };\n      const result = feed.isExpired({\n        cachedData: { layout },\n        key: \"layout\",\n        isStartup: true,\n      });\n\n      assert.isFalse(result);\n    });\n    it(\"should return true for layout for isStartup=false after 30 mins\", () => {\n      const layout = { lastUpdated: Date.now() };\n      clock.tick(THIRTY_MINUTES + 1);\n      const result = feed.isExpired({ cachedData: { layout }, key: \"layout\" });\n\n      assert.isTrue(result);\n    });\n    it(\"should return true for layout on startup for content over 1 week\", () => {\n      const layout = { lastUpdated: Date.now() };\n      clock.tick(ONE_WEEK + 1);\n      const result = feed.isExpired({\n        cachedData: { layout },\n        key: \"layout\",\n        isStartup: true,\n      });\n\n      assert.isTrue(result);\n    });\n    it(\"should return false for hardcoded layout on startup for content over 1 week\", () => {\n      feed._prefCache.config = {\n        hardcoded_layout: true,\n      };\n      const layout = { lastUpdated: Date.now() };\n      clock.tick(ONE_WEEK + 1);\n      const result = feed.isExpired({\n        cachedData: { layout },\n        key: \"layout\",\n        isStartup: true,\n      });\n\n      assert.isFalse(result);\n    });\n  });\n\n  describe(\"#checkIfAnyCacheExpired\", () => {\n    let cache;\n    beforeEach(() => {\n      cache = {\n        layout: { lastUpdated: Date.now() },\n        feeds: { \"foo.com\": { lastUpdated: Date.now() } },\n        spocs: { lastUpdated: Date.now() },\n      };\n      sandbox.stub(feed.cache, \"get\").resolves(cache);\n    });\n\n    it(\"should return false if nothing in the cache is expired\", async () => {\n      const result = await feed.checkIfAnyCacheExpired();\n      assert.isFalse(result);\n    });\n\n    it(\"should return true if .layout is missing\", async () => {\n      delete cache.layout;\n      assert.isTrue(await feed.checkIfAnyCacheExpired());\n    });\n    it(\"should return true if .layout is expired\", async () => {\n      clock.tick(THIRTY_MINUTES + 1);\n      // Update other caches we aren't testing\n      cache.feeds[\"foo.com\"].lastUpdate = Date.now();\n      cache.spocs.lastUpdate = Date.now();\n\n      assert.isTrue(await feed.checkIfAnyCacheExpired());\n    });\n\n    it(\"should return true if .spocs is missing\", async () => {\n      delete cache.spocs;\n      assert.isTrue(await feed.checkIfAnyCacheExpired());\n    });\n    it(\"should return true if .spocs is expired\", async () => {\n      clock.tick(THIRTY_MINUTES + 1);\n      // Update other caches we aren't testing\n      cache.layout.lastUpdated = Date.now();\n      cache.feeds[\"foo.com\"].lastUpdate = Date.now();\n\n      assert.isTrue(await feed.checkIfAnyCacheExpired());\n    });\n\n    it(\"should return true if .feeds is missing\", async () => {\n      delete cache.feeds;\n      assert.isTrue(await feed.checkIfAnyCacheExpired());\n    });\n    it(\"should return true if data for .feeds[url] is missing\", async () => {\n      cache.feeds[\"foo.com\"] = null;\n      assert.isTrue(await feed.checkIfAnyCacheExpired());\n    });\n    it(\"should return true if data for .feeds[url] is expired\", async () => {\n      clock.tick(THIRTY_MINUTES + 1);\n      // Update other caches we aren't testing\n      cache.layout.lastUpdated = Date.now();\n      cache.spocs.lastUpdate = Date.now();\n      assert.isTrue(await feed.checkIfAnyCacheExpired());\n    });\n  });\n\n  describe(\"#refreshAll\", () => {\n    beforeEach(() => {\n      sandbox.stub(feed, \"loadLayout\").resolves();\n      sandbox.stub(feed, \"loadComponentFeeds\").resolves();\n      sandbox.stub(feed, \"loadSpocs\").resolves();\n      sandbox.spy(feed.store, \"dispatch\");\n      Object.defineProperty(feed, \"showSpocs\", { get: () => true });\n    });\n\n    it(\"should call layout, component, spocs update and telemetry reporting functions\", async () => {\n      await feed.refreshAll();\n\n      assert.calledOnce(feed.loadLayout);\n      assert.calledOnce(feed.loadComponentFeeds);\n      assert.calledOnce(feed.loadSpocs);\n    });\n    it(\"should pass in dispatch wrapped with broadcast if options.updateOpenTabs is true\", async () => {\n      await feed.refreshAll({ updateOpenTabs: true });\n      [feed.loadLayout, feed.loadComponentFeeds, feed.loadSpocs].forEach(fn => {\n        assert.calledOnce(fn);\n        const result = fn.firstCall.args[0]({ type: \"FOO\" });\n        assert.isTrue(au.isBroadcastToContent(result));\n      });\n    });\n    it(\"should pass in dispatch with regular actions if options.updateOpenTabs is false\", async () => {\n      await feed.refreshAll({ updateOpenTabs: false });\n      [feed.loadLayout, feed.loadComponentFeeds, feed.loadSpocs].forEach(fn => {\n        assert.calledOnce(fn);\n        const result = fn.firstCall.args[0]({ type: \"FOO\" });\n        assert.deepEqual(result, { type: \"FOO\" });\n      });\n    });\n    it(\"should set loaded to true if loadSpocs and loadComponentFeeds fails\", async () => {\n      feed.loadComponentFeeds.rejects(\"loadComponentFeeds error\");\n      feed.loadSpocs.rejects(\"loadSpocs error\");\n\n      await feed.enable();\n\n      assert.isTrue(feed.loaded);\n    });\n    it(\"should call loadComponentFeeds and loadSpocs in Promise.all\", async () => {\n      sandbox.stub(global.Promise, \"all\").resolves();\n\n      await feed.refreshAll();\n\n      assert.calledOnce(global.Promise.all);\n      const { args } = global.Promise.all.firstCall;\n      assert.equal(args[0].length, 2);\n    });\n    describe(\"test startup cache behaviour\", () => {\n      beforeEach(() => {\n        feed._maybeUpdateCachedData.restore();\n        sandbox.stub(feed.cache, \"set\").resolves();\n      });\n      it(\"should refresh layout on startup if it was served from cache\", async () => {\n        feed.loadLayout.restore();\n        sandbox\n          .stub(feed.cache, \"get\")\n          .resolves({ layout: { lastUpdated: Date.now(), layout: {} } });\n        sandbox.stub(feed, \"fetchFromEndpoint\").resolves({ layout: {} });\n        clock.tick(THIRTY_MINUTES + 1);\n\n        await feed.refreshAll({ isStartup: true });\n\n        assert.calledOnce(feed.fetchFromEndpoint);\n        // Once from cache, once to update the store\n        assert.calledTwice(feed.store.dispatch);\n        assert.equal(\n          feed.store.dispatch.firstCall.args[0].type,\n          at.DISCOVERY_STREAM_LAYOUT_UPDATE\n        );\n      });\n      it(\"should not refresh layout on startup if it is under THIRTY_MINUTES\", async () => {\n        feed.loadLayout.restore();\n        sandbox\n          .stub(feed.cache, \"get\")\n          .resolves({ layout: { lastUpdated: Date.now(), layout: {} } });\n        sandbox.stub(feed, \"fetchFromEndpoint\").resolves({ layout: {} });\n\n        await feed.refreshAll({ isStartup: true });\n\n        assert.notCalled(feed.fetchFromEndpoint);\n      });\n      it(\"should refresh spocs on startup if it was served from cache\", async () => {\n        feed.loadSpocs.restore();\n        sandbox\n          .stub(feed.cache, \"get\")\n          .resolves({ spocs: { lastUpdated: Date.now() } });\n        sandbox.stub(feed, \"fetchFromEndpoint\").resolves(\"data\");\n        clock.tick(THIRTY_MINUTES + 1);\n\n        await feed.refreshAll({ isStartup: true });\n\n        assert.calledOnce(feed.fetchFromEndpoint);\n        // Once from cache, once to update the store\n        assert.calledTwice(feed.store.dispatch);\n        assert.equal(\n          feed.store.dispatch.firstCall.args[0].type,\n          at.DISCOVERY_STREAM_SPOCS_UPDATE\n        );\n      });\n      it(\"should not refresh spocs on startup if it is under THIRTY_MINUTES\", async () => {\n        feed.loadSpocs.restore();\n        sandbox\n          .stub(feed.cache, \"get\")\n          .resolves({ spocs: { lastUpdated: Date.now() } });\n        sandbox.stub(feed, \"fetchFromEndpoint\").resolves(\"data\");\n\n        await feed.refreshAll({ isStartup: true });\n\n        assert.notCalled(feed.fetchFromEndpoint);\n      });\n      it(\"should refresh feeds on startup if it was served from cache\", async () => {\n        feed.loadComponentFeeds.restore();\n\n        const fakeComponents = { components: [{ feed: { url: \"foo.com\" } }] };\n        const fakeLayout = [fakeComponents];\n        const fakeDiscoveryStream = { DiscoveryStream: { layout: fakeLayout } };\n        sandbox.stub(feed.store, \"getState\").returns(fakeDiscoveryStream);\n        sandbox.stub(feed, \"rotate\").callsFake(val => val);\n        sandbox\n          .stub(feed, \"scoreItems\")\n          .callsFake(val => ({ data: val, filtered: [] }));\n        sandbox.stub(feed, \"cleanUpTopRecImpressionPref\").callsFake(val => val);\n\n        const fakeCache = {\n          feeds: { \"foo.com\": { lastUpdated: Date.now(), data: \"data\" } },\n        };\n        sandbox.stub(feed.cache, \"get\").resolves(fakeCache);\n        clock.tick(THIRTY_MINUTES + 1);\n        sandbox.stub(feed, \"fetchFromEndpoint\").resolves({\n          recommendations: \"data\",\n          settings: {\n            recsExpireTime: 1,\n          },\n        });\n\n        await feed.refreshAll({ isStartup: true });\n\n        assert.calledOnce(feed.fetchFromEndpoint);\n        // Once from cache, once to update the feed, once to update that all feeds are done.\n        assert.calledThrice(feed.store.dispatch);\n        assert.equal(\n          feed.store.dispatch.secondCall.args[0].type,\n          at.DISCOVERY_STREAM_FEEDS_UPDATE\n        );\n      });\n    });\n  });\n\n  describe(\"#reportCacheAge\", () => {\n    let cache;\n    const cacheAge = 30;\n    beforeEach(() => {\n      cache = {\n        layout: { lastUpdated: Date.now() - 10 * 1000 },\n        feeds: { \"foo.com\": { lastUpdated: Date.now() - cacheAge * 1000 } },\n        spocs: { lastUpdated: Date.now() - 20 * 1000 },\n      };\n      sandbox.stub(feed.cache, \"get\").resolves(cache);\n    });\n\n    it(\"should report the oldest lastUpdated date as the cache age\", async () => {\n      sandbox.spy(feed.store, \"dispatch\");\n      feed.loaded = false;\n      await feed.reportCacheAge();\n\n      assert.calledOnce(feed.store.dispatch);\n\n      const [action] = feed.store.dispatch.firstCall.args;\n      assert.equal(action.type, at.TELEMETRY_PERFORMANCE_EVENT);\n      assert.equal(action.data.event, \"DS_CACHE_AGE_IN_SEC\");\n      assert.isAtLeast(action.data.value, cacheAge);\n      feed.loaded = true;\n    });\n  });\n\n  describe(\"#reportRequestTime\", () => {\n    let cache;\n    const cacheAge = 30;\n    beforeEach(() => {\n      cache = {\n        layout: { lastUpdated: Date.now() - 10 * 1000 },\n        feeds: { \"foo.com\": { lastUpdated: Date.now() - cacheAge * 1000 } },\n        spocs: { lastUpdated: Date.now() - 20 * 1000 },\n      };\n      sandbox.stub(feed.cache, \"get\").resolves(cache);\n    });\n\n    it(\"should report all the request times\", async () => {\n      sandbox.spy(feed.store, \"dispatch\");\n      feed.loaded = false;\n      feed.layoutRequestTime = 1000;\n      feed.spocsRequestTime = 2000;\n      feed.componentFeedRequestTime = 3000;\n      feed.totalRequestTime = 5000;\n      feed.reportRequestTime();\n\n      assert.equal(feed.store.dispatch.callCount, 4);\n\n      let [action] = feed.store.dispatch.getCall(0).args;\n      assert.equal(action.type, at.TELEMETRY_PERFORMANCE_EVENT);\n      assert.equal(action.data.event, \"LAYOUT_REQUEST_TIME\");\n      assert.equal(action.data.value, 1000);\n\n      [action] = feed.store.dispatch.getCall(1).args;\n      assert.equal(action.type, at.TELEMETRY_PERFORMANCE_EVENT);\n      assert.equal(action.data.event, \"SPOCS_REQUEST_TIME\");\n      assert.equal(action.data.value, 2000);\n\n      [action] = feed.store.dispatch.getCall(2).args;\n      assert.equal(action.type, at.TELEMETRY_PERFORMANCE_EVENT);\n      assert.equal(action.data.event, \"COMPONENT_FEED_REQUEST_TIME\");\n      assert.equal(action.data.value, 3000);\n\n      [action] = feed.store.dispatch.getCall(3).args;\n      assert.equal(action.type, at.TELEMETRY_PERFORMANCE_EVENT);\n      assert.equal(action.data.event, \"DS_FEED_TOTAL_REQUEST_TIME\");\n      assert.equal(action.data.value, 5000);\n      feed.loaded = true;\n    });\n  });\n\n  describe(\"#loadAffinityScoresCache\", () => {\n    it(\"should create an affinity provider from cached affinities\", async () => {\n      feed._prefCache.config = {\n        personalized: true,\n      };\n      const fakeCache = {\n        affinities: {\n          scores: 123,\n          _timestamp: 456,\n        },\n      };\n      sandbox.stub(feed.cache, \"get\").returns(Promise.resolve(fakeCache));\n\n      await feed.loadAffinityScoresCache();\n\n      assert.equal(feed.domainAffinitiesLastUpdated, 456);\n    });\n  });\n\n  describe(\"#updateDomainAffinityScores\", () => {\n    it(\"should update affinity provider on idle daily\", async () => {\n      feed._prefCache.config = {\n        personalized: true,\n      };\n      feed.affinities = {\n        parameterSets: {\n          default: {},\n        },\n        maxHistoryQueryResults: 1000,\n        timeSegments: [],\n        version: \"123\",\n      };\n\n      feed.observe(null, \"idle-daily\");\n\n      assert.equal(feed.affinityProvider.version, \"123\");\n    });\n    it(\"should not update affinity provider on idle daily\", async () => {\n      feed._prefCache.config = {\n        personalized: false,\n      };\n\n      feed.observe(null, \"idle-daily\");\n\n      assert.isTrue(!feed.affinityProvider);\n    });\n  });\n  describe(\"#scoreItems\", () => {\n    it(\"should score items using item_score and min_score\", () => {\n      const { data: result, filtered } = feed.scoreItems([\n        { item_score: 0.8, min_score: 0.1 },\n        { item_score: 0.5, min_score: 0.6 },\n        { item_score: 0.7, min_score: 0.1 },\n        { item_score: 0.9, min_score: 0.1 },\n      ]);\n      assert.deepEqual(result, [\n        { item_score: 0.9, score: 0.9, min_score: 0.1 },\n        { item_score: 0.8, score: 0.8, min_score: 0.1 },\n        { item_score: 0.7, score: 0.7, min_score: 0.1 },\n      ]);\n      assert.deepEqual(filtered, [\n        { item_score: 0.5, min_score: 0.6, score: 0.5 },\n      ]);\n    });\n  });\n\n  describe(\"#scoreItem\", () => {\n    it(\"should use personalized score with affinity provider\", () => {\n      const item = {};\n      feed._prefCache.config = {\n        personalized: true,\n      };\n      feed.affinityProvider = {\n        calculateItemRelevanceScore: () => 0.5,\n      };\n      const result = feed.scoreItem(item);\n      assert.equal(result.score, 0.5);\n    });\n    it(\"should use item_score score without affinity provider score\", () => {\n      const item = {\n        item_score: 0.6,\n      };\n      feed._prefCache.config = {\n        personalized: true,\n      };\n      feed.affinityProvider = {\n        calculateItemRelevanceScore: () => {},\n      };\n      const result = feed.scoreItem(item);\n      assert.equal(result.score, 0.6);\n    });\n    it(\"should add min_score of 0 if undefined\", () => {\n      const item = {};\n      feed._prefCache.config = {\n        personalized: true,\n      };\n      feed.affinityProvider = {\n        calculateItemRelevanceScore: () => 0.5,\n      };\n      const result = feed.scoreItem(item);\n      assert.equal(result.min_score, 0);\n    });\n  });\n\n  describe(\"#_sendSpocsFill\", () => {\n    it(\"should send out all the SPOCS Fill pings\", () => {\n      sandbox.spy(feed.store, \"dispatch\");\n      const expected = [\n        { id: 1, reason: \"frequency_cap\", displayed: 0, full_recalc: 1 },\n        { id: 2, reason: \"frequency_cap\", displayed: 0, full_recalc: 1 },\n        { id: 3, reason: \"blocked_by_user\", displayed: 0, full_recalc: 1 },\n        { id: 4, reason: \"blocked_by_user\", displayed: 0, full_recalc: 1 },\n        { id: 5, reason: \"flight_duplicate\", displayed: 0, full_recalc: 1 },\n        { id: 6, reason: \"flight_duplicate\", displayed: 0, full_recalc: 1 },\n        { id: 7, reason: \"below_min_score\", displayed: 0, full_recalc: 1 },\n        { id: 8, reason: \"below_min_score\", displayed: 0, full_recalc: 1 },\n      ];\n      const filtered = {\n        frequency_cap: [{ id: 1, flight_id: 1 }, { id: 2, flight_id: 2 }],\n        blocked_by_user: [{ id: 3, flight_id: 3 }, { id: 4, flight_id: 4 }],\n        flight_duplicate: [{ id: 5, flight_id: 5 }, { id: 6, flight_id: 6 }],\n        below_min_score: [{ id: 7, flight_id: 7 }, { id: 8, flight_id: 8 }],\n      };\n      feed._sendSpocsFill(filtered, true);\n\n      assert.deepEqual(\n        feed.store.dispatch.firstCall.args[0].data.spoc_fills,\n        expected\n      );\n    });\n    it(\"should send SPOCS Fill ping with the correct full_recalc\", () => {\n      sandbox.spy(feed.store, \"dispatch\");\n      const expected = [\n        { id: 1, reason: \"frequency_cap\", displayed: 0, full_recalc: 0 },\n        { id: 2, reason: \"frequency_cap\", displayed: 0, full_recalc: 0 },\n      ];\n      const filtered = {\n        frequency_cap: [{ id: 1, flight_id: 1 }, { id: 2, flight_id: 2 }],\n      };\n      feed._sendSpocsFill(filtered, false);\n\n      assert.deepEqual(\n        feed.store.dispatch.firstCall.args[0].data.spoc_fills,\n        expected\n      );\n    });\n    it(\"should not send non-SPOCS Fill pings\", () => {\n      sandbox.spy(feed.store, \"dispatch\");\n      const expected = [\n        { id: 1, reason: \"frequency_cap\", displayed: 0, full_recalc: 1 },\n        { id: 3, reason: \"blocked_by_user\", displayed: 0, full_recalc: 1 },\n        { id: 5, reason: \"flight_duplicate\", displayed: 0, full_recalc: 1 },\n        { id: 7, reason: \"below_min_score\", displayed: 0, full_recalc: 1 },\n      ];\n      const filtered = {\n        frequency_cap: [{ id: 1, flight_id: 1 }, { id: 2 }],\n        blocked_by_user: [{ id: 3, flight_id: 3 }, { id: 4 }],\n        flight_duplicate: [{ id: 5, flight_id: 5 }, { id: 6 }],\n        below_min_score: [{ id: 7, flight_id: 7 }, { id: 8 }],\n      };\n      feed._sendSpocsFill(filtered, true);\n\n      assert.deepEqual(\n        feed.store.dispatch.firstCall.args[0].data.spoc_fills,\n        expected\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/lib/DownloadsManager.test.js",
    "content": "import { actionTypes as at } from \"common/Actions.jsm\";\nimport { DownloadsManager } from \"lib/DownloadsManager.jsm\";\nimport { GlobalOverrider } from \"test/unit/utils\";\n\ndescribe(\"Downloads Manager\", () => {\n  let downloadsManager;\n  let globals;\n  const DOWNLOAD_URL = \"https://site.com/download.mov\";\n\n  beforeEach(() => {\n    globals = new GlobalOverrider();\n    global.Cc[\"@mozilla.org/timer;1\"] = {\n      createInstance() {\n        return {\n          initWithCallback: sinon.stub().callsFake(callback => callback()),\n          cancel: sinon.spy(),\n        };\n      },\n    };\n\n    globals.set(\"DownloadsCommon\", {\n      getData: sinon.stub().returns({\n        addView: sinon.stub(),\n        removeView: sinon.stub(),\n      }),\n      copyDownloadLink: sinon.stub(),\n      deleteDownload: sinon.stub().returns(Promise.resolve()),\n      openDownloadedFile: sinon.stub(),\n      showDownloadedFile: sinon.stub(),\n    });\n\n    downloadsManager = new DownloadsManager();\n    downloadsManager.init({ dispatch() {} });\n    downloadsManager.onDownloadAdded({\n      source: { url: DOWNLOAD_URL },\n      endTime: Date.now(),\n      target: { path: \"/path/to/download.mov\", exists: true },\n      succeeded: true,\n      refresh: async () => {},\n    });\n    assert.ok(downloadsManager._downloadItems.has(DOWNLOAD_URL));\n\n    globals.set(\"NewTabUtils\", { blockedLinks: { isBlocked() {} } });\n  });\n  afterEach(() => {\n    downloadsManager._downloadItems.clear();\n    globals.restore();\n  });\n  describe(\"#init\", () => {\n    it(\"should add a DownloadsCommon view on init\", () => {\n      downloadsManager.init({ dispatch() {} });\n      assert.calledTwice(global.DownloadsCommon.getData().addView);\n    });\n  });\n  describe(\"#onAction\", () => {\n    it(\"should copy the file on COPY_DOWNLOAD_LINK\", () => {\n      downloadsManager.onAction({\n        type: at.COPY_DOWNLOAD_LINK,\n        data: { url: DOWNLOAD_URL },\n      });\n      assert.calledOnce(global.DownloadsCommon.copyDownloadLink);\n    });\n    it(\"should remove the file on REMOVE_DOWNLOAD_FILE\", () => {\n      downloadsManager.onAction({\n        type: at.REMOVE_DOWNLOAD_FILE,\n        data: { url: DOWNLOAD_URL },\n      });\n      assert.calledOnce(global.DownloadsCommon.deleteDownload);\n    });\n    it(\"should show the file on SHOW_DOWNLOAD_FILE\", () => {\n      downloadsManager.onAction({\n        type: at.SHOW_DOWNLOAD_FILE,\n        data: { url: DOWNLOAD_URL },\n      });\n      assert.calledOnce(global.DownloadsCommon.showDownloadedFile);\n    });\n    it(\"should open the file on OPEN_DOWNLOAD_FILE if the type is download\", () => {\n      downloadsManager.onAction({\n        type: at.OPEN_DOWNLOAD_FILE,\n        data: { url: DOWNLOAD_URL, type: \"download\" },\n      });\n      assert.calledOnce(global.DownloadsCommon.openDownloadedFile);\n    });\n    it(\"should copy the file on UNINIT\", () => {\n      // DownloadsManager._downloadData needs to exist first\n      downloadsManager.onAction({ type: at.UNINIT });\n      assert.calledOnce(global.DownloadsCommon.getData().removeView);\n    });\n    it(\"should not execute a download command if we do not have the correct url\", () => {\n      downloadsManager.onAction({\n        type: at.SHOW_DOWNLOAD_FILE,\n        data: { url: \"unknown_url\" },\n      });\n      assert.notCalled(global.DownloadsCommon.showDownloadedFile);\n    });\n  });\n  describe(\"#onDownloadAdded\", () => {\n    let newDownload;\n    beforeEach(() => {\n      downloadsManager._downloadItems.clear();\n      newDownload = {\n        source: { url: \"https://site.com/newDownload.mov\" },\n        endTime: Date.now(),\n        target: { path: \"/path/to/newDownload.mov\", exists: true },\n        succeeded: true,\n        refresh: async () => {},\n      };\n    });\n    afterEach(() => {\n      downloadsManager._downloadItems.clear();\n    });\n    it(\"should add a download on onDownloadAdded\", () => {\n      downloadsManager.onDownloadAdded(newDownload);\n      assert.ok(\n        downloadsManager._downloadItems.has(\"https://site.com/newDownload.mov\")\n      );\n    });\n    it(\"should not add a download if it already exists\", () => {\n      downloadsManager.onDownloadAdded(newDownload);\n      downloadsManager.onDownloadAdded(newDownload);\n      downloadsManager.onDownloadAdded(newDownload);\n      downloadsManager.onDownloadAdded(newDownload);\n      const results = downloadsManager._downloadItems;\n      assert.equal(results.size, 1);\n    });\n    it(\"should not return any downloads if no threshold is provided\", async () => {\n      downloadsManager.onDownloadAdded(newDownload);\n      const results = await downloadsManager.getDownloads(null, {});\n      assert.equal(results.length, 0);\n    });\n    it(\"should stop at numItems when it found one it's looking for\", async () => {\n      const aDownload = {\n        source: { url: \"https://site.com/aDownload.pdf\" },\n        endTime: Date.now(),\n        target: { path: \"/path/to/aDownload.pdf\", exists: true },\n        succeeded: true,\n        refresh: async () => {},\n      };\n      downloadsManager.onDownloadAdded(aDownload);\n      downloadsManager.onDownloadAdded(newDownload);\n      const results = await downloadsManager.getDownloads(Infinity, {\n        numItems: 1,\n        onlySucceeded: true,\n        onlyExists: true,\n      });\n      assert.equal(results.length, 1);\n      assert.equal(results[0].url, aDownload.source.url);\n    });\n    it(\"should get all the downloads younger than the threshold provided\", async () => {\n      const oldDownload = {\n        source: { url: \"https://site.com/oldDownload.pdf\" },\n        endTime: Date.now() - 40 * 60 * 60 * 1000,\n        target: { path: \"/path/to/oldDownload.pdf\", exists: true },\n        succeeded: true,\n        refresh: async () => {},\n      };\n      // Add an old download (older than 36 hours in this case)\n      downloadsManager.onDownloadAdded(oldDownload);\n      downloadsManager.onDownloadAdded(newDownload);\n      const RECENT_DOWNLOAD_THRESHOLD = 36 * 60 * 60 * 1000;\n      const results = await downloadsManager.getDownloads(\n        RECENT_DOWNLOAD_THRESHOLD,\n        { numItems: 5, onlySucceeded: true, onlyExists: true }\n      );\n      assert.equal(results.length, 1);\n      assert.equal(results[0].url, newDownload.source.url);\n    });\n    it(\"should dispatch DOWNLOAD_CHANGED when adding a download\", () => {\n      downloadsManager._store.dispatch = sinon.spy();\n      downloadsManager._downloadTimer = null; // Nuke the timer\n      downloadsManager.onDownloadAdded(newDownload);\n      assert.calledOnce(downloadsManager._store.dispatch);\n    });\n    it(\"should refresh the downloads if onlyExists is true\", async () => {\n      const aDownload = {\n        source: { url: \"https://site.com/aDownload.pdf\" },\n        endTime: Date.now() - 40 * 60 * 60 * 1000,\n        target: { path: \"/path/to/aDownload.pdf\", exists: true },\n        succeeded: true,\n        refresh: () => {},\n      };\n      sinon.stub(aDownload, \"refresh\").returns(Promise.resolve());\n      downloadsManager.onDownloadAdded(aDownload);\n      await downloadsManager.getDownloads(Infinity, {\n        numItems: 5,\n        onlySucceeded: true,\n        onlyExists: true,\n      });\n      assert.calledOnce(aDownload.refresh);\n    });\n    it(\"should not refresh the downloads if onlyExists is false (by default)\", async () => {\n      const aDownload = {\n        source: { url: \"https://site.com/aDownload.pdf\" },\n        endTime: Date.now() - 40 * 60 * 60 * 1000,\n        target: { path: \"/path/to/aDownload.pdf\", exists: true },\n        succeeded: true,\n        refresh: () => {},\n      };\n      sinon.stub(aDownload, \"refresh\").returns(Promise.resolve());\n      downloadsManager.onDownloadAdded(aDownload);\n      await downloadsManager.getDownloads(Infinity, {\n        numItems: 5,\n        onlySucceeded: true,\n      });\n      assert.notCalled(aDownload.refresh);\n    });\n    it(\"should only return downloads that exist if specified\", async () => {\n      const nonExistantDownload = {\n        source: { url: \"https://site.com/nonExistantDownload.pdf\" },\n        endTime: Date.now() - 40 * 60 * 60 * 1000,\n        target: { path: \"/path/to/nonExistantDownload.pdf\", exists: false },\n        succeeded: true,\n        refresh: async () => {},\n      };\n      downloadsManager.onDownloadAdded(newDownload);\n      downloadsManager.onDownloadAdded(nonExistantDownload);\n      const results = await downloadsManager.getDownloads(Infinity, {\n        numItems: 5,\n        onlySucceeded: true,\n        onlyExists: true,\n      });\n      assert.equal(results.length, 1);\n      assert.equal(results[0].url, newDownload.source.url);\n    });\n    it(\"should return all downloads that either exist or don't exist if not specified\", async () => {\n      const nonExistantDownload = {\n        source: { url: \"https://site.com/nonExistantDownload.pdf\" },\n        endTime: Date.now() - 40 * 60 * 60 * 1000,\n        target: { path: \"/path/to/nonExistantDownload.pdf\", exists: false },\n        succeeded: true,\n        refresh: async () => {},\n      };\n      downloadsManager.onDownloadAdded(newDownload);\n      downloadsManager.onDownloadAdded(nonExistantDownload);\n      const results = await downloadsManager.getDownloads(Infinity, {\n        numItems: 5,\n        onlySucceeded: true,\n      });\n      assert.equal(results.length, 2);\n      assert.equal(results[0].url, newDownload.source.url);\n      assert.equal(results[1].url, nonExistantDownload.source.url);\n    });\n    it(\"should return only unblocked downloads\", async () => {\n      const nonExistantDownload = {\n        source: { url: \"https://site.com/nonExistantDownload.pdf\" },\n        endTime: Date.now() - 40 * 60 * 60 * 1000,\n        target: { path: \"/path/to/nonExistantDownload.pdf\", exists: false },\n        succeeded: true,\n        refresh: async () => {},\n      };\n      downloadsManager.onDownloadAdded(newDownload);\n      downloadsManager.onDownloadAdded(nonExistantDownload);\n      globals.set(\"NewTabUtils\", {\n        blockedLinks: {\n          isBlocked: item => item.url === nonExistantDownload.source.url,\n        },\n      });\n\n      const results = await downloadsManager.getDownloads(Infinity, {\n        numItems: 5,\n        onlySucceeded: true,\n      });\n\n      assert.equal(results.length, 1);\n      assert.propertyVal(results[0], \"url\", newDownload.source.url);\n    });\n    it(\"should only return downloads that were successful if specified\", async () => {\n      const nonSuccessfulDownload = {\n        source: { url: \"https://site.com/nonSuccessfulDownload.pdf\" },\n        endTime: Date.now() - 40 * 60 * 60 * 1000,\n        target: { path: \"/path/to/nonSuccessfulDownload.pdf\", exists: false },\n        succeeded: false,\n        refresh: async () => {},\n      };\n      downloadsManager.onDownloadAdded(newDownload);\n      downloadsManager.onDownloadAdded(nonSuccessfulDownload);\n      const results = await downloadsManager.getDownloads(Infinity, {\n        numItems: 5,\n        onlySucceeded: true,\n      });\n      assert.equal(results.length, 1);\n      assert.equal(results[0].url, newDownload.source.url);\n    });\n    it(\"should return all downloads that were either successful or not if not specified\", async () => {\n      const nonExistantDownload = {\n        source: { url: \"https://site.com/nonExistantDownload.pdf\" },\n        endTime: Date.now() - 40 * 60 * 60 * 1000,\n        target: { path: \"/path/to/nonExistantDownload.pdf\", exists: true },\n        succeeded: false,\n        refresh: async () => {},\n      };\n      downloadsManager.onDownloadAdded(newDownload);\n      downloadsManager.onDownloadAdded(nonExistantDownload);\n      const results = await downloadsManager.getDownloads(Infinity, {\n        numItems: 5,\n      });\n      assert.equal(results.length, 2);\n      assert.equal(results[0].url, newDownload.source.url);\n      assert.equal(results[1].url, nonExistantDownload.source.url);\n    });\n    it(\"should sort the downloads by recency\", async () => {\n      const olderDownload1 = {\n        source: { url: \"https://site.com/oldDownload1.pdf\" },\n        endTime: Date.now() - 2 * 60 * 60 * 1000, // 2 hours ago\n        target: { path: \"/path/to/oldDownload1.pdf\", exists: true },\n        succeeded: true,\n        refresh: async () => {},\n      };\n      const olderDownload2 = {\n        source: { url: \"https://site.com/oldDownload2.pdf\" },\n        endTime: Date.now() - 60 * 60 * 1000, // 1 hour ago\n        target: { path: \"/path/to/oldDownload2.pdf\", exists: true },\n        succeeded: true,\n        refresh: async () => {},\n      };\n      // Add some older downloads and check that they are in order\n      downloadsManager.onDownloadAdded(olderDownload1);\n      downloadsManager.onDownloadAdded(olderDownload2);\n      downloadsManager.onDownloadAdded(newDownload);\n      const results = await downloadsManager.getDownloads(Infinity, {\n        numItems: 5,\n        onlySucceeded: true,\n        onlyExists: true,\n      });\n      assert.equal(results.length, 3);\n      assert.equal(results[0].url, newDownload.source.url);\n      assert.equal(results[1].url, olderDownload2.source.url);\n      assert.equal(results[2].url, olderDownload1.source.url);\n    });\n    it(\"should format the description properly if there is no file type\", async () => {\n      newDownload.target.path = null;\n      downloadsManager.onDownloadAdded(newDownload);\n      const results = await downloadsManager.getDownloads(Infinity, {\n        numItems: 5,\n        onlySucceeded: true,\n        onlyExists: true,\n      });\n      assert.equal(results.length, 1);\n      assert.equal(results[0].description, \"1.5 MB\"); // see unit-entry.js to see where this comes from\n    });\n  });\n  describe(\"#onDownloadRemoved\", () => {\n    let newDownload;\n    beforeEach(() => {\n      downloadsManager._downloadItems.clear();\n      newDownload = {\n        source: { url: \"https://site.com/removeMe.mov\" },\n        endTime: Date.now(),\n        target: { path: \"/path/to/removeMe.mov\", exists: true },\n        succeeded: true,\n        refresh: async () => {},\n      };\n      downloadsManager.onDownloadAdded(newDownload);\n    });\n    it(\"should remove a download if it exists on onDownloadRemoved\", async () => {\n      downloadsManager.onDownloadRemoved({\n        source: { url: \"https://site.com/removeMe.mov\" },\n      });\n      const results = await downloadsManager.getDownloads(Infinity, {\n        numItems: 5,\n      });\n      assert.deepEqual(results, []);\n    });\n    it(\"should dispatch DOWNLOAD_CHANGED when removing a download\", () => {\n      downloadsManager._store.dispatch = sinon.spy();\n      downloadsManager.onDownloadRemoved({\n        source: { url: \"https://site.com/removeMe.mov\" },\n      });\n      assert.calledOnce(downloadsManager._store.dispatch);\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/lib/FaviconFeed.test.js",
    "content": "\"use strict\";\nimport { FaviconFeed, fetchIconFromRedirects } from \"lib/FaviconFeed.jsm\";\nimport { actionTypes as at } from \"common/Actions.jsm\";\nimport { GlobalOverrider } from \"test/unit/utils\";\n\nconst FAKE_ENDPOINT = \"https://foo.com/\";\n\ndescribe(\"FaviconFeed\", () => {\n  let feed;\n  let globals;\n  let sandbox;\n  let clock;\n  let siteIconsPref;\n\n  beforeEach(() => {\n    clock = sinon.useFakeTimers();\n    globals = new GlobalOverrider();\n    sandbox = globals.sandbox;\n    globals.set(\"PlacesUtils\", {\n      favicons: {\n        setAndFetchFaviconForPage: sandbox.spy(),\n        getFaviconDataForPage: () => Promise.resolve(null),\n        FAVICON_LOAD_NON_PRIVATE: 1,\n      },\n      history: {\n        TRANSITIONS: {\n          REDIRECT_TEMPORARY: 1,\n          REDIRECT_PERMANENT: 2,\n        },\n      },\n    });\n    globals.set(\"NewTabUtils\", {\n      activityStreamProvider: { executePlacesQuery: () => Promise.resolve([]) },\n    });\n    siteIconsPref = true;\n    sandbox\n      .stub(global.Services.prefs, \"getBoolPref\")\n      .withArgs(\"browser.chrome.site_icons\")\n      .callsFake(() => siteIconsPref);\n\n    feed = new FaviconFeed();\n    feed.store = {\n      dispatch: sinon.spy(),\n      getState() {\n        return this.state;\n      },\n      state: {\n        Prefs: { values: { \"tippyTop.service.endpoint\": FAKE_ENDPOINT } },\n      },\n    };\n  });\n  afterEach(() => {\n    clock.restore();\n    globals.restore();\n  });\n\n  it(\"should create a FaviconFeed\", () => {\n    assert.instanceOf(feed, FaviconFeed);\n  });\n\n  describe(\"#fetchIcon\", () => {\n    let domain;\n    let url;\n    beforeEach(() => {\n      domain = \"mozilla.org\";\n      url = `https://${domain}/`;\n      feed.getSite = sandbox\n        .stub()\n        .returns(Promise.resolve({ domain, image_url: `${url}/icon.png` }));\n      feed._queryForRedirects.clear();\n    });\n\n    it(\"should setAndFetchFaviconForPage if the url is in the TippyTop data\", async () => {\n      await feed.fetchIcon(url);\n\n      assert.calledOnce(global.PlacesUtils.favicons.setAndFetchFaviconForPage);\n      assert.calledWith(\n        global.PlacesUtils.favicons.setAndFetchFaviconForPage,\n        sinon.match({ spec: url }),\n        { ref: \"tippytop\", spec: `${url}/icon.png` },\n        false,\n        global.PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,\n        null,\n        undefined\n      );\n    });\n    it(\"should NOT setAndFetchFaviconForPage if site_icons pref is false\", async () => {\n      siteIconsPref = false;\n\n      await feed.fetchIcon(url);\n\n      assert.notCalled(global.PlacesUtils.favicons.setAndFetchFaviconForPage);\n    });\n    it(\"should NOT setAndFetchFaviconForPage if the url is NOT in the TippyTop data\", async () => {\n      feed.getSite = sandbox.stub().returns(Promise.resolve(null));\n      await feed.fetchIcon(\"https://example.com\");\n\n      assert.notCalled(global.PlacesUtils.favicons.setAndFetchFaviconForPage);\n    });\n    it(\"should issue a fetchIconFromRedirects if the url is NOT in the TippyTop data\", async () => {\n      feed.getSite = sandbox.stub().returns(Promise.resolve(null));\n      sandbox.spy(global.Services.tm, \"idleDispatchToMainThread\");\n\n      await feed.fetchIcon(\"https://example.com\");\n\n      assert.calledOnce(global.Services.tm.idleDispatchToMainThread);\n    });\n    it(\"should only issue fetchIconFromRedirects once on the same url\", async () => {\n      feed.getSite = sandbox.stub().returns(Promise.resolve(null));\n      sandbox.spy(global.Services.tm, \"idleDispatchToMainThread\");\n\n      await feed.fetchIcon(\"https://example.com\");\n      await feed.fetchIcon(\"https://example.com\");\n\n      assert.calledOnce(global.Services.tm.idleDispatchToMainThread);\n    });\n    it(\"should issue fetchIconFromRedirects twice on two different urls\", async () => {\n      feed.getSite = sandbox.stub().returns(Promise.resolve(null));\n      sandbox.spy(global.Services.tm, \"idleDispatchToMainThread\");\n\n      await feed.fetchIcon(\"https://example.com\");\n      await feed.fetchIcon(\"https://another.example.com\");\n\n      assert.calledTwice(global.Services.tm.idleDispatchToMainThread);\n    });\n  });\n\n  describe(\"#getSite\", () => {\n    it(\"should return site data if RemoteSettings has an entry for the domain\", async () => {\n      const get = () =>\n        Promise.resolve([{ domain: \"example.com\", image_url: \"foo.img\" }]);\n      feed._tippyTop = { get };\n      const site = await feed.getSite(\"example.com\");\n      assert.equal(site.domain, \"example.com\");\n    });\n    it(\"should return null if RemoteSettings doesn't have an entry for the domain\", async () => {\n      const get = () => Promise.resolve([]);\n      feed._tippyTop = { get };\n      const site = await feed.getSite(\"example.com\");\n      assert.isNull(site);\n    });\n    it(\"should lazy init _tippyTop\", async () => {\n      assert.isUndefined(feed._tippyTop);\n      await feed.getSite(\"example.com\");\n      assert.ok(feed._tippyTop);\n    });\n  });\n\n  describe(\"#onAction\", () => {\n    it(\"should fetchIcon on RICH_ICON_MISSING\", async () => {\n      feed.fetchIcon = sinon.spy();\n      const url = \"https://mozilla.org\";\n      feed.onAction({ type: at.RICH_ICON_MISSING, data: { url } });\n      assert.calledOnce(feed.fetchIcon);\n      assert.calledWith(feed.fetchIcon, url);\n    });\n  });\n\n  describe(\"#fetchIconFromRedirects\", () => {\n    let domain;\n    let url;\n    let iconUrl;\n\n    beforeEach(() => {\n      domain = \"mozilla.org\";\n      url = `https://${domain}/`;\n      iconUrl = `${url}/icon.png`;\n    });\n    it(\"should setAndFetchFaviconForPage if the url was redirected with a icon\", async () => {\n      sandbox\n        .stub(global.NewTabUtils.activityStreamProvider, \"executePlacesQuery\")\n        .resolves([{ visit_id: 1, url: domain }, { visit_id: 2, url }]);\n      sandbox\n        .stub(global.PlacesUtils.favicons, \"getFaviconDataForPage\")\n        .callsArgWith(1, { spec: iconUrl }, 0, null, null, 96);\n\n      await fetchIconFromRedirects(domain);\n\n      assert.calledOnce(global.PlacesUtils.favicons.setAndFetchFaviconForPage);\n      assert.calledWith(\n        global.PlacesUtils.favicons.setAndFetchFaviconForPage,\n        sinon.match({ spec: domain }),\n        { spec: iconUrl },\n        false,\n        global.PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,\n        null,\n        undefined\n      );\n    });\n    it(\"should NOT setAndFetchFaviconForPage if the url doesn't have any redirect\", async () => {\n      sandbox\n        .stub(global.NewTabUtils.activityStreamProvider, \"executePlacesQuery\")\n        .resolves([]);\n\n      await fetchIconFromRedirects(domain);\n\n      assert.notCalled(global.PlacesUtils.favicons.setAndFetchFaviconForPage);\n    });\n    it(\"should NOT setAndFetchFaviconForPage if the original url doesn't have a icon\", async () => {\n      sandbox\n        .stub(global.NewTabUtils.activityStreamProvider, \"executePlacesQuery\")\n        .resolves([{ visit_id: 1, url: domain }, { visit_id: 2, url }]);\n      sandbox\n        .stub(global.PlacesUtils.favicons, \"getFaviconDataForPage\")\n        .callsArgWith(1, null, null, null, null, null);\n\n      await fetchIconFromRedirects(domain);\n\n      assert.notCalled(global.PlacesUtils.favicons.setAndFetchFaviconForPage);\n    });\n    it(\"should NOT setAndFetchFaviconForPage if the original url doesn't have a rich icon\", async () => {\n      sandbox\n        .stub(global.NewTabUtils.activityStreamProvider, \"executePlacesQuery\")\n        .resolves([{ visit_id: 1, url: domain }, { visit_id: 2, url }]);\n      sandbox\n        .stub(global.PlacesUtils.favicons, \"getFaviconDataForPage\")\n        .callsArgWith(1, { spec: iconUrl }, 0, null, null, 16);\n\n      await fetchIconFromRedirects(domain);\n\n      assert.notCalled(global.PlacesUtils.favicons.setAndFetchFaviconForPage);\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/lib/FilterAdult.test.js",
    "content": "import { filterAdult } from \"lib/FilterAdult.jsm\";\nimport { GlobalOverrider } from \"test/unit/utils\";\n\ndescribe(\"filterAdult\", () => {\n  let hashStub;\n  let hashValue;\n  let globals;\n\n  beforeEach(() => {\n    globals = new GlobalOverrider();\n    hashStub = {\n      finish: sinon.stub().callsFake(() => hashValue),\n      init: sinon.stub(),\n      update: sinon.stub(),\n    };\n    globals.set(\"Cc\", {\n      \"@mozilla.org/security/hash;1\": {\n        createInstance() {\n          return hashStub;\n        },\n      },\n    });\n  });\n\n  afterEach(() => {\n    globals.restore();\n  });\n\n  it(\"should default to include on unexpected urls\", () => {\n    const empty = {};\n\n    const result = filterAdult([empty]);\n\n    assert.equal(result.length, 1);\n    assert.equal(result[0], empty);\n  });\n  it(\"should not filter out non-adult urls\", () => {\n    const link = { url: \"https://mozilla.org/\" };\n\n    const result = filterAdult([link]);\n\n    assert.equal(result.length, 1);\n    assert.equal(result[0], link);\n  });\n  it(\"should filter out adult urls\", () => {\n    // Use a hash value that is in the adult set\n    hashValue = \"+/UCpAhZhz368iGioEO8aQ==\";\n    const link = { url: \"https://some-adult-site/\" };\n\n    const result = filterAdult([link]);\n\n    assert.equal(result.length, 0);\n  });\n});\n"
  },
  {
    "path": "test/unit/lib/HighlightsFeed.test.js",
    "content": "\"use strict\";\n\nimport { actionTypes as at } from \"common/Actions.jsm\";\nimport { Dedupe } from \"common/Dedupe.jsm\";\nimport { GlobalOverrider } from \"test/unit/utils\";\nimport injector from \"inject!lib/HighlightsFeed.jsm\";\nimport { Screenshots } from \"lib/Screenshots.jsm\";\n\nconst FAKE_LINKS = new Array(20)\n  .fill(null)\n  .map((v, i) => ({ url: `http://www.site${i}.com` }));\nconst FAKE_IMAGE = \"data123\";\n\ndescribe(\"Highlights Feed\", () => {\n  let HighlightsFeed;\n  let SECTION_ID;\n  let SYNC_BOOKMARKS_FINISHED_EVENT;\n  let BOOKMARKS_RESTORE_SUCCESS_EVENT;\n  let BOOKMARKS_RESTORE_FAILED_EVENT;\n  let feed;\n  let globals;\n  let sandbox;\n  let links;\n  let fakeScreenshot;\n  let fakeNewTabUtils;\n  let filterAdultStub;\n  let sectionsManagerStub;\n  let downloadsManagerStub;\n  let shortURLStub;\n  let fakePageThumbs;\n\n  beforeEach(() => {\n    globals = new GlobalOverrider();\n    sandbox = globals.sandbox;\n    fakeNewTabUtils = {\n      activityStreamLinks: {\n        getHighlights: sandbox.spy(() => Promise.resolve(links)),\n        deletePocketEntry: sandbox.spy(() => Promise.resolve({})),\n        archivePocketEntry: sandbox.spy(() => Promise.resolve({})),\n      },\n      activityStreamProvider: {\n        _processHighlights: sandbox.spy(l => l.slice(0, 1)),\n      },\n    };\n    sectionsManagerStub = {\n      onceInitialized: sinon.stub().callsFake(callback => callback()),\n      enableSection: sinon.spy(),\n      disableSection: sinon.spy(),\n      updateSection: sinon.spy(),\n      updateSectionCard: sinon.spy(),\n      sections: new Map([[\"highlights\", { id: \"highlights\" }]]),\n    };\n    downloadsManagerStub = sinon.stub().returns({\n      getDownloads: () => [{ url: \"https://site.com/download\" }],\n      onAction: sinon.spy(),\n      init: sinon.spy(),\n    });\n    fakeScreenshot = {\n      getScreenshotForURL: sandbox.spy(() => Promise.resolve(FAKE_IMAGE)),\n      maybeCacheScreenshot: Screenshots.maybeCacheScreenshot,\n      _shouldGetScreenshots: sinon.stub().returns(true),\n    };\n    filterAdultStub = sinon.stub().returns([]);\n    shortURLStub = sinon\n      .stub()\n      .callsFake(site => site.url.match(/\\/([^/]+)/)[1]);\n    fakePageThumbs = {\n      addExpirationFilter: sinon.stub(),\n      removeExpirationFilter: sinon.stub(),\n    };\n\n    globals.set(\"NewTabUtils\", fakeNewTabUtils);\n    globals.set(\"PageThumbs\", fakePageThumbs);\n    ({\n      HighlightsFeed,\n      SECTION_ID,\n      SYNC_BOOKMARKS_FINISHED_EVENT,\n      BOOKMARKS_RESTORE_SUCCESS_EVENT,\n      BOOKMARKS_RESTORE_FAILED_EVENT,\n    } = injector({\n      \"lib/FilterAdult.jsm\": { filterAdult: filterAdultStub },\n      \"lib/ShortURL.jsm\": { shortURL: shortURLStub },\n      \"lib/SectionsManager.jsm\": { SectionsManager: sectionsManagerStub },\n      \"lib/Screenshots.jsm\": { Screenshots: fakeScreenshot },\n      \"common/Dedupe.jsm\": { Dedupe },\n      \"lib/DownloadsManager.jsm\": { DownloadsManager: downloadsManagerStub },\n    }));\n    sandbox.spy(global.Services.obs, \"addObserver\");\n    sandbox.spy(global.Services.obs, \"removeObserver\");\n    feed = new HighlightsFeed();\n    feed.store = {\n      dispatch: sinon.spy(),\n      getState() {\n        return this.state;\n      },\n      state: {\n        Prefs: {\n          values: {\n            filterAdult: false,\n            \"section.highlights.includePocket\": false,\n            \"section.highlights.includeDownloads\": false,\n          },\n        },\n        TopSites: {\n          initialized: true,\n          rows: Array(12)\n            .fill(null)\n            .map((v, i) => ({ url: `http://www.topsite${i}.com` })),\n        },\n        Sections: [{ id: \"highlights\", initialized: false }],\n      },\n      subscribe: sinon.stub().callsFake(cb => {\n        cb();\n        return () => {};\n      }),\n    };\n    links = FAKE_LINKS;\n  });\n  afterEach(() => {\n    globals.restore();\n  });\n\n  describe(\"#init\", () => {\n    it(\"should create a HighlightsFeed\", () => {\n      assert.instanceOf(feed, HighlightsFeed);\n    });\n    it(\"should register a expiration filter\", () => {\n      assert.calledOnce(fakePageThumbs.addExpirationFilter);\n    });\n    it(\"should add the sync observer\", () => {\n      feed.onAction({ type: at.INIT });\n      assert.calledWith(\n        global.Services.obs.addObserver,\n        feed,\n        SYNC_BOOKMARKS_FINISHED_EVENT\n      );\n      assert.calledWith(\n        global.Services.obs.addObserver,\n        feed,\n        BOOKMARKS_RESTORE_SUCCESS_EVENT\n      );\n      assert.calledWith(\n        global.Services.obs.addObserver,\n        feed,\n        BOOKMARKS_RESTORE_FAILED_EVENT\n      );\n    });\n    it(\"should call SectionsManager.onceInitialized on INIT\", () => {\n      feed.onAction({ type: at.INIT });\n      assert.calledOnce(sectionsManagerStub.onceInitialized);\n    });\n    it(\"should enable its section\", () => {\n      feed.onAction({ type: at.INIT });\n      assert.calledOnce(sectionsManagerStub.enableSection);\n      assert.calledWith(sectionsManagerStub.enableSection, SECTION_ID);\n    });\n    it(\"should fetch highlights on postInit\", () => {\n      feed.fetchHighlights = sinon.spy();\n      feed.postInit();\n      assert.calledOnce(feed.fetchHighlights);\n    });\n    it(\"should hook up the store for the DownloadsManager\", () => {\n      feed.onAction({ type: at.INIT });\n      assert.calledOnce(feed.downloadsManager.init);\n    });\n  });\n  describe(\"#observe\", () => {\n    beforeEach(() => {\n      feed.fetchHighlights = sinon.spy();\n    });\n    it(\"should fetch higlights when we are done a sync for bookmarks\", () => {\n      feed.observe(null, SYNC_BOOKMARKS_FINISHED_EVENT, \"bookmarks\");\n      assert.calledWith(feed.fetchHighlights, { broadcast: true });\n    });\n    it(\"should fetch highlights after a successful import\", () => {\n      feed.observe(null, BOOKMARKS_RESTORE_SUCCESS_EVENT, \"html\");\n      assert.calledWith(feed.fetchHighlights, { broadcast: true });\n    });\n    it(\"should fetch highlights after a failed import\", () => {\n      feed.observe(null, BOOKMARKS_RESTORE_FAILED_EVENT, \"json\");\n      assert.calledWith(feed.fetchHighlights, { broadcast: true });\n    });\n    it(\"should not fetch higlights when we are doing a sync for something that is not bookmarks\", () => {\n      feed.observe(null, SYNC_BOOKMARKS_FINISHED_EVENT, \"tabs\");\n      assert.notCalled(feed.fetchHighlights);\n    });\n    it(\"should not fetch higlights for other events\", () => {\n      feed.observe(null, \"someotherevent\", \"bookmarks\");\n      assert.notCalled(feed.fetchHighlights);\n    });\n  });\n  describe(\"#filterForThumbnailExpiration\", () => {\n    it(\"should pass rows.urls to the callback provided\", () => {\n      const rows = [{ url: \"foo.com\" }, { url: \"bar.com\" }];\n      feed.store.state.Sections = [\n        { id: \"highlights\", rows, initialized: true },\n      ];\n      const stub = sinon.stub();\n\n      feed.filterForThumbnailExpiration(stub);\n\n      assert.calledOnce(stub);\n      assert.calledWithExactly(stub, rows.map(r => r.url));\n    });\n    it(\"should include preview_image_url (if present) in the callback results\", () => {\n      const rows = [\n        { url: \"foo.com\" },\n        { url: \"bar.com\", preview_image_url: \"bar.jpg\" },\n      ];\n      feed.store.state.Sections = [\n        { id: \"highlights\", rows, initialized: true },\n      ];\n      const stub = sinon.stub();\n\n      feed.filterForThumbnailExpiration(stub);\n\n      assert.calledOnce(stub);\n      assert.calledWithExactly(stub, [\"foo.com\", \"bar.com\", \"bar.jpg\"]);\n    });\n    it(\"should pass an empty array if not initialized\", () => {\n      const rows = [{ url: \"foo.com\" }, { url: \"bar.com\" }];\n      feed.store.state.Sections = [{ rows, initialized: false }];\n      const stub = sinon.stub();\n\n      feed.filterForThumbnailExpiration(stub);\n\n      assert.calledOnce(stub);\n      assert.calledWithExactly(stub, []);\n    });\n  });\n  describe(\"#fetchHighlights\", () => {\n    const fetchHighlights = async options => {\n      await feed.fetchHighlights(options);\n      return sectionsManagerStub.updateSection.firstCall.args[1].rows;\n    };\n    it(\"should return early if TopSites are not initialised\", async () => {\n      sandbox.spy(feed.linksCache, \"request\");\n      feed.store.state.TopSites.initialized = false;\n      feed.store.state.Prefs.values[\"feeds.topsites\"] = true;\n\n      // Initially TopSites is uninitialised and fetchHighlights should return.\n      await feed.fetchHighlights();\n\n      assert.notCalled(fakeNewTabUtils.activityStreamLinks.getHighlights);\n      assert.notCalled(feed.linksCache.request);\n    });\n    it(\"should return early if Sections are not initialised\", async () => {\n      sandbox.spy(feed.linksCache, \"request\");\n      feed.store.state.TopSites.initialized = true;\n      feed.store.state.Prefs.values[\"feeds.topsites\"] = true;\n      feed.store.state.Sections = [];\n\n      await feed.fetchHighlights();\n\n      assert.notCalled(fakeNewTabUtils.activityStreamLinks.getHighlights);\n      assert.notCalled(feed.linksCache.request);\n    });\n    it(\"should fetch Highlights if TopSites are initialised\", async () => {\n      sandbox.spy(feed.linksCache, \"request\");\n      // fetchHighlights should continue\n      feed.store.state.TopSites.initialized = true;\n\n      await feed.fetchHighlights();\n\n      assert.calledOnce(feed.linksCache.request);\n      assert.calledOnce(fakeNewTabUtils.activityStreamLinks.getHighlights);\n    });\n    it(\"should chronologically order highlight data types\", async () => {\n      links = [\n        {\n          url: \"https://site0.com\",\n          type: \"bookmark\",\n          bookmarkGuid: \"1234\",\n          date_added: Date.now() - 80,\n        }, // 3rd newest\n        {\n          url: \"https://site1.com\",\n          type: \"history\",\n          bookmarkGuid: \"1234\",\n          date_added: Date.now() - 60,\n        }, // append at the end\n        {\n          url: \"https://site2.com\",\n          type: \"history\",\n          date_added: Date.now() - 160,\n        }, // append at the end\n        {\n          url: \"https://site3.com\",\n          type: \"history\",\n          date_added: Date.now() - 60,\n        }, // append at the end\n        { url: \"https://site4.com\", type: \"pocket\", date_added: Date.now() }, // newest highlight\n        {\n          url: \"https://site5.com\",\n          type: \"pocket\",\n          date_added: Date.now() - 100,\n        }, // 4th newest\n        {\n          url: \"https://site6.com\",\n          type: \"bookmark\",\n          bookmarkGuid: \"1234\",\n          date_added: Date.now() - 40,\n        }, // 2nd newest\n      ];\n      const expectedChronological = [4, 6, 0, 5];\n      const expectedHistory = [1, 2, 3];\n\n      let highlights = await fetchHighlights();\n\n      [...expectedChronological, ...expectedHistory].forEach((link, index) => {\n        assert.propertyVal(\n          highlights[index],\n          \"url\",\n          links[link].url,\n          `highlight[${index}] should be link[${link}]`\n        );\n      });\n    });\n    it(\"should fetch Highlights if TopSites are not enabled\", async () => {\n      sandbox.spy(feed.linksCache, \"request\");\n      feed.store.state.Prefs.values[\"feeds.topsites\"] = false;\n\n      await feed.fetchHighlights();\n\n      assert.calledOnce(feed.linksCache.request);\n      assert.calledOnce(fakeNewTabUtils.activityStreamLinks.getHighlights);\n    });\n    it(\"should add hostname and hasImage to each link\", async () => {\n      links = [{ url: \"https://mozilla.org\" }];\n\n      const highlights = await fetchHighlights();\n\n      assert.equal(highlights[0].hostname, \"mozilla.org\");\n      assert.equal(highlights[0].hasImage, true);\n    });\n    it(\"should add an existing image if it exists to the link without calling fetchImage\", async () => {\n      links = [{ url: \"https://mozilla.org\", image: FAKE_IMAGE }];\n      sinon.spy(feed, \"fetchImage\");\n\n      const highlights = await fetchHighlights();\n\n      assert.equal(highlights[0].image, FAKE_IMAGE);\n      assert.notCalled(feed.fetchImage);\n    });\n    it(\"should call fetchImage with the correct arguments for new links\", async () => {\n      links = [\n        {\n          url: \"https://mozilla.org\",\n          preview_image_url: \"https://mozilla.org/preview.jog\",\n        },\n      ];\n      sinon.spy(feed, \"fetchImage\");\n\n      await feed.fetchHighlights();\n\n      assert.calledOnce(feed.fetchImage);\n      const [arg] = feed.fetchImage.firstCall.args;\n      assert.propertyVal(arg, \"url\", links[0].url);\n      assert.propertyVal(arg, \"preview_image_url\", links[0].preview_image_url);\n    });\n    it(\"should not include any links already in Top Sites\", async () => {\n      links = [\n        { url: \"https://mozilla.org\" },\n        { url: \"http://www.topsite0.com\" },\n        { url: \"http://www.topsite1.com\" },\n        { url: \"http://www.topsite2.com\" },\n      ];\n\n      const highlights = await fetchHighlights();\n\n      assert.equal(highlights.length, 1);\n      assert.equal(highlights[0].url, links[0].url);\n    });\n    it(\"should include bookmark but not history already in Top Sites\", async () => {\n      links = [\n        { url: \"http://www.topsite0.com\", type: \"bookmark\" },\n        { url: \"http://www.topsite1.com\", type: \"history\" },\n      ];\n\n      const highlights = await fetchHighlights();\n\n      assert.equal(highlights.length, 1);\n      assert.equal(highlights[0].url, links[0].url);\n    });\n    it(\"should not include history of same hostname as a bookmark\", async () => {\n      links = [\n        { url: \"https://site.com/bookmark\", type: \"bookmark\" },\n        { url: \"https://site.com/history\", type: \"history\" },\n      ];\n\n      const highlights = await fetchHighlights();\n\n      assert.equal(highlights.length, 1);\n      assert.equal(highlights[0].url, links[0].url);\n    });\n    it(\"should take the first history of a hostname\", async () => {\n      links = [\n        { url: \"https://site.com/first\", type: \"history\" },\n        { url: \"https://site.com/second\", type: \"history\" },\n        { url: \"https://other\", type: \"history\" },\n      ];\n\n      const highlights = await fetchHighlights();\n\n      assert.equal(highlights.length, 2);\n      assert.equal(highlights[0].url, links[0].url);\n      assert.equal(highlights[1].url, links[2].url);\n    });\n    it(\"should take a bookmark, a pocket, and downloaded item of the same hostname\", async () => {\n      links = [\n        { url: \"https://site.com/bookmark\", type: \"bookmark\" },\n        { url: \"https://site.com/pocket\", type: \"pocket\" },\n        { url: \"https://site.com/download\", type: \"download\" },\n      ];\n\n      const highlights = await fetchHighlights();\n\n      assert.equal(highlights.length, 3);\n      assert.equal(highlights[0].url, links[0].url);\n      assert.equal(highlights[1].url, links[1].url);\n      assert.equal(highlights[2].url, links[2].url);\n    });\n    it(\"should includePocket pocket items when pref is true\", async () => {\n      feed.store.state.Prefs.values[\"section.highlights.includePocket\"] = true;\n      sandbox.spy(feed.linksCache, \"request\");\n      await feed.fetchHighlights();\n\n      assert.propertyVal(\n        feed.linksCache.request.firstCall.args[0],\n        \"excludePocket\",\n        false\n      );\n    });\n    it(\"should not includePocket pocket items when pref is false\", async () => {\n      sandbox.spy(feed.linksCache, \"request\");\n      await feed.fetchHighlights();\n\n      assert.propertyVal(\n        feed.linksCache.request.firstCall.args[0],\n        \"excludePocket\",\n        true\n      );\n    });\n    it(\"should not include downloads when includeDownloads pref is false\", async () => {\n      links = [\n        { url: \"https://site.com/bookmark\", type: \"bookmark\" },\n        { url: \"https://site.com/pocket\", type: \"pocket\" },\n      ];\n\n      // Check that we don't have the downloaded item in highlights\n      const highlights = await fetchHighlights();\n      assert.equal(highlights.length, 2);\n      assert.equal(highlights[0].url, links[0].url);\n      assert.equal(highlights[1].url, links[1].url);\n    });\n    it(\"should include downloads when includeDownloads pref is true\", async () => {\n      feed.store.state.Prefs.values[\n        \"section.highlights.includeDownloads\"\n      ] = true;\n      links = [\n        { url: \"https://site.com/bookmark\", type: \"bookmark\" },\n        { url: \"https://site.com/pocket\", type: \"pocket\" },\n      ];\n\n      // Check that we did get the downloaded item in highlights\n      const highlights = await fetchHighlights();\n      assert.equal(highlights.length, 3);\n      assert.equal(highlights[0].url, links[0].url);\n      assert.equal(highlights[1].url, links[1].url);\n      assert.equal(highlights[2].url, \"https://site.com/download\");\n\n      assert.propertyVal(highlights[2], \"type\", \"download\");\n    });\n    it(\"should only take 1 download\", async () => {\n      feed.store.state.Prefs.values[\n        \"section.highlights.includeDownloads\"\n      ] = true;\n      feed.downloadsManager.getDownloads = () => [\n        { url: \"https://site1.com/download\" },\n        { url: \"https://site2.com/download\" },\n      ];\n      links = [{ url: \"https://site.com/bookmark\", type: \"bookmark\" }];\n\n      // Check that we did get the most single recent downloaded item in highlights\n      const highlights = await fetchHighlights();\n      assert.equal(highlights.length, 2);\n      assert.equal(highlights[0].url, links[0].url);\n      assert.equal(highlights[1].url, \"https://site1.com/download\");\n    });\n    it(\"should sort bookmarks, pocket, and downloads chronologically\", async () => {\n      feed.store.state.Prefs.values[\n        \"section.highlights.includeDownloads\"\n      ] = true;\n      feed.downloadsManager.getDownloads = () => [\n        {\n          url: \"https://site1.com/download\",\n          type: \"download\",\n          date_added: Date.now(),\n        },\n      ];\n      links = [\n        {\n          url: \"https://site.com/bookmark\",\n          type: \"bookmark\",\n          date_added: Date.now() - 10000,\n        },\n        {\n          url: \"https://site2.com/pocket\",\n          type: \"pocket\",\n          date_added: Date.now() - 5000,\n        },\n        {\n          url: \"https://site3.com/visited\",\n          type: \"history\",\n          date_added: Date.now(),\n        },\n      ];\n\n      // Check that the higlights are ordered chronologically by their 'date_added'\n      const highlights = await fetchHighlights();\n      assert.equal(highlights.length, 4);\n      assert.equal(highlights[0].url, \"https://site1.com/download\");\n      assert.equal(highlights[1].url, links[1].url);\n      assert.equal(highlights[2].url, links[0].url);\n      assert.equal(highlights[3].url, links[2].url); // history item goes last\n    });\n    it(\"should set type to bookmark if there is a bookmarkGuid\", async () => {\n      feed.store.state.Prefs.values[\n        \"section.highlights.includeBookmarks\"\n      ] = true;\n      links = [\n        {\n          url: \"https://mozilla.org\",\n          type: \"history\",\n          bookmarkGuid: \"1234567890\",\n        },\n      ];\n\n      const highlights = await fetchHighlights();\n\n      assert.equal(highlights[0].type, \"bookmark\");\n    });\n    it(\"should keep history type if there is a bookmarkGuid but don't include bookmarks\", async () => {\n      feed.store.state.Prefs.values[\n        \"section.highlights.includeBookmarks\"\n      ] = false;\n      links = [\n        {\n          url: \"https://mozilla.org\",\n          type: \"history\",\n          bookmarkGuid: \"1234567890\",\n        },\n      ];\n\n      const highlights = await fetchHighlights();\n\n      assert.propertyVal(highlights[0], \"type\", \"history\");\n    });\n    it(\"should not filter out adult pages when pref is false\", async () => {\n      await feed.fetchHighlights();\n\n      assert.notCalled(filterAdultStub);\n    });\n    it(\"should filter out adult pages when pref is true\", async () => {\n      feed.store.state.Prefs.values.filterAdult = true;\n\n      const highlights = await fetchHighlights();\n\n      // The stub filters out everything\n      assert.calledOnce(filterAdultStub);\n      assert.equal(highlights.length, 0);\n    });\n    it(\"should not expose internal link properties\", async () => {\n      const highlights = await fetchHighlights();\n\n      const internal = Object.keys(highlights[0]).filter(key =>\n        key.startsWith(\"__\")\n      );\n      assert.equal(internal.join(\"\"), \"\");\n    });\n    it(\"should broadcast if feed is not initialized\", async () => {\n      links = [];\n      await fetchHighlights();\n\n      assert.calledOnce(sectionsManagerStub.updateSection);\n      assert.calledWithExactly(\n        sectionsManagerStub.updateSection,\n        SECTION_ID,\n        { rows: [] },\n        true\n      );\n    });\n    it(\"should broadcast if options.broadcast is true\", async () => {\n      links = [];\n      feed.store.state.Sections[0].initialized = true;\n      await fetchHighlights({ broadcast: true });\n\n      assert.calledOnce(sectionsManagerStub.updateSection);\n      assert.calledWithExactly(\n        sectionsManagerStub.updateSection,\n        SECTION_ID,\n        { rows: [] },\n        true\n      );\n    });\n    it(\"should not broadcast if options.broadcast is false and initialized is true\", async () => {\n      links = [];\n      feed.store.state.Sections[0].initialized = true;\n      await fetchHighlights({ broadcast: false });\n\n      assert.calledOnce(sectionsManagerStub.updateSection);\n      assert.calledWithExactly(\n        sectionsManagerStub.updateSection,\n        SECTION_ID,\n        { rows: [] },\n        false\n      );\n    });\n  });\n  describe(\"#fetchImage\", () => {\n    const FAKE_URL = \"https://mozilla.org\";\n    const FAKE_IMAGE_URL = \"https://mozilla.org/preview.jpg\";\n    function fetchImage(page) {\n      return feed.fetchImage(\n        Object.assign({ __sharedCache: { updateLink() {} } }, page)\n      );\n    }\n    it(\"should capture the image, if available\", async () => {\n      await fetchImage({\n        preview_image_url: FAKE_IMAGE_URL,\n        url: FAKE_URL,\n      });\n\n      assert.calledOnce(fakeScreenshot.getScreenshotForURL);\n      assert.calledWith(fakeScreenshot.getScreenshotForURL, FAKE_IMAGE_URL);\n    });\n    it(\"should fall back to capturing a screenshot\", async () => {\n      await fetchImage({ url: FAKE_URL });\n\n      assert.calledOnce(fakeScreenshot.getScreenshotForURL);\n      assert.calledWith(fakeScreenshot.getScreenshotForURL, FAKE_URL);\n    });\n    it(\"should call SectionsManager.updateSectionCard with the right arguments\", async () => {\n      await fetchImage({\n        preview_image_url: FAKE_IMAGE_URL,\n        url: FAKE_URL,\n      });\n\n      assert.calledOnce(sectionsManagerStub.updateSectionCard);\n      assert.calledWith(\n        sectionsManagerStub.updateSectionCard,\n        \"highlights\",\n        FAKE_URL,\n        { image: FAKE_IMAGE },\n        true\n      );\n    });\n    it(\"should not update the card with the image\", async () => {\n      const card = {\n        preview_image_url: FAKE_IMAGE_URL,\n        url: FAKE_URL,\n      };\n\n      await fetchImage(card);\n\n      assert.notProperty(card, \"image\");\n    });\n  });\n  describe(\"#uninit\", () => {\n    it(\"should disable its section\", () => {\n      feed.onAction({ type: at.UNINIT });\n      assert.calledOnce(sectionsManagerStub.disableSection);\n      assert.calledWith(sectionsManagerStub.disableSection, SECTION_ID);\n    });\n    it(\"should remove the expiration filter\", () => {\n      feed.onAction({ type: at.UNINIT });\n      assert.calledOnce(fakePageThumbs.removeExpirationFilter);\n    });\n    it(\"should remove the sync and Places observers\", () => {\n      feed.onAction({ type: at.UNINIT });\n      assert.calledWith(\n        global.Services.obs.removeObserver,\n        feed,\n        SYNC_BOOKMARKS_FINISHED_EVENT\n      );\n      assert.calledWith(\n        global.Services.obs.removeObserver,\n        feed,\n        BOOKMARKS_RESTORE_SUCCESS_EVENT\n      );\n      assert.calledWith(\n        global.Services.obs.removeObserver,\n        feed,\n        BOOKMARKS_RESTORE_FAILED_EVENT\n      );\n    });\n  });\n  describe(\"#onAction\", () => {\n    it(\"should relay all actions to DownloadsManager.onAction\", () => {\n      let action = {\n        type: at.COPY_DOWNLOAD_LINK,\n        data: { url: \"foo.png\" },\n        _target: {},\n      };\n      feed.onAction(action);\n      assert.calledWith(feed.downloadsManager.onAction, action);\n    });\n    it(\"should fetch highlights on SYSTEM_TICK\", async () => {\n      await feed.fetchHighlights();\n      feed.fetchHighlights = sinon.spy();\n      feed.onAction({ type: at.SYSTEM_TICK });\n\n      assert.calledOnce(feed.fetchHighlights);\n      assert.calledWithExactly(feed.fetchHighlights, { broadcast: false });\n    });\n    it(\"should fetch highlights on PREF_CHANGED for include prefs\", async () => {\n      feed.fetchHighlights = sinon.spy();\n\n      feed.onAction({\n        type: at.PREF_CHANGED,\n        data: { name: \"section.highlights.includeBookmarks\" },\n      });\n\n      assert.calledOnce(feed.fetchHighlights);\n      assert.calledWith(feed.fetchHighlights, { broadcast: true });\n    });\n    it(\"should not fetch highlights on PREF_CHANGED for other prefs\", async () => {\n      feed.fetchHighlights = sinon.spy();\n\n      feed.onAction({\n        type: at.PREF_CHANGED,\n        data: { name: \"section.topstories.pocketCta\" },\n      });\n\n      assert.notCalled(feed.fetchHighlights);\n    });\n    it(\"should fetch highlights on PLACES_HISTORY_CLEARED\", async () => {\n      await feed.fetchHighlights();\n      feed.fetchHighlights = sinon.spy();\n      feed.onAction({ type: at.PLACES_HISTORY_CLEARED });\n      assert.calledOnce(feed.fetchHighlights);\n      assert.calledWith(feed.fetchHighlights, { broadcast: true });\n    });\n    it(\"should fetch highlights on DOWNLOAD_CHANGED\", async () => {\n      await feed.fetchHighlights();\n      feed.fetchHighlights = sinon.spy();\n      feed.onAction({ type: at.DOWNLOAD_CHANGED });\n      assert.calledOnce(feed.fetchHighlights);\n      assert.calledWith(feed.fetchHighlights, { broadcast: true });\n    });\n    it(\"should fetch highlights on PLACES_LINKS_CHANGED\", async () => {\n      await feed.fetchHighlights();\n      feed.fetchHighlights = sinon.spy();\n      sandbox.stub(feed.linksCache, \"expire\");\n\n      feed.onAction({ type: at.PLACES_LINKS_CHANGED });\n      assert.calledOnce(feed.fetchHighlights);\n      assert.calledWith(feed.fetchHighlights, { broadcast: false });\n      assert.calledOnce(feed.linksCache.expire);\n    });\n    it(\"should fetch highlights on PLACES_LINK_BLOCKED\", async () => {\n      await feed.fetchHighlights();\n      feed.fetchHighlights = sinon.spy();\n      feed.onAction({ type: at.PLACES_LINK_BLOCKED });\n      assert.calledOnce(feed.fetchHighlights);\n      assert.calledWith(feed.fetchHighlights, { broadcast: true });\n    });\n    it(\"should fetch highlights and expire the cache on PLACES_SAVED_TO_POCKET\", async () => {\n      await feed.fetchHighlights();\n      feed.fetchHighlights = sinon.spy();\n      sandbox.stub(feed.linksCache, \"expire\");\n\n      feed.onAction({ type: at.PLACES_SAVED_TO_POCKET });\n      assert.calledOnce(feed.fetchHighlights);\n      assert.calledWith(feed.fetchHighlights, { broadcast: false });\n      assert.calledOnce(feed.linksCache.expire);\n    });\n    it(\"should call fetchHighlights with broadcast false on TOP_SITES_UPDATED\", () => {\n      sandbox.stub(feed, \"fetchHighlights\");\n      feed.onAction({ type: at.TOP_SITES_UPDATED });\n\n      assert.calledOnce(feed.fetchHighlights);\n      assert.calledWithExactly(feed.fetchHighlights, { broadcast: false });\n    });\n    it(\"should call fetchHighlights when deleting or archiving from Pocket\", async () => {\n      feed.fetchHighlights = sinon.spy();\n      feed.onAction({\n        type: at.POCKET_LINK_DELETED_OR_ARCHIVED,\n        data: { pocket_id: 12345 },\n      });\n\n      assert.calledOnce(feed.fetchHighlights);\n      assert.calledWithExactly(feed.fetchHighlights, { broadcast: true });\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/lib/LinksCache.test.js",
    "content": "import { LinksCache } from \"lib/LinksCache.jsm\";\n\ndescribe(\"LinksCache\", () => {\n  it(\"throws when failing request\", async () => {\n    const cache = new LinksCache();\n\n    let rejected = false;\n    try {\n      await cache.request();\n    } catch (error) {\n      rejected = true;\n    }\n\n    assert(rejected);\n  });\n});\n"
  },
  {
    "path": "test/unit/lib/NaiveBayesTextTagger.test.js",
    "content": "import { NaiveBayesTextTagger } from \"lib/NaiveBayesTextTagger.jsm\";\nimport { tokenize } from \"lib/Tokenize.jsm\";\n\nconst EPSILON = 0.00001;\n\ndescribe(\"Naive Bayes Tagger\", () => {\n  describe(\"#tag\", () => {\n    let model = {\n      model_type: \"nb\",\n      positive_class_label: \"military\",\n      positive_class_id: 0,\n      positive_class_threshold_log_prob: -0.5108256237659907,\n      classes: [\n        {\n          log_prior: -0.6881346387364013,\n          feature_log_probs: [\n            -6.2149425847276,\n            -6.829869141665873,\n            -7.124856122235796,\n            -7.116661287797188,\n            -6.694751331313906,\n            -7.11798266787003,\n            -6.5094904366004185,\n            -7.1639509366900604,\n            -7.218981434452414,\n            -6.854842907887801,\n            -7.080328841624584,\n          ],\n        },\n        {\n          log_prior: -0.6981849745899025,\n          feature_log_probs: [\n            -7.0575941199203465,\n            -6.632333513597953,\n            -7.382756370680115,\n            -7.1160793981275905,\n            -8.467120918791892,\n            -8.369201274990882,\n            -8.518506617006922,\n            -7.015756380369387,\n            -7.739036845511857,\n            -9.748294397894645,\n            -3.9353548206941955,\n          ],\n        },\n      ],\n      vocab_idfs: {\n        deal: [0, 5.5058519847862275],\n        easy: [1, 5.5058519847862275],\n        tanks: [2, 5.601162164590552],\n        sites: [3, 5.957837108529285],\n        care: [4, 5.957837108529285],\n        needs: [5, 5.824305715904762],\n        finally: [6, 5.706522680248379],\n        super: [7, 5.264689927969339],\n        heard: [8, 5.5058519847862275],\n        reached: [9, 5.957837108529285],\n        words: [10, 5.070533913528382],\n      },\n    };\n    let instance = new NaiveBayesTextTagger(model);\n\n    let testCases = [\n      {\n        input: \"Finally! Super easy care for your tanks!\",\n        expected: {\n          label: \"military\",\n          logProb: -0.16299510296630082,\n          confident: true,\n        },\n      },\n      {\n        input: \"heard\",\n        expected: {\n          label: \"military\",\n          logProb: -0.4628170738373294,\n          confident: false,\n        },\n      },\n      {\n        input: \"words\",\n        expected: {\n          label: null,\n          logProb: -0.04258339303757985,\n          confident: false,\n        },\n      },\n    ];\n\n    let checkTag = tc => {\n      let actual = instance.tagTokens(tokenize(tc.input));\n      it(`should tag ${tc.input} with ${tc.expected.label}`, () => {\n        assert.equal(tc.expected.label, actual.label);\n      });\n      it(`should give ${tc.input} the correct probability`, () => {\n        let delta = Math.abs(tc.expected.logProb - actual.logProb);\n        assert.isTrue(delta <= EPSILON);\n      });\n    };\n\n    // RELEASE THE TESTS!\n    for (let tc of testCases) {\n      checkTag(tc);\n    }\n  });\n});\n"
  },
  {
    "path": "test/unit/lib/NewTabInit.test.js",
    "content": "import { actionCreators as ac, actionTypes as at } from \"common/Actions.jsm\";\nimport { NewTabInit } from \"lib/NewTabInit.jsm\";\n\ndescribe(\"NewTabInit\", () => {\n  let instance;\n  let store;\n  let STATE;\n  const requestFromTab = portID =>\n    instance.onAction(\n      ac.AlsoToMain({ type: at.NEW_TAB_STATE_REQUEST }, portID)\n    );\n  beforeEach(() => {\n    STATE = {};\n    store = { getState: sinon.stub().returns(STATE), dispatch: sinon.stub() };\n    instance = new NewTabInit();\n    instance.store = store;\n  });\n  it(\"should reply with a copy of the state immediately\", () => {\n    requestFromTab(123);\n\n    const resp = ac.AlsoToOneContent(\n      { type: at.NEW_TAB_INITIAL_STATE, data: STATE },\n      123\n    );\n    assert.calledWith(store.dispatch, resp);\n  });\n  describe(\"early / simulated new tabs\", () => {\n    const simulateTabInit = portID =>\n      instance.onAction({\n        type: at.NEW_TAB_INIT,\n        data: { portID, simulated: true },\n      });\n    beforeEach(() => {\n      simulateTabInit(\"foo\");\n    });\n    it(\"should dispatch if not replied yet\", () => {\n      requestFromTab(\"foo\");\n\n      assert.calledWith(\n        store.dispatch,\n        ac.AlsoToOneContent(\n          { type: at.NEW_TAB_INITIAL_STATE, data: STATE },\n          \"foo\"\n        )\n      );\n    });\n    it(\"should dispatch once for multiple requests\", () => {\n      requestFromTab(\"foo\");\n      requestFromTab(\"foo\");\n      requestFromTab(\"foo\");\n\n      assert.calledOnce(store.dispatch);\n    });\n    describe(\"multiple tabs\", () => {\n      beforeEach(() => {\n        simulateTabInit(\"bar\");\n      });\n      it(\"should dispatch once to each tab\", () => {\n        requestFromTab(\"foo\");\n        requestFromTab(\"bar\");\n        assert.calledTwice(store.dispatch);\n        requestFromTab(\"foo\");\n        requestFromTab(\"bar\");\n\n        assert.calledTwice(store.dispatch);\n      });\n      it(\"should clean up when tabs close\", () => {\n        assert.propertyVal(instance._repliedEarlyTabs, \"size\", 2);\n        instance.onAction(ac.AlsoToMain({ type: at.NEW_TAB_UNLOAD }, \"foo\"));\n        assert.propertyVal(instance._repliedEarlyTabs, \"size\", 1);\n        instance.onAction(ac.AlsoToMain({ type: at.NEW_TAB_UNLOAD }, \"foo\"));\n        assert.propertyVal(instance._repliedEarlyTabs, \"size\", 1);\n        instance.onAction(ac.AlsoToMain({ type: at.NEW_TAB_UNLOAD }, \"bar\"));\n        assert.propertyVal(instance._repliedEarlyTabs, \"size\", 0);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/lib/NmfTextTagger.test.js",
    "content": "import { NmfTextTagger } from \"lib/NmfTextTagger.jsm\";\nimport { tokenize } from \"lib/Tokenize.jsm\";\n\nconst EPSILON = 0.00001;\n\ndescribe(\"NMF Tagger\", () => {\n  describe(\"#tag\", () => {\n    // The numbers in this model were pulled from existing trained model.\n    let model = {\n      document_topic: {\n        environment: [\n          0.05313956429537541,\n          0.07314019377743895,\n          0.03247190024863182,\n          0.016189529772591395,\n          0.003812317145412572,\n          0.03863075834647775,\n          0.007495425135831521,\n          0.005100298003919777,\n          0.005245622179405364,\n          0.036196010766427554,\n          0.02189970342121833,\n          0.03514130992119014,\n          0.001248114096050196,\n          0.0030908722594824665,\n          0.0023874256586350626,\n          0.008533674814792993,\n          0.0009424690250135675,\n          0.01603124888144218,\n          0.00752822798092765,\n          0.0039046678154748796,\n          0.03521776907836766,\n          0.00614546613169027,\n          0.0008272200196643818,\n          0.01405638079154697,\n          0.001990670259485496,\n          0.002803666919676377,\n          0.013841677883061631,\n          0.004093362693745272,\n          0.009310678536276432,\n          0.006158920150866703,\n          0.006821027337091937,\n          0.002712031105462971,\n          0.009093298611644996,\n          0.014642160500331744,\n          0.0067239941045715386,\n          0.007150418784462898,\n          0.0064652818600521265,\n          0.0006735690394489199,\n          0.02063188588742841,\n          0.003213083349614106,\n          0.0031998068360970093,\n          0.00264520606931871,\n          0.008854824468146531,\n          0.0024170562884908786,\n          0.0013705390639746128,\n          0.0030575940757273288,\n          0.010417378215688392,\n          0.002356164040132228,\n          0.0026710154645455007,\n          0.0007295327370144145,\n          0.0585307418954327,\n          0.0037987763460599574,\n          0.003199095437138493,\n          0.004368800434950577,\n          0.005087168372751965,\n          0.0011100904433965942,\n          0.01700096791869979,\n          0.01929226435023826,\n          0.010536397909643058,\n          0.001734999985783697,\n          0.003852807194017686,\n          0.007916805773686475,\n          0.028375307444815964,\n          0.0012422599635274355,\n          0.0009298594944844238,\n          0.02095410849846837,\n          0.0017269844428419192,\n          0.002152880993141985,\n          0.0030226616228192387,\n          0.004804812297400959,\n          0.0012383636748462198,\n          0.006991278216261148,\n          0.0013747035300597538,\n          0.002041541234639563,\n          0.012076270996247411,\n          0.006643837514421182,\n          0.003974012776560734,\n          0.015794539051705442,\n          0.007601190171659186,\n          0.016474925942594837,\n          0.002729423078513777,\n          0.007635146179880609,\n          0.013457547041824648,\n          0.0007592338429017099,\n          0.002947096673767141,\n          0.006371935735541048,\n          0.003356178481568716,\n          0.00451933490245723,\n          0.0019006306992329104,\n          0.013048046603391707,\n          0.023541628496101297,\n          0.027659066125377194,\n          0.002312727786055524,\n          0.0014189157259186062,\n          0.01963766030236683,\n          0.0026014761547439634,\n          0.002333697870992923,\n          0.003401734295211338,\n          0.002522073778255918,\n          0.0015769783084977752,\n        ],\n        space: [\n          0.045976774394786174,\n          0.04386532305052323,\n          0.03346748817597193,\n          0.008498345884036708,\n          0.005802390890667938,\n          0.0017673346473868704,\n          0.00468037374691276,\n          0.0036807899985757367,\n          0.0034951488381868424,\n          0.015073756869093244,\n          0.006784747891785806,\n          0.03069702365741547,\n          0.004945214461908244,\n          0.002527030239506901,\n          0.0012201743197690308,\n          0.010191409658936534,\n          0.0013882500616525532,\n          0.014559679471816162,\n          0.005308140956577744,\n          0.002067005832569046,\n          0.006092496689239475,\n          0.0029308442356851265,\n          0.0006407392160713908,\n          0.01669972147417425,\n          0.0018920321527190246,\n          0.002436089537269062,\n          0.05542174181989591,\n          0.006448761215865303,\n          0.012804154851567844,\n          0.014553974971946687,\n          0.004927456148063145,\n          0.006085620881900181,\n          0.011626122370522652,\n          0.002994267915422563,\n          0.0038291031528493898,\n          0.006987917175322377,\n          0.00719289436611732,\n          0.0008398926158042337,\n          0.019068654506361523,\n          0.004453895285397824,\n          0.00401164781243836,\n          0.0031309255764704544,\n          0.013210118660087334,\n          0.0015542151889036313,\n          0.0013951089590218057,\n          0.002790924761398501,\n          0.008739250167366135,\n          0.0027834569638271025,\n          0.09198161284531065,\n          0.0019488047187835441,\n          0.001739971582806101,\n          0.005113637251322287,\n          0.12140493794373561,\n          0.005535368890812829,\n          0.004198222617607059,\n          0.0010670629105233682,\n          0.005298717616708989,\n          0.0048291586850982855,\n          0.005140125537186181,\n          0.0011663683373124493,\n          0.0024499638218810943,\n          0.012532772497286819,\n          0.0015564613278042862,\n          0.0012252899339204029,\n          0.0005095187051357676,\n          0.0035442657060978655,\n          0.014030578705118285,\n          0.0017653534252553718,\n          0.004026729875153457,\n          0.004002067082856801,\n          0.00809773970333208,\n          0.017160384509220625,\n          0.002981945110677171,\n          0.0018338446554387704,\n          0.0031886913904107484,\n          0.004654622711785796,\n          0.0053886727821435415,\n          0.009023511029300392,\n          0.005246967669202147,\n          0.022806469628558337,\n          0.0035142224878495355,\n          0.006793295047927272,\n          0.017396620747821886,\n          0.000922278971300957,\n          0.001695889413253992,\n          0.007015197552957029,\n          0.003908581792868586,\n          0.010136260994789877,\n          0.0032880552208979508,\n          0.0039712539426523625,\n          0.009672046620728448,\n          0.007290428293346,\n          0.0017814796852793386,\n          0.0005388988974780036,\n          0.013936726486762537,\n          0.003427738251710856,\n          0.002206664729558829,\n          0.05072392472622557,\n          0.004424158921356747,\n          0.0003680061331891622,\n        ],\n        biology: [\n          0.054433533850037796,\n          0.039689474154513994,\n          0.027661000660240884,\n          0.021655563357213067,\n          0.007862624595639219,\n          0.006280655377019006,\n          0.013407714984668861,\n          0.004038592819712647,\n          0.009652765217013826,\n          0.0011353987945632667,\n          0.00925298156804724,\n          0.004870163054917538,\n          0.04911204317171355,\n          0.006921538451191124,\n          0.004003624507234068,\n          0.016600722822360296,\n          0.002179735905957767,\n          0.010801493818182368,\n          0.00918922860910538,\n          0.022115576350545514,\n          0.0027720850555002148,\n          0.003290714340925284,\n          0.0006359939927595049,\n          0.020564054347194806,\n          0.019590591011010666,\n          0.0029008397180383077,\n          0.030414664509122412,\n          0.002864704837438281,\n          0.030933936414333993,\n          0.00222576969791357,\n          0.007077232390623289,\n          0.005876547862506722,\n          0.016917705934608753,\n          0.016466207380001166,\n          0.006648808144677746,\n          0.017876914915160164,\n          0.008216930648675583,\n          0.0026813239798232098,\n          0.012171904585413245,\n          0.012319763594831614,\n          0.003909608203628946,\n          0.003205613981613637,\n          0.027729523430009183,\n          0.0019938396819227074,\n          0.002752482544417343,\n          0.0016746657427111145,\n          0.019564250521109314,\n          0.027250898086440583,\n          0.000954251437229793,\n          0.0020431321836649734,\n          0.0014636128217840221,\n          0.006821766389705783,\n          0.003272989792090916,\n          0.011086677363737012,\n          0.0044279892365732595,\n          0.0029213721398486203,\n          0.013081117655947345,\n          0.012102962176204816,\n          0.0029165848047082825,\n          0.002363073972325097,\n          0.0028567640089643695,\n          0.013692951578614878,\n          0.0013189478722657382,\n          0.0030662419379415885,\n          0.001688218039583749,\n          0.0007806438728749603,\n          0.025458033834110355,\n          0.009584308792578437,\n          0.0033243840056188263,\n          0.0068361098488461045,\n          0.005178034666939756,\n          0.006831575853694424,\n          0.010170774789130092,\n          0.004639315532453418,\n          0.00655511046953238,\n          0.005661100806175219,\n          0.006238755352678196,\n          0.023282136482285103,\n          0.007790828526461584,\n          0.011840304456780202,\n          0.0021953903460442225,\n          0.011205225479328193,\n          0.01665869590158306,\n          0.0009257333679666402,\n          0.0032380769616003604,\n          0.007379754534437712,\n          0.01804771060116468,\n          0.02540492978451049,\n          0.0027900782593570507,\n          0.0029721824342474694,\n          0.005666888959879564,\n          0.003629523931553047,\n          0.0017838703067849428,\n          0.004996486217852931,\n          0.006086510142627035,\n          0.0023570031997685236,\n          0.002718397814380002,\n          0.003908858478916721,\n          0.02080129902865465,\n          0.005591305783253238,\n        ],\n      },\n      topic_word: [\n        [\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.003173633134427233,\n          0.0,\n          0.0,\n          0.0019409914586816176,\n          0.0,\n        ],\n        [\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          5.135548639746091e-5,\n          0.0,\n          0.0,\n          0.0,\n          0.00015384770766669982,\n        ],\n        [\n          0.0,\n          0.0,\n          0.0005001441880557176,\n          0.0,\n          0.0,\n          0.0012069823147301646,\n          0.02401141538644239,\n          8.831990149479376e-5,\n          0.001813504147854849,\n          0.0,\n        ],\n        [\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0003577161362340021,\n          0.0005744157863408606,\n          0.0,\n          0.0,\n          0.0,\n        ],\n        [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],\n        [\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.002662246533243532,\n          0.0,\n          0.0,\n          0.0008394369973758684,\n          0.0,\n        ],\n        [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],\n        [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],\n        [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],\n        [\n          0.0,\n          4.768637450522633e-5,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0010421065429755969,\n          0.0,\n          0.0,\n          2.3210938729937306e-5,\n        ],\n        [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],\n        [\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0006034363278588148,\n          0.001690622339085902,\n          0.0,\n          0.0,\n        ],\n        [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],\n        [0.0, 0.0, 0.0, 0.0, 0.0, 0.004257728522853072, 0.0, 0.0, 0.0, 0.0],\n        [\n          0.0007238839225620208,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0009507496006759083,\n          0.0012635532859311572,\n          0.0,\n          0.0,\n          0.0,\n        ],\n        [\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          1.2699264109324263e-5,\n          0.00032868342552128994,\n          0.0,\n        ],\n        [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],\n        [\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0011157667743487598,\n          0.001278875789622101,\n          9.011724853181247e-6,\n          0.0,\n          3.22069766200917e-5,\n          0.004124963644732435,\n        ],\n        [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],\n        [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],\n        [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.00011961487736485771],\n        [0.0, 0.0, 0.0, 5.734703813314615e-5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],\n        [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 4.0340264022466226e-5, 0.0, 0.0],\n        [0.0, 0.0, 0.0, 0.0, 0.0, 0.00039701897786057513, 0.0, 0.0, 0.0, 0.0],\n        [\n          0.0,\n          0.0,\n          0.0,\n          0.19635202968946042,\n          0.0,\n          0.0008873887898279083,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n        ],\n        [\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          1.552973162326247e-5,\n          0.0,\n          0.002284331845105356,\n          0.0,\n          0.0,\n        ],\n        [\n          0.0,\n          0.0,\n          0.005561738919282601,\n          0.0,\n          0.0,\n          0.0,\n          0.010700323065082812,\n          0.0,\n          0.0005795117202094265,\n          0.0,\n        ],\n        [0.0, 0.0, 0.0, 0.0, 0.0, 0.0005085828329663487, 0.0, 0.0, 0.0, 0.0],\n        [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],\n        [\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.029261090049475084,\n          0.0020864946050332834,\n          0.0018513709831557076,\n          0.0,\n          0.0,\n          0.0,\n        ],\n        [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0008328286790309667, 0.0, 0.0, 0.0],\n        [0.0, 0.0, 0.0, 0.0, 0.0, 0.0013227647245223537, 0.0, 0.0, 0.0, 0.0],\n        [\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0024010554774254685,\n          5.357245317969706e-5,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n        ],\n        [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],\n        [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0014484032312145462, 0.0],\n        [0.0, 0.0, 0.0, 0.0, 0.0, 0.0012081428144960678, 0.0, 0.0, 0.0, 0.0],\n        [\n          0.0,\n          0.0,\n          0.000616488580813398,\n          0.0,\n          0.0,\n          0.0017954524796671627,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n        ],\n        [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],\n        [0.0, 0.0, 0.0, 0.0, 0.0, 0.0006660554263924299, 0.0, 0.0, 0.0, 0.0],\n        [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],\n        [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0011891151421092303, 0.0, 0.0],\n        [\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0024885434472066534,\n          0.0,\n          0.0010165824086743897,\n          0.0,\n          0.0,\n        ],\n        [\n          0.0,\n          5.692292246819767e-5,\n          0.0,\n          0.0,\n          0.001006289633741549,\n          0.0,\n          0.0,\n          0.001897882990870404,\n          0.0,\n          0.0,\n        ],\n        [\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.00010646854330751878,\n          0.0,\n          0.0013480243353754932,\n          0.0,\n        ],\n        [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],\n        [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0002608785715957589, 0.0],\n        [\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0010620422134845085,\n          0.0,\n          0.0,\n          0.0002032215308376943,\n          0.0,\n        ],\n        [\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0008928062238389307,\n          0.0,\n          0.0,\n          5.727265080002417e-5,\n          0.0,\n        ],\n        [\n          0.0,\n          0.0,\n          0.06061253593083364,\n          0.0,\n          0.02739898181912798,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n        ],\n        [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],\n        [\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0014338134220455178,\n          0.0,\n          0.0011276871850520397,\n          0.002840121913315777,\n        ],\n        [0.0008014293374641945, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],\n        [\n          0.0,\n          0.000345858724152025,\n          0.013078498367906305,\n          0.0,\n          0.002815596608197659,\n          0.0,\n          0.0,\n          0.0030778986683343023,\n          0.0,\n          0.0,\n        ],\n        [0.0, 0.0, 0.0, 0.0, 0.0, 0.0010177321509216356, 0.0, 0.0, 0.0, 0.0],\n        [0.0, 0.0, 0.0, 0.0, 0.00015333347872060042, 0.0, 0.0, 0.0, 0.0, 0.0],\n        [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0009655934464519347, 0.0],\n        [\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0008542046515290346,\n          0.0,\n          0.0,\n          0.00016472517230317488,\n          0.0,\n        ],\n        [\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0007759590139787148,\n          0.0037535348789227703,\n          0.0007205740927611773,\n        ],\n        [\n          0.0,\n          0.0,\n          0.0010313963595627862,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0069665132800572115,\n          0.0,\n        ],\n        [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],\n        [\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0006880323929924655,\n          9.207429290830475e-5,\n          0.0,\n          0.0,\n          0.0,\n        ],\n        [0.0, 0.0, 0.0, 0.0, 0.0, 0.0008404475484102756, 0.0, 0.0, 0.0, 0.0],\n        [\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.00016603822882009137,\n          0.0,\n          0.0,\n          0.0,\n          0.0004386724451378034,\n        ],\n        [0.0, 0.0, 0.0, 0.0, 0.0, 0.003971386830918022, 0.0, 0.0, 0.0, 0.0],\n        [0.000983926199078037, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],\n        [0.0, 0.0, 0.0, 0.0, 0.0, 0.001299108775819868, 0.0, 0.0, 0.0, 0.0],\n        [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],\n        [\n          0.16326515307916822,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0028677496385613155,\n          0.023677620702293598,\n          0.0,\n          0.0,\n          0.0,\n        ],\n        [0.0, 0.0, 5.737710913345495e-6, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],\n        [\n          0.0,\n          0.0,\n          0.0002081792662367579,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0002840163488982256,\n        ],\n        [0.0, 0.0, 0.0, 0.0, 0.0005021534925351664, 0.0, 0.0, 0.0, 0.0, 0.0],\n        [\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.001057424953719077,\n          0.0,\n          0.003578658690485632,\n          0.0,\n          0.0,\n        ],\n        [\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.00022950619982206556,\n          0.0018791783657735252,\n          0.0008530683004027156,\n          4.5513911743540586e-5,\n          0.0,\n        ],\n        [0.0, 0.0, 0.0, 0.0, 0.0, 0.0045523319463242765, 0.0, 0.0, 0.0, 0.0],\n        [\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0006160628426134845,\n          0.0,\n          0.0023393152617350653,\n          0.0,\n          0.0,\n          0.0012979890699731222,\n        ],\n        [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],\n        [\n          0.0,\n          0.0,\n          0.003391399407584813,\n          0.0,\n          0.0,\n          0.000719659722017165,\n          0.0,\n          0.004722518573572638,\n          0.002758841738663124,\n          0.0,\n        ],\n        [\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.002127862313876461,\n          0.0,\n          0.005031998155190167,\n          0.0,\n          0.0,\n          0.0,\n        ],\n        [\n          0.0,\n          0.0,\n          0.00055401373160389,\n          0.0,\n          0.0,\n          0.000333325450244618,\n          0.0017824446558959168,\n          0.0011398506826041158,\n          0.0,\n          0.0006366915431430632,\n        ],\n        [\n          0.0,\n          0.21687336139378274,\n          0.0,\n          0.0,\n          0.0,\n          0.0030345303266644387,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n        ],\n        [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],\n        [\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0012637173523723526,\n          0.0,\n          0.0010158476831041915,\n          0.0035425832276585615,\n          0.0,\n        ],\n        [\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0015451984659512325,\n          0.019909953764629045,\n          0.0013484737840911303,\n          0.0033472098053086113,\n          0.0016951819626954759,\n        ],\n        [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],\n        [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],\n        [\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.00015923419851654453,\n          0.0,\n          0.0024056492047359367,\n        ],\n        [\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.01305313280419075,\n          0.00014197157780982973,\n          0.0,\n          0.0,\n        ],\n        [\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.000746430999979358,\n          0.0,\n          0.0010041202546700189,\n          0.004557016648181857,\n          0.0,\n        ],\n        [\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.00021372865758801545,\n          0.00025925151316940747,\n          0.0,\n        ],\n        [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.001658746582791234, 0.0],\n        [\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.00973640859923001,\n          0.0012404719999980969,\n          0.0006365355864806626,\n          0.0008291013715577852,\n          0.0,\n          0.0,\n        ],\n        [\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.001473459191608214,\n          0.0,\n          0.0,\n          0.0009195459918865811,\n          0.002012929485852207,\n        ],\n        [\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0005850456523130979,\n          0.0,\n          0.00014396718214395852,\n          0.0,\n        ],\n        [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0011858302272740567, 0.0],\n        [\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0046803403116507545,\n          0.002083219444498354,\n          0.0,\n          0.0,\n          0.0,\n          0.006104495765365948,\n        ],\n        [\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.005456944646675863,\n          0.0,\n          0.00011428354610339084,\n          0.0,\n          0.0,\n        ],\n        [0.0, 0.0, 0.0, 0.0, 0.0, 0.0013384597578988894, 0.0, 0.0, 0.0, 0.0],\n        [\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0018450592044551373,\n          0.0,\n          0.005182965872305058,\n          0.0,\n          0.0,\n        ],\n        [\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0,\n          0.0003041074021307749,\n          0.0,\n          0.0020827735275448823,\n          0.0,\n          0.0008494429669380388,\n        ],\n        [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],\n      ],\n      vocab_idfs: {\n        blood: [0, 5.0948820521571045],\n        earth: [1, 4.2248041634380815],\n        rocket: [2, 5.666668375712782],\n        brain: [3, 4.616846251214104],\n        mars: [4, 6.226284163648205],\n        nothing: [5, 5.270772718620769],\n        nada: [6, 4.815297189937943],\n        star: [7, 6.38880309314598],\n        zilch: [8, 5.889811927026992],\n        soil: [9, 7.14257489552236],\n      },\n    };\n\n    let instance = new NmfTextTagger(model);\n\n    let testCases = [\n      {\n        input: \"blood is in the brain\",\n        expected: {\n          environment: 0.00037336337061919943,\n          space: 0.0003307690554984028,\n          biology: 0.0026549079818439627,\n        },\n      },\n\n      {\n        input: \"rocket to the star\",\n        expected: {\n          environment: 0.0002855180592590448,\n          space: 0.004006242743506598,\n          biology: 0.0003094182371360131,\n        },\n      },\n      {\n        input: \"rocket to the star mars\",\n        expected: {\n          environment: 0.0004180326651780644,\n          space: 0.003844259295376754,\n          biology: 0.0003135623817729136,\n        },\n      },\n      {\n        input: \"rocket rocket rocket\",\n        expected: {\n          environment: 0.00033052002469507015,\n          space: 0.007519787053895712,\n          biology: 0.00031862864995569246,\n        },\n      },\n      {\n        input: \"nothing nada rocket\",\n        expected: {\n          environment: 0.0008597524218029812,\n          space: 0.0035401031629944506,\n          biology: 0.000950627767326667,\n        },\n      },\n      {\n        input: \"rocket\",\n        expected: {\n          environment: 0.00033052002469507015,\n          space: 0.007519787053895712,\n          biology: 0.00031862864995569246,\n        },\n      },\n      {\n        input: \"this sentence is out of vocabulary\",\n        expected: {\n          environment: 0.0,\n          space: 0.0,\n          biology: 0.0,\n        },\n      },\n      {\n        input: \"this sentence is out of vocabulary except for rocket\",\n        expected: {\n          environment: 0.00033052002469507015,\n          space: 0.007519787053895712,\n          biology: 0.00031862864995569246,\n        },\n      },\n    ];\n\n    let checkTag = tc => {\n      let actual = instance.tagTokens(tokenize(tc.input));\n      it(`should score ${tc.input} correctly`, () => {\n        Object.keys(actual).forEach(tag => {\n          let delta = Math.abs(tc.expected[tag] - actual[tag]);\n          assert.isTrue(delta <= EPSILON);\n        });\n      });\n    };\n\n    // RELEASE THE TESTS!\n    for (let tc of testCases) {\n      checkTag(tc);\n    }\n  });\n});\n"
  },
  {
    "path": "test/unit/lib/PersistentCache.test.js",
    "content": "import { GlobalOverrider } from \"test/unit/utils\";\nimport { PersistentCache } from \"lib/PersistentCache.jsm\";\n\ndescribe(\"PersistentCache\", () => {\n  let fakeOS;\n  let fakeJsonParse;\n  let fakeFetch;\n  let cache;\n  let filename = \"cache.json\";\n  let reportErrorStub;\n  let globals;\n  let sandbox;\n\n  beforeEach(() => {\n    globals = new GlobalOverrider();\n    sandbox = sinon.createSandbox();\n    fakeOS = {\n      Constants: { Path: { localProfileDir: \"/foo/bar\" } },\n      File: {\n        writeAtomic: sinon.stub().returns(Promise.resolve()),\n      },\n      Path: { join: () => filename },\n    };\n    fakeJsonParse = sandbox.stub().resolves({});\n    fakeFetch = sandbox.stub().resolves({ json: fakeJsonParse });\n    reportErrorStub = sandbox.stub();\n    globals.set(\"OS\", fakeOS);\n    globals.set(\"Cu\", { reportError: reportErrorStub });\n    globals.set(\"fetch\", fakeFetch);\n\n    cache = new PersistentCache(filename);\n  });\n  afterEach(() => {\n    globals.restore();\n    sandbox.restore();\n  });\n\n  describe(\"#get\", () => {\n    it(\"tries to fetch the file\", async () => {\n      await cache.get(\"foo\");\n      assert.calledOnce(fakeFetch);\n    });\n    it(\"doesnt try to parse file if it doesn't exist\", async () => {\n      fakeFetch.throws();\n      await cache.get(\"foo\");\n      assert.notCalled(fakeJsonParse);\n    });\n    it(\"doesnt try to fetch the file if it was already loaded\", async () => {\n      await cache._load();\n      fakeFetch.resetHistory();\n      await cache.get(\"foo\");\n      assert.notCalled(fakeFetch);\n    });\n    it(\"should catch and report errors\", async () => {\n      fakeJsonParse.throws();\n      await cache._load();\n      assert.calledOnce(reportErrorStub);\n    });\n    it(\"returns data for a given cache key\", async () => {\n      fakeJsonParse.resolves({ foo: \"bar\" });\n      let value = await cache.get(\"foo\");\n      assert.equal(value, \"bar\");\n    });\n    it(\"returns undefined for a cache key that doesn't exist\", async () => {\n      let value = await cache.get(\"baz\");\n      assert.equal(value, undefined);\n    });\n    it(\"returns all the data if no cache key is specified\", async () => {\n      fakeJsonParse.resolves({ foo: \"bar\" });\n      let value = await cache.get();\n      assert.deepEqual(value, { foo: \"bar\" });\n    });\n  });\n\n  describe(\"#set\", () => {\n    it(\"tries to fetch the file on the first set\", async () => {\n      await cache.set(\"foo\", { x: 42 });\n      assert.calledOnce(fakeFetch);\n    });\n    it(\"doesnt try to fetch the file if it was already loaded\", async () => {\n      cache = new PersistentCache(filename, true);\n      await cache._load();\n      fakeFetch.resetHistory();\n      await cache.set(\"foo\", { x: 42 });\n      assert.notCalled(fakeFetch);\n    });\n    it(\"tries to fetch the file on the first set\", async () => {\n      await cache.set(\"foo\", { x: 42 });\n      assert.calledOnce(fakeFetch);\n    });\n    it(\"sets a string value\", async () => {\n      const key = \"testkey\";\n      const value = \"testvalue\";\n      await cache.set(key, value);\n      const cachedValue = await cache.get(key);\n      assert.equal(cachedValue, value);\n    });\n    it(\"sets an object value\", async () => {\n      const key = \"testkey\";\n      const value = { x: 1, y: 2, z: 3 };\n      await cache.set(key, value);\n      const cachedValue = await cache.get(key);\n      assert.deepEqual(cachedValue, value);\n    });\n    it(\"writes the data to file\", async () => {\n      const key = \"testkey\";\n      const value = { x: 1, y: 2, z: 3 };\n      fakeOS.File.exists = async () => false;\n      await cache.set(key, value);\n      assert.calledOnce(fakeOS.File.writeAtomic);\n      assert.calledWith(\n        fakeOS.File.writeAtomic,\n        filename,\n        `{\"testkey\":{\"x\":1,\"y\":2,\"z\":3}}`,\n        { tmpPath: `${filename}.tmp` }\n      );\n    });\n    it(\"throws when failing the file\", async () => {\n      sandbox.stub(OS.Path, \"join\").throws(\"bad file\");\n\n      let rejected = false;\n      try {\n        await cache.set(\"key\", \"val\");\n      } catch (error) {\n        rejected = true;\n      }\n\n      assert(rejected);\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/lib/PersonalityProvider.test.js",
    "content": "import { GlobalOverrider } from \"test/unit/utils\";\nimport injector from \"inject!lib/PersonalityProvider.jsm\";\n\nconst TIME_SEGMENTS = [\n  { id: \"hour\", startTime: 3600, endTime: 0, weightPosition: 1 },\n  { id: \"day\", startTime: 86400, endTime: 3600, weightPosition: 0.75 },\n  { id: \"week\", startTime: 604800, endTime: 86400, weightPosition: 0.5 },\n  { id: \"weekPlus\", startTime: null, endTime: 604800, weightPosition: 0.25 },\n];\n\nconst PARAMETER_SETS = {\n  paramSet1: {\n    recencyFactor: 0.5,\n    frequencyFactor: 0.5,\n    combinedDomainFactor: 0.5,\n    perfectFrequencyVisits: 10,\n    perfectCombinedDomainScore: 2,\n    multiDomainBoost: 0.1,\n    itemScoreFactor: 0,\n  },\n  paramSet2: {\n    recencyFactor: 1,\n    frequencyFactor: 0.7,\n    combinedDomainFactor: 0.8,\n    perfectFrequencyVisits: 10,\n    perfectCombinedDomainScore: 2,\n    multiDomainBoost: 0.1,\n    itemScoreFactor: 0,\n  },\n};\n\ndescribe(\"Personality Provider\", () => {\n  let instance;\n  let PersonalityProvider;\n  let globals;\n  let NaiveBayesTextTaggerStub;\n  let NmfTextTaggerStub;\n  let RecipeExecutorStub;\n  let baseURLStub;\n\n  beforeEach(() => {\n    globals = new GlobalOverrider();\n\n    const testUrl = \"www.somedomain.com\";\n    globals.sandbox\n      .stub(global.Services.io, \"newURI\")\n      .returns({ host: testUrl });\n\n    globals.sandbox.stub(global.PlacesUtils.history, \"executeQuery\").returns({\n      root: {\n        childCount: 1,\n        getChild: index => ({ uri: testUrl, accessCount: 1 }),\n      },\n    });\n    globals.sandbox\n      .stub(global.PlacesUtils.history, \"getNewQuery\")\n      .returns({ TIME_RELATIVE_NOW: 1 });\n    globals.sandbox\n      .stub(global.PlacesUtils.history, \"getNewQueryOptions\")\n      .returns({});\n\n    NaiveBayesTextTaggerStub = globals.sandbox.stub();\n    NmfTextTaggerStub = globals.sandbox.stub();\n    RecipeExecutorStub = globals.sandbox.stub();\n\n    baseURLStub = \"\";\n\n    global.fetch = async server => ({\n      ok: true,\n      json: async () => {\n        if (server === \"services.settings.server/\") {\n          return { capabilities: { attachments: { base_url: baseURLStub } } };\n        }\n        return {};\n      },\n    });\n    globals.sandbox\n      .stub(global.Services.prefs, \"getCharPref\")\n      .callsFake(pref => pref);\n\n    ({ PersonalityProvider } = injector({\n      \"lib/NaiveBayesTextTagger.jsm\": {\n        NaiveBayesTextTagger: NaiveBayesTextTaggerStub,\n      },\n      \"lib/NmfTextTagger.jsm\": { NmfTextTagger: NmfTextTaggerStub },\n      \"lib/RecipeExecutor.jsm\": { RecipeExecutor: RecipeExecutorStub },\n    }));\n\n    instance = new PersonalityProvider(TIME_SEGMENTS, PARAMETER_SETS);\n\n    instance.interestConfig = {\n      history_item_builder: \"history_item_builder\",\n      history_required_fields: [\"a\", \"b\", \"c\"],\n      interest_finalizer: \"interest_finalizer\",\n      item_to_rank_builder: \"item_to_rank_builder\",\n      item_ranker: \"item_ranker\",\n      interest_combiner: \"interest_combiner\",\n    };\n\n    // mock the RecipeExecutor\n    instance.recipeExecutor = {\n      executeRecipe: (item, recipe) => {\n        if (recipe === \"history_item_builder\") {\n          if (item.title === \"fail\") {\n            return null;\n          }\n          return {\n            title: item.title,\n            score: item.frecency,\n            type: \"history_item\",\n          };\n        } else if (recipe === \"interest_finalizer\") {\n          return {\n            title: item.title,\n            score: item.score * 100,\n            type: \"interest_vector\",\n          };\n        } else if (recipe === \"item_to_rank_builder\") {\n          if (item.title === \"fail\") {\n            return null;\n          }\n          return {\n            item_title: item.title,\n            item_score: item.score,\n            type: \"item_to_rank\",\n          };\n        } else if (recipe === \"item_ranker\") {\n          if (item.title === \"fail\" || item.item_title === \"fail\") {\n            return null;\n          }\n          return {\n            title: item.title,\n            score: item.item_score * item.score,\n            type: \"ranked_item\",\n          };\n        }\n        return null;\n      },\n      executeCombinerRecipe: (item1, item2, recipe) => {\n        if (recipe === \"interest_combiner\") {\n          if (\n            item1.title === \"combiner_fail\" ||\n            item2.title === \"combiner_fail\"\n          ) {\n            return null;\n          }\n          if (item1.type === undefined) {\n            item1.type = \"combined_iv\";\n          }\n          if (item1.score === undefined) {\n            item1.score = 0;\n          }\n          return { type: item1.type, score: item1.score + item2.score };\n        }\n        return null;\n      },\n    };\n  });\n\n  afterEach(() => {\n    globals.restore();\n  });\n  describe(\"#init\", () => {\n    it(\"should return correct data for getAffinities\", () => {\n      const affinities = instance.getAffinities();\n      assert.isDefined(affinities.timeSegments);\n      assert.isDefined(affinities.parameterSets);\n    });\n    it(\"should return early and not initialize if getRecipe fails\", async () => {\n      sinon.stub(instance, \"getRecipe\").returns(Promise.resolve());\n      await instance.init();\n      assert.isUndefined(instance.initialized);\n    });\n    it(\"should return early if get recipe fails\", async () => {\n      sinon.stub(instance, \"getRecipe\").returns(Promise.resolve());\n      sinon.stub(instance, \"generateRecipeExecutor\").returns(Promise.resolve());\n      instance.interestConfig = undefined;\n      await instance.init();\n      assert.calledOnce(instance.getRecipe);\n      assert.notCalled(instance.generateRecipeExecutor);\n      assert.isUndefined(instance.initialized);\n      assert.isUndefined(instance.interestConfig);\n    });\n    it(\"should call callback on successful init\", async () => {\n      sinon.stub(instance, \"getRecipe\").returns(Promise.resolve(true));\n      instance.interestConfig = undefined;\n      const callback = globals.sandbox.stub();\n      instance.createInterestVector = async () => ({});\n      sinon\n        .stub(instance, \"generateRecipeExecutor\")\n        .returns(Promise.resolve(true));\n      await instance.init(callback);\n      assert.calledOnce(instance.getRecipe);\n      assert.calledOnce(instance.generateRecipeExecutor);\n      assert.calledOnce(callback);\n      assert.isDefined(instance.interestVector);\n      assert.isTrue(instance.initialized);\n    });\n    it(\"should return early and not initialize if generateRecipeExecutor fails\", async () => {\n      sinon.stub(instance, \"getRecipe\").returns(Promise.resolve(true));\n      sinon.stub(instance, \"generateRecipeExecutor\").returns(Promise.resolve());\n      instance.interestConfig = undefined;\n      await instance.init();\n      assert.calledOnce(instance.getRecipe);\n      assert.isUndefined(instance.initialized);\n    });\n    it(\"should return early and not initialize if createInterestVector fails\", async () => {\n      sinon.stub(instance, \"getRecipe\").returns(Promise.resolve(true));\n      instance.interestConfig = undefined;\n      sinon\n        .stub(instance, \"generateRecipeExecutor\")\n        .returns(Promise.resolve(true));\n      instance.createInterestVector = async () => null;\n      await instance.init();\n      assert.calledOnce(instance.getRecipe);\n      assert.calledOnce(instance.generateRecipeExecutor);\n      assert.isUndefined(instance.initialized);\n    });\n    it(\"should do generic init stuff when calling init with no cache\", async () => {\n      sinon.stub(instance, \"getRecipe\").returns(Promise.resolve(true));\n      instance.interestConfig = undefined;\n      instance.createInterestVector = async () => ({});\n      sinon\n        .stub(instance, \"generateRecipeExecutor\")\n        .returns(Promise.resolve(true));\n      await instance.init();\n      assert.calledOnce(instance.getRecipe);\n      assert.calledOnce(instance.generateRecipeExecutor);\n      assert.isDefined(instance.interestVector);\n      assert.isTrue(instance.initialized);\n    });\n  });\n  describe(\"#remote-settings\", () => {\n    it(\"should return a remote setting for getFromRemoteSettings\", async () => {\n      const settings = await instance.getFromRemoteSettings(\"attachment\");\n      assert.equal(typeof settings, \"object\");\n      assert.equal(settings.length, 1);\n    });\n  });\n  describe(\"#executor\", () => {\n    it(\"should return null if generateRecipeExecutor has no models\", async () => {\n      assert.isNull(await instance.generateRecipeExecutor());\n    });\n    it(\"should not generate taggers if already available\", async () => {\n      instance.taggers = {\n        nbTaggers: [\"first\"],\n        nmfTaggers: { first: \"first\" },\n      };\n      await instance.generateRecipeExecutor();\n      assert.calledOnce(RecipeExecutorStub);\n      const { args } = RecipeExecutorStub.firstCall;\n      assert.equal(args[0].length, 1);\n      assert.equal(args[0], \"first\");\n      assert.equal(args[1].first, \"first\");\n    });\n    it(\"should pass recipe models to getRecipeExecutor on generateRecipeExecutor\", async () => {\n      instance.modelKeys = [\"nb_model_sports\", \"nmf_model_sports\"];\n\n      instance.getFromRemoteSettings = async name => [\n        { recordKey: \"nb_model_sports\", model_type: \"nb\" },\n        {\n          recordKey: \"nmf_model_sports\",\n          model_type: \"nmf\",\n          parent_tag: \"nmf_sports_parent_tag\",\n        },\n      ];\n\n      await instance.generateRecipeExecutor();\n      assert.calledOnce(RecipeExecutorStub);\n      assert.calledOnce(NaiveBayesTextTaggerStub);\n      assert.calledOnce(NmfTextTaggerStub);\n\n      const { args } = RecipeExecutorStub.firstCall;\n      assert.equal(args[0].length, 1);\n      assert.isDefined(args[1].nmf_sports_parent_tag);\n    });\n    it(\"should skip any models not in modelKeys\", async () => {\n      instance.modelKeys = [\"nb_model_sports\"];\n\n      instance.getFromRemoteSettings = async name => [\n        { recordKey: \"nb_model_sports\", model_type: \"nb\" },\n        {\n          recordKey: \"nmf_model_sports\",\n          model_type: \"nmf\",\n          parent_tag: \"nmf_sports_parent_tag\",\n        },\n      ];\n\n      await instance.generateRecipeExecutor();\n      assert.calledOnce(RecipeExecutorStub);\n      assert.calledOnce(NaiveBayesTextTaggerStub);\n      assert.notCalled(NmfTextTaggerStub);\n\n      const { args } = RecipeExecutorStub.firstCall;\n      assert.equal(args[0].length, 1);\n      assert.equal(Object.keys(args[1]).length, 0);\n    });\n    it(\"should skip any models not defined\", async () => {\n      instance.modelKeys = [\"nb_model_sports\", \"nmf_model_sports\"];\n\n      instance.getFromRemoteSettings = async name => [\n        { recordKey: \"nb_model_sports\", model_type: \"nb\" },\n      ];\n      await instance.generateRecipeExecutor();\n      assert.calledOnce(RecipeExecutorStub);\n      assert.calledOnce(NaiveBayesTextTaggerStub);\n      assert.notCalled(NmfTextTaggerStub);\n\n      const { args } = RecipeExecutorStub.firstCall;\n      assert.equal(args[0].length, 1);\n      assert.equal(Object.keys(args[1]).length, 0);\n    });\n  });\n  describe(\"#recipe\", () => {\n    it(\"should get and fetch a new recipe on first getRecipe\", async () => {\n      sinon\n        .stub(instance, \"getFromRemoteSettings\")\n        .returns(Promise.resolve([]));\n      await instance.getRecipe();\n      assert.calledOnce(instance.getFromRemoteSettings);\n      assert.calledWith(\n        instance.getFromRemoteSettings,\n        \"personality-provider-recipe\"\n      );\n    });\n    it(\"should not fetch a recipe on getRecipe if cached\", async () => {\n      sinon\n        .stub(instance, \"getFromRemoteSettings\")\n        .returns(Promise.resolve([]));\n      instance.recipes = [\"blah\"];\n      await instance.getRecipe();\n      assert.notCalled(instance.getFromRemoteSettings);\n    });\n  });\n  describe(\"#createInterestVector\", () => {\n    let mockHistory = [];\n    beforeEach(() => {\n      mockHistory = [\n        {\n          title: \"automotive\",\n          description: \"something about automotive\",\n          url: \"http://example.com/automotive\",\n          frecency: 10,\n        },\n        {\n          title: \"fashion\",\n          description: \"something about fashion\",\n          url: \"http://example.com/fashion\",\n          frecency: 5,\n        },\n        {\n          title: \"tech\",\n          description: \"something about tech\",\n          url: \"http://example.com/tech\",\n          frecency: 1,\n        },\n      ];\n\n      instance.fetchHistory = async () => mockHistory;\n    });\n    afterEach(() => {\n      globals.restore();\n    });\n    it(\"should gracefully handle history entries that fail\", async () => {\n      mockHistory.push({ title: \"fail\" });\n      assert.isNotNull(await instance.createInterestVector());\n    });\n\n    it(\"should fail if the combiner fails\", async () => {\n      mockHistory.push({ title: \"combiner_fail\", frecency: 111 });\n      let actual = await instance.createInterestVector();\n      assert.isNull(actual);\n    });\n\n    it(\"should process history, combine, and finalize\", async () => {\n      let actual = await instance.createInterestVector();\n      assert.equal(actual.score, 1600);\n    });\n  });\n\n  describe(\"#calculateItemRelevanceScore\", () => {\n    it(\"it should return score for uninitialized provider\", () => {\n      instance.initialized = false;\n      assert.equal(instance.calculateItemRelevanceScore({ item_score: 2 }), 2);\n    });\n    it(\"it should return 1 for uninitialized provider and no score\", () => {\n      instance.initialized = false;\n      assert.equal(instance.calculateItemRelevanceScore({}), 1);\n    });\n    it(\"it should return -1 for busted item\", () => {\n      instance.initialized = true;\n      assert.equal(instance.calculateItemRelevanceScore({ title: \"fail\" }), -1);\n    });\n    it(\"it should return -1 for a busted ranking\", () => {\n      instance.initialized = true;\n      instance.interestVector = { title: \"fail\", score: 10 };\n      assert.equal(\n        instance.calculateItemRelevanceScore({ title: \"some item\", score: 6 }),\n        -1\n      );\n    });\n    it(\"it should return a score, and not change with interestVector\", () => {\n      instance.interestVector = { score: 10 };\n      instance.initialized = true;\n      assert.equal(instance.calculateItemRelevanceScore({ score: 2 }), 20);\n      assert.deepEqual(instance.interestVector, { score: 10 });\n    });\n  });\n  describe(\"#fetchHistory\", () => {\n    it(\"should return a history object for fetchHistory\", async () => {\n      const history = await instance.fetchHistory([\"requiredColumn\"], 1, 1);\n      assert.equal(\n        history.sql,\n        `SELECT url, title, visit_count, frecency, last_visit_date, description\\n    FROM moz_places\\n    WHERE last_visit_date >= 1000000\\n    AND last_visit_date < 1000000 AND IFNULL(requiredColumn, '') <> '' LIMIT 30000`\n      );\n      assert.equal(history.options.columns.length, 1);\n      assert.equal(Object.keys(history.options.params).length, 0);\n    });\n  });\n  describe(\"#attachments\", () => {\n    it(\"should sync remote settings collection from onSync\", async () => {\n      sinon.stub(instance, \"deleteAttachment\").returns(Promise.resolve({}));\n      sinon\n        .stub(instance, \"maybeDownloadAttachment\")\n        .returns(Promise.resolve({}));\n\n      await instance.onSync({\n        data: {\n          created: [\"create-1\", \"create-2\"],\n          updated: [\n            { old: \"update-old-1\", new: \"update-new-1\" },\n            { old: \"update-old-2\", new: \"update-new-2\" },\n          ],\n          deleted: [\"delete-2\", \"delete-1\"],\n        },\n      });\n\n      assert(instance.maybeDownloadAttachment.withArgs(\"create-1\").calledOnce);\n      assert(instance.maybeDownloadAttachment.withArgs(\"create-2\").calledOnce);\n      assert(\n        instance.maybeDownloadAttachment.withArgs(\"update-new-1\").calledOnce\n      );\n      assert(\n        instance.maybeDownloadAttachment.withArgs(\"update-new-2\").calledOnce\n      );\n\n      assert(instance.deleteAttachment.withArgs(\"delete-1\").calledOnce);\n      assert(instance.deleteAttachment.withArgs(\"delete-2\").calledOnce);\n      assert(instance.deleteAttachment.withArgs(\"update-old-1\").calledOnce);\n      assert(instance.deleteAttachment.withArgs(\"update-old-2\").calledOnce);\n    });\n    it(\"should write a file from _downloadAttachment\", async () => {\n      const fetchStub = globals.sandbox.stub(global, \"fetch\").resolves({\n        ok: true,\n        arrayBuffer: async () => {},\n      });\n      baseURLStub = \"/\";\n\n      const writeAtomicStub = globals.sandbox\n        .stub(global.OS.File, \"writeAtomic\")\n        .resolves(Promise.resolve());\n      globals.sandbox\n        .stub(global.OS.Path, \"join\")\n        .callsFake((first, second) => first + second);\n\n      globals.set(\"Uint8Array\", class Uint8Array {});\n\n      await instance._downloadAttachment({\n        attachment: { location: \"location\", filename: \"filename\" },\n      });\n\n      const fetchArgs = fetchStub.firstCall.args;\n      assert.equal(fetchArgs[0], \"/location\");\n      const writeArgs = writeAtomicStub.firstCall.args;\n      assert.equal(writeArgs[0], \"/filename\");\n      assert.equal(writeArgs[2].tmpPath, \"/filename.tmp\");\n    });\n    it(\"should call reportError from _downloadAttachment if not valid response\", async () => {\n      globals.sandbox.stub(global, \"fetch\").resolves({ ok: false });\n      globals.sandbox.spy(global.Cu, \"reportError\");\n      baseURLStub = \"/\";\n\n      await instance._downloadAttachment({\n        attachment: { location: \"location\", filename: \"filename\" },\n      });\n      assert.calledWith(Cu.reportError, \"Failed to fetch /location: undefined\");\n    });\n    it(\"should attempt _downloadAttachment three times for maybeDownloadAttachment\", async () => {\n      let existsStub;\n      let statStub;\n      let attachmentStub;\n      sinon.stub(instance, \"_downloadAttachment\").returns(Promise.resolve());\n      sinon.stub(instance, \"_getFileStr\").returns(Promise.resolve(\"1\"));\n      const makeDirStub = globals.sandbox\n        .stub(global.OS.File, \"makeDir\")\n        .returns(Promise.resolve());\n      globals.sandbox\n        .stub(global.OS.Path, \"join\")\n        .callsFake((first, second) => first + second);\n\n      existsStub = globals.sandbox\n        .stub(global.OS.File, \"exists\")\n        .returns(Promise.resolve(true));\n      statStub = globals.sandbox\n        .stub(global.OS.File, \"stat\")\n        .returns(Promise.resolve({ size: \"1\" }));\n\n      attachmentStub = {\n        attachment: {\n          filename: \"file\",\n          hash: \"30\",\n          size: \"1\",\n        },\n      };\n\n      await instance.maybeDownloadAttachment(attachmentStub);\n      assert.calledWith(makeDirStub, \"/\");\n      assert.calledOnce(existsStub);\n      assert.calledOnce(statStub);\n      assert.calledOnce(instance._getFileStr);\n      assert.notCalled(instance._downloadAttachment);\n\n      instance._getFileStr.resetHistory();\n      existsStub.resetHistory();\n      statStub.resetHistory();\n\n      attachmentStub = {\n        attachment: {\n          filename: \"file\",\n          hash: \"31\",\n          size: \"1\",\n        },\n      };\n\n      await instance.maybeDownloadAttachment(attachmentStub);\n      assert.calledThrice(existsStub);\n      assert.calledThrice(statStub);\n      assert.calledThrice(instance._getFileStr);\n      assert.calledThrice(instance._downloadAttachment);\n    });\n    it(\"should remove attachments when calling deleteAttachment\", async () => {\n      const makeDirStub = globals.sandbox\n        .stub(global.OS.File, \"makeDir\")\n        .returns(Promise.resolve());\n      const removeStub = globals.sandbox\n        .stub(global.OS.File, \"remove\")\n        .returns(Promise.resolve());\n      const removeEmptyDirStub = globals.sandbox\n        .stub(global.OS.File, \"removeEmptyDir\")\n        .returns(Promise.resolve());\n      globals.sandbox\n        .stub(global.OS.Path, \"join\")\n        .callsFake((first, second) => first + second);\n      await instance.deleteAttachment({ attachment: { filename: \"filename\" } });\n      assert.calledOnce(makeDirStub);\n      assert.calledOnce(removeStub);\n      assert.calledOnce(removeEmptyDirStub);\n      assert.calledWith(removeStub, \"/filename\", { ignoreAbsent: true });\n    });\n    it(\"should return JSON when calling getAttachment\", async () => {\n      sinon\n        .stub(instance, \"maybeDownloadAttachment\")\n        .returns(Promise.resolve());\n      sinon.stub(instance, \"_getFileStr\").returns(Promise.resolve(\"{}\"));\n      const reportErrorStub = globals.sandbox.stub(global.Cu, \"reportError\");\n      globals.sandbox\n        .stub(global.OS.Path, \"join\")\n        .callsFake((first, second) => first + second);\n      const record = { attachment: { filename: \"filename\" } };\n      let returnValue = await instance.getAttachment(record);\n\n      assert.notCalled(reportErrorStub);\n      assert.calledOnce(instance._getFileStr);\n      assert.calledWith(instance._getFileStr, \"/filename\");\n      assert.calledOnce(instance.maybeDownloadAttachment);\n      assert.calledWith(instance.maybeDownloadAttachment, record);\n      assert.deepEqual(returnValue, {});\n\n      instance._getFileStr.restore();\n      sinon.stub(instance, \"_getFileStr\").returns(Promise.resolve({}));\n      returnValue = await instance.getAttachment(record);\n      assert.calledOnce(reportErrorStub);\n      assert.calledWith(\n        reportErrorStub,\n        \"Failed to load /filename: JSON.parse: unexpected character at line 1 column 2 of the JSON data\"\n      );\n      assert.deepEqual(returnValue, {});\n    });\n    it(\"should read and decode a file with _getFileStr\", async () => {\n      global.OS.File.read = async path => {\n        if (path === \"/filename\") {\n          return \"binaryData\";\n        }\n        return \"\";\n      };\n      globals.set(\"gTextDecoder\", {\n        decode: async binaryData => {\n          if (binaryData === \"binaryData\") {\n            return \"binaryData\";\n          }\n          return \"\";\n        },\n      });\n      const returnValue = await instance._getFileStr(\"/filename\");\n      assert.equal(returnValue, \"binaryData\");\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/lib/PlacesFeed.test.js",
    "content": "import { actionCreators as ac, actionTypes as at } from \"common/Actions.jsm\";\nimport { GlobalOverrider } from \"test/unit/utils\";\nimport { PlacesFeed } from \"lib/PlacesFeed.jsm\";\nconst { HistoryObserver, BookmarksObserver, PlacesObserver } = PlacesFeed;\n\nconst FAKE_BOOKMARK = {\n  bookmarkGuid: \"xi31\",\n  bookmarkTitle: \"Foo\",\n  dateAdded: 123214232,\n  url: \"foo.com\",\n};\nconst TYPE_BOOKMARK = 0; // This is fake, for testing\nconst SOURCES = {\n  DEFAULT: 0,\n  SYNC: 1,\n  IMPORT: 2,\n  RESTORE: 5,\n  RESTORE_ON_STARTUP: 6,\n};\n\nconst BLOCKED_EVENT = \"newtab-linkBlocked\"; // The event dispatched in NewTabUtils when a link is blocked;\n\ndescribe(\"PlacesFeed\", () => {\n  let globals;\n  let sandbox;\n  let feed;\n  beforeEach(() => {\n    globals = new GlobalOverrider();\n    sandbox = globals.sandbox;\n    globals.set(\"NewTabUtils\", {\n      activityStreamProvider: { getBookmark() {} },\n      activityStreamLinks: {\n        addBookmark: sandbox.spy(),\n        deleteBookmark: sandbox.spy(),\n        deleteHistoryEntry: sandbox.spy(),\n        blockURL: sandbox.spy(),\n        addPocketEntry: sandbox.spy(() => Promise.resolve()),\n        deletePocketEntry: sandbox.spy(() => Promise.resolve()),\n        archivePocketEntry: sandbox.spy(() => Promise.resolve()),\n      },\n    });\n    sandbox\n      .stub(global.PlacesUtils.bookmarks, \"TYPE_BOOKMARK\")\n      .value(TYPE_BOOKMARK);\n    sandbox.stub(global.PlacesUtils.bookmarks, \"SOURCES\").value(SOURCES);\n    sandbox.spy(global.PlacesUtils.bookmarks, \"addObserver\");\n    sandbox.spy(global.PlacesUtils.bookmarks, \"removeObserver\");\n    sandbox.spy(global.PlacesUtils.history, \"addObserver\");\n    sandbox.spy(global.PlacesUtils.history, \"removeObserver\");\n    sandbox.spy(global.PlacesUtils.observers, \"addListener\");\n    sandbox.spy(global.PlacesUtils.observers, \"removeListener\");\n    sandbox.spy(global.Services.obs, \"addObserver\");\n    sandbox.spy(global.Services.obs, \"removeObserver\");\n    sandbox.spy(global.Cu, \"reportError\");\n\n    global.Cc[\"@mozilla.org/timer;1\"] = {\n      createInstance() {\n        return {\n          initWithCallback: sinon.stub().callsFake(callback => callback()),\n          cancel: sinon.spy(),\n        };\n      },\n    };\n    feed = new PlacesFeed();\n    feed.store = { dispatch: sinon.spy() };\n  });\n  afterEach(() => globals.restore());\n\n  it(\"should have a HistoryObserver that dispatches to the store\", () => {\n    assert.instanceOf(feed.historyObserver, HistoryObserver);\n    const action = { type: \"FOO\" };\n\n    feed.historyObserver.dispatch(action);\n\n    assert.calledOnce(feed.store.dispatch);\n    assert.equal(feed.store.dispatch.firstCall.args[0].type, action.type);\n  });\n\n  it(\"should have a BookmarksObserver that dispatch to the store\", () => {\n    assert.instanceOf(feed.bookmarksObserver, BookmarksObserver);\n    const action = { type: \"FOO\" };\n\n    feed.bookmarksObserver.dispatch(action);\n\n    assert.calledOnce(feed.store.dispatch);\n    assert.equal(feed.store.dispatch.firstCall.args[0].type, action.type);\n  });\n\n  it(\"should have a PlacesObserver that dispatches to the store\", () => {\n    assert.instanceOf(feed.placesObserver, PlacesObserver);\n    const action = { type: \"FOO\" };\n\n    feed.placesObserver.dispatch(action);\n\n    assert.calledOnce(feed.store.dispatch);\n    assert.equal(feed.store.dispatch.firstCall.args[0].type, action.type);\n  });\n  describe(\"#onAction\", () => {\n    it(\"should add bookmark, history, places, blocked observers on INIT\", () => {\n      feed.onAction({ type: at.INIT });\n\n      assert.calledWith(\n        global.PlacesUtils.history.addObserver,\n        feed.historyObserver,\n        true\n      );\n      assert.calledWith(\n        global.PlacesUtils.bookmarks.addObserver,\n        feed.bookmarksObserver,\n        true\n      );\n      assert.calledWith(\n        global.PlacesUtils.observers.addListener,\n        [\"bookmark-added\"],\n        feed.placesObserver.handlePlacesEvent\n      );\n      assert.calledWith(global.Services.obs.addObserver, feed, BLOCKED_EVENT);\n    });\n    it(\"should remove bookmark, history, places, blocked observers, and timers on UNINIT\", () => {\n      feed.placesChangedTimer = global.Cc[\n        \"@mozilla.org/timer;1\"\n      ].createInstance();\n      let spy = feed.placesChangedTimer.cancel;\n      feed.onAction({ type: at.UNINIT });\n\n      assert.calledWith(\n        global.PlacesUtils.history.removeObserver,\n        feed.historyObserver\n      );\n      assert.calledWith(\n        global.PlacesUtils.bookmarks.removeObserver,\n        feed.bookmarksObserver\n      );\n      assert.calledWith(\n        global.PlacesUtils.observers.removeListener,\n        [\"bookmark-added\"],\n        feed.placesObserver.handlePlacesEvent\n      );\n      assert.calledWith(\n        global.Services.obs.removeObserver,\n        feed,\n        BLOCKED_EVENT\n      );\n      assert.equal(feed.placesChangedTimer, null);\n      assert.calledOnce(spy);\n    });\n    it(\"should block a url on BLOCK_URL\", () => {\n      feed.onAction({\n        type: at.BLOCK_URL,\n        data: { url: \"apple.com\", pocket_id: 1234 },\n      });\n      assert.calledWith(global.NewTabUtils.activityStreamLinks.blockURL, {\n        url: \"apple.com\",\n        pocket_id: 1234,\n      });\n    });\n    it(\"should bookmark a url on BOOKMARK_URL\", () => {\n      const data = { url: \"pear.com\", title: \"A pear\" };\n      const _target = { browser: { ownerGlobal() {} } };\n      feed.onAction({ type: at.BOOKMARK_URL, data, _target });\n      assert.calledWith(\n        global.NewTabUtils.activityStreamLinks.addBookmark,\n        data,\n        _target.browser.ownerGlobal\n      );\n    });\n    it(\"should delete a bookmark on DELETE_BOOKMARK_BY_ID\", () => {\n      feed.onAction({ type: at.DELETE_BOOKMARK_BY_ID, data: \"g123kd\" });\n      assert.calledWith(\n        global.NewTabUtils.activityStreamLinks.deleteBookmark,\n        \"g123kd\"\n      );\n    });\n    it(\"should delete a history entry on DELETE_HISTORY_URL\", () => {\n      feed.onAction({\n        type: at.DELETE_HISTORY_URL,\n        data: { url: \"guava.com\", forceBlock: null },\n      });\n      assert.calledWith(\n        global.NewTabUtils.activityStreamLinks.deleteHistoryEntry,\n        \"guava.com\"\n      );\n      assert.notCalled(global.NewTabUtils.activityStreamLinks.blockURL);\n    });\n    it(\"should delete a history entry on DELETE_HISTORY_URL and force a site to be blocked if specified\", () => {\n      feed.onAction({\n        type: at.DELETE_HISTORY_URL,\n        data: { url: \"guava.com\", forceBlock: \"g123kd\" },\n      });\n      assert.calledWith(\n        global.NewTabUtils.activityStreamLinks.deleteHistoryEntry,\n        \"guava.com\"\n      );\n      assert.calledWith(global.NewTabUtils.activityStreamLinks.blockURL, {\n        url: \"guava.com\",\n        pocket_id: undefined,\n      });\n    });\n    it(\"should call openLinkIn with the correct url and where on OPEN_NEW_WINDOW\", () => {\n      const openLinkIn = sinon.stub();\n      const openWindowAction = {\n        type: at.OPEN_NEW_WINDOW,\n        data: { url: \"foo.com\" },\n        _target: { browser: { ownerGlobal: { openLinkIn } } },\n      };\n\n      feed.onAction(openWindowAction);\n\n      assert.calledOnce(openLinkIn);\n      const [url, where, params] = openLinkIn.firstCall.args;\n      assert.equal(url, \"foo.com\");\n      assert.equal(where, \"window\");\n      assert.propertyVal(params, \"private\", false);\n    });\n    it(\"should call openLinkIn with the correct url, where and privacy args on OPEN_PRIVATE_WINDOW\", () => {\n      const openLinkIn = sinon.stub();\n      const openWindowAction = {\n        type: at.OPEN_PRIVATE_WINDOW,\n        data: { url: \"foo.com\" },\n        _target: { browser: { ownerGlobal: { openLinkIn } } },\n      };\n\n      feed.onAction(openWindowAction);\n\n      assert.calledOnce(openLinkIn);\n      const [url, where, params] = openLinkIn.firstCall.args;\n      assert.equal(url, \"foo.com\");\n      assert.equal(where, \"window\");\n      assert.propertyVal(params, \"private\", true);\n    });\n    it(\"should open link on OPEN_LINK\", () => {\n      const openLinkIn = sinon.stub();\n      const openLinkAction = {\n        type: at.OPEN_LINK,\n        data: { url: \"foo.com\" },\n        _target: {\n          browser: {\n            ownerGlobal: { openLinkIn, whereToOpenLink: e => \"current\" },\n          },\n        },\n      };\n\n      feed.onAction(openLinkAction);\n\n      assert.calledOnce(openLinkIn);\n      const [url, where, params] = openLinkIn.firstCall.args;\n      assert.equal(url, \"foo.com\");\n      assert.equal(where, \"current\");\n      assert.propertyVal(params, \"private\", false);\n      assert.propertyVal(params, \"triggeringPrincipal\", undefined);\n    });\n    it(\"should open link with referrer on OPEN_LINK\", () => {\n      const openLinkIn = sinon.stub();\n      const openLinkAction = {\n        type: at.OPEN_LINK,\n        data: { url: \"foo.com\", referrer: \"foo.com/ref\" },\n        _target: {\n          browser: { ownerGlobal: { openLinkIn, whereToOpenLink: e => \"tab\" } },\n        },\n      };\n\n      feed.onAction(openLinkAction);\n\n      const [, , params] = openLinkIn.firstCall.args;\n      assert.nestedPropertyVal(params, \"referrerInfo.referrerPolicy\", 5);\n      assert.nestedPropertyVal(\n        params,\n        \"referrerInfo.originalReferrer.spec\",\n        \"foo.com/ref\"\n      );\n    });\n    it(\"should mark link with typed bonus as typed before opening OPEN_LINK\", () => {\n      const callOrder = [];\n      sinon\n        .stub(global.PlacesUtils.history, \"markPageAsTyped\")\n        .callsFake(() => {\n          callOrder.push(\"markPageAsTyped\");\n        });\n      const openLinkIn = sinon.stub().callsFake(() => {\n        callOrder.push(\"openLinkIn\");\n      });\n      const openLinkAction = {\n        type: at.OPEN_LINK,\n        data: {\n          typedBonus: true,\n          url: \"foo.com\",\n        },\n        _target: {\n          browser: { ownerGlobal: { openLinkIn, whereToOpenLink: e => \"tab\" } },\n        },\n      };\n\n      feed.onAction(openLinkAction);\n\n      assert.sameOrderedMembers(callOrder, [\"markPageAsTyped\", \"openLinkIn\"]);\n    });\n    it(\"should open the pocket link if it's a pocket story on OPEN_LINK\", () => {\n      const openLinkIn = sinon.stub();\n      const openLinkAction = {\n        type: at.OPEN_LINK,\n        data: { url: \"foo.com\", open_url: \"getpocket.com/foo\", type: \"pocket\" },\n        _target: {\n          browser: {\n            ownerGlobal: { openLinkIn, whereToOpenLink: e => \"current\" },\n          },\n        },\n      };\n\n      feed.onAction(openLinkAction);\n\n      assert.calledOnce(openLinkIn);\n      const [url, where, params] = openLinkIn.firstCall.args;\n      assert.equal(url, \"getpocket.com/foo\");\n      assert.equal(where, \"current\");\n      assert.propertyVal(params, \"private\", false);\n      assert.propertyVal(params, \"triggeringPrincipal\", undefined);\n    });\n    it(\"should call fillSearchTopSiteTerm on FILL_SEARCH_TERM\", () => {\n      sinon.stub(feed, \"fillSearchTopSiteTerm\");\n\n      feed.onAction({ type: at.FILL_SEARCH_TERM });\n\n      assert.calledOnce(feed.fillSearchTopSiteTerm);\n    });\n    it(\"should set the URL bar value to the label value\", () => {\n      const locationBar = { search: sandbox.stub() };\n      const action = {\n        type: at.FILL_SEARCH_TERM,\n        data: { label: \"@Foo\" },\n        _target: { browser: { ownerGlobal: { gURLBar: locationBar } } },\n      };\n\n      feed.fillSearchTopSiteTerm(action);\n\n      assert.calledOnce(locationBar.search);\n      assert.calledWithExactly(locationBar.search, \"@Foo \");\n    });\n    it(\"should call saveToPocket on SAVE_TO_POCKET\", () => {\n      const action = {\n        type: at.SAVE_TO_POCKET,\n        data: { site: { url: \"raspberry.com\", title: \"raspberry\" } },\n        _target: { browser: {} },\n      };\n      sinon.stub(feed, \"saveToPocket\");\n      feed.onAction(action);\n      assert.calledWithExactly(\n        feed.saveToPocket,\n        action.data.site,\n        action._target.browser\n      );\n    });\n    it(\"should call NewTabUtils.activityStreamLinks.addPocketEntry if we are saving a pocket story\", async () => {\n      const action = {\n        data: { site: { url: \"raspberry.com\", title: \"raspberry\" } },\n        _target: { browser: {} },\n      };\n      await feed.saveToPocket(action.data.site, action._target.browser);\n      assert.calledOnce(global.NewTabUtils.activityStreamLinks.addPocketEntry);\n      assert.calledWithExactly(\n        global.NewTabUtils.activityStreamLinks.addPocketEntry,\n        action.data.site.url,\n        action.data.site.title,\n        action._target.browser\n      );\n    });\n    it(\"should reject the promise if NewTabUtils.activityStreamLinks.addPocketEntry rejects\", async () => {\n      const e = new Error(\"Error\");\n      const action = {\n        data: { site: { url: \"raspberry.com\", title: \"raspberry\" } },\n        _target: { browser: {} },\n      };\n      global.NewTabUtils.activityStreamLinks.addPocketEntry = sandbox\n        .stub()\n        .rejects(e);\n      await feed.saveToPocket(action.data.site, action._target.browser);\n      assert.calledWith(global.Cu.reportError, e);\n    });\n    it(\"should broadcast to content if we successfully added a link to Pocket\", async () => {\n      // test in the form that the API returns data based on: https://getpocket.com/developer/docs/v3/add\n      global.NewTabUtils.activityStreamLinks.addPocketEntry = sandbox\n        .stub()\n        .resolves({ item: { open_url: \"pocket.com/itemID\", item_id: 1234 } });\n      const action = {\n        data: { site: { url: \"raspberry.com\", title: \"raspberry\" } },\n        _target: { browser: {} },\n      };\n      await feed.saveToPocket(action.data.site, action._target.browser);\n      assert.equal(\n        feed.store.dispatch.firstCall.args[0].type,\n        at.PLACES_SAVED_TO_POCKET\n      );\n      assert.deepEqual(feed.store.dispatch.firstCall.args[0].data, {\n        url: \"raspberry.com\",\n        title: \"raspberry\",\n        pocket_id: 1234,\n        open_url: \"pocket.com/itemID\",\n      });\n    });\n    it(\"should only broadcast if we got some data back from addPocketEntry\", async () => {\n      global.NewTabUtils.activityStreamLinks.addPocketEntry = sandbox\n        .stub()\n        .resolves(null);\n      const action = {\n        data: { site: { url: \"raspberry.com\", title: \"raspberry\" } },\n        _target: { browser: {} },\n      };\n      await feed.saveToPocket(action.data.site, action._target.browser);\n      assert.notCalled(feed.store.dispatch);\n    });\n    it(\"should call deleteFromPocket on DELETE_FROM_POCKET\", () => {\n      sandbox.stub(feed, \"deleteFromPocket\");\n      feed.onAction({\n        type: at.DELETE_FROM_POCKET,\n        data: { pocket_id: 12345 },\n      });\n\n      assert.calledOnce(feed.deleteFromPocket);\n      assert.calledWithExactly(feed.deleteFromPocket, 12345);\n    });\n    it(\"should catch if deletePocketEntry throws\", async () => {\n      const e = new Error(\"Error\");\n      global.NewTabUtils.activityStreamLinks.deletePocketEntry = sandbox\n        .stub()\n        .rejects(e);\n      await feed.deleteFromPocket(12345);\n\n      assert.calledWith(global.Cu.reportError, e);\n    });\n    it(\"should call NewTabUtils.deletePocketEntry and dispatch POCKET_LINK_DELETED_OR_ARCHIVED when deleting from Pocket\", async () => {\n      await feed.deleteFromPocket(12345);\n\n      assert.calledOnce(\n        global.NewTabUtils.activityStreamLinks.deletePocketEntry\n      );\n      assert.calledWith(\n        global.NewTabUtils.activityStreamLinks.deletePocketEntry,\n        12345\n      );\n\n      assert.calledOnce(feed.store.dispatch);\n      assert.calledWith(feed.store.dispatch, {\n        type: at.POCKET_LINK_DELETED_OR_ARCHIVED,\n      });\n    });\n    it(\"should call archiveFromPocket on ARCHIVE_FROM_POCKET\", async () => {\n      sandbox.stub(feed, \"archiveFromPocket\");\n      await feed.onAction({\n        type: at.ARCHIVE_FROM_POCKET,\n        data: { pocket_id: 12345 },\n      });\n\n      assert.calledOnce(feed.archiveFromPocket);\n      assert.calledWithExactly(feed.archiveFromPocket, 12345);\n    });\n    it(\"should catch if archiveFromPocket throws\", async () => {\n      const e = new Error(\"Error\");\n      global.NewTabUtils.activityStreamLinks.archivePocketEntry = sandbox\n        .stub()\n        .rejects(e);\n      await feed.archiveFromPocket(12345);\n\n      assert.calledWith(global.Cu.reportError, e);\n    });\n    it(\"should call NewTabUtils.archivePocketEntry and dispatch POCKET_LINK_DELETED_OR_ARCHIVED when archiving from Pocket\", async () => {\n      await feed.archiveFromPocket(12345);\n\n      assert.calledOnce(\n        global.NewTabUtils.activityStreamLinks.archivePocketEntry\n      );\n      assert.calledWith(\n        global.NewTabUtils.activityStreamLinks.archivePocketEntry,\n        12345\n      );\n\n      assert.calledOnce(feed.store.dispatch);\n      assert.calledWith(feed.store.dispatch, {\n        type: at.POCKET_LINK_DELETED_OR_ARCHIVED,\n      });\n    });\n    it(\"should call handoffSearchToAwesomebar on HANDOFF_SEARCH_TO_AWESOMEBAR\", () => {\n      const action = {\n        type: at.HANDOFF_SEARCH_TO_AWESOMEBAR,\n        data: { text: \"f\" },\n        meta: { fromTarget: {} },\n        _target: { browser: { ownerGlobal: { gURLBar: { focus: () => {} } } } },\n      };\n      sinon.stub(feed, \"handoffSearchToAwesomebar\");\n      feed.onAction(action);\n      assert.calledWith(feed.handoffSearchToAwesomebar, action);\n    });\n  });\n\n  describe(\"handoffSearchToAwesomebar\", () => {\n    let fakeUrlBar;\n    let listeners;\n\n    beforeEach(() => {\n      fakeUrlBar = {\n        focus: sinon.spy(),\n        search: sinon.spy(),\n        setHiddenFocus: sinon.spy(),\n        removeHiddenFocus: sinon.spy(),\n        addEventListener: (ev, cb) => {\n          listeners[ev] = cb;\n        },\n        removeEventListener: sinon.spy(),\n      };\n      listeners = {};\n    });\n    it(\"should properly handle handoff with no text passed in\", () => {\n      feed.handoffSearchToAwesomebar({\n        _target: { browser: { ownerGlobal: { gURLBar: fakeUrlBar } } },\n        data: {},\n        meta: { fromTarget: {} },\n      });\n      assert.calledOnce(fakeUrlBar.setHiddenFocus);\n      assert.notCalled(fakeUrlBar.search);\n      assert.notCalled(feed.store.dispatch);\n\n      // Now type a character.\n      listeners.keydown({ key: \"f\" });\n      assert.calledOnce(fakeUrlBar.search);\n      assert.calledOnce(fakeUrlBar.removeHiddenFocus);\n      assert.calledOnce(feed.store.dispatch);\n      assert.calledWith(feed.store.dispatch, {\n        meta: {\n          from: \"ActivityStream:Main\",\n          skipMain: true,\n          to: \"ActivityStream:Content\",\n          toTarget: {},\n        },\n        type: \"HIDE_SEARCH\",\n      });\n    });\n    it(\"should properly handle handoff with text data passed in\", () => {\n      feed.handoffSearchToAwesomebar({\n        _target: { browser: { ownerGlobal: { gURLBar: fakeUrlBar } } },\n        data: { text: \"foo\" },\n        meta: { fromTarget: {} },\n      });\n      assert.calledOnce(fakeUrlBar.search);\n      assert.calledWith(fakeUrlBar.search, \"@google foo\");\n      assert.notCalled(fakeUrlBar.focus);\n      assert.notCalled(fakeUrlBar.setHiddenFocus);\n\n      // Now call blur listener.\n      listeners.blur();\n      assert.calledOnce(feed.store.dispatch);\n      assert.calledWith(feed.store.dispatch, {\n        meta: {\n          from: \"ActivityStream:Main\",\n          skipMain: true,\n          to: \"ActivityStream:Content\",\n          toTarget: {},\n        },\n        type: \"SHOW_SEARCH\",\n      });\n    });\n    it(\"should properly handle handoff with text data passed in, in private browsing mode\", () => {\n      global.PrivateBrowsingUtils.isBrowserPrivate = () => true;\n      feed.handoffSearchToAwesomebar({\n        _target: { browser: { ownerGlobal: { gURLBar: fakeUrlBar } } },\n        data: { text: \"foo\" },\n        meta: { fromTarget: {} },\n      });\n      assert.calledOnce(fakeUrlBar.search);\n      assert.calledWith(fakeUrlBar.search, \"@bing foo\");\n      assert.notCalled(fakeUrlBar.focus);\n      assert.notCalled(fakeUrlBar.setHiddenFocus);\n\n      // Now call blur listener.\n      listeners.blur();\n      assert.calledOnce(feed.store.dispatch);\n      assert.calledWith(feed.store.dispatch, {\n        meta: {\n          from: \"ActivityStream:Main\",\n          skipMain: true,\n          to: \"ActivityStream:Content\",\n          toTarget: {},\n        },\n        type: \"SHOW_SEARCH\",\n      });\n      global.PrivateBrowsingUtils.isBrowserPrivate = () => false;\n    });\n    it(\"should SHOW_SEARCH on ESC keydown\", () => {\n      feed.handoffSearchToAwesomebar({\n        _target: { browser: { ownerGlobal: { gURLBar: fakeUrlBar } } },\n        data: { text: \"foo\" },\n        meta: { fromTarget: {} },\n      });\n      assert.calledOnce(fakeUrlBar.search);\n      assert.calledWith(fakeUrlBar.search, \"@google foo\");\n      assert.notCalled(fakeUrlBar.focus);\n\n      // Now call ESC keydown.\n      listeners.keydown({ key: \"Escape\" });\n      assert.calledOnce(feed.store.dispatch);\n      assert.calledWith(feed.store.dispatch, {\n        meta: {\n          from: \"ActivityStream:Main\",\n          skipMain: true,\n          to: \"ActivityStream:Content\",\n          toTarget: {},\n        },\n        type: \"SHOW_SEARCH\",\n      });\n    });\n    it(\"should properly handle no defined search alias\", () => {\n      global.Services.search.defaultEngine.wrappedJSObject.__internalAliases = [];\n      feed.handoffSearchToAwesomebar({\n        _target: { browser: { ownerGlobal: { gURLBar: fakeUrlBar } } },\n        data: { text: \"foo\" },\n        meta: { fromTarget: {} },\n      });\n      assert.calledOnce(fakeUrlBar.search);\n      assert.calledWith(fakeUrlBar.search, \"foo\");\n    });\n  });\n\n  describe(\"#observe\", () => {\n    it(\"should dispatch a PLACES_LINK_BLOCKED action with the url of the blocked link\", () => {\n      feed.observe(null, BLOCKED_EVENT, \"foo123.com\");\n      assert.equal(\n        feed.store.dispatch.firstCall.args[0].type,\n        at.PLACES_LINK_BLOCKED\n      );\n      assert.deepEqual(feed.store.dispatch.firstCall.args[0].data, {\n        url: \"foo123.com\",\n      });\n    });\n    it(\"should not call dispatch if the topic is something other than BLOCKED_EVENT\", () => {\n      feed.observe(null, \"someotherevent\");\n      assert.notCalled(feed.store.dispatch);\n    });\n  });\n\n  describe(\"HistoryObserver\", () => {\n    let dispatch;\n    let observer;\n    beforeEach(() => {\n      dispatch = sandbox.spy();\n      observer = new HistoryObserver(dispatch);\n    });\n    it(\"should have a QueryInterface property\", () => {\n      assert.property(observer, \"QueryInterface\");\n    });\n    describe(\"#onDeleteURI\", () => {\n      it(\"should dispatch a PLACES_LINK_DELETED action with the right url\", async () => {\n        await observer.onDeleteURI({ spec: \"foo.com\" });\n\n        assert.calledWith(dispatch, {\n          type: at.PLACES_LINK_DELETED,\n          data: { url: \"foo.com\" },\n        });\n      });\n    });\n    describe(\"#onClearHistory\", () => {\n      it(\"should dispatch a PLACES_HISTORY_CLEARED action\", () => {\n        observer.onClearHistory();\n        assert.calledWith(dispatch, { type: at.PLACES_HISTORY_CLEARED });\n      });\n    });\n    describe(\"Other empty methods (to keep code coverage happy)\", () => {\n      it(\"should have a various empty functions for xpconnect happiness\", () => {\n        observer.onBeginUpdateBatch();\n        observer.onEndUpdateBatch();\n        observer.onTitleChanged();\n        observer.onFrecencyChanged();\n        observer.onManyFrecenciesChanged();\n        observer.onPageChanged();\n        observer.onDeleteVisits();\n      });\n    });\n  });\n\n  describe(\"Custom dispatch\", () => {\n    it(\"should only dispatch 1 PLACES_LINKS_CHANGED action if many bookmark-added notifications happened at once\", async () => {\n      // Yes, onItemAdded has at least 8 arguments. See function definition for docs.\n      const args = [\n        {\n          itemType: TYPE_BOOKMARK,\n          source: SOURCES.DEFAULT,\n          dateAdded: FAKE_BOOKMARK.dateAdded,\n          guid: FAKE_BOOKMARK.bookmarkGuid,\n          title: FAKE_BOOKMARK.bookmarkTitle,\n          url: \"https://www.foo.com\",\n          isTagging: false,\n        },\n      ];\n      await feed.placesObserver.handlePlacesEvent(args);\n      await feed.placesObserver.handlePlacesEvent(args);\n      await feed.placesObserver.handlePlacesEvent(args);\n      await feed.placesObserver.handlePlacesEvent(args);\n      assert.calledOnce(\n        feed.store.dispatch.withArgs(\n          ac.OnlyToMain({ type: at.PLACES_LINKS_CHANGED })\n        )\n      );\n    });\n    it(\"should only dispatch 1 PLACES_LINKS_CHANGED action if many onItemRemoved notifications happened at once\", async () => {\n      const args = [\n        null,\n        null,\n        null,\n        TYPE_BOOKMARK,\n        { spec: \"foo.com\" },\n        \"123foo\",\n        \"\",\n        SOURCES.DEFAULT,\n      ];\n      await feed.bookmarksObserver.onItemRemoved(...args);\n      await feed.bookmarksObserver.onItemRemoved(...args);\n      await feed.bookmarksObserver.onItemRemoved(...args);\n      await feed.bookmarksObserver.onItemRemoved(...args);\n\n      assert.calledOnce(\n        feed.store.dispatch.withArgs(\n          ac.OnlyToMain({ type: at.PLACES_LINKS_CHANGED })\n        )\n      );\n    });\n    it(\"should only dispatch 1 PLACES_LINKS_CHANGED action if any onDeleteURI notifications happened at once\", async () => {\n      await feed.historyObserver.onDeleteURI({ spec: \"foo.com\" });\n      await feed.historyObserver.onDeleteURI({ spec: \"foo1.com\" });\n      await feed.historyObserver.onDeleteURI({ spec: \"foo2.com\" });\n\n      assert.calledOnce(\n        feed.store.dispatch.withArgs(\n          ac.OnlyToMain({ type: at.PLACES_LINKS_CHANGED })\n        )\n      );\n    });\n  });\n\n  describe(\"PlacesObserver\", () => {\n    describe(\"#bookmark-added\", () => {\n      let dispatch;\n      let observer;\n      beforeEach(() => {\n        dispatch = sandbox.spy();\n        observer = new PlacesObserver(dispatch);\n      });\n      it(\"should dispatch a PLACES_BOOKMARK_ADDED action with the bookmark data - http\", async () => {\n        const args = [\n          {\n            itemType: TYPE_BOOKMARK,\n            source: SOURCES.DEFAULT,\n            dateAdded: FAKE_BOOKMARK.dateAdded,\n            guid: FAKE_BOOKMARK.bookmarkGuid,\n            title: FAKE_BOOKMARK.bookmarkTitle,\n            url: \"http://www.foo.com\",\n            isTagging: false,\n          },\n        ];\n        await observer.handlePlacesEvent(args);\n\n        assert.calledWith(dispatch.secondCall, {\n          type: at.PLACES_BOOKMARK_ADDED,\n          data: {\n            bookmarkGuid: FAKE_BOOKMARK.bookmarkGuid,\n            bookmarkTitle: FAKE_BOOKMARK.bookmarkTitle,\n            dateAdded: FAKE_BOOKMARK.dateAdded * 1000,\n            url: \"http://www.foo.com\",\n          },\n        });\n      });\n      it(\"should dispatch a PLACES_BOOKMARK_ADDED action with the bookmark data - https\", async () => {\n        const args = [\n          {\n            itemType: TYPE_BOOKMARK,\n            source: SOURCES.DEFAULT,\n            dateAdded: FAKE_BOOKMARK.dateAdded,\n            guid: FAKE_BOOKMARK.bookmarkGuid,\n            title: FAKE_BOOKMARK.bookmarkTitle,\n            url: \"https://www.foo.com\",\n            isTagging: false,\n          },\n        ];\n        await observer.handlePlacesEvent(args);\n\n        assert.calledWith(dispatch.secondCall, {\n          type: at.PLACES_BOOKMARK_ADDED,\n          data: {\n            bookmarkGuid: FAKE_BOOKMARK.bookmarkGuid,\n            bookmarkTitle: FAKE_BOOKMARK.bookmarkTitle,\n            dateAdded: FAKE_BOOKMARK.dateAdded * 1000,\n            url: \"https://www.foo.com\",\n          },\n        });\n      });\n      it(\"should not dispatch a PLACES_BOOKMARK_ADDED action - not http/https\", async () => {\n        const args = [\n          {\n            itemType: TYPE_BOOKMARK,\n            source: SOURCES.DEFAULT,\n            dateAdded: FAKE_BOOKMARK.dateAdded,\n            guid: FAKE_BOOKMARK.bookmarkGuid,\n            title: FAKE_BOOKMARK.bookmarkTitle,\n            url: \"foo.com\",\n            isTagging: false,\n          },\n        ];\n        await observer.handlePlacesEvent(args);\n\n        assert.notCalled(dispatch);\n      });\n      it(\"should not dispatch a PLACES_BOOKMARK_ADDED action - has IMPORT source\", async () => {\n        const args = [\n          {\n            itemType: TYPE_BOOKMARK,\n            source: SOURCES.IMPORT,\n            dateAdded: FAKE_BOOKMARK.dateAdded,\n            guid: FAKE_BOOKMARK.bookmarkGuid,\n            title: FAKE_BOOKMARK.bookmarkTitle,\n            url: \"foo.com\",\n            isTagging: false,\n          },\n        ];\n        await observer.handlePlacesEvent(args);\n\n        assert.notCalled(dispatch);\n      });\n      it(\"should not dispatch a PLACES_BOOKMARK_ADDED action - has RESTORE source\", async () => {\n        const args = [\n          {\n            itemType: TYPE_BOOKMARK,\n            source: SOURCES.RESTORE,\n            dateAdded: FAKE_BOOKMARK.dateAdded,\n            guid: FAKE_BOOKMARK.bookmarkGuid,\n            title: FAKE_BOOKMARK.bookmarkTitle,\n            url: \"foo.com\",\n            isTagging: false,\n          },\n        ];\n        await observer.handlePlacesEvent(args);\n\n        assert.notCalled(dispatch);\n      });\n      it(\"should not dispatch a PLACES_BOOKMARK_ADDED action - has RESTORE_ON_STARTUP source\", async () => {\n        const args = [\n          {\n            itemType: TYPE_BOOKMARK,\n            source: SOURCES.RESTORE_ON_STARTUP,\n            dateAdded: FAKE_BOOKMARK.dateAdded,\n            guid: FAKE_BOOKMARK.bookmarkGuid,\n            title: FAKE_BOOKMARK.bookmarkTitle,\n            url: \"foo.com\",\n            isTagging: false,\n          },\n        ];\n        await observer.handlePlacesEvent(args);\n\n        assert.notCalled(dispatch);\n      });\n      it(\"should not dispatch a PLACES_BOOKMARK_ADDED action - has SYNC source\", async () => {\n        const args = [\n          {\n            itemType: TYPE_BOOKMARK,\n            source: SOURCES.SYNC,\n            dateAdded: FAKE_BOOKMARK.dateAdded,\n            guid: FAKE_BOOKMARK.bookmarkGuid,\n            title: FAKE_BOOKMARK.bookmarkTitle,\n            url: \"foo.com\",\n            isTagging: false,\n          },\n        ];\n        await observer.handlePlacesEvent(args);\n\n        assert.notCalled(dispatch);\n      });\n      it(\"should ignore events that are not of TYPE_BOOKMARK\", async () => {\n        const args = [\n          {\n            itemType: \"nottypebookmark\",\n            source: SOURCES.DEFAULT,\n            dateAdded: FAKE_BOOKMARK.dateAdded,\n            guid: FAKE_BOOKMARK.bookmarkGuid,\n            title: FAKE_BOOKMARK.bookmarkTitle,\n            url: \"https://www.foo.com\",\n            isTagging: false,\n          },\n        ];\n        await observer.handlePlacesEvent(args);\n\n        assert.notCalled(dispatch);\n      });\n    });\n  });\n\n  describe(\"BookmarksObserver\", () => {\n    let dispatch;\n    let observer;\n    beforeEach(() => {\n      dispatch = sandbox.spy();\n      observer = new BookmarksObserver(dispatch);\n    });\n    it(\"should have a QueryInterface property\", () => {\n      assert.property(observer, \"QueryInterface\");\n    });\n    describe(\"#onItemRemoved\", () => {\n      it(\"should ignore events that are not of TYPE_BOOKMARK\", async () => {\n        await observer.onItemRemoved(\n          null,\n          null,\n          null,\n          \"nottypebookmark\",\n          null,\n          \"123foo\",\n          \"\",\n          SOURCES.DEFAULT\n        );\n        assert.notCalled(dispatch);\n      });\n      it(\"should not dispatch a PLACES_BOOKMARK_REMOVED action - has SYNC source\", async () => {\n        const args = [\n          null,\n          null,\n          null,\n          TYPE_BOOKMARK,\n          { spec: \"foo.com\" },\n          \"123foo\",\n          \"\",\n          SOURCES.SYNC,\n        ];\n        await observer.onItemRemoved(...args);\n\n        assert.notCalled(dispatch);\n      });\n      it(\"should not dispatch a PLACES_BOOKMARK_REMOVED action - has IMPORT source\", async () => {\n        const args = [\n          null,\n          null,\n          null,\n          TYPE_BOOKMARK,\n          { spec: \"foo.com\" },\n          \"123foo\",\n          \"\",\n          SOURCES.IMPORT,\n        ];\n        await observer.onItemRemoved(...args);\n\n        assert.notCalled(dispatch);\n      });\n      it(\"should not dispatch a PLACES_BOOKMARK_REMOVED action - has RESTORE source\", async () => {\n        const args = [\n          null,\n          null,\n          null,\n          TYPE_BOOKMARK,\n          { spec: \"foo.com\" },\n          \"123foo\",\n          \"\",\n          SOURCES.RESTORE,\n        ];\n        await observer.onItemRemoved(...args);\n\n        assert.notCalled(dispatch);\n      });\n      it(\"should not dispatch a PLACES_BOOKMARK_REMOVED action - has RESTORE_ON_STARTUP source\", async () => {\n        const args = [\n          null,\n          null,\n          null,\n          TYPE_BOOKMARK,\n          { spec: \"foo.com\" },\n          \"123foo\",\n          \"\",\n          SOURCES.RESTORE_ON_STARTUP,\n        ];\n        await observer.onItemRemoved(...args);\n\n        assert.notCalled(dispatch);\n      });\n      it(\"should dispatch a PLACES_BOOKMARK_REMOVED action with the right URL and bookmarkGuid\", () => {\n        observer.onItemRemoved(\n          null,\n          null,\n          null,\n          TYPE_BOOKMARK,\n          { spec: \"foo.com\" },\n          \"123foo\",\n          \"\",\n          SOURCES.DEFAULT\n        );\n        assert.calledWith(dispatch, {\n          type: at.PLACES_BOOKMARK_REMOVED,\n          data: { bookmarkGuid: \"123foo\", url: \"foo.com\" },\n        });\n      });\n    });\n    describe(\"Other empty methods (to keep code coverage happy)\", () => {\n      it(\"should have a various empty functions for xpconnect happiness\", () => {\n        observer.onBeginUpdateBatch();\n        observer.onEndUpdateBatch();\n        observer.onItemVisited();\n        observer.onItemMoved();\n        observer.onItemChanged();\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/lib/PrefsFeed.test.js",
    "content": "import { actionCreators as ac, actionTypes as at } from \"common/Actions.jsm\";\nimport { GlobalOverrider } from \"test/unit/utils\";\nimport { PrefsFeed } from \"lib/PrefsFeed.jsm\";\n\nlet overrider = new GlobalOverrider();\n\ndescribe(\"PrefsFeed\", () => {\n  let feed;\n  let FAKE_PREFS;\n  let sandbox;\n  let ServicesStub;\n  beforeEach(() => {\n    sandbox = sinon.createSandbox();\n    FAKE_PREFS = new Map([\n      [\"foo\", 1],\n      [\"bar\", 2],\n      [\"baz\", { value: 1, skipBroadcast: true }],\n    ]);\n    feed = new PrefsFeed(FAKE_PREFS);\n    const storage = {\n      getAll: sandbox.stub().resolves(),\n      set: sandbox.stub().resolves(),\n    };\n    ServicesStub = {\n      prefs: {\n        clearUserPref: sinon.spy(),\n        getStringPref: sinon.spy(),\n        getBoolPref: sinon.spy(),\n      },\n    };\n    feed.store = {\n      dispatch: sinon.spy(),\n      getState() {\n        return this.state;\n      },\n      dbStorage: { getDbTable: sandbox.stub().returns(storage) },\n    };\n    // Setup for tests that don't call `init`\n    feed._storage = storage;\n    feed._prefs = {\n      get: sinon.spy(item => FAKE_PREFS.get(item)),\n      set: sinon.spy((name, value) => FAKE_PREFS.set(name, value)),\n      observe: sinon.spy(),\n      observeBranch: sinon.spy(),\n      ignore: sinon.spy(),\n      ignoreBranch: sinon.spy(),\n      reset: sinon.stub(),\n      _branchStr: \"branch.str.\",\n    };\n    overrider.set({\n      PrivateBrowsingUtils: { enabled: true },\n      Services: ServicesStub,\n    });\n  });\n  afterEach(() => {\n    overrider.restore();\n    sandbox.restore();\n  });\n\n  it(\"should set a pref when a SET_PREF action is received\", () => {\n    feed.onAction(ac.SetPref(\"foo\", 2));\n    assert.calledWith(feed._prefs.set, \"foo\", 2);\n  });\n  it(\"should call clearUserPref with action CLEAR_PREF\", () => {\n    feed.onAction({ type: at.CLEAR_PREF, data: { name: \"pref.test\" } });\n    assert.calledWith(ServicesStub.prefs.clearUserPref, \"branch.str.pref.test\");\n  });\n  it(\"should dispatch PREFS_INITIAL_VALUES on init with pref values and .isPrivateBrowsingEnabled\", () => {\n    feed.onAction({ type: at.INIT });\n    assert.calledOnce(feed.store.dispatch);\n    assert.equal(\n      feed.store.dispatch.firstCall.args[0].type,\n      at.PREFS_INITIAL_VALUES\n    );\n    const [{ data }] = feed.store.dispatch.firstCall.args;\n    assert.equal(data.foo, 1);\n    assert.equal(data.bar, 2);\n    assert.isTrue(data.isPrivateBrowsingEnabled);\n  });\n  it(\"should add one branch observer on init\", () => {\n    feed.onAction({ type: at.INIT });\n    assert.calledOnce(feed._prefs.observeBranch);\n    assert.calledWith(feed._prefs.observeBranch, feed);\n  });\n  it(\"should initialise the storage on init\", () => {\n    feed.init();\n\n    assert.calledOnce(feed.store.dbStorage.getDbTable);\n    assert.calledWithExactly(feed.store.dbStorage.getDbTable, \"sectionPrefs\");\n  });\n  it(\"should remove the branch observer on uninit\", () => {\n    feed.onAction({ type: at.UNINIT });\n    assert.calledOnce(feed._prefs.ignoreBranch);\n    assert.calledWith(feed._prefs.ignoreBranch, feed);\n  });\n  it(\"should send a PREF_CHANGED action when onPrefChanged is called\", () => {\n    feed.onPrefChanged(\"foo\", 2);\n    assert.calledWith(\n      feed.store.dispatch,\n      ac.BroadcastToContent({\n        type: at.PREF_CHANGED,\n        data: { name: \"foo\", value: 2 },\n      })\n    );\n  });\n  it(\"should set storage pref on UPDATE_SECTION_PREFS\", async () => {\n    await feed.onAction({\n      type: at.UPDATE_SECTION_PREFS,\n      data: { id: \"topsites\", value: { collapsed: false } },\n    });\n    assert.calledWith(feed._storage.set, \"topsites\", { collapsed: false });\n  });\n  it(\"should set storage pref with section prefix on UPDATE_SECTION_PREFS\", async () => {\n    await feed.onAction({\n      type: at.UPDATE_SECTION_PREFS,\n      data: { id: \"topstories\", value: { collapsed: false } },\n    });\n    assert.calledWith(feed._storage.set, \"feeds.section.topstories\", {\n      collapsed: false,\n    });\n  });\n  it(\"should catch errors on UPDATE_SECTION_PREFS\", async () => {\n    feed._storage.set.throws(new Error(\"foo\"));\n    assert.doesNotThrow(async () => {\n      await feed.onAction({\n        type: at.UPDATE_SECTION_PREFS,\n        data: { id: \"topstories\", value: { collapsed: false } },\n      });\n    });\n  });\n  it(\"should send OnlyToMain pref update if config for pref has skipBroadcast: true\", async () => {\n    feed.onPrefChanged(\"baz\", { value: 2, skipBroadcast: true });\n    assert.calledWith(\n      feed.store.dispatch,\n      ac.OnlyToMain({\n        type: at.PREF_CHANGED,\n        data: { name: \"baz\", value: { value: 2, skipBroadcast: true } },\n      })\n    );\n  });\n});\n"
  },
  {
    "path": "test/unit/lib/RecipeExecutor.test.js",
    "content": "import { RecipeExecutor } from \"lib/RecipeExecutor.jsm\";\n\nclass MockTagger {\n  constructor(mode, tagScoreMap) {\n    this.mode = mode;\n    this.tagScoreMap = tagScoreMap;\n  }\n  tagTokens(tokens) {\n    if (this.mode === \"nb\") {\n      // eslint-disable-next-line prefer-destructuring\n      let tag = Object.keys(this.tagScoreMap)[0];\n      // eslint-disable-next-line prefer-destructuring\n      let prob = this.tagScoreMap[tag];\n      let conf = prob >= 0.85;\n      return {\n        label: tag,\n        logProb: Math.log(prob),\n        confident: conf,\n      };\n    }\n    return this.tagScoreMap;\n  }\n  tag(text) {\n    return this.tagTokens([text]);\n  }\n}\n\ndescribe(\"RecipeExecutor\", () => {\n  let makeItem = () => {\n    let x = {\n      lhs: 2,\n      one: 1,\n      two: 2,\n      three: 3,\n      foo: \"FOO\",\n      bar: \"BAR\",\n      baz: [\"one\", \"two\", \"three\"],\n      qux: 42,\n      text: \"This Is A_sentence.\",\n      url:\n        \"http://www.wonder.example.com/dir1/dir2a-dir2b/dir3+4?key1&key2=val2&key3&%26amp=%3D3+4\",\n      url2:\n        \"http://wonder.example.com/dir1/dir2a-dir2b/dir3+4?key1&key2=val2&key3&%26amp=%3D3+4\",\n      map: {\n        c: 3,\n        a: 1,\n        b: 2,\n      },\n      map2: {\n        b: 2,\n        c: 3,\n        d: 4,\n      },\n      arr1: [2, 3, 4],\n      arr2: [3, 4, 5],\n      long: [3, 4, 5, 6, 7],\n      tags: {\n        a: {\n          aa: 0.1,\n          ab: 0.2,\n          ac: 0.3,\n        },\n        b: {\n          ba: 4,\n          bb: 5,\n          bc: 6,\n        },\n      },\n      bogus: {\n        a: {\n          aa: \"0.1\",\n          ab: \"0.2\",\n          ac: \"0.3\",\n        },\n        b: {\n          ba: \"4\",\n          bb: \"5\",\n          bc: \"6\",\n        },\n      },\n      zero: {\n        a: 0,\n        b: 0,\n      },\n      zaro: [0, 0],\n    };\n    return x;\n  };\n\n  let EPSILON = 0.00001;\n\n  let instance = new RecipeExecutor(\n    [\n      new MockTagger(\"nb\", { tag1: 0.7 }),\n      new MockTagger(\"nb\", { tag2: 0.86 }),\n      new MockTagger(\"nb\", { tag3: 0.9 }),\n      new MockTagger(\"nb\", { tag5: 0.9 }),\n    ],\n    {\n      tag1: new MockTagger(\"nmf\", {\n        tag11: 0.9,\n        tag12: 0.8,\n        tag13: 0.7,\n      }),\n      tag2: new MockTagger(\"nmf\", {\n        tag21: 0.8,\n        tag22: 0.7,\n        tag23: 0.6,\n      }),\n      tag3: new MockTagger(\"nmf\", {\n        tag31: 0.7,\n        tag32: 0.6,\n        tag33: 0.5,\n      }),\n      tag4: new MockTagger(\"nmf\", { tag41: 0.99 }),\n    }\n  );\n  let item = null;\n\n  beforeEach(() => {\n    item = makeItem();\n  });\n\n  describe(\"#_assembleText\", () => {\n    it(\"should simply copy a single string\", () => {\n      assert.equal(instance._assembleText(item, [\"foo\"]), \"FOO\");\n    });\n    it(\"should append some strings with a space\", () => {\n      assert.equal(instance._assembleText(item, [\"foo\", \"bar\"]), \"FOO BAR\");\n    });\n    it(\"should give an empty string for a missing field\", () => {\n      assert.equal(instance._assembleText(item, [\"missing\"]), \"\");\n    });\n    it(\"should not double space an interior missing field\", () => {\n      assert.equal(\n        instance._assembleText(item, [\"foo\", \"missing\", \"bar\"]),\n        \"FOO BAR\"\n      );\n    });\n    it(\"should splice in an array of strings\", () => {\n      assert.equal(\n        instance._assembleText(item, [\"foo\", \"baz\", \"bar\"]),\n        \"FOO one two three BAR\"\n      );\n    });\n    it(\"should handle numbers\", () => {\n      assert.equal(\n        instance._assembleText(item, [\"foo\", \"qux\", \"bar\"]),\n        \"FOO 42 BAR\"\n      );\n    });\n  });\n\n  describe(\"#naiveBayesTag\", () => {\n    it(\"should understand NaiveBayesTextTagger\", () => {\n      item = instance.naiveBayesTag(item, { fields: [\"text\"] });\n      assert.isTrue(\"nb_tags\" in item);\n      assert.isTrue(!(\"tag1\" in item.nb_tags));\n      assert.equal(item.nb_tags.tag2, 0.86);\n      assert.equal(item.nb_tags.tag3, 0.9);\n      assert.equal(item.nb_tags.tag5, 0.9);\n      assert.isTrue(\"nb_tokens\" in item);\n      assert.deepEqual(item.nb_tokens, [\"this\", \"is\", \"a\", \"sentence\"]);\n      assert.isTrue(\"nb_tags_extended\" in item);\n      assert.isTrue(!(\"tag1\" in item.nb_tags_extended));\n      assert.deepEqual(item.nb_tags_extended.tag2, {\n        label: \"tag2\",\n        logProb: Math.log(0.86),\n        confident: true,\n      });\n      assert.deepEqual(item.nb_tags_extended.tag3, {\n        label: \"tag3\",\n        logProb: Math.log(0.9),\n        confident: true,\n      });\n      assert.deepEqual(item.nb_tags_extended.tag5, {\n        label: \"tag5\",\n        logProb: Math.log(0.9),\n        confident: true,\n      });\n      assert.isTrue(\"nb_tokens\" in item);\n      assert.deepEqual(item.nb_tokens, [\"this\", \"is\", \"a\", \"sentence\"]);\n    });\n  });\n\n  describe(\"#conditionallyNmfTag\", () => {\n    it(\"should do nothing if it's not nb tagged\", () => {\n      item = instance.conditionallyNmfTag(item, {});\n      assert.equal(item, null);\n    });\n    it(\"should populate nmf tags for the nb tags\", () => {\n      item = instance.naiveBayesTag(item, { fields: [\"text\"] });\n      item = instance.conditionallyNmfTag(item, {});\n      assert.isTrue(\"nb_tags\" in item);\n      assert.deepEqual(item.nmf_tags, {\n        tag2: {\n          tag21: 0.8,\n          tag22: 0.7,\n          tag23: 0.6,\n        },\n        tag3: {\n          tag31: 0.7,\n          tag32: 0.6,\n          tag33: 0.5,\n        },\n      });\n      assert.deepEqual(item.nmf_tags_parent, {\n        tag21: \"tag2\",\n        tag22: \"tag2\",\n        tag23: \"tag2\",\n        tag31: \"tag3\",\n        tag32: \"tag3\",\n        tag33: \"tag3\",\n      });\n    });\n    it(\"should not populate nmf tags for things that were not nb tagged\", () => {\n      item = instance.naiveBayesTag(item, { fields: [\"text\"] });\n      item = instance.conditionallyNmfTag(item, {});\n      assert.isTrue(\"nmf_tags\" in item);\n      assert.isTrue(!(\"tag4\" in item.nmf_tags));\n      assert.isTrue(\"nmf_tags_parent\" in item);\n      assert.isTrue(!(\"tag4\" in item.nmf_tags_parent));\n    });\n  });\n\n  describe(\"#acceptItemByFieldValue\", () => {\n    it(\"should implement ==\", () => {\n      assert.isTrue(\n        instance.acceptItemByFieldValue(item, {\n          field: \"lhs\",\n          op: \"==\",\n          rhsValue: 2,\n        }) !== null\n      );\n      assert.isTrue(\n        instance.acceptItemByFieldValue(item, {\n          field: \"lhs\",\n          op: \"==\",\n          rhsValue: 3,\n        }) === null\n      );\n      assert.isTrue(\n        instance.acceptItemByFieldValue(item, {\n          field: \"lhs\",\n          op: \"==\",\n          rhsField: \"two\",\n        }) !== null\n      );\n      assert.isTrue(\n        instance.acceptItemByFieldValue(item, {\n          field: \"lhs\",\n          op: \"==\",\n          rhsField: \"three\",\n        }) === null\n      );\n    });\n    it(\"should implement !=\", () => {\n      assert.isTrue(\n        instance.acceptItemByFieldValue(item, {\n          field: \"lhs\",\n          op: \"!=\",\n          rhsValue: 2,\n        }) === null\n      );\n      assert.isTrue(\n        instance.acceptItemByFieldValue(item, {\n          field: \"lhs\",\n          op: \"!=\",\n          rhsValue: 3,\n        }) !== null\n      );\n    });\n    it(\"should implement < \", () => {\n      assert.isTrue(\n        instance.acceptItemByFieldValue(item, {\n          field: \"lhs\",\n          op: \"<\",\n          rhsValue: 1,\n        }) === null\n      );\n      assert.isTrue(\n        instance.acceptItemByFieldValue(item, {\n          field: \"lhs\",\n          op: \"<\",\n          rhsValue: 2,\n        }) === null\n      );\n      assert.isTrue(\n        instance.acceptItemByFieldValue(item, {\n          field: \"lhs\",\n          op: \"<\",\n          rhsValue: 3,\n        }) !== null\n      );\n    });\n    it(\"should implement <= \", () => {\n      assert.isTrue(\n        instance.acceptItemByFieldValue(item, {\n          field: \"lhs\",\n          op: \"<=\",\n          rhsValue: 1,\n        }) === null\n      );\n      assert.isTrue(\n        instance.acceptItemByFieldValue(item, {\n          field: \"lhs\",\n          op: \"<=\",\n          rhsValue: 2,\n        }) !== null\n      );\n      assert.isTrue(\n        instance.acceptItemByFieldValue(item, {\n          field: \"lhs\",\n          op: \"<=\",\n          rhsValue: 3,\n        }) !== null\n      );\n    });\n    it(\"should implement > \", () => {\n      assert.isTrue(\n        instance.acceptItemByFieldValue(item, {\n          field: \"lhs\",\n          op: \">\",\n          rhsValue: 1,\n        }) !== null\n      );\n      assert.isTrue(\n        instance.acceptItemByFieldValue(item, {\n          field: \"lhs\",\n          op: \">\",\n          rhsValue: 2,\n        }) === null\n      );\n      assert.isTrue(\n        instance.acceptItemByFieldValue(item, {\n          field: \"lhs\",\n          op: \">\",\n          rhsValue: 3,\n        }) === null\n      );\n    });\n    it(\"should implement >= \", () => {\n      assert.isTrue(\n        instance.acceptItemByFieldValue(item, {\n          field: \"lhs\",\n          op: \">=\",\n          rhsValue: 1,\n        }) !== null\n      );\n      assert.isTrue(\n        instance.acceptItemByFieldValue(item, {\n          field: \"lhs\",\n          op: \">=\",\n          rhsValue: 2,\n        }) !== null\n      );\n      assert.isTrue(\n        instance.acceptItemByFieldValue(item, {\n          field: \"lhs\",\n          op: \">=\",\n          rhsValue: 3,\n        }) === null\n      );\n    });\n    it(\"should skip items with missing fields\", () => {\n      assert.isTrue(\n        instance.acceptItemByFieldValue(item, {\n          field: \"no-left\",\n          op: \"==\",\n          rhsValue: 1,\n        }) === null\n      );\n      assert.isTrue(\n        instance.acceptItemByFieldValue(item, {\n          field: \"lhs\",\n          op: \"==\",\n          rhsField: \"no-right\",\n        }) === null\n      );\n      assert.isTrue(\n        instance.acceptItemByFieldValue(item, { field: \"lhs\", op: \"==\" }) ===\n          null\n      );\n    });\n    it(\"should skip items with bogus operators\", () => {\n      assert.isTrue(\n        instance.acceptItemByFieldValue(item, {\n          field: \"lhs\",\n          op: \"bogus\",\n          rhsField: \"two\",\n        }) === null\n      );\n    });\n  });\n\n  describe(\"#tokenizeUrl\", () => {\n    it(\"should strip the leading www from a url\", () => {\n      item = instance.tokenizeUrl(item, { field: \"url\", dest: \"url_toks\" });\n      assert.deepEqual(\n        [\n          \"wonder\",\n          \"example\",\n          \"com\",\n          \"dir1\",\n          \"dir2a\",\n          \"dir2b\",\n          \"dir3\",\n          \"4\",\n          \"key1\",\n          \"key2\",\n          \"val2\",\n          \"key3\",\n          \"amp\",\n          \"3\",\n          \"4\",\n        ],\n        item.url_toks\n      );\n    });\n    it(\"should tokenize the not strip the leading non-wwww token from a url\", () => {\n      item = instance.tokenizeUrl(item, { field: \"url2\", dest: \"url_toks\" });\n      assert.deepEqual(\n        [\n          \"wonder\",\n          \"example\",\n          \"com\",\n          \"dir1\",\n          \"dir2a\",\n          \"dir2b\",\n          \"dir3\",\n          \"4\",\n          \"key1\",\n          \"key2\",\n          \"val2\",\n          \"key3\",\n          \"amp\",\n          \"3\",\n          \"4\",\n        ],\n        item.url_toks\n      );\n    });\n    it(\"should error for a missing url\", () => {\n      item = instance.tokenizeUrl(item, { field: \"missing\", dest: \"url_toks\" });\n      assert.equal(item, null);\n    });\n  });\n\n  describe(\"#getUrlDomain\", () => {\n    it(\"should get only the hostname skipping the www\", () => {\n      item = instance.getUrlDomain(item, { field: \"url\", dest: \"url_domain\" });\n      assert.isTrue(\"url_domain\" in item);\n      assert.deepEqual(\"wonder.example.com\", item.url_domain);\n    });\n    it(\"should get only the hostname\", () => {\n      item = instance.getUrlDomain(item, { field: \"url2\", dest: \"url_domain\" });\n      assert.isTrue(\"url_domain\" in item);\n      assert.deepEqual(\"wonder.example.com\", item.url_domain);\n    });\n    it(\"should get the hostname and 2 levels of directories\", () => {\n      item = instance.getUrlDomain(item, {\n        field: \"url\",\n        path_length: 2,\n        dest: \"url_plus_2\",\n      });\n      assert.isTrue(\"url_plus_2\" in item);\n      assert.deepEqual(\"wonder.example.com/dir1/dir2a-dir2b\", item.url_plus_2);\n    });\n    it(\"should error for a missing url\", () => {\n      item = instance.getUrlDomain(item, {\n        field: \"missing\",\n        dest: \"url_domain\",\n      });\n      assert.equal(item, null);\n    });\n  });\n\n  describe(\"#tokenizeField\", () => {\n    it(\"should tokenize the field\", () => {\n      item = instance.tokenizeField(item, { field: \"text\", dest: \"toks\" });\n      assert.isTrue(\"toks\" in item);\n      assert.deepEqual([\"this\", \"is\", \"a\", \"sentence\"], item.toks);\n    });\n    it(\"should error for a missing field\", () => {\n      item = instance.tokenizeField(item, { field: \"missing\", dest: \"toks\" });\n      assert.equal(item, null);\n    });\n    it(\"should error for a broken config\", () => {\n      item = instance.tokenizeField(item, {});\n      assert.equal(item, null);\n    });\n  });\n\n  describe(\"#_typeOf\", () => {\n    it(\"should know this is a map\", () => {\n      assert.equal(instance._typeOf({}), \"map\");\n    });\n    it(\"should know this is an array\", () => {\n      assert.equal(instance._typeOf([]), \"array\");\n    });\n    it(\"should know this is a string\", () => {\n      assert.equal(instance._typeOf(\"blah\"), \"string\");\n    });\n    it(\"should know this is a boolean\", () => {\n      assert.equal(instance._typeOf(true), \"boolean\");\n    });\n\n    it(\"should know this is a null\", () => {\n      assert.equal(instance._typeOf(null), \"null\");\n    });\n  });\n\n  describe(\"#_lookupScalar\", () => {\n    it(\"should return the constant\", () => {\n      assert.equal(instance._lookupScalar({}, 1, 0), 1);\n    });\n    it(\"should return the default\", () => {\n      assert.equal(instance._lookupScalar({}, \"blah\", 42), 42);\n    });\n    it(\"should return the field's value\", () => {\n      assert.equal(instance._lookupScalar({ blah: 11 }, \"blah\", 42), 11);\n    });\n  });\n\n  describe(\"#copyValue\", () => {\n    it(\"should copy values\", () => {\n      item = instance.copyValue(item, { src: \"one\", dest: \"again\" });\n      assert.isTrue(\"again\" in item);\n      assert.equal(item.again, 1);\n      item.one = 100;\n      assert.equal(item.one, 100);\n      assert.equal(item.again, 1);\n    });\n    it(\"should handle maps corrects\", () => {\n      item = instance.copyValue(item, { src: \"map\", dest: \"again\" });\n      assert.deepEqual(item.again, { a: 1, b: 2, c: 3 });\n      item.map.c = 100;\n      assert.deepEqual(item.again, { a: 1, b: 2, c: 3 });\n      item.map = 342;\n      assert.deepEqual(item.again, { a: 1, b: 2, c: 3 });\n    });\n    it(\"should error for a missing field\", () => {\n      item = instance.copyValue(item, { src: \"missing\", dest: \"toks\" });\n      assert.equal(item, null);\n    });\n  });\n\n  describe(\"#keepTopK\", () => {\n    it(\"should keep the 2 smallest\", () => {\n      item = instance.keepTopK(item, { field: \"map\", k: 2, descending: false });\n      assert.equal(Object.keys(item.map).length, 2);\n      assert.isTrue(\"a\" in item.map);\n      assert.equal(item.map.a, 1);\n      assert.isTrue(\"b\" in item.map);\n      assert.equal(item.map.b, 2);\n      assert.isTrue(!(\"c\" in item.map));\n    });\n    it(\"should keep the 2 largest\", () => {\n      item = instance.keepTopK(item, { field: \"map\", k: 2, descending: true });\n      assert.equal(Object.keys(item.map).length, 2);\n      assert.isTrue(!(\"a\" in item.map));\n      assert.isTrue(\"b\" in item.map);\n      assert.equal(item.map.b, 2);\n      assert.isTrue(\"c\" in item.map);\n      assert.equal(item.map.c, 3);\n    });\n    it(\"should still keep the 2 largest\", () => {\n      item = instance.keepTopK(item, { field: \"map\", k: 2 });\n      assert.equal(Object.keys(item.map).length, 2);\n      assert.isTrue(!(\"a\" in item.map));\n      assert.isTrue(\"b\" in item.map);\n      assert.equal(item.map.b, 2);\n      assert.isTrue(\"c\" in item.map);\n      assert.equal(item.map.c, 3);\n    });\n    it(\"should promote up nested fields\", () => {\n      item = instance.keepTopK(item, { field: \"tags\", k: 2 });\n      assert.equal(Object.keys(item.tags).length, 2);\n      assert.deepEqual(item.tags, { bb: 5, bc: 6 });\n    });\n    it(\"should error for a missing field\", () => {\n      item = instance.keepTopK(item, { field: \"missing\", k: 3 });\n      assert.equal(item, null);\n    });\n  });\n\n  describe(\"#scalarMultiply\", () => {\n    it(\"should use constants\", () => {\n      item = instance.scalarMultiply(item, { field: \"map\", k: 2 });\n      assert.equal(item.map.a, 2);\n      assert.equal(item.map.b, 4);\n      assert.equal(item.map.c, 6);\n    });\n    it(\"should use fields\", () => {\n      item = instance.scalarMultiply(item, { field: \"map\", k: \"three\" });\n      assert.equal(item.map.a, 3);\n      assert.equal(item.map.b, 6);\n      assert.equal(item.map.c, 9);\n    });\n    it(\"should use default\", () => {\n      item = instance.scalarMultiply(item, {\n        field: \"map\",\n        k: \"missing\",\n        dfault: 4,\n      });\n      assert.equal(item.map.a, 4);\n      assert.equal(item.map.b, 8);\n      assert.equal(item.map.c, 12);\n    });\n    it(\"should error for a missing field\", () => {\n      item = instance.scalarMultiply(item, { field: \"missing\", k: 3 });\n      assert.equal(item, null);\n    });\n    it(\"should multiply numbers\", () => {\n      item = instance.scalarMultiply(item, { field: \"lhs\", k: 2 });\n      assert.equal(item.lhs, 4);\n    });\n    it(\"should multiply arrays\", () => {\n      item = instance.scalarMultiply(item, { field: \"arr1\", k: 2 });\n      assert.deepEqual(item.arr1, [4, 6, 8]);\n    });\n    it(\"should should error on strings\", () => {\n      item = instance.scalarMultiply(item, { field: \"foo\", k: 2 });\n      assert.equal(item, null);\n    });\n  });\n\n  describe(\"#elementwiseMultiply\", () => {\n    it(\"should handle maps\", () => {\n      item = instance.elementwiseMultiply(item, {\n        left: \"tags\",\n        right: \"map2\",\n      });\n      assert.deepEqual(item.tags, {\n        a: { aa: 0, ab: 0, ac: 0 },\n        b: { ba: 8, bb: 10, bc: 12 },\n      });\n    });\n    it(\"should handle arrays of same length\", () => {\n      item = instance.elementwiseMultiply(item, {\n        left: \"arr1\",\n        right: \"arr2\",\n      });\n      assert.deepEqual(item.arr1, [6, 12, 20]);\n    });\n    it(\"should error for arrays of different lengths\", () => {\n      item = instance.elementwiseMultiply(item, {\n        left: \"arr1\",\n        right: \"long\",\n      });\n      assert.equal(item, null);\n    });\n    it(\"should error for a missing left\", () => {\n      item = instance.elementwiseMultiply(item, {\n        left: \"missing\",\n        right: \"arr2\",\n      });\n      assert.equal(item, null);\n    });\n    it(\"should error for a missing right\", () => {\n      item = instance.elementwiseMultiply(item, {\n        left: \"arr1\",\n        right: \"missing\",\n      });\n      assert.equal(item, null);\n    });\n    it(\"should handle numbers\", () => {\n      item = instance.elementwiseMultiply(item, {\n        left: \"three\",\n        right: \"two\",\n      });\n      assert.equal(item.three, 6);\n    });\n    it(\"should error for mismatched types\", () => {\n      item = instance.elementwiseMultiply(item, { left: \"arr1\", right: \"two\" });\n      assert.equal(item, null);\n    });\n    it(\"should error for strings\", () => {\n      item = instance.elementwiseMultiply(item, { left: \"foo\", right: \"bar\" });\n      assert.equal(item, null);\n    });\n  });\n\n  describe(\"#vectorMultiply\", () => {\n    it(\"should calculate dot products from maps\", () => {\n      item = instance.vectorMultiply(item, {\n        left: \"map\",\n        right: \"map2\",\n        dest: \"dot\",\n      });\n      assert.equal(item.dot, 13);\n    });\n    it(\"should calculate dot products from arrays\", () => {\n      item = instance.vectorMultiply(item, {\n        left: \"arr1\",\n        right: \"arr2\",\n        dest: \"dot\",\n      });\n      assert.equal(item.dot, 38);\n    });\n    it(\"should error for arrays of different lengths\", () => {\n      item = instance.vectorMultiply(item, { left: \"arr1\", right: \"long\" });\n      assert.equal(item, null);\n    });\n    it(\"should error for a missing left\", () => {\n      item = instance.vectorMultiply(item, { left: \"missing\", right: \"arr2\" });\n      assert.equal(item, null);\n    });\n    it(\"should error for a missing right\", () => {\n      item = instance.vectorMultiply(item, { left: \"arr1\", right: \"missing\" });\n      assert.equal(item, null);\n    });\n    it(\"should error for mismatched types\", () => {\n      item = instance.vectorMultiply(item, { left: \"arr1\", right: \"two\" });\n      assert.equal(item, null);\n    });\n    it(\"should error for strings\", () => {\n      item = instance.vectorMultiply(item, { left: \"foo\", right: \"bar\" });\n      assert.equal(item, null);\n    });\n  });\n\n  describe(\"#scalarAdd\", () => {\n    it(\"should error for a missing field\", () => {\n      item = instance.scalarAdd(item, { field: \"missing\", k: 10 });\n      assert.equal(item, null);\n    });\n    it(\"should error for strings\", () => {\n      item = instance.scalarAdd(item, { field: \"foo\", k: 10 });\n      assert.equal(item, null);\n    });\n    it(\"should work for numbers\", () => {\n      item = instance.scalarAdd(item, { field: \"one\", k: 10 });\n      assert.equal(item.one, 11);\n    });\n    it(\"should add a constant to every cell on a map\", () => {\n      item = instance.scalarAdd(item, { field: \"map\", k: 10 });\n      assert.deepEqual(item.map, { a: 11, b: 12, c: 13 });\n    });\n    it(\"should add a value from a field to every cell on a map\", () => {\n      item = instance.scalarAdd(item, { field: \"map\", k: \"qux\" });\n      assert.deepEqual(item.map, { a: 43, b: 44, c: 45 });\n    });\n    it(\"should add a constant to every cell on an array\", () => {\n      item = instance.scalarAdd(item, { field: \"arr1\", k: 10 });\n      assert.deepEqual(item.arr1, [12, 13, 14]);\n    });\n  });\n\n  describe(\"#vectorAdd\", () => {\n    it(\"should calculate add vectors from maps\", () => {\n      item = instance.vectorAdd(item, { left: \"map\", right: \"map2\" });\n      assert.equal(Object.keys(item.map).length, 4);\n      assert.isTrue(\"a\" in item.map);\n      assert.equal(item.map.a, 1);\n      assert.isTrue(\"b\" in item.map);\n      assert.equal(item.map.b, 4);\n      assert.isTrue(\"c\" in item.map);\n      assert.equal(item.map.c, 6);\n      assert.isTrue(\"d\" in item.map);\n      assert.equal(item.map.d, 4);\n    });\n    it(\"should work for missing left\", () => {\n      item = instance.vectorAdd(item, { left: \"missing\", right: \"arr2\" });\n      assert.deepEqual(item.missing, [3, 4, 5]);\n    });\n    it(\"should error for missing right\", () => {\n      item = instance.vectorAdd(item, { left: \"arr2\", right: \"missing\" });\n      assert.equal(item, null);\n    });\n    it(\"should error error for strings\", () => {\n      item = instance.vectorAdd(item, { left: \"foo\", right: \"bar\" });\n      assert.equal(item, null);\n    });\n    it(\"should error for different types\", () => {\n      item = instance.vectorAdd(item, { left: \"arr2\", right: \"map\" });\n      assert.equal(item, null);\n    });\n    it(\"should calculate add vectors from arrays\", () => {\n      item = instance.vectorAdd(item, { left: \"arr1\", right: \"arr2\" });\n      assert.deepEqual(item.arr1, [5, 7, 9]);\n    });\n    it(\"should abort on different sized arrays\", () => {\n      item = instance.vectorAdd(item, { left: \"arr1\", right: \"long\" });\n      assert.equal(item, null);\n    });\n    it(\"should calculate add vectors from arrays\", () => {\n      item = instance.vectorAdd(item, { left: \"arr1\", right: \"arr2\" });\n      assert.deepEqual(item.arr1, [5, 7, 9]);\n    });\n  });\n\n  describe(\"#makeBoolean\", () => {\n    it(\"should error for missing field\", () => {\n      item = instance.makeBoolean(item, { field: \"missing\", threshold: 2 });\n      assert.equal(item, null);\n    });\n    it(\"should 0/1 a map\", () => {\n      item = instance.makeBoolean(item, { field: \"map\", threshold: 2 });\n      assert.deepEqual(item.map, { a: 0, b: 0, c: 1 });\n    });\n    it(\"should a map of all 1s\", () => {\n      item = instance.makeBoolean(item, { field: \"map\" });\n      assert.deepEqual(item.map, { a: 1, b: 1, c: 1 });\n    });\n    it(\"should -1/1 a map\", () => {\n      item = instance.makeBoolean(item, {\n        field: \"map\",\n        threshold: 2,\n        keep_negative: true,\n      });\n      assert.deepEqual(item.map, { a: -1, b: -1, c: 1 });\n    });\n    it(\"should work an array\", () => {\n      item = instance.makeBoolean(item, { field: \"arr1\", threshold: 3 });\n      assert.deepEqual(item.arr1, [0, 0, 1]);\n    });\n    it(\"should -1/1 an array\", () => {\n      item = instance.makeBoolean(item, {\n        field: \"arr1\",\n        threshold: 3,\n        keep_negative: true,\n      });\n      assert.deepEqual(item.arr1, [-1, -1, 1]);\n    });\n    it(\"should 1 a high number\", () => {\n      item = instance.makeBoolean(item, { field: \"qux\", threshold: 3 });\n      assert.equal(item.qux, 1);\n    });\n    it(\"should 0 a low number\", () => {\n      item = instance.makeBoolean(item, { field: \"qux\", threshold: 70 });\n      assert.equal(item.qux, 0);\n    });\n    it(\"should -1 a low number\", () => {\n      item = instance.makeBoolean(item, {\n        field: \"qux\",\n        threshold: 83,\n        keep_negative: true,\n      });\n      assert.equal(item.qux, -1);\n    });\n    it(\"should fail a string\", () => {\n      item = instance.makeBoolean(item, { field: \"foo\", threshold: 3 });\n      assert.equal(item, null);\n    });\n  });\n\n  describe(\"#whitelistFields\", () => {\n    it(\"should filter the keys out of a map\", () => {\n      item = instance.whitelistFields(item, {\n        fields: [\"foo\", \"missing\", \"bar\"],\n      });\n      assert.deepEqual(item, { foo: \"FOO\", bar: \"BAR\" });\n    });\n  });\n\n  describe(\"#filterByValue\", () => {\n    it(\"should fail on missing field\", () => {\n      item = instance.filterByValue(item, { field: \"missing\", threshold: 2 });\n      assert.equal(item, null);\n    });\n    it(\"should filter the keys out of a map\", () => {\n      item = instance.filterByValue(item, { field: \"map\", threshold: 2 });\n      assert.deepEqual(item.map, { c: 3 });\n    });\n  });\n\n  describe(\"#l2Normalize\", () => {\n    it(\"should fail on missing field\", () => {\n      item = instance.l2Normalize(item, { field: \"missing\" });\n      assert.equal(item, null);\n    });\n    it(\"should L2 normalize an array\", () => {\n      item = instance.l2Normalize(item, { field: \"arr1\" });\n      assert.deepEqual(item.arr1, [\n        0.3713906763541037,\n        0.5570860145311556,\n        0.7427813527082074,\n      ]);\n    });\n    it(\"should L2 normalize a map\", () => {\n      item = instance.l2Normalize(item, { field: \"map\" });\n      assert.deepEqual(item.map, {\n        a: 0.2672612419124244,\n        b: 0.5345224838248488,\n        c: 0.8017837257372732,\n      });\n    });\n    it(\"should fail a string\", () => {\n      item = instance.l2Normalize(item, { field: \"foo\" });\n      assert.equal(item, null);\n    });\n    it(\"should not bomb on a zero vector\", () => {\n      item = instance.l2Normalize(item, { field: \"zero\" });\n      assert.deepEqual(item.zero, { a: 0, b: 0 });\n      item = instance.l2Normalize(item, { field: \"zaro\" });\n      assert.deepEqual(item.zaro, [0, 0]);\n    });\n  });\n\n  describe(\"#probNormalize\", () => {\n    it(\"should fail on missing field\", () => {\n      item = instance.probNormalize(item, { field: \"missing\" });\n      assert.equal(item, null);\n    });\n    it(\"should normalize an array to sum to 1\", () => {\n      item = instance.probNormalize(item, { field: \"arr1\" });\n      assert.deepEqual(item.arr1, [\n        0.2222222222222222,\n        0.3333333333333333,\n        0.4444444444444444,\n      ]);\n    });\n    it(\"should normalize a map to sum to 1\", () => {\n      item = instance.probNormalize(item, { field: \"map\" });\n      assert.equal(Object.keys(item.map).length, 3);\n      assert.isTrue(\"a\" in item.map);\n      assert.isTrue(Math.abs(item.map.a - 0.16667) <= EPSILON);\n      assert.isTrue(\"b\" in item.map);\n      assert.isTrue(Math.abs(item.map.b - 0.33333) <= EPSILON);\n      assert.isTrue(\"c\" in item.map);\n      assert.isTrue(Math.abs(item.map.c - 0.5) <= EPSILON);\n    });\n    it(\"should fail a string\", () => {\n      item = instance.probNormalize(item, { field: \"foo\" });\n      assert.equal(item, null);\n    });\n    it(\"should not bomb on a zero vector\", () => {\n      item = instance.probNormalize(item, { field: \"zero\" });\n      assert.deepEqual(item.zero, { a: 0, b: 0 });\n      item = instance.probNormalize(item, { field: \"zaro\" });\n      assert.deepEqual(item.zaro, [0, 0]);\n    });\n  });\n\n  describe(\"#scalarMultiplyTag\", () => {\n    it(\"should fail on missing field\", () => {\n      item = instance.scalarMultiplyTag(item, { field: \"missing\", k: 3 });\n      assert.equal(item, null);\n    });\n    it(\"should scalar multiply a nested map\", () => {\n      item = instance.scalarMultiplyTag(item, {\n        field: \"tags\",\n        k: 3,\n        log_scale: false,\n      });\n      assert.isTrue(Math.abs(item.tags.a.aa - 0.3) <= EPSILON);\n      assert.isTrue(Math.abs(item.tags.a.ab - 0.6) <= EPSILON);\n      assert.isTrue(Math.abs(item.tags.a.ac - 0.9) <= EPSILON);\n      assert.isTrue(Math.abs(item.tags.b.ba - 12) <= EPSILON);\n      assert.isTrue(Math.abs(item.tags.b.bb - 15) <= EPSILON);\n      assert.isTrue(Math.abs(item.tags.b.bc - 18) <= EPSILON);\n    });\n    it(\"should scalar multiply a nested map with logrithms\", () => {\n      item = instance.scalarMultiplyTag(item, {\n        field: \"tags\",\n        k: 3,\n        log_scale: true,\n      });\n      assert.isTrue(\n        Math.abs(item.tags.a.aa - Math.log(0.1 + 0.000001) * 3) <= EPSILON\n      );\n      assert.isTrue(\n        Math.abs(item.tags.a.ab - Math.log(0.2 + 0.000001) * 3) <= EPSILON\n      );\n      assert.isTrue(\n        Math.abs(item.tags.a.ac - Math.log(0.3 + 0.000001) * 3) <= EPSILON\n      );\n      assert.isTrue(\n        Math.abs(item.tags.b.ba - Math.log(4.0 + 0.000001) * 3) <= EPSILON\n      );\n      assert.isTrue(\n        Math.abs(item.tags.b.bb - Math.log(5.0 + 0.000001) * 3) <= EPSILON\n      );\n      assert.isTrue(\n        Math.abs(item.tags.b.bc - Math.log(6.0 + 0.000001) * 3) <= EPSILON\n      );\n    });\n    it(\"should fail a string\", () => {\n      item = instance.scalarMultiplyTag(item, { field: \"foo\", k: 3 });\n      assert.equal(item, null);\n    });\n  });\n\n  describe(\"#setDefault\", () => {\n    it(\"should store a missing value\", () => {\n      item = instance.setDefault(item, { field: \"missing\", value: 1111 });\n      assert.equal(item.missing, 1111);\n    });\n    it(\"should not overwrite an existing value\", () => {\n      item = instance.setDefault(item, { field: \"lhs\", value: 1111 });\n      assert.equal(item.lhs, 2);\n    });\n    it(\"should store a complex value\", () => {\n      item = instance.setDefault(item, { field: \"missing\", value: { a: 1 } });\n      assert.deepEqual(item.missing, { a: 1 });\n    });\n  });\n\n  describe(\"#lookupValue\", () => {\n    it(\"should promote a value\", () => {\n      item = instance.lookupValue(item, {\n        haystack: \"map\",\n        needle: \"c\",\n        dest: \"ccc\",\n      });\n      assert.equal(item.ccc, 3);\n    });\n    it(\"should handle a missing haystack\", () => {\n      item = instance.lookupValue(item, {\n        haystack: \"missing\",\n        needle: \"c\",\n        dest: \"ccc\",\n      });\n      assert.isTrue(!(\"ccc\" in item));\n    });\n    it(\"should handle a missing needle\", () => {\n      item = instance.lookupValue(item, {\n        haystack: \"map\",\n        needle: \"missing\",\n        dest: \"ccc\",\n      });\n      assert.isTrue(!(\"ccc\" in item));\n    });\n  });\n\n  describe(\"#copyToMap\", () => {\n    it(\"should copy a value to a map\", () => {\n      item = instance.copyToMap(item, {\n        src: \"qux\",\n        dest_map: \"map\",\n        dest_key: \"zzz\",\n      });\n      assert.isTrue(\"zzz\" in item.map);\n      assert.equal(item.map.zzz, item.qux);\n    });\n    it(\"should create a new map to hold the key\", () => {\n      item = instance.copyToMap(item, {\n        src: \"qux\",\n        dest_map: \"missing\",\n        dest_key: \"zzz\",\n      });\n      assert.equal(Object.keys(item.missing).length, 1);\n      assert.equal(item.missing.zzz, item.qux);\n    });\n    it(\"should not create an empty map if the src is missing\", () => {\n      item = instance.copyToMap(item, {\n        src: \"missing\",\n        dest_map: \"no_map\",\n        dest_key: \"zzz\",\n      });\n      assert.isTrue(!(\"no_map\" in item));\n    });\n  });\n\n  describe(\"#applySoftmaxTags\", () => {\n    it(\"should error on missing field\", () => {\n      item = instance.applySoftmaxTags(item, { field: \"missing\" });\n      assert.equal(item, null);\n    });\n    it(\"should error on nonmaps\", () => {\n      item = instance.applySoftmaxTags(item, { field: \"arr1\" });\n      assert.equal(item, null);\n    });\n    it(\"should error on unnested maps\", () => {\n      item = instance.applySoftmaxTags(item, { field: \"map\" });\n      assert.equal(item, null);\n    });\n    it(\"should error on wrong nested maps\", () => {\n      item = instance.applySoftmaxTags(item, { field: \"bogus\" });\n      assert.equal(item, null);\n    });\n    it(\"should apply softmax across the subtags\", () => {\n      item = instance.applySoftmaxTags(item, { field: \"tags\" });\n      assert.isTrue(\"a\" in item.tags);\n      assert.isTrue(\"aa\" in item.tags.a);\n      assert.isTrue(\"ab\" in item.tags.a);\n      assert.isTrue(\"ac\" in item.tags.a);\n      assert.isTrue(Math.abs(item.tags.a.aa - 0.30061) <= EPSILON);\n      assert.isTrue(Math.abs(item.tags.a.ab - 0.33222) <= EPSILON);\n      assert.isTrue(Math.abs(item.tags.a.ac - 0.36717) <= EPSILON);\n\n      assert.isTrue(\"b\" in item.tags);\n      assert.isTrue(\"ba\" in item.tags.b);\n      assert.isTrue(\"bb\" in item.tags.b);\n      assert.isTrue(\"bc\" in item.tags.b);\n      assert.isTrue(Math.abs(item.tags.b.ba - 0.09003) <= EPSILON);\n      assert.isTrue(Math.abs(item.tags.b.bb - 0.24473) <= EPSILON);\n      assert.isTrue(Math.abs(item.tags.b.bc - 0.66524) <= EPSILON);\n    });\n  });\n\n  describe(\"#combinerAdd\", () => {\n    it(\"should do nothing when right field is missing\", () => {\n      let right = makeItem();\n      let combined = instance.combinerAdd(item, right, { field: \"missing\" });\n      assert.deepEqual(combined, item);\n    });\n    it(\"should handle missing left maps\", () => {\n      let right = makeItem();\n      right.missingmap = { a: 5, b: -1, c: 3 };\n      let combined = instance.combinerAdd(item, right, { field: \"missingmap\" });\n      assert.deepEqual(combined.missingmap, { a: 5, b: -1, c: 3 });\n    });\n    it(\"should add equal sized maps\", () => {\n      let right = makeItem();\n      let combined = instance.combinerAdd(item, right, { field: \"map\" });\n      assert.deepEqual(combined.map, { a: 2, b: 4, c: 6 });\n    });\n    it(\"should add long map to short map\", () => {\n      let right = makeItem();\n      right.map.d = 999;\n      let combined = instance.combinerAdd(item, right, { field: \"map\" });\n      assert.deepEqual(combined.map, { a: 2, b: 4, c: 6, d: 999 });\n    });\n    it(\"should add short map to long map\", () => {\n      let right = makeItem();\n      item.map.d = 999;\n      let combined = instance.combinerAdd(item, right, { field: \"map\" });\n      assert.deepEqual(combined.map, { a: 2, b: 4, c: 6, d: 999 });\n    });\n    it(\"should add equal sized arrays\", () => {\n      let right = makeItem();\n      let combined = instance.combinerAdd(item, right, { field: \"arr1\" });\n      assert.deepEqual(combined.arr1, [4, 6, 8]);\n    });\n    it(\"should handle missing left arrays\", () => {\n      let right = makeItem();\n      right.missingarray = [5, 1, 4];\n      let combined = instance.combinerAdd(item, right, {\n        field: \"missingarray\",\n      });\n      assert.deepEqual(combined.missingarray, [5, 1, 4]);\n    });\n    it(\"should add long array to short array\", () => {\n      let right = makeItem();\n      right.arr1 = [2, 3, 4, 12];\n      let combined = instance.combinerAdd(item, right, { field: \"arr1\" });\n      assert.deepEqual(combined.arr1, [4, 6, 8, 12]);\n    });\n    it(\"should add short array to long array\", () => {\n      let right = makeItem();\n      item.arr1 = [2, 3, 4, 12];\n      let combined = instance.combinerAdd(item, right, { field: \"arr1\" });\n      assert.deepEqual(combined.arr1, [4, 6, 8, 12]);\n    });\n    it(\"should handle missing left number\", () => {\n      let right = makeItem();\n      right.missingnumber = 999;\n      let combined = instance.combinerAdd(item, right, {\n        field: \"missingnumber\",\n      });\n      assert.deepEqual(combined.missingnumber, 999);\n    });\n    it(\"should add numbers\", () => {\n      let right = makeItem();\n      let combined = instance.combinerAdd(item, right, { field: \"lhs\" });\n      assert.equal(combined.lhs, 4);\n    });\n    it(\"should error on missing left, and right is a string\", () => {\n      let right = makeItem();\n      right.error = \"error\";\n      let combined = instance.combinerAdd(item, right, { field: \"error\" });\n      assert.equal(combined, null);\n    });\n    it(\"should error on left string\", () => {\n      let right = makeItem();\n      let combined = instance.combinerAdd(item, right, { field: \"foo\" });\n      assert.equal(combined, null);\n    });\n    it(\"should error on mismatch types\", () => {\n      let right = makeItem();\n      right.lhs = [1, 2, 3];\n      let combined = instance.combinerAdd(item, right, { field: \"lhs\" });\n      assert.equal(combined, null);\n    });\n  });\n\n  describe(\"#combinerMax\", () => {\n    it(\"should do nothing when right field is missing\", () => {\n      let right = makeItem();\n      let combined = instance.combinerMax(item, right, { field: \"missing\" });\n      assert.deepEqual(combined, item);\n    });\n    it(\"should handle missing left maps\", () => {\n      let right = makeItem();\n      right.missingmap = { a: 5, b: -1, c: 3 };\n      let combined = instance.combinerMax(item, right, { field: \"missingmap\" });\n      assert.deepEqual(combined.missingmap, { a: 5, b: -1, c: 3 });\n    });\n    it(\"should handle equal sized maps\", () => {\n      let right = makeItem();\n      right.map = { a: 5, b: -1, c: 3 };\n      let combined = instance.combinerMax(item, right, { field: \"map\" });\n      assert.deepEqual(combined.map, { a: 5, b: 2, c: 3 });\n    });\n    it(\"should handle short map to long map\", () => {\n      let right = makeItem();\n      right.map = { a: 5, b: -1, c: 3, d: 999 };\n      let combined = instance.combinerMax(item, right, { field: \"map\" });\n      assert.deepEqual(combined.map, { a: 5, b: 2, c: 3, d: 999 });\n    });\n    it(\"should handle long map to short map\", () => {\n      let right = makeItem();\n      right.map = { a: 5, b: -1, c: 3 };\n      item.map.d = 999;\n      let combined = instance.combinerMax(item, right, { field: \"map\" });\n      assert.deepEqual(combined.map, { a: 5, b: 2, c: 3, d: 999 });\n    });\n    it(\"should handle equal sized arrays\", () => {\n      let right = makeItem();\n      right.arr1 = [5, 1, 4];\n      let combined = instance.combinerMax(item, right, { field: \"arr1\" });\n      assert.deepEqual(combined.arr1, [5, 3, 4]);\n    });\n    it(\"should handle missing left arrays\", () => {\n      let right = makeItem();\n      right.missingarray = [5, 1, 4];\n      let combined = instance.combinerMax(item, right, {\n        field: \"missingarray\",\n      });\n      assert.deepEqual(combined.missingarray, [5, 1, 4]);\n    });\n    it(\"should handle short array to long array\", () => {\n      let right = makeItem();\n      right.arr1 = [5, 1, 4, 7];\n      let combined = instance.combinerMax(item, right, { field: \"arr1\" });\n      assert.deepEqual(combined.arr1, [5, 3, 4, 7]);\n    });\n    it(\"should handle long array to short array\", () => {\n      let right = makeItem();\n      right.arr1 = [5, 1, 4];\n      item.arr1.push(7);\n      let combined = instance.combinerMax(item, right, { field: \"arr1\" });\n      assert.deepEqual(combined.arr1, [5, 3, 4, 7]);\n    });\n    it(\"should handle missing left number\", () => {\n      let right = makeItem();\n      right.missingnumber = 999;\n      let combined = instance.combinerMax(item, right, {\n        field: \"missingnumber\",\n      });\n      assert.deepEqual(combined.missingnumber, 999);\n    });\n    it(\"should handle big number\", () => {\n      let right = makeItem();\n      right.lhs = 99;\n      let combined = instance.combinerMax(item, right, { field: \"lhs\" });\n      assert.equal(combined.lhs, 99);\n    });\n    it(\"should handle small number\", () => {\n      let right = makeItem();\n      item.lhs = 99;\n      let combined = instance.combinerMax(item, right, { field: \"lhs\" });\n      assert.equal(combined.lhs, 99);\n    });\n    it(\"should error on missing left, and right is a string\", () => {\n      let right = makeItem();\n      right.error = \"error\";\n      let combined = instance.combinerMax(item, right, { field: \"error\" });\n      assert.equal(combined, null);\n    });\n    it(\"should error on left string\", () => {\n      let right = makeItem();\n      let combined = instance.combinerMax(item, right, { field: \"foo\" });\n      assert.equal(combined, null);\n    });\n    it(\"should error on mismatch types\", () => {\n      let right = makeItem();\n      right.lhs = [1, 2, 3];\n      let combined = instance.combinerMax(item, right, { field: \"lhs\" });\n      assert.equal(combined, null);\n    });\n  });\n\n  describe(\"#combinerCollectValues\", () => {\n    it(\"should error on bogus operation\", () => {\n      let right = makeItem();\n      right.url_domain = \"maseratiusa.com/maserati\";\n      right.time = 41;\n      let combined = instance.combinerCollectValues(item, right, {\n        left_field: \"combined_map\",\n        right_key_field: \"url_domain\",\n        right_value_field: \"time\",\n        operation: \"missing\",\n      });\n      assert.equal(combined, null);\n    });\n    it(\"should sum when missing left\", () => {\n      let right = makeItem();\n      right.url_domain = \"maseratiusa.com/maserati\";\n      right.time = 41;\n      let combined = instance.combinerCollectValues(item, right, {\n        left_field: \"combined_map\",\n        right_key_field: \"url_domain\",\n        right_value_field: \"time\",\n        operation: \"sum\",\n      });\n      assert.deepEqual(combined.combined_map, {\n        \"maseratiusa.com/maserati\": 41,\n      });\n    });\n    it(\"should sum when missing right\", () => {\n      let right = makeItem();\n      item.combined_map = { fake: 42 };\n      let combined = instance.combinerCollectValues(item, right, {\n        left_field: \"combined_map\",\n        right_key_field: \"url_domain\",\n        right_value_field: \"time\",\n        operation: \"sum\",\n      });\n      assert.deepEqual(combined.combined_map, { fake: 42 });\n    });\n    it(\"should sum when both\", () => {\n      let right = makeItem();\n      right.url_domain = \"maseratiusa.com/maserati\";\n      right.time = 41;\n      item.combined_map = { fake: 42, \"maseratiusa.com/maserati\": 41 };\n      let combined = instance.combinerCollectValues(item, right, {\n        left_field: \"combined_map\",\n        right_key_field: \"url_domain\",\n        right_value_field: \"time\",\n        operation: \"sum\",\n      });\n      assert.deepEqual(combined.combined_map, {\n        fake: 42,\n        \"maseratiusa.com/maserati\": 82,\n      });\n    });\n\n    it(\"should max when missing left\", () => {\n      let right = makeItem();\n      right.url_domain = \"maseratiusa.com/maserati\";\n      right.time = 41;\n      let combined = instance.combinerCollectValues(item, right, {\n        left_field: \"combined_map\",\n        right_key_field: \"url_domain\",\n        right_value_field: \"time\",\n        operation: \"max\",\n      });\n      assert.deepEqual(combined.combined_map, {\n        \"maseratiusa.com/maserati\": 41,\n      });\n    });\n    it(\"should max when missing right\", () => {\n      let right = makeItem();\n      item.combined_map = { fake: 42 };\n      let combined = instance.combinerCollectValues(item, right, {\n        left_field: \"combined_map\",\n        right_key_field: \"url_domain\",\n        right_value_field: \"time\",\n        operation: \"max\",\n      });\n      assert.deepEqual(combined.combined_map, { fake: 42 });\n    });\n    it(\"should max when both (right)\", () => {\n      let right = makeItem();\n      right.url_domain = \"maseratiusa.com/maserati\";\n      right.time = 99;\n      item.combined_map = { fake: 42, \"maseratiusa.com/maserati\": 41 };\n      let combined = instance.combinerCollectValues(item, right, {\n        left_field: \"combined_map\",\n        right_key_field: \"url_domain\",\n        right_value_field: \"time\",\n        operation: \"max\",\n      });\n      assert.deepEqual(combined.combined_map, {\n        fake: 42,\n        \"maseratiusa.com/maserati\": 99,\n      });\n    });\n    it(\"should max when both (left)\", () => {\n      let right = makeItem();\n      right.url_domain = \"maseratiusa.com/maserati\";\n      right.time = -99;\n      item.combined_map = { fake: 42, \"maseratiusa.com/maserati\": 41 };\n      let combined = instance.combinerCollectValues(item, right, {\n        left_field: \"combined_map\",\n        right_key_field: \"url_domain\",\n        right_value_field: \"time\",\n        operation: \"max\",\n      });\n      assert.deepEqual(combined.combined_map, {\n        fake: 42,\n        \"maseratiusa.com/maserati\": 41,\n      });\n    });\n\n    it(\"should overwrite when missing left\", () => {\n      let right = makeItem();\n      right.url_domain = \"maseratiusa.com/maserati\";\n      right.time = 41;\n      let combined = instance.combinerCollectValues(item, right, {\n        left_field: \"combined_map\",\n        right_key_field: \"url_domain\",\n        right_value_field: \"time\",\n        operation: \"overwrite\",\n      });\n      assert.deepEqual(combined.combined_map, {\n        \"maseratiusa.com/maserati\": 41,\n      });\n    });\n    it(\"should overwrite when missing right\", () => {\n      let right = makeItem();\n      item.combined_map = { fake: 42 };\n      let combined = instance.combinerCollectValues(item, right, {\n        left_field: \"combined_map\",\n        right_key_field: \"url_domain\",\n        right_value_field: \"time\",\n        operation: \"overwrite\",\n      });\n      assert.deepEqual(combined.combined_map, { fake: 42 });\n    });\n    it(\"should overwrite when both\", () => {\n      let right = makeItem();\n      right.url_domain = \"maseratiusa.com/maserati\";\n      right.time = 41;\n      item.combined_map = { fake: 42, \"maseratiusa.com/maserati\": 77 };\n      let combined = instance.combinerCollectValues(item, right, {\n        left_field: \"combined_map\",\n        right_key_field: \"url_domain\",\n        right_value_field: \"time\",\n        operation: \"overwrite\",\n      });\n      assert.deepEqual(combined.combined_map, {\n        fake: 42,\n        \"maseratiusa.com/maserati\": 41,\n      });\n    });\n\n    it(\"should count when missing left\", () => {\n      let right = makeItem();\n      right.url_domain = \"maseratiusa.com/maserati\";\n      right.time = 41;\n      let combined = instance.combinerCollectValues(item, right, {\n        left_field: \"combined_map\",\n        right_key_field: \"url_domain\",\n        right_value_field: \"time\",\n        operation: \"count\",\n      });\n      assert.deepEqual(combined.combined_map, {\n        \"maseratiusa.com/maserati\": 1,\n      });\n    });\n    it(\"should count when missing right\", () => {\n      let right = makeItem();\n      item.combined_map = { fake: 42 };\n      let combined = instance.combinerCollectValues(item, right, {\n        left_field: \"combined_map\",\n        right_key_field: \"url_domain\",\n        right_value_field: \"time\",\n        operation: \"count\",\n      });\n      assert.deepEqual(combined.combined_map, { fake: 42 });\n    });\n    it(\"should count when both\", () => {\n      let right = makeItem();\n      right.url_domain = \"maseratiusa.com/maserati\";\n      right.time = 41;\n      item.combined_map = { fake: 42, \"maseratiusa.com/maserati\": 1 };\n      let combined = instance.combinerCollectValues(item, right, {\n        left_field: \"combined_map\",\n        right_key_field: \"url_domain\",\n        right_value_field: \"time\",\n        operation: \"count\",\n      });\n      assert.deepEqual(combined.combined_map, {\n        fake: 42,\n        \"maseratiusa.com/maserati\": 2,\n      });\n    });\n  });\n\n  describe(\"#executeRecipe\", () => {\n    it(\"should handle working steps\", () => {\n      let final = instance.executeRecipe({}, [\n        { function: \"set_default\", field: \"foo\", value: 1 },\n        { function: \"set_default\", field: \"bar\", value: 10 },\n      ]);\n      assert.equal(final.foo, 1);\n      assert.equal(final.bar, 10);\n    });\n    it(\"should handle unknown steps\", () => {\n      let final = instance.executeRecipe({}, [\n        { function: \"set_default\", field: \"foo\", value: 1 },\n        { function: \"missing\" },\n        { function: \"set_default\", field: \"bar\", value: 10 },\n      ]);\n      assert.equal(final, null);\n    });\n    it(\"should handle erroring steps\", () => {\n      let final = instance.executeRecipe({}, [\n        { function: \"set_default\", field: \"foo\", value: 1 },\n        {\n          function: \"accept_item_by_field_value\",\n          field: \"missing\",\n          op: \"invalid\",\n          rhsField: \"moot\",\n          rhsValue: \"m00t\",\n        },\n        { function: \"set_default\", field: \"bar\", value: 10 },\n      ]);\n      assert.equal(final, null);\n    });\n  });\n\n  describe(\"#executeCombinerRecipe\", () => {\n    it(\"should handle working steps\", () => {\n      let final = instance.executeCombinerRecipe(\n        { foo: 1, bar: 10 },\n        { foo: 1, bar: 10 },\n        [\n          { function: \"combiner_add\", field: \"foo\" },\n          { function: \"combiner_add\", field: \"bar\" },\n        ]\n      );\n      assert.equal(final.foo, 2);\n      assert.equal(final.bar, 20);\n    });\n    it(\"should handle unknown steps\", () => {\n      let final = instance.executeCombinerRecipe(\n        { foo: 1, bar: 10 },\n        { foo: 1, bar: 10 },\n        [\n          { function: \"combiner_add\", field: \"foo\" },\n          { function: \"missing\" },\n          { function: \"combiner_add\", field: \"bar\" },\n        ]\n      );\n      assert.equal(final, null);\n    });\n    it(\"should handle erroring steps\", () => {\n      let final = instance.executeCombinerRecipe(\n        { foo: 1, bar: 10, baz: 0 },\n        { foo: 1, bar: 10, baz: \"hundred\" },\n        [\n          { function: \"combiner_add\", field: \"foo\" },\n          { function: \"combiner_add\", field: \"baz\" },\n          { function: \"combiner_add\", field: \"bar\" },\n        ]\n      );\n      assert.equal(final, null);\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/lib/Screenshots.test.js",
    "content": "\"use strict\";\nimport { GlobalOverrider } from \"test/unit/utils\";\nimport { Screenshots } from \"lib/Screenshots.jsm\";\n\nconst URL = \"foo.com\";\nconst FAKE_THUMBNAIL_PATH = \"fake/path/thumb.jpg\";\n\ndescribe(\"Screenshots\", () => {\n  let globals;\n  let sandbox;\n  let fakeServices;\n  let testFile;\n\n  beforeEach(() => {\n    globals = new GlobalOverrider();\n    sandbox = globals.sandbox;\n    fakeServices = {\n      wm: {\n        getEnumerator() {\n          return Array(10);\n        },\n      },\n    };\n    globals.set(\"BackgroundPageThumbs\", {\n      captureIfMissing: sandbox.spy(() => Promise.resolve()),\n    });\n    globals.set(\"PageThumbs\", {\n      _store: sandbox.stub(),\n      getThumbnailPath: sandbox.spy(() => FAKE_THUMBNAIL_PATH),\n    });\n    globals.set(\"PrivateBrowsingUtils\", {\n      isWindowPrivate: sandbox.spy(() => false),\n    });\n    testFile = { size: 1 };\n    globals.set(\"Services\", fakeServices);\n    globals.set(\n      \"fetch\",\n      sandbox.spy(() =>\n        Promise.resolve({ blob: () => Promise.resolve(testFile) })\n      )\n    );\n  });\n  afterEach(() => {\n    globals.restore();\n  });\n\n  describe(\"#getScreenshotForURL\", () => {\n    it(\"should call BackgroundPageThumbs.captureIfMissing with the correct url\", async () => {\n      await Screenshots.getScreenshotForURL(URL);\n      assert.calledWith(global.BackgroundPageThumbs.captureIfMissing, URL);\n    });\n    it(\"should call PageThumbs.getThumbnailPath with the correct url\", async () => {\n      await Screenshots.getScreenshotForURL(URL);\n      assert.calledWith(global.PageThumbs.getThumbnailPath, URL);\n    });\n    it(\"should call fetch\", async () => {\n      await Screenshots.getScreenshotForURL(URL);\n      assert.calledOnce(global.fetch);\n    });\n    it(\"should have the necessary keys in the response object\", async () => {\n      const screenshot = await Screenshots.getScreenshotForURL(URL);\n\n      assert.notEqual(screenshot.path, undefined);\n      assert.notEqual(screenshot.data, undefined);\n    });\n    it(\"should get null if something goes wrong\", async () => {\n      globals.set(\"BackgroundPageThumbs\", {\n        captureIfMissing: () =>\n          Promise.reject(new Error(\"Cannot capture thumbnail\")),\n      });\n\n      const screenshot = await Screenshots.getScreenshotForURL(URL);\n\n      assert.calledOnce(global.PageThumbs._store);\n      assert.equal(screenshot, null);\n    });\n    it(\"should get null without storing if existing thumbnail is empty\", async () => {\n      testFile.size = 0;\n\n      const screenshot = await Screenshots.getScreenshotForURL(URL);\n\n      assert.notCalled(global.PageThumbs._store);\n      assert.equal(screenshot, null);\n    });\n  });\n\n  describe(\"#maybeCacheScreenshot\", () => {\n    let link;\n    beforeEach(() => {\n      link = {\n        __sharedCache: {\n          updateLink: (prop, val) => {\n            link[prop] = val;\n          },\n        },\n      };\n    });\n    it(\"should call getScreenshotForURL\", () => {\n      sandbox.stub(Screenshots, \"getScreenshotForURL\");\n      sandbox.stub(Screenshots, \"_shouldGetScreenshots\").returns(true);\n      Screenshots.maybeCacheScreenshot(\n        link,\n        \"mozilla.com\",\n        \"image\",\n        sinon.stub()\n      );\n\n      assert.calledOnce(Screenshots.getScreenshotForURL);\n      assert.calledWithExactly(Screenshots.getScreenshotForURL, \"mozilla.com\");\n    });\n    it(\"should not call getScreenshotForURL twice if a fetch is in progress\", () => {\n      sandbox\n        .stub(Screenshots, \"getScreenshotForURL\")\n        .returns(new Promise(() => {}));\n      sandbox.stub(Screenshots, \"_shouldGetScreenshots\").returns(true);\n      Screenshots.maybeCacheScreenshot(\n        link,\n        \"mozilla.com\",\n        \"image\",\n        sinon.stub()\n      );\n      Screenshots.maybeCacheScreenshot(\n        link,\n        \"mozilla.org\",\n        \"image\",\n        sinon.stub()\n      );\n\n      assert.calledOnce(Screenshots.getScreenshotForURL);\n      assert.calledWithExactly(Screenshots.getScreenshotForURL, \"mozilla.com\");\n    });\n    it(\"should not call getScreenshotsForURL if property !== undefined\", async () => {\n      sandbox\n        .stub(Screenshots, \"getScreenshotForURL\")\n        .returns(Promise.resolve(null));\n      sandbox.stub(Screenshots, \"_shouldGetScreenshots\").returns(true);\n      await Screenshots.maybeCacheScreenshot(\n        link,\n        \"mozilla.com\",\n        \"image\",\n        sinon.stub()\n      );\n      await Screenshots.maybeCacheScreenshot(\n        link,\n        \"mozilla.org\",\n        \"image\",\n        sinon.stub()\n      );\n\n      assert.calledOnce(Screenshots.getScreenshotForURL);\n      assert.calledWithExactly(Screenshots.getScreenshotForURL, \"mozilla.com\");\n    });\n    it(\"should check if we are in private browsing before getting screenshots\", async () => {\n      sandbox.stub(Screenshots, \"_shouldGetScreenshots\").returns(true);\n      await Screenshots.maybeCacheScreenshot(\n        link,\n        \"mozilla.com\",\n        \"image\",\n        sinon.stub()\n      );\n\n      assert.calledOnce(Screenshots._shouldGetScreenshots);\n    });\n    it(\"should not get a screenshot if we are in private browsing\", async () => {\n      sandbox.stub(Screenshots, \"getScreenshotForURL\");\n      sandbox.stub(Screenshots, \"_shouldGetScreenshots\").returns(false);\n      await Screenshots.maybeCacheScreenshot(\n        link,\n        \"mozilla.com\",\n        \"image\",\n        sinon.stub()\n      );\n\n      assert.notCalled(Screenshots.getScreenshotForURL);\n    });\n  });\n\n  describe(\"#_shouldGetScreenshots\", () => {\n    beforeEach(() => {\n      let more = 2;\n      sandbox\n        .stub(global.Services.wm, \"getEnumerator\")\n        .callsFake(() => Array(Math.max(more--, 0)));\n    });\n    it(\"should use private browsing utils to determine if a window is private\", () => {\n      Screenshots._shouldGetScreenshots();\n      assert.calledOnce(global.PrivateBrowsingUtils.isWindowPrivate);\n    });\n    it(\"should return true if there exists at least 1 non-private window\", () => {\n      assert.isTrue(Screenshots._shouldGetScreenshots());\n    });\n    it(\"should return false if there exists private windows\", () => {\n      global.PrivateBrowsingUtils = {\n        isWindowPrivate: sandbox.spy(() => true),\n      };\n      assert.isFalse(Screenshots._shouldGetScreenshots());\n      assert.calledTwice(global.PrivateBrowsingUtils.isWindowPrivate);\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/lib/SectionsManager.test.js",
    "content": "\"use strict\";\nimport {\n  actionCreators as ac,\n  actionTypes as at,\n  CONTENT_MESSAGE_TYPE,\n  MAIN_MESSAGE_TYPE,\n  PRELOAD_MESSAGE_TYPE,\n} from \"common/Actions.jsm\";\nimport { EventEmitter, GlobalOverrider } from \"test/unit/utils\";\nimport { SectionsFeed, SectionsManager } from \"lib/SectionsManager.jsm\";\n\nconst FAKE_ID = \"FAKE_ID\";\nconst FAKE_OPTIONS = { icon: \"FAKE_ICON\", title: \"FAKE_TITLE\" };\nconst FAKE_ROWS = [\n  { url: \"1.example.com\", type: \"bookmark\" },\n  { url: \"2.example.com\", type: \"pocket\" },\n  { url: \"3.example.com\", type: \"history\" },\n];\nconst FAKE_TRENDING_ROWS = [{ url: \"bar\", type: \"trending\" }];\nconst FAKE_URL = \"2.example.com\";\nconst FAKE_CARD_OPTIONS = { title: \"Some fake title\" };\n\ndescribe(\"SectionsManager\", () => {\n  let globals;\n  let fakeServices;\n  let fakePlacesUtils;\n  let sandbox;\n  let storage;\n\n  beforeEach(async () => {\n    sandbox = sinon.createSandbox();\n    globals = new GlobalOverrider();\n    fakeServices = {\n      prefs: {\n        getBoolPref: sandbox.stub(),\n        addObserver: sandbox.stub(),\n        removeObserver: sandbox.stub(),\n      },\n    };\n    fakePlacesUtils = {\n      history: { update: sinon.stub(), insert: sinon.stub() },\n    };\n    globals.set({\n      Services: fakeServices,\n      PlacesUtils: fakePlacesUtils,\n    });\n    // Redecorate SectionsManager to remove any listeners that have been added\n    EventEmitter.decorate(SectionsManager);\n    storage = {\n      get: sandbox.stub().resolves(),\n      set: sandbox.stub().resolves(),\n    };\n  });\n\n  afterEach(() => {\n    globals.restore();\n    sandbox.restore();\n  });\n\n  describe(\"#init\", () => {\n    it(\"should initialise the sections map with the built in sections\", async () => {\n      SectionsManager.sections.clear();\n      SectionsManager.initialized = false;\n      await SectionsManager.init({}, storage);\n      assert.equal(SectionsManager.sections.size, 2);\n      assert.ok(SectionsManager.sections.has(\"topstories\"));\n      assert.ok(SectionsManager.sections.has(\"highlights\"));\n    });\n    it(\"should set .initialized to true\", async () => {\n      SectionsManager.sections.clear();\n      SectionsManager.initialized = false;\n      await SectionsManager.init({}, storage);\n      assert.ok(SectionsManager.initialized);\n    });\n    it(\"should add observer for context menu prefs\", async () => {\n      SectionsManager.CONTEXT_MENU_PREFS = { MENU_ITEM: \"MENU_ITEM_PREF\" };\n      await SectionsManager.init({}, storage);\n      assert.calledOnce(fakeServices.prefs.addObserver);\n      assert.calledWith(\n        fakeServices.prefs.addObserver,\n        \"MENU_ITEM_PREF\",\n        SectionsManager\n      );\n    });\n    it(\"should save the reference to `storage` passed in\", async () => {\n      await SectionsManager.init({}, storage);\n\n      assert.equal(SectionsManager._storage, storage);\n    });\n  });\n  describe(\"#uninit\", () => {\n    it(\"should remove observer for context menu prefs\", () => {\n      SectionsManager.CONTEXT_MENU_PREFS = { MENU_ITEM: \"MENU_ITEM_PREF\" };\n      SectionsManager.initialized = true;\n      SectionsManager.uninit();\n      assert.calledOnce(fakeServices.prefs.removeObserver);\n      assert.calledWith(\n        fakeServices.prefs.removeObserver,\n        \"MENU_ITEM_PREF\",\n        SectionsManager\n      );\n      assert.isFalse(SectionsManager.initialized);\n    });\n  });\n  describe(\"#addBuiltInSection\", () => {\n    it(\"should not report an error if options is undefined\", async () => {\n      globals.sandbox.spy(global.Cu, \"reportError\");\n      SectionsManager._storage.get = sandbox.stub().returns(Promise.resolve());\n      await SectionsManager.addBuiltInSection(\n        \"feeds.section.topstories\",\n        undefined\n      );\n\n      assert.notCalled(Cu.reportError);\n    });\n    it(\"should report an error if options is malformed\", async () => {\n      globals.sandbox.spy(global.Cu, \"reportError\");\n      SectionsManager._storage.get = sandbox.stub().returns(Promise.resolve());\n      await SectionsManager.addBuiltInSection(\n        \"feeds.section.topstories\",\n        \"invalid\"\n      );\n\n      assert.calledOnce(Cu.reportError);\n    });\n    it(\"should not throw if the indexedDB operation fails\", async () => {\n      globals.sandbox.spy(global.Cu, \"reportError\");\n      storage.get = sandbox.stub().throws();\n      SectionsManager._storage = storage;\n\n      try {\n        await SectionsManager.addBuiltInSection(\"feeds.section.topstories\");\n      } catch (e) {\n        assert.fail();\n      }\n\n      assert.calledOnce(storage.get);\n      assert.calledOnce(Cu.reportError);\n    });\n  });\n  describe(\"#updateSectionPrefs\", () => {\n    it(\"should update the collapsed value of the section\", async () => {\n      sandbox.stub(SectionsManager, \"updateSection\");\n      let topstories = SectionsManager.sections.get(\"topstories\");\n      assert.isFalse(topstories.pref.collapsed);\n\n      await SectionsManager.updateSectionPrefs(\"topstories\", {\n        collapsed: true,\n      });\n      topstories = SectionsManager.sections.get(\"topstories\");\n\n      assert.isTrue(SectionsManager.updateSection.args[0][1].pref.collapsed);\n    });\n    it(\"should ignore invalid ids\", async () => {\n      sandbox.stub(SectionsManager, \"updateSection\");\n      await SectionsManager.updateSectionPrefs(\"foo\", { collapsed: true });\n\n      assert.notCalled(SectionsManager.updateSection);\n    });\n  });\n  describe(\"#addSection\", () => {\n    it(\"should add the id to sections and emit an ADD_SECTION event\", () => {\n      const spy = sinon.spy();\n      SectionsManager.on(SectionsManager.ADD_SECTION, spy);\n      SectionsManager.addSection(FAKE_ID, FAKE_OPTIONS);\n      assert.ok(SectionsManager.sections.has(FAKE_ID));\n      assert.calledOnce(spy);\n      assert.calledWith(\n        spy,\n        SectionsManager.ADD_SECTION,\n        FAKE_ID,\n        FAKE_OPTIONS\n      );\n    });\n  });\n  describe(\"#removeSection\", () => {\n    it(\"should remove the id from sections and emit an REMOVE_SECTION event\", () => {\n      // Ensure we start with the id in the set\n      assert.ok(SectionsManager.sections.has(FAKE_ID));\n      const spy = sinon.spy();\n      SectionsManager.on(SectionsManager.REMOVE_SECTION, spy);\n      SectionsManager.removeSection(FAKE_ID);\n      assert.notOk(SectionsManager.sections.has(FAKE_ID));\n      assert.calledOnce(spy);\n      assert.calledWith(spy, SectionsManager.REMOVE_SECTION, FAKE_ID);\n    });\n  });\n  describe(\"#enableSection\", () => {\n    it(\"should call updateSection with {enabled: true}\", () => {\n      sinon.spy(SectionsManager, \"updateSection\");\n      SectionsManager.addSection(FAKE_ID, FAKE_OPTIONS);\n      SectionsManager.enableSection(FAKE_ID);\n      assert.calledOnce(SectionsManager.updateSection);\n      assert.calledWith(\n        SectionsManager.updateSection,\n        FAKE_ID,\n        { enabled: true },\n        true\n      );\n      SectionsManager.updateSection.restore();\n    });\n    it(\"should emit an ENABLE_SECTION event\", () => {\n      const spy = sinon.spy();\n      SectionsManager.on(SectionsManager.ENABLE_SECTION, spy);\n      SectionsManager.enableSection(FAKE_ID);\n      assert.calledOnce(spy);\n      assert.calledWith(spy, SectionsManager.ENABLE_SECTION, FAKE_ID);\n    });\n  });\n  describe(\"#disableSection\", () => {\n    it(\"should call updateSection with {enabled: false, rows: [], initialized: false}\", () => {\n      sinon.spy(SectionsManager, \"updateSection\");\n      SectionsManager.addSection(FAKE_ID, FAKE_OPTIONS);\n      SectionsManager.disableSection(FAKE_ID);\n      assert.calledOnce(SectionsManager.updateSection);\n      assert.calledWith(\n        SectionsManager.updateSection,\n        FAKE_ID,\n        { enabled: false, rows: [], initialized: false },\n        true\n      );\n      SectionsManager.updateSection.restore();\n    });\n    it(\"should emit a DISABLE_SECTION event\", () => {\n      const spy = sinon.spy();\n      SectionsManager.on(SectionsManager.DISABLE_SECTION, spy);\n      SectionsManager.disableSection(FAKE_ID);\n      assert.calledOnce(spy);\n      assert.calledWith(spy, SectionsManager.DISABLE_SECTION, FAKE_ID);\n    });\n  });\n  describe(\"#updateSection\", () => {\n    it(\"should emit an UPDATE_SECTION event with correct arguments\", () => {\n      SectionsManager.addSection(FAKE_ID, FAKE_OPTIONS);\n      const spy = sinon.spy();\n      const dedupeConfigurations = [\n        { id: \"topstories\", dedupeFrom: [\"highlights\"] },\n      ];\n      SectionsManager.on(SectionsManager.UPDATE_SECTION, spy);\n      SectionsManager.updateSection(FAKE_ID, { rows: FAKE_ROWS }, true);\n      assert.calledOnce(spy);\n      assert.calledWith(\n        spy,\n        SectionsManager.UPDATE_SECTION,\n        FAKE_ID,\n        { rows: FAKE_ROWS, dedupeConfigurations },\n        true\n      );\n    });\n    it(\"should do nothing if the section doesn't exist\", () => {\n      SectionsManager.removeSection(FAKE_ID);\n      const spy = sinon.spy();\n      SectionsManager.on(SectionsManager.UPDATE_SECTION, spy);\n      SectionsManager.updateSection(FAKE_ID, { rows: FAKE_ROWS }, true);\n      assert.notCalled(spy);\n    });\n    it(\"should update all sections\", () => {\n      SectionsManager.sections.clear();\n      const updateSectionOrig = SectionsManager.updateSection;\n      SectionsManager.updateSection = sinon.spy();\n\n      SectionsManager.addSection(\"ID1\", { title: \"FAKE_TITLE_1\" });\n      SectionsManager.addSection(\"ID2\", { title: \"FAKE_TITLE_2\" });\n      SectionsManager.updateSections();\n\n      assert.calledTwice(SectionsManager.updateSection);\n      assert.calledWith(\n        SectionsManager.updateSection,\n        \"ID1\",\n        { title: \"FAKE_TITLE_1\" },\n        true\n      );\n      assert.calledWith(\n        SectionsManager.updateSection,\n        \"ID2\",\n        { title: \"FAKE_TITLE_2\" },\n        true\n      );\n      SectionsManager.updateSection = updateSectionOrig;\n    });\n    it(\"context menu pref change should update sections\", async () => {\n      let observer;\n      const services = {\n        prefs: {\n          getBoolPref: sinon.spy(),\n          addObserver: (pref, o) => (observer = o),\n          removeObserver: sinon.spy(),\n        },\n      };\n      globals.set(\"Services\", services);\n\n      SectionsManager.updateSections = sinon.spy();\n      SectionsManager.CONTEXT_MENU_PREFS = { MENU_ITEM: \"MENU_ITEM_PREF\" };\n      await SectionsManager.init({}, storage);\n      observer.observe(\"\", \"nsPref:changed\", \"MENU_ITEM_PREF\");\n\n      assert.calledOnce(SectionsManager.updateSections);\n    });\n  });\n  describe(\"#_addCardTypeLinkMenuOptions\", () => {\n    const addCardTypeLinkMenuOptionsOrig =\n      SectionsManager._addCardTypeLinkMenuOptions;\n    const contextMenuOptionsOrig =\n      SectionsManager.CONTEXT_MENU_OPTIONS_FOR_HIGHLIGHT_TYPES;\n    beforeEach(() => {\n      // Add a topstories section and a highlights section, with types for each card\n      SectionsManager.addSection(\"topstories\", { FAKE_TRENDING_ROWS });\n      SectionsManager.addSection(\"highlights\", { FAKE_ROWS });\n    });\n    it(\"should only call _addCardTypeLinkMenuOptions if the section update is for highlights\", () => {\n      SectionsManager._addCardTypeLinkMenuOptions = sinon.spy();\n      SectionsManager.updateSection(\"topstories\", { rows: FAKE_ROWS }, false);\n      assert.notCalled(SectionsManager._addCardTypeLinkMenuOptions);\n\n      SectionsManager.updateSection(\"highlights\", { rows: FAKE_ROWS }, false);\n      assert.calledWith(SectionsManager._addCardTypeLinkMenuOptions, FAKE_ROWS);\n    });\n    it(\"should only call _addCardTypeLinkMenuOptions if the section update has rows\", () => {\n      SectionsManager._addCardTypeLinkMenuOptions = sinon.spy();\n      SectionsManager.updateSection(\"highlights\", {}, false);\n      assert.notCalled(SectionsManager._addCardTypeLinkMenuOptions);\n    });\n    it(\"should assign the correct context menu options based on the type of highlight\", () => {\n      SectionsManager._addCardTypeLinkMenuOptions = addCardTypeLinkMenuOptionsOrig;\n\n      SectionsManager.updateSection(\"highlights\", { rows: FAKE_ROWS }, false);\n      const highlights = SectionsManager.sections.get(\"highlights\").FAKE_ROWS;\n\n      // FAKE_ROWS was added in the following order: bookmark, pocket, history\n      assert.deepEqual(\n        highlights[0].contextMenuOptions,\n        SectionsManager.CONTEXT_MENU_OPTIONS_FOR_HIGHLIGHT_TYPES.bookmark\n      );\n      assert.deepEqual(\n        highlights[1].contextMenuOptions,\n        SectionsManager.CONTEXT_MENU_OPTIONS_FOR_HIGHLIGHT_TYPES.pocket\n      );\n      assert.deepEqual(\n        highlights[2].contextMenuOptions,\n        SectionsManager.CONTEXT_MENU_OPTIONS_FOR_HIGHLIGHT_TYPES.history\n      );\n    });\n    it(\"should throw an error if you are assigning a context menu to a non-existant highlight type\", () => {\n      globals.sandbox.spy(global.Cu, \"reportError\");\n      SectionsManager.updateSection(\n        \"highlights\",\n        { rows: [{ url: \"foo\", type: \"badtype\" }] },\n        false\n      );\n      const highlights = SectionsManager.sections.get(\"highlights\").rows;\n      assert.calledOnce(Cu.reportError);\n      assert.equal(highlights[0].contextMenuOptions, undefined);\n    });\n    it(\"should filter out context menu options that are in CONTEXT_MENU_PREFS\", () => {\n      const services = {\n        prefs: {\n          getBoolPref: o =>\n            SectionsManager.CONTEXT_MENU_PREFS[o] !== \"RemoveMe\",\n          addObserver() {},\n          removeObserver() {},\n        },\n      };\n      globals.set(\"Services\", services);\n      SectionsManager.CONTEXT_MENU_PREFS = { RemoveMe: \"RemoveMe\" };\n      SectionsManager.CONTEXT_MENU_OPTIONS_FOR_HIGHLIGHT_TYPES = {\n        bookmark: [\"KeepMe\", \"RemoveMe\"],\n        pocket: [\"KeepMe\", \"RemoveMe\"],\n        history: [\"KeepMe\", \"RemoveMe\"],\n      };\n      SectionsManager.updateSection(\"highlights\", { rows: FAKE_ROWS }, false);\n      const highlights = SectionsManager.sections.get(\"highlights\").FAKE_ROWS;\n\n      // Only keep context menu options that were not supposed to be removed based on CONTEXT_MENU_PREFS\n      assert.deepEqual(highlights[0].contextMenuOptions, [\"KeepMe\"]);\n      assert.deepEqual(highlights[1].contextMenuOptions, [\"KeepMe\"]);\n      assert.deepEqual(highlights[2].contextMenuOptions, [\"KeepMe\"]);\n      SectionsManager.CONTEXT_MENU_OPTIONS_FOR_HIGHLIGHT_TYPES = contextMenuOptionsOrig;\n      globals.restore();\n    });\n  });\n  describe(\"#onceInitialized\", () => {\n    it(\"should call the callback immediately if SectionsManager is initialised\", () => {\n      SectionsManager.initialized = true;\n      const callback = sinon.spy();\n      SectionsManager.onceInitialized(callback);\n      assert.calledOnce(callback);\n    });\n    it(\"should bind the callback to .once(INIT) if SectionsManager is not initialised\", () => {\n      SectionsManager.initialized = false;\n      sinon.spy(SectionsManager, \"once\");\n      const callback = () => {};\n      SectionsManager.onceInitialized(callback);\n      assert.calledOnce(SectionsManager.once);\n      assert.calledWith(SectionsManager.once, SectionsManager.INIT, callback);\n    });\n  });\n  describe(\"#updateSectionCard\", () => {\n    it(\"should emit an UPDATE_SECTION_CARD event with correct arguments\", () => {\n      SectionsManager.addSection(\n        FAKE_ID,\n        Object.assign({}, FAKE_OPTIONS, { rows: FAKE_ROWS })\n      );\n      const spy = sinon.spy();\n      SectionsManager.on(SectionsManager.UPDATE_SECTION_CARD, spy);\n      SectionsManager.updateSectionCard(\n        FAKE_ID,\n        FAKE_URL,\n        FAKE_CARD_OPTIONS,\n        true\n      );\n      assert.calledOnce(spy);\n      assert.calledWith(\n        spy,\n        SectionsManager.UPDATE_SECTION_CARD,\n        FAKE_ID,\n        FAKE_URL,\n        FAKE_CARD_OPTIONS,\n        true\n      );\n    });\n    it(\"should do nothing if the section doesn't exist\", () => {\n      SectionsManager.removeSection(FAKE_ID);\n      const spy = sinon.spy();\n      SectionsManager.on(SectionsManager.UPDATE_SECTION_CARD, spy);\n      SectionsManager.updateSectionCard(\n        FAKE_ID,\n        FAKE_URL,\n        FAKE_CARD_OPTIONS,\n        true\n      );\n      assert.notCalled(spy);\n    });\n  });\n  describe(\"#removeSectionCard\", () => {\n    it(\"should dispatch an SECTION_UPDATE action in which cards corresponding to the given url are removed\", () => {\n      const rows = [{ url: \"foo.com\" }, { url: \"bar.com\" }];\n\n      SectionsManager.addSection(\n        FAKE_ID,\n        Object.assign({}, FAKE_OPTIONS, { rows })\n      );\n      const spy = sinon.spy();\n      SectionsManager.on(SectionsManager.UPDATE_SECTION, spy);\n      SectionsManager.removeSectionCard(FAKE_ID, \"foo.com\");\n\n      assert.calledOnce(spy);\n      assert.equal(spy.firstCall.args[1], FAKE_ID);\n      assert.deepEqual(spy.firstCall.args[2].rows, [{ url: \"bar.com\" }]);\n    });\n    it(\"should do nothing if the section doesn't exist\", () => {\n      SectionsManager.removeSection(FAKE_ID);\n      const spy = sinon.spy();\n      SectionsManager.on(SectionsManager.UPDATE_SECTION, spy);\n      SectionsManager.removeSectionCard(FAKE_ID, \"bar.com\");\n      assert.notCalled(spy);\n    });\n  });\n  describe(\"#updateBookmarkMetadata\", () => {\n    beforeEach(() => {\n      let rows = [\n        {\n          url: \"bar\",\n          title: \"title\",\n          description: \"description\",\n          image: \"image\",\n          type: \"trending\",\n        },\n      ];\n      SectionsManager.addSection(\"topstories\", { rows });\n      // Simulate 2 sections.\n      rows = [\n        {\n          url: \"foo\",\n          title: \"title\",\n          description: \"description\",\n          image: \"image\",\n          type: \"bookmark\",\n        },\n      ];\n      SectionsManager.addSection(\"highlights\", { rows });\n    });\n\n    it(\"shouldn't call PlacesUtils if URL is not in topstories\", () => {\n      SectionsManager.updateBookmarkMetadata({ url: \"foo\" });\n\n      assert.notCalled(fakePlacesUtils.history.update);\n    });\n    it(\"should call PlacesUtils.history.update\", () => {\n      SectionsManager.updateBookmarkMetadata({ url: \"bar\" });\n\n      assert.calledOnce(fakePlacesUtils.history.update);\n      assert.calledWithExactly(fakePlacesUtils.history.update, {\n        url: \"bar\",\n        title: \"title\",\n        description: \"description\",\n        previewImageURL: \"image\",\n      });\n    });\n    it(\"should call PlacesUtils.history.insert\", () => {\n      SectionsManager.updateBookmarkMetadata({ url: \"bar\" });\n\n      assert.calledOnce(fakePlacesUtils.history.insert);\n      assert.calledWithExactly(fakePlacesUtils.history.insert, {\n        url: \"bar\",\n        title: \"title\",\n        visits: [{}],\n      });\n    });\n  });\n});\n\ndescribe(\"SectionsFeed\", () => {\n  let feed;\n  let sandbox;\n  let storage;\n\n  beforeEach(() => {\n    sandbox = sinon.createSandbox();\n    SectionsManager.sections.clear();\n    SectionsManager.initialized = false;\n    storage = {\n      get: sandbox.stub().resolves(),\n      set: sandbox.stub().resolves(),\n    };\n    feed = new SectionsFeed();\n    feed.store = { dispatch: sinon.spy() };\n    feed.store = {\n      dispatch: sinon.spy(),\n      getState() {\n        return this.state;\n      },\n      state: {\n        Prefs: {\n          values: {\n            sectionOrder: \"topsites,topstories,highlights\",\n            \"feeds.topsites\": true,\n          },\n        },\n        Sections: [{ initialized: false }],\n      },\n      dbStorage: { getDbTable: sandbox.stub().returns(storage) },\n    };\n  });\n  afterEach(() => {\n    feed.uninit();\n  });\n  describe(\"#init\", () => {\n    it(\"should create a SectionsFeed\", () => {\n      assert.instanceOf(feed, SectionsFeed);\n    });\n    it(\"should bind appropriate listeners\", () => {\n      sinon.spy(SectionsManager, \"on\");\n      feed.init();\n      assert.callCount(SectionsManager.on, 4);\n      for (const [event, listener] of [\n        [SectionsManager.ADD_SECTION, feed.onAddSection],\n        [SectionsManager.REMOVE_SECTION, feed.onRemoveSection],\n        [SectionsManager.UPDATE_SECTION, feed.onUpdateSection],\n        [SectionsManager.UPDATE_SECTION_CARD, feed.onUpdateSectionCard],\n      ]) {\n        assert.calledWith(SectionsManager.on, event, listener);\n      }\n    });\n    it(\"should call onAddSection for any already added sections in SectionsManager\", async () => {\n      await SectionsManager.init({}, storage);\n      assert.ok(SectionsManager.sections.has(\"topstories\"));\n      assert.ok(SectionsManager.sections.has(\"highlights\"));\n      const topstories = SectionsManager.sections.get(\"topstories\");\n      const highlights = SectionsManager.sections.get(\"highlights\");\n      sinon.spy(feed, \"onAddSection\");\n      feed.init();\n      assert.calledTwice(feed.onAddSection);\n      assert.calledWith(\n        feed.onAddSection,\n        SectionsManager.ADD_SECTION,\n        \"topstories\",\n        topstories\n      );\n      assert.calledWith(\n        feed.onAddSection,\n        SectionsManager.ADD_SECTION,\n        \"highlights\",\n        highlights\n      );\n    });\n  });\n  describe(\"#uninit\", () => {\n    it(\"should unbind all listeners\", () => {\n      sinon.spy(SectionsManager, \"off\");\n      feed.init();\n      feed.uninit();\n      assert.callCount(SectionsManager.off, 4);\n      for (const [event, listener] of [\n        [SectionsManager.ADD_SECTION, feed.onAddSection],\n        [SectionsManager.REMOVE_SECTION, feed.onRemoveSection],\n        [SectionsManager.UPDATE_SECTION, feed.onUpdateSection],\n        [SectionsManager.UPDATE_SECTION_CARD, feed.onUpdateSectionCard],\n      ]) {\n        assert.calledWith(SectionsManager.off, event, listener);\n      }\n    });\n    it(\"should emit an UNINIT event and set SectionsManager.initialized to false\", () => {\n      const spy = sinon.spy();\n      SectionsManager.on(SectionsManager.UNINIT, spy);\n      feed.init();\n      feed.uninit();\n      assert.calledOnce(spy);\n      assert.notOk(SectionsManager.initialized);\n    });\n  });\n  describe(\"#onAddSection\", () => {\n    it(\"should broadcast a SECTION_REGISTER action with the correct data\", () => {\n      feed.onAddSection(null, FAKE_ID, FAKE_OPTIONS);\n      const [action] = feed.store.dispatch.firstCall.args;\n      assert.equal(action.type, \"SECTION_REGISTER\");\n      assert.deepEqual(\n        action.data,\n        Object.assign({ id: FAKE_ID }, FAKE_OPTIONS)\n      );\n      assert.equal(action.meta.from, MAIN_MESSAGE_TYPE);\n      assert.equal(action.meta.to, CONTENT_MESSAGE_TYPE);\n    });\n    it(\"should prepend id to sectionOrder pref if not already included\", () => {\n      feed.store.state.Sections = [\n        { id: \"topstories\", enabled: true },\n        { id: \"highlights\", enabled: true },\n      ];\n      feed.onAddSection(null, FAKE_ID, FAKE_OPTIONS);\n      assert.calledWith(feed.store.dispatch, {\n        data: {\n          name: \"sectionOrder\",\n          value: `${FAKE_ID},topsites,topstories,highlights`,\n        },\n        meta: { from: \"ActivityStream:Content\", to: \"ActivityStream:Main\" },\n        type: \"SET_PREF\",\n      });\n    });\n  });\n  describe(\"#onRemoveSection\", () => {\n    it(\"should broadcast a SECTION_DEREGISTER action with the correct data\", () => {\n      feed.onRemoveSection(null, FAKE_ID);\n      const [action] = feed.store.dispatch.firstCall.args;\n      assert.equal(action.type, \"SECTION_DEREGISTER\");\n      assert.deepEqual(action.data, FAKE_ID);\n      // Should be broadcast\n      assert.equal(action.meta.from, MAIN_MESSAGE_TYPE);\n      assert.equal(action.meta.to, CONTENT_MESSAGE_TYPE);\n    });\n  });\n  describe(\"#onUpdateSection\", () => {\n    it(\"should do nothing if no options are provided\", () => {\n      feed.onUpdateSection(null, FAKE_ID, null);\n      assert.notCalled(feed.store.dispatch);\n    });\n    it(\"should dispatch a SECTION_UPDATE action with the correct data\", () => {\n      feed.onUpdateSection(null, FAKE_ID, { rows: FAKE_ROWS });\n      const [action] = feed.store.dispatch.firstCall.args;\n      assert.equal(action.type, \"SECTION_UPDATE\");\n      assert.deepEqual(action.data, { id: FAKE_ID, rows: FAKE_ROWS });\n      // Should be not broadcast by default, but should update the preloaded tab, so check meta\n      assert.equal(action.meta.from, MAIN_MESSAGE_TYPE);\n      assert.equal(action.meta.to, PRELOAD_MESSAGE_TYPE);\n    });\n    it(\"should broadcast the action only if shouldBroadcast is true\", () => {\n      feed.onUpdateSection(null, FAKE_ID, { rows: FAKE_ROWS }, true);\n      const [action] = feed.store.dispatch.firstCall.args;\n      // Should be broadcast\n      assert.equal(action.meta.from, MAIN_MESSAGE_TYPE);\n      assert.equal(action.meta.to, CONTENT_MESSAGE_TYPE);\n    });\n  });\n  describe(\"#onUpdateSectionCard\", () => {\n    it(\"should do nothing if no options are provided\", () => {\n      feed.onUpdateSectionCard(null, FAKE_ID, FAKE_URL, null);\n      assert.notCalled(feed.store.dispatch);\n    });\n    it(\"should dispatch a SECTION_UPDATE_CARD action with the correct data\", () => {\n      feed.onUpdateSectionCard(null, FAKE_ID, FAKE_URL, FAKE_CARD_OPTIONS);\n      const [action] = feed.store.dispatch.firstCall.args;\n      assert.equal(action.type, \"SECTION_UPDATE_CARD\");\n      assert.deepEqual(action.data, {\n        id: FAKE_ID,\n        url: FAKE_URL,\n        options: FAKE_CARD_OPTIONS,\n      });\n      // Should be not broadcast by default, but should update the preloaded tab, so check meta\n      assert.equal(action.meta.from, MAIN_MESSAGE_TYPE);\n      assert.equal(action.meta.to, PRELOAD_MESSAGE_TYPE);\n    });\n    it(\"should broadcast the action only if shouldBroadcast is true\", () => {\n      feed.onUpdateSectionCard(\n        null,\n        FAKE_ID,\n        FAKE_URL,\n        FAKE_CARD_OPTIONS,\n        true\n      );\n      const [action] = feed.store.dispatch.firstCall.args;\n      // Should be broadcast\n      assert.equal(action.meta.from, MAIN_MESSAGE_TYPE);\n      assert.equal(action.meta.to, CONTENT_MESSAGE_TYPE);\n    });\n  });\n  describe(\"#onAction\", () => {\n    it(\"should bind this.init to SectionsManager.INIT on INIT\", () => {\n      sinon.spy(SectionsManager, \"once\");\n      feed.onAction({ type: \"INIT\" });\n      assert.calledOnce(SectionsManager.once);\n      assert.calledWith(SectionsManager.once, SectionsManager.INIT, feed.init);\n    });\n    it(\"should call SectionsManager.init on action PREFS_INITIAL_VALUES\", () => {\n      sinon.spy(SectionsManager, \"init\");\n      feed.onAction({ type: \"PREFS_INITIAL_VALUES\", data: { foo: \"bar\" } });\n      assert.calledOnce(SectionsManager.init);\n      assert.calledWith(SectionsManager.init, { foo: \"bar\" });\n      assert.calledOnce(feed.store.dbStorage.getDbTable);\n      assert.calledWithExactly(feed.store.dbStorage.getDbTable, \"sectionPrefs\");\n    });\n    it(\"should call SectionsManager.addBuiltInSection on suitable PREF_CHANGED events\", () => {\n      sinon.spy(SectionsManager, \"addBuiltInSection\");\n      feed.onAction({\n        type: \"PREF_CHANGED\",\n        data: { name: \"feeds.section.topstories.options\", value: \"foo\" },\n      });\n      assert.calledOnce(SectionsManager.addBuiltInSection);\n      assert.calledWith(\n        SectionsManager.addBuiltInSection,\n        \"feeds.section.topstories\",\n        \"foo\"\n      );\n    });\n    it(\"should fire SECTION_OPTIONS_UPDATED on suitable PREF_CHANGED events\", async () => {\n      await feed.onAction({\n        type: \"PREF_CHANGED\",\n        data: { name: \"feeds.section.topstories.options\", value: \"foo\" },\n      });\n      assert.calledOnce(feed.store.dispatch);\n      const [action] = feed.store.dispatch.firstCall.args;\n      assert.equal(action.type, \"SECTION_OPTIONS_CHANGED\");\n      assert.equal(action.data, \"topstories\");\n    });\n    it(\"should call SectionsManager.disableSection on SECTION_DISABLE\", () => {\n      sinon.spy(SectionsManager, \"disableSection\");\n      feed.onAction({ type: \"SECTION_DISABLE\", data: 1234 });\n      assert.calledOnce(SectionsManager.disableSection);\n      assert.calledWith(SectionsManager.disableSection, 1234);\n      SectionsManager.disableSection.restore();\n    });\n    it(\"should call SectionsManager.enableSection on SECTION_ENABLE\", () => {\n      sinon.spy(SectionsManager, \"enableSection\");\n      feed.onAction({ type: \"SECTION_ENABLE\", data: 1234 });\n      assert.calledOnce(SectionsManager.enableSection);\n      assert.calledWith(SectionsManager.enableSection, 1234);\n      SectionsManager.enableSection.restore();\n    });\n    it(\"should call the feed's uninit on UNINIT\", () => {\n      sinon.stub(feed, \"uninit\");\n\n      feed.onAction({ type: \"UNINIT\" });\n\n      assert.calledOnce(feed.uninit);\n    });\n    it(\"should emit a ACTION_DISPATCHED event and forward any action in ACTIONS_TO_PROXY if there are any sections\", () => {\n      const spy = sinon.spy();\n      const allowedActions = SectionsManager.ACTIONS_TO_PROXY;\n      const disallowedActions = [\"PREF_CHANGED\", \"OPEN_PRIVATE_WINDOW\"];\n      feed.init();\n      SectionsManager.on(SectionsManager.ACTION_DISPATCHED, spy);\n      // Make sure we start with no sections - no event should be emitted\n      SectionsManager.sections.clear();\n      feed.onAction({ type: allowedActions[0] });\n      assert.notCalled(spy);\n      // Then add a section and check correct behaviour\n      SectionsManager.addSection(FAKE_ID, FAKE_OPTIONS);\n      for (const action of allowedActions.concat(disallowedActions)) {\n        feed.onAction({ type: action });\n      }\n      for (const action of allowedActions) {\n        assert.calledWith(spy, \"ACTION_DISPATCHED\", action);\n      }\n      for (const action of disallowedActions) {\n        assert.neverCalledWith(spy, \"ACTION_DISPATCHED\", action);\n      }\n    });\n    it(\"should call updateBookmarkMetadata on PLACES_BOOKMARK_ADDED\", () => {\n      const stub = sinon.stub(SectionsManager, \"updateBookmarkMetadata\");\n\n      feed.onAction({ type: \"PLACES_BOOKMARK_ADDED\", data: {} });\n\n      assert.calledOnce(stub);\n    });\n    it(\"should call updateSectionPrefs on UPDATE_SECTION_PREFS\", () => {\n      const stub = sinon.stub(SectionsManager, \"updateSectionPrefs\");\n\n      feed.onAction({ type: \"UPDATE_SECTION_PREFS\", data: {} });\n\n      assert.calledOnce(stub);\n    });\n    it(\"should call SectionManager.removeSectionCard on WEBEXT_DISMISS\", () => {\n      const stub = sinon.stub(SectionsManager, \"removeSectionCard\");\n\n      feed.onAction(\n        ac.WebExtEvent(at.WEBEXT_DISMISS, { source: \"Foo\", url: \"bar.com\" })\n      );\n\n      assert.calledOnce(stub);\n      assert.calledWith(stub, \"Foo\", \"bar.com\");\n    });\n    it(\"should call the feed's moveSection on SECTION_MOVE\", () => {\n      sinon.stub(feed, \"moveSection\");\n      const id = \"topsites\";\n      const direction = +1;\n      feed.onAction({ type: \"SECTION_MOVE\", data: { id, direction } });\n\n      assert.calledOnce(feed.moveSection);\n      assert.calledWith(feed.moveSection, id, direction);\n    });\n  });\n  describe(\"#moveSection\", () => {\n    it(\"should Move Down correctly\", () => {\n      feed.store.state.Sections = [\n        { id: \"topstories\", enabled: true },\n        { id: \"highlights\", enabled: true },\n      ];\n      feed.moveSection(\"topsites\", +1);\n      assert.calledOnce(feed.store.dispatch);\n      assert.calledWith(feed.store.dispatch, {\n        data: { name: \"sectionOrder\", value: \"topstories,topsites,highlights\" },\n        meta: { from: \"ActivityStream:Content\", to: \"ActivityStream:Main\" },\n        type: \"SET_PREF\",\n      });\n      feed.store.dispatch.resetHistory();\n      feed.moveSection(\"topstories\", +1);\n      assert.calledOnce(feed.store.dispatch);\n      assert.calledWith(feed.store.dispatch, {\n        data: { name: \"sectionOrder\", value: \"topsites,highlights,topstories\" },\n        meta: { from: \"ActivityStream:Content\", to: \"ActivityStream:Main\" },\n        type: \"SET_PREF\",\n      });\n    });\n    it(\"should Move Up correctly\", () => {\n      feed.store.state.Sections = [\n        { id: \"topstories\", enabled: true },\n        { id: \"highlights\", enabled: true },\n      ];\n      feed.moveSection(\"topstories\", -1);\n      assert.calledOnce(feed.store.dispatch);\n      assert.calledWith(feed.store.dispatch, {\n        data: { name: \"sectionOrder\", value: \"topstories,topsites,highlights\" },\n        meta: { from: \"ActivityStream:Content\", to: \"ActivityStream:Main\" },\n        type: \"SET_PREF\",\n      });\n      feed.store.dispatch.resetHistory();\n      feed.moveSection(\"highlights\", -1);\n      assert.calledOnce(feed.store.dispatch);\n      assert.calledWith(feed.store.dispatch, {\n        data: { name: \"sectionOrder\", value: \"topsites,highlights,topstories\" },\n        meta: { from: \"ActivityStream:Content\", to: \"ActivityStream:Main\" },\n        type: \"SET_PREF\",\n      });\n    });\n    it(\"should skip over sections that aren't enabled\", () => {\n      feed.store.state.Sections = [\n        { id: \"topstories\", enabled: false },\n        { id: \"highlights\", enabled: true },\n      ];\n      feed.moveSection(\"highlights\", -1);\n      assert.calledOnce(feed.store.dispatch);\n      assert.calledWith(feed.store.dispatch, {\n        data: { name: \"sectionOrder\", value: \"highlights,topsites,topstories\" },\n        meta: { from: \"ActivityStream:Content\", to: \"ActivityStream:Main\" },\n        type: \"SET_PREF\",\n      });\n      feed.store.dispatch.resetHistory();\n      feed.moveSection(\"topsites\", +1);\n      assert.calledOnce(feed.store.dispatch);\n      assert.calledWith(feed.store.dispatch, {\n        data: { name: \"sectionOrder\", value: \"topstories,highlights,topsites\" },\n        meta: { from: \"ActivityStream:Content\", to: \"ActivityStream:Main\" },\n        type: \"SET_PREF\",\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/lib/ShortUrl.test.js",
    "content": "import { GlobalOverrider } from \"test/unit/utils\";\nimport { shortURL } from \"lib/ShortURL.jsm\";\n\nconst puny = \"xn--kpry57d\";\nconst idn = \"台灣\";\n\ndescribe(\"shortURL\", () => {\n  let globals;\n  let IDNStub;\n  let getPublicSuffixFromHostStub;\n\n  beforeEach(() => {\n    IDNStub = sinon.stub().callsFake(host => host.replace(puny, idn));\n    getPublicSuffixFromHostStub = sinon.stub().returns(\"com\");\n\n    globals = new GlobalOverrider();\n    globals.set(\"IDNService\", { convertToDisplayIDN: IDNStub });\n    globals.set(\"Services\", {\n      eTLD: { getPublicSuffixFromHost: getPublicSuffixFromHostStub },\n    });\n  });\n\n  afterEach(() => {\n    globals.restore();\n  });\n\n  it(\"should return a blank string if url is falsey\", () => {\n    assert.equal(shortURL({ url: false }), \"\");\n    assert.equal(shortURL({ url: \"\" }), \"\");\n    assert.equal(shortURL({}), \"\");\n  });\n\n  it(\"should return the 'url' if not a valid url\", () => {\n    const checkInvalid = url => assert.equal(shortURL({ url }), url);\n    checkInvalid(true);\n    checkInvalid(\"something\");\n    checkInvalid(\"http:\");\n    checkInvalid(\"http::double\");\n    checkInvalid(\"http://badport:65536/\");\n  });\n\n  it(\"should remove the eTLD\", () => {\n    assert.equal(shortURL({ url: \"http://com.blah.com\" }), \"com.blah\");\n  });\n\n  it(\"should convert host to idn when calling shortURL\", () => {\n    assert.equal(shortURL({ url: `http://${puny}.blah.com` }), `${idn}.blah`);\n  });\n\n  it(\"should get the hostname from .url\", () => {\n    assert.equal(shortURL({ url: \"http://bar.com\" }), \"bar\");\n  });\n\n  it(\"should not strip out www if not first subdomain\", () => {\n    assert.equal(shortURL({ url: \"http://foo.www.com\" }), \"foo.www\");\n  });\n\n  it(\"should convert to lowercase\", () => {\n    assert.equal(shortURL({ url: \"HTTP://FOO.COM\" }), \"foo\");\n  });\n\n  it(\"should not include the port\", () => {\n    assert.equal(shortURL({ url: \"http://foo.com:8888\" }), \"foo\");\n  });\n\n  it(\"should return hostname for localhost\", () => {\n    getPublicSuffixFromHostStub.throws(\"insufficient domain levels\");\n\n    assert.equal(shortURL({ url: \"http://localhost:8000/\" }), \"localhost\");\n  });\n\n  it(\"should return hostname for ip address\", () => {\n    getPublicSuffixFromHostStub.throws(\"host is ip address\");\n\n    assert.equal(shortURL({ url: \"http://127.0.0.1/foo\" }), \"127.0.0.1\");\n  });\n\n  it(\"should return etld for www.gov.uk (www-only non-etld)\", () => {\n    getPublicSuffixFromHostStub.returns(\"gov.uk\");\n\n    assert.equal(\n      shortURL({ url: \"https://www.gov.uk/countersigning\" }),\n      \"gov.uk\"\n    );\n  });\n\n  it(\"should return idn etld for www-only non-etld\", () => {\n    getPublicSuffixFromHostStub.returns(puny);\n\n    assert.equal(shortURL({ url: `https://www.${puny}/foo` }), idn);\n  });\n\n  it(\"should return not the protocol for file:\", () => {\n    assert.equal(shortURL({ url: \"file:///foo/bar.txt\" }), \"/foo/bar.txt\");\n  });\n\n  it(\"should return not the protocol for about:\", () => {\n    assert.equal(shortURL({ url: \"about:newtab\" }), \"newtab\");\n  });\n\n  it(\"should fall back to full url as a last resort\", () => {\n    assert.equal(shortURL({ url: \"about:\" }), \"about:\");\n  });\n});\n"
  },
  {
    "path": "test/unit/lib/SiteClassifier.test.js",
    "content": "import { classifySite } from \"lib/SiteClassifier.jsm\";\n\nconst FAKE_CLASSIFIER_DATA = [\n  {\n    type: \"hostname-and-params-match\",\n    criteria: [\n      {\n        hostname: \"hostnameandparams.com\",\n        params: [\n          {\n            key: \"param1\",\n            value: \"val1\",\n          },\n        ],\n      },\n    ],\n    weight: 300,\n  },\n  {\n    type: \"url-match\",\n    criteria: [{ url: \"https://fullurl.com/must/match\" }],\n    weight: 400,\n  },\n  {\n    type: \"params-match\",\n    criteria: [\n      {\n        params: [\n          {\n            key: \"param1\",\n            value: \"val1\",\n          },\n          {\n            key: \"param2\",\n            value: \"val2\",\n          },\n        ],\n      },\n    ],\n    weight: 200,\n  },\n  {\n    type: \"params-prefix-match\",\n    criteria: [\n      {\n        params: [\n          {\n            key: \"client\",\n            prefix: \"fir\",\n          },\n        ],\n      },\n    ],\n    weight: 200,\n  },\n  {\n    type: \"has-params\",\n    criteria: [\n      {\n        params: [{ key: \"has-param1\" }, { key: \"has-param2\" }],\n      },\n    ],\n    weight: 100,\n  },\n  {\n    type: \"search-engine\",\n    criteria: [\n      { sld: \"google\" },\n      { hostname: \"bing.com\" },\n      { hostname: \"duckduckgo.com\" },\n    ],\n    weight: 1,\n  },\n  {\n    type: \"news-portal\",\n    criteria: [\n      { hostname: \"yahoo.com\" },\n      { hostname: \"aol.com\" },\n      { hostname: \"msn.com\" },\n    ],\n    weight: 1,\n  },\n  {\n    type: \"social-media\",\n    criteria: [{ hostname: \"facebook.com\" }, { hostname: \"twitter.com\" }],\n    weight: 1,\n  },\n  {\n    type: \"ecommerce\",\n    criteria: [{ sld: \"amazon\" }, { hostname: \"ebay.com\" }],\n    weight: 1,\n  },\n];\n\ndescribe(\"SiteClassifier\", () => {\n  function RemoteSettings() {\n    return {\n      get() {\n        return Promise.resolve(FAKE_CLASSIFIER_DATA);\n      },\n    };\n  }\n\n  it(\"should return the right category\", async () => {\n    assert.equal(\n      \"hostname-and-params-match\",\n      await classifySite(\n        \"https://hostnameandparams.com?param1=val1\",\n        RemoteSettings\n      )\n    );\n    assert.equal(\n      \"other\",\n      await classifySite(\n        \"https://hostnameandparams.com?param1=val\",\n        RemoteSettings\n      )\n    );\n    assert.equal(\n      \"other\",\n      await classifySite(\n        \"https://hostnameandparams.com?param=val1\",\n        RemoteSettings\n      )\n    );\n    assert.equal(\n      \"other\",\n      await classifySite(\"https://hostnameandparams.com\", RemoteSettings)\n    );\n    assert.equal(\n      \"other\",\n      await classifySite(\"https://params.com?param1=val1\", RemoteSettings)\n    );\n\n    assert.equal(\n      \"url-match\",\n      await classifySite(\"https://fullurl.com/must/match\", RemoteSettings)\n    );\n    assert.equal(\n      \"other\",\n      await classifySite(\"http://fullurl.com/must/match\", RemoteSettings)\n    );\n\n    assert.equal(\n      \"params-match\",\n      await classifySite(\n        \"https://example.com?param1=val1&param2=val2\",\n        RemoteSettings\n      )\n    );\n    assert.equal(\n      \"params-match\",\n      await classifySite(\n        \"https://example.com?param1=val1&param2=val2&other=other\",\n        RemoteSettings\n      )\n    );\n    assert.equal(\n      \"other\",\n      await classifySite(\n        \"https://example.com?param1=val2&param2=val1\",\n        RemoteSettings\n      )\n    );\n    assert.equal(\n      \"other\",\n      await classifySite(\"https://example.com?param1&param2\", RemoteSettings)\n    );\n\n    assert.equal(\n      \"params-prefix-match\",\n      await classifySite(\"https://search.com?client=firefox\", RemoteSettings)\n    );\n    assert.equal(\n      \"params-prefix-match\",\n      await classifySite(\"https://search.com?client=fir\", RemoteSettings)\n    );\n    assert.equal(\n      \"other\",\n      await classifySite(\n        \"https://search.com?client=mozillafirefox\",\n        RemoteSettings\n      )\n    );\n\n    assert.equal(\n      \"has-params\",\n      await classifySite(\n        \"https://example.com?has-param1=val1&has-param2=val2\",\n        RemoteSettings\n      )\n    );\n    assert.equal(\n      \"has-params\",\n      await classifySite(\n        \"https://example.com?has-param1&has-param2\",\n        RemoteSettings\n      )\n    );\n    assert.equal(\n      \"has-params\",\n      await classifySite(\n        \"https://example.com?has-param1&has-param2&other=other\",\n        RemoteSettings\n      )\n    );\n    assert.equal(\n      \"other\",\n      await classifySite(\"https://example.com?has-param1\", RemoteSettings)\n    );\n    assert.equal(\n      \"other\",\n      await classifySite(\"https://example.com?has-param2\", RemoteSettings)\n    );\n\n    assert.equal(\n      \"search-engine\",\n      await classifySite(\"https://google.com\", RemoteSettings)\n    );\n    assert.equal(\n      \"search-engine\",\n      await classifySite(\"https://google.de\", RemoteSettings)\n    );\n    assert.equal(\n      \"search-engine\",\n      await classifySite(\"http://bing.com/?q=firefox\", RemoteSettings)\n    );\n\n    assert.equal(\n      \"news-portal\",\n      await classifySite(\"https://yahoo.com\", RemoteSettings)\n    );\n\n    assert.equal(\n      \"social-media\",\n      await classifySite(\"http://twitter.com/firefox\", RemoteSettings)\n    );\n\n    assert.equal(\n      \"ecommerce\",\n      await classifySite(\"https://amazon.com\", RemoteSettings)\n    );\n    assert.equal(\n      \"ecommerce\",\n      await classifySite(\"https://amazon.ca\", RemoteSettings)\n    );\n    assert.equal(\n      \"ecommerce\",\n      await classifySite(\"https://ebay.com\", RemoteSettings)\n    );\n  });\n});\n"
  },
  {
    "path": "test/unit/lib/Store.test.js",
    "content": "import { addNumberReducer, FakePrefs } from \"test/unit/utils\";\nimport { createStore } from \"redux\";\nimport injector from \"inject!lib/Store.jsm\";\n\ndescribe(\"Store\", () => {\n  let Store;\n  let sandbox;\n  let store;\n  let dbStub;\n  beforeEach(() => {\n    sandbox = sinon.createSandbox();\n    function ActivityStreamMessageChannel(options) {\n      this.dispatch = options.dispatch;\n      this.createChannel = sandbox.spy();\n      this.destroyChannel = sandbox.spy();\n      this.middleware = sandbox.spy(s => next => action => next(action));\n      this.simulateMessagesForExistingTabs = sandbox.stub();\n    }\n    dbStub = sandbox.stub().resolves();\n    function FakeActivityStreamStorage() {\n      this.db = {};\n      sinon.stub(this, \"db\").get(dbStub);\n    }\n    ({ Store } = injector({\n      \"lib/ActivityStreamMessageChannel.jsm\": { ActivityStreamMessageChannel },\n      \"lib/ActivityStreamPrefs.jsm\": { Prefs: FakePrefs },\n      \"lib/ActivityStreamStorage.jsm\": {\n        ActivityStreamStorage: FakeActivityStreamStorage,\n      },\n    }));\n    store = new Store();\n    sandbox.stub(store, \"_initIndexedDB\").resolves();\n  });\n  afterEach(() => {\n    sandbox.restore();\n  });\n  it(\"should have a .feeds property that is a Map\", () => {\n    assert.instanceOf(store.feeds, Map);\n    assert.equal(store.feeds.size, 0, \".feeds.size\");\n  });\n  it(\"should have a redux store at ._store\", () => {\n    assert.ok(store._store);\n    assert.property(store, \"dispatch\");\n    assert.property(store, \"getState\");\n  });\n  it(\"should create a ActivityStreamMessageChannel with the right dispatcher\", () => {\n    assert.ok(store._messageChannel);\n    assert.equal(store._messageChannel.dispatch, store.dispatch);\n  });\n  it(\"should connect the ActivityStreamMessageChannel's middleware\", () => {\n    store.dispatch({ type: \"FOO\" });\n    assert.calledOnce(store._messageChannel.middleware);\n  });\n  describe(\"#initFeed\", () => {\n    it(\"should add an instance of the feed to .feeds\", () => {\n      class Foo {}\n      store._prefs.set(\"foo\", true);\n      store.init(new Map([[\"foo\", () => new Foo()]]));\n      store.initFeed(\"foo\");\n\n      assert.isTrue(store.feeds.has(\"foo\"), \"foo is set\");\n      assert.instanceOf(store.feeds.get(\"foo\"), Foo);\n    });\n    it(\"should call the feed's onAction with uninit action if it exists\", () => {\n      let feed;\n      function createFeed() {\n        feed = { onAction: sinon.spy() };\n        return feed;\n      }\n      const action = { type: \"FOO\" };\n      store._feedFactories = new Map([[\"foo\", createFeed]]);\n\n      store.initFeed(\"foo\", action);\n\n      assert.calledOnce(feed.onAction);\n      assert.calledWith(feed.onAction, action);\n    });\n    it(\"should add a .store property to the feed\", () => {\n      class Foo {}\n      store._feedFactories = new Map([[\"foo\", () => new Foo()]]);\n      store.initFeed(\"foo\");\n\n      assert.propertyVal(store.feeds.get(\"foo\"), \"store\", store);\n    });\n  });\n  describe(\"#uninitFeed\", () => {\n    it(\"should not throw if no feed with that name exists\", () => {\n      assert.doesNotThrow(() => {\n        store.uninitFeed(\"bar\");\n      });\n    });\n    it(\"should call the feed's onAction with uninit action if it exists\", () => {\n      let feed;\n      function createFeed() {\n        feed = { onAction: sinon.spy() };\n        return feed;\n      }\n      const action = { type: \"BAR\" };\n      store._feedFactories = new Map([[\"foo\", createFeed]]);\n      store.initFeed(\"foo\");\n\n      store.uninitFeed(\"foo\", action);\n\n      assert.calledOnce(feed.onAction);\n      assert.calledWith(feed.onAction, action);\n    });\n    it(\"should remove the feed from .feeds\", () => {\n      class Foo {}\n      store._feedFactories = new Map([[\"foo\", () => new Foo()]]);\n\n      store.initFeed(\"foo\");\n      store.uninitFeed(\"foo\");\n\n      assert.isFalse(store.feeds.has(\"foo\"), \"foo is not in .feeds\");\n    });\n  });\n  describe(\"onPrefChanged\", () => {\n    beforeEach(() => {\n      sinon.stub(store, \"initFeed\");\n      sinon.stub(store, \"uninitFeed\");\n      store._prefs.set(\"foo\", false);\n      store.init(new Map([[\"foo\", () => ({})]]));\n    });\n    it(\"should initialize the feed if called with true\", () => {\n      store.onPrefChanged(\"foo\", true);\n\n      assert.calledWith(store.initFeed, \"foo\");\n      assert.notCalled(store.uninitFeed);\n    });\n    it(\"should uninitialize the feed if called with false\", () => {\n      store.onPrefChanged(\"foo\", false);\n\n      assert.calledWith(store.uninitFeed, \"foo\");\n      assert.notCalled(store.initFeed);\n    });\n    it(\"should do nothing if not an expected feed\", () => {\n      store.onPrefChanged(\"bar\", false);\n\n      assert.notCalled(store.initFeed);\n      assert.notCalled(store.uninitFeed);\n    });\n  });\n  describe(\"#init\", () => {\n    it(\"should call .initFeed with each key\", async () => {\n      sinon.stub(store, \"initFeed\");\n      store._prefs.set(\"foo\", true);\n      store._prefs.set(\"bar\", true);\n      await store.init(new Map([[\"foo\", () => {}], [\"bar\", () => {}]]));\n      assert.calledWith(store.initFeed, \"foo\");\n      assert.calledWith(store.initFeed, \"bar\");\n    });\n    it(\"should call _initIndexedDB\", async () => {\n      await store.init(new Map());\n\n      assert.calledOnce(store._initIndexedDB);\n      assert.calledWithExactly(store._initIndexedDB, \"feeds.telemetry\");\n    });\n    it(\"should access the db property of indexedDB\", async () => {\n      store._initIndexedDB.restore();\n      await store.init(new Map());\n\n      assert.calledOnce(dbStub);\n    });\n    it(\"should reset ActivityStreamStorage telemetry if opening the db fails\", async () => {\n      store._initIndexedDB.restore();\n      // Force an IndexedDB error\n      dbStub.rejects();\n\n      await store.init(new Map());\n\n      assert.calledOnce(dbStub);\n      assert.isNull(store.dbStorage.telemetry);\n    });\n    it(\"should not initialize the feed if the Pref is set to false\", async () => {\n      sinon.stub(store, \"initFeed\");\n      store._prefs.set(\"foo\", false);\n      await store.init(new Map([[\"foo\", () => {}]]));\n      assert.notCalled(store.initFeed);\n    });\n    it(\"should observe the pref branch\", async () => {\n      sinon.stub(store._prefs, \"observeBranch\");\n      await store.init(new Map());\n      assert.calledOnce(store._prefs.observeBranch);\n      assert.calledWith(store._prefs.observeBranch, store);\n    });\n    it(\"should initialize the ActivityStreamMessageChannel channel\", async () => {\n      await store.init(new Map());\n      assert.calledOnce(store._messageChannel.createChannel);\n    });\n    it(\"should emit an initial event if provided\", async () => {\n      sinon.stub(store, \"dispatch\");\n      const action = { type: \"FOO\" };\n\n      await store.init(new Map(), action);\n\n      assert.calledOnce(store.dispatch);\n      assert.calledWith(store.dispatch, action);\n    });\n    it(\"should initialize the telemtry feed first\", () => {\n      store._prefs.set(\"feeds.foo\", true);\n      store._prefs.set(\"feeds.telemetry\", true);\n      const telemetrySpy = sandbox.stub().returns({});\n      const fooSpy = sandbox.stub().returns({});\n      // Intentionally put the telemetry feed as the second item.\n      const feedFactories = new Map([\n        [\"feeds.foo\", fooSpy],\n        [\"feeds.telemetry\", telemetrySpy],\n      ]);\n      store.init(feedFactories);\n      assert.ok(telemetrySpy.calledBefore(fooSpy));\n    });\n    it(\"should dispatch init/load events\", async () => {\n      await store.init(new Map(), { type: \"FOO\" });\n\n      assert.calledOnce(store._messageChannel.simulateMessagesForExistingTabs);\n    });\n    it(\"should dispatch INIT before LOAD\", async () => {\n      const init = { type: \"INIT\" };\n      const load = { type: \"TAB_LOAD\" };\n      sandbox.stub(store, \"dispatch\");\n      store._messageChannel.simulateMessagesForExistingTabs.callsFake(() =>\n        store.dispatch(load)\n      );\n      await store.init(new Map(), init);\n\n      assert.calledTwice(store.dispatch);\n      assert.equal(store.dispatch.firstCall.args[0], init);\n      assert.equal(store.dispatch.secondCall.args[0], load);\n    });\n  });\n  describe(\"#uninit\", () => {\n    it(\"should emit an uninit event if provided on init\", () => {\n      sinon.stub(store, \"dispatch\");\n      const action = { type: \"BAR\" };\n      store.init(new Map(), null, action);\n\n      store.uninit();\n\n      assert.calledOnce(store.dispatch);\n      assert.calledWith(store.dispatch, action);\n    });\n    it(\"should clear .feeds and ._feedFactories\", () => {\n      store._prefs.set(\"a\", true);\n      store.init(\n        new Map([[\"a\", () => ({})], [\"b\", () => ({})], [\"c\", () => ({})]])\n      );\n\n      store.uninit();\n\n      assert.equal(store.feeds.size, 0);\n      assert.isNull(store._feedFactories);\n    });\n    it(\"should destroy the ActivityStreamMessageChannel channel\", () => {\n      store.uninit();\n      assert.calledOnce(store._messageChannel.destroyChannel);\n    });\n  });\n  describe(\"#getState\", () => {\n    it(\"should return the redux state\", () => {\n      store._store = createStore((prevState = 123) => prevState);\n      const { getState } = store;\n      assert.equal(getState(), 123);\n    });\n  });\n  describe(\"#dispatch\", () => {\n    it(\"should call .onAction of each feed\", async () => {\n      const { dispatch } = store;\n      const sub = { onAction: sinon.spy() };\n      const action = { type: \"FOO\" };\n\n      store._prefs.set(\"sub\", true);\n      await store.init(new Map([[\"sub\", () => sub]]));\n\n      dispatch(action);\n\n      assert.calledWith(sub.onAction, action);\n    });\n    it(\"should call the reducers\", () => {\n      const { dispatch } = store;\n      store._store = createStore(addNumberReducer);\n\n      dispatch({ type: \"ADD\", data: 14 });\n\n      assert.equal(store.getState(), 14);\n    });\n  });\n  describe(\"#subscribe\", () => {\n    it(\"should subscribe to changes to the store\", () => {\n      const sub = sinon.spy();\n      const action = { type: \"FOO\" };\n\n      store.subscribe(sub);\n      store.dispatch(action);\n\n      assert.calledOnce(sub);\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/lib/SystemTickFeed.test.js",
    "content": "import { SYSTEM_TICK_INTERVAL, SystemTickFeed } from \"lib/SystemTickFeed.jsm\";\nimport { actionTypes as at } from \"common/Actions.jsm\";\n\ndescribe(\"System Tick Feed\", () => {\n  let instance;\n  let clock;\n\n  beforeEach(() => {\n    clock = sinon.useFakeTimers();\n\n    instance = new SystemTickFeed();\n    instance.store = {\n      getState() {\n        return {};\n      },\n      dispatch() {},\n    };\n  });\n  afterEach(() => {\n    clock.restore();\n  });\n  it(\"should create a SystemTickFeed\", () => {\n    assert.instanceOf(instance, SystemTickFeed);\n  });\n  it(\"should fire SYSTEM_TICK events at configured interval\", () => {\n    let expectation = sinon\n      .mock(instance.store)\n      .expects(\"dispatch\")\n      .twice()\n      .withExactArgs({ type: at.SYSTEM_TICK });\n\n    instance.onAction({ type: at.INIT });\n    clock.tick(SYSTEM_TICK_INTERVAL * 2);\n    expectation.verify();\n  });\n  it(\"should not fire SYSTEM_TICK events after UNINIT\", () => {\n    let expectation = sinon\n      .mock(instance.store)\n      .expects(\"dispatch\")\n      .never();\n\n    instance.onAction({ type: at.UNINIT });\n    clock.tick(SYSTEM_TICK_INTERVAL * 2);\n    expectation.verify();\n  });\n});\n"
  },
  {
    "path": "test/unit/lib/TelemetryFeed.test.js",
    "content": "/* global Services */\nimport {\n  actionCreators as ac,\n  actionTypes as at,\n  actionUtils as au,\n} from \"common/Actions.jsm\";\nimport {\n  ASRouterEventPing,\n  BasePing,\n  ImpressionStatsPing,\n  PerfPing,\n  SessionPing,\n  SpocsFillPing,\n  UndesiredPing,\n  UserEventPing,\n} from \"test/schemas/pings\";\nimport { FakePrefs, GlobalOverrider } from \"test/unit/utils\";\nimport { ASRouterPreferences } from \"lib/ASRouterPreferences.jsm\";\nimport injector from \"inject!lib/TelemetryFeed.jsm\";\n\nconst FAKE_UUID = \"{foo-123-foo}\";\nconst FAKE_ROUTER_MESSAGE_PROVIDER = [{ id: \"cfr\", enabled: true }];\nconst FAKE_ROUTER_MESSAGE_PROVIDER_COHORT = [\n  { id: \"cfr\", enabled: true, cohort: \"cohort_group\" },\n];\nconst FAKE_TELEMETRY_ID = \"foo123\";\n\ndescribe(\"TelemetryFeed\", () => {\n  let globals;\n  let sandbox;\n  let expectedUserPrefs;\n  let browser = {\n    getAttribute() {\n      return \"true\";\n    },\n  };\n  let instance;\n  let clock;\n  let fakeHomePageUrl;\n  let fakeHomePage;\n  let fakeExtensionSettingsStore;\n  class PingCentre {\n    sendPing() {}\n    uninit() {}\n    sendStructuredIngestionPing() {}\n  }\n  class UTEventReporting {\n    sendUserEvent() {}\n    sendSessionEndEvent() {}\n    sendTrailheadEnrollEvent() {}\n    uninit() {}\n  }\n  class PerfService {\n    getMostRecentAbsMarkStartByName() {\n      return 1234;\n    }\n    mark() {}\n    absNow() {\n      return 123;\n    }\n    get timeOrigin() {\n      return 123456;\n    }\n  }\n  const perfService = new PerfService();\n  const {\n    TelemetryFeed,\n    USER_PREFS_ENCODING,\n    PREF_IMPRESSION_ID,\n    TELEMETRY_PREF,\n    EVENTS_TELEMETRY_PREF,\n    STRUCTURED_INGESTION_TELEMETRY_PREF,\n    STRUCTURED_INGESTION_ENDPOINT_PREF,\n  } = injector({\n    \"common/PerfService.jsm\": { perfService },\n    \"lib/UTEventReporting.jsm\": { UTEventReporting },\n  });\n\n  beforeEach(() => {\n    globals = new GlobalOverrider();\n    sandbox = globals.sandbox;\n    clock = sinon.useFakeTimers();\n    fakeHomePageUrl = \"about:home\";\n    fakeHomePage = {\n      get() {\n        return fakeHomePageUrl;\n      },\n    };\n    fakeExtensionSettingsStore = {\n      initialize() {\n        return Promise.resolve();\n      },\n      getSetting() {},\n    };\n    sandbox.spy(global.Cu, \"reportError\");\n    globals.set(\"gUUIDGenerator\", { generateUUID: () => FAKE_UUID });\n    globals.set(\"aboutNewTabService\", {\n      overridden: false,\n      newTabURL: \"\",\n    });\n    globals.set(\"HomePage\", fakeHomePage);\n    globals.set(\"ExtensionSettingsStore\", fakeExtensionSettingsStore);\n    globals.set(\"PingCentre\", PingCentre);\n    globals.set(\"UTEventReporting\", UTEventReporting);\n    globals.set(\"ClientID\", {\n      getClientID: sandbox.spy(async () => FAKE_TELEMETRY_ID),\n    });\n    sandbox\n      .stub(ASRouterPreferences, \"providers\")\n      .get(() => FAKE_ROUTER_MESSAGE_PROVIDER);\n    instance = new TelemetryFeed();\n  });\n  afterEach(() => {\n    clock.restore();\n    globals.restore();\n    FakePrefs.prototype.prefs = {};\n    ASRouterPreferences.uninit();\n  });\n  describe(\"#init\", () => {\n    it(\"should add .pingCentre, a PingCentre instance\", () => {\n      assert.instanceOf(instance.pingCentre, PingCentre);\n    });\n    it(\"should add .utEvents, a UTEventReporting instance\", () => {\n      assert.instanceOf(instance.utEvents, UTEventReporting);\n    });\n    it(\"should make this.browserOpenNewtabStart() observe browser-open-newtab-start\", () => {\n      sandbox.spy(Services.obs, \"addObserver\");\n\n      instance.init();\n\n      assert.calledTwice(Services.obs.addObserver);\n      assert.calledWithExactly(\n        Services.obs.addObserver,\n        instance.browserOpenNewtabStart,\n        \"browser-open-newtab-start\"\n      );\n    });\n    it(\"should add window open listener\", () => {\n      sandbox.spy(Services.obs, \"addObserver\");\n\n      instance.init();\n\n      assert.calledTwice(Services.obs.addObserver);\n      assert.calledWithExactly(\n        Services.obs.addObserver,\n        instance._addWindowListeners,\n        \"domwindowopened\"\n      );\n    });\n    it(\"should add TabPinned event listener on new windows\", () => {\n      const stub = { addEventListener: sandbox.stub() };\n      sandbox.spy(Services.obs, \"addObserver\");\n\n      instance.init();\n\n      assert.calledTwice(Services.obs.addObserver);\n      const [cb] = Services.obs.addObserver.secondCall.args;\n      cb(stub);\n      assert.calledTwice(stub.addEventListener);\n      assert.calledWithExactly(\n        stub.addEventListener,\n        \"unload\",\n        instance.handleEvent\n      );\n      assert.calledWithExactly(\n        stub.addEventListener,\n        \"TabPinned\",\n        instance.handleEvent\n      );\n    });\n    it(\"should create impression id if none exists\", () => {\n      assert.equal(instance._impressionId, FAKE_UUID);\n    });\n    it(\"should set impression id if it exists\", () => {\n      FakePrefs.prototype.prefs = {};\n      FakePrefs.prototype.prefs[PREF_IMPRESSION_ID] = \"fakeImpressionId\";\n      assert.equal(new TelemetryFeed()._impressionId, \"fakeImpressionId\");\n    });\n    it(\"should register listeners on existing windows\", () => {\n      const stub = sandbox.stub();\n      globals.set({\n        Services: {\n          ...Services,\n          wm: { getEnumerator: () => [{ addEventListener: stub }] },\n        },\n      });\n\n      instance.init();\n\n      assert.calledTwice(stub);\n      assert.calledWithExactly(stub, \"unload\", instance.handleEvent);\n      assert.calledWithExactly(stub, \"TabPinned\", instance.handleEvent);\n    });\n    describe(\"telemetry pref changes from false to true\", () => {\n      beforeEach(() => {\n        FakePrefs.prototype.prefs = {};\n        FakePrefs.prototype.prefs[TELEMETRY_PREF] = false;\n        instance = new TelemetryFeed();\n\n        assert.propertyVal(instance, \"telemetryEnabled\", false);\n      });\n\n      it(\"should set the enabled property to true\", () => {\n        instance._prefs.set(TELEMETRY_PREF, true);\n\n        assert.propertyVal(instance, \"telemetryEnabled\", true);\n      });\n    });\n    describe(\"events telemetry pref changes from false to true\", () => {\n      beforeEach(() => {\n        FakePrefs.prototype.prefs = {};\n        FakePrefs.prototype.prefs[EVENTS_TELEMETRY_PREF] = false;\n        instance = new TelemetryFeed();\n\n        assert.propertyVal(instance, \"eventTelemetryEnabled\", false);\n      });\n\n      it(\"should set the enabled property to true\", () => {\n        instance._prefs.set(EVENTS_TELEMETRY_PREF, true);\n\n        assert.propertyVal(instance, \"eventTelemetryEnabled\", true);\n      });\n    });\n    describe(\"Structured Ingestion telemetry pref changes from false to true\", () => {\n      beforeEach(() => {\n        FakePrefs.prototype.prefs = {};\n        FakePrefs.prototype.prefs[STRUCTURED_INGESTION_TELEMETRY_PREF] = false;\n        instance = new TelemetryFeed();\n\n        assert.propertyVal(\n          instance,\n          \"structuredIngestionTelemetryEnabled\",\n          false\n        );\n      });\n\n      it(\"should set the enabled property to true\", () => {\n        instance._prefs.set(STRUCTURED_INGESTION_TELEMETRY_PREF, true);\n\n        assert.propertyVal(\n          instance,\n          \"structuredIngestionTelemetryEnabled\",\n          true\n        );\n      });\n    });\n  });\n  describe(\"#handleEvent\", () => {\n    it(\"should dispatch a TAB_PINNED_EVENT\", () => {\n      sandbox.stub(instance, \"sendEvent\");\n      globals.set({\n        Services: {\n          ...Services,\n          wm: {\n            getEnumerator: () => [{ gBrowser: { tabs: [{ pinned: true }] } }],\n          },\n        },\n      });\n\n      instance.handleEvent({ type: \"TabPinned\", target: {} });\n\n      assert.calledOnce(instance.sendEvent);\n      const [ping] = instance.sendEvent.firstCall.args;\n      assert.propertyVal(ping, \"event\", \"TABPINNED\");\n      assert.propertyVal(ping, \"source\", \"TAB_CONTEXT_MENU\");\n      assert.propertyVal(ping, \"session_id\", \"n/a\");\n      assert.propertyVal(ping.value, \"total_pinned_tabs\", 1);\n    });\n    it(\"should skip private windows\", () => {\n      sandbox.stub(instance, \"sendEvent\");\n      globals.set({ PrivateBrowsingUtils: { isWindowPrivate: () => true } });\n\n      instance.handleEvent({ type: \"TabPinned\", target: {} });\n\n      assert.notCalled(instance.sendEvent);\n    });\n    it(\"should return the correct value for total_pinned_tabs\", () => {\n      sandbox.stub(instance, \"sendEvent\");\n      globals.set({\n        Services: {\n          ...Services,\n          wm: {\n            getEnumerator: () => [\n              {\n                gBrowser: { tabs: [{ pinned: true }, { pinned: false }] },\n              },\n            ],\n          },\n        },\n      });\n\n      instance.handleEvent({ type: \"TabPinned\", target: {} });\n\n      assert.calledOnce(instance.sendEvent);\n      const [ping] = instance.sendEvent.firstCall.args;\n      assert.propertyVal(ping, \"event\", \"TABPINNED\");\n      assert.propertyVal(ping, \"source\", \"TAB_CONTEXT_MENU\");\n      assert.propertyVal(ping, \"session_id\", \"n/a\");\n      assert.propertyVal(ping.value, \"total_pinned_tabs\", 1);\n    });\n    it(\"should return the correct value for total_pinned_tabs (when private windows are open)\", () => {\n      sandbox.stub(instance, \"sendEvent\");\n      const privateWinStub = sandbox\n        .stub()\n        .onCall(0)\n        .returns(false)\n        .onCall(1)\n        .returns(true);\n      globals.set({\n        PrivateBrowsingUtils: { isWindowPrivate: privateWinStub },\n      });\n      globals.set({\n        Services: {\n          ...Services,\n          wm: {\n            getEnumerator: () => [\n              {\n                gBrowser: { tabs: [{ pinned: true }, { pinned: true }] },\n              },\n            ],\n          },\n        },\n      });\n\n      instance.handleEvent({ type: \"TabPinned\", target: {} });\n\n      assert.calledOnce(instance.sendEvent);\n      const [ping] = instance.sendEvent.firstCall.args;\n      assert.propertyVal(ping.value, \"total_pinned_tabs\", 0);\n    });\n    it(\"should unregister the event listeners\", () => {\n      const stub = { removeEventListener: sandbox.stub() };\n\n      instance.handleEvent({ type: \"unload\", target: stub });\n\n      assert.calledTwice(stub.removeEventListener);\n      assert.calledWithExactly(\n        stub.removeEventListener,\n        \"unload\",\n        instance.handleEvent\n      );\n      assert.calledWithExactly(\n        stub.removeEventListener,\n        \"TabPinned\",\n        instance.handleEvent\n      );\n    });\n  });\n  describe(\"#addSession\", () => {\n    it(\"should add a session and return it\", () => {\n      const session = instance.addSession(\"foo\");\n\n      assert.equal(instance.sessions.get(\"foo\"), session);\n    });\n    it(\"should set the session_id\", () => {\n      sandbox.spy(global.gUUIDGenerator, \"generateUUID\");\n\n      const session = instance.addSession(\"foo\");\n\n      assert.calledOnce(global.gUUIDGenerator.generateUUID);\n      assert.equal(\n        session.session_id,\n        global.gUUIDGenerator.generateUUID.firstCall.returnValue\n      );\n    });\n    it(\"should set the page if a url parameter is given\", () => {\n      const session = instance.addSession(\"foo\", \"about:monkeys\");\n\n      assert.propertyVal(session, \"page\", \"about:monkeys\");\n    });\n    it(\"should set the page prop to 'unknown' if no URL parameter given\", () => {\n      const session = instance.addSession(\"foo\");\n\n      assert.propertyVal(session, \"page\", \"unknown\");\n    });\n    it(\"should set the perf type when lacking timestamp\", () => {\n      const session = instance.addSession(\"foo\");\n\n      assert.propertyVal(session.perf, \"load_trigger_type\", \"unexpected\");\n    });\n    it(\"should set load_trigger_type to first_window_opened on the first about:home seen\", () => {\n      const session = instance.addSession(\"foo\", \"about:home\");\n\n      assert.propertyVal(\n        session.perf,\n        \"load_trigger_type\",\n        \"first_window_opened\"\n      );\n    });\n    it(\"should not set load_trigger_type to first_window_opened on the second about:home seen\", () => {\n      instance.addSession(\"foo\", \"about:home\");\n\n      const session2 = instance.addSession(\"foo\", \"about:home\");\n\n      assert.notPropertyVal(\n        session2.perf,\n        \"load_trigger_type\",\n        \"first_window_opened\"\n      );\n    });\n    it(\"should set load_trigger_ts to the value of perfService.timeOrigin\", () => {\n      const session = instance.addSession(\"foo\", \"about:home\");\n\n      assert.propertyVal(session.perf, \"load_trigger_ts\", 123456);\n    });\n    it(\"should create a valid session ping on the first about:home seen\", () => {\n      // Add a session\n      const portID = \"foo\";\n      const session = instance.addSession(portID, \"about:home\");\n\n      // Create a ping referencing the session\n      const ping = instance.createSessionEndEvent(session);\n      assert.validate(ping, SessionPing);\n    });\n    it(\"should be a valid ping with the data_late_by_ms perf\", () => {\n      // Add a session\n      const portID = \"foo\";\n      const session = instance.addSession(portID, \"about:home\");\n      instance.saveSessionPerfData(\"foo\", { topsites_data_late_by_ms: 10 });\n      instance.saveSessionPerfData(\"foo\", { highlights_data_late_by_ms: 20 });\n\n      // Create a ping referencing the session\n      const ping = instance.createSessionEndEvent(session);\n      assert.validate(ping, SessionPing);\n      assert.propertyVal(\n        instance.sessions.get(\"foo\").perf,\n        \"highlights_data_late_by_ms\",\n        20\n      );\n      assert.propertyVal(\n        instance.sessions.get(\"foo\").perf,\n        \"topsites_data_late_by_ms\",\n        10\n      );\n    });\n    it(\"should be a valid ping with the topsites stats perf\", () => {\n      // Add a session\n      const portID = \"foo\";\n      const session = instance.addSession(portID, \"about:home\");\n      instance.saveSessionPerfData(\"foo\", {\n        topsites_icon_stats: {\n          custom_screenshot: 0,\n          screenshot_with_icon: 2,\n          screenshot: 1,\n          tippytop: 2,\n          rich_icon: 1,\n          no_image: 0,\n        },\n        topsites_pinned: 3,\n        topsites_search_shortcuts: 2,\n      });\n\n      // Create a ping referencing the session\n      const ping = instance.createSessionEndEvent(session);\n      assert.validate(ping, SessionPing);\n      assert.propertyVal(\n        instance.sessions.get(\"foo\").perf.topsites_icon_stats,\n        \"screenshot_with_icon\",\n        2\n      );\n      assert.equal(instance.sessions.get(\"foo\").perf.topsites_pinned, 3);\n      assert.equal(\n        instance.sessions.get(\"foo\").perf.topsites_search_shortcuts,\n        2\n      );\n    });\n  });\n\n  describe(\"#browserOpenNewtabStart\", () => {\n    it(\"should call perfService.mark with browser-open-newtab-start\", () => {\n      sandbox.stub(perfService, \"mark\");\n\n      instance.browserOpenNewtabStart();\n\n      assert.calledOnce(perfService.mark);\n      assert.calledWithExactly(perfService.mark, \"browser-open-newtab-start\");\n    });\n  });\n\n  describe(\"#endSession\", () => {\n    it(\"should not throw if there is no session for the given port ID\", () => {\n      assert.doesNotThrow(() => instance.endSession(\"doesn't exist\"));\n    });\n    it(\"should add a session_duration integer if there is a visibility_event_rcvd_ts\", () => {\n      sandbox.stub(instance, \"sendEvent\");\n      const session = instance.addSession(\"foo\");\n      session.perf.visibility_event_rcvd_ts = 444.4732;\n\n      instance.endSession(\"foo\");\n\n      assert.isNumber(session.session_duration);\n      assert.ok(\n        Number.isInteger(session.session_duration),\n        \"session_duration should be an integer\"\n      );\n    });\n    it(\"shouldn't send session ping if there's no visibility_event_rcvd_ts\", () => {\n      sandbox.stub(instance, \"sendEvent\");\n      instance.addSession(\"foo\");\n\n      instance.endSession(\"foo\");\n\n      assert.notCalled(instance.sendEvent);\n      assert.isFalse(instance.sessions.has(\"foo\"));\n    });\n    it(\"should remove the session from .sessions\", () => {\n      sandbox.stub(instance, \"sendEvent\");\n      instance.addSession(\"foo\");\n\n      instance.endSession(\"foo\");\n\n      assert.isFalse(instance.sessions.has(\"foo\"));\n    });\n    it(\"should call createSessionSendEvent and sendEvent with the sesssion\", () => {\n      FakePrefs.prototype.prefs[TELEMETRY_PREF] = true;\n      FakePrefs.prototype.prefs[EVENTS_TELEMETRY_PREF] = true;\n      instance = new TelemetryFeed();\n\n      sandbox.stub(instance, \"sendEvent\");\n      sandbox.stub(instance, \"createSessionEndEvent\");\n      sandbox.stub(instance.utEvents, \"sendSessionEndEvent\");\n      const session = instance.addSession(\"foo\");\n      session.perf.visibility_event_rcvd_ts = 444.4732;\n\n      instance.endSession(\"foo\");\n\n      // Did we call sendEvent with the result of createSessionEndEvent?\n      assert.calledWith(instance.createSessionEndEvent, session);\n\n      let sessionEndEvent =\n        instance.createSessionEndEvent.firstCall.returnValue;\n      assert.calledWith(instance.sendEvent, sessionEndEvent);\n      assert.calledWith(instance.utEvents.sendSessionEndEvent, sessionEndEvent);\n    });\n  });\n  describe(\"ping creators\", () => {\n    beforeEach(() => {\n      FakePrefs.prototype.prefs = {};\n      for (const pref of Object.keys(USER_PREFS_ENCODING)) {\n        FakePrefs.prototype.prefs[pref] = true;\n        expectedUserPrefs |= USER_PREFS_ENCODING[pref];\n      }\n      instance.init();\n    });\n    describe(\"#createPing\", () => {\n      it(\"should create a valid base ping without a session if no portID is supplied\", async () => {\n        const ping = await instance.createPing();\n        assert.validate(ping, BasePing);\n        assert.notProperty(ping, \"session_id\");\n        assert.notProperty(ping, \"page\");\n      });\n      it(\"should create a valid base ping with session info if a portID is supplied\", async () => {\n        // Add a session\n        const portID = \"foo\";\n        instance.addSession(portID, \"about:home\");\n        const sessionID = instance.sessions.get(portID).session_id;\n\n        // Create a ping referencing the session\n        const ping = await instance.createPing(portID);\n        assert.validate(ping, BasePing);\n\n        // Make sure we added the right session-related stuff to the ping\n        assert.propertyVal(ping, \"session_id\", sessionID);\n        assert.propertyVal(ping, \"page\", \"about:home\");\n      });\n      it(\"should create an unexpected base ping if no session yet portID is supplied\", async () => {\n        const ping = await instance.createPing(\"foo\");\n\n        assert.validate(ping, BasePing);\n        assert.propertyVal(ping, \"page\", \"unknown\");\n        assert.propertyVal(\n          instance.sessions.get(\"foo\").perf,\n          \"load_trigger_type\",\n          \"unexpected\"\n        );\n      });\n      it(\"should create a base ping with user_prefs\", async () => {\n        const ping = await instance.createPing(\"foo\");\n\n        assert.validate(ping, BasePing);\n        assert.propertyVal(ping, \"user_prefs\", expectedUserPrefs);\n      });\n    });\n    describe(\"#createUserEvent\", () => {\n      it(\"should create a valid event\", async () => {\n        const portID = \"foo\";\n        const data = { source: \"TOP_SITES\", event: \"CLICK\" };\n        const action = ac.AlsoToMain(ac.UserEvent(data), portID);\n        const session = instance.addSession(portID);\n\n        const ping = await instance.createUserEvent(action);\n\n        // Is it valid?\n        assert.validate(ping, UserEventPing);\n        // Does it have the right session_id?\n        assert.propertyVal(ping, \"session_id\", session.session_id);\n      });\n    });\n    describe(\"#createUndesiredEvent\", () => {\n      it(\"should create a valid event without a session\", async () => {\n        const action = ac.UndesiredEvent({\n          source: \"TOP_SITES\",\n          event: \"MISSING_IMAGE\",\n          value: 10,\n        });\n\n        const ping = await instance.createUndesiredEvent(action);\n\n        // Is it valid?\n        assert.validate(ping, UndesiredPing);\n        // Does it have the right value?\n        assert.propertyVal(ping, \"value\", 10);\n      });\n      it(\"should create a valid event with a session\", async () => {\n        const portID = \"foo\";\n        const data = { source: \"TOP_SITES\", event: \"MISSING_IMAGE\", value: 10 };\n        const action = ac.AlsoToMain(ac.UndesiredEvent(data), portID);\n        const session = instance.addSession(portID);\n\n        const ping = await instance.createUndesiredEvent(action);\n\n        // Is it valid?\n        assert.validate(ping, UndesiredPing);\n        // Does it have the right session_id?\n        assert.propertyVal(ping, \"session_id\", session.session_id);\n        // Does it have the right value?\n        assert.propertyVal(ping, \"value\", 10);\n      });\n      describe(\"#validate *_data_late_by_ms\", () => {\n        it(\"should create a valid highlights_data_late_by_ms ping\", () => {\n          const data = {\n            type: at.TELEMETRY_UNDESIRED_EVENT,\n            data: {\n              source: \"HIGHLIGHTS\",\n              event: `highlights_data_late_by_ms`,\n              value: 2,\n            },\n          };\n          const ping = instance.createUndesiredEvent(data);\n\n          assert.validate(ping, UndesiredPing);\n          assert.propertyVal(ping, \"value\", data.data.value);\n          assert.propertyVal(ping, \"event\", data.data.event);\n        });\n      });\n    });\n    describe(\"#createPerformanceEvent\", () => {\n      it(\"should create a valid event without a session\", async () => {\n        const action = ac.PerfEvent({\n          event: \"SCREENSHOT_FINISHED\",\n          value: 100,\n        });\n        const ping = await instance.createPerformanceEvent(action);\n\n        // Is it valid?\n        assert.validate(ping, PerfPing);\n        // Does it have the right value?\n        assert.propertyVal(ping, \"value\", 100);\n      });\n    });\n    describe(\"#createSessionEndEvent\", () => {\n      it(\"should create a valid event\", async () => {\n        const ping = await instance.createSessionEndEvent({\n          session_id: FAKE_UUID,\n          page: \"about:newtab\",\n          session_duration: 12345,\n          perf: {\n            load_trigger_ts: 10,\n            load_trigger_type: \"menu_plus_or_keyboard\",\n            visibility_event_rcvd_ts: 20,\n            is_preloaded: true,\n          },\n        });\n\n        // Is it valid?\n        assert.validate(ping, SessionPing);\n        assert.propertyVal(ping, \"session_id\", FAKE_UUID);\n        assert.propertyVal(ping, \"page\", \"about:newtab\");\n        assert.propertyVal(ping, \"session_duration\", 12345);\n      });\n      it(\"should create a valid unexpected session event\", async () => {\n        const ping = await instance.createSessionEndEvent({\n          session_id: FAKE_UUID,\n          page: \"about:newtab\",\n          session_duration: 12345,\n          perf: {\n            load_trigger_type: \"unexpected\",\n            is_preloaded: true,\n          },\n        });\n\n        // Is it valid?\n        assert.validate(ping, SessionPing);\n        assert.propertyVal(ping, \"session_id\", FAKE_UUID);\n        assert.propertyVal(ping, \"page\", \"about:newtab\");\n        assert.propertyVal(ping, \"session_duration\", 12345);\n        assert.propertyVal(ping.perf, \"load_trigger_type\", \"unexpected\");\n      });\n    });\n  });\n  describe(\"#createImpressionStats\", () => {\n    it(\"should create a valid impression stats ping\", async () => {\n      const tiles = [{ id: 10001 }, { id: 10002 }, { id: 10003 }];\n      const action = ac.ImpressionStats({ source: \"POCKET\", tiles });\n      const ping = await instance.createImpressionStats(\n        au.getPortIdOfSender(action),\n        action.data\n      );\n\n      assert.validate(ping, ImpressionStatsPing);\n      assert.propertyVal(ping, \"source\", \"POCKET\");\n      assert.propertyVal(ping, \"tiles\", tiles);\n    });\n    it(\"should create a valid click ping\", async () => {\n      const tiles = [{ id: 10001, pos: 2 }];\n      const action = ac.ImpressionStats({ source: \"POCKET\", tiles, click: 0 });\n      const ping = await instance.createImpressionStats(\n        au.getPortIdOfSender(action),\n        action.data\n      );\n\n      assert.validate(ping, ImpressionStatsPing);\n      assert.propertyVal(ping, \"click\", 0);\n      assert.propertyVal(ping, \"tiles\", tiles);\n    });\n    it(\"should create a valid block ping\", async () => {\n      const tiles = [{ id: 10001, pos: 2 }];\n      const action = ac.ImpressionStats({ source: \"POCKET\", tiles, block: 0 });\n      const ping = await instance.createImpressionStats(\n        au.getPortIdOfSender(action),\n        action.data\n      );\n\n      assert.validate(ping, ImpressionStatsPing);\n      assert.propertyVal(ping, \"block\", 0);\n      assert.propertyVal(ping, \"tiles\", tiles);\n    });\n    it(\"should create a valid pocket ping\", async () => {\n      const tiles = [{ id: 10001, pos: 2 }];\n      const action = ac.ImpressionStats({ source: \"POCKET\", tiles, pocket: 0 });\n      const ping = await instance.createImpressionStats(\n        au.getPortIdOfSender(action),\n        action.data\n      );\n\n      assert.validate(ping, ImpressionStatsPing);\n      assert.propertyVal(ping, \"pocket\", 0);\n      assert.propertyVal(ping, \"tiles\", tiles);\n    });\n    it(\"should pass shim if it is available to impression ping\", async () => {\n      const tiles = [{ id: 10001, pos: 2, shim: 1234 }];\n      const action = ac.ImpressionStats({ source: \"POCKET\", tiles });\n      const ping = await instance.createImpressionStats(\n        au.getPortIdOfSender(action),\n        action.data\n      );\n\n      assert.propertyVal(ping, \"tiles\", tiles);\n      assert.propertyVal(ping.tiles[0], \"shim\", tiles[0].shim);\n    });\n  });\n  describe(\"#createSpocsFillPing\", () => {\n    it(\"should create a valid SPOCS Fill ping\", async () => {\n      const spocFills = [\n        { id: 10001, displayed: 0, reason: \"frequency_cap\", full_recalc: 1 },\n        { id: 10002, displayed: 0, reason: \"blocked_by_user\", full_recalc: 1 },\n        { id: 10003, displayed: 1, reason: \"n/a\", full_recalc: 1 },\n      ];\n      const action = ac.DiscoveryStreamSpocsFill({ spoc_fills: spocFills });\n      const ping = await instance.createSpocsFillPing(action.data);\n\n      assert.validate(ping, SpocsFillPing);\n      assert.propertyVal(ping, \"spoc_fills\", spocFills);\n    });\n  });\n  describe(\"#applyCFRPolicy\", () => {\n    it(\"should use client_id and message_id in prerelease\", async () => {\n      globals.set(\"UpdateUtils\", {\n        getUpdateChannel() {\n          return \"nightly\";\n        },\n      });\n      const data = {\n        action: \"cfr_user_event\",\n        event: \"IMPRESSION\",\n        message_id: \"cfr_message_01\",\n        bucket_id: \"cfr_bucket_01\",\n      };\n      const { ping, pingType } = await instance.applyCFRPolicy(data);\n\n      assert.equal(pingType, \"cfr\");\n      assert.isUndefined(ping.impression_id);\n      assert.propertyVal(ping, \"client_id\", FAKE_TELEMETRY_ID);\n      assert.propertyVal(ping, \"bucket_id\", \"cfr_bucket_01\");\n      assert.propertyVal(ping, \"message_id\", \"cfr_message_01\");\n    });\n    it(\"should use impression_id and bucket_id in release\", async () => {\n      globals.set(\"UpdateUtils\", {\n        getUpdateChannel() {\n          return \"release\";\n        },\n      });\n      const data = {\n        action: \"cfr_user_event\",\n        event: \"IMPRESSION\",\n        message_id: \"cfr_message_01\",\n        bucket_id: \"cfr_bucket_01\",\n      };\n      const { ping, pingType } = await instance.applyCFRPolicy(data);\n\n      assert.equal(pingType, \"cfr\");\n      assert.isUndefined(ping.client_id);\n      assert.propertyVal(ping, \"impression_id\", FAKE_UUID);\n      assert.propertyVal(ping, \"message_id\", \"n/a\");\n      assert.propertyVal(ping, \"bucket_id\", \"cfr_bucket_01\");\n    });\n    it(\"should use client_id and message_id in the experiment cohort in release\", async () => {\n      globals.set(\"UpdateUtils\", {\n        getUpdateChannel() {\n          return \"release\";\n        },\n      });\n      sandbox\n        .stub(ASRouterPreferences, \"providers\")\n        .get(() => FAKE_ROUTER_MESSAGE_PROVIDER_COHORT);\n      const data = {\n        action: \"cfr_user_event\",\n        event: \"IMPRESSION\",\n        message_id: \"cfr_message_01\",\n        bucket_id: \"cfr_bucket_01\",\n      };\n      const { ping, pingType } = await instance.applyCFRPolicy(data);\n\n      assert.equal(pingType, \"cfr\");\n      assert.isUndefined(ping.impression_id);\n      assert.propertyVal(ping, \"client_id\", FAKE_TELEMETRY_ID);\n      assert.propertyVal(ping, \"bucket_id\", \"cfr_bucket_01\");\n      assert.propertyVal(ping, \"message_id\", \"cfr_message_01\");\n    });\n  });\n  describe(\"#applySnippetsPolicy\", () => {\n    it(\"should include client_id\", async () => {\n      const data = {\n        action: \"snippets_user_event\",\n        event: \"IMPRESSION\",\n        message_id: \"snippets_message_01\",\n      };\n      const { ping, pingType } = await instance.applySnippetsPolicy(data);\n\n      assert.equal(pingType, \"snippets\");\n      assert.propertyVal(ping, \"client_id\", FAKE_TELEMETRY_ID);\n      assert.propertyVal(ping, \"message_id\", \"snippets_message_01\");\n    });\n  });\n  describe(\"#applyOnboardingPolicy\", () => {\n    it(\"should include client_id\", async () => {\n      const data = {\n        action: \"onboarding_user_event\",\n        event: \"CLICK_BUTTION\",\n        message_id: \"onboarding_message_01\",\n      };\n      const { ping, pingType } = await instance.applyOnboardingPolicy(data);\n\n      assert.equal(pingType, \"onboarding\");\n      assert.propertyVal(ping, \"client_id\", FAKE_TELEMETRY_ID);\n      assert.propertyVal(ping, \"message_id\", \"onboarding_message_01\");\n    });\n  });\n  describe(\"#applyUndesiredEventPolicy\", () => {\n    it(\"should exclude client_id and use impression_id\", () => {\n      const data = {\n        action: \"asrouter_undesired_event\",\n        event: \"RS_MISSING_DATA\",\n      };\n      const { ping, pingType } = instance.applyUndesiredEventPolicy(data);\n\n      assert.equal(pingType, \"undesired-events\");\n      assert.isUndefined(ping.client_id);\n      assert.propertyVal(ping, \"impression_id\", FAKE_UUID);\n    });\n  });\n  describe(\"#createASRouterEvent\", () => {\n    it(\"should create a valid AS Router event\", async () => {\n      const data = {\n        action: \"snippets_user_event\",\n        event: \"CLICK\",\n        message_id: \"snippets_message_01\",\n      };\n      const action = ac.ASRouterUserEvent(data);\n      const { ping } = await instance.createASRouterEvent(action);\n\n      assert.validate(ping, ASRouterEventPing);\n      assert.propertyVal(ping, \"event\", \"CLICK\");\n    });\n    it(\"should call applyCFRPolicy if action equals to cfr_user_event\", async () => {\n      const data = {\n        action: \"cfr_user_event\",\n        event: \"IMPRESSION\",\n        message_id: \"cfr_message_01\",\n      };\n      sandbox.stub(instance, \"applyCFRPolicy\");\n      const action = ac.ASRouterUserEvent(data);\n      await instance.createASRouterEvent(action);\n\n      assert.calledOnce(instance.applyCFRPolicy);\n    });\n    it(\"should call applySnippetsPolicy if action equals to snippets_user_event\", async () => {\n      const data = {\n        action: \"snippets_user_event\",\n        event: \"IMPRESSION\",\n        message_id: \"snippets_message_01\",\n      };\n      sandbox.stub(instance, \"applySnippetsPolicy\");\n      const action = ac.ASRouterUserEvent(data);\n      await instance.createASRouterEvent(action);\n\n      assert.calledOnce(instance.applySnippetsPolicy);\n    });\n    it(\"should call applyOnboardingPolicy if action equals to onboarding_user_event\", async () => {\n      const data = {\n        action: \"onboarding_user_event\",\n        event: \"CLICK_BUTTON\",\n        message_id: \"onboarding_message_01\",\n      };\n      sandbox.stub(instance, \"applyOnboardingPolicy\");\n      const action = ac.ASRouterUserEvent(data);\n      await instance.createASRouterEvent(action);\n\n      assert.calledOnce(instance.applyOnboardingPolicy);\n    });\n    it(\"should call applyOnboardingPolicy if action equals to whats-new-panel_user_event\", async () => {\n      const data = {\n        action: \"whats-new-panel_user_event\",\n        event: \"CLICK_BUTTON\",\n        message_id: \"whats-new-panel_message_01\",\n      };\n      sandbox.stub(instance, \"applyOnboardingPolicy\");\n      const action = ac.ASRouterUserEvent(data);\n      await instance.createASRouterEvent(action);\n\n      assert.calledOnce(instance.applyOnboardingPolicy);\n    });\n    it(\"should call applyUndesiredEventPolicy if action equals to asrouter_undesired_event\", async () => {\n      const data = {\n        action: \"asrouter_undesired_event\",\n        event: \"UNDESIRED_EVENT\",\n      };\n      sandbox.stub(instance, \"applyUndesiredEventPolicy\");\n      const action = ac.ASRouterUserEvent(data);\n      await instance.createASRouterEvent(action);\n\n      assert.calledOnce(instance.applyUndesiredEventPolicy);\n    });\n    it(\"should stringify event_context if it is an Object\", async () => {\n      const data = {\n        action: \"asrouter_undesired_event\",\n        event: \"UNDESIRED_EVENT\",\n        event_context: { foo: \"bar\" },\n      };\n      const action = ac.ASRouterUserEvent(data);\n      const { ping } = await instance.createASRouterEvent(action);\n\n      assert.propertyVal(ping, \"event_context\", JSON.stringify({ foo: \"bar\" }));\n    });\n    it(\"should not stringify event_context if it is a String\", async () => {\n      const data = {\n        action: \"asrouter_undesired_event\",\n        event: \"UNDESIRED_EVENT\",\n        event_context: \"foo\",\n      };\n      const action = ac.ASRouterUserEvent(data);\n      const { ping } = await instance.createASRouterEvent(action);\n\n      assert.propertyVal(ping, \"event_context\", \"foo\");\n    });\n  });\n  describe(\"#sendEventPing\", () => {\n    it(\"should call sendStructuredIngestionEvent\", async () => {\n      const data = {\n        action: \"activity_stream_user_event\",\n        event: \"CLICK\",\n      };\n      instance = new TelemetryFeed();\n      sandbox.spy(instance, \"sendStructuredIngestionEvent\");\n\n      await instance.sendEventPing(data);\n\n      const expectedPayload = {\n        client_id: FAKE_TELEMETRY_ID,\n        event: \"CLICK\",\n      };\n      assert.calledWith(instance.sendStructuredIngestionEvent, expectedPayload);\n    });\n    it(\"should stringify value if it is an Object\", async () => {\n      const data = {\n        action: \"activity_stream_user_event\",\n        event: \"CLICK\",\n        value: { foo: \"bar\" },\n      };\n      instance = new TelemetryFeed();\n      sandbox.spy(instance, \"sendStructuredIngestionEvent\");\n\n      await instance.sendEventPing(data);\n\n      const expectedPayload = {\n        client_id: FAKE_TELEMETRY_ID,\n        event: \"CLICK\",\n        value: JSON.stringify({ foo: \"bar\" }),\n      };\n      assert.calledWith(instance.sendStructuredIngestionEvent, expectedPayload);\n    });\n  });\n  describe(\"#sendEvent\", () => {\n    it(\"should call sendEventPing on activity_stream_user_event\", () => {\n      FakePrefs.prototype.prefs.telemetry = true;\n      FakePrefs.prototype.prefs[STRUCTURED_INGESTION_TELEMETRY_PREF] = true;\n      const event = { action: \"activity_stream_user_event\" };\n      instance = new TelemetryFeed();\n      sandbox.spy(instance, \"sendEventPing\");\n\n      instance.sendEvent(event);\n\n      assert.calledOnce(instance.sendEventPing);\n    });\n  });\n  describe(\"#sendUTEvent\", () => {\n    it(\"should call the UT event function passed in\", async () => {\n      FakePrefs.prototype.prefs[TELEMETRY_PREF] = true;\n      FakePrefs.prototype.prefs[EVENTS_TELEMETRY_PREF] = true;\n      const event = {};\n      instance = new TelemetryFeed();\n      sandbox.stub(instance.utEvents, \"sendUserEvent\");\n\n      await instance.sendUTEvent(event, instance.utEvents.sendUserEvent);\n\n      assert.calledWith(instance.utEvents.sendUserEvent, event);\n    });\n  });\n  describe(\"#sendStructuredIngestionEvent\", () => {\n    it(\"should call PingCentre sendStructuredIngestionPing\", async () => {\n      FakePrefs.prototype.prefs[TELEMETRY_PREF] = true;\n      FakePrefs.prototype.prefs[STRUCTURED_INGESTION_TELEMETRY_PREF] = true;\n      const event = {};\n      instance = new TelemetryFeed();\n      sandbox.stub(instance.pingCentre, \"sendStructuredIngestionPing\");\n\n      await instance.sendStructuredIngestionEvent(\n        event,\n        \"http://foo.com/base/\"\n      );\n\n      assert.calledWith(instance.pingCentre.sendStructuredIngestionPing, event);\n    });\n  });\n  describe(\"#setLoadTriggerInfo\", () => {\n    it(\"should call saveSessionPerfData w/load_trigger_{ts,type} data\", () => {\n      const stub = sandbox.stub(instance, \"saveSessionPerfData\");\n      sandbox.stub(perfService, \"getMostRecentAbsMarkStartByName\").returns(777);\n      instance.addSession(\"port123\");\n\n      instance.setLoadTriggerInfo(\"port123\");\n\n      assert.calledWith(stub, \"port123\", {\n        load_trigger_ts: 777,\n        load_trigger_type: \"menu_plus_or_keyboard\",\n      });\n    });\n\n    it(\"should not call saveSessionPerfData when getting mark throws\", () => {\n      const stub = sandbox.stub(instance, \"saveSessionPerfData\");\n      sandbox.stub(perfService, \"getMostRecentAbsMarkStartByName\").throws();\n      instance.addSession(\"port123\");\n\n      instance.setLoadTriggerInfo(\"port123\");\n\n      assert.notCalled(stub);\n    });\n  });\n\n  describe(\"#saveSessionPerfData\", () => {\n    it(\"should update the given session with the given data\", () => {\n      instance.addSession(\"port123\");\n      assert.notProperty(instance.sessions.get(\"port123\"), \"fake_ts\");\n      const data = { fake_ts: 456, other_fake_ts: 789 };\n\n      instance.saveSessionPerfData(\"port123\", data);\n\n      assert.include(instance.sessions.get(\"port123\").perf, data);\n    });\n\n    it(\"should call setLoadTriggerInfo if data has visibility_event_rcvd_ts\", () => {\n      sandbox.stub(instance, \"setLoadTriggerInfo\");\n      instance.addSession(\"port123\");\n      const data = { visibility_event_rcvd_ts: 444455 };\n\n      instance.saveSessionPerfData(\"port123\", data);\n\n      assert.calledOnce(instance.setLoadTriggerInfo);\n      assert.calledWithExactly(instance.setLoadTriggerInfo, \"port123\");\n      assert.include(instance.sessions.get(\"port123\").perf, data);\n    });\n\n    it(\"shouldn't call setLoadTriggerInfo if data has no visibility_event_rcvd_ts\", () => {\n      sandbox.stub(instance, \"setLoadTriggerInfo\");\n      instance.addSession(\"port123\");\n\n      instance.saveSessionPerfData(\"port123\", { monkeys_ts: 444455 });\n\n      assert.notCalled(instance.setLoadTriggerInfo);\n    });\n\n    it(\"should not call setLoadTriggerInfo when url is about:home\", () => {\n      sandbox.stub(instance, \"setLoadTriggerInfo\");\n      instance.addSession(\"port123\", \"about:home\");\n      const data = { visibility_event_rcvd_ts: 444455 };\n\n      instance.saveSessionPerfData(\"port123\", data);\n\n      assert.notCalled(instance.setLoadTriggerInfo);\n    });\n\n    it(\"should call maybeRecordTopsitesPainted when url is about:home and topsites_first_painted_ts is given\", () => {\n      const topsites_first_painted_ts = 44455;\n      const data = { topsites_first_painted_ts };\n      const spy = sandbox.spy();\n\n      sandbox.stub(Services.prefs, \"getIntPref\").returns(1);\n      globals.set(\"aboutNewTabService\", {\n        overridden: false,\n        newTabURL: \"\",\n        maybeRecordTopsitesPainted: spy,\n      });\n      instance.addSession(\"port123\", \"about:home\");\n      instance.saveSessionPerfData(\"port123\", data);\n\n      assert.calledOnce(spy);\n      assert.calledWith(spy, topsites_first_painted_ts);\n    });\n  });\n  describe(\"#uninit\", () => {\n    it(\"should call .pingCentre.uninit\", () => {\n      const stub = sandbox.stub(instance.pingCentre, \"uninit\");\n\n      instance.uninit();\n\n      assert.calledOnce(stub);\n    });\n    it(\"should call .utEvents.uninit\", () => {\n      const stub = sandbox.stub(instance.utEvents, \"uninit\");\n\n      instance.uninit();\n\n      assert.calledOnce(stub);\n    });\n    it(\"should make this.browserOpenNewtabStart() stop observing browser-open-newtab-start and domwindowopened\", async () => {\n      await instance.init();\n      sandbox.spy(Services.obs, \"removeObserver\");\n      sandbox.stub(instance.pingCentre, \"uninit\");\n\n      await instance.uninit();\n\n      assert.calledTwice(Services.obs.removeObserver);\n      assert.calledWithExactly(\n        Services.obs.removeObserver,\n        instance.browserOpenNewtabStart,\n        \"browser-open-newtab-start\"\n      );\n      assert.calledWithExactly(\n        Services.obs.removeObserver,\n        instance._addWindowListeners,\n        \"domwindowopened\"\n      );\n    });\n  });\n  describe(\"#onAction\", () => {\n    beforeEach(() => {\n      FakePrefs.prototype.prefs = {};\n    });\n    it(\"should call .init() on an INIT action\", () => {\n      const init = sandbox.stub(instance, \"init\");\n      const sendPageTakeoverData = sandbox.stub(\n        instance,\n        \"sendPageTakeoverData\"\n      );\n\n      instance.onAction({ type: at.INIT });\n\n      assert.calledOnce(init);\n      assert.calledOnce(sendPageTakeoverData);\n    });\n    it(\"should call .uninit() on an UNINIT action\", () => {\n      const stub = sandbox.stub(instance, \"uninit\");\n\n      instance.onAction({ type: at.UNINIT });\n\n      assert.calledOnce(stub);\n    });\n    it(\"should call .handleNewTabInit on a NEW_TAB_INIT action\", () => {\n      sandbox.spy(instance, \"handleNewTabInit\");\n\n      instance.onAction(\n        ac.AlsoToMain({\n          type: at.NEW_TAB_INIT,\n          data: { url: \"about:newtab\", browser },\n        })\n      );\n\n      assert.calledOnce(instance.handleNewTabInit);\n    });\n    it(\"should call .addSession() on a NEW_TAB_INIT action\", () => {\n      const stub = sandbox.stub(instance, \"addSession\").returns({ perf: {} });\n      sandbox.stub(instance, \"setLoadTriggerInfo\");\n\n      instance.onAction(\n        ac.AlsoToMain(\n          {\n            type: at.NEW_TAB_INIT,\n            data: { url: \"about:monkeys\", browser },\n          },\n          \"port123\"\n        )\n      );\n\n      assert.calledOnce(stub);\n      assert.calledWith(stub, \"port123\", \"about:monkeys\");\n    });\n    it(\"should call .endSession() on a NEW_TAB_UNLOAD action\", () => {\n      const stub = sandbox.stub(instance, \"endSession\");\n\n      instance.onAction(ac.AlsoToMain({ type: at.NEW_TAB_UNLOAD }, \"port123\"));\n\n      assert.calledWith(stub, \"port123\");\n    });\n    it(\"should call .saveSessionPerfData on SAVE_SESSION_PERF_DATA\", () => {\n      const stub = sandbox.stub(instance, \"saveSessionPerfData\");\n      const data = { some_ts: 10 };\n      const action = { type: at.SAVE_SESSION_PERF_DATA, data };\n\n      instance.onAction(ac.AlsoToMain(action, \"port123\"));\n\n      assert.calledWith(stub, \"port123\", data);\n    });\n    it(\"should send an event on a TELEMETRY_UNDESIRED_EVENT action\", () => {\n      const sendEvent = sandbox.stub(instance, \"sendEvent\");\n      const eventCreator = sandbox.stub(instance, \"createUndesiredEvent\");\n      const action = { type: at.TELEMETRY_UNDESIRED_EVENT };\n\n      instance.onAction(action);\n\n      assert.calledWith(eventCreator, action);\n      assert.calledWith(sendEvent, eventCreator.returnValue);\n    });\n    it(\"should send an event on a TELEMETRY_USER_EVENT action\", () => {\n      FakePrefs.prototype.prefs[TELEMETRY_PREF] = true;\n      FakePrefs.prototype.prefs[EVENTS_TELEMETRY_PREF] = true;\n      instance = new TelemetryFeed();\n\n      const sendEvent = sandbox.stub(instance, \"sendEvent\");\n      const utSendUserEvent = sandbox.stub(instance.utEvents, \"sendUserEvent\");\n      const eventCreator = sandbox.stub(instance, \"createUserEvent\");\n      const action = { type: at.TELEMETRY_USER_EVENT };\n\n      instance.onAction(action);\n\n      assert.calledWith(eventCreator, action);\n      assert.calledWith(sendEvent, eventCreator.returnValue);\n      assert.calledWith(utSendUserEvent, eventCreator.returnValue);\n    });\n    it(\"should call handleASRouterUserEvent on TELEMETRY_USER_EVENT action\", () => {\n      FakePrefs.prototype.prefs[TELEMETRY_PREF] = true;\n      FakePrefs.prototype.prefs[EVENTS_TELEMETRY_PREF] = true;\n      instance = new TelemetryFeed();\n\n      const eventHandler = sandbox.spy(instance, \"handleASRouterUserEvent\");\n      const action = {\n        type: at.AS_ROUTER_TELEMETRY_USER_EVENT,\n        data: { event: \"CLICK\" },\n      };\n\n      instance.onAction(action);\n\n      assert.calledWith(eventHandler, action);\n    });\n    it(\"should send an event on a TELEMETRY_PERFORMANCE_EVENT action\", () => {\n      const sendEvent = sandbox.stub(instance, \"sendEvent\");\n      const eventCreator = sandbox.stub(instance, \"createPerformanceEvent\");\n      const action = { type: at.TELEMETRY_PERFORMANCE_EVENT };\n\n      instance.onAction(action);\n\n      assert.calledWith(eventCreator, action);\n      assert.calledWith(sendEvent, eventCreator.returnValue);\n    });\n    it(\"should send an event on a TELEMETRY_IMPRESSION_STATS action\", () => {\n      const sendEvent = sandbox.stub(instance, \"sendStructuredIngestionEvent\");\n      const eventCreator = sandbox.stub(instance, \"createImpressionStats\");\n      const tiles = [{ id: 10001 }, { id: 10002 }, { id: 10003 }];\n      const action = ac.ImpressionStats({ source: \"POCKET\", tiles });\n\n      instance.onAction(action);\n\n      assert.calledWith(\n        eventCreator,\n        au.getPortIdOfSender(action),\n        action.data\n      );\n      assert.calledWith(sendEvent, eventCreator.returnValue);\n    });\n    it(\"should call .handleDiscoveryStreamImpressionStats on a DISCOVERY_STREAM_IMPRESSION_STATS action\", () => {\n      const session = {};\n      sandbox.stub(instance.sessions, \"get\").returns(session);\n      const data = { source: \"foo\", tiles: [{ id: 1 }] };\n      const action = { type: at.DISCOVERY_STREAM_IMPRESSION_STATS, data };\n      sandbox.spy(instance, \"handleDiscoveryStreamImpressionStats\");\n\n      instance.onAction(ac.AlsoToMain(action, \"port123\"));\n\n      assert.calledWith(\n        instance.handleDiscoveryStreamImpressionStats,\n        \"port123\",\n        data\n      );\n    });\n    it(\"should call .handleDiscoveryStreamLoadedContent on a DISCOVERY_STREAM_LOADED_CONTENT action\", () => {\n      const session = {};\n      sandbox.stub(instance.sessions, \"get\").returns(session);\n      const data = { source: \"foo\", tiles: [{ id: 1 }] };\n      const action = { type: at.DISCOVERY_STREAM_LOADED_CONTENT, data };\n      sandbox.spy(instance, \"handleDiscoveryStreamLoadedContent\");\n\n      instance.onAction(ac.AlsoToMain(action, \"port123\"));\n\n      assert.calledWith(\n        instance.handleDiscoveryStreamLoadedContent,\n        \"port123\",\n        data\n      );\n    });\n    it(\"should send an event on a DISCOVERY_STREAM_SPOCS_FILL action\", () => {\n      const sendEvent = sandbox.stub(instance, \"sendStructuredIngestionEvent\");\n      const eventCreator = sandbox.stub(instance, \"createSpocsFillPing\");\n      const spocFills = [\n        { id: 10001, displayed: 0, reason: \"frequency_cap\", full_recalc: 1 },\n        { id: 10002, displayed: 0, reason: \"blocked_by_user\", full_recalc: 1 },\n        { id: 10003, displayed: 1, reason: \"n/a\", full_recalc: 1 },\n      ];\n      const action = ac.DiscoveryStreamSpocsFill({ spoc_fills: spocFills });\n\n      instance.onAction(action);\n\n      assert.calledWith(eventCreator, action.data);\n      assert.calledWith(sendEvent, eventCreator.returnValue);\n    });\n    it(\"should call .handleTrailheadEnrollEvent on a TRAILHEAD_ENROLL_EVENT action\", () => {\n      const data = { experiment: \"foo\", type: \"bar\", branch: \"baz\" };\n      const action = { type: at.TRAILHEAD_ENROLL_EVENT, data };\n      sandbox.spy(instance, \"handleTrailheadEnrollEvent\");\n\n      instance.onAction(action);\n\n      assert.calledWith(instance.handleTrailheadEnrollEvent, action);\n    });\n  });\n  describe(\"#handleNewTabInit\", () => {\n    it(\"should set the session as preloaded if the browser is preloaded\", () => {\n      const session = { perf: {} };\n      let preloadedBrowser = {\n        getAttribute() {\n          return \"preloaded\";\n        },\n      };\n      sandbox.stub(instance, \"addSession\").returns(session);\n\n      instance.onAction(\n        ac.AlsoToMain({\n          type: at.NEW_TAB_INIT,\n          data: { url: \"about:newtab\", browser: preloadedBrowser },\n        })\n      );\n\n      assert.ok(session.perf.is_preloaded);\n    });\n    it(\"should set the session as non-preloaded if the browser is non-preloaded\", () => {\n      const session = { perf: {} };\n      let nonPreloadedBrowser = {\n        getAttribute() {\n          return \"\";\n        },\n      };\n      sandbox.stub(instance, \"addSession\").returns(session);\n\n      instance.onAction(\n        ac.AlsoToMain({\n          type: at.NEW_TAB_INIT,\n          data: { url: \"about:newtab\", browser: nonPreloadedBrowser },\n        })\n      );\n\n      assert.ok(!session.perf.is_preloaded);\n    });\n  });\n  describe(\"#sendPageTakeoverData\", () => {\n    let fakePrefs = { \"browser.newtabpage.enabled\": true };\n\n    beforeEach(() => {\n      globals.set(\n        \"Services\",\n        Object.assign({}, Services, {\n          prefs: { getBoolPref: key => fakePrefs[key] },\n        })\n      );\n      // Services.prefs = {getBoolPref: key => fakePrefs[key]};\n    });\n    it(\"should send correct event data for about:home set to custom URL\", async () => {\n      fakeHomePageUrl = \"https://searchprovider.com\";\n      instance._prefs.set(TELEMETRY_PREF, true);\n      instance._classifySite = () => Promise.resolve(\"other\");\n      const sendEvent = sandbox.stub(instance, \"sendEvent\");\n\n      await instance.sendPageTakeoverData();\n      assert.calledOnce(sendEvent);\n      assert.equal(sendEvent.firstCall.args[0].event, \"PAGE_TAKEOVER_DATA\");\n      assert.deepEqual(sendEvent.firstCall.args[0].value, {\n        home_url_category: \"other\",\n      });\n      assert.validate(sendEvent.firstCall.args[0], UserEventPing);\n    });\n    it(\"should send correct event data for about:newtab set to custom URL\", async () => {\n      globals.set(\"aboutNewTabService\", {\n        overridden: true,\n        newTabURL: \"https://searchprovider.com\",\n      });\n      instance._prefs.set(TELEMETRY_PREF, true);\n      instance._classifySite = () => Promise.resolve(\"other\");\n      const sendEvent = sandbox.stub(instance, \"sendEvent\");\n\n      await instance.sendPageTakeoverData();\n      assert.calledOnce(sendEvent);\n      assert.equal(sendEvent.firstCall.args[0].event, \"PAGE_TAKEOVER_DATA\");\n      assert.deepEqual(sendEvent.firstCall.args[0].value, {\n        newtab_url_category: \"other\",\n      });\n      assert.validate(sendEvent.firstCall.args[0], UserEventPing);\n    });\n    it(\"should not send an event if neither about:{home,newtab} are set to custom URL\", async () => {\n      instance._prefs.set(TELEMETRY_PREF, true);\n      const sendEvent = sandbox.stub(instance, \"sendEvent\");\n\n      await instance.sendPageTakeoverData();\n      assert.notCalled(sendEvent);\n    });\n    it(\"should send home_extension_id and newtab_extension_id when appropriate\", async () => {\n      const ID = \"{abc-foo-bar}\";\n      fakeExtensionSettingsStore.getSetting = () => ({ id: ID });\n      instance._prefs.set(TELEMETRY_PREF, true);\n      instance._classifySite = () => Promise.resolve(\"other\");\n      const sendEvent = sandbox.stub(instance, \"sendEvent\");\n\n      await instance.sendPageTakeoverData();\n      assert.calledOnce(sendEvent);\n      assert.equal(sendEvent.firstCall.args[0].event, \"PAGE_TAKEOVER_DATA\");\n      assert.deepEqual(sendEvent.firstCall.args[0].value, {\n        home_extension_id: ID,\n        newtab_extension_id: ID,\n      });\n      assert.validate(sendEvent.firstCall.args[0], UserEventPing);\n    });\n  });\n  describe(\"#sendDiscoveryStreamImpressions\", () => {\n    it(\"should not send impression pings if there is no impression data\", () => {\n      const spy = sandbox.spy(instance, \"sendEvent\");\n      const session = {};\n      instance.sendDiscoveryStreamImpressions(\"foo\", session);\n\n      assert.notCalled(spy);\n    });\n    it(\"should send impression pings if there is impression data\", () => {\n      const spy = sandbox.spy(instance, \"sendStructuredIngestionEvent\");\n      const session = {\n        impressionSets: {\n          source_foo: [{ id: 1, pos: 0 }, { id: 2, pos: 1 }],\n          source_bar: [{ id: 3, pos: 0 }, { id: 4, pos: 1 }],\n        },\n      };\n      instance.sendDiscoveryStreamImpressions(\"foo\", session);\n\n      assert.calledTwice(spy);\n    });\n  });\n  describe(\"#sendDiscoveryStreamLoadedContent\", () => {\n    it(\"should not send loaded content pings if there is no loaded content data\", () => {\n      const spy = sandbox.spy(instance, \"sendEvent\");\n      const session = {};\n      instance.sendDiscoveryStreamLoadedContent(\"foo\", session);\n\n      assert.notCalled(spy);\n    });\n    it(\"should send loaded content pings if there is loaded content data\", () => {\n      const spy = sandbox.spy(instance, \"sendStructuredIngestionEvent\");\n      const session = {\n        loadedContentSets: {\n          source_foo: [{ id: 1, pos: 0 }, { id: 2, pos: 1 }],\n          source_bar: [{ id: 3, pos: 0 }, { id: 4, pos: 1 }],\n        },\n      };\n      instance.sendDiscoveryStreamLoadedContent(\"foo\", session);\n\n      assert.calledTwice(spy);\n\n      let [payload] = spy.firstCall.args;\n      let sources = new Set([]);\n      sources.add(payload.source);\n      assert.equal(payload.loaded, 2);\n      assert.deepEqual(\n        payload.tiles,\n        session.loadedContentSets[payload.source]\n      );\n\n      [payload] = spy.secondCall.args;\n      sources.add(payload.source);\n      assert.equal(payload.loaded, 2);\n      assert.deepEqual(\n        payload.tiles,\n        session.loadedContentSets[payload.source]\n      );\n\n      assert.deepEqual(sources, new Set([\"source_foo\", \"source_bar\"]));\n    });\n  });\n  describe(\"#handleDiscoveryStreamImpressionStats\", () => {\n    it(\"should throw for a missing session\", () => {\n      assert.throws(() => {\n        instance.handleDiscoveryStreamImpressionStats(\"a_missing_port\", {});\n      }, \"Session does not exist.\");\n    });\n    it(\"should store impression to impressionSets\", () => {\n      const session = instance.addSession(\"new_session\", \"about:newtab\");\n      instance.handleDiscoveryStreamImpressionStats(\"new_session\", {\n        source: \"foo\",\n        tiles: [{ id: 1, pos: 0 }],\n      });\n\n      assert.equal(Object.keys(session.impressionSets).length, 1);\n      assert.deepEqual(session.impressionSets.foo, [{ id: 1, pos: 0 }]);\n\n      // Add another ping with the same source\n      instance.handleDiscoveryStreamImpressionStats(\"new_session\", {\n        source: \"foo\",\n        tiles: [{ id: 2, pos: 1 }],\n      });\n\n      assert.deepEqual(session.impressionSets.foo, [\n        { id: 1, pos: 0 },\n        { id: 2, pos: 1 },\n      ]);\n\n      // Add another ping with a different source\n      instance.handleDiscoveryStreamImpressionStats(\"new_session\", {\n        source: \"bar\",\n        tiles: [{ id: 3, pos: 2 }],\n      });\n\n      assert.equal(Object.keys(session.impressionSets).length, 2);\n      assert.deepEqual(session.impressionSets.foo, [\n        { id: 1, pos: 0 },\n        { id: 2, pos: 1 },\n      ]);\n      assert.deepEqual(session.impressionSets.bar, [{ id: 3, pos: 2 }]);\n    });\n  });\n  describe(\"#handleDiscoveryStreamLoadedContent\", () => {\n    it(\"should throw for a missing session\", () => {\n      assert.throws(() => {\n        instance.handleDiscoveryStreamLoadedContent(\"a_missing_port\", {});\n      }, \"Session does not exist.\");\n    });\n    it(\"should store loaded content to loadedContentSets\", () => {\n      const session = instance.addSession(\"new_session\", \"about:newtab\");\n      instance.handleDiscoveryStreamLoadedContent(\"new_session\", {\n        source: \"foo\",\n        tiles: [{ id: 1, pos: 0 }],\n      });\n\n      assert.equal(Object.keys(session.loadedContentSets).length, 1);\n      assert.deepEqual(session.loadedContentSets.foo, [{ id: 1, pos: 0 }]);\n\n      // Add another ping with the same source\n      instance.handleDiscoveryStreamLoadedContent(\"new_session\", {\n        source: \"foo\",\n        tiles: [{ id: 2, pos: 1 }],\n      });\n\n      assert.deepEqual(session.loadedContentSets.foo, [\n        { id: 1, pos: 0 },\n        { id: 2, pos: 1 },\n      ]);\n\n      // Add another ping with a different source\n      instance.handleDiscoveryStreamLoadedContent(\"new_session\", {\n        source: \"bar\",\n        tiles: [{ id: 3, pos: 2 }],\n      });\n\n      assert.equal(Object.keys(session.loadedContentSets).length, 2);\n      assert.deepEqual(session.loadedContentSets.foo, [\n        { id: 1, pos: 0 },\n        { id: 2, pos: 1 },\n      ]);\n      assert.deepEqual(session.loadedContentSets.bar, [{ id: 3, pos: 2 }]);\n    });\n  });\n  describe(\"#_generateStructuredIngestionEndpoint\", () => {\n    it(\"should generate a valid endpoint\", () => {\n      const fakeEndpoint = \"http://fakeendpoint.com/base/\";\n      const fakeUUID = \"{34f24486-f01a-9749-9c5b-21476af1fa77}\";\n      const fakeUUIDWithoutBraces = fakeUUID.substring(1, fakeUUID.length - 1);\n      FakePrefs.prototype.prefs = {};\n      FakePrefs.prototype.prefs[\n        STRUCTURED_INGESTION_ENDPOINT_PREF\n      ] = fakeEndpoint;\n      sandbox.stub(global.gUUIDGenerator, \"generateUUID\").returns(fakeUUID);\n      const feed = new TelemetryFeed();\n      const url = feed._generateStructuredIngestionEndpoint(\n        \"testNameSpace\",\n        \"testPingType\",\n        \"1\"\n      );\n\n      assert.equal(\n        url,\n        `${fakeEndpoint}/testNameSpace/testPingType/1/${fakeUUIDWithoutBraces}`\n      );\n    });\n  });\n  describe(\"#handleTrailheadEnrollEvent\", () => {\n    it(\"should send a TRAILHEAD_ENROLL_EVENT if the telemetry is enabled\", () => {\n      FakePrefs.prototype.prefs[TELEMETRY_PREF] = true;\n      const data = { experiment: \"foo\", type: \"bar\", branch: \"baz\" };\n      instance = new TelemetryFeed();\n      sandbox.stub(instance.utEvents, \"sendTrailheadEnrollEvent\");\n\n      instance.handleTrailheadEnrollEvent({ data });\n\n      assert.calledWith(instance.utEvents.sendTrailheadEnrollEvent, data);\n    });\n    it(\"should not send TRAILHEAD_ENROLL_EVENT if the telemetry is disabled\", () => {\n      FakePrefs.prototype.prefs[TELEMETRY_PREF] = false;\n      const data = { experiment: \"foo\", type: \"bar\", branch: \"baz\" };\n      instance = new TelemetryFeed();\n      sandbox.stub(instance.utEvents, \"sendTrailheadEnrollEvent\");\n\n      instance.handleTrailheadEnrollEvent({ data });\n\n      assert.notCalled(instance.utEvents.sendTrailheadEnrollEvent);\n    });\n  });\n  describe(\"#handleASRouterUserEvent\", () => {\n    it(\"should call sendStructuredIngestionEvent on known pingTypes\", async () => {\n      const data = {\n        action: \"onboarding_user_event\",\n        event: \"IMPRESSION\",\n        message_id: \"12345\",\n      };\n      instance = new TelemetryFeed();\n      sandbox.spy(instance, \"sendStructuredIngestionEvent\");\n\n      await instance.handleASRouterUserEvent({ data });\n\n      assert.calledOnce(instance.sendStructuredIngestionEvent);\n    });\n    it(\"should reportError on unknown pingTypes\", async () => {\n      const data = {\n        action: \"unknown_event\",\n        event: \"IMPRESSION\",\n        message_id: \"12345\",\n      };\n      instance = new TelemetryFeed();\n      sandbox.spy(instance, \"sendStructuredIngestionEvent\");\n\n      await instance.handleASRouterUserEvent({ data });\n\n      assert.calledOnce(global.Cu.reportError);\n      assert.notCalled(instance.sendStructuredIngestionEvent);\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/lib/TippyTopProvider.test.js",
    "content": "import { GlobalOverrider } from \"test/unit/utils\";\nimport { TippyTopProvider } from \"lib/TippyTopProvider.jsm\";\n\ndescribe(\"TippyTopProvider\", () => {\n  let instance;\n  let globals;\n  beforeEach(async () => {\n    globals = new GlobalOverrider();\n    let fetchStub = globals.sandbox.stub();\n    globals.set(\"fetch\", fetchStub);\n    fetchStub.resolves({\n      ok: true,\n      status: 200,\n      json: () =>\n        Promise.resolve([\n          {\n            title: \"facebook\",\n            url: \"https://www.facebook.com/\",\n            image_url: \"facebook-com.png\",\n            background_color: \"#3b5998\",\n            domain: \"facebook.com\",\n          },\n          {\n            title: \"gmail\",\n            urls: [\"https://www.gmail.com/\", \"https://mail.google.com\"],\n            image_url: \"gmail-com.png\",\n            background_color: \"#000000\",\n            domain: \"gmail.com\",\n          },\n        ]),\n    });\n    instance = new TippyTopProvider();\n    await instance.init();\n  });\n  it(\"should provide an icon for facebook.com\", () => {\n    const site = instance.processSite({ url: \"https://facebook.com\" });\n    assert.equal(\n      site.tippyTopIcon,\n      \"resource://activity-stream/data/content/tippytop/images/facebook-com.png\"\n    );\n    assert.equal(site.backgroundColor, \"#3b5998\");\n  });\n  it(\"should provide an icon for www.facebook.com\", () => {\n    const site = instance.processSite({ url: \"https://www.facebook.com\" });\n    assert.equal(\n      site.tippyTopIcon,\n      \"resource://activity-stream/data/content/tippytop/images/facebook-com.png\"\n    );\n    assert.equal(site.backgroundColor, \"#3b5998\");\n  });\n  it(\"should provide an icon for facebook.com/foobar\", () => {\n    const site = instance.processSite({ url: \"https://facebook.com/foobar\" });\n    assert.equal(\n      site.tippyTopIcon,\n      \"resource://activity-stream/data/content/tippytop/images/facebook-com.png\"\n    );\n    assert.equal(site.backgroundColor, \"#3b5998\");\n  });\n  it(\"should provide an icon for gmail.com\", () => {\n    const site = instance.processSite({ url: \"https://gmail.com\" });\n    assert.equal(\n      site.tippyTopIcon,\n      \"resource://activity-stream/data/content/tippytop/images/gmail-com.png\"\n    );\n    assert.equal(site.backgroundColor, \"#000000\");\n  });\n  it(\"should provide an icon for mail.google.com\", () => {\n    const site = instance.processSite({ url: \"https://mail.google.com\" });\n    assert.equal(\n      site.tippyTopIcon,\n      \"resource://activity-stream/data/content/tippytop/images/gmail-com.png\"\n    );\n    assert.equal(site.backgroundColor, \"#000000\");\n  });\n  it(\"should handle garbage URLs gracefully\", () => {\n    const site = instance.processSite({ url: \"garbagejlfkdsa\" });\n    assert.isUndefined(site.tippyTopIcon);\n    assert.isUndefined(site.backgroundColor);\n  });\n  it(\"should handle error when fetching and parsing manifest\", async () => {\n    globals = new GlobalOverrider();\n    let fetchStub = globals.sandbox.stub();\n    globals.set(\"fetch\", fetchStub);\n    fetchStub.rejects(\"whaaaa\");\n    instance = new TippyTopProvider();\n    await instance.init();\n    instance.processSite({ url: \"https://facebook.com\" });\n  });\n});\n"
  },
  {
    "path": "test/unit/lib/Tokenize.test.js",
    "content": "import { tokenize, toksToTfIdfVector } from \"lib/Tokenize.jsm\";\n\nconst EPSILON = 0.00001;\n\ndescribe(\"TF-IDF Term Vectorizer\", () => {\n  describe(\"#tokenize\", () => {\n    let testCases = [\n      { input: \"HELLO there\", expected: [\"hello\", \"there\"] },\n      { input: \"blah,,,blah,blah\", expected: [\"blah\", \"blah\", \"blah\"] },\n      {\n        input: \"Call Jenny: 967-5309\",\n        expected: [\"call\", \"jenny\", \"967\", \"5309\"],\n      },\n      {\n        input: \"Yo(what)[[hello]]{{jim}}}bob{1:2:1+2=$3\",\n        expected: [\n          \"yo\",\n          \"what\",\n          \"hello\",\n          \"jim\",\n          \"bob\",\n          \"1\",\n          \"2\",\n          \"1\",\n          \"2\",\n          \"3\",\n        ],\n      },\n      { input: \"čÄfė 80's\", expected: [\"čäfė\", \"80\", \"s\"] },\n      { input: \"我知道很多东西。\", expected: [\"我知道很多东西\"] },\n    ];\n    let checkTokenization = tc => {\n      it(`${tc.input} should tokenize to ${tc.expected}`, () => {\n        assert.deepEqual(tc.expected, tokenize(tc.input));\n      });\n    };\n\n    for (let i = 0; i < testCases.length; i++) {\n      checkTokenization(testCases[i]);\n    }\n  });\n\n  describe(\"#tfidf\", () => {\n    let vocab_idfs = {\n      deal: [221, 5.5058519847862275],\n      easy: [269, 5.5058519847862275],\n      tanks: [867, 5.601162164590552],\n      sites: [792, 5.957837108529285],\n      care: [153, 5.957837108529285],\n      needs: [596, 5.824305715904762],\n      finally: [334, 5.706522680248379],\n    };\n    let testCases = [\n      {\n        input: \"Finally! Easy care for your tanks!\",\n        expected: {\n          finally: [334, 0.5009816295853761],\n          easy: [269, 0.48336453811728713],\n          care: [153, 0.5230447876368227],\n          tanks: [867, 0.49173191907236774],\n        },\n      },\n      {\n        input: \"Easy easy EASY\",\n        expected: { easy: [269, 1.0] },\n      },\n      {\n        input: \"Easy easy care\",\n        expected: {\n          easy: [269, 0.8795205218806832],\n          care: [153, 0.4758609582543317],\n        },\n      },\n      {\n        input: \"easy care\",\n        expected: {\n          easy: [269, 0.6786999710383944],\n          care: [153, 0.7344156515982504],\n        },\n      },\n      {\n        input: \"这个空间故意留空。\",\n        expected: {\n          /* This space is left intentionally blank. */\n        },\n      },\n    ];\n    let checkTokenGeneration = tc => {\n      describe(`${tc.input} should have only vocabulary tokens`, () => {\n        let actual = toksToTfIdfVector(tokenize(tc.input), vocab_idfs);\n\n        it(`${tc.input} should generate exactly ${Object.keys(\n          tc.expected\n        )}`, () => {\n          let seen = {};\n          Object.keys(actual).forEach(actualTok => {\n            assert.isTrue(actualTok in tc.expected);\n            seen[actualTok] = true;\n          });\n          Object.keys(tc.expected).forEach(expectedTok => {\n            assert.isTrue(expectedTok in seen);\n          });\n        });\n\n        it(`${tc.input} should have the correct token ids`, () => {\n          Object.keys(actual).forEach(actualTok => {\n            assert.equal(tc.expected[actualTok][0], actual[actualTok][0]);\n          });\n        });\n      });\n    };\n\n    let checkTfIdfVector = tc => {\n      let actual = toksToTfIdfVector(tokenize(tc.input), vocab_idfs);\n      it(`${tc.input} should have the correct tf-idf`, () => {\n        Object.keys(actual).forEach(actualTok => {\n          let delta = Math.abs(\n            tc.expected[actualTok][1] - actual[actualTok][1]\n          );\n          assert.isTrue(delta <= EPSILON);\n        });\n      });\n    };\n\n    // run the tests\n    for (let i = 0; i < testCases.length; i++) {\n      checkTokenGeneration(testCases[i]);\n      checkTfIdfVector(testCases[i]);\n    }\n  });\n});\n"
  },
  {
    "path": "test/unit/lib/ToolbarBadgeHub.test.js",
    "content": "import { _ToolbarBadgeHub } from \"lib/ToolbarBadgeHub.jsm\";\nimport { GlobalOverrider } from \"test/unit/utils\";\nimport { OnboardingMessageProvider } from \"lib/OnboardingMessageProvider.jsm\";\nimport { _ToolbarPanelHub, ToolbarPanelHub } from \"lib/ToolbarPanelHub.jsm\";\n\ndescribe(\"ToolbarBadgeHub\", () => {\n  let sandbox;\n  let instance;\n  let fakeAddImpression;\n  let fakeDispatch;\n  let isBrowserPrivateStub;\n  let fxaMessage;\n  let whatsnewMessage;\n  let fakeElement;\n  let globals;\n  let everyWindowStub;\n  let clearTimeoutStub;\n  let setTimeoutStub;\n  let setIntervalStub;\n  let addObserverStub;\n  let removeObserverStub;\n  let getStringPrefStub;\n  let clearUserPrefStub;\n  let setStringPrefStub;\n  let requestIdleCallbackStub;\n  let fakeWindow;\n  beforeEach(async () => {\n    globals = new GlobalOverrider();\n    sandbox = sinon.createSandbox();\n    instance = new _ToolbarBadgeHub();\n    fakeAddImpression = sandbox.stub();\n    fakeDispatch = sandbox.stub();\n    isBrowserPrivateStub = sandbox.stub();\n    const onboardingMsgs = await OnboardingMessageProvider.getUntranslatedMessages();\n    fxaMessage = onboardingMsgs.find(({ id }) => id === \"FXA_ACCOUNTS_BADGE\");\n    whatsnewMessage = {\n      id: `WHATS_NEW_BADGE_71`,\n      template: \"toolbar_badge\",\n      content: {\n        delay: 1000,\n        target: \"whats-new-menu-button\",\n        action: { id: \"show-whatsnew-button\" },\n        badgeDescription: { string_id: \"cfr-badge-reader-label-newfeature\" },\n      },\n      priority: 1,\n      trigger: { id: \"toolbarBadgeUpdate\" },\n      frequency: {\n        // Makes it so that we track impressions for this message while at the\n        // same time it can have unlimited impressions\n        lifetime: Infinity,\n      },\n      // Never saw this message or saw it in the past 4 days or more recent\n      targeting: `isWhatsNewPanelEnabled &&\n      (!messageImpressions['WHATS_NEW_BADGE_71'] ||\n        (messageImpressions['WHATS_NEW_BADGE_71']|length >= 1 &&\n          currentDate|date - messageImpressions['WHATS_NEW_BADGE_71'][0] <= 4 * 24 * 3600 * 1000))`,\n    };\n    fakeElement = {\n      classList: {\n        add: sandbox.stub(),\n        remove: sandbox.stub(),\n      },\n      setAttribute: sandbox.stub(),\n      removeAttribute: sandbox.stub(),\n      querySelector: sandbox.stub(),\n      addEventListener: sandbox.stub(),\n      remove: sandbox.stub(),\n      appendChild: sandbox.stub(),\n    };\n    // Share the same element when selecting child nodes\n    fakeElement.querySelector.returns(fakeElement);\n    everyWindowStub = {\n      registerCallback: sandbox.stub(),\n      unregisterCallback: sandbox.stub(),\n    };\n    clearTimeoutStub = sandbox.stub();\n    setTimeoutStub = sandbox.stub();\n    setIntervalStub = sandbox.stub();\n    fakeWindow = {\n      MozXULElement: { insertFTLIfNeeded: sandbox.stub() },\n      ownerGlobal: {\n        gBrowser: {\n          selectedBrowser: \"browser\",\n        },\n      },\n    };\n    addObserverStub = sandbox.stub();\n    removeObserverStub = sandbox.stub();\n    getStringPrefStub = sandbox.stub();\n    clearUserPrefStub = sandbox.stub();\n    setStringPrefStub = sandbox.stub();\n    requestIdleCallbackStub = sandbox.stub().callsFake(fn => fn());\n    globals.set({\n      ToolbarPanelHub,\n      requestIdleCallback: requestIdleCallbackStub,\n      EveryWindow: everyWindowStub,\n      PrivateBrowsingUtils: { isBrowserPrivate: isBrowserPrivateStub },\n      setTimeout: setTimeoutStub,\n      clearTimeout: clearTimeoutStub,\n      setInterval: setIntervalStub,\n      Services: {\n        wm: {\n          getMostRecentWindow: () => fakeWindow,\n        },\n        prefs: {\n          addObserver: addObserverStub,\n          removeObserver: removeObserverStub,\n          getStringPref: getStringPrefStub,\n          clearUserPref: clearUserPrefStub,\n          setStringPref: setStringPrefStub,\n        },\n      },\n    });\n  });\n  afterEach(() => {\n    sandbox.restore();\n    globals.restore();\n  });\n  it(\"should create an instance\", () => {\n    assert.ok(instance);\n  });\n  describe(\"#init\", () => {\n    it(\"should make a messageRequest on init\", async () => {\n      sandbox.stub(instance, \"messageRequest\");\n      const waitForInitialized = sandbox.stub().resolves();\n\n      await instance.init(waitForInitialized, {});\n      assert.calledOnce(instance.messageRequest);\n      assert.calledWithExactly(instance.messageRequest, {\n        template: \"toolbar_badge\",\n        triggerId: \"toolbarBadgeUpdate\",\n      });\n    });\n    it(\"should add a pref observer\", async () => {\n      await instance.init(sandbox.stub().resolves(), {});\n\n      assert.calledOnce(addObserverStub);\n      assert.calledWithExactly(\n        addObserverStub,\n        instance.prefs.WHATSNEW_TOOLBAR_PANEL,\n        instance\n      );\n    });\n    it(\"should setInterval for `checkHomepageOverridePref`\", async () => {\n      await instance.init(sandbox.stub().resolves(), {});\n      sandbox.stub(instance, \"checkHomepageOverridePref\");\n\n      assert.calledOnce(setIntervalStub);\n      assert.calledWithExactly(\n        setIntervalStub,\n        sinon.match.func,\n        5 * 60 * 1000\n      );\n\n      assert.notCalled(instance.checkHomepageOverridePref);\n      const [cb] = setIntervalStub.firstCall.args;\n\n      cb();\n\n      assert.calledOnce(instance.checkHomepageOverridePref);\n    });\n  });\n  describe(\"#uninit\", () => {\n    beforeEach(async () => {\n      await instance.init(sandbox.stub().resolves(), {});\n    });\n    it(\"should clear any setTimeout cbs\", async () => {\n      await instance.init(sandbox.stub().resolves(), {});\n\n      instance.state.showBadgeTimeoutId = 2;\n\n      instance.uninit();\n\n      assert.calledOnce(clearTimeoutStub);\n      assert.calledWithExactly(clearTimeoutStub, 2);\n    });\n    it(\"should remove the pref observer\", () => {\n      instance.uninit();\n\n      assert.calledOnce(removeObserverStub);\n      assert.calledWithExactly(\n        removeObserverStub,\n        instance.prefs.WHATSNEW_TOOLBAR_PANEL,\n        instance\n      );\n    });\n  });\n  describe(\"messageRequest\", () => {\n    let handleMessageRequestStub;\n    beforeEach(() => {\n      handleMessageRequestStub = sandbox.stub().returns(fxaMessage);\n      sandbox\n        .stub(instance, \"_handleMessageRequest\")\n        .value(handleMessageRequestStub);\n      sandbox.stub(instance, \"registerBadgeNotificationListener\");\n    });\n    it(\"should fetch a message with the provided trigger and template\", async () => {\n      await instance.messageRequest({\n        triggerId: \"trigger\",\n        template: \"template\",\n      });\n\n      assert.calledOnce(handleMessageRequestStub);\n      assert.calledWithExactly(handleMessageRequestStub, {\n        triggerId: \"trigger\",\n        template: \"template\",\n      });\n    });\n    it(\"should call addToolbarNotification with browser window and message\", async () => {\n      await instance.messageRequest(\"trigger\");\n\n      assert.calledOnce(instance.registerBadgeNotificationListener);\n      assert.calledWithExactly(\n        instance.registerBadgeNotificationListener,\n        fxaMessage\n      );\n    });\n    it(\"shouldn't do anything if no message is provided\", () => {\n      handleMessageRequestStub.returns(null);\n      instance.messageRequest(\"trigger\");\n\n      assert.notCalled(instance.registerBadgeNotificationListener);\n    });\n  });\n  describe(\"addToolbarNotification\", () => {\n    let target;\n    let fakeDocument;\n    beforeEach(async () => {\n      await instance.init(sandbox.stub().resolves(), {\n        addImpression: fakeAddImpression,\n        dispatch: fakeDispatch,\n      });\n      fakeDocument = {\n        getElementById: sandbox.stub().returns(fakeElement),\n        createElement: sandbox.stub().returns(fakeElement),\n        l10n: { setAttributes: sandbox.stub() },\n      };\n      target = { ...fakeWindow, browser: { ownerDocument: fakeDocument } };\n    });\n    afterEach(() => {\n      instance.uninit();\n    });\n    it(\"shouldn't do anything if target element is not found\", () => {\n      fakeDocument.getElementById.returns(null);\n      instance.addToolbarNotification(target, fxaMessage);\n\n      assert.notCalled(fakeElement.setAttribute);\n    });\n    it(\"should target the element specified in the message\", () => {\n      instance.addToolbarNotification(target, fxaMessage);\n\n      assert.calledOnce(fakeDocument.getElementById);\n      assert.calledWithExactly(\n        fakeDocument.getElementById,\n        fxaMessage.content.target\n      );\n    });\n    it(\"should show a notification\", () => {\n      instance.addToolbarNotification(target, fxaMessage);\n\n      assert.calledOnce(fakeElement.setAttribute);\n      assert.calledWithExactly(fakeElement.setAttribute, \"badged\", true);\n      assert.calledWithExactly(fakeElement.classList.add, \"feature-callout\");\n    });\n    it(\"should attach a cb on the notification\", () => {\n      instance.addToolbarNotification(target, fxaMessage);\n\n      assert.calledTwice(fakeElement.addEventListener);\n      assert.calledWithExactly(\n        fakeElement.addEventListener,\n        \"mousedown\",\n        instance.removeAllNotifications\n      );\n      assert.calledWithExactly(\n        fakeElement.addEventListener,\n        \"keypress\",\n        instance.removeAllNotifications\n      );\n    });\n    it(\"should execute actions if they exist\", () => {\n      sandbox.stub(instance, \"executeAction\");\n      instance.addToolbarNotification(target, whatsnewMessage);\n\n      assert.calledOnce(instance.executeAction);\n      assert.calledWithExactly(instance.executeAction, {\n        ...whatsnewMessage.content.action,\n        message_id: whatsnewMessage.id,\n      });\n    });\n    it(\"should create a description element\", () => {\n      sandbox.stub(instance, \"executeAction\");\n      instance.addToolbarNotification(target, whatsnewMessage);\n\n      assert.calledOnce(fakeDocument.createElement);\n      assert.calledWithExactly(fakeDocument.createElement, \"span\");\n    });\n    it(\"should set description id to element and to button\", () => {\n      sandbox.stub(instance, \"executeAction\");\n      instance.addToolbarNotification(target, whatsnewMessage);\n\n      assert.calledWithExactly(\n        fakeElement.setAttribute,\n        \"id\",\n        \"toolbarbutton-notification-description\"\n      );\n      assert.calledWithExactly(\n        fakeElement.setAttribute,\n        \"aria-labelledby\",\n        `toolbarbutton-notification-description ${\n          whatsnewMessage.content.target\n        }`\n      );\n    });\n    it(\"should attach fluent id to description\", () => {\n      sandbox.stub(instance, \"executeAction\");\n      instance.addToolbarNotification(target, whatsnewMessage);\n\n      assert.calledOnce(fakeDocument.l10n.setAttributes);\n      assert.calledWithExactly(\n        fakeDocument.l10n.setAttributes,\n        fakeElement,\n        whatsnewMessage.content.badgeDescription.string_id\n      );\n    });\n    it(\"should add an impression for the message\", () => {\n      instance.addToolbarNotification(target, whatsnewMessage);\n\n      assert.calledOnce(instance._addImpression);\n      assert.calledWithExactly(instance._addImpression, whatsnewMessage);\n    });\n    it(\"should send an impression ping\", async () => {\n      sandbox.stub(instance, \"sendUserEventTelemetry\");\n      instance.addToolbarNotification(target, whatsnewMessage);\n\n      assert.calledOnce(instance.sendUserEventTelemetry);\n      assert.calledWithExactly(\n        instance.sendUserEventTelemetry,\n        \"IMPRESSION\",\n        whatsnewMessage\n      );\n    });\n  });\n  describe(\"registerBadgeNotificationListener\", () => {\n    let msg_no_delay;\n    beforeEach(async () => {\n      await instance.init(sandbox.stub().resolves(), {\n        addImpression: fakeAddImpression,\n        dispatch: fakeDispatch,\n      });\n      sandbox.stub(instance, \"addToolbarNotification\").returns(fakeElement);\n      sandbox.stub(instance, \"removeToolbarNotification\");\n      msg_no_delay = {\n        ...fxaMessage,\n        content: {\n          ...fxaMessage.content,\n          delay: 0,\n        },\n      };\n    });\n    afterEach(() => {\n      instance.uninit();\n    });\n    it(\"should register a callback that adds/removes the notification\", () => {\n      instance.registerBadgeNotificationListener(msg_no_delay);\n\n      assert.calledOnce(everyWindowStub.registerCallback);\n      assert.calledWithExactly(\n        everyWindowStub.registerCallback,\n        instance.id,\n        sinon.match.func,\n        sinon.match.func\n      );\n\n      const [\n        ,\n        initFn,\n        uninitFn,\n      ] = everyWindowStub.registerCallback.firstCall.args;\n\n      initFn(window);\n      // Test that it doesn't try to add a second notification\n      initFn(window);\n\n      assert.calledOnce(instance.addToolbarNotification);\n      assert.calledWithExactly(\n        instance.addToolbarNotification,\n        window,\n        msg_no_delay\n      );\n\n      uninitFn(window);\n\n      assert.calledOnce(instance.removeToolbarNotification);\n      assert.calledWithExactly(instance.removeToolbarNotification, fakeElement);\n    });\n    it(\"should unregister notifications when forcing a badge via devtools\", () => {\n      instance.registerBadgeNotificationListener(msg_no_delay, { force: true });\n\n      assert.calledOnce(everyWindowStub.unregisterCallback);\n      assert.calledWithExactly(everyWindowStub.unregisterCallback, instance.id);\n    });\n    it(\"should only call executeAction for 'update_action' messages\", () => {\n      const stub = sandbox.stub(instance, \"executeAction\");\n      const updateActionMsg = { ...msg_no_delay, template: \"update_action\" };\n\n      instance.registerBadgeNotificationListener(updateActionMsg);\n\n      assert.notCalled(everyWindowStub.registerCallback);\n      assert.calledOnce(stub);\n    });\n  });\n  describe(\"executeAction\", () => {\n    let blockMessageByIdStub;\n    beforeEach(async () => {\n      blockMessageByIdStub = sandbox.stub();\n      await instance.init(sandbox.stub().resolves(), {\n        blockMessageById: blockMessageByIdStub,\n      });\n    });\n    it(\"should call ToolbarPanelHub.enableToolbarButton\", () => {\n      const stub = sandbox.stub(\n        _ToolbarPanelHub.prototype,\n        \"enableToolbarButton\"\n      );\n\n      instance.executeAction({ id: \"show-whatsnew-button\" });\n\n      assert.calledOnce(stub);\n    });\n    it(\"should call ToolbarPanelHub.enableAppmenuButton\", () => {\n      const stub = sandbox.stub(\n        _ToolbarPanelHub.prototype,\n        \"enableAppmenuButton\"\n      );\n\n      instance.executeAction({ id: \"show-whatsnew-button\" });\n\n      assert.calledOnce(stub);\n    });\n    it(\"should set HOMEPAGE_OVERRIDE_PREF on `moments-wnp` action\", () => {\n      instance.executeAction({\n        id: \"moments-wnp\",\n        data: {\n          url: \"foo.com\",\n          expire: 1,\n        },\n        message_id: \"bar\",\n      });\n\n      assert.calledOnce(setStringPrefStub);\n      assert.calledWithExactly(\n        setStringPrefStub,\n        instance.prefs.HOMEPAGE_OVERRIDE_PREF,\n        JSON.stringify({ message_id: \"bar\", url: \"foo.com\", expire: 1 })\n      );\n    });\n    it(\"should block after taking the action\", () => {\n      instance.executeAction({\n        id: \"moments-wnp\",\n        data: {\n          url: \"foo.com\",\n          expire: 1,\n        },\n        message_id: \"bar\",\n      });\n\n      assert.calledOnce(blockMessageByIdStub);\n      assert.calledWithExactly(blockMessageByIdStub, \"bar\");\n    });\n    it(\"should compute expire based on expireDelta\", () => {\n      sandbox.spy(instance, \"getExpirationDate\");\n\n      instance.executeAction({\n        id: \"moments-wnp\",\n        data: {\n          url: \"foo.com\",\n          expireDelta: 10,\n        },\n        message_id: \"bar\",\n      });\n\n      assert.calledOnce(instance.getExpirationDate);\n      assert.calledWithExactly(instance.getExpirationDate, 10);\n    });\n  });\n  describe(\"removeToolbarNotification\", () => {\n    it(\"should remove the notification\", () => {\n      instance.removeToolbarNotification(fakeElement);\n\n      assert.calledThrice(fakeElement.removeAttribute);\n      assert.calledWithExactly(fakeElement.removeAttribute, \"badged\");\n      assert.calledWithExactly(fakeElement.removeAttribute, \"aria-labelledby\");\n      assert.calledWithExactly(fakeElement.removeAttribute, \"aria-describedby\");\n      assert.calledOnce(fakeElement.classList.remove);\n      assert.calledWithExactly(fakeElement.classList.remove, \"feature-callout\");\n      assert.calledOnce(fakeElement.remove);\n    });\n  });\n  describe(\"removeAllNotifications\", () => {\n    let blockMessageByIdStub;\n    let fakeEvent;\n    beforeEach(async () => {\n      await instance.init(sandbox.stub().resolves(), {\n        dispatch: fakeDispatch,\n      });\n      blockMessageByIdStub = sandbox.stub();\n      sandbox.stub(instance, \"_blockMessageById\").value(blockMessageByIdStub);\n      instance.state = { notification: { id: fxaMessage.id } };\n      fakeEvent = { target: { removeEventListener: sandbox.stub() } };\n    });\n    it(\"should call to block the message\", () => {\n      instance.removeAllNotifications();\n\n      assert.calledOnce(blockMessageByIdStub);\n      assert.calledWithExactly(blockMessageByIdStub, fxaMessage.id);\n    });\n    it(\"should remove the window listener\", () => {\n      instance.removeAllNotifications();\n\n      assert.calledOnce(everyWindowStub.unregisterCallback);\n      assert.calledWithExactly(everyWindowStub.unregisterCallback, instance.id);\n    });\n    it(\"should ignore right mouse button (mousedown event)\", () => {\n      fakeEvent.type = \"mousedown\";\n      fakeEvent.button = 1; // not left click\n\n      instance.removeAllNotifications(fakeEvent);\n\n      assert.notCalled(fakeEvent.target.removeEventListener);\n      assert.notCalled(everyWindowStub.unregisterCallback);\n    });\n    it(\"should ignore right mouse button (click event)\", () => {\n      fakeEvent.type = \"click\";\n      fakeEvent.button = 1; // not left click\n\n      instance.removeAllNotifications(fakeEvent);\n\n      assert.notCalled(fakeEvent.target.removeEventListener);\n      assert.notCalled(everyWindowStub.unregisterCallback);\n    });\n    it(\"should ignore keypresses that are not meant to focus the target\", () => {\n      fakeEvent.type = \"keypress\";\n      fakeEvent.key = \"\\t\"; // not enter\n\n      instance.removeAllNotifications(fakeEvent);\n\n      assert.notCalled(fakeEvent.target.removeEventListener);\n      assert.notCalled(everyWindowStub.unregisterCallback);\n    });\n    it(\"should remove the event listeners after succesfully focusing the element\", () => {\n      fakeEvent.type = \"click\";\n      fakeEvent.button = 0;\n\n      instance.removeAllNotifications(fakeEvent);\n\n      assert.calledTwice(fakeEvent.target.removeEventListener);\n      assert.calledWithExactly(\n        fakeEvent.target.removeEventListener,\n        \"mousedown\",\n        instance.removeAllNotifications\n      );\n      assert.calledWithExactly(\n        fakeEvent.target.removeEventListener,\n        \"keypress\",\n        instance.removeAllNotifications\n      );\n    });\n    it(\"should send telemetry\", () => {\n      fakeEvent.type = \"click\";\n      fakeEvent.button = 0;\n      sandbox.stub(instance, \"sendUserEventTelemetry\");\n\n      instance.removeAllNotifications(fakeEvent);\n\n      assert.calledOnce(instance.sendUserEventTelemetry);\n      assert.calledWithExactly(instance.sendUserEventTelemetry, \"CLICK\", {\n        id: \"FXA_ACCOUNTS_BADGE\",\n      });\n    });\n    it(\"should remove the event listeners after succesfully focusing the element\", () => {\n      fakeEvent.type = \"keypress\";\n      fakeEvent.key = \"Enter\";\n\n      instance.removeAllNotifications(fakeEvent);\n\n      assert.calledTwice(fakeEvent.target.removeEventListener);\n      assert.calledWithExactly(\n        fakeEvent.target.removeEventListener,\n        \"mousedown\",\n        instance.removeAllNotifications\n      );\n      assert.calledWithExactly(\n        fakeEvent.target.removeEventListener,\n        \"keypress\",\n        instance.removeAllNotifications\n      );\n    });\n  });\n  describe(\"message with delay\", () => {\n    let msg_with_delay;\n    beforeEach(async () => {\n      await instance.init(sandbox.stub().resolves(), {\n        addImpression: fakeAddImpression,\n      });\n      msg_with_delay = {\n        ...fxaMessage,\n        content: {\n          ...fxaMessage.content,\n          delay: 500,\n        },\n      };\n      sandbox.stub(instance, \"registerBadgeToAllWindows\");\n    });\n    afterEach(() => {\n      instance.uninit();\n    });\n    it(\"should register a cb to fire after msg.content.delay ms\", () => {\n      instance.registerBadgeNotificationListener(msg_with_delay);\n\n      assert.calledOnce(setTimeoutStub);\n      assert.calledWithExactly(\n        setTimeoutStub,\n        sinon.match.func,\n        msg_with_delay.content.delay\n      );\n\n      const [cb] = setTimeoutStub.firstCall.args;\n\n      assert.notCalled(instance.registerBadgeToAllWindows);\n\n      cb();\n\n      assert.calledOnce(instance.registerBadgeToAllWindows);\n      assert.calledWithExactly(\n        instance.registerBadgeToAllWindows,\n        msg_with_delay\n      );\n      // Delayed actions should be executed inside requestIdleCallback\n      assert.calledOnce(requestIdleCallbackStub);\n    });\n  });\n  describe(\"#sendUserEventTelemetry\", () => {\n    beforeEach(async () => {\n      await instance.init(sandbox.stub().resolves(), {\n        dispatch: fakeDispatch,\n      });\n    });\n    it(\"should check for private window and not send\", () => {\n      isBrowserPrivateStub.returns(true);\n\n      instance.sendUserEventTelemetry(\"CLICK\", { id: fxaMessage });\n\n      assert.notCalled(instance._dispatch);\n    });\n    it(\"should check for private window and send\", () => {\n      isBrowserPrivateStub.returns(false);\n\n      instance.sendUserEventTelemetry(\"CLICK\", { id: fxaMessage });\n\n      assert.calledOnce(fakeDispatch);\n      const [ping] = instance._dispatch.firstCall.args;\n      assert.propertyVal(ping, \"type\", \"TOOLBAR_BADGE_TELEMETRY\");\n      assert.propertyVal(ping.data, \"event\", \"CLICK\");\n    });\n  });\n  describe(\"#observe\", () => {\n    it(\"should make a message request when the whats new pref is changed\", () => {\n      sandbox.stub(instance, \"messageRequest\");\n\n      instance.observe(\"\", \"\", instance.prefs.WHATSNEW_TOOLBAR_PANEL);\n\n      assert.calledOnce(instance.messageRequest);\n      assert.calledWithExactly(instance.messageRequest, {\n        template: \"toolbar_badge\",\n        triggerId: \"toolbarBadgeUpdate\",\n      });\n    });\n    it(\"should not react to other pref changes\", () => {\n      sandbox.stub(instance, \"messageRequest\");\n\n      instance.observe(\"\", \"\", \"foo\");\n\n      assert.notCalled(instance.messageRequest);\n    });\n  });\n  describe(\"#checkHomepageOverridePref\", () => {\n    let messageRequestStub;\n    let unblockMessageByIdStub;\n    beforeEach(async () => {\n      unblockMessageByIdStub = sandbox.stub();\n      await instance.init(sandbox.stub().resolves(), {\n        unblockMessageById: unblockMessageByIdStub,\n      });\n      messageRequestStub = sandbox.stub(instance, \"messageRequest\");\n    });\n    it(\"should reset HOMEPAGE_OVERRIDE_PREF if set\", () => {\n      getStringPrefStub.returns(true);\n\n      instance.checkHomepageOverridePref();\n\n      assert.calledOnce(getStringPrefStub);\n      assert.calledWithExactly(\n        getStringPrefStub,\n        instance.prefs.HOMEPAGE_OVERRIDE_PREF,\n        \"\"\n      );\n      assert.calledOnce(clearUserPrefStub);\n      assert.calledWithExactly(\n        clearUserPrefStub,\n        instance.prefs.HOMEPAGE_OVERRIDE_PREF\n      );\n    });\n    it(\"should unblock the message set in the pref\", () => {\n      getStringPrefStub.returns(JSON.stringify({ message_id: \"foo\" }));\n\n      instance.checkHomepageOverridePref();\n\n      assert.calledOnce(unblockMessageByIdStub);\n      assert.calledWithExactly(unblockMessageByIdStub, \"foo\");\n    });\n    it(\"should catch parse errors\", () => {\n      getStringPrefStub.returns({});\n\n      instance.checkHomepageOverridePref();\n\n      assert.notCalled(unblockMessageByIdStub);\n      assert.calledOnce(messageRequestStub);\n      assert.calledWithExactly(messageRequestStub, {\n        template: \"update_action\",\n        triggerId: \"momentsUpdate\",\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/lib/ToolbarPanelHub.test.js",
    "content": "import { _ToolbarPanelHub } from \"lib/ToolbarPanelHub.jsm\";\nimport { GlobalOverrider } from \"test/unit/utils\";\nimport { OnboardingMessageProvider } from \"lib/OnboardingMessageProvider.jsm\";\nimport { PanelTestProvider } from \"lib/PanelTestProvider.jsm\";\n\ndescribe(\"ToolbarPanelHub\", () => {\n  let globals;\n  let sandbox;\n  let instance;\n  let everyWindowStub;\n  let fakeDocument;\n  let fakeWindow;\n  let fakeElementById;\n  let createdElements = [];\n  let eventListeners = {};\n  let addObserverStub;\n  let removeObserverStub;\n  let getBoolPrefStub;\n  let setBoolPrefStub;\n  let waitForInitializedStub;\n  let isBrowserPrivateStub;\n  let fakeDispatch;\n  let getEarliestRecordedDateStub;\n  let getEventsByDateRangeStub;\n  let handleUserActionStub;\n\n  beforeEach(async () => {\n    sandbox = sinon.createSandbox();\n    globals = new GlobalOverrider();\n    instance = new _ToolbarPanelHub();\n    waitForInitializedStub = sandbox.stub().resolves();\n    fakeElementById = {\n      setAttribute: sandbox.stub(),\n      removeAttribute: sandbox.stub(),\n      querySelector: sandbox.stub().returns(null),\n      querySelectorAll: sandbox.stub().returns([]),\n      appendChild: sandbox.stub(),\n      addEventListener: sandbox.stub(),\n      hasAttribute: sandbox.stub(),\n      toggleAttribute: sandbox.stub(),\n      remove: sandbox.stub(),\n      removeChild: sandbox.stub(),\n    };\n    fakeDocument = {\n      getElementById: sandbox.stub().returns(fakeElementById),\n      querySelector: sandbox.stub().returns({}),\n      createElementNS: (ns, tagName) => {\n        const element = {\n          tagName,\n          classList: {\n            add: sandbox.stub(),\n          },\n          addEventListener: (ev, fn) => {\n            eventListeners[ev] = fn;\n          },\n          appendChild: sandbox.stub(),\n          setAttribute: sandbox.stub(),\n        };\n        createdElements.push(element);\n        return element;\n      },\n    };\n    fakeWindow = {\n      // eslint-disable-next-line object-shorthand\n      DocumentFragment: function() {\n        return fakeElementById;\n      },\n      document: fakeDocument,\n      browser: {\n        ownerDocument: fakeDocument,\n      },\n      MozXULElement: { insertFTLIfNeeded: sandbox.stub() },\n      ownerGlobal: {\n        openLinkIn: sandbox.stub(),\n        gBrowser: \"gBrowser\",\n      },\n      PanelUI: {\n        panel: fakeElementById,\n        whatsNewPanel: fakeElementById,\n      },\n    };\n    everyWindowStub = {\n      registerCallback: sandbox.stub(),\n      unregisterCallback: sandbox.stub(),\n    };\n    addObserverStub = sandbox.stub();\n    removeObserverStub = sandbox.stub();\n    getBoolPrefStub = sandbox.stub();\n    setBoolPrefStub = sandbox.stub();\n    fakeDispatch = sandbox.stub();\n    isBrowserPrivateStub = sandbox.stub();\n    getEarliestRecordedDateStub = sandbox.stub().returns(\n      // A random date that's not the current timestamp\n      new Date() - 500\n    );\n    getEventsByDateRangeStub = sandbox.stub().returns([]);\n    handleUserActionStub = sandbox.stub();\n    globals.set({\n      EveryWindow: everyWindowStub,\n      Services: {\n        ...Services,\n        prefs: {\n          addObserver: addObserverStub,\n          removeObserver: removeObserverStub,\n          getBoolPref: getBoolPrefStub,\n          setBoolPref: setBoolPrefStub,\n        },\n      },\n      PrivateBrowsingUtils: {\n        isBrowserPrivate: isBrowserPrivateStub,\n      },\n      TrackingDBService: {\n        getEarliestRecordedDate: getEarliestRecordedDateStub,\n        getEventsByDateRange: getEventsByDateRangeStub,\n      },\n      RemoteL10n: {\n        l10n: {\n          translateElements: sandbox.stub(),\n          translateFragment: sandbox.stub(),\n          formatMessages: sandbox.stub().resolves([{}]),\n        },\n      },\n    });\n  });\n  afterEach(() => {\n    instance.uninit();\n    sandbox.restore();\n    globals.restore();\n    eventListeners = {};\n    createdElements = [];\n  });\n  it(\"should create an instance\", () => {\n    assert.ok(instance);\n  });\n  it(\"should not enableAppmenuButton() on init() if pref is not enabled\", async () => {\n    getBoolPrefStub.returns(false);\n    instance.enableAppmenuButton = sandbox.stub();\n    await instance.init(waitForInitializedStub, { getMessages: () => {} });\n    assert.notCalled(instance.enableAppmenuButton);\n  });\n  it(\"should enableAppmenuButton() on init() if pref is enabled\", async () => {\n    getBoolPrefStub.returns(true);\n    instance.enableAppmenuButton = sandbox.stub();\n\n    await instance.init(waitForInitializedStub, { getMessages: () => {} });\n\n    assert.calledOnce(instance.enableAppmenuButton);\n  });\n  it(\"should unregisterCallback on uninit()\", () => {\n    instance.uninit();\n    assert.calledTwice(everyWindowStub.unregisterCallback);\n  });\n  it(\"should observe pref changes on init\", async () => {\n    await instance.init(waitForInitializedStub, {});\n\n    assert.calledOnce(addObserverStub);\n    assert.calledWithExactly(\n      addObserverStub,\n      \"browser.messaging-system.whatsNewPanel.enabled\",\n      instance\n    );\n  });\n  it(\"should remove the observer on uninit\", () => {\n    instance.uninit();\n\n    assert.calledOnce(removeObserverStub);\n    assert.calledWithExactly(\n      removeObserverStub,\n      \"browser.messaging-system.whatsNewPanel.enabled\",\n      instance\n    );\n  });\n  describe(\"#observe\", () => {\n    it(\"should uninit if the pref is turned off\", () => {\n      sandbox.stub(instance, \"uninit\");\n      getBoolPrefStub.returns(false);\n\n      instance.observe(\n        \"\",\n        \"\",\n        \"browser.messaging-system.whatsNewPanel.enabled\"\n      );\n\n      assert.calledOnce(instance.uninit);\n    });\n    it(\"shouldn't do anything if the pref is true\", () => {\n      sandbox.stub(instance, \"uninit\");\n      getBoolPrefStub.returns(true);\n\n      instance.observe(\n        \"\",\n        \"\",\n        \"browser.messaging-system.whatsNewPanel.enabled\"\n      );\n\n      assert.notCalled(instance.uninit);\n    });\n  });\n  describe(\"#enableAppmenuButton\", () => {\n    it(\"should registerCallback on enableAppmenuButton() if there are messages\", async () => {\n      instance.init(waitForInitializedStub, {\n        getMessages: sandbox.stub().resolves([{}, {}]),\n      });\n      // init calls `enableAppmenuButton`\n      everyWindowStub.registerCallback.resetHistory();\n\n      await instance.enableAppmenuButton();\n\n      assert.calledOnce(everyWindowStub.registerCallback);\n      assert.calledWithExactly(\n        everyWindowStub.registerCallback,\n        \"appMenu-whatsnew-button\",\n        sinon.match.func,\n        sinon.match.func\n      );\n    });\n    it(\"should not registerCallback on enableAppmenuButton() if there are no messages\", async () => {\n      instance.init(waitForInitializedStub, {\n        getMessages: sandbox.stub().resolves([]),\n      });\n      // init calls `enableAppmenuButton`\n      everyWindowStub.registerCallback.resetHistory();\n\n      await instance.enableAppmenuButton();\n\n      assert.notCalled(everyWindowStub.registerCallback);\n    });\n  });\n  describe(\"#enableToolbarButton\", () => {\n    it(\"should registerCallback on enableToolbarButton if messages.length\", async () => {\n      instance.init(waitForInitializedStub, {\n        getMessages: sandbox.stub().resolves([{}, {}]),\n      });\n\n      await instance.enableToolbarButton();\n\n      assert.calledOnce(everyWindowStub.registerCallback);\n    });\n    it(\"should not registerCallback on enableToolbarButton if no messages\", async () => {\n      instance.init(waitForInitializedStub, {\n        getMessages: sandbox.stub().resolves([]),\n      });\n\n      await instance.enableToolbarButton();\n\n      assert.notCalled(everyWindowStub.registerCallback);\n    });\n  });\n  it(\"should unhide appmenu button on _showAppmenuButton()\", async () => {\n    await instance._showAppmenuButton(fakeWindow);\n\n    assert.calledWith(fakeElementById.removeAttribute, \"hidden\");\n  });\n  it(\"should hide appmenu button on _hideAppmenuButton()\", () => {\n    instance._hideAppmenuButton(fakeWindow);\n    assert.calledWith(fakeElementById.setAttribute, \"hidden\", true);\n  });\n  it(\"should unhide toolbar button on _showToolbarButton()\", async () => {\n    await instance._showToolbarButton(fakeWindow);\n\n    assert.calledWith(fakeElementById.removeAttribute, \"hidden\");\n  });\n  it(\"should hide toolbar button on _hideToolbarButton()\", () => {\n    instance._hideToolbarButton(fakeWindow);\n    assert.calledWith(fakeElementById.setAttribute, \"hidden\", true);\n  });\n  describe(\"#renderMessages\", () => {\n    let getMessagesStub;\n    beforeEach(() => {\n      getMessagesStub = sandbox.stub();\n      instance.init(waitForInitializedStub, {\n        getMessages: getMessagesStub,\n        dispatch: fakeDispatch,\n        handleUserAction: handleUserActionStub,\n      });\n    });\n    it(\"should render messages to the panel on renderMessages()\", async () => {\n      const messages = (await PanelTestProvider.getMessages()).filter(\n        m => m.template === \"whatsnew_panel_message\"\n      );\n      messages[0].content.link_text = { string_id: \"link_text_id\" };\n\n      getMessagesStub.returns(messages);\n      const ev1 = sandbox.stub();\n      ev1.withArgs(\"type\").returns(1); // tracker\n      ev1.withArgs(\"count\").returns(4);\n      const ev2 = sandbox.stub();\n      ev2.withArgs(\"type\").returns(4); // fingerprinter\n      ev2.withArgs(\"count\").returns(3);\n      getEventsByDateRangeStub.returns([\n        { getResultByName: ev1 },\n        { getResultByName: ev2 },\n      ]);\n\n      await instance.renderMessages(fakeWindow, fakeDocument, \"container-id\");\n\n      for (let message of messages) {\n        assert.ok(createdElements.find(el => el.tagName === \"h2\"));\n        if (message.content.layout === \"tracking-protections\") {\n          assert.ok(createdElements.find(el => el.tagName === \"h4\"));\n        }\n        if (message.id === \"WHATS_NEW_FINGERPRINTER_COUNTER_72\") {\n          assert.ok(createdElements.find(el => el.tagName === \"h4\"));\n          assert.ok(\n            createdElements.find(\n              el => el.tagName === \"h2\" && el.textContent === 3\n            )\n          );\n        }\n        assert.ok(createdElements.find(el => el.tagName === \"p\"));\n      }\n      // Call the click handler to make coverage happy.\n      eventListeners.mouseup();\n      assert.calledOnce(handleUserActionStub);\n    });\n    it(\"should clear previous messages on 2nd renderMessages()\", async () => {\n      const messages = (await PanelTestProvider.getMessages()).filter(\n        m => m.template === \"whatsnew_panel_message\"\n      );\n      fakeElementById.querySelectorAll.onCall(0).returns([]);\n      fakeElementById.querySelectorAll.onCall(1).returns([\"a\", \"b\", \"c\"]);\n\n      getMessagesStub.returns(messages);\n\n      await instance.renderMessages(fakeWindow, fakeDocument, \"container-id\");\n      await instance.renderMessages(fakeWindow, fakeDocument, \"container-id\");\n\n      assert.calledThrice(fakeElementById.removeChild);\n      assert.equal(fakeElementById.removeChild.firstCall.args[0], \"a\");\n      assert.equal(fakeElementById.removeChild.secondCall.args[0], \"b\");\n    });\n    it(\"should sort based on order field value\", async () => {\n      const messages = (await PanelTestProvider.getMessages()).filter(\n        m =>\n          m.template === \"whatsnew_panel_message\" &&\n          m.content.published_date === 1560969794394\n      );\n\n      messages.forEach(m => (m.content.title = m.order));\n\n      getMessagesStub.returns(messages);\n\n      await instance.renderMessages(fakeWindow, fakeDocument, \"container-id\");\n\n      // Select the title elements that are supposed to be set to the same\n      // value as the `order` field of the message\n      const titleEls = createdElements\n        .filter(\n          el =>\n            el.classList.add.firstCall &&\n            el.classList.add.firstCall.args[0] === \"whatsNew-message-title\"\n        )\n        .map(el => el.textContent);\n      assert.deepEqual(titleEls, [1, 2, 3]);\n    });\n    it(\"should accept string for image attributes\", async () => {\n      const messages = (await PanelTestProvider.getMessages()).filter(\n        m => m.id === \"WHATS_NEW_70_1\"\n      );\n      getMessagesStub.returns(messages);\n\n      await instance.renderMessages(fakeWindow, fakeDocument, \"container-id\");\n\n      const imageEl = createdElements.find(el => el.tagName === \"img\");\n      assert.calledOnce(imageEl.setAttribute);\n      assert.calledWithExactly(\n        imageEl.setAttribute,\n        \"alt\",\n        \"Firefox Send Logo\"\n      );\n    });\n    it(\"should accept fluent ids for image attributes\", async () => {\n      const messages = (await PanelTestProvider.getMessages()).filter(\n        m => m.id === \"WHATS_NEW_70_1\"\n      );\n      messages[0].content.icon_alt = { string_id: \"foo\" };\n      getMessagesStub.returns(messages);\n\n      await instance.renderMessages(fakeWindow, fakeDocument, \"container-id\");\n\n      assert.calledWithExactly(global.RemoteL10n.l10n.formatMessages, [\n        {\n          id: \"foo\",\n          args: instance.state.contentArguments,\n        },\n      ]);\n    });\n    it(\"handle fluent attributes\", async () => {\n      const messages = (await PanelTestProvider.getMessages()).filter(\n        m => m.id === \"WHATS_NEW_70_1\"\n      );\n      messages[0].content.icon_alt = { string_id: \"foo\" };\n      getMessagesStub.returns(messages);\n      global.RemoteL10n.l10n.formatMessages\n        .withArgs([{ id: \"foo\", args: sinon.match.object }])\n        .resolves([{ attributes: [{ name: \"alt\", value: \"bar\" }] }]);\n\n      await instance.renderMessages(fakeWindow, fakeDocument, \"container-id\");\n      const imgEl = createdElements.find(e => e.tagName === \"img\");\n\n      assert.calledWithExactly(imgEl.setAttribute, \"alt\", \"bar\");\n    });\n    it(\"should accept fluent ids for elements attributes\", async () => {\n      const [message] = (await PanelTestProvider.getMessages()).filter(\n        m =>\n          m.template === \"whatsnew_panel_message\" &&\n          m.content.layout === \"tracking-protections\"\n      );\n      getMessagesStub.returns([message]);\n      instance.state.contentArguments = { foo: \"foo\", bar: \"bar\" };\n\n      await instance.renderMessages(fakeWindow, fakeDocument, \"container-id\");\n\n      assert.calledWithExactly(global.RemoteL10n.l10n.formatMessages, [\n        {\n          id: message.content.subtitle.string_id,\n          args: instance.state.contentArguments,\n        },\n      ]);\n    });\n    it(\"should correctly compute blocker trackers and date\", async () => {\n      const messages = (await PanelTestProvider.getMessages()).filter(\n        m => m.template === \"whatsnew_panel_message\"\n      );\n      getMessagesStub.returns(messages);\n      const ev1 = sandbox.stub();\n      ev1.withArgs(\"type\").returns(2); // cookie\n      ev1.withArgs(\"count\").returns(4);\n      const ev2 = sandbox.stub();\n      ev2.withArgs(\"type\").returns(2); // cookie\n      ev2.withArgs(\"count\").returns(3);\n      getEventsByDateRangeStub.returns([\n        { getResultByName: ev1 },\n        { getResultByName: ev2 },\n      ]);\n\n      await instance.renderMessages(fakeWindow, fakeDocument, \"container-id\");\n\n      assert.calledWithExactly(global.RemoteL10n.l10n.formatMessages, [\n        {\n          id: sinon.match.string,\n          args: {\n            blockedCount: 7,\n            earliestDate: getEarliestRecordedDateStub(),\n            cookieCount: 7,\n            cryptominerCount: 0,\n            socialCount: 0,\n            trackerCount: 0,\n            fingerprinterCount: 0,\n          },\n        },\n      ]);\n    });\n    it(\"should correctly compute event counts per type\", async () => {\n      const messages = (await PanelTestProvider.getMessages()).filter(\n        m => m.template === \"whatsnew_panel_message\"\n      );\n      getMessagesStub.returns(messages);\n      const ev1 = sandbox.stub();\n      ev1.withArgs(\"type\").returns(1); // tracker\n      ev1.withArgs(\"count\").returns(4);\n      const ev2 = sandbox.stub();\n      ev2.withArgs(\"type\").returns(4); // fingerprinter\n      ev2.withArgs(\"count\").returns(3);\n      getEventsByDateRangeStub.returns([\n        { getResultByName: ev1 },\n        { getResultByName: ev2 },\n      ]);\n\n      await instance.renderMessages(fakeWindow, fakeDocument, \"container-id\");\n\n      assert.calledWithExactly(global.RemoteL10n.l10n.formatMessages, [\n        {\n          id: sinon.match.string,\n          args: {\n            blockedCount: 7,\n            earliestDate: getEarliestRecordedDateStub(),\n            trackerCount: 4,\n            fingerprinterCount: 3,\n            cookieCount: 0,\n            cryptominerCount: 0,\n            socialCount: 0,\n          },\n        },\n      ]);\n    });\n    it(\"should only render unique dates (no duplicates)\", async () => {\n      const messages = (await PanelTestProvider.getMessages()).filter(\n        m => m.template === \"whatsnew_panel_message\"\n      );\n      const uniqueDates = [\n        ...new Set(messages.map(m => m.content.published_date)),\n      ];\n      getMessagesStub.returns(messages);\n\n      await instance.renderMessages(fakeWindow, fakeDocument, \"container-id\");\n\n      const dateElements = createdElements.filter(\n        el =>\n          el.tagName === \"p\" &&\n          el.classList.add.firstCall &&\n          el.classList.add.firstCall.args[0] === \"whatsNew-message-date\"\n      );\n      assert.lengthOf(dateElements, uniqueDates.length);\n    });\n    it(\"should listen for panelhidden and remove the toolbar button\", async () => {\n      getMessagesStub.returns([]);\n      fakeDocument.getElementById\n        .withArgs(\"customizationui-widget-panel\")\n        .returns(null);\n\n      await instance.renderMessages(fakeWindow, fakeDocument, \"container-id\");\n\n      assert.notCalled(fakeElementById.addEventListener);\n    });\n    it(\"should attach doCommand cbs that handle user actions\", async () => {\n      const messages = (await PanelTestProvider.getMessages()).filter(\n        m => m.template === \"whatsnew_panel_message\"\n      );\n      getMessagesStub.returns(messages);\n\n      await instance.renderMessages(fakeWindow, fakeDocument, \"container-id\");\n\n      const buttonEl = createdElements.find(el => el.tagName === \"button\");\n      const anchorEl = createdElements.find(el => el.tagName === \"a\");\n\n      assert.notCalled(handleUserActionStub);\n\n      buttonEl.doCommand();\n      anchorEl.doCommand();\n\n      assert.calledTwice(handleUserActionStub);\n    });\n    it(\"should listen for panelhidden and remove the toolbar button\", async () => {\n      getMessagesStub.returns([]);\n\n      await instance.renderMessages(fakeWindow, fakeDocument, \"container-id\");\n\n      assert.calledOnce(fakeElementById.addEventListener);\n      assert.calledWithExactly(\n        fakeElementById.addEventListener,\n        \"popuphidden\",\n        sinon.match.func,\n        {\n          once: true,\n        }\n      );\n      const [, cb] = fakeElementById.addEventListener.firstCall.args;\n\n      assert.notCalled(everyWindowStub.unregisterCallback);\n\n      cb();\n\n      assert.calledOnce(everyWindowStub.unregisterCallback);\n      assert.calledWithExactly(\n        everyWindowStub.unregisterCallback,\n        \"whats-new-menu-button\"\n      );\n    });\n    describe(\"#IMPRESSION\", () => {\n      it(\"should dispatch a IMPRESSION for messages\", async () => {\n        // means panel is triggered from the toolbar button\n        fakeElementById.hasAttribute.returns(true);\n        const messages = (await PanelTestProvider.getMessages()).filter(\n          m => m.template === \"whatsnew_panel_message\"\n        );\n        getMessagesStub.returns(messages);\n        const spy = sandbox.spy(instance, \"sendUserEventTelemetry\");\n\n        await instance.renderMessages(fakeWindow, fakeDocument, \"container-id\");\n\n        assert.calledOnce(spy);\n        assert.calledOnce(fakeDispatch);\n        assert.propertyVal(\n          spy.firstCall.args[2],\n          \"id\",\n          messages\n            .map(({ id }) => id)\n            .sort()\n            .join(\",\")\n        );\n      });\n      it(\"should dispatch a CLICK for clicking a message\", async () => {\n        // means panel is triggered from the toolbar button\n        fakeElementById.hasAttribute.returns(true);\n        // Force to render the message\n        fakeElementById.querySelector.returns(null);\n        const messages = (await PanelTestProvider.getMessages()).filter(\n          m => m.template === \"whatsnew_panel_message\"\n        );\n        getMessagesStub.returns([messages[0]]);\n        const spy = sandbox.spy(instance, \"sendUserEventTelemetry\");\n\n        await instance.renderMessages(fakeWindow, fakeDocument, \"container-id\");\n\n        assert.calledOnce(spy);\n        assert.calledOnce(fakeDispatch);\n\n        spy.resetHistory();\n\n        // Message click event listener cb\n        eventListeners.mouseup();\n\n        assert.calledOnce(spy);\n        assert.calledWithExactly(spy, fakeWindow, \"CLICK\", messages[0]);\n      });\n      it(\"should dispatch a IMPRESSION with toolbar_dropdown\", async () => {\n        // means panel is triggered from the toolbar button\n        fakeElementById.hasAttribute.returns(true);\n        const messages = (await PanelTestProvider.getMessages()).filter(\n          m => m.template === \"whatsnew_panel_message\"\n        );\n        getMessagesStub.resolves(messages);\n        const spy = sandbox.spy(instance, \"sendUserEventTelemetry\");\n        const panelPingId = messages\n          .map(({ id }) => id)\n          .sort()\n          .join(\",\");\n\n        await instance.renderMessages(fakeWindow, fakeDocument, \"container-id\");\n\n        assert.calledOnce(spy);\n        assert.calledWithExactly(\n          spy,\n          fakeWindow,\n          \"IMPRESSION\",\n          {\n            id: panelPingId,\n          },\n          {\n            value: {\n              view: \"toolbar_dropdown\",\n            },\n          }\n        );\n        assert.calledOnce(fakeDispatch);\n        const {\n          args: [dispatchPayload],\n        } = fakeDispatch.lastCall;\n        assert.propertyVal(dispatchPayload, \"type\", \"TOOLBAR_PANEL_TELEMETRY\");\n        assert.propertyVal(dispatchPayload.data, \"message_id\", panelPingId);\n        assert.deepEqual(dispatchPayload.data.event_context, {\n          view: \"toolbar_dropdown\",\n        });\n      });\n      it(\"should dispatch a IMPRESSION with application_menu\", async () => {\n        // means panel is triggered as a subview in the application menu\n        fakeElementById.hasAttribute.returns(false);\n        const messages = (await PanelTestProvider.getMessages()).filter(\n          m => m.template === \"whatsnew_panel_message\"\n        );\n        getMessagesStub.resolves(messages);\n        const spy = sandbox.spy(instance, \"sendUserEventTelemetry\");\n        const panelPingId = messages\n          .map(({ id }) => id)\n          .sort()\n          .join(\",\");\n\n        await instance.renderMessages(fakeWindow, fakeDocument, \"container-id\");\n\n        assert.calledOnce(spy);\n        assert.calledWithExactly(\n          spy,\n          fakeWindow,\n          \"IMPRESSION\",\n          {\n            id: panelPingId,\n          },\n          {\n            value: {\n              view: \"application_menu\",\n            },\n          }\n        );\n        assert.calledOnce(fakeDispatch);\n        const {\n          args: [dispatchPayload],\n        } = fakeDispatch.lastCall;\n        assert.propertyVal(dispatchPayload, \"type\", \"TOOLBAR_PANEL_TELEMETRY\");\n        assert.propertyVal(dispatchPayload.data, \"message_id\", panelPingId);\n        assert.deepEqual(dispatchPayload.data.event_context, {\n          view: \"application_menu\",\n        });\n      });\n    });\n    describe(\"#forceShowMessage\", () => {\n      const panelSelector = \"PanelUI-whatsNew-message-container\";\n      let removeMessagesSpy;\n      let renderMessagesStub;\n      let addEventListenerStub;\n      let message;\n      let browser;\n      beforeEach(async () => {\n        message = (await PanelTestProvider.getMessages()).find(\n          m => m.id === \"WHATS_NEW_70_1\"\n        );\n        removeMessagesSpy = sandbox.spy(instance, \"removeMessages\");\n        renderMessagesStub = sandbox.spy(instance, \"renderMessages\");\n        addEventListenerStub = fakeElementById.addEventListener;\n        browser = {\n          browser: { ownerGlobal: fakeWindow, ownerDocument: fakeDocument },\n        };\n        fakeElementById.querySelectorAll.returns([fakeElementById]);\n      });\n      it(\"should call removeMessages when forcing a message to show\", () => {\n        instance.forceShowMessage(browser, message);\n\n        assert.calledOnce(removeMessagesSpy);\n        assert.calledWithExactly(removeMessagesSpy, fakeWindow, panelSelector);\n      });\n      it(\"should call renderMessages when forcing a message to show\", () => {\n        instance.forceShowMessage(browser, message);\n\n        assert.calledOnce(renderMessagesStub);\n        assert.calledWithExactly(\n          renderMessagesStub,\n          fakeWindow,\n          fakeDocument,\n          panelSelector,\n          {\n            force: true,\n            messages: [message],\n          }\n        );\n      });\n      it(\"should cleanup after the panel is hidden when forcing a message to show\", () => {\n        instance.forceShowMessage(browser, message);\n\n        assert.calledOnce(addEventListenerStub);\n        assert.calledWithExactly(\n          addEventListenerStub,\n          \"popuphidden\",\n          sinon.match.func\n        );\n\n        const [, cb] = addEventListenerStub.firstCall.args;\n        // Reset the call count from the first `forceShowMessage` call\n        removeMessagesSpy.resetHistory();\n        cb({ target: { ownerGlobal: fakeWindow } });\n\n        assert.calledOnce(removeMessagesSpy);\n        assert.calledWithExactly(removeMessagesSpy, fakeWindow, panelSelector);\n      });\n    });\n  });\n  describe(\"#insertProtectionPanelMessage\", () => {\n    const fakeInsert = () =>\n      instance.insertProtectionPanelMessage({\n        target: { ownerGlobal: fakeWindow, ownerDocument: fakeDocument },\n      });\n    let getMessagesStub;\n    beforeEach(async () => {\n      const onboardingMsgs = await OnboardingMessageProvider.getUntranslatedMessages();\n      getMessagesStub = sandbox\n        .stub()\n        .resolves(\n          onboardingMsgs.find(msg => msg.template === \"protections_panel\")\n        );\n      await instance.init(waitForInitializedStub, {\n        dispatch: fakeDispatch,\n        getMessages: getMessagesStub,\n        handleUserAction: handleUserActionStub,\n      });\n    });\n    it(\"should remember it showed\", async () => {\n      await fakeInsert();\n\n      assert.calledWithExactly(\n        setBoolPrefStub,\n        \"browser.protections_panel.infoMessage.seen\",\n        true\n      );\n    });\n    it(\"should toggle/expand when default collapsed/disabled\", async () => {\n      fakeElementById.hasAttribute.returns(true);\n\n      await fakeInsert();\n\n      assert.calledThrice(fakeElementById.toggleAttribute);\n    });\n    it(\"should toggle again when popup hides\", async () => {\n      fakeElementById.addEventListener.callsArg(1);\n\n      await fakeInsert();\n\n      assert.callCount(fakeElementById.toggleAttribute, 6);\n    });\n    it(\"should open link on click (separate link element)\", async () => {\n      const sendTelemetryStub = sandbox.stub(\n        instance,\n        \"sendUserEventTelemetry\"\n      );\n      const onboardingMsgs = await OnboardingMessageProvider.getUntranslatedMessages();\n      const msg = onboardingMsgs.find(m => m.template === \"protections_panel\");\n\n      await fakeInsert();\n\n      assert.calledOnce(sendTelemetryStub);\n      assert.calledWithExactly(\n        sendTelemetryStub,\n        fakeWindow,\n        \"IMPRESSION\",\n        msg\n      );\n\n      eventListeners.mouseup();\n\n      assert.calledOnce(handleUserActionStub);\n      assert.calledWithExactly(handleUserActionStub, {\n        target: fakeWindow,\n        data: {\n          type: \"OPEN_URL\",\n          data: {\n            args: sinon.match.string,\n            where: \"tabshifted\",\n          },\n        },\n      });\n    });\n    it(\"should format the url\", async () => {\n      const stub = sandbox\n        .stub(global.Services.urlFormatter, \"formatURL\")\n        .returns(\"formattedURL\");\n      const onboardingMsgs = await OnboardingMessageProvider.getUntranslatedMessages();\n      const msg = onboardingMsgs.find(m => m.template === \"protections_panel\");\n\n      await fakeInsert();\n\n      eventListeners.mouseup();\n\n      assert.calledOnce(stub);\n      assert.calledWithExactly(stub, msg.content.cta_url);\n      assert.calledOnce(handleUserActionStub);\n      assert.calledWithExactly(handleUserActionStub, {\n        target: fakeWindow,\n        data: {\n          type: \"OPEN_URL\",\n          data: {\n            args: \"formattedURL\",\n            where: \"tabshifted\",\n          },\n        },\n      });\n    });\n    it(\"should report format url errors\", async () => {\n      const stub = sandbox\n        .stub(global.Services.urlFormatter, \"formatURL\")\n        .throws();\n      const onboardingMsgs = await OnboardingMessageProvider.getUntranslatedMessages();\n      const msg = onboardingMsgs.find(m => m.template === \"protections_panel\");\n      sandbox.spy(global.Cu, \"reportError\");\n\n      await fakeInsert();\n\n      eventListeners.mouseup();\n\n      assert.calledOnce(stub);\n      assert.calledOnce(global.Cu.reportError);\n      assert.calledOnce(handleUserActionStub);\n      assert.calledWithExactly(handleUserActionStub, {\n        target: fakeWindow,\n        data: {\n          type: \"OPEN_URL\",\n          data: {\n            args: msg.content.cta_url,\n            where: \"tabshifted\",\n          },\n        },\n      });\n    });\n    it(\"should open link on click (directly attached to the message)\", async () => {\n      const onboardingMsgs = await OnboardingMessageProvider.getUntranslatedMessages();\n      const msg = onboardingMsgs.find(m => m.template === \"protections_panel\");\n      getMessagesStub.resolves({\n        ...msg,\n        content: { ...msg.content, link_text: null },\n      });\n      await fakeInsert();\n\n      eventListeners.mouseup();\n\n      assert.calledOnce(handleUserActionStub);\n      assert.calledWithExactly(handleUserActionStub, {\n        target: fakeWindow,\n        data: {\n          type: \"OPEN_URL\",\n          data: {\n            args: sinon.match.string,\n            where: \"tabshifted\",\n          },\n        },\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/lib/TopSitesFeed.test.js",
    "content": "\"use strict\";\n\nimport { actionCreators as ac, actionTypes as at } from \"common/Actions.jsm\";\nimport { FakePrefs, GlobalOverrider } from \"test/unit/utils\";\nimport {\n  insertPinned,\n  TOP_SITES_DEFAULT_ROWS,\n  TOP_SITES_MAX_SITES_PER_ROW,\n} from \"common/Reducers.jsm\";\nimport { getDefaultOptions } from \"lib/ActivityStreamStorage.jsm\";\nimport injector from \"inject!lib/TopSitesFeed.jsm\";\nimport { Screenshots } from \"lib/Screenshots.jsm\";\n\nconst FAKE_FAVICON = \"data987\";\nconst FAKE_FAVICON_SIZE = 128;\nconst FAKE_FRECENCY = 200;\nconst FAKE_LINKS = new Array(2 * TOP_SITES_MAX_SITES_PER_ROW)\n  .fill(null)\n  .map((v, i) => ({\n    frecency: FAKE_FRECENCY,\n    url: `http://www.site${i}.com`,\n  }));\nconst FAKE_SCREENSHOT = \"data123\";\nconst SEARCH_SHORTCUTS_EXPERIMENT_PREF = \"improvesearch.topSiteSearchShortcuts\";\nconst SEARCH_SHORTCUTS_SEARCH_ENGINES_PREF =\n  \"improvesearch.topSiteSearchShortcuts.searchEngines\";\nconst SEARCH_SHORTCUTS_HAVE_PINNED_PREF =\n  \"improvesearch.topSiteSearchShortcuts.havePinned\";\n\nfunction FakeTippyTopProvider() {}\nFakeTippyTopProvider.prototype = {\n  async init() {\n    this.initialized = true;\n  },\n  processSite(site) {\n    return site;\n  },\n};\n\ndescribe(\"Top Sites Feed\", () => {\n  let TopSitesFeed;\n  let DEFAULT_TOP_SITES;\n  let feed;\n  let globals;\n  let sandbox;\n  let links;\n  let fakeNewTabUtils;\n  let fakeScreenshot;\n  let filterAdultStub;\n  let shortURLStub;\n  let fakePageThumbs;\n\n  beforeEach(() => {\n    globals = new GlobalOverrider();\n    sandbox = globals.sandbox;\n    fakeNewTabUtils = {\n      blockedLinks: {\n        links: [],\n        isBlocked: () => false,\n        unblock: sandbox.spy(),\n      },\n      activityStreamLinks: {\n        getTopSites: sandbox.spy(() => Promise.resolve(links)),\n      },\n      activityStreamProvider: {\n        _addFavicons: sandbox.spy(l =>\n          Promise.resolve(\n            l.map(link => {\n              link.favicon = FAKE_FAVICON;\n              link.faviconSize = FAKE_FAVICON_SIZE;\n              return link;\n            })\n          )\n        ),\n        _faviconBytesToDataURI: sandbox.spy(),\n      },\n      pinnedLinks: {\n        links: [],\n        isPinned: () => false,\n        pin: sandbox.spy(),\n        unpin: sandbox.spy(),\n      },\n    };\n    fakeScreenshot = {\n      getScreenshotForURL: sandbox.spy(() => Promise.resolve(FAKE_SCREENSHOT)),\n      maybeCacheScreenshot: sandbox.spy(Screenshots.maybeCacheScreenshot),\n      _shouldGetScreenshots: sinon.stub().returns(true),\n    };\n    filterAdultStub = sinon.stub().returns([]);\n    shortURLStub = sinon\n      .stub()\n      .callsFake(site =>\n        site.url.replace(/(.com|.ca)/, \"\").replace(\"https://\", \"\")\n      );\n    const fakeDedupe = function() {};\n    fakePageThumbs = {\n      addExpirationFilter: sinon.stub(),\n      removeExpirationFilter: sinon.stub(),\n    };\n    globals.set(\"PageThumbs\", fakePageThumbs);\n    globals.set(\"NewTabUtils\", fakeNewTabUtils);\n    sandbox.spy(global.XPCOMUtils, \"defineLazyGetter\");\n    FakePrefs.prototype.prefs[\"default.sites\"] = \"https://foo.com/\";\n    ({ TopSitesFeed, DEFAULT_TOP_SITES } = injector({\n      \"lib/ActivityStreamPrefs.jsm\": { Prefs: FakePrefs },\n      \"common/Dedupe.jsm\": { Dedupe: fakeDedupe },\n      \"common/Reducers.jsm\": {\n        insertPinned,\n        TOP_SITES_DEFAULT_ROWS,\n        TOP_SITES_MAX_SITES_PER_ROW,\n      },\n      \"lib/FilterAdult.jsm\": { filterAdult: filterAdultStub },\n      \"lib/Screenshots.jsm\": { Screenshots: fakeScreenshot },\n      \"lib/TippyTopProvider.jsm\": { TippyTopProvider: FakeTippyTopProvider },\n      \"lib/ShortURL.jsm\": { shortURL: shortURLStub },\n      \"lib/ActivityStreamStorage.jsm\": {\n        ActivityStreamStorage: function Fake() {},\n        getDefaultOptions,\n      },\n    }));\n    feed = new TopSitesFeed();\n    const storage = {\n      init: sandbox.stub().resolves(),\n      get: sandbox.stub().resolves(),\n      set: sandbox.stub().resolves(),\n    };\n    // Setup for tests that don't call `init` but require feed.storage\n    feed._storage = storage;\n    feed.store = {\n      dispatch: sinon.spy(),\n      getState() {\n        return this.state;\n      },\n      state: {\n        Prefs: { values: { filterAdult: false, topSitesRows: 2 } },\n        TopSites: { rows: Array(12).fill(\"site\") },\n      },\n      dbStorage: { getDbTable: sandbox.stub().returns(storage) },\n    };\n    feed.dedupe.group = (...sites) => sites;\n    links = FAKE_LINKS;\n    // Turn off the search shortcuts experiment by default for other tests\n    feed.store.state.Prefs.values[SEARCH_SHORTCUTS_EXPERIMENT_PREF] = false;\n    feed.store.state.Prefs.values[SEARCH_SHORTCUTS_HAVE_PINNED_PREF] =\n      \"google,amazon\";\n  });\n  afterEach(() => {\n    globals.restore();\n    sandbox.restore();\n  });\n\n  function stubFaviconsToUseScreenshots() {\n    fakeNewTabUtils.activityStreamProvider._addFavicons = sandbox.stub();\n  }\n\n  describe(\"#constructor\", () => {\n    it(\"should defineLazyGetter for _currentSearchHostname\", () => {\n      assert.calledOnce(global.XPCOMUtils.defineLazyGetter);\n      assert.calledWith(\n        global.XPCOMUtils.defineLazyGetter,\n        feed,\n        \"_currentSearchHostname\",\n        sinon.match.func\n      );\n    });\n  });\n\n  describe(\"#refreshDefaults\", () => {\n    it(\"should add defaults on PREFS_INITIAL_VALUES\", () => {\n      feed.onAction({\n        type: at.PREFS_INITIAL_VALUES,\n        data: { \"default.sites\": \"https://foo.com\" },\n      });\n\n      assert.isAbove(DEFAULT_TOP_SITES.length, 0);\n    });\n    it(\"should add defaults on default.sites PREF_CHANGED\", () => {\n      feed.onAction({\n        type: at.PREF_CHANGED,\n        data: { name: \"default.sites\", value: \"https://foo.com\" },\n      });\n\n      assert.isAbove(DEFAULT_TOP_SITES.length, 0);\n    });\n    it(\"should refresh on topSiteRows PREF_CHANGED\", () => {\n      feed.refresh = sinon.spy();\n      feed.onAction({ type: at.PREF_CHANGED, data: { name: \"topSitesRows\" } });\n\n      assert.calledOnce(feed.refresh);\n    });\n    it(\"should have default sites with .isDefault = true\", () => {\n      feed.refreshDefaults(\"https://foo.com\");\n\n      DEFAULT_TOP_SITES.forEach(link =>\n        assert.propertyVal(link, \"isDefault\", true)\n      );\n    });\n    it(\"should have default sites with appropriate hostname\", () => {\n      feed.refreshDefaults(\"https://foo.com\");\n\n      DEFAULT_TOP_SITES.forEach(link =>\n        assert.propertyVal(link, \"hostname\", shortURLStub(link))\n      );\n    });\n    it(\"should add no defaults on empty pref\", () => {\n      feed.refreshDefaults(\"\");\n\n      assert.equal(DEFAULT_TOP_SITES.length, 0);\n    });\n    it(\"should clear defaults\", () => {\n      feed.refreshDefaults(\"https://foo.com\");\n      feed.refreshDefaults(\"\");\n\n      assert.equal(DEFAULT_TOP_SITES.length, 0);\n    });\n  });\n  describe(\"#filterForThumbnailExpiration\", () => {\n    it(\"should pass rows.urls to the callback provided\", () => {\n      const rows = [\n        { url: \"foo.com\" },\n        { url: \"bar.com\", customScreenshotURL: \"custom\" },\n      ];\n      feed.store.state.TopSites = { rows };\n      const stub = sinon.stub();\n\n      feed.filterForThumbnailExpiration(stub);\n\n      assert.calledOnce(stub);\n      assert.calledWithExactly(stub, [\"foo.com\", \"bar.com\", \"custom\"]);\n    });\n  });\n  describe(\"#getLinksWithDefaults\", () => {\n    beforeEach(() => {\n      feed.refreshDefaults(\"https://foo.com\");\n    });\n\n    describe(\"general\", () => {\n      it(\"should get the links from NewTabUtils\", async () => {\n        const result = await feed.getLinksWithDefaults();\n        const reference = links.map(site =>\n          Object.assign({}, site, {\n            hostname: shortURLStub(site),\n            typedBonus: true,\n          })\n        );\n\n        assert.deepEqual(result, reference);\n        assert.calledOnce(global.NewTabUtils.activityStreamLinks.getTopSites);\n      });\n      it(\"should indicate the links get typed bonus\", async () => {\n        const result = await feed.getLinksWithDefaults();\n\n        assert.propertyVal(result[0], \"typedBonus\", true);\n      });\n      it(\"should not filter out adult sites when pref is false\", async () => {\n        await feed.getLinksWithDefaults();\n\n        assert.notCalled(filterAdultStub);\n      });\n      it(\"should filter out non-pinned adult sites when pref is true\", async () => {\n        feed.store.state.Prefs.values.filterAdult = true;\n        fakeNewTabUtils.pinnedLinks.links = [{ url: \"https://foo.com/\" }];\n\n        const result = await feed.getLinksWithDefaults();\n\n        // The stub filters out everything\n        assert.calledOnce(filterAdultStub);\n        assert.equal(result.length, 1);\n        assert.equal(result[0].url, fakeNewTabUtils.pinnedLinks.links[0].url);\n      });\n      it(\"should filter out the defaults that have been blocked\", async () => {\n        // make sure we only have one top site, and we block the only default site we have to show\n        const url = \"www.myonlytopsite.com\";\n        const topsite = {\n          frecency: FAKE_FRECENCY,\n          hostname: shortURLStub({ url }),\n          typedBonus: true,\n          url,\n        };\n        const blockedDefaultSite = { url: \"https://foo.com\" };\n        fakeNewTabUtils.activityStreamLinks.getTopSites = () => [topsite];\n        fakeNewTabUtils.blockedLinks.isBlocked = site =>\n          site.url === blockedDefaultSite.url;\n        const result = await feed.getLinksWithDefaults();\n\n        // what we should be left with is just the top site we added, and not the default site we blocked\n        assert.lengthOf(result, 1);\n        assert.deepEqual(result[0], topsite);\n        assert.notInclude(result, blockedDefaultSite);\n      });\n      it(\"should call dedupe on the links\", async () => {\n        const stub = sinon.stub(feed.dedupe, \"group\").callsFake((...id) => id);\n\n        await feed.getLinksWithDefaults();\n\n        assert.calledOnce(stub);\n      });\n      it(\"should dedupe the links by hostname\", async () => {\n        const site = { url: \"foo\", hostname: \"bar\" };\n        const result = feed._dedupeKey(site);\n\n        assert.equal(result, site.hostname);\n      });\n      it(\"should add defaults if there are are not enough links\", async () => {\n        links = [{ frecency: FAKE_FRECENCY, url: \"foo.com\" }];\n\n        const result = await feed.getLinksWithDefaults();\n        const reference = [...links, ...DEFAULT_TOP_SITES].map(s =>\n          Object.assign({}, s, {\n            hostname: shortURLStub(s),\n            typedBonus: true,\n          })\n        );\n\n        assert.deepEqual(result, reference);\n      });\n      it(\"should only add defaults up to the number of visible slots\", async () => {\n        links = [];\n        const numVisible = TOP_SITES_DEFAULT_ROWS * TOP_SITES_MAX_SITES_PER_ROW;\n        for (let i = 0; i < numVisible - 1; i++) {\n          links.push({ frecency: FAKE_FRECENCY, url: `foo${i}.com` });\n        }\n        const result = await feed.getLinksWithDefaults();\n        const reference = [...links, DEFAULT_TOP_SITES[0]].map(s =>\n          Object.assign({}, s, {\n            hostname: shortURLStub(s),\n            typedBonus: true,\n          })\n        );\n\n        assert.lengthOf(result, numVisible);\n        assert.deepEqual(result, reference);\n      });\n      it(\"should not throw if NewTabUtils returns null\", () => {\n        links = null;\n        assert.doesNotThrow(() => {\n          feed.getLinksWithDefaults();\n        });\n      });\n      it(\"should get more if the user has asked for more\", async () => {\n        links = new Array(4 * TOP_SITES_MAX_SITES_PER_ROW)\n          .fill(null)\n          .map((v, i) => ({\n            frecency: FAKE_FRECENCY,\n            url: `http://www.site${i}.com`,\n          }));\n        feed.store.state.Prefs.values.topSitesRows = 3;\n\n        const result = await feed.getLinksWithDefaults();\n\n        assert.propertyVal(\n          result,\n          \"length\",\n          feed.store.state.Prefs.values.topSitesRows *\n            TOP_SITES_MAX_SITES_PER_ROW\n        );\n      });\n    });\n    describe(\"caching\", () => {\n      it(\"should reuse the cache on subsequent calls\", async () => {\n        await feed.getLinksWithDefaults();\n        await feed.getLinksWithDefaults();\n\n        assert.calledOnce(global.NewTabUtils.activityStreamLinks.getTopSites);\n      });\n      it(\"should ignore the cache when requesting more\", async () => {\n        await feed.getLinksWithDefaults();\n        feed.store.state.Prefs.values.topSitesRows *= 3;\n\n        await feed.getLinksWithDefaults();\n\n        assert.calledTwice(global.NewTabUtils.activityStreamLinks.getTopSites);\n      });\n      it(\"should migrate frecent screenshot data without getting screenshots again\", async () => {\n        stubFaviconsToUseScreenshots();\n        await feed.getLinksWithDefaults();\n        const { callCount } = fakeScreenshot.getScreenshotForURL;\n        feed.frecentCache.expire();\n\n        const result = await feed.getLinksWithDefaults();\n\n        assert.calledTwice(global.NewTabUtils.activityStreamLinks.getTopSites);\n        assert.callCount(fakeScreenshot.getScreenshotForURL, callCount);\n        assert.propertyVal(result[0], \"screenshot\", FAKE_SCREENSHOT);\n      });\n      it(\"should migrate pinned favicon data without getting favicons again\", async () => {\n        fakeNewTabUtils.pinnedLinks.links = [{ url: \"https://foo.com/\" }];\n        await feed.getLinksWithDefaults();\n        const {\n          callCount,\n        } = fakeNewTabUtils.activityStreamProvider._addFavicons;\n        feed.pinnedCache.expire();\n\n        const result = await feed.getLinksWithDefaults();\n\n        assert.callCount(\n          fakeNewTabUtils.activityStreamProvider._addFavicons,\n          callCount\n        );\n        assert.propertyVal(result[0], \"favicon\", FAKE_FAVICON);\n        assert.propertyVal(result[0], \"faviconSize\", FAKE_FAVICON_SIZE);\n      });\n      it(\"should not expose internal link properties\", async () => {\n        const result = await feed.getLinksWithDefaults();\n\n        const internal = Object.keys(result[0]).filter(key =>\n          key.startsWith(\"__\")\n        );\n        assert.equal(internal.join(\"\"), \"\");\n      });\n      it(\"should copy the screenshot of the frecent site if pinned site doesn't have customScreenshotURL\", async () => {\n        links = [{ url: \"https://foo.com/\", screenshot: \"screenshot\" }];\n        fakeNewTabUtils.pinnedLinks.links = [{ url: \"https://foo.com/\" }];\n\n        const result = await feed.getLinksWithDefaults();\n\n        assert.equal(result[0].screenshot, links[0].screenshot);\n      });\n      it(\"should not copy the frecent screenshot if customScreenshotURL is set\", async () => {\n        links = [{ url: \"https://foo.com/\", screenshot: \"screenshot\" }];\n        fakeNewTabUtils.pinnedLinks.links = [\n          { url: \"https://foo.com/\", customScreenshotURL: \"custom\" },\n        ];\n\n        const result = await feed.getLinksWithDefaults();\n\n        assert.isUndefined(result[0].screenshot);\n      });\n      it(\"should keep the same screenshot if no frecent site is found\", async () => {\n        links = [];\n        fakeNewTabUtils.pinnedLinks.links = [\n          { url: \"https://foo.com/\", screenshot: \"custom\" },\n        ];\n\n        const result = await feed.getLinksWithDefaults();\n\n        assert.equal(result[0].screenshot, \"custom\");\n      });\n      it(\"should not overwrite pinned site screenshot\", async () => {\n        links = [{ url: \"https://foo.com/\", screenshot: \"foo\" }];\n        fakeNewTabUtils.pinnedLinks.links = [\n          { url: \"https://foo.com/\", screenshot: \"bar\" },\n        ];\n\n        const result = await feed.getLinksWithDefaults();\n\n        assert.equal(result[0].screenshot, \"bar\");\n      });\n      it(\"should not set searchTopSite from frecent site\", async () => {\n        links = [\n          {\n            url: \"https://foo.com/\",\n            searchTopSite: true,\n            screenshot: \"screenshot\",\n          },\n        ];\n        fakeNewTabUtils.pinnedLinks.links = [{ url: \"https://foo.com/\" }];\n\n        const result = await feed.getLinksWithDefaults();\n\n        assert.propertyVal(result[0], \"searchTopSite\", false);\n        // But it should copy over other properties\n        assert.propertyVal(result[0], \"screenshot\", \"screenshot\");\n      });\n      describe(\"concurrency\", () => {\n        beforeEach(() => {\n          stubFaviconsToUseScreenshots();\n          fakeScreenshot.getScreenshotForURL = sandbox\n            .stub()\n            .resolves(FAKE_SCREENSHOT);\n        });\n        afterEach(() => {\n          sandbox.restore();\n        });\n\n        const getTwice = () =>\n          Promise.all([\n            feed.getLinksWithDefaults(),\n            feed.getLinksWithDefaults(),\n          ]);\n\n        it(\"should call the backing data once\", async () => {\n          await getTwice();\n\n          assert.calledOnce(global.NewTabUtils.activityStreamLinks.getTopSites);\n        });\n        it(\"should get screenshots once per link\", async () => {\n          await getTwice();\n\n          assert.callCount(\n            fakeScreenshot.getScreenshotForURL,\n            FAKE_LINKS.length\n          );\n        });\n        it(\"should dispatch once per link screenshot fetched\", async () => {\n          feed._requestRichIcon = sinon.stub();\n          await getTwice();\n\n          assert.callCount(feed.store.dispatch, FAKE_LINKS.length);\n        });\n      });\n    });\n    describe(\"deduping\", () => {\n      beforeEach(() => {\n        ({ TopSitesFeed, DEFAULT_TOP_SITES } = injector({\n          \"lib/ActivityStreamPrefs.jsm\": { Prefs: FakePrefs },\n          \"common/Reducers.jsm\": {\n            insertPinned,\n            TOP_SITES_DEFAULT_ROWS,\n            TOP_SITES_MAX_SITES_PER_ROW,\n          },\n          \"lib/Screenshots.jsm\": { Screenshots: fakeScreenshot },\n        }));\n        sandbox.stub(global.Services.eTLD, \"getPublicSuffix\").returns(\"com\");\n        feed = Object.assign(new TopSitesFeed(), { store: feed.store });\n      });\n      it(\"should not dedupe pinned sites\", async () => {\n        fakeNewTabUtils.pinnedLinks.links = [\n          { url: \"https://developer.mozilla.org/en-US/docs/Web\" },\n          { url: \"https://developer.mozilla.org/en-US/docs/Learn\" },\n        ];\n\n        const sites = await feed.getLinksWithDefaults();\n\n        assert.lengthOf(sites, 2 * TOP_SITES_MAX_SITES_PER_ROW);\n        assert.equal(sites[0].url, fakeNewTabUtils.pinnedLinks.links[0].url);\n        assert.equal(sites[1].url, fakeNewTabUtils.pinnedLinks.links[1].url);\n        assert.equal(sites[0].hostname, sites[1].hostname);\n      });\n      it(\"should prefer pinned sites over links\", async () => {\n        fakeNewTabUtils.pinnedLinks.links = [\n          { url: \"https://developer.mozilla.org/en-US/docs/Web\" },\n          { url: \"https://developer.mozilla.org/en-US/docs/Learn\" },\n        ];\n        // These will be the frecent results.\n        links = [\n          { frecency: FAKE_FRECENCY, url: \"https://developer.mozilla.org/\" },\n          { frecency: FAKE_FRECENCY, url: \"https://www.mozilla.org/\" },\n        ];\n\n        const sites = await feed.getLinksWithDefaults();\n\n        // Expecting 3 links where there's 2 pinned and 1 www.mozilla.org, so\n        // the frecent with matching hostname as pinned is removed.\n        assert.lengthOf(sites, 3);\n        assert.equal(sites[0].url, fakeNewTabUtils.pinnedLinks.links[0].url);\n        assert.equal(sites[1].url, fakeNewTabUtils.pinnedLinks.links[1].url);\n        assert.equal(sites[2].url, links[1].url);\n      });\n      it(\"should return sites that have a title\", async () => {\n        // Simulate a pinned link with no title.\n        fakeNewTabUtils.pinnedLinks.links = [\n          { url: \"https://github.com/mozilla/activity-stream\" },\n        ];\n\n        const sites = await feed.getLinksWithDefaults();\n\n        for (const site of sites) {\n          assert.isDefined(site.hostname);\n        }\n      });\n      it(\"should check against null entries\", async () => {\n        fakeNewTabUtils.pinnedLinks.links = [null];\n\n        await feed.getLinksWithDefaults();\n      });\n    });\n    it(\"should call _fetchIcon for each link\", async () => {\n      sinon.spy(feed, \"_fetchIcon\");\n\n      const results = await feed.getLinksWithDefaults();\n\n      assert.callCount(feed._fetchIcon, results.length);\n      results.forEach(link => {\n        assert.calledWith(feed._fetchIcon, link);\n      });\n    });\n    it(\"should call _fetchScreenshot when customScreenshotURL is set\", async () => {\n      links = [];\n      fakeNewTabUtils.pinnedLinks.links = [\n        { url: \"https://foo.com\", customScreenshotURL: \"custom\" },\n      ];\n      sinon.stub(feed, \"_fetchScreenshot\");\n\n      await feed.getLinksWithDefaults();\n\n      assert.calledWith(feed._fetchScreenshot, sinon.match.object, \"custom\");\n    });\n  });\n  describe(\"#init\", () => {\n    it(\"should call refresh (broadcast:true)\", async () => {\n      sandbox.stub(feed, \"refresh\");\n\n      await feed.init();\n\n      assert.calledOnce(feed.refresh);\n      assert.calledWithExactly(feed.refresh, { broadcast: true });\n    });\n    it(\"should initialise the storage\", async () => {\n      await feed.init();\n\n      assert.calledOnce(feed.store.dbStorage.getDbTable);\n      assert.calledWithExactly(feed.store.dbStorage.getDbTable, \"sectionPrefs\");\n    });\n  });\n  describe(\"#refresh\", () => {\n    beforeEach(() => {\n      sandbox.stub(feed, \"_fetchIcon\");\n    });\n    it(\"should wait for tippytop to initialize\", async () => {\n      feed._tippyTopProvider.initialized = false;\n      sinon.stub(feed._tippyTopProvider, \"init\").resolves();\n\n      await feed.refresh();\n\n      assert.calledOnce(feed._tippyTopProvider.init);\n    });\n    it(\"should not init the tippyTopProvider if already initialized\", async () => {\n      feed._tippyTopProvider.initialized = true;\n      sinon.stub(feed._tippyTopProvider, \"init\").resolves();\n\n      await feed.refresh();\n\n      assert.notCalled(feed._tippyTopProvider.init);\n    });\n    it(\"should broadcast TOP_SITES_UPDATED\", async () => {\n      sinon.stub(feed, \"getLinksWithDefaults\").returns(Promise.resolve([]));\n\n      await feed.refresh({ broadcast: true });\n\n      assert.calledOnce(feed.store.dispatch);\n      assert.calledWithExactly(\n        feed.store.dispatch,\n        ac.BroadcastToContent({\n          type: at.TOP_SITES_UPDATED,\n          data: { links: [], pref: { collapsed: false } },\n        })\n      );\n    });\n    it(\"should dispatch an action with the links returned\", async () => {\n      await feed.refresh({ broadcast: true });\n      const reference = links.map(site =>\n        Object.assign({}, site, {\n          hostname: shortURLStub(site),\n          typedBonus: true,\n        })\n      );\n\n      assert.calledOnce(feed.store.dispatch);\n      assert.propertyVal(\n        feed.store.dispatch.firstCall.args[0],\n        \"type\",\n        at.TOP_SITES_UPDATED\n      );\n      assert.deepEqual(\n        feed.store.dispatch.firstCall.args[0].data.links,\n        reference\n      );\n    });\n    it(\"should handle empty slots in the resulting top sites array\", async () => {\n      links = [FAKE_LINKS[0]];\n      fakeNewTabUtils.pinnedLinks.links = [\n        null,\n        null,\n        FAKE_LINKS[1],\n        null,\n        null,\n        null,\n        null,\n        null,\n        FAKE_LINKS[2],\n      ];\n      await feed.refresh({ broadcast: true });\n      assert.calledOnce(feed.store.dispatch);\n    });\n    it(\"should dispatch AlsoToPreloaded when broadcast is false\", async () => {\n      sandbox.stub(feed, \"getLinksWithDefaults\").returns([]);\n      await feed.refresh({ broadcast: false });\n\n      assert.calledOnce(feed.store.dispatch);\n      assert.calledWithExactly(\n        feed.store.dispatch,\n        ac.AlsoToPreloaded({\n          type: at.TOP_SITES_UPDATED,\n          data: { links: [], pref: { collapsed: false } },\n        })\n      );\n    });\n    it(\"should not init storage if it is already initialized\", async () => {\n      feed._storage.initialized = true;\n\n      await feed.refresh({ broadcast: false });\n\n      assert.notCalled(feed._storage.init);\n    });\n    it(\"should catch indexedDB errors\", async () => {\n      feed._storage.get.throws(new Error());\n      globals.sandbox.spy(global.Cu, \"reportError\");\n\n      try {\n        await feed.refresh({ broadcast: false });\n      } catch (e) {\n        assert.fails();\n      }\n\n      assert.calledOnce(Cu.reportError);\n    });\n  });\n  describe(\"#updateSectionPrefs\", () => {\n    it(\"should call updateSectionPrefs on UPDATE_SECTION_PREFS\", () => {\n      sandbox.stub(feed, \"updateSectionPrefs\");\n\n      feed.onAction({\n        type: at.UPDATE_SECTION_PREFS,\n        data: { id: \"topsites\" },\n      });\n\n      assert.calledOnce(feed.updateSectionPrefs);\n    });\n    it(\"should dispatch TOP_SITES_PREFS_UPDATED\", async () => {\n      await feed.updateSectionPrefs({ collapsed: true });\n\n      assert.calledOnce(feed.store.dispatch);\n      assert.calledWithExactly(\n        feed.store.dispatch,\n        ac.BroadcastToContent({\n          type: at.TOP_SITES_PREFS_UPDATED,\n          data: { pref: { collapsed: true } },\n        })\n      );\n    });\n  });\n  describe(\"#getScreenshotPreview\", () => {\n    it(\"should dispatch preview if request is succesful\", async () => {\n      await feed.getScreenshotPreview(\"custom\", 1234);\n\n      assert.calledOnce(feed.store.dispatch);\n      assert.calledWithExactly(\n        feed.store.dispatch,\n        ac.OnlyToOneContent(\n          {\n            data: { preview: FAKE_SCREENSHOT, url: \"custom\" },\n            type: at.PREVIEW_RESPONSE,\n          },\n          1234\n        )\n      );\n    });\n    it(\"should return empty string if request fails\", async () => {\n      fakeScreenshot.getScreenshotForURL = sandbox\n        .stub()\n        .returns(Promise.resolve(null));\n      await feed.getScreenshotPreview(\"custom\", 1234);\n\n      assert.calledOnce(feed.store.dispatch);\n      assert.calledWithExactly(\n        feed.store.dispatch,\n        ac.OnlyToOneContent(\n          {\n            data: { preview: \"\", url: \"custom\" },\n            type: at.PREVIEW_RESPONSE,\n          },\n          1234\n        )\n      );\n    });\n  });\n  describe(\"#_fetchIcon\", () => {\n    it(\"should reuse screenshot on the link\", () => {\n      const link = { screenshot: \"reuse.png\" };\n\n      feed._fetchIcon(link);\n\n      assert.notCalled(fakeScreenshot.getScreenshotForURL);\n      assert.propertyVal(link, \"screenshot\", \"reuse.png\");\n    });\n    it(\"should reuse existing fetching screenshot on the link\", async () => {\n      const link = {\n        __sharedCache: { fetchingScreenshot: Promise.resolve(\"fetching.png\") },\n      };\n\n      await feed._fetchIcon(link);\n\n      assert.notCalled(fakeScreenshot.getScreenshotForURL);\n    });\n    it(\"should get a screenshot if the link is missing it\", () => {\n      feed._fetchIcon(Object.assign({ __sharedCache: {} }, FAKE_LINKS[0]));\n\n      assert.calledOnce(fakeScreenshot.getScreenshotForURL);\n      assert.calledWith(fakeScreenshot.getScreenshotForURL, FAKE_LINKS[0].url);\n    });\n    it(\"should update the link's cache with a screenshot\", async () => {\n      const updateLink = sandbox.stub();\n      const link = { __sharedCache: { updateLink } };\n\n      await feed._fetchIcon(link);\n\n      assert.calledOnce(updateLink);\n      assert.calledWith(updateLink, \"screenshot\", FAKE_SCREENSHOT);\n    });\n    it(\"should skip getting a screenshot if there is a tippy top icon\", () => {\n      feed._tippyTopProvider.processSite = site => {\n        site.tippyTopIcon = \"icon.png\";\n        site.backgroundColor = \"#fff\";\n        return site;\n      };\n      const link = { url: \"example.com\" };\n      feed._fetchIcon(link);\n      assert.propertyVal(link, \"tippyTopIcon\", \"icon.png\");\n      assert.notProperty(link, \"screenshot\");\n      assert.notCalled(fakeScreenshot.getScreenshotForURL);\n    });\n    it(\"should skip getting a screenshot if there is an icon of size greater than 96x96 and no tippy top\", () => {\n      const link = {\n        url: \"foo.com\",\n        favicon: \"data:foo\",\n        faviconSize: 196,\n      };\n      feed._fetchIcon(link);\n      assert.notProperty(link, \"tippyTopIcon\");\n      assert.notProperty(link, \"screenshot\");\n      assert.notCalled(fakeScreenshot.getScreenshotForURL);\n    });\n    it(\"should use the link's rich icon even if there's a tippy top\", () => {\n      feed._tippyTopProvider.processSite = site => {\n        site.tippyTopIcon = \"icon.png\";\n        site.backgroundColor = \"#fff\";\n        return site;\n      };\n      const link = {\n        url: \"foo.com\",\n        favicon: \"data:foo\",\n        faviconSize: 196,\n      };\n      feed._fetchIcon(link);\n      assert.notProperty(link, \"tippyTopIcon\");\n    });\n  });\n  describe(\"#_fetchScreenshot\", () => {\n    it(\"should call maybeCacheScreenshot\", async () => {\n      const updateLink = sinon.stub();\n      const link = {\n        customScreenshotURL: \"custom\",\n        __sharedCache: { updateLink },\n      };\n      await feed._fetchScreenshot(link, \"custom\");\n\n      assert.calledOnce(fakeScreenshot.maybeCacheScreenshot);\n      assert.calledWithExactly(\n        fakeScreenshot.maybeCacheScreenshot,\n        link,\n        link.customScreenshotURL,\n        \"screenshot\",\n        sinon.match.func\n      );\n    });\n    it(\"should not call maybeCacheScreenshot if screenshot is set\", async () => {\n      const updateLink = sinon.stub();\n      const link = {\n        customScreenshotURL: \"custom\",\n        __sharedCache: { updateLink },\n        screenshot: true,\n      };\n      await feed._fetchScreenshot(link, \"custom\");\n\n      assert.notCalled(fakeScreenshot.maybeCacheScreenshot);\n    });\n  });\n  describe(\"#onAction\", () => {\n    it(\"should call getScreenshotPreview on PREVIEW_REQUEST\", () => {\n      sandbox.stub(feed, \"getScreenshotPreview\");\n\n      feed.onAction({\n        type: at.PREVIEW_REQUEST,\n        data: { url: \"foo\" },\n        meta: { fromTarget: 1234 },\n      });\n\n      assert.calledOnce(feed.getScreenshotPreview);\n      assert.calledWithExactly(feed.getScreenshotPreview, \"foo\", 1234);\n    });\n    it(\"should refresh on SYSTEM_TICK\", async () => {\n      sandbox.stub(feed, \"refresh\");\n\n      feed.onAction({ type: at.SYSTEM_TICK });\n\n      assert.calledOnce(feed.refresh);\n      assert.calledWithExactly(feed.refresh, { broadcast: false });\n    });\n    it(\"should call with correct parameters on TOP_SITES_PIN\", () => {\n      const pinAction = {\n        type: at.TOP_SITES_PIN,\n        data: { site: { url: \"foo.com\" }, index: 7 },\n      };\n      feed.onAction(pinAction);\n      assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin);\n      assert.calledWith(\n        fakeNewTabUtils.pinnedLinks.pin,\n        pinAction.data.site,\n        pinAction.data.index\n      );\n    });\n    it(\"should call pin on TOP_SITES_PIN\", () => {\n      sinon.stub(feed, \"pin\");\n      const pinExistingAction = {\n        type: at.TOP_SITES_PIN,\n        data: { site: FAKE_LINKS[4], index: 4 },\n      };\n\n      feed.onAction(pinExistingAction);\n\n      assert.calledOnce(feed.pin);\n    });\n    it(\"should trigger refresh on TOP_SITES_PIN\", async () => {\n      sinon.stub(feed, \"refresh\");\n      const pinExistingAction = {\n        type: at.TOP_SITES_PIN,\n        data: { site: FAKE_LINKS[4], index: 4 },\n      };\n\n      await feed.pin(pinExistingAction);\n\n      assert.calledOnce(feed.refresh);\n    });\n    it(\"should unblock a previously blocked top site if we are now adding it manually via 'Add a Top Site' option\", async () => {\n      const pinAction = {\n        type: at.TOP_SITES_PIN,\n        data: { site: { url: \"foo.com\" }, index: -1 },\n      };\n      feed.onAction(pinAction);\n      assert.calledWith(fakeNewTabUtils.blockedLinks.unblock, {\n        url: pinAction.data.site.url,\n      });\n    });\n    it(\"should call insert on TOP_SITES_INSERT\", async () => {\n      sinon.stub(feed, \"insert\");\n      const addAction = {\n        type: at.TOP_SITES_INSERT,\n        data: { site: { url: \"foo.com\" } },\n      };\n\n      feed.onAction(addAction);\n\n      assert.calledOnce(feed.insert);\n    });\n    it(\"should trigger refresh on TOP_SITES_INSERT\", async () => {\n      sinon.stub(feed, \"refresh\");\n      const addAction = {\n        type: at.TOP_SITES_INSERT,\n        data: { site: { url: \"foo.com\" } },\n      };\n\n      await feed.insert(addAction);\n\n      assert.calledOnce(feed.refresh);\n    });\n    it(\"should call unpin with correct parameters on TOP_SITES_UNPIN\", () => {\n      fakeNewTabUtils.pinnedLinks.links = [\n        null,\n        null,\n        { url: \"foo.com\" },\n        null,\n        null,\n        null,\n        null,\n        null,\n        FAKE_LINKS[0],\n      ];\n      const unpinAction = {\n        type: at.TOP_SITES_UNPIN,\n        data: { site: { url: \"foo.com\" } },\n      };\n      feed.onAction(unpinAction);\n      assert.calledOnce(fakeNewTabUtils.pinnedLinks.unpin);\n      assert.calledWith(\n        fakeNewTabUtils.pinnedLinks.unpin,\n        unpinAction.data.site\n      );\n    });\n    it(\"should call refresh without a target if we clear history with PLACES_HISTORY_CLEARED\", () => {\n      sandbox.stub(feed, \"refresh\");\n\n      feed.onAction({ type: at.PLACES_HISTORY_CLEARED });\n\n      assert.calledOnce(feed.refresh);\n      assert.calledWithExactly(feed.refresh, { broadcast: true });\n    });\n    it(\"should call refresh without a target if we remove a Topsite from history\", () => {\n      sandbox.stub(feed, \"refresh\");\n\n      feed.onAction({ type: at.PLACES_LINK_DELETED });\n\n      assert.calledOnce(feed.refresh);\n      assert.calledWithExactly(feed.refresh, { broadcast: true });\n    });\n    it(\"should still dispatch an action even if there's no target provided\", async () => {\n      sandbox.stub(feed, \"_fetchIcon\");\n      await feed.refresh({ broadcast: true });\n      assert.calledOnce(feed.store.dispatch);\n      assert.propertyVal(\n        feed.store.dispatch.firstCall.args[0],\n        \"type\",\n        at.TOP_SITES_UPDATED\n      );\n    });\n    it(\"should call init on INIT action\", async () => {\n      sinon.stub(feed, \"init\");\n      feed.onAction({ type: at.INIT });\n      assert.calledOnce(feed.init);\n    });\n    it(\"should call refresh on PLACES_LINK_BLOCKED action\", async () => {\n      sinon.stub(feed, \"refresh\");\n      await feed.onAction({ type: at.PLACES_LINK_BLOCKED });\n      assert.calledOnce(feed.refresh);\n      assert.calledWithExactly(feed.refresh, { broadcast: true });\n    });\n    it(\"should call refresh on PLACES_LINKS_CHANGED action\", async () => {\n      sinon.stub(feed, \"refresh\");\n      await feed.onAction({ type: at.PLACES_LINKS_CHANGED });\n      assert.calledOnce(feed.refresh);\n      assert.calledWithExactly(feed.refresh, { broadcast: false });\n    });\n    it(\"should call pin with correct args on TOP_SITES_INSERT without an index specified\", () => {\n      const addAction = {\n        type: at.TOP_SITES_INSERT,\n        data: { site: { url: \"foo.bar\", label: \"foo\" } },\n      };\n      feed.onAction(addAction);\n      assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin);\n      assert.calledWith(\n        fakeNewTabUtils.pinnedLinks.pin,\n        addAction.data.site,\n        0\n      );\n    });\n    it(\"should call pin with correct args on TOP_SITES_INSERT\", () => {\n      const dropAction = {\n        type: at.TOP_SITES_INSERT,\n        data: { site: { url: \"foo.bar\", label: \"foo\" }, index: 3 },\n      };\n      feed.onAction(dropAction);\n      assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin);\n      assert.calledWith(\n        fakeNewTabUtils.pinnedLinks.pin,\n        dropAction.data.site,\n        3\n      );\n    });\n    it(\"should remove the expiration filter on UNINIT\", () => {\n      feed.onAction({ type: \"UNINIT\" });\n\n      assert.calledOnce(fakePageThumbs.removeExpirationFilter);\n    });\n    it(\"should call updatePinnedSearchShortcuts on UPDATE_PINNED_SEARCH_SHORTCUTS action\", async () => {\n      sinon.stub(feed, \"updatePinnedSearchShortcuts\");\n      const addedShortcuts = [\n        {\n          url: \"https://google.com\",\n          searchVendor: \"google\",\n          label: \"google\",\n          searchTopSite: true,\n        },\n      ];\n      await feed.onAction({\n        type: at.UPDATE_PINNED_SEARCH_SHORTCUTS,\n        data: { addedShortcuts },\n      });\n      assert.calledOnce(feed.updatePinnedSearchShortcuts);\n    });\n  });\n  describe(\"#add\", () => {\n    it(\"should pin site in first slot of empty pinned list\", () => {\n      const site = { url: \"foo.bar\", label: \"foo\" };\n      feed.insert({ data: { site } });\n      assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin);\n      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 0);\n    });\n    it(\"should pin site in first slot of pinned list with empty first slot\", () => {\n      fakeNewTabUtils.pinnedLinks.links = [null, { url: \"example.com\" }];\n      const site = { url: \"foo.bar\", label: \"foo\" };\n      feed.insert({ data: { site } });\n      assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin);\n      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 0);\n    });\n    it(\"should move a pinned site in first slot to the next slot: part 1\", () => {\n      const site1 = { url: \"example.com\" };\n      fakeNewTabUtils.pinnedLinks.links = [site1];\n      const site = { url: \"foo.bar\", label: \"foo\" };\n      feed.insert({ data: { site } });\n      assert.calledTwice(fakeNewTabUtils.pinnedLinks.pin);\n      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 0);\n      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site1, 1);\n    });\n    it(\"should move a pinned site in first slot to the next slot: part 2\", () => {\n      const site1 = { url: \"example.com\" };\n      const site2 = { url: \"example.org\" };\n      fakeNewTabUtils.pinnedLinks.links = [site1, null, site2];\n      const site = { url: \"foo.bar\", label: \"foo\" };\n      feed.insert({ data: { site } });\n      assert.calledTwice(fakeNewTabUtils.pinnedLinks.pin);\n      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 0);\n      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site1, 1);\n    });\n    it(\"should unpin the last site if all slots are already pinned\", () => {\n      const site1 = { url: \"example.com\" };\n      const site2 = { url: \"example.org\" };\n      const site3 = { url: \"example.net\" };\n      const site4 = { url: \"example.biz\" };\n      const site5 = { url: \"example.info\" };\n      const site6 = { url: \"example.news\" };\n      const site7 = { url: \"example.lol\" };\n      const site8 = { url: \"example.golf\" };\n      fakeNewTabUtils.pinnedLinks.links = [\n        site1,\n        site2,\n        site3,\n        site4,\n        site5,\n        site6,\n        site7,\n        site8,\n      ];\n      feed.store.state.Prefs.values.topSitesRows = 1;\n      const site = { url: \"foo.bar\", label: \"foo\" };\n      feed.insert({ data: { site } });\n      assert.equal(fakeNewTabUtils.pinnedLinks.pin.callCount, 8);\n      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 0);\n      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site1, 1);\n      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site2, 2);\n      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site3, 3);\n      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site4, 4);\n      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site5, 5);\n      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site6, 6);\n      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site7, 7);\n    });\n  });\n  describe(\"#pin\", () => {\n    it(\"should pin site in specified slot empty pinned list\", async () => {\n      const site = {\n        url: \"foo.bar\",\n        label: \"foo\",\n        customScreenshotURL: \"screenshot\",\n      };\n      await feed.pin({ data: { index: 2, site } });\n      assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin);\n      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 2);\n    });\n    it(\"should lookup the link object to update the custom screenshot\", async () => {\n      const site = {\n        url: \"foo.bar\",\n        label: \"foo\",\n        customScreenshotURL: \"screenshot\",\n      };\n      sandbox.spy(feed.pinnedCache, \"request\");\n\n      await feed.pin({ data: { index: 2, site } });\n\n      assert.calledOnce(feed.pinnedCache.request);\n    });\n    it(\"should lookup the link object to update the custom screenshot\", async () => {\n      const site = { url: \"foo.bar\", label: \"foo\", customScreenshotURL: null };\n      sandbox.spy(feed.pinnedCache, \"request\");\n\n      await feed.pin({ data: { index: 2, site } });\n\n      assert.calledOnce(feed.pinnedCache.request);\n    });\n    it(\"should not do a link object lookup if custom screenshot field is not set\", async () => {\n      const site = { url: \"foo.bar\", label: \"foo\" };\n      sandbox.spy(feed.pinnedCache, \"request\");\n\n      await feed.pin({ data: { index: 2, site } });\n\n      assert.notCalled(feed.pinnedCache.request);\n    });\n    it(\"should pin site in specified slot of pinned list that is free\", () => {\n      fakeNewTabUtils.pinnedLinks.links = [null, { url: \"example.com\" }];\n      const site = { url: \"foo.bar\", label: \"foo\" };\n      feed.pin({ data: { index: 2, site } });\n      assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin);\n      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 2);\n    });\n    it(\"should save the searchTopSite attribute if set\", () => {\n      fakeNewTabUtils.pinnedLinks.links = [null, { url: \"example.com\" }];\n      const site = { url: \"foo.bar\", label: \"foo\", searchTopSite: true };\n      feed.pin({ data: { index: 2, site } });\n      assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin);\n      assert.propertyVal(\n        fakeNewTabUtils.pinnedLinks.pin.firstCall.args[0],\n        \"searchTopSite\",\n        true\n      );\n    });\n    it(\"should NOT move a pinned site in specified slot to the next slot\", () => {\n      fakeNewTabUtils.pinnedLinks.links = [null, null, { url: \"example.com\" }];\n      const site = { url: \"foo.bar\", label: \"foo\" };\n      feed.pin({ data: { index: 2, site } });\n      assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin);\n      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 2);\n    });\n    it(\"should properly update LinksCache object properties between migrations\", async () => {\n      fakeNewTabUtils.pinnedLinks.links = [{ url: \"https://foo.com/\" }];\n\n      let pinnedLinks = await feed.pinnedCache.request();\n      assert.equal(pinnedLinks.length, 1);\n      feed.pinnedCache.expire();\n      pinnedLinks[0].__sharedCache.updateLink(\"screenshot\", \"foo\");\n\n      pinnedLinks = await feed.pinnedCache.request();\n      assert.propertyVal(pinnedLinks[0], \"screenshot\", \"foo\");\n\n      // Force cache expiration in order to trigger a migration of objects\n      feed.pinnedCache.expire();\n      pinnedLinks[0].__sharedCache.updateLink(\"screenshot\", \"bar\");\n\n      pinnedLinks = await feed.pinnedCache.request();\n      assert.propertyVal(pinnedLinks[0], \"screenshot\", \"bar\");\n    });\n    it(\"should call insert if index < 0\", () => {\n      const site = { url: \"foo.bar\", label: \"foo\" };\n      const action = { data: { index: -1, site } };\n\n      sandbox.spy(feed, \"insert\");\n      feed.pin(action);\n\n      assert.calledOnce(feed.insert);\n      assert.calledWithExactly(feed.insert, action);\n    });\n    it(\"should not call insert if index == 0\", () => {\n      const site = { url: \"foo.bar\", label: \"foo\" };\n      const action = { data: { index: 0, site } };\n\n      sandbox.spy(feed, \"insert\");\n      feed.pin(action);\n\n      assert.notCalled(feed.insert);\n    });\n  });\n  describe(\"clearLinkCustomScreenshot\", () => {\n    it(\"should remove cached screenshot if custom url changes\", async () => {\n      const stub = sandbox.stub();\n      sandbox.stub(feed.pinnedCache, \"request\").returns(\n        Promise.resolve([\n          {\n            url: \"foo\",\n            customScreenshotURL: \"old_screenshot\",\n            __sharedCache: { updateLink: stub },\n          },\n        ])\n      );\n\n      await feed._clearLinkCustomScreenshot({\n        url: \"foo\",\n        customScreenshotURL: \"new_screenshot\",\n      });\n\n      assert.calledOnce(stub);\n      assert.calledWithExactly(stub, \"screenshot\", undefined);\n    });\n    it(\"should remove cached screenshot if custom url is removed\", async () => {\n      const stub = sandbox.stub();\n      sandbox.stub(feed.pinnedCache, \"request\").returns(\n        Promise.resolve([\n          {\n            url: \"foo\",\n            customScreenshotURL: \"old_screenshot\",\n            __sharedCache: { updateLink: stub },\n          },\n        ])\n      );\n\n      await feed._clearLinkCustomScreenshot({\n        url: \"foo\",\n        customScreenshotURL: \"new_screenshot\",\n      });\n\n      assert.calledOnce(stub);\n      assert.calledWithExactly(stub, \"screenshot\", undefined);\n    });\n  });\n  describe(\"#drop\", () => {\n    it(\"should correctly handle different index values\", () => {\n      let index = -1;\n      const site = { url: \"foo.bar\", label: \"foo\" };\n      const action = { data: { index, site } };\n\n      feed.insert(action);\n\n      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 0);\n\n      index = undefined;\n      feed.insert(action);\n\n      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 0);\n    });\n    it(\"should pin site in specified slot that is free\", () => {\n      fakeNewTabUtils.pinnedLinks.links = [null, { url: \"example.com\" }];\n      const site = { url: \"foo.bar\", label: \"foo\" };\n      feed.insert({ data: { index: 2, site, draggedFromIndex: 0 } });\n      assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin);\n      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 2);\n    });\n    it(\"should move a pinned site in specified slot to the next slot\", () => {\n      fakeNewTabUtils.pinnedLinks.links = [null, null, { url: \"example.com\" }];\n      const site = { url: \"foo.bar\", label: \"foo\" };\n      feed.insert({ data: { index: 2, site, draggedFromIndex: 3 } });\n      assert.calledTwice(fakeNewTabUtils.pinnedLinks.pin);\n      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site, 2);\n      assert.calledWith(\n        fakeNewTabUtils.pinnedLinks.pin,\n        { url: \"example.com\" },\n        3\n      );\n    });\n    it(\"should move pinned sites in the direction of the dragged site\", () => {\n      const site1 = { url: \"foo.bar\", label: \"foo\" };\n      const site2 = { url: \"example.com\", label: \"example\" };\n      fakeNewTabUtils.pinnedLinks.links = [null, null, site2];\n      feed.insert({ data: { index: 2, site: site1, draggedFromIndex: 0 } });\n      assert.calledTwice(fakeNewTabUtils.pinnedLinks.pin);\n      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site1, 2);\n      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site2, 1);\n      fakeNewTabUtils.pinnedLinks.pin.resetHistory();\n      feed.insert({ data: { index: 2, site: site1, draggedFromIndex: 5 } });\n      assert.calledTwice(fakeNewTabUtils.pinnedLinks.pin);\n      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site1, 2);\n      assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, site2, 3);\n    });\n    it(\"should not insert past the visible top sites\", () => {\n      const site1 = { url: \"foo.bar\", label: \"foo\" };\n      feed.insert({ data: { index: 42, site: site1, draggedFromIndex: 0 } });\n      assert.notCalled(fakeNewTabUtils.pinnedLinks.pin);\n    });\n  });\n  describe(\"integration\", () => {\n    let resolvers = [];\n    beforeEach(() => {\n      feed.store.dispatch = sandbox.stub().callsFake(() => {\n        resolvers.shift()();\n      });\n      sandbox.stub(feed, \"_fetchScreenshot\");\n    });\n    afterEach(() => {\n      sandbox.restore();\n    });\n\n    const forDispatch = action =>\n      new Promise(resolve => {\n        resolvers.push(resolve);\n        feed.onAction(action);\n      });\n\n    it(\"should add a pinned site and remove it\", async () => {\n      feed._requestRichIcon = sinon.stub();\n      const url = \"https://pin.me\";\n      fakeNewTabUtils.pinnedLinks.pin = sandbox.stub().callsFake(link => {\n        fakeNewTabUtils.pinnedLinks.links.push(link);\n      });\n\n      await forDispatch({ type: at.TOP_SITES_INSERT, data: { site: { url } } });\n      fakeNewTabUtils.pinnedLinks.links.pop();\n      await forDispatch({ type: at.PLACES_LINK_BLOCKED });\n\n      assert.calledTwice(feed.store.dispatch);\n      assert.equal(\n        feed.store.dispatch.firstCall.args[0].data.links[0].url,\n        url\n      );\n      assert.equal(\n        feed.store.dispatch.secondCall.args[0].data.links[0].url,\n        FAKE_LINKS[0].url\n      );\n    });\n  });\n\n  describe(\"improvesearch.noDefaultSearchTile experiment\", () => {\n    const NO_DEFAULT_SEARCH_TILE_PREF = \"improvesearch.noDefaultSearchTile\";\n    beforeEach(() => {\n      global.Services.search.getDefault = async () => ({\n        identifier: \"google\",\n        searchForm: \"google.com\",\n      });\n      feed.store.state.Prefs.values[NO_DEFAULT_SEARCH_TILE_PREF] = true;\n    });\n    it(\"should filter out alexa top 5 search from the default sites\", async () => {\n      const TOP_5_TEST = [\n        \"google.com\",\n        \"search.yahoo.com\",\n        \"yahoo.com\",\n        \"bing.com\",\n        \"ask.com\",\n        \"duckduckgo.com\",\n      ];\n      links = [{ url: \"amazon.com\" }, ...TOP_5_TEST.map(url => ({ url }))];\n      const urlsReturned = (await feed.getLinksWithDefaults()).map(\n        link => link.url\n      );\n      assert.include(urlsReturned, \"amazon.com\");\n      TOP_5_TEST.forEach(url => assert.notInclude(urlsReturned, url));\n    });\n    it(\"should not filter out alexa, default search from the query results if the experiment pref is off\", async () => {\n      links = [\n        { url: \"google.com\" },\n        { url: \"foo.com\" },\n        { url: \"duckduckgo\" },\n      ];\n      feed.store.state.Prefs.values[NO_DEFAULT_SEARCH_TILE_PREF] = false;\n      const urlsReturned = (await feed.getLinksWithDefaults()).map(\n        link => link.url\n      );\n      assert.include(urlsReturned, \"google.com\");\n    });\n    it(\"should filter out the current default search from the default sites\", async () => {\n      feed._currentSearchHostname = \"amazon\";\n      feed.onAction({\n        type: at.PREFS_INITIAL_VALUES,\n        data: { \"default.sites\": \"google.com,amazon.com\" },\n      });\n      links = [{ url: \"foo.com\" }];\n      const urlsReturned = (await feed.getLinksWithDefaults()).map(\n        link => link.url\n      );\n      assert.notInclude(urlsReturned, \"amazon.com\");\n    });\n    it(\"should not filter out current default search from pinned sites even if it matches the current default search\", async () => {\n      links = [{ url: \"foo.com\" }];\n      fakeNewTabUtils.pinnedLinks.links = [{ url: \"google.com\" }];\n      const urlsReturned = (await feed.getLinksWithDefaults()).map(\n        link => link.url\n      );\n      assert.include(urlsReturned, \"google.com\");\n    });\n    it(\"should call refresh and set ._currentSearchHostname to the new engine hostname when the the default search engine has been set\", () => {\n      sinon.stub(feed, \"refresh\");\n      sandbox\n        .stub(global.Services.search, \"defaultEngine\")\n        .value({ identifier: \"ddg\", searchForm: \"duckduckgo.com\" });\n      feed.observe(null, \"browser-search-engine-modified\", \"engine-default\");\n      assert.equal(feed._currentSearchHostname, \"duckduckgo\");\n      assert.calledOnce(feed.refresh);\n    });\n    it(\"should call refresh when the experiment pref has changed\", () => {\n      sinon.stub(feed, \"refresh\");\n\n      feed.onAction({\n        type: at.PREF_CHANGED,\n        data: { name: NO_DEFAULT_SEARCH_TILE_PREF, value: true },\n      });\n      assert.calledOnce(feed.refresh);\n\n      feed.onAction({\n        type: at.PREF_CHANGED,\n        data: { name: NO_DEFAULT_SEARCH_TILE_PREF, value: false },\n      });\n      assert.calledTwice(feed.refresh);\n    });\n  });\n\n  describe(\"improvesearch.topSitesSearchShortcuts\", () => {\n    beforeEach(() => {\n      feed.store.state.Prefs.values[SEARCH_SHORTCUTS_EXPERIMENT_PREF] = true;\n      feed.store.state.Prefs.values[SEARCH_SHORTCUTS_SEARCH_ENGINES_PREF] =\n        \"google,amazon\";\n      feed.store.state.Prefs.values[SEARCH_SHORTCUTS_HAVE_PINNED_PREF] = \"\";\n      const searchEngines = [\n        { wrappedJSObject: { _internalAliases: [\"@google\"] } },\n        { wrappedJSObject: { _internalAliases: [\"@amazon\"] } },\n      ];\n      global.Services.search.getDefaultEngines = async () => searchEngines;\n      fakeNewTabUtils.pinnedLinks.pin = sinon\n        .stub()\n        .callsFake((site, index) => {\n          fakeNewTabUtils.pinnedLinks.links[index] = site;\n        });\n    });\n\n    it(\"should properly disable search improvements if the pref is off\", async () => {\n      sandbox.stub(global.Services.prefs, \"clearUserPref\");\n      sandbox.spy(feed.pinnedCache, \"expire\");\n      sandbox.spy(feed, \"refresh\");\n\n      // an actual implementation of unpin (until we can get a mochitest for search improvements)\n      fakeNewTabUtils.pinnedLinks.unpin = sinon.stub().callsFake(site => {\n        let index = -1;\n        for (let i = 0; i < fakeNewTabUtils.pinnedLinks.links.length; i++) {\n          let link = fakeNewTabUtils.pinnedLinks.links[i];\n          if (link && link.url === site.url) {\n            index = i;\n          }\n        }\n        if (index > -1) {\n          fakeNewTabUtils.pinnedLinks.links[index] = null;\n        }\n      });\n\n      // ensure we've inserted search shorcuts + pin an additional site in space 4\n      await feed._maybeInsertSearchShortcuts(fakeNewTabUtils.pinnedLinks.links);\n      fakeNewTabUtils.pinnedLinks.pin({ url: \"https://dontunpinme.com\" }, 3);\n\n      // turn the experiment off\n      feed.onAction({\n        type: at.PREF_CHANGED,\n        data: { name: SEARCH_SHORTCUTS_EXPERIMENT_PREF, value: false },\n      });\n\n      // check we cleared the pref, expired the pinned cache, and refreshed the feed\n      assert.calledWith(\n        global.Services.prefs.clearUserPref,\n        `browser.newtabpage.activity-stream.${SEARCH_SHORTCUTS_HAVE_PINNED_PREF}`\n      );\n      assert.calledOnce(feed.pinnedCache.expire);\n      assert.calledWith(feed.refresh, { broadcast: true });\n\n      // check that the search shortcuts were removed from the list of pinned sites\n      const urlsReturned = fakeNewTabUtils.pinnedLinks.links\n        .filter(s => s)\n        .map(link => link.url);\n      assert.notInclude(urlsReturned, \"https://amazon.com\");\n      assert.notInclude(urlsReturned, \"https://google.com\");\n      assert.include(urlsReturned, \"https://dontunpinme.com\");\n\n      // check that the positions where the search shortcuts were null, and the additional pinned site is untouched in space 4\n      assert.equal(fakeNewTabUtils.pinnedLinks.links[0], null);\n      assert.equal(fakeNewTabUtils.pinnedLinks.links[1], null);\n      assert.equal(fakeNewTabUtils.pinnedLinks.links[2], undefined);\n      assert.deepEqual(fakeNewTabUtils.pinnedLinks.links[3], {\n        url: \"https://dontunpinme.com\",\n      });\n    });\n\n    it(\"should updateCustomSearchShortcuts when experiment pref is turned on\", async () => {\n      feed.store.state.Prefs.values[SEARCH_SHORTCUTS_EXPERIMENT_PREF] = false;\n      feed.updateCustomSearchShortcuts = sinon.spy();\n\n      // turn the experiment on\n      feed.onAction({\n        type: at.PREF_CHANGED,\n        data: { name: SEARCH_SHORTCUTS_EXPERIMENT_PREF, value: true },\n      });\n\n      assert.calledOnce(feed.updateCustomSearchShortcuts);\n    });\n\n    it(\"should filter out default top sites that match a hostname of a search shortcut if previously blocked\", async () => {\n      feed.refreshDefaults(\"https://amazon.ca\");\n      fakeNewTabUtils.blockedLinks.links = [{ url: \"https://amazon.com\" }];\n      fakeNewTabUtils.blockedLinks.isBlocked = site =>\n        fakeNewTabUtils.blockedLinks.links[0].url === site.url;\n      const urlsReturned = (await feed.getLinksWithDefaults()).map(\n        link => link.url\n      );\n      assert.notInclude(urlsReturned, \"https://amazon.ca\");\n    });\n\n    it(\"should update frecent search topsite icon\", async () => {\n      feed._tippyTopProvider.processSite = site => {\n        site.tippyTopIcon = \"icon.png\";\n        site.backgroundColor = \"#fff\";\n        return site;\n      };\n      links = [{ url: \"google.com\" }];\n\n      const urlsReturned = await feed.getLinksWithDefaults();\n\n      const defaultSearchTopsite = urlsReturned.find(\n        s => s.url === \"google.com\"\n      );\n      assert.propertyVal(defaultSearchTopsite, \"searchTopSite\", true);\n      assert.equal(defaultSearchTopsite.tippyTopIcon, \"icon.png\");\n      assert.equal(defaultSearchTopsite.backgroundColor, \"#fff\");\n    });\n    it(\"should update default search topsite icon\", async () => {\n      feed._tippyTopProvider.processSite = site => {\n        site.tippyTopIcon = \"icon.png\";\n        site.backgroundColor = \"#fff\";\n        return site;\n      };\n      links = [{ url: \"foo.com\" }];\n      feed.onAction({\n        type: at.PREFS_INITIAL_VALUES,\n        data: { \"default.sites\": \"google.com,amazon.com\" },\n      });\n\n      const urlsReturned = await feed.getLinksWithDefaults();\n\n      const defaultSearchTopsite = urlsReturned.find(\n        s => s.url === \"amazon.com\"\n      );\n      assert.propertyVal(defaultSearchTopsite, \"searchTopSite\", true);\n      assert.equal(defaultSearchTopsite.tippyTopIcon, \"icon.png\");\n      assert.equal(defaultSearchTopsite.backgroundColor, \"#fff\");\n    });\n    it(\"should dispatch UPDATE_SEARCH_SHORTCUTS on updateCustomSearchShortcuts\", async () => {\n      feed.store.state.Prefs.values[\"improvesearch.noDefaultSearchTile\"] = true;\n      await feed.updateCustomSearchShortcuts();\n      assert.calledOnce(feed.store.dispatch);\n      assert.calledWith(feed.store.dispatch, {\n        data: {\n          searchShortcuts: [\n            {\n              keyword: \"@google\",\n              shortURL: \"google\",\n              url: \"https://google.com\",\n            },\n            {\n              keyword: \"@amazon\",\n              shortURL: \"amazon\",\n              url: \"https://amazon.com\",\n            },\n          ],\n        },\n        meta: { from: \"ActivityStream:Main\", to: \"ActivityStream:Content\" },\n        type: \"UPDATE_SEARCH_SHORTCUTS\",\n      });\n    });\n\n    describe(\"_maybeInsertSearchShortcuts\", () => {\n      beforeEach(() => {\n        // Default is one row\n        feed.store.state.Prefs.values.topSitesRows = TOP_SITES_DEFAULT_ROWS;\n        // Eight slots per row\n        fakeNewTabUtils.pinnedLinks.links = [\n          { url: \"\" },\n          { url: \"\" },\n          { url: \"\" },\n          null,\n          { url: \"\" },\n          { url: \"\" },\n          null,\n          { url: \"\" },\n        ];\n      });\n\n      it(\"should be called on getLinksWithDefaults\", async () => {\n        sandbox.spy(feed, \"_maybeInsertSearchShortcuts\");\n        await feed.getLinksWithDefaults();\n        assert.calledOnce(feed._maybeInsertSearchShortcuts);\n      });\n\n      it(\"should do nothing and return false if the experiment is disabled\", async () => {\n        feed.store.state.Prefs.values[SEARCH_SHORTCUTS_EXPERIMENT_PREF] = false;\n        assert.isFalse(\n          await feed._maybeInsertSearchShortcuts(\n            fakeNewTabUtils.pinnedLinks.links\n          )\n        );\n        assert.notCalled(fakeNewTabUtils.pinnedLinks.pin);\n      });\n\n      it(\"should pin shortcuts in the correct order, into the available unpinned slots\", async () => {\n        await feed._maybeInsertSearchShortcuts(\n          fakeNewTabUtils.pinnedLinks.links\n        );\n        // The shouldPin pref is \"google,amazon\" so expect the shortcuts in that order\n        assert.deepEqual(fakeNewTabUtils.pinnedLinks.links[3], {\n          url: \"https://google.com\",\n          searchTopSite: true,\n          label: \"@google\",\n        });\n        assert.deepEqual(fakeNewTabUtils.pinnedLinks.links[6], {\n          url: \"https://amazon.com\",\n          searchTopSite: true,\n          label: \"@amazon\",\n        });\n      });\n\n      it(\"should not pin shortcuts for the current default search engine\", async () => {\n        feed._currentSearchHostname = \"google\";\n        await feed._maybeInsertSearchShortcuts(\n          fakeNewTabUtils.pinnedLinks.links\n        );\n        assert.deepEqual(fakeNewTabUtils.pinnedLinks.links[3], {\n          url: \"https://amazon.com\",\n          searchTopSite: true,\n          label: \"@amazon\",\n        });\n      });\n\n      it(\"should only pin the first shortcut if there's only one available slot\", async () => {\n        fakeNewTabUtils.pinnedLinks.links[3] = { url: \"\" };\n        await feed._maybeInsertSearchShortcuts(\n          fakeNewTabUtils.pinnedLinks.links\n        );\n        // The first item in the shouldPin pref is \"google\" so expect only Google to be pinned\n        assert.ok(\n          fakeNewTabUtils.pinnedLinks.links.find(\n            s => s && s.url === \"https://google.com\"\n          )\n        );\n        assert.notOk(\n          fakeNewTabUtils.pinnedLinks.links.find(\n            s => s && s.url === \"https://amazon.com\"\n          )\n        );\n      });\n\n      it(\"should pin none if there's no available slot\", async () => {\n        fakeNewTabUtils.pinnedLinks.links[3] = { url: \"\" };\n        fakeNewTabUtils.pinnedLinks.links[6] = { url: \"\" };\n        await feed._maybeInsertSearchShortcuts(\n          fakeNewTabUtils.pinnedLinks.links\n        );\n        assert.notOk(\n          fakeNewTabUtils.pinnedLinks.links.find(\n            s => s && s.url === \"https://google.com\"\n          )\n        );\n        assert.notOk(\n          fakeNewTabUtils.pinnedLinks.links.find(\n            s => s && s.url === \"https://amazon.com\"\n          )\n        );\n      });\n\n      it(\"should not pin a shortcut if the corresponding search engine is not available\", async () => {\n        // Make Amazon search engine unavailable\n        global.Services.search.getDefaultEngines = async () => [\n          { wrappedJSObject: { _internalAliases: [\"@google\"] } },\n        ];\n        fakeNewTabUtils.pinnedLinks.links.fill(null);\n        await feed._maybeInsertSearchShortcuts(\n          fakeNewTabUtils.pinnedLinks.links\n        );\n        assert.notOk(\n          fakeNewTabUtils.pinnedLinks.links.find(\n            s => s && s.url === \"https://amazon.com\"\n          )\n        );\n      });\n\n      it(\"should not pin a search shortcut if it's been pinned before\", async () => {\n        fakeNewTabUtils.pinnedLinks.links.fill(null);\n        feed.store.state.Prefs.values[SEARCH_SHORTCUTS_HAVE_PINNED_PREF] =\n          \"google,amazon\";\n        await feed._maybeInsertSearchShortcuts(\n          fakeNewTabUtils.pinnedLinks.links\n        );\n        assert.notOk(\n          fakeNewTabUtils.pinnedLinks.links.find(\n            s => s && s.url === \"https://google.com\"\n          )\n        );\n        assert.notOk(\n          fakeNewTabUtils.pinnedLinks.links.find(\n            s => s && s.url === \"https://amazon.com\"\n          )\n        );\n\n        fakeNewTabUtils.pinnedLinks.links.fill(null);\n        feed.store.state.Prefs.values[SEARCH_SHORTCUTS_HAVE_PINNED_PREF] =\n          \"amazon\";\n        await feed._maybeInsertSearchShortcuts(\n          fakeNewTabUtils.pinnedLinks.links\n        );\n        assert.ok(\n          fakeNewTabUtils.pinnedLinks.links.find(\n            s => s && s.url === \"https://google.com\"\n          )\n        );\n        assert.notOk(\n          fakeNewTabUtils.pinnedLinks.links.find(\n            s => s && s.url === \"https://amazon.com\"\n          )\n        );\n\n        fakeNewTabUtils.pinnedLinks.links.fill(null);\n        feed.store.state.Prefs.values[SEARCH_SHORTCUTS_HAVE_PINNED_PREF] =\n          \"google\";\n        await feed._maybeInsertSearchShortcuts(\n          fakeNewTabUtils.pinnedLinks.links\n        );\n        assert.notOk(\n          fakeNewTabUtils.pinnedLinks.links.find(\n            s => s && s.url === \"https://google.com\"\n          )\n        );\n        assert.ok(\n          fakeNewTabUtils.pinnedLinks.links.find(\n            s => s && s.url === \"https://amazon.com\"\n          )\n        );\n      });\n\n      it(\"should record the insertion of a search shortcut\", async () => {\n        feed.store.state.Prefs.values[SEARCH_SHORTCUTS_HAVE_PINNED_PREF] = \"\";\n        // Fill up one slot, so there's only one left - to be filled by Google\n        fakeNewTabUtils.pinnedLinks.links[3] = { url: \"\" };\n        await feed._maybeInsertSearchShortcuts(\n          fakeNewTabUtils.pinnedLinks.links\n        );\n        assert.calledWithExactly(feed.store.dispatch, {\n          data: { name: SEARCH_SHORTCUTS_HAVE_PINNED_PREF, value: \"google\" },\n          meta: { from: \"ActivityStream:Content\", to: \"ActivityStream:Main\" },\n          type: \"SET_PREF\",\n        });\n      });\n    });\n  });\n\n  describe(\"updatePinnedSearchShortcuts\", () => {\n    it(\"should unpin a shortcut in deletedShortcuts\", () => {\n      const deletedShortcuts = [\n        {\n          url: \"https://google.com\",\n          searchVendor: \"google\",\n          label: \"google\",\n          searchTopSite: true,\n        },\n      ];\n      const addedShortcuts = [];\n      fakeNewTabUtils.pinnedLinks.links = [\n        null,\n        null,\n        {\n          url: \"https://amazon.com\",\n          searchVendor: \"amazon\",\n          label: \"amazon\",\n          searchTopSite: true,\n        },\n      ];\n      feed.updatePinnedSearchShortcuts({ addedShortcuts, deletedShortcuts });\n      assert.notCalled(fakeNewTabUtils.pinnedLinks.pin);\n      assert.calledOnce(fakeNewTabUtils.pinnedLinks.unpin);\n      assert.calledWith(fakeNewTabUtils.pinnedLinks.unpin, {\n        url: \"https://google.com\",\n      });\n    });\n\n    it(\"should pin a shortcut in addedShortcuts\", () => {\n      const addedShortcuts = [\n        {\n          url: \"https://google.com\",\n          searchVendor: \"google\",\n          label: \"google\",\n          searchTopSite: true,\n        },\n      ];\n      const deletedShortcuts = [];\n      fakeNewTabUtils.pinnedLinks.links = [\n        null,\n        null,\n        {\n          url: \"https://amazon.com\",\n          searchVendor: \"amazon\",\n          label: \"amazon\",\n          searchTopSite: true,\n        },\n      ];\n      feed.updatePinnedSearchShortcuts({ addedShortcuts, deletedShortcuts });\n      assert.notCalled(fakeNewTabUtils.pinnedLinks.unpin);\n      assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin);\n      assert.calledWith(\n        fakeNewTabUtils.pinnedLinks.pin,\n        {\n          label: \"google\",\n          searchTopSite: true,\n          searchVendor: \"google\",\n          url: \"https://google.com\",\n        },\n        0\n      );\n    });\n\n    it(\"should pin and unpin in the same action\", () => {\n      const addedShortcuts = [\n        {\n          url: \"https://google.com\",\n          searchVendor: \"google\",\n          label: \"google\",\n          searchTopSite: true,\n        },\n        {\n          url: \"https://ebay.com\",\n          searchVendor: \"ebay\",\n          label: \"ebay\",\n          searchTopSite: true,\n        },\n      ];\n      const deletedShortcuts = [\n        {\n          url: \"https://amazon.com\",\n          searchVendor: \"amazon\",\n          label: \"amazon\",\n          searchTopSite: true,\n        },\n      ];\n      fakeNewTabUtils.pinnedLinks.links = [\n        { url: \"https://foo.com\" },\n        {\n          url: \"https://amazon.com\",\n          searchVendor: \"amazon\",\n          label: \"amazon\",\n          searchTopSite: true,\n        },\n      ];\n      feed.updatePinnedSearchShortcuts({ addedShortcuts, deletedShortcuts });\n      assert.calledOnce(fakeNewTabUtils.pinnedLinks.unpin);\n      assert.calledTwice(fakeNewTabUtils.pinnedLinks.pin);\n    });\n\n    it(\"should pin a shortcut in addedShortcuts even if pinnedLinks is full\", () => {\n      const addedShortcuts = [\n        {\n          url: \"https://google.com\",\n          searchVendor: \"google\",\n          label: \"google\",\n          searchTopSite: true,\n        },\n      ];\n      const deletedShortcuts = [];\n      fakeNewTabUtils.pinnedLinks.links = FAKE_LINKS;\n      feed.updatePinnedSearchShortcuts({ addedShortcuts, deletedShortcuts });\n      assert.notCalled(fakeNewTabUtils.pinnedLinks.unpin);\n      assert.calledWith(\n        fakeNewTabUtils.pinnedLinks.pin,\n        { label: \"google\", searchTopSite: true, url: \"https://google.com\" },\n        0\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/lib/TopStoriesFeed.test.js",
    "content": "import { FakePrefs, GlobalOverrider } from \"test/unit/utils\";\nimport { actionTypes as at } from \"common/Actions.jsm\";\nimport injector from \"inject!lib/TopStoriesFeed.jsm\";\n\ndescribe(\"Top Stories Feed\", () => {\n  let TopStoriesFeed;\n  let STORIES_UPDATE_TIME;\n  let TOPICS_UPDATE_TIME;\n  let SECTION_ID;\n  let SPOC_IMPRESSION_TRACKING_PREF;\n  let REC_IMPRESSION_TRACKING_PREF;\n  let MIN_DOMAIN_AFFINITIES_UPDATE_TIME;\n  let DEFAULT_RECS_EXPIRE_TIME;\n  let instance;\n  let clock;\n  let globals;\n  let sectionsManagerStub;\n  let shortURLStub;\n\n  const FAKE_OPTIONS = {\n    stories_endpoint: \"https://somedomain.org/stories?key=$apiKey\",\n    stories_referrer: \"https://somedomain.org/referrer\",\n    topics_endpoint: \"https://somedomain.org/topics?key=$apiKey\",\n    survey_link: \"https://www.surveymonkey.com/r/newtabffx\",\n    api_key_pref: \"apiKeyPref\",\n    provider_name: \"test-provider\",\n    provider_icon: \"provider-icon\",\n    provider_description: \"provider_desc\",\n  };\n\n  beforeEach(() => {\n    FakePrefs.prototype.prefs.apiKeyPref = \"test-api-key\";\n    FakePrefs.prototype.prefs.pocketCta = JSON.stringify({\n      cta_button: \"\",\n      cta_text: \"\",\n      cta_url: \"\",\n      use_cta: false,\n    });\n\n    globals = new GlobalOverrider();\n    globals.set(\"PlacesUtils\", { history: {} });\n    globals.set(\"pktApi\", { isUserLoggedIn() {} });\n    clock = sinon.useFakeTimers();\n    shortURLStub = sinon.stub().callsFake(site => site.url);\n    sectionsManagerStub = {\n      onceInitialized: sinon.stub().callsFake(callback => callback()),\n      enableSection: sinon.spy(),\n      disableSection: sinon.spy(),\n      updateSection: sinon.spy(),\n      sections: new Map([[\"topstories\", { options: FAKE_OPTIONS }]]),\n    };\n\n    class FakeUserDomainAffinityProvider {\n      constructor(\n        timeSegments,\n        parameterSets,\n        maxHistoryQueryResults,\n        version,\n        scores\n      ) {\n        this.timeSegments = timeSegments;\n        this.parameterSets = parameterSets;\n        this.maxHistoryQueryResults = maxHistoryQueryResults;\n        this.version = version;\n        this.scores = scores;\n      }\n\n      getAffinities() {\n        return {};\n      }\n    }\n    class FakePersonalityProvider extends FakeUserDomainAffinityProvider {}\n\n    ({\n      TopStoriesFeed,\n      STORIES_UPDATE_TIME,\n      TOPICS_UPDATE_TIME,\n      SECTION_ID,\n      SPOC_IMPRESSION_TRACKING_PREF,\n      REC_IMPRESSION_TRACKING_PREF,\n      MIN_DOMAIN_AFFINITIES_UPDATE_TIME,\n      DEFAULT_RECS_EXPIRE_TIME,\n    } = injector({\n      \"lib/ActivityStreamPrefs.jsm\": { Prefs: FakePrefs },\n      \"lib/ShortURL.jsm\": { shortURL: shortURLStub },\n      \"lib/PersonalityProvider.jsm\": {\n        PersonalityProvider: FakePersonalityProvider,\n      },\n      \"lib/UserDomainAffinityProvider.jsm\": {\n        UserDomainAffinityProvider: FakeUserDomainAffinityProvider,\n      },\n      \"lib/SectionsManager.jsm\": { SectionsManager: sectionsManagerStub },\n    }));\n\n    instance = new TopStoriesFeed();\n    instance.store = {\n      getState() {\n        return { Prefs: { values: { showSponsored: true } } };\n      },\n      dispatch: sinon.spy(),\n    };\n    instance.storiesLastUpdated = 0;\n    instance.topicsLastUpdated = 0;\n  });\n  afterEach(() => {\n    globals.restore();\n    clock.restore();\n  });\n\n  describe(\"#lazyloading TopStories\", () => {\n    beforeEach(() => {\n      instance.discoveryStreamEnabled = true;\n    });\n    it(\"should bind parseOptions to SectionsManager.onceInitialized when discovery stream is true\", () => {\n      instance.discoveryStreamEnabled = false;\n      instance.store.getState = () => ({\n        Prefs: {\n          values: {\n            \"discoverystream.config\": JSON.stringify({ enabled: true }),\n          },\n        },\n      });\n      instance.onAction({ type: at.INIT, data: {} });\n\n      assert.calledOnce(sectionsManagerStub.onceInitialized);\n    });\n    it(\"should bind parseOptions to SectionsManager.onceInitialized when discovery stream is false\", () => {\n      instance.store.getState = () => ({\n        Prefs: {\n          values: {\n            \"discoverystream.config\": JSON.stringify({ enabled: false }),\n          },\n        },\n      });\n      instance.onAction({ type: at.INIT, data: {} });\n      assert.calledOnce(sectionsManagerStub.onceInitialized);\n    });\n    it(\"Should initialize properties once while lazy loading if not initialized earlier\", () => {\n      instance.discoveryStreamEnabled = false;\n      instance.propertiesInitialized = false;\n      sinon.stub(instance, \"initializeProperties\");\n      instance.lazyLoadTopStories();\n      assert.calledOnce(instance.initializeProperties);\n    });\n    it(\"should not re-initialize properties\", () => {\n      // For discovery stream experience disabled TopStoriesFeed properties\n      // are initialized in constructor and should not be called again while lazy loading topstories\n      sinon.stub(instance, \"initializeProperties\");\n      instance.discoveryStreamEnabled = false;\n      instance.propertiesInitialized = true;\n      instance.lazyLoadTopStories();\n      assert.notCalled(instance.initializeProperties);\n    });\n    it(\"should have early exit onInit when discovery is true\", async () => {\n      sinon.stub(instance, \"doContentUpdate\");\n      await instance.onInit();\n      assert.notCalled(instance.doContentUpdate);\n      assert.isUndefined(instance.storiesLoaded);\n    });\n    it(\"should complete onInit when discovery is false\", async () => {\n      instance.discoveryStreamEnabled = false;\n      sinon.stub(instance, \"doContentUpdate\");\n      await instance.onInit();\n      assert.calledOnce(instance.doContentUpdate);\n      assert.isTrue(instance.storiesLoaded);\n    });\n    it(\"should handle limited actions when discoverystream is enabled\", async () => {\n      sinon.spy(instance, \"handleDisabled\");\n      sinon.stub(instance, \"getPocketState\");\n      instance.store.getState = () => ({\n        Prefs: {\n          values: {\n            \"discoverystream.config\": JSON.stringify({ enabled: true }),\n            \"discoverystream.enabled\": true,\n          },\n        },\n      });\n\n      instance.onAction({ type: at.INIT, data: {} });\n\n      assert.calledOnce(instance.handleDisabled);\n      instance.onAction({\n        type: at.NEW_TAB_REHYDRATED,\n        meta: { fromTarget: {} },\n      });\n      assert.notCalled(instance.getPocketState);\n    });\n    it(\"should handle NEW_TAB_REHYDRATED when discoverystream is disabled\", async () => {\n      instance.discoveryStreamEnabled = false;\n      sinon.spy(instance, \"handleDisabled\");\n      sinon.stub(instance, \"getPocketState\");\n      instance.store.getState = () => ({\n        Prefs: {\n          values: {\n            \"discoverystream.config\": JSON.stringify({ enabled: false }),\n          },\n        },\n      });\n      instance.onAction({ type: at.INIT, data: {} });\n      assert.notCalled(instance.handleDisabled);\n\n      instance.onAction({\n        type: at.NEW_TAB_REHYDRATED,\n        meta: { fromTarget: {} },\n      });\n      assert.calledOnce(instance.getPocketState);\n    });\n    it(\"should handle UNINIT when discoverystream is enabled\", async () => {\n      sinon.stub(instance, \"uninit\");\n      instance.onAction({ type: at.UNINIT });\n      assert.calledOnce(instance.uninit);\n    });\n    it(\"should fire init on PREF_CHANGED\", () => {\n      sinon.stub(instance, \"onInit\");\n      instance.onAction({\n        type: at.PREF_CHANGED,\n        data: { name: \"discoverystream.config\", value: {} },\n      });\n      assert.calledOnce(instance.onInit);\n    });\n    it(\"should fire init on DISCOVERY_STREAM_PREF_ENABLED\", () => {\n      sinon.stub(instance, \"onInit\");\n      instance.onAction({\n        type: at.PREF_CHANGED,\n        data: { name: \"discoverystream.enabled\", value: true },\n      });\n      assert.calledOnce(instance.onInit);\n    });\n    it(\"should not fire init on PREF_CHANGED if stories are loaded\", () => {\n      sinon.stub(instance, \"onInit\");\n      sinon.spy(instance, \"lazyLoadTopStories\");\n      instance.storiesLoaded = true;\n      instance.onAction({\n        type: at.PREF_CHANGED,\n        data: { name: \"discoverystream.config\", value: {} },\n      });\n      assert.calledOnce(instance.lazyLoadTopStories);\n      assert.notCalled(instance.onInit);\n    });\n    it(\"should fire init on PREF_CHANGED when discoverystream is disabled\", () => {\n      instance.discoveryStreamEnabled = false;\n      sinon.stub(instance, \"onInit\");\n      instance.onAction({\n        type: at.PREF_CHANGED,\n        data: { name: \"discoverystream.config\", value: {} },\n      });\n      assert.calledOnce(instance.onInit);\n    });\n    it(\"should not fire init on PREF_CHANGED when discoverystream is disabled and stories are loaded\", () => {\n      instance.discoveryStreamEnabled = false;\n      sinon.stub(instance, \"onInit\");\n      sinon.spy(instance, \"lazyLoadTopStories\");\n      instance.storiesLoaded = true;\n      instance.onAction({\n        type: at.PREF_CHANGED,\n        data: { name: \"discoverystream.config\", value: {} },\n      });\n      assert.calledOnce(instance.lazyLoadTopStories);\n      assert.notCalled(instance.onInit);\n    });\n  });\n\n  describe(\"#init\", () => {\n    it(\"should create a TopStoriesFeed\", () => {\n      assert.instanceOf(instance, TopStoriesFeed);\n    });\n    it(\"should bind parseOptions to SectionsManager.onceInitialized\", () => {\n      instance.onAction({ type: at.INIT, data: {} });\n      assert.calledOnce(sectionsManagerStub.onceInitialized);\n    });\n    it(\"should initialize endpoints based on options\", async () => {\n      await instance.onInit();\n      assert.equal(\n        \"https://somedomain.org/stories?key=test-api-key\",\n        instance.stories_endpoint\n      );\n      assert.equal(\n        \"https://somedomain.org/referrer\",\n        instance.stories_referrer\n      );\n      assert.equal(\n        \"https://somedomain.org/topics?key=test-api-key\",\n        instance.topics_endpoint\n      );\n    });\n    it(\"should enable its section\", () => {\n      instance.onAction({ type: at.INIT, data: {} });\n      assert.calledOnce(sectionsManagerStub.enableSection);\n      assert.calledWith(sectionsManagerStub.enableSection, SECTION_ID);\n    });\n    it(\"init should fire onInit\", () => {\n      instance.onInit = sinon.spy();\n      instance.onAction({ type: at.INIT, data: {} });\n      assert.calledOnce(instance.onInit);\n    });\n    it(\"should fetch stories on init\", async () => {\n      instance.fetchStories = sinon.spy();\n      await instance.onInit();\n      assert.calledOnce(instance.fetchStories);\n    });\n    it(\"should fetch topics on init\", async () => {\n      instance.fetchTopics = sinon.spy();\n      await instance.onInit();\n      assert.calledOnce(instance.fetchTopics);\n    });\n    it(\"should not fetch if endpoint not configured\", () => {\n      let fetchStub = globals.sandbox.stub();\n      globals.set(\"fetch\", fetchStub);\n      sectionsManagerStub.sections.set(\"topstories\", { options: {} });\n      instance.init();\n      assert.notCalled(fetchStub);\n    });\n    it(\"should report error for invalid configuration\", () => {\n      globals.sandbox.spy(global.Cu, \"reportError\");\n      sectionsManagerStub.sections.set(\"topstories\", {\n        options: {\n          api_key_pref: \"invalid\",\n          stories_endpoint: \"https://invalid.com/?apiKey=$apiKey\",\n        },\n      });\n      instance.init();\n\n      assert.calledWith(\n        Cu.reportError,\n        \"Problem initializing top stories feed: An API key was specified but none configured: https://invalid.com/?apiKey=$apiKey\"\n      );\n    });\n    it(\"should report error for missing api key\", () => {\n      globals.sandbox.spy(global.Cu, \"reportError\");\n      sectionsManagerStub.sections.set(\"topstories\", {\n        options: {\n          stories_endpoint: \"https://somedomain.org/stories?key=$apiKey\",\n          topics_endpoint: \"https://somedomain.org/topics?key=$apiKey\",\n        },\n      });\n      instance.init();\n\n      assert.called(Cu.reportError);\n    });\n    it(\"should load data from cache on init\", async () => {\n      instance.loadCachedData = sinon.spy();\n      await instance.onInit();\n      assert.calledOnce(instance.loadCachedData);\n    });\n  });\n  describe(\"#uninit\", () => {\n    it(\"should disable its section\", () => {\n      instance.onAction({ type: at.UNINIT });\n      assert.calledOnce(sectionsManagerStub.disableSection);\n      assert.calledWith(sectionsManagerStub.disableSection, SECTION_ID);\n    });\n    it(\"should unload stories on uninit\", async () => {\n      sinon.stub(instance.cache, \"set\").returns(Promise.resolve());\n      await instance.clearCache();\n      assert.calledWith(instance.cache.set.firstCall, \"stories\", {});\n      assert.calledWith(instance.cache.set.secondCall, \"topics\", {});\n      assert.calledWith(instance.cache.set.thirdCall, \"spocs\", {});\n    });\n  });\n  describe(\"#cache\", () => {\n    it(\"should clear all cache items when calling clearCache\", () => {\n      sinon.stub(instance.cache, \"set\").returns(Promise.resolve());\n      instance.storiesLoaded = true;\n      instance.uninit();\n      assert.equal(instance.storiesLoaded, false);\n    });\n    it(\"should set spocs cache on fetch\", async () => {\n      const response = {\n        recommendations: [{ id: \"1\" }, { id: \"2\" }],\n        settings: { timeSegments: {}, domainAffinityParameterSets: {} },\n        spocs: [{ id: \"spoc1\" }],\n      };\n\n      instance.show_spocs = true;\n      instance.personalized = true;\n      instance.stories_endpoint = \"stories-endpoint\";\n\n      let fetchStub = globals.sandbox.stub();\n      globals.set(\"fetch\", fetchStub);\n      globals.set(\"NewTabUtils\", { blockedLinks: { isBlocked: () => {} } });\n      fetchStub.resolves({\n        ok: true,\n        status: 200,\n        json: () => Promise.resolve(response),\n      });\n      sinon.spy(instance.cache, \"set\");\n\n      await instance.fetchStories();\n\n      assert.calledOnce(instance.cache.set);\n      const { args } = instance.cache.set.firstCall;\n      assert.equal(args[0], \"stories\");\n      assert.equal(args[1].spocs[0].id, \"spoc1\");\n    });\n    it(\"should get spocs on cache load\", async () => {\n      instance.cache.get = () => ({\n        stories: {\n          recommendations: [{ id: \"1\" }, { id: \"2\" }],\n          spocs: [{ id: \"spoc1\" }],\n        },\n      });\n      instance.storiesLastUpdated = 0;\n      globals.set(\"NewTabUtils\", { blockedLinks: { isBlocked: () => {} } });\n\n      await instance.loadCachedData();\n      assert.equal(instance.spocs[0].guid, \"spoc1\");\n    });\n  });\n  describe(\"#fetch\", () => {\n    it(\"should fetch stories, send event and cache results\", async () => {\n      let fetchStub = globals.sandbox.stub();\n      sectionsManagerStub.sections.set(\"topstories\", {\n        options: {\n          stories_endpoint: \"stories-endpoint\",\n          stories_referrer: \"referrer\",\n        },\n      });\n      globals.set(\"fetch\", fetchStub);\n      globals.set(\"NewTabUtils\", {\n        blockedLinks: { isBlocked: globals.sandbox.spy() },\n      });\n\n      const response = {\n        recommendations: [\n          {\n            id: \"1\",\n            title: \"title\",\n            excerpt: \"description\",\n            image_src: \"image-url\",\n            url: \"rec-url\",\n            published_timestamp: \"123\",\n            context: \"trending\",\n            icon: \"icon\",\n          },\n        ],\n      };\n      const stories = [\n        {\n          guid: \"1\",\n          type: \"now\",\n          title: \"title\",\n          context: \"trending\",\n          icon: \"icon\",\n          description: \"description\",\n          image: \"image-url\",\n          referrer: \"referrer\",\n          url: \"rec-url\",\n          hostname: \"rec-url\",\n          min_score: 0,\n          score: 1,\n          spoc_meta: {},\n        },\n      ];\n\n      instance.cache.set = sinon.spy();\n      fetchStub.resolves({\n        ok: true,\n        status: 200,\n        json: () => Promise.resolve(response),\n      });\n      await instance.onInit();\n\n      assert.calledOnce(fetchStub);\n      assert.calledOnce(shortURLStub);\n      assert.calledWithExactly(fetchStub, instance.stories_endpoint, {\n        credentials: \"omit\",\n      });\n      assert.calledOnce(sectionsManagerStub.updateSection);\n      assert.calledWith(sectionsManagerStub.updateSection, SECTION_ID, {\n        rows: stories,\n      });\n      assert.calledOnce(instance.cache.set);\n      assert.calledWith(\n        instance.cache.set,\n        \"stories\",\n        Object.assign({}, response, { _timestamp: 0 })\n      );\n    });\n    it(\"should use domain as hostname, if present\", async () => {\n      let fetchStub = globals.sandbox.stub();\n      sectionsManagerStub.sections.set(\"topstories\", {\n        options: {\n          stories_endpoint: \"stories-endpoint\",\n          stories_referrer: \"referrer\",\n        },\n      });\n      globals.set(\"fetch\", fetchStub);\n      globals.set(\"NewTabUtils\", {\n        blockedLinks: { isBlocked: globals.sandbox.spy() },\n      });\n\n      const response = {\n        recommendations: [\n          {\n            id: \"1\",\n            title: \"title\",\n            excerpt: \"description\",\n            image_src: \"image-url\",\n            url: \"rec-url\",\n            domain: \"domain\",\n            published_timestamp: \"123\",\n            context: \"trending\",\n            icon: \"icon\",\n          },\n        ],\n      };\n      const stories = [\n        {\n          guid: \"1\",\n          type: \"now\",\n          title: \"title\",\n          context: \"trending\",\n          icon: \"icon\",\n          description: \"description\",\n          image: \"image-url\",\n          referrer: \"referrer\",\n          url: \"rec-url\",\n          hostname: \"domain\",\n          min_score: 0,\n          score: 1,\n          spoc_meta: {},\n        },\n      ];\n\n      instance.cache.set = sinon.spy();\n      fetchStub.resolves({\n        ok: true,\n        status: 200,\n        json: () => Promise.resolve(response),\n      });\n      await instance.onInit();\n\n      assert.calledOnce(fetchStub);\n      assert.notCalled(shortURLStub);\n      assert.calledWith(sectionsManagerStub.updateSection, SECTION_ID, {\n        rows: stories,\n      });\n    });\n    it(\"should call SectionsManager.updateSection\", () => {\n      instance.dispatchUpdateEvent(123, {});\n      assert.calledOnce(sectionsManagerStub.updateSection);\n    });\n    it(\"should report error for unexpected stories response\", async () => {\n      let fetchStub = globals.sandbox.stub();\n      sectionsManagerStub.sections.set(\"topstories\", {\n        options: { stories_endpoint: \"stories-endpoint\" },\n      });\n      globals.set(\"fetch\", fetchStub);\n      globals.sandbox.spy(global.Cu, \"reportError\");\n\n      fetchStub.resolves({ ok: false, status: 400 });\n      await instance.onInit();\n\n      assert.calledOnce(fetchStub);\n      assert.calledWithExactly(fetchStub, instance.stories_endpoint, {\n        credentials: \"omit\",\n      });\n      assert.equal(instance.storiesLastUpdated, 0);\n      assert.called(Cu.reportError);\n    });\n    it(\"should exclude blocked (dismissed) URLs\", async () => {\n      let fetchStub = globals.sandbox.stub();\n      sectionsManagerStub.sections.set(\"topstories\", {\n        options: { stories_endpoint: \"stories-endpoint\" },\n      });\n      globals.set(\"fetch\", fetchStub);\n      globals.set(\"NewTabUtils\", {\n        blockedLinks: { isBlocked: site => site.url === \"blocked\" },\n      });\n\n      const response = {\n        recommendations: [{ url: \"blocked\" }, { url: \"not_blocked\" }],\n      };\n      fetchStub.resolves({\n        ok: true,\n        status: 200,\n        json: () => Promise.resolve(response),\n      });\n      await instance.onInit();\n\n      // Issue!\n      // Should actually be fixed when cache is fixed.\n      assert.calledOnce(sectionsManagerStub.updateSection);\n      assert.equal(\n        sectionsManagerStub.updateSection.firstCall.args[1].rows.length,\n        1\n      );\n      assert.equal(\n        sectionsManagerStub.updateSection.firstCall.args[1].rows[0].url,\n        \"not_blocked\"\n      );\n    });\n    it(\"should mark stories as new\", async () => {\n      let fetchStub = globals.sandbox.stub();\n      sectionsManagerStub.sections.set(\"topstories\", {\n        options: { stories_endpoint: \"stories-endpoint\" },\n      });\n      globals.set(\"fetch\", fetchStub);\n      globals.set(\"NewTabUtils\", {\n        blockedLinks: { isBlocked: globals.sandbox.spy() },\n      });\n      clock.restore();\n      const response = {\n        recommendations: [\n          { published_timestamp: Date.now() / 1000 },\n          { published_timestamp: \"0\" },\n          {\n            published_timestamp: (Date.now() - 2 * 24 * 60 * 60 * 1000) / 1000,\n          },\n        ],\n      };\n\n      fetchStub.resolves({\n        ok: true,\n        status: 200,\n        json: () => Promise.resolve(response),\n      });\n\n      await instance.onInit();\n      assert.calledOnce(sectionsManagerStub.updateSection);\n      assert.equal(\n        sectionsManagerStub.updateSection.firstCall.args[1].rows.length,\n        3\n      );\n      assert.equal(\n        sectionsManagerStub.updateSection.firstCall.args[1].rows[0].type,\n        \"now\"\n      );\n      assert.equal(\n        sectionsManagerStub.updateSection.firstCall.args[1].rows[1].type,\n        \"trending\"\n      );\n      assert.equal(\n        sectionsManagerStub.updateSection.firstCall.args[1].rows[2].type,\n        \"trending\"\n      );\n    });\n    it(\"should fetch topics, send event and cache results\", async () => {\n      let fetchStub = globals.sandbox.stub();\n      sectionsManagerStub.sections.set(\"topstories\", {\n        options: { topics_endpoint: \"topics-endpoint\" },\n      });\n      globals.set(\"fetch\", fetchStub);\n\n      const response = {\n        topics: [\n          { name: \"topic1\", url: \"url-topic1\" },\n          { name: \"topic2\", url: \"url-topic2\" },\n        ],\n      };\n      const topics = [\n        {\n          name: \"topic1\",\n          url: \"url-topic1\",\n        },\n        {\n          name: \"topic2\",\n          url: \"url-topic2\",\n        },\n      ];\n\n      instance.cache.set = sinon.spy();\n      fetchStub.resolves({\n        ok: true,\n        status: 200,\n        json: () => Promise.resolve(response),\n      });\n      await instance.onInit();\n\n      assert.calledOnce(fetchStub);\n      assert.calledWithExactly(fetchStub, instance.topics_endpoint, {\n        credentials: \"omit\",\n      });\n      assert.calledOnce(sectionsManagerStub.updateSection);\n      assert.calledWithMatch(sectionsManagerStub.updateSection, SECTION_ID, {\n        topics,\n      });\n      assert.calledOnce(instance.cache.set);\n      assert.calledWith(\n        instance.cache.set,\n        \"topics\",\n        Object.assign({}, response, { _timestamp: 0 })\n      );\n    });\n    it(\"should report error for unexpected topics response\", async () => {\n      let fetchStub = globals.sandbox.stub();\n      globals.set(\"fetch\", fetchStub);\n      globals.sandbox.spy(global.Cu, \"reportError\");\n\n      instance.topics_endpoint = \"topics-endpoint\";\n      fetchStub.resolves({ ok: false, status: 400 });\n      await instance.fetchTopics();\n\n      assert.calledOnce(fetchStub);\n      assert.calledWithExactly(fetchStub, instance.topics_endpoint, {\n        credentials: \"omit\",\n      });\n      assert.notCalled(instance.store.dispatch);\n      assert.called(Cu.reportError);\n    });\n  });\n  describe(\"#personalization\", () => {\n    it(\"should sort stories if personalization is preffed on\", async () => {\n      const response = {\n        recommendations: [{ id: \"1\" }, { id: \"2\" }],\n        settings: { timeSegments: {}, domainAffinityParameterSets: {} },\n      };\n\n      instance.personalized = true;\n      instance.compareScore = sinon.spy();\n      instance.stories_endpoint = \"stories-endpoint\";\n\n      let fetchStub = globals.sandbox.stub();\n      globals.set(\"fetch\", fetchStub);\n      globals.set(\"NewTabUtils\", {\n        blockedLinks: { isBlocked: globals.sandbox.spy() },\n      });\n      fetchStub.resolves({\n        ok: true,\n        status: 200,\n        json: () => Promise.resolve(response),\n      });\n\n      await instance.fetchStories();\n      assert.calledOnce(instance.compareScore);\n    });\n    it(\"should not sort stories if personalization is preffed off\", async () => {\n      const response = `{\n        \"recommendations\":  [{\"id\" : \"1\"}, {\"id\" : \"2\"}],\n        \"settings\": {\"timeSegments\": {}, \"domainAffinityParameterSets\": {}}\n      }`;\n\n      instance.personalized = false;\n      instance.compareScore = sinon.spy();\n      instance.stories_endpoint = \"stories-endpoint\";\n\n      let fetchStub = globals.sandbox.stub();\n      globals.set(\"fetch\", fetchStub);\n      globals.set(\"NewTabUtils\", {\n        blockedLinks: { isBlocked: globals.sandbox.spy() },\n      });\n      fetchStub.resolves({\n        ok: true,\n        status: 200,\n        json: () => Promise.resolve(response),\n      });\n\n      await instance.fetchStories();\n      assert.notCalled(instance.compareScore);\n    });\n    it(\"should sort items based on relevance score\", () => {\n      let items = [{ score: 0.1 }, { score: 0.2 }];\n      items = items.sort(instance.compareScore);\n      assert.deepEqual(items, [{ score: 0.2 }, { score: 0.1 }]);\n    });\n    it(\"should rotate items if personalization is preffed on\", () => {\n      let items = [\n        { guid: \"g1\" },\n        { guid: \"g2\" },\n        { guid: \"g3\" },\n        { guid: \"g4\" },\n        { guid: \"g5\" },\n        { guid: \"g6\" },\n      ];\n      instance.personalized = true;\n\n      // No impressions should leave items unchanged\n      let rotated = instance.rotate(items);\n      assert.deepEqual(items, rotated);\n\n      // Recent impression should leave items unchanged\n      instance._prefs.get = pref =>\n        pref === REC_IMPRESSION_TRACKING_PREF &&\n        JSON.stringify({ g1: 1, g2: 1, g3: 1 });\n      rotated = instance.rotate(items);\n      assert.deepEqual(items, rotated);\n\n      // Impression older than expiration time should rotate items\n      clock.tick(DEFAULT_RECS_EXPIRE_TIME + 1);\n      rotated = instance.rotate(items);\n      assert.deepEqual(\n        [\n          { guid: \"g4\" },\n          { guid: \"g5\" },\n          { guid: \"g6\" },\n          { guid: \"g1\" },\n          { guid: \"g2\" },\n          { guid: \"g3\" },\n        ],\n        rotated\n      );\n\n      instance._prefs.get = pref =>\n        pref === REC_IMPRESSION_TRACKING_PREF &&\n        JSON.stringify({\n          g1: 1,\n          g2: 1,\n          g3: 1,\n          g4: DEFAULT_RECS_EXPIRE_TIME + 1,\n        });\n      clock.tick(DEFAULT_RECS_EXPIRE_TIME);\n      rotated = instance.rotate(items);\n      assert.deepEqual(\n        [\n          { guid: \"g5\" },\n          { guid: \"g6\" },\n          { guid: \"g1\" },\n          { guid: \"g2\" },\n          { guid: \"g3\" },\n          { guid: \"g4\" },\n        ],\n        rotated\n      );\n    });\n    it(\"should not rotate items if personalization is preffed off\", () => {\n      let items = [\n        { guid: \"g1\" },\n        { guid: \"g2\" },\n        { guid: \"g3\" },\n        { guid: \"g4\" },\n      ];\n\n      instance.personalized = false;\n\n      instance._prefs.get = pref =>\n        pref === REC_IMPRESSION_TRACKING_PREF &&\n        JSON.stringify({ g1: 1, g2: 1, g3: 1 });\n      clock.tick(DEFAULT_RECS_EXPIRE_TIME + 1);\n      let rotated = instance.rotate(items);\n      assert.deepEqual(items, rotated);\n    });\n    it(\"should not dispatch ITEM_RELEVANCE_SCORE_DURATION metrics for not personalized\", () => {\n      instance.personalized = false;\n      instance.dispatchRelevanceScore(50);\n      assert.notCalled(instance.store.dispatch);\n    });\n    it(\"should not dispatch v2 ITEM_RELEVANCE_SCORE_DURATION until initialized\", () => {\n      instance.personalized = true;\n      instance.affinityProviderV2 = { use_v2: true };\n      instance.affinityProvider = {};\n      instance.dispatchRelevanceScore(50);\n      assert.notCalled(instance.store.dispatch);\n      instance.affinityProvider = { initialized: true };\n      instance.dispatchRelevanceScore(50);\n      assert.calledOnce(instance.store.dispatch);\n      const [action] = instance.store.dispatch.firstCall.args;\n      assert.equal(action.type, \"TELEMETRY_PERFORMANCE_EVENT\");\n      assert.equal(\n        action.data.event,\n        \"PERSONALIZATION_V2_ITEM_RELEVANCE_SCORE_DURATION\"\n      );\n    });\n    it(\"should dispatch v1 ITEM_RELEVANCE_SCORE_DURATION\", () => {\n      instance.personalized = true;\n      instance.dispatchRelevanceScore(50);\n      assert.calledOnce(instance.store.dispatch);\n      const [action] = instance.store.dispatch.firstCall.args;\n      assert.equal(action.type, \"TELEMETRY_PERFORMANCE_EVENT\");\n      assert.equal(\n        action.data.event,\n        \"PERSONALIZATION_V1_ITEM_RELEVANCE_SCORE_DURATION\"\n      );\n    });\n    it(\"should record top story impressions\", async () => {\n      instance._prefs = { get: pref => undefined, set: sinon.spy() };\n      instance.personalized = true;\n\n      clock.tick(1);\n      let expectedPrefValue = JSON.stringify({ 1: 1, 2: 1, 3: 1 });\n      instance.onAction({\n        type: at.TELEMETRY_IMPRESSION_STATS,\n        data: {\n          source: \"TOP_STORIES\",\n          tiles: [{ id: 1 }, { id: 2 }, { id: 3 }],\n        },\n      });\n      assert.calledWith(\n        instance._prefs.set.firstCall,\n        REC_IMPRESSION_TRACKING_PREF,\n        expectedPrefValue\n      );\n\n      // Only need to record first impression, so impression pref shouldn't change\n      instance._prefs.get = pref => expectedPrefValue;\n      clock.tick(1);\n      instance.onAction({\n        type: at.TELEMETRY_IMPRESSION_STATS,\n        data: {\n          source: \"TOP_STORIES\",\n          tiles: [{ id: 1 }, { id: 2 }, { id: 3 }],\n        },\n      });\n      assert.calledOnce(instance._prefs.set);\n\n      // New first impressions should be added\n      clock.tick(1);\n      let expectedPrefValueTwo = JSON.stringify({\n        1: 1,\n        2: 1,\n        3: 1,\n        4: 3,\n        5: 3,\n        6: 3,\n      });\n      instance.onAction({\n        type: at.TELEMETRY_IMPRESSION_STATS,\n        data: {\n          source: \"TOP_STORIES\",\n          tiles: [{ id: 4 }, { id: 5 }, { id: 6 }],\n        },\n      });\n      assert.calledWith(\n        instance._prefs.set.secondCall,\n        REC_IMPRESSION_TRACKING_PREF,\n        expectedPrefValueTwo\n      );\n    });\n    it(\"should not record top story impressions for non-view impressions\", async () => {\n      instance._prefs = { get: pref => undefined, set: sinon.spy() };\n      instance.personalized = true;\n\n      instance.onAction({\n        type: at.TELEMETRY_IMPRESSION_STATS,\n        data: { source: \"TOP_STORIES\", click: 0, tiles: [{ id: 1 }] },\n      });\n      assert.notCalled(instance._prefs.set);\n\n      instance.onAction({\n        type: at.TELEMETRY_IMPRESSION_STATS,\n        data: { source: \"TOP_STORIES\", block: 0, tiles: [{ id: 1 }] },\n      });\n      assert.notCalled(instance._prefs.set);\n\n      instance.onAction({\n        type: at.TELEMETRY_IMPRESSION_STATS,\n        data: { source: \"TOP_STORIES\", pocket: 0, tiles: [{ id: 1 }] },\n      });\n      assert.notCalled(instance._prefs.set);\n    });\n    it(\"should clean up top story impressions\", async () => {\n      instance._prefs = {\n        get: pref => JSON.stringify({ 1: 1, 2: 1, 3: 1 }),\n        set: sinon.spy(),\n      };\n\n      let fetchStub = globals.sandbox.stub();\n      globals.set(\"fetch\", fetchStub);\n      globals.set(\"NewTabUtils\", {\n        blockedLinks: { isBlocked: globals.sandbox.spy() },\n      });\n\n      instance.stories_endpoint = \"stories-endpoint\";\n      const response = { recommendations: [{ id: 3 }, { id: 4 }, { id: 5 }] };\n      fetchStub.resolves({\n        ok: true,\n        status: 200,\n        json: () => Promise.resolve(response),\n      });\n      await instance.fetchStories();\n\n      // Should remove impressions for rec 1 and 2 as no longer in the feed\n      assert.calledWith(\n        instance._prefs.set.firstCall,\n        REC_IMPRESSION_TRACKING_PREF,\n        JSON.stringify({ 3: 1 })\n      );\n    });\n    it(\"should re init on affinityProviderV2 pref change\", async () => {\n      sinon.stub(instance, \"uninit\");\n      sinon.stub(instance, \"init\");\n      sinon.stub(instance, \"clearCache\").returns(Promise.resolve());\n      await instance.onAction({\n        type: at.PREF_CHANGED,\n        data: {\n          name: \"feeds.section.topstories.options\",\n          value: JSON.stringify({ version: 2 }),\n        },\n      });\n      assert.calledOnce(instance.uninit);\n      assert.calledOnce(instance.init);\n      assert.calledOnce(instance.clearCache);\n    });\n    it(\"should use UserDomainAffinityProvider from affinityProividerSwitcher not using v2\", async () => {\n      instance.affinityProviderV2 = { use_v2: false };\n      sinon.stub(instance, \"UserDomainAffinityProvider\");\n      sinon.stub(instance, \"PersonalityProvider\");\n\n      await instance.affinityProividerSwitcher();\n      assert.notCalled(instance.PersonalityProvider);\n      assert.calledOnce(instance.UserDomainAffinityProvider);\n    });\n    it(\"should not change provider with badly formed JSON\", async () => {\n      sinon.stub(instance, \"uninit\");\n      sinon.stub(instance, \"init\");\n      sinon.stub(instance, \"clearCache\").returns(Promise.resolve());\n      await instance.onAction({\n        type: at.PREF_CHANGED,\n        data: {\n          name: \"feeds.section.topstories.options\",\n          value: \"{version: 2}\",\n        },\n      });\n      assert.notCalled(instance.uninit);\n      assert.notCalled(instance.init);\n      assert.notCalled(instance.clearCache);\n    });\n    it(\"should use PersonalityProvider from affinityProividerSwitcher using v2\", async () => {\n      instance.affinityProviderV2 = { use_v2: true };\n      sinon.stub(instance, \"UserDomainAffinityProvider\");\n      sinon.stub(instance, \"PersonalityProvider\");\n      instance.PersonalityProvider = () => ({ init: sinon.stub() });\n\n      const provider = instance.affinityProividerSwitcher();\n      assert.calledOnce(provider.init);\n      assert.notCalled(instance.UserDomainAffinityProvider);\n    });\n    it(\"should use init and callback from affinityProividerSwitcher using v2\", async () => {\n      const stories = { recommendations: {} };\n      sinon.stub(instance, \"doContentUpdate\");\n      sinon.stub(instance, \"rotate\").returns(stories);\n      sinon.stub(instance, \"transform\");\n      instance.cache.get = () => ({ stories });\n      instance.cache.set = sinon.spy();\n      instance.affinityProvider = { getAffinities: () => ({}) };\n      await instance.onPersonalityProviderInit();\n\n      assert.calledOnce(instance.doContentUpdate);\n      assert.calledWith(\n        instance.doContentUpdate,\n        { stories: { recommendations: {} } },\n        false\n      );\n      assert.calledOnce(instance.rotate);\n      assert.calledOnce(instance.transform);\n      const { args } = instance.cache.set.firstCall;\n      assert.equal(args[0], \"domainAffinities\");\n      assert.equal(args[1]._timestamp, 0);\n    });\n    it(\"should call dispatchUpdateEvent from affinityProividerSwitcher using v2\", async () => {\n      const stories = { recommendations: {} };\n      sinon.stub(instance, \"rotate\").returns(stories);\n      sinon.stub(instance, \"transform\");\n      sinon.spy(instance, \"dispatchUpdateEvent\");\n      instance.cache.get = () => ({ stories });\n      instance.cache.set = sinon.spy();\n      instance.affinityProvider = { getAffinities: () => ({}) };\n\n      await instance.onPersonalityProviderInit();\n\n      assert.calledOnce(instance.dispatchUpdateEvent);\n    });\n    it(\"should return an object for UserDomainAffinityProvider\", () => {\n      assert.equal(typeof instance.UserDomainAffinityProvider(), \"object\");\n    });\n    it(\"should return an object for PersonalityProvider\", () => {\n      assert.equal(typeof instance.PersonalityProvider(), \"object\");\n    });\n    it(\"should call affinityProividerSwitcher on loadCachedData\", async () => {\n      instance.affinityProviderV2 = true;\n      instance.personalized = true;\n      sinon\n        .stub(instance, \"affinityProividerSwitcher\")\n        .returns(Promise.resolve());\n      const domainAffinities = {\n        parameterSets: {\n          default: {\n            recencyFactor: 0.4,\n            frequencyFactor: 0.5,\n            combinedDomainFactor: 0.5,\n            perfectFrequencyVisits: 10,\n            perfectCombinedDomainScore: 2,\n            multiDomainBoost: 0.1,\n            itemScoreFactor: 0,\n          },\n        },\n        scores: { \"a.com\": 1, \"b.com\": 0.9 },\n        maxHistoryQueryResults: 1000,\n        timeSegments: {},\n        version: \"v1\",\n      };\n\n      instance.cache.get = () => ({ domainAffinities });\n      await instance.loadCachedData();\n      assert.calledOnce(instance.affinityProividerSwitcher);\n    });\n    it(\"should change domainAffinitiesLastUpdated on loadCachedData\", async () => {\n      instance.affinityProviderV2 = true;\n      instance.personalized = true;\n      const domainAffinities = {\n        parameterSets: {\n          default: {\n            recencyFactor: 0.4,\n            frequencyFactor: 0.5,\n            combinedDomainFactor: 0.5,\n            perfectFrequencyVisits: 10,\n            perfectCombinedDomainScore: 2,\n            multiDomainBoost: 0.1,\n            itemScoreFactor: 0,\n          },\n        },\n        scores: { \"a.com\": 1, \"b.com\": 0.9 },\n        maxHistoryQueryResults: 1000,\n        timeSegments: {},\n        version: \"v1\",\n      };\n\n      instance.cache.get = () => ({ domainAffinities });\n      await instance.loadCachedData();\n      assert.notEqual(instance.domainAffinitiesLastUpdated, 0);\n    });\n    it(\"should return false and do nothing if v2 already set\", () => {\n      instance.affinityProviderV2 = { use_v2: true, model_keys: [\"item1orig\"] };\n      const result = instance.processAffinityProividerVersion({\n        version: 2,\n        model_keys: [\"item1\"],\n      });\n      assert.isTrue(instance.affinityProviderV2.use_v2);\n      assert.isFalse(result);\n      assert.equal(instance.affinityProviderV2.model_keys[0], \"item1orig\");\n    });\n    it(\"should return false and do nothing if v1 already set\", () => {\n      instance.affinityProviderV2 = null;\n      const result = instance.processAffinityProividerVersion({ version: 1 });\n      assert.isFalse(result);\n      assert.isNull(instance.affinityProviderV2);\n    });\n    it(\"should return true and set v2\", () => {\n      const result = instance.processAffinityProividerVersion({\n        version: 2,\n        model_keys: [\"item1\"],\n      });\n      assert.isTrue(instance.affinityProviderV2.use_v2);\n      assert.isTrue(result);\n      assert.equal(instance.affinityProviderV2.model_keys[0], \"item1\");\n    });\n    it(\"should return true and set v1\", () => {\n      instance.affinityProviderV2 = {};\n      const result = instance.processAffinityProividerVersion({ version: 1 });\n      assert.isTrue(result);\n      assert.isNull(instance.affinityProviderV2);\n    });\n  });\n  describe(\"#spocs\", async () => {\n    it(\"should not display expired or untimestamped spocs\", async () => {\n      clock.tick(441792000000); // 01/01/1984\n\n      instance.spocsPerNewTabs = 1;\n      instance.show_spocs = true;\n      instance.isBelowFrequencyCap = () => true;\n\n      // NOTE: `expiration_timestamp` is seconds since UNIX epoch\n      instance.spocs = [\n        // No timestamp stays visible\n        {\n          id: \"spoc1\",\n        },\n        // Expired spoc gets filtered out\n        {\n          id: \"spoc2\",\n          expiration_timestamp: 1,\n        },\n        // Far future expiration spoc stays visible\n        {\n          id: \"spoc3\",\n          expiration_timestamp: 32503708800, // 01/01/3000\n        },\n      ];\n\n      sinon.spy(instance, \"filterSpocs\");\n\n      instance.filterSpocs();\n\n      assert.equal(instance.filterSpocs.firstCall.returnValue.length, 2);\n      assert.equal(instance.filterSpocs.firstCall.returnValue[0].id, \"spoc1\");\n      assert.equal(instance.filterSpocs.firstCall.returnValue[1].id, \"spoc3\");\n    });\n    it(\"should insert spoc with provided probability\", async () => {\n      let fetchStub = globals.sandbox.stub();\n      globals.set(\"fetch\", fetchStub);\n      globals.set(\"NewTabUtils\", {\n        blockedLinks: { isBlocked: globals.sandbox.spy() },\n      });\n      instance.dispatchRelevanceScore = () => {};\n\n      const response = {\n        settings: { spocsPerNewTabs: 0.5 },\n        recommendations: [{ guid: \"rec1\" }, { guid: \"rec2\" }, { guid: \"rec3\" }],\n        // Include spocs with a expiration in the very distant future\n        spocs: [\n          { id: \"spoc1\", expiration_timestamp: 9999999999999 },\n          { id: \"spoc2\", expiration_timestamp: 9999999999999 },\n        ],\n      };\n\n      instance.personalized = true;\n      instance.show_spocs = true;\n      instance.stories_endpoint = \"stories-endpoint\";\n      instance.storiesLoaded = true;\n      fetchStub.resolves({\n        ok: true,\n        status: 200,\n        json: () => Promise.resolve(response),\n      });\n      await instance.fetchStories();\n\n      instance.store.getState = () => ({\n        Sections: [{ id: \"topstories\", rows: response.recommendations }],\n        Prefs: { values: { showSponsored: true } },\n      });\n\n      globals.set(\"Math\", {\n        random: () => 0.4,\n        min: Math.min,\n      });\n      instance.dispatchSpocDone = () => {};\n      instance.getPocketState = () => {};\n\n      instance.onAction({\n        type: at.NEW_TAB_REHYDRATED,\n        meta: { fromTarget: {} },\n      });\n      assert.calledOnce(instance.store.dispatch);\n      let [action] = instance.store.dispatch.firstCall.args;\n\n      assert.equal(at.SECTION_UPDATE, action.type);\n      assert.equal(true, action.meta.skipMain);\n      assert.equal(action.data.rows[0].guid, \"rec1\");\n      assert.equal(action.data.rows[1].guid, \"rec2\");\n      assert.equal(action.data.rows[2].guid, \"spoc1\");\n      // Make sure spoc is marked as pinned so it doesn't get removed when preloaded tabs refresh\n      assert.equal(action.data.rows[2].pinned, true);\n\n      // Second new tab shouldn't trigger a section update event (spocsPerNewTab === 0.5)\n      globals.set(\"Math\", {\n        random: () => 0.6,\n        min: Math.min,\n      });\n      instance.onAction({\n        type: at.NEW_TAB_REHYDRATED,\n        meta: { fromTarget: {} },\n      });\n      assert.calledOnce(instance.store.dispatch);\n\n      globals.set(\"Math\", {\n        random: () => 0.3,\n        min: Math.min,\n      });\n      instance.onAction({\n        type: at.NEW_TAB_REHYDRATED,\n        meta: { fromTarget: {} },\n      });\n      assert.calledTwice(instance.store.dispatch);\n      [action] = instance.store.dispatch.secondCall.args;\n      assert.equal(at.SECTION_UPDATE, action.type);\n      assert.equal(true, action.meta.skipMain);\n      assert.equal(action.data.rows[0].guid, \"rec1\");\n      assert.equal(action.data.rows[1].guid, \"rec2\");\n      assert.equal(action.data.rows[2].guid, \"spoc1\");\n      // Make sure spoc is marked as pinned so it doesn't get removed when preloaded tabs refresh\n      assert.equal(action.data.rows[2].pinned, true);\n    });\n    it(\"should delay inserting spoc if stories haven't been fetched\", async () => {\n      let fetchStub = globals.sandbox.stub();\n      instance.dispatchRelevanceScore = () => {};\n      instance.dispatchSpocDone = () => {};\n      sectionsManagerStub.sections.set(\"topstories\", {\n        options: {\n          show_spocs: true,\n          personalized: true,\n          stories_endpoint: \"stories-endpoint\",\n        },\n      });\n      globals.set(\"fetch\", fetchStub);\n      globals.set(\"NewTabUtils\", {\n        blockedLinks: { isBlocked: globals.sandbox.spy() },\n      });\n      globals.set(\"Math\", {\n        random: () => 0.4,\n        min: Math.min,\n        floor: Math.floor,\n      });\n      instance.getPocketState = () => {};\n      instance.dispatchPocketCta = () => {};\n\n      const response = {\n        settings: { spocsPerNewTabs: 0.5 },\n        recommendations: [{ id: \"rec1\" }, { id: \"rec2\" }, { id: \"rec3\" }],\n        // Include one spoc with a expiration in the very distant future\n        spocs: [\n          { id: \"spoc1\", expiration_timestamp: 9999999999999 },\n          { id: \"spoc2\" },\n        ],\n      };\n\n      instance.onAction({\n        type: at.NEW_TAB_REHYDRATED,\n        meta: { fromTarget: {} },\n      });\n      assert.notCalled(instance.store.dispatch);\n      assert.equal(instance.contentUpdateQueue.length, 1);\n\n      instance.spocsPerNewTabs = 0.5;\n      instance.store.getState = () => ({\n        Sections: [{ id: \"topstories\", rows: response.recommendations }],\n        Prefs: { values: { showSponsored: true } },\n      });\n      fetchStub.resolves({\n        ok: true,\n        status: 200,\n        json: () => Promise.resolve(response),\n      });\n\n      await instance.onInit();\n      assert.equal(instance.contentUpdateQueue.length, 0);\n      assert.calledOnce(instance.store.dispatch);\n      let [action] = instance.store.dispatch.firstCall.args;\n      assert.equal(action.type, at.SECTION_UPDATE);\n    });\n    it(\"should not insert spoc if preffed off\", async () => {\n      let fetchStub = globals.sandbox.stub();\n      instance.dispatchSpocDone = () => {};\n      sectionsManagerStub.sections.set(\"topstories\", {\n        options: {\n          show_spocs: false,\n          personalized: true,\n          stories_endpoint: \"stories-endpoint\",\n        },\n      });\n      globals.set(\"fetch\", fetchStub);\n      globals.set(\"NewTabUtils\", {\n        blockedLinks: { isBlocked: globals.sandbox.spy() },\n      });\n      instance.getPocketState = () => {};\n      instance.dispatchPocketCta = () => {};\n\n      const response = {\n        settings: { spocsPerNewTabs: 0.5 },\n        spocs: [{ id: \"spoc1\" }, { id: \"spoc2\" }],\n      };\n      sinon.spy(instance, \"maybeAddSpoc\");\n      sinon.spy(instance, \"shouldShowSpocs\");\n\n      fetchStub.resolves({\n        ok: true,\n        status: 200,\n        json: () => Promise.resolve(response),\n      });\n      await instance.onInit();\n\n      instance.onAction({\n        type: at.NEW_TAB_REHYDRATED,\n        meta: { fromTarget: {} },\n      });\n      assert.calledOnce(instance.maybeAddSpoc);\n      assert.calledOnce(instance.shouldShowSpocs);\n      assert.notCalled(instance.store.dispatch);\n    });\n    it(\"should call dispatchSpocDone when calling maybeAddSpoc\", async () => {\n      instance.dispatchSpocDone = sinon.spy();\n      instance.storiesLoaded = true;\n      await instance.onAction({\n        type: at.NEW_TAB_REHYDRATED,\n        meta: { fromTarget: {} },\n      });\n      assert.calledOnce(instance.dispatchSpocDone);\n      assert.calledWith(instance.dispatchSpocDone, {});\n    });\n    it(\"should fire POCKET_WAITING_FOR_SPOC action with false\", () => {\n      instance.dispatchSpocDone({});\n      assert.calledOnce(instance.store.dispatch);\n      const [action] = instance.store.dispatch.firstCall.args;\n      assert.equal(action.type, \"POCKET_WAITING_FOR_SPOC\");\n      assert.equal(action.data, false);\n    });\n    it(\"should not insert spoc if user opted out\", async () => {\n      let fetchStub = globals.sandbox.stub();\n      instance.dispatchRelevanceScore = () => {};\n      instance.dispatchSpocDone = () => {};\n      sectionsManagerStub.sections.set(\"topstories\", {\n        options: {\n          show_spocs: true,\n          personalized: true,\n          stories_endpoint: \"stories-endpoint\",\n        },\n      });\n      instance.getPocketState = () => {};\n      instance.dispatchPocketCta = () => {};\n      globals.set(\"fetch\", fetchStub);\n      globals.set(\"NewTabUtils\", {\n        blockedLinks: { isBlocked: globals.sandbox.spy() },\n      });\n\n      const response = {\n        settings: { spocsPerNewTabs: 0.5 },\n        spocs: [{ id: \"spoc1\" }, { id: \"spoc2\" }],\n      };\n\n      instance.store.getState = () => ({\n        Sections: [{ id: \"topstories\", rows: response.recommendations }],\n        Prefs: { values: { showSponsored: false } },\n      });\n      fetchStub.resolves({\n        ok: true,\n        status: 200,\n        json: () => Promise.resolve(response),\n      });\n      await instance.onInit();\n\n      instance.onAction({\n        type: at.NEW_TAB_REHYDRATED,\n        meta: { fromTarget: {} },\n      });\n      assert.notCalled(instance.store.dispatch);\n    });\n    it(\"should not fail if there is no spoc\", async () => {\n      let fetchStub = globals.sandbox.stub();\n      instance.dispatchSpocDone = () => {};\n      sectionsManagerStub.sections.set(\"topstories\", {\n        options: {\n          show_spocs: true,\n          personalized: true,\n          stories_endpoint: \"stories-endpoint\",\n        },\n      });\n      instance.getPocketState = () => {};\n      instance.dispatchPocketCta = () => {};\n      globals.set(\"fetch\", fetchStub);\n      globals.set(\"NewTabUtils\", {\n        blockedLinks: { isBlocked: globals.sandbox.spy() },\n      });\n      globals.set(\"Math\", {\n        random: () => 0.4,\n        min: Math.min,\n      });\n\n      const response = {\n        settings: { spocsPerNewTabs: 0.5 },\n        recommendations: [{ id: \"rec1\" }, { id: \"rec2\" }, { id: \"rec3\" }],\n      };\n\n      fetchStub.resolves({\n        ok: true,\n        status: 200,\n        json: () => Promise.resolve(response),\n      });\n      await instance.onInit();\n\n      instance.onAction({\n        type: at.NEW_TAB_REHYDRATED,\n        meta: { fromTarget: {} },\n      });\n      assert.notCalled(instance.store.dispatch);\n    });\n    it(\"should record spoc/campaign impressions for frequency capping\", async () => {\n      let fetchStub = globals.sandbox.stub();\n      globals.set(\"fetch\", fetchStub);\n      globals.set(\"NewTabUtils\", {\n        blockedLinks: { isBlocked: globals.sandbox.spy() },\n      });\n      globals.set(\"Math\", {\n        random: () => 0.4,\n        min: Math.min,\n        floor: Math.floor,\n      });\n\n      const response = {\n        settings: { spocsPerNewTabs: 0.5 },\n        spocs: [{ id: 1, campaign_id: 5 }, { id: 4, campaign_id: 6 }],\n      };\n\n      instance._prefs = { get: pref => undefined, set: sinon.spy() };\n      instance.show_spocs = true;\n      instance.stories_endpoint = \"stories-endpoint\";\n      fetchStub.resolves({\n        ok: true,\n        status: 200,\n        json: () => Promise.resolve(response),\n      });\n      await instance.fetchStories();\n\n      let expectedPrefValue = JSON.stringify({ 5: [0] });\n      instance.onAction({\n        type: at.TELEMETRY_IMPRESSION_STATS,\n        data: {\n          source: \"TOP_STORIES\",\n          tiles: [{ id: 3 }, { id: 2 }, { id: 1 }],\n        },\n      });\n      assert.calledWith(\n        instance._prefs.set.firstCall,\n        SPOC_IMPRESSION_TRACKING_PREF,\n        expectedPrefValue\n      );\n\n      clock.tick(1);\n      instance._prefs.get = pref => expectedPrefValue;\n      let expectedPrefValueCallTwo = JSON.stringify({ 5: [0, 1] });\n      instance.onAction({\n        type: at.TELEMETRY_IMPRESSION_STATS,\n        data: {\n          source: \"TOP_STORIES\",\n          tiles: [{ id: 3 }, { id: 2 }, { id: 1 }],\n        },\n      });\n      assert.calledWith(\n        instance._prefs.set.secondCall,\n        SPOC_IMPRESSION_TRACKING_PREF,\n        expectedPrefValueCallTwo\n      );\n\n      clock.tick(1);\n      instance._prefs.get = pref => expectedPrefValueCallTwo;\n      instance.onAction({\n        type: at.TELEMETRY_IMPRESSION_STATS,\n        data: {\n          source: \"TOP_STORIES\",\n          tiles: [{ id: 3 }, { id: 2 }, { id: 4 }],\n        },\n      });\n      assert.calledWith(\n        instance._prefs.set.thirdCall,\n        SPOC_IMPRESSION_TRACKING_PREF,\n        JSON.stringify({ 5: [0, 1], 6: [2] })\n      );\n    });\n    it(\"should not record spoc/campaign impressions for non-view impressions\", async () => {\n      let fetchStub = globals.sandbox.stub();\n      sectionsManagerStub.sections.set(\"topstories\", {\n        options: {\n          show_spocs: true,\n          stories_endpoint: \"stories-endpoint\",\n        },\n      });\n      globals.set(\"fetch\", fetchStub);\n      globals.set(\"NewTabUtils\", {\n        blockedLinks: { isBlocked: globals.sandbox.spy() },\n      });\n\n      const response = {\n        settings: { spocsPerNewTabs: 0.5 },\n        spocs: [{ id: 1, campaign_id: 5 }, { id: 4, campaign_id: 6 }],\n      };\n\n      instance._prefs = { get: pref => undefined, set: sinon.spy() };\n      fetchStub.resolves({\n        ok: true,\n        status: 200,\n        json: () => Promise.resolve(response),\n      });\n      await instance.onInit();\n\n      instance.onAction({\n        type: at.TELEMETRY_IMPRESSION_STATS,\n        data: { source: \"TOP_STORIES\", click: 0, tiles: [{ id: 1 }] },\n      });\n      assert.notCalled(instance._prefs.set);\n\n      instance.onAction({\n        type: at.TELEMETRY_IMPRESSION_STATS,\n        data: { source: \"TOP_STORIES\", block: 0, tiles: [{ id: 1 }] },\n      });\n      assert.notCalled(instance._prefs.set);\n\n      instance.onAction({\n        type: at.TELEMETRY_IMPRESSION_STATS,\n        data: { source: \"TOP_STORIES\", pocket: 0, tiles: [{ id: 1 }] },\n      });\n      assert.notCalled(instance._prefs.set);\n    });\n    it(\"should clean up spoc/campaign impressions\", async () => {\n      let fetchStub = globals.sandbox.stub();\n      globals.set(\"fetch\", fetchStub);\n      globals.set(\"NewTabUtils\", {\n        blockedLinks: { isBlocked: globals.sandbox.spy() },\n      });\n\n      instance._prefs = { get: pref => undefined, set: sinon.spy() };\n      instance.show_spocs = true;\n      instance.stories_endpoint = \"stories-endpoint\";\n\n      const response = {\n        settings: { spocsPerNewTabs: 0.5 },\n        spocs: [{ id: 1, campaign_id: 5 }, { id: 4, campaign_id: 6 }],\n      };\n      fetchStub.resolves({\n        ok: true,\n        status: 200,\n        json: () => Promise.resolve(response),\n      });\n      await instance.fetchStories();\n\n      // simulate impressions for campaign 5 and 6\n      instance.onAction({\n        type: at.TELEMETRY_IMPRESSION_STATS,\n        data: {\n          source: \"TOP_STORIES\",\n          tiles: [{ id: 3 }, { id: 2 }, { id: 1 }],\n        },\n      });\n      instance._prefs.get = pref =>\n        pref === SPOC_IMPRESSION_TRACKING_PREF && JSON.stringify({ 5: [0] });\n      instance.onAction({\n        type: at.TELEMETRY_IMPRESSION_STATS,\n        data: {\n          source: \"TOP_STORIES\",\n          tiles: [{ id: 3 }, { id: 2 }, { id: 4 }],\n        },\n      });\n\n      let expectedPrefValue = JSON.stringify({ 5: [0], 6: [0] });\n      assert.calledWith(\n        instance._prefs.set.secondCall,\n        SPOC_IMPRESSION_TRACKING_PREF,\n        expectedPrefValue\n      );\n      instance._prefs.get = pref =>\n        pref === SPOC_IMPRESSION_TRACKING_PREF && expectedPrefValue;\n\n      // remove campaign 5 from response\n      const updatedResponse = {\n        settings: { spocsPerNewTabs: 1 },\n        spocs: [{ id: 4, campaign_id: 6 }],\n      };\n      fetchStub.resolves({\n        ok: true,\n        status: 200,\n        json: () => Promise.resolve(updatedResponse),\n      });\n      await instance.fetchStories();\n\n      // should remove campaign 5 from pref as no longer active\n      assert.calledWith(\n        instance._prefs.set.thirdCall,\n        SPOC_IMPRESSION_TRACKING_PREF,\n        JSON.stringify({ 6: [0] })\n      );\n    });\n    it(\"should maintain frequency caps when inserting spocs\", async () => {\n      let fetchStub = globals.sandbox.stub();\n      instance.dispatchRelevanceScore = () => {};\n      instance.dispatchSpocDone = () => {};\n      sectionsManagerStub.sections.set(\"topstories\", {\n        options: {\n          show_spocs: true,\n          personalized: true,\n          stories_endpoint: \"stories-endpoint\",\n        },\n      });\n      instance.getPocketState = () => {};\n      instance.dispatchPocketCta = () => {};\n      globals.set(\"fetch\", fetchStub);\n      globals.set(\"NewTabUtils\", {\n        blockedLinks: { isBlocked: globals.sandbox.spy() },\n      });\n\n      const response = {\n        settings: { spocsPerNewTabs: 1 },\n        recommendations: [{ guid: \"rec1\" }, { guid: \"rec2\" }, { guid: \"rec3\" }],\n        spocs: [\n          // Set spoc `expiration_timestamp`s in the very distant future to ensure they show up\n          {\n            id: \"spoc1\",\n            campaign_id: 1,\n            caps: { lifetime: 3, campaign: { count: 2, period: 3600 } },\n            expiration_timestamp: 999999999999,\n          },\n          {\n            id: \"spoc2\",\n            campaign_id: 2,\n            caps: { lifetime: 1 },\n            expiration_timestamp: 999999999999,\n          },\n        ],\n      };\n\n      instance.store.getState = () => ({\n        Sections: [{ id: \"topstories\", rows: response.recommendations }],\n        Prefs: { values: { showSponsored: true } },\n      });\n      fetchStub.resolves({\n        ok: true,\n        status: 200,\n        json: () => Promise.resolve(response),\n      });\n      await instance.onInit();\n      instance.spocsPerNewTabs = 1;\n\n      clock.tick();\n      instance.onAction({\n        type: at.NEW_TAB_REHYDRATED,\n        meta: { fromTarget: {} },\n      });\n      let [action] = instance.store.dispatch.firstCall.args;\n      assert.equal(action.data.rows[0].guid, \"rec1\");\n      assert.equal(action.data.rows[1].guid, \"rec2\");\n      assert.equal(action.data.rows[2].guid, \"spoc1\");\n      instance._prefs.get = pref => JSON.stringify({ 1: [1] });\n\n      clock.tick();\n      instance.onAction({\n        type: at.NEW_TAB_REHYDRATED,\n        meta: { fromTarget: {} },\n      });\n      [action] = instance.store.dispatch.secondCall.args;\n      assert.equal(action.data.rows[0].guid, \"rec1\");\n      assert.equal(action.data.rows[1].guid, \"rec2\");\n      assert.equal(action.data.rows[2].guid, \"spoc1\");\n      instance._prefs.get = pref => JSON.stringify({ 1: [1, 2] });\n\n      // campaign 1 period frequency cap now reached (spoc 2 should be shown)\n      clock.tick();\n      instance.onAction({\n        type: at.NEW_TAB_REHYDRATED,\n        meta: { fromTarget: {} },\n      });\n      [action] = instance.store.dispatch.thirdCall.args;\n      assert.equal(action.data.rows[0].guid, \"rec1\");\n      assert.equal(action.data.rows[1].guid, \"rec2\");\n      assert.equal(action.data.rows[2].guid, \"spoc2\");\n      instance._prefs.get = pref => JSON.stringify({ 1: [1, 2], 2: [3] });\n\n      // new campaign 1 period starting (spoc 1 sohuld be shown again)\n      clock.tick(2 * 60 * 60 * 1000);\n      instance.onAction({\n        type: at.NEW_TAB_REHYDRATED,\n        meta: { fromTarget: {} },\n      });\n      [action] = instance.store.dispatch.lastCall.args;\n      assert.equal(action.data.rows[0].guid, \"rec1\");\n      assert.equal(action.data.rows[1].guid, \"rec2\");\n      assert.equal(action.data.rows[2].guid, \"spoc1\");\n      instance._prefs.get = pref =>\n        JSON.stringify({ 1: [1, 2, 7200003], 2: [3] });\n\n      // campaign 1 lifetime cap now reached (no spoc should be sent)\n      instance.onAction({\n        type: at.NEW_TAB_REHYDRATED,\n        meta: { fromTarget: {} },\n      });\n      assert.callCount(instance.store.dispatch, 4);\n    });\n    it(\"should maintain client-side MAX_LIFETIME_CAP\", async () => {\n      let fetchStub = globals.sandbox.stub();\n      instance.dispatchRelevanceScore = () => {};\n      instance.dispatchSpocDone = () => {};\n      sectionsManagerStub.sections.set(\"topstories\", {\n        options: {\n          show_spocs: true,\n          personalized: true,\n          stories_endpoint: \"stories-endpoint\",\n        },\n      });\n      globals.set(\"fetch\", fetchStub);\n      globals.set(\"NewTabUtils\", {\n        blockedLinks: { isBlocked: globals.sandbox.spy() },\n      });\n      instance.getPocketState = () => {};\n      instance.dispatchPocketCta = () => {};\n\n      const response = {\n        settings: { spocsPerNewTabs: 1 },\n        recommendations: [{ guid: \"rec1\" }, { guid: \"rec2\" }, { guid: \"rec3\" }],\n        spocs: [{ id: \"spoc1\", campaign_id: 1, caps: { lifetime: 501 } }],\n      };\n\n      instance.store.getState = () => ({\n        Sections: [{ id: \"topstories\", rows: response.recommendations }],\n        Prefs: { values: { showSponsored: true } },\n      });\n      fetchStub.resolves({\n        ok: true,\n        status: 200,\n        json: () => Promise.resolve(response),\n      });\n      await instance.onInit();\n\n      instance._prefs.get = pref =>\n        JSON.stringify({ 1: [...Array(500).keys()] });\n      instance.onAction({\n        type: at.NEW_TAB_REHYDRATED,\n        meta: { fromTarget: {} },\n      });\n      assert.notCalled(instance.store.dispatch);\n    });\n  });\n  describe(\"#update\", () => {\n    it(\"should fetch stories after update interval\", async () => {\n      await instance.onInit();\n      sinon.spy(instance, \"fetchStories\");\n      await instance.onAction({ type: at.SYSTEM_TICK });\n      assert.notCalled(instance.fetchStories);\n\n      clock.tick(STORIES_UPDATE_TIME);\n      await instance.onAction({ type: at.SYSTEM_TICK });\n      assert.calledOnce(instance.fetchStories);\n    });\n    it(\"should fetch topics after update interval\", async () => {\n      await instance.onInit();\n      sinon.spy(instance, \"fetchTopics\");\n      await instance.onAction({ type: at.SYSTEM_TICK });\n      assert.notCalled(instance.fetchTopics);\n\n      clock.tick(TOPICS_UPDATE_TIME);\n      await instance.onAction({ type: at.SYSTEM_TICK });\n      assert.calledOnce(instance.fetchTopics);\n    });\n    it(\"should return updated stories and topics on system tick\", async () => {\n      await instance.onInit();\n      sinon.spy(instance, \"dispatchUpdateEvent\");\n      const stories = [{ guid: \"rec1\" }, { guid: \"rec2\" }, { guid: \"rec3\" }];\n      const topics = [\n        { name: \"topic1\", url: \"url-topic1\" },\n        { name: \"topic2\", url: \"url-topic2\" },\n      ];\n      clock.tick(TOPICS_UPDATE_TIME);\n      globals.sandbox.stub(instance, \"fetchStories\").resolves(stories);\n      globals.sandbox.stub(instance, \"fetchTopics\").resolves(topics);\n\n      await instance.onAction({ type: at.SYSTEM_TICK });\n\n      assert.calledOnce(instance.dispatchUpdateEvent);\n      assert.calledWith(instance.dispatchUpdateEvent, false, {\n        rows: [{ guid: \"rec1\" }, { guid: \"rec2\" }, { guid: \"rec3\" }],\n        topics: [\n          { name: \"topic1\", url: \"url-topic1\" },\n          { name: \"topic2\", url: \"url-topic2\" },\n        ],\n        read_more_endpoint: undefined,\n      });\n    });\n    it(\"should update domain affinities on idle-daily, if personalization preffed on\", async () => {\n      instance.init();\n      instance.affinityProvider = undefined;\n      instance.cache.set = sinon.spy();\n\n      instance.observe(\"\", \"idle-daily\");\n      assert.isUndefined(instance.affinityProvider);\n\n      instance.personalized = true;\n      instance.updateSettings({\n        timeSegments: {},\n        domainAffinityParameterSets: {},\n      });\n      clock.tick(MIN_DOMAIN_AFFINITIES_UPDATE_TIME);\n      await instance.observe(\"\", \"idle-daily\");\n      assert.isDefined(instance.affinityProvider);\n      assert.calledOnce(instance.cache.set);\n      assert.calledWith(\n        instance.cache.set,\n        \"domainAffinities\",\n        Object.assign({}, instance.affinityProvider.getAffinities(), {\n          _timestamp: MIN_DOMAIN_AFFINITIES_UPDATE_TIME,\n        })\n      );\n    });\n    it(\"should not update domain affinities too often\", () => {\n      instance.init();\n      instance.affinityProvider = undefined;\n      instance.cache.set = sinon.spy();\n\n      instance.personalized = true;\n      instance.updateSettings({\n        timeSegments: {},\n        domainAffinityParameterSets: {},\n      });\n      clock.tick(MIN_DOMAIN_AFFINITIES_UPDATE_TIME);\n      instance.domainAffinitiesLastUpdated = Date.now();\n      instance.observe(\"\", \"idle-daily\");\n      assert.isUndefined(instance.affinityProvider);\n    });\n    it(\"should send performance telemetry when updating domain affinities\", async () => {\n      instance.getPocketState = () => {};\n      instance.dispatchPocketCta = () => {};\n      instance.init();\n      instance.personalized = true;\n      clock.tick(MIN_DOMAIN_AFFINITIES_UPDATE_TIME);\n      instance.updateSettings({\n        timeSegments: {},\n        domainAffinityParameterSets: {},\n      });\n      await instance.observe(\"\", \"idle-daily\");\n\n      assert.calledOnce(instance.store.dispatch);\n      let [action] = instance.store.dispatch.firstCall.args;\n      assert.equal(action.type, at.TELEMETRY_PERFORMANCE_EVENT);\n      assert.equal(\n        action.data.event,\n        \"topstories.domain.affinity.calculation.ms\"\n      );\n    });\n    it(\"should add idle-daily observer right away, before waiting on init data\", async () => {\n      const addObserver = globals.sandbox.stub();\n      globals.set(\"Services\", { obs: { addObserver } });\n      const initPromise = instance.onInit();\n      assert.calledOnce(addObserver);\n      await initPromise;\n    });\n    it(\"should not call init and uninit if data doesn't match on options change \", () => {\n      sinon.spy(instance, \"init\");\n      sinon.spy(instance, \"uninit\");\n      instance.onAction({ type: at.SECTION_OPTIONS_CHANGED, data: \"foo\" });\n      assert.notCalled(sectionsManagerStub.disableSection);\n      assert.notCalled(sectionsManagerStub.enableSection);\n      assert.notCalled(instance.init);\n      assert.notCalled(instance.uninit);\n    });\n    it(\"should call init and uninit on options change\", async () => {\n      sinon.stub(instance, \"clearCache\").returns(Promise.resolve());\n      sinon.spy(instance, \"init\");\n      sinon.spy(instance, \"uninit\");\n      await instance.onAction({\n        type: at.SECTION_OPTIONS_CHANGED,\n        data: \"topstories\",\n      });\n      assert.calledOnce(sectionsManagerStub.disableSection);\n      assert.calledOnce(sectionsManagerStub.enableSection);\n      assert.calledOnce(instance.clearCache);\n      assert.calledOnce(instance.init);\n      assert.calledOnce(instance.uninit);\n    });\n    it(\"should set LastUpdated to 0 on init\", async () => {\n      instance.storiesLastUpdated = 1;\n      instance.topicsLastUpdated = 1;\n\n      await instance.onInit();\n      assert.equal(instance.storiesLastUpdated, 0);\n      assert.equal(instance.topicsLastUpdated, 0);\n    });\n    it(\"should filter spocs when link is blocked\", async () => {\n      instance.spocs = [{ url: \"not_blocked\" }, { url: \"blocked\" }];\n      await instance.onAction({\n        type: at.PLACES_LINK_BLOCKED,\n        data: { url: \"blocked\" },\n      });\n\n      assert.deepEqual(instance.spocs, [{ url: \"not_blocked\" }]);\n    });\n    it(\"should reset domain affinity scores if version changed\", async () => {\n      instance.init();\n      instance.personalized = true;\n      instance.resetDomainAffinityScores = sinon.spy();\n      instance.updateSettings({\n        timeSegments: {},\n        domainAffinityParameterSets: {},\n        version: \"1\",\n      });\n      clock.tick(MIN_DOMAIN_AFFINITIES_UPDATE_TIME);\n      await instance.observe(\"\", \"idle-daily\");\n      assert.notCalled(instance.resetDomainAffinityScores);\n\n      instance.updateSettings({\n        timeSegments: {},\n        domainAffinityParameterSets: {},\n        version: \"2\",\n      });\n      assert.calledOnce(instance.resetDomainAffinityScores);\n    });\n  });\n  describe(\"#loadCachedData\", () => {\n    it(\"should update section with cached stories and topics if available\", async () => {\n      sectionsManagerStub.sections.set(\"topstories\", {\n        options: { stories_referrer: \"referrer\" },\n      });\n      const stories = {\n        _timestamp: 123,\n        recommendations: [\n          {\n            id: \"1\",\n            title: \"title\",\n            excerpt: \"description\",\n            image_src: \"image-url\",\n            url: \"rec-url\",\n            published_timestamp: \"123\",\n            context: \"trending\",\n            icon: \"icon\",\n            item_score: 0.98,\n          },\n        ],\n      };\n      const transformedStories = [\n        {\n          guid: \"1\",\n          type: \"now\",\n          title: \"title\",\n          context: \"trending\",\n          icon: \"icon\",\n          description: \"description\",\n          image: \"image-url\",\n          referrer: \"referrer\",\n          url: \"rec-url\",\n          hostname: \"rec-url\",\n          min_score: 0,\n          score: 0.98,\n          spoc_meta: {},\n        },\n      ];\n      const topics = {\n        _timestamp: 123,\n        topics: [\n          { name: \"topic1\", url: \"url-topic1\" },\n          { name: \"topic2\", url: \"url-topic2\" },\n        ],\n      };\n      instance.cache.get = () => ({ stories, topics });\n      globals.set(\"NewTabUtils\", {\n        blockedLinks: { isBlocked: globals.sandbox.spy() },\n      });\n\n      await instance.onInit();\n      assert.calledOnce(sectionsManagerStub.updateSection);\n      assert.calledWith(sectionsManagerStub.updateSection, SECTION_ID, {\n        rows: transformedStories,\n        topics: topics.topics,\n        read_more_endpoint: undefined,\n      });\n    });\n    it(\"should NOT update section if there is no cached data\", async () => {\n      instance.cache.get = () => ({});\n      globals.set(\"NewTabUtils\", {\n        blockedLinks: { isBlocked: globals.sandbox.spy() },\n      });\n      await instance.loadCachedData();\n      assert.notCalled(sectionsManagerStub.updateSection);\n    });\n    it(\"should use store rows if no stories sent to doContentUpdate\", async () => {\n      instance.store = {\n        getState() {\n          return {\n            Sections: [{ id: \"topstories\", rows: [1, 2, 3] }],\n          };\n        },\n      };\n      sinon.spy(instance, \"dispatchUpdateEvent\");\n\n      instance.doContentUpdate({}, false);\n\n      assert.calledOnce(instance.dispatchUpdateEvent);\n      assert.calledWith(instance.dispatchUpdateEvent, false, {\n        rows: [1, 2, 3],\n      });\n    });\n    it(\"should broadcast in doContentUpdate when updating from cache\", async () => {\n      sectionsManagerStub.sections.set(\"topstories\", {\n        options: { stories_referrer: \"referrer\" },\n      });\n      globals.set(\"NewTabUtils\", { blockedLinks: { isBlocked: () => {} } });\n      const stories = { recommendations: [{}] };\n      const topics = { topics: [{}] };\n      sinon.spy(instance, \"doContentUpdate\");\n      instance.cache.get = () => ({ stories, topics });\n      await instance.onInit();\n      assert.calledOnce(instance.doContentUpdate);\n      assert.calledWith(\n        instance.doContentUpdate,\n        {\n          stories: [\n            {\n              context: undefined,\n              description: undefined,\n              guid: undefined,\n              hostname: undefined,\n              icon: undefined,\n              image: undefined,\n              min_score: 0,\n              referrer: \"referrer\",\n              score: 1,\n              spoc_meta: {},\n              title: undefined,\n              type: \"trending\",\n              url: undefined,\n            },\n          ],\n          topics: [{}],\n        },\n        true\n      );\n    });\n    it(\"should initialize user domain affinity provider from cache if personalization is preffed on\", async () => {\n      const domainAffinities = {\n        parameterSets: {\n          default: {\n            recencyFactor: 0.4,\n            frequencyFactor: 0.5,\n            combinedDomainFactor: 0.5,\n            perfectFrequencyVisits: 10,\n            perfectCombinedDomainScore: 2,\n            multiDomainBoost: 0.1,\n            itemScoreFactor: 0,\n          },\n        },\n        scores: { \"a.com\": 1, \"b.com\": 0.9 },\n        maxHistoryQueryResults: 1000,\n        timeSegments: {},\n        version: \"v1\",\n      };\n\n      instance.affinityProvider = undefined;\n      instance.cache.get = () => ({ domainAffinities });\n\n      await instance.loadCachedData();\n      assert.isUndefined(instance.affinityProvider);\n      instance.personalized = true;\n      await instance.loadCachedData();\n      assert.isDefined(instance.affinityProvider);\n      assert.deepEqual(\n        instance.affinityProvider.timeSegments,\n        domainAffinities.timeSegments\n      );\n      assert.equal(\n        instance.affinityProvider.maxHistoryQueryResults,\n        domainAffinities.maxHistoryQueryResults\n      );\n      assert.deepEqual(\n        instance.affinityProvider.parameterSets,\n        domainAffinities.parameterSets\n      );\n      assert.deepEqual(\n        instance.affinityProvider.scores,\n        domainAffinities.scores\n      );\n      assert.deepEqual(\n        instance.affinityProvider.version,\n        domainAffinities.version\n      );\n    });\n    it(\"should clear domain affinity cache when history is cleared\", () => {\n      instance.cache.set = sinon.spy();\n      instance.affinityProvider = {};\n      instance.personalized = true;\n\n      instance.onAction({ type: at.PLACES_HISTORY_CLEARED });\n      assert.calledWith(instance.cache.set, \"domainAffinities\", {});\n      assert.isUndefined(instance.affinityProvider);\n    });\n  });\n  describe(\"#pocket\", () => {\n    it(\"should call getPocketState when hitting NEW_TAB_REHYDRATED\", () => {\n      instance.getPocketState = sinon.spy();\n      instance.onAction({\n        type: at.NEW_TAB_REHYDRATED,\n        meta: { fromTarget: {} },\n      });\n      assert.calledOnce(instance.getPocketState);\n      assert.calledWith(instance.getPocketState, {});\n    });\n    it(\"should call dispatch in getPocketState\", () => {\n      const isUserLoggedIn = sinon.spy();\n      globals.set(\"pktApi\", { isUserLoggedIn });\n      instance.getPocketState({});\n      assert.calledOnce(instance.store.dispatch);\n      const [action] = instance.store.dispatch.firstCall.args;\n      assert.equal(action.type, \"POCKET_LOGGED_IN\");\n      assert.calledOnce(isUserLoggedIn);\n    });\n    it(\"should call dispatchPocketCta when hitting onInit\", async () => {\n      instance.dispatchPocketCta = sinon.spy();\n      await instance.onInit();\n      assert.calledOnce(instance.dispatchPocketCta);\n      assert.calledWith(\n        instance.dispatchPocketCta,\n        JSON.stringify({\n          cta_button: \"\",\n          cta_text: \"\",\n          cta_url: \"\",\n          use_cta: false,\n        }),\n        false\n      );\n    });\n    it(\"should call dispatch in dispatchPocketCta\", () => {\n      instance.dispatchPocketCta(JSON.stringify({ use_cta: true }), false);\n      assert.calledOnce(instance.store.dispatch);\n      const [action] = instance.store.dispatch.firstCall.args;\n      assert.equal(action.type, \"POCKET_CTA\");\n      assert.equal(action.data.use_cta, true);\n    });\n    it(\"should call dispatchPocketCta with a pocketCta pref change\", () => {\n      instance.dispatchPocketCta = sinon.spy();\n      instance.onAction({\n        type: at.PREF_CHANGED,\n        data: {\n          name: \"pocketCta\",\n          value: JSON.stringify({\n            cta_button: \"\",\n            cta_text: \"\",\n            cta_url: \"\",\n            use_cta: false,\n          }),\n        },\n      });\n      assert.calledOnce(instance.dispatchPocketCta);\n      assert.calledWith(\n        instance.dispatchPocketCta,\n        JSON.stringify({\n          cta_button: \"\",\n          cta_text: \"\",\n          cta_url: \"\",\n          use_cta: false,\n        }),\n        true\n      );\n    });\n  });\n  it(\"should call uninit and init on disabling of showSponsored pref\", async () => {\n    sinon.stub(instance, \"clearCache\").returns(Promise.resolve());\n    sinon.stub(instance, \"uninit\");\n    sinon.stub(instance, \"init\");\n    await instance.onAction({\n      type: at.PREF_CHANGED,\n      data: { name: \"showSponsored\", value: false },\n    });\n    assert.calledOnce(instance.clearCache);\n    assert.calledOnce(instance.uninit);\n    assert.calledOnce(instance.init);\n  });\n});\n"
  },
  {
    "path": "test/unit/lib/UTEventReporting.test.js",
    "content": "import {\n  UTSessionPing,\n  UTTrailheadEnrollPing,\n  UTUserEventPing,\n} from \"test/schemas/pings\";\nimport { GlobalOverrider } from \"test/unit/utils\";\nimport { UTEventReporting } from \"lib/UTEventReporting.jsm\";\n\nconst FAKE_EVENT_PING_PC = {\n  event: \"CLICK\",\n  source: \"TOP_SITES\",\n  addon_version: \"123\",\n  user_prefs: 63,\n  session_id: \"abc\",\n  page: \"about:newtab\",\n  action_position: 5,\n  locale: \"en-US\",\n};\nconst FAKE_SESSION_PING_PC = {\n  session_duration: 1234,\n  addon_version: \"123\",\n  user_prefs: 63,\n  session_id: \"abc\",\n  page: \"about:newtab\",\n  locale: \"en-US\",\n};\nconst FAKE_EVENT_PING_UT = [\n  \"activity_stream\",\n  \"event\",\n  \"CLICK\",\n  \"TOP_SITES\",\n  {\n    addon_version: \"123\",\n    user_prefs: \"63\",\n    session_id: \"abc\",\n    page: \"about:newtab\",\n    action_position: \"5\",\n  },\n];\nconst FAKE_SESSION_PING_UT = [\n  \"activity_stream\",\n  \"end\",\n  \"session\",\n  \"1234\",\n  {\n    addon_version: \"123\",\n    user_prefs: \"63\",\n    session_id: \"abc\",\n    page: \"about:newtab\",\n  },\n];\nconst FAKE_TRAILHEAD_ENROLL_EVENT = {\n  experiment: \"activity-stream-trailhead-firstrun-interrupts\",\n  type: \"as-firstrun\",\n  branch: \"supercharge\",\n};\nconst FAKE_TRAILHEAD_ENROLL_EVENT_UT = [\n  \"activity_stream\",\n  \"enroll\",\n  \"preference_study\",\n  \"activity-stream-trailhead-firstrun-interrupts\",\n  {\n    experimentType: \"as-firstrun\",\n    branch: \"supercharge\",\n  },\n];\n\ndescribe(\"UTEventReporting\", () => {\n  let globals;\n  let sandbox;\n  let utEvents;\n\n  beforeEach(() => {\n    globals = new GlobalOverrider();\n    sandbox = globals.sandbox;\n    sandbox.stub(global.Services.telemetry, \"setEventRecordingEnabled\");\n    sandbox.stub(global.Services.telemetry, \"recordEvent\");\n\n    utEvents = new UTEventReporting();\n  });\n\n  afterEach(() => {\n    globals.restore();\n  });\n\n  describe(\"#sendUserEvent()\", () => {\n    it(\"should queue up the correct data to send to Events Telemetry\", async () => {\n      utEvents.sendUserEvent(FAKE_EVENT_PING_PC);\n      assert.calledWithExactly(\n        global.Services.telemetry.recordEvent,\n        ...FAKE_EVENT_PING_UT\n      );\n\n      let ping = global.Services.telemetry.recordEvent.firstCall.args;\n      assert.validate(ping, UTUserEventPing);\n    });\n  });\n\n  describe(\"#sendSessionEndEvent()\", () => {\n    it(\"should queue up the correct data to send to Events Telemetry\", async () => {\n      utEvents.sendSessionEndEvent(FAKE_SESSION_PING_PC);\n      assert.calledWithExactly(\n        global.Services.telemetry.recordEvent,\n        ...FAKE_SESSION_PING_UT\n      );\n\n      let ping = global.Services.telemetry.recordEvent.firstCall.args;\n      assert.validate(ping, UTSessionPing);\n    });\n  });\n\n  describe(\"#sendTrailheadEnrollEvent()\", () => {\n    it(\"should queue up the correct data to send to Events Telemetry\", async () => {\n      utEvents.sendTrailheadEnrollEvent(FAKE_TRAILHEAD_ENROLL_EVENT);\n      assert.calledWithExactly(\n        global.Services.telemetry.recordEvent,\n        ...FAKE_TRAILHEAD_ENROLL_EVENT_UT\n      );\n\n      let ping = global.Services.telemetry.recordEvent.firstCall.args;\n      assert.validate(ping, UTTrailheadEnrollPing);\n    });\n  });\n\n  describe(\"#uninit()\", () => {\n    it(\"should call setEventRecordingEnabled with a false value\", () => {\n      assert.equal(\n        global.Services.telemetry.setEventRecordingEnabled.firstCall.args[0],\n        \"activity_stream\"\n      );\n      assert.equal(\n        global.Services.telemetry.setEventRecordingEnabled.firstCall.args[1],\n        true\n      );\n\n      utEvents.uninit();\n      assert.equal(\n        global.Services.telemetry.setEventRecordingEnabled.secondCall.args[0],\n        \"activity_stream\"\n      );\n      assert.equal(\n        global.Services.telemetry.setEventRecordingEnabled.secondCall.args[1],\n        false\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/lib/UserDomainAffinityProvider.test.js",
    "content": "import { GlobalOverrider } from \"test/unit/utils\";\nimport { UserDomainAffinityProvider } from \"lib/UserDomainAffinityProvider.jsm\";\n\nconst TIME_SEGMENTS = [\n  { id: \"hour\", startTime: 3600, endTime: 0, weightPosition: 1 },\n  { id: \"day\", startTime: 86400, endTime: 3600, weightPosition: 0.75 },\n  { id: \"week\", startTime: 604800, endTime: 86400, weightPosition: 0.5 },\n  { id: \"weekPlus\", startTime: null, endTime: 604800, weightPosition: 0.25 },\n];\n\nconst PARAMETER_SETS = {\n  paramSet1: {\n    recencyFactor: 0.5,\n    frequencyFactor: 0.5,\n    combinedDomainFactor: 0.5,\n    perfectFrequencyVisits: 10,\n    perfectCombinedDomainScore: 2,\n    multiDomainBoost: 0.1,\n    itemScoreFactor: 0,\n  },\n  paramSet2: {\n    recencyFactor: 1,\n    frequencyFactor: 0.7,\n    combinedDomainFactor: 0.8,\n    perfectFrequencyVisits: 10,\n    perfectCombinedDomainScore: 2,\n    multiDomainBoost: 0.1,\n    itemScoreFactor: 0,\n  },\n};\n\ndescribe(\"User Domain Affinity Provider\", () => {\n  let instance;\n  let globals;\n\n  beforeEach(() => {\n    globals = new GlobalOverrider();\n\n    const testUrl = \"www.somedomain.com\";\n    globals.sandbox\n      .stub(global.Services.io, \"newURI\")\n      .returns({ host: testUrl });\n\n    globals.sandbox.stub(global.PlacesUtils.history, \"executeQuery\").returns({\n      root: {\n        childCount: 1,\n        getChild: index => ({ uri: testUrl, accessCount: 1 }),\n      },\n    });\n    globals.sandbox\n      .stub(global.PlacesUtils.history, \"getNewQuery\")\n      .returns({ TIME_RELATIVE_NOW: 1 });\n    globals.sandbox\n      .stub(global.PlacesUtils.history, \"getNewQueryOptions\")\n      .returns({});\n\n    instance = new UserDomainAffinityProvider(TIME_SEGMENTS, PARAMETER_SETS);\n  });\n  afterEach(() => {\n    globals.restore();\n  });\n  describe(\"#init\", () => {\n    function calculateScore(visitCounts, timeSeg, domain, ps) {\n      const vc = visitCounts[timeSeg][domain];\n      const score = instance.calculateScore(\n        vc * Number(ps.timeSegmentWeights[timeSeg]),\n        ps.perfectFrequencyVisits,\n        ps.frequencyFactor\n      );\n      return Math.min(1, score);\n    }\n    it(\"should create a UserDomainAffinityProvider\", () => {\n      assert.instanceOf(instance, UserDomainAffinityProvider);\n    });\n    it(\"should calculate time segment weights for parameter sets\", () => {\n      const expectedParamSets = Object.assign({}, PARAMETER_SETS);\n\n      // Verify that parameter set specific recencyFactor was applied\n      expectedParamSets.paramSet1.timeSegmentWeights = {\n        hour: 1,\n        day: 0.75,\n        week: 0.5,\n        weekPlus: 0.25,\n      };\n      expectedParamSets.paramSet2.timeSegmentWeights = {\n        hour: 1,\n        day: 1,\n        week: 1,\n        weekPlus: 1,\n      };\n      assert.deepEqual(expectedParamSets, instance.parameterSets);\n    });\n    it(\"should calculate user domain affinity scores\", () => {\n      const ps1 = instance.parameterSets.paramSet1;\n      const ps2 = instance.parameterSets.paramSet2;\n\n      const visitCounts = {\n        hour: { \"a.com\": 1, \"b.com\": 2 },\n        day: { \"a.com\": 4 },\n        week: { \"c.com\": 1 },\n        weekPlus: { \"a.com\": 1, \"d.com\": 3 },\n      };\n      instance.queryVisits = ts => visitCounts[ts.id];\n\n      const expScoreAHourPs1 = calculateScore(\n        visitCounts,\n        \"hour\",\n        \"a.com\",\n        ps1\n      );\n      const expScoreBHourPs1 = calculateScore(\n        visitCounts,\n        \"hour\",\n        \"b.com\",\n        ps1\n      );\n      const expScoreAHourPs2 = calculateScore(\n        visitCounts,\n        \"hour\",\n        \"a.com\",\n        ps2\n      );\n      const expScoreBHourPs2 = calculateScore(\n        visitCounts,\n        \"hour\",\n        \"b.com\",\n        ps2\n      );\n      const expScoreADayPs1 = calculateScore(visitCounts, \"day\", \"a.com\", ps1);\n      const expScoreADayPs2 = calculateScore(visitCounts, \"day\", \"a.com\", ps2);\n      const expScoreCWeekPs1 = calculateScore(\n        visitCounts,\n        \"week\",\n        \"c.com\",\n        ps1\n      );\n      const expScoreCWeekPs2 = calculateScore(\n        visitCounts,\n        \"week\",\n        \"c.com\",\n        ps2\n      );\n      const expScoreAWeekPlusPs1 = calculateScore(\n        visitCounts,\n        \"weekPlus\",\n        \"a.com\",\n        ps1\n      );\n      const expScoreAWeekPlusPs2 = calculateScore(\n        visitCounts,\n        \"weekPlus\",\n        \"a.com\",\n        ps2\n      );\n      const expScoreDWeekPlusPs1 = calculateScore(\n        visitCounts,\n        \"weekPlus\",\n        \"d.com\",\n        ps1\n      );\n      const expScoreDWeekPlusPs2 = calculateScore(\n        visitCounts,\n        \"weekPlus\",\n        \"d.com\",\n        ps2\n      );\n      const expectedScores = {\n        \"a.com\": {\n          paramSet1: Math.min(\n            1,\n            expScoreAHourPs1 + expScoreADayPs1 + expScoreAWeekPlusPs1\n          ),\n          paramSet2: Math.min(\n            1,\n            expScoreAHourPs2 + expScoreADayPs2 + expScoreAWeekPlusPs2\n          ),\n        },\n        \"b.com\": { paramSet1: expScoreBHourPs1, paramSet2: expScoreBHourPs2 },\n        \"c.com\": { paramSet1: expScoreCWeekPs1, paramSet2: expScoreCWeekPs2 },\n        \"d.com\": {\n          paramSet1: expScoreDWeekPlusPs1,\n          paramSet2: expScoreDWeekPlusPs2,\n        },\n      };\n\n      const scores = instance.calculateAllUserDomainAffinityScores();\n      assert.deepEqual(expectedScores, scores);\n    });\n    it(\"should return domain affinities\", () => {\n      const scores = {\n        \"a.com\": {\n          paramSet1: 1,\n          paramSet2: 0.9,\n        },\n      };\n      instance = new UserDomainAffinityProvider(\n        TIME_SEGMENTS,\n        PARAMETER_SETS,\n        100,\n        \"v1\",\n        scores\n      );\n\n      const expectedAffinities = {\n        timeSegments: TIME_SEGMENTS,\n        parameterSets: PARAMETER_SETS,\n        maxHistoryQueryResults: 100,\n        scores,\n        version: \"v1\",\n      };\n      assert.deepEqual(instance.getAffinities(), expectedAffinities);\n    });\n  });\n  describe(\"#score\", () => {\n    it(\"should calculate item relevance score\", () => {\n      const ps = instance.parameterSets.paramSet2;\n\n      const visitCounts = {\n        hour: { \"a.com\": 1, \"b.com\": 2 },\n        day: { \"a.com\": 4 },\n        week: { \"c.com\": 1 },\n        weekPlus: { \"a.com\": 1, \"d.com\": 3 },\n      };\n      instance.queryVisits = ts => visitCounts[ts.id];\n      instance.scores = instance.calculateAllUserDomainAffinityScores();\n\n      const testItem = {\n        domain_affinities: { \"a.com\": 1 },\n        item_score: 1,\n        parameter_set: \"paramSet2\",\n      };\n      const combinedDomainScore =\n        instance.scores[\"a.com\"].paramSet2 *\n        Math.pow(ps.multiDomainBoost + 1, 1);\n      const expectedItemScore = instance.calculateScore(\n        combinedDomainScore,\n        ps.perfectCombinedDomainScore,\n        ps.combinedDomainFactor\n      );\n\n      const itemScore = instance.calculateItemRelevanceScore(testItem);\n      assert.equal(expectedItemScore, itemScore);\n    });\n    it(\"should calculate relevance score equal to item_score if item has no domain affinities\", () => {\n      const testItem = { item_score: 0.985 };\n      const itemScore = instance.calculateItemRelevanceScore(testItem);\n      assert.equal(testItem.item_score, itemScore);\n    });\n    it(\"should calculate scores with factor\", () => {\n      assert.equal(1, instance.calculateScore(2, 1, 0.5));\n      assert.equal(0.5, instance.calculateScore(0.5, 1, 0.5));\n      assert.isBelow(instance.calculateScore(0.5, 1, 0.49), 1);\n      assert.isBelow(instance.calculateScore(0.5, 1, 0.51), 1);\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/ping-centre/PingCentre.test.js",
    "content": "// Any copyright is dedicated to the Public Domain.\n// http://creativecommons.org/publicdomain/zero/1.0/\n\nimport { FakePrefs, GlobalOverrider } from \"test/unit/utils\";\nimport { PingCentre, PingCentreConstants } from \"ping-centre/PingCentre.jsm\";\nconst {\n  FHR_UPLOAD_ENABLED_PREF,\n  TELEMETRY_PREF,\n  LOGGING_PREF,\n} = PingCentreConstants;\n\n/**\n * A reference to the fake preferences object created by the PingCentre\n * constructor so that we can use the API.\n */\nlet fakePrefs;\nconst prefInitHook = function() {\n  fakePrefs = this; // eslint-disable-line consistent-this\n};\n\nconst FAKE_UPDATE_CHANNEL = \"beta\";\nconst FAKE_LOCALE = \"en-US\";\nconst FAKE_ACTIVE_EXPERIMENTS = {\n  \"pref-flip-quantum-css-style-r1-1381147\": { branch: \"stylo\" },\n  \"nightly-nothing-burger-1-pref\": { branch: \"Control\" },\n};\nconst FAKE_PROFILE_CREATION_DATE = 16587;\nconst FAKE_BROWSER_SEARCH_REGION = \"US\";\n\ndescribe(\"PingCentre\", () => {\n  let globals;\n  let tSender;\n  let sandbox;\n  let fetchStub;\n  const fakePingJSON = { action: \"fake_action\", monkey: 1 };\n\n  beforeEach(() => {\n    globals = new GlobalOverrider();\n    sandbox = globals.sandbox;\n    fetchStub = sandbox.stub();\n\n    sandbox\n      .stub(global.Services.prefs, \"getBranch\")\n      .returns(new FakePrefs({ initHook: prefInitHook }));\n    sandbox\n      .stub(global.Services.locale, \"appLocaleAsLangTag\")\n      .get(() => FAKE_LOCALE);\n    globals.set(\"fetch\", fetchStub);\n    globals.set(\"TelemetryEnvironment\", {\n      getActiveExperiments: sandbox.spy(() => FAKE_ACTIVE_EXPERIMENTS),\n      currentEnvironment: {\n        profile: { creationDate: FAKE_PROFILE_CREATION_DATE },\n      },\n    });\n    globals.set(\"UpdateUtils\", {\n      getUpdateChannel() {\n        return FAKE_UPDATE_CHANNEL;\n      },\n    });\n    sandbox.spy(global.Cu, \"reportError\");\n  });\n\n  afterEach(() => {\n    globals.restore();\n    FakePrefs.prototype.prefs = {};\n  });\n\n  it(\"should construct the Prefs object\", () => {\n    tSender = new PingCentre({ topic: \"activity-stream\" });\n\n    assert.calledOnce(global.Services.prefs.getBranch);\n  });\n\n  it(\"should throw when topic is not specified\", () => {\n    assert.throws(() => {\n      tSender = new PingCentre();\n    });\n  });\n\n  describe(\"#enabled\", () => {\n    let testParams = [\n      { enabledPref: true, fhrPref: true, result: true },\n      { enabledPref: false, fhrPref: true, result: false },\n      { enabledPref: true, fhrPref: false, result: false },\n      { enabledPref: false, fhrPref: false, result: false },\n    ];\n\n    function testEnabled(p) {\n      FakePrefs.prototype.prefs[TELEMETRY_PREF] = p.enabledPref;\n      FakePrefs.prototype.prefs[FHR_UPLOAD_ENABLED_PREF] = p.fhrPref;\n\n      tSender = new PingCentre({ topic: \"activity-stream\" });\n\n      assert.equal(tSender.enabled, p.result);\n    }\n\n    for (let p of testParams) {\n      it(`should return ${p.result} if the fhrPref is ${\n        p.fhrPref\n      } and telemetry.enabled is ${p.enabledPref}`, () => {\n        testEnabled(p);\n      });\n    }\n\n    describe(\"telemetry.enabled pref changes from true to false\", () => {\n      beforeEach(() => {\n        FakePrefs.prototype.prefs = {};\n        FakePrefs.prototype.prefs[TELEMETRY_PREF] = true;\n        FakePrefs.prototype.prefs[FHR_UPLOAD_ENABLED_PREF] = true;\n\n        tSender = new PingCentre({ topic: \"activity-stream\" });\n        assert.propertyVal(tSender, \"enabled\", true);\n      });\n\n      it(\"should set the enabled property to false\", () => {\n        fakePrefs.setBoolPref(TELEMETRY_PREF, false);\n\n        assert.propertyVal(tSender, \"enabled\", false);\n      });\n    });\n\n    describe(\"telemetry.enabled pref changes from false to true\", () => {\n      beforeEach(() => {\n        FakePrefs.prototype.prefs = {};\n        FakePrefs.prototype.prefs[FHR_UPLOAD_ENABLED_PREF] = true;\n        FakePrefs.prototype.prefs[TELEMETRY_PREF] = false;\n        tSender = new PingCentre({ topic: \"activity-stream\" });\n\n        assert.propertyVal(tSender, \"enabled\", false);\n      });\n\n      it(\"should set the enabled property to true\", () => {\n        fakePrefs.setBoolPref(TELEMETRY_PREF, true);\n\n        assert.propertyVal(tSender, \"enabled\", true);\n      });\n    });\n\n    describe(\"FHR enabled pref changes from true to false\", () => {\n      beforeEach(() => {\n        FakePrefs.prototype.prefs = {};\n        FakePrefs.prototype.prefs[TELEMETRY_PREF] = true;\n        FakePrefs.prototype.prefs[FHR_UPLOAD_ENABLED_PREF] = true;\n        tSender = new PingCentre({ topic: \"activity-stream\" });\n        assert.propertyVal(tSender, \"enabled\", true);\n      });\n\n      it(\"should set the enabled property to false\", () => {\n        fakePrefs.setBoolPref(FHR_UPLOAD_ENABLED_PREF, false);\n\n        assert.propertyVal(tSender, \"enabled\", false);\n      });\n    });\n\n    describe(\"FHR enabled pref changes from false to true\", () => {\n      beforeEach(() => {\n        FakePrefs.prototype.prefs = {};\n        FakePrefs.prototype.prefs[FHR_UPLOAD_ENABLED_PREF] = false;\n        FakePrefs.prototype.prefs[TELEMETRY_PREF] = true;\n        tSender = new PingCentre({ topic: \"activity-stream\" });\n\n        assert.propertyVal(tSender, \"enabled\", false);\n      });\n\n      it(\"should set the enabled property to true\", () => {\n        fakePrefs.setBoolPref(FHR_UPLOAD_ENABLED_PREF, true);\n\n        assert.propertyVal(tSender, \"enabled\", true);\n      });\n    });\n  });\n\n  describe(\"#_createExperimentsString\", () => {\n    beforeEach(() => {\n      tSender = new PingCentre({ topic: \"activity-stream\" });\n    });\n\n    function testExperimentString(experimentString, activeExperiments, filter) {\n      for (let experimentID in activeExperiments) {\n        if (\n          activeExperiments[experimentID] &&\n          activeExperiments[experimentID].branch\n        ) {\n          const EXPECTED_SUBSTRING = `${experimentID}:${\n            activeExperiments[experimentID].branch\n          }`;\n\n          if (filter && !experimentID.includes(filter)) {\n            assert.isFalse(experimentString.includes(EXPECTED_SUBSTRING));\n            continue;\n          }\n\n          assert.isTrue(experimentString.includes(EXPECTED_SUBSTRING));\n        }\n      }\n    }\n\n    it(\"should apply filter to experiment list\", () => {\n      const FILTER = \"boop\";\n\n      tSender = new PingCentre({ topic: \"activity-stream\" });\n\n      let expString = tSender._createExperimentsString(\n        FAKE_ACTIVE_EXPERIMENTS,\n        FILTER\n      );\n      testExperimentString(expString, FAKE_ACTIVE_EXPERIMENTS, FILTER);\n    });\n\n    it(\"should generate the correct experiment string\", () => {\n      let expString = tSender._createExperimentsString(FAKE_ACTIVE_EXPERIMENTS);\n      testExperimentString(expString, FAKE_ACTIVE_EXPERIMENTS);\n    });\n\n    it(\"should exclude malformed experiments from experiment string\", () => {\n      let MALFORMED_EXPERIMENTS = Object.assign({}, FAKE_ACTIVE_EXPERIMENTS);\n      MALFORMED_EXPERIMENTS[\"test-id\"] = \"beep\";\n\n      let expString = tSender._createExperimentsString(MALFORMED_EXPERIMENTS);\n      testExperimentString(expString, MALFORMED_EXPERIMENTS);\n    });\n  });\n\n  describe(\"#_getRegion\", () => {\n    let prefStub;\n    let getStub;\n\n    beforeEach(() => {\n      tSender = new PingCentre({ topic: \"activity-stream\" });\n    });\n\n    afterEach(() => {\n      prefStub.restore();\n      getStub.restore();\n    });\n\n    it(\"should return UNSET if the region pref is missing\", () => {\n      prefStub = sinon\n        .stub(global.Services.prefs, \"prefHasUserValue\")\n        .returns(false);\n      getStub = sinon.stub(global.Services.prefs, \"getStringPref\").returns(\"\");\n      let region = tSender._getRegion();\n      assert.equal(region, \"UNSET\");\n    });\n\n    it(\"should return EMPTY if the region pref is empty\", () => {\n      prefStub = sinon\n        .stub(global.Services.prefs, \"prefHasUserValue\")\n        .returns(true);\n      getStub = sinon.stub(global.Services.prefs, \"getStringPref\").returns(\"\");\n      let region = tSender._getRegion();\n      assert.equal(region, \"EMPTY\");\n    });\n\n    it(\"should return OTHER if the region is not in the region whitelist\", () => {\n      prefStub = sinon\n        .stub(global.Services.prefs, \"prefHasUserValue\")\n        .returns(true);\n      getStub = sinon\n        .stub(global.Services.prefs, \"getStringPref\")\n        .returns(\"SOME_REGION\");\n      let region = tSender._getRegion();\n      assert.equal(region, \"OTHER\");\n    });\n\n    it(\"should return REGION if the region is in the region whitelist\", () => {\n      prefStub = sinon\n        .stub(global.Services.prefs, \"prefHasUserValue\")\n        .returns(true);\n      getStub = sinon\n        .stub(global.Services.prefs, \"getStringPref\")\n        .returns(FAKE_BROWSER_SEARCH_REGION);\n      let region = tSender._getRegion();\n      assert.equal(region, FAKE_BROWSER_SEARCH_REGION);\n    });\n  });\n\n  describe(\"#_createStructuredIngestionPing\", () => {\n    it(\"should create a ping for structured ingestion with expected properties\", async () => {\n      tSender = new PingCentre({ topic: \"activity-stream\" });\n      const ping = await tSender._createStructuredIngestionPing(fakePingJSON);\n\n      const EXPECTED_SHIELD_STRING =\n        \"pref-flip-quantum-css-style-r1-1381147:stylo;nightly-nothing-burger-1-pref:Control;\";\n      let EXPECTED_RESULT = Object.assign(\n        {\n          locale: FAKE_LOCALE,\n          version: \"69.0a1\",\n          release_channel: FAKE_UPDATE_CHANNEL,\n        },\n        fakePingJSON\n      );\n      EXPECTED_RESULT.shield_id = EXPECTED_SHIELD_STRING;\n\n      assert.equal(JSON.stringify(ping), JSON.stringify(EXPECTED_RESULT));\n    });\n  });\n\n  describe(\"#sendStructuredIngestionPing()\", () => {\n    let prefStub;\n    let getStub;\n    const fakeEndpointUrl = \"https://fake-endpoint.com\";\n\n    beforeEach(() => {\n      FakePrefs.prototype.prefs = {};\n      FakePrefs.prototype.prefs[FHR_UPLOAD_ENABLED_PREF] = true;\n      FakePrefs.prototype.prefs[TELEMETRY_PREF] = true;\n\n      prefStub = sinon\n        .stub(global.Services.prefs, \"prefHasUserValue\")\n        .returns(true);\n      getStub = sinon\n        .stub(global.Services.prefs, \"getStringPref\")\n        .returns(FAKE_BROWSER_SEARCH_REGION);\n      sandbox.stub(PingCentre, \"_sendInGzip\").resolves();\n\n      tSender = new PingCentre({ topic: \"activity-stream\" });\n    });\n\n    afterEach(() => {\n      prefStub.restore();\n      getStub.restore();\n    });\n\n    it(\"should not send if the PingCentre is disabled\", async () => {\n      FakePrefs.prototype.prefs[TELEMETRY_PREF] = false;\n      tSender = new PingCentre({ topic: \"activity-stream\" });\n\n      await tSender.sendStructuredIngestionPing(fakePingJSON, fakeEndpointUrl);\n\n      assert.notCalled(PingCentre._sendInGzip);\n    });\n\n    it(\"should POST given ping data compressed to telemetry.ping.endpoint pref w/fetch\", async () => {\n      await tSender.sendStructuredIngestionPing(fakePingJSON, fakeEndpointUrl);\n\n      const EXPECTED_SHIELD_STRING =\n        \"pref-flip-quantum-css-style-r1-1381147:stylo;nightly-nothing-burger-1-pref:Control;\";\n      let EXPECTED_RESULT = Object.assign(\n        {\n          locale: FAKE_LOCALE,\n          version: \"69.0a1\",\n          release_channel: FAKE_UPDATE_CHANNEL,\n        },\n        fakePingJSON\n      );\n      EXPECTED_RESULT.shield_id = EXPECTED_SHIELD_STRING;\n\n      assert.calledOnce(PingCentre._sendInGzip);\n      assert.calledWithExactly(\n        PingCentre._sendInGzip,\n        fakeEndpointUrl,\n        JSON.stringify(EXPECTED_RESULT)\n      );\n    });\n\n    it(\"should log an error using Cu.reportError if send rejects\", async () => {\n      PingCentre._sendInGzip.rejects({ type: \"Oh noes!\" });\n\n      await tSender.sendStructuredIngestionPing(fakePingJSON, fakeEndpointUrl);\n\n      assert.called(Cu.reportError);\n    });\n  });\n\n  describe(\"#uninit()\", () => {\n    it(\"should remove the telemetry pref listener\", () => {\n      tSender = new PingCentre({ topic: \"activity-stream\" });\n      assert.property(fakePrefs.observers, TELEMETRY_PREF);\n\n      tSender.uninit();\n\n      assert.notProperty(fakePrefs.observers, TELEMETRY_PREF);\n    });\n\n    it(\"should remove the fhrpref listener\", () => {\n      tSender = new PingCentre({ topic: \"activity-stream\" });\n      assert.property(fakePrefs.observers, FHR_UPLOAD_ENABLED_PREF);\n\n      tSender.uninit();\n\n      assert.notProperty(fakePrefs.observers, FHR_UPLOAD_ENABLED_PREF);\n    });\n\n    it(\"should remove the telemetry log listener\", () => {\n      tSender = new PingCentre({ topic: \"activity-stream\" });\n      assert.property(fakePrefs.observers, LOGGING_PREF);\n\n      tSender.uninit();\n\n      assert.notProperty(fakePrefs.observers, TELEMETRY_PREF);\n    });\n\n    it(\"should call Cu.reportError if this._prefs.removeObserver throws\", () => {\n      globals.sandbox\n        .stub(FakePrefs.prototype, \"removeObserver\")\n        .throws(\"Some Error\");\n      tSender = new PingCentre({ topic: \"activity-stream\" });\n\n      tSender.uninit();\n\n      assert.called(global.Cu.reportError);\n    });\n  });\n\n  describe(\"Misc pref changes\", () => {\n    describe(\"performance.log changes from false to true\", () => {\n      it(\"should change this.logging from false to true\", () => {\n        FakePrefs.prototype.prefs = {};\n        FakePrefs.prototype.prefs[LOGGING_PREF] = false;\n        tSender = new PingCentre({ topic: \"activity-stream\" });\n        assert.propertyVal(tSender, \"logging\", false);\n\n        fakePrefs.setBoolPref(LOGGING_PREF, true);\n\n        assert.propertyVal(tSender, \"logging\", true);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/unit-entry.js",
    "content": "import {\n  EventEmitter,\n  FakePerformance,\n  FakePrefs,\n  GlobalOverrider,\n} from \"test/unit/utils\";\nimport Adapter from \"enzyme-adapter-react-16\";\nimport { chaiAssertions } from \"test/schemas/pings\";\nimport chaiJsonSchema from \"chai-json-schema\";\nimport enzyme from \"enzyme\";\nenzyme.configure({ adapter: new Adapter() });\n\n// Cause React warnings to make tests that trigger them fail\nconst origConsoleError = console.error; // eslint-disable-line no-console\n// eslint-disable-next-line no-console\nconsole.error = function(msg, ...args) {\n  // eslint-disable-next-line no-console\n  origConsoleError.apply(console, [msg, ...args]);\n\n  if (\n    /(Invalid prop|Failed prop type|Check the render method|React Intl)/.test(\n      msg\n    )\n  ) {\n    throw new Error(msg);\n  }\n};\n\nconst req = require.context(\".\", true, /\\.test\\.jsx?$/);\nconst files = req.keys();\n\n// This exposes sinon assertions to chai.assert\nsinon.assert.expose(assert, { prefix: \"\" });\n\nchai.use(chaiAssertions);\nchai.use(chaiJsonSchema);\n\nconst overrider = new GlobalOverrider();\nconst TEST_GLOBAL = {\n  AddonManager: {\n    getActiveAddons() {\n      return Promise.resolve({ addons: [], fullData: false });\n    },\n  },\n  AppConstants: { MOZILLA_OFFICIAL: true, MOZ_APP_VERSION: \"69.0a1\" },\n  UpdateUtils: { getUpdateChannel() {} },\n  BrowserWindowTracker: { getTopWindow() {} },\n  ChromeUtils: {\n    defineModuleGetter() {},\n    generateQI() {\n      return {};\n    },\n    import() {\n      return global;\n    },\n  },\n  ClientEnvironment: {\n    get userId() {\n      return \"foo123\";\n    },\n  },\n  Components: {\n    Constructor(classId) {\n      switch (classId) {\n        case \"@mozilla.org/referrer-info;1\":\n          return function(referrerPolicy, sendReferrer, originalReferrer) {\n            this.referrerPolicy = referrerPolicy;\n            this.sendReferrer = sendReferrer;\n            this.originalReferrer = originalReferrer;\n          };\n      }\n      return function() {};\n    },\n    isSuccessCode: () => true,\n  },\n  // eslint-disable-next-line object-shorthand\n  ContentSearchUIController: function() {}, // NB: This is a function/constructor\n  Cc: {\n    \"@mozilla.org/browser/nav-bookmarks-service;1\": {\n      addObserver() {},\n      getService() {\n        return this;\n      },\n      removeObserver() {},\n      SOURCES: {},\n      TYPE_BOOKMARK: {},\n    },\n    \"@mozilla.org/browser/nav-history-service;1\": {\n      addObserver() {},\n      executeQuery() {},\n      getNewQuery() {},\n      getNewQueryOptions() {},\n      getService() {\n        return this;\n      },\n      insert() {},\n      markPageAsTyped() {},\n      removeObserver() {},\n    },\n    \"@mozilla.org/io/string-input-stream;1\": {\n      createInstance() {\n        return {};\n      },\n    },\n    \"@mozilla.org/security/hash;1\": {\n      createInstance() {\n        return {\n          init() {},\n          updateFromStream() {},\n          finish() {\n            return \"0\";\n          },\n        };\n      },\n    },\n    \"@mozilla.org/updates/update-checker;1\": { createInstance() {} },\n    \"@mozilla.org/streamConverters;1\": {\n      getService() {\n        return this;\n      },\n    },\n    \"@mozilla.org/network/stream-loader;1\": {\n      createInstance() {\n        return {};\n      },\n    },\n  },\n  Ci: {\n    nsICryptoHash: {},\n    nsIReferrerInfo: { UNSAFE_URL: 5 },\n    nsITimer: { TYPE_ONE_SHOT: 1 },\n    nsIWebProgressListener: { LOCATION_CHANGE_SAME_DOCUMENT: 1 },\n    nsIDOMWindow: Object,\n    nsITrackingDBService: {\n      TRACKERS_ID: 1,\n      TRACKING_COOKIES_ID: 2,\n      CRYPTOMINERS_ID: 3,\n      FINGERPRINTERS_ID: 4,\n      SOCIAL_ID: 5,\n    },\n  },\n  Cu: {\n    importGlobalProperties() {},\n    now: () => window.performance.now(),\n    reportError() {},\n  },\n  dump() {},\n  EveryWindow: {\n    registerCallback: (id, init, uninit) => {},\n    unregisterCallback: id => {},\n  },\n  fetch() {},\n  // eslint-disable-next-line object-shorthand\n  Image: function() {}, // NB: This is a function/constructor\n  NewTabUtils: {\n    activityStreamProvider: {\n      getTopFrecentSites: () => [],\n      executePlacesQuery: async (sql, options) => ({ sql, options }),\n    },\n  },\n  OS: {\n    File: {\n      writeAtomic() {},\n      makeDir() {},\n      stat() {},\n      exists() {},\n      remove() {},\n      removeEmptyDir() {},\n    },\n    Path: {\n      join() {\n        return \"/\";\n      },\n    },\n    Constants: {\n      Path: {\n        localProfileDir: \"/\",\n      },\n    },\n  },\n  PlacesUtils: {\n    get bookmarks() {\n      return TEST_GLOBAL.Cc[\"@mozilla.org/browser/nav-bookmarks-service;1\"];\n    },\n    get history() {\n      return TEST_GLOBAL.Cc[\"@mozilla.org/browser/nav-history-service;1\"];\n    },\n    observers: {\n      addListener() {},\n      removeListener() {},\n    },\n  },\n  PluralForm: { get() {} },\n  Preferences: FakePrefs,\n  PrivateBrowsingUtils: {\n    isBrowserPrivate: () => false,\n    isWindowPrivate: () => false,\n  },\n  DownloadsViewUI: {\n    getDisplayName: () => \"filename.ext\",\n    getSizeWithUnits: () => \"1.5 MB\",\n  },\n  FileUtils: {\n    // eslint-disable-next-line object-shorthand\n    File: function() {}, // NB: This is a function/constructor\n  },\n  Services: {\n    dirsvc: {\n      get: () => ({ parent: { parent: { path: \"appPath\" } } }),\n    },\n    locale: {\n      get appLocaleAsLangTag() {\n        return \"en-US\";\n      },\n      negotiateLanguages() {},\n    },\n    urlFormatter: { formatURL: str => str, formatURLPref: str => str },\n    mm: {\n      addMessageListener: (msg, cb) => this.receiveMessage(),\n      removeMessageListener() {},\n    },\n    appShell: { hiddenDOMWindow: { performance: new FakePerformance() } },\n    obs: {\n      addObserver() {},\n      removeObserver() {},\n    },\n    telemetry: {\n      setEventRecordingEnabled: () => {},\n      recordEvent: eventDetails => {},\n    },\n    console: { logStringMessage: () => {} },\n    prefs: {\n      addObserver() {},\n      prefHasUserValue() {},\n      removeObserver() {},\n      getPrefType() {},\n      clearUserPref() {},\n      getChildList() {\n        return [];\n      },\n      getStringPref() {},\n      setStringPref() {},\n      getIntPref() {},\n      getBoolPref() {},\n      getCharPref() {},\n      setBoolPref() {},\n      setCharPref() {},\n      setIntPref() {},\n      getBranch() {},\n      PREF_BOOL: \"boolean\",\n      PREF_INT: \"integer\",\n      PREF_STRING: \"string\",\n      getDefaultBranch() {\n        return {\n          setBoolPref() {},\n          setIntPref() {},\n          setStringPref() {},\n          clearUserPref() {},\n        };\n      },\n    },\n    tm: {\n      dispatchToMainThread: cb => cb(),\n      idleDispatchToMainThread: cb => cb(),\n    },\n    eTLD: {\n      getBaseDomain({ spec }) {\n        return spec.match(/\\/([^/]+)/)[1];\n      },\n      getPublicSuffix() {},\n    },\n    io: {\n      newURI: spec => ({\n        mutate: () => ({\n          setRef: ref => ({\n            finalize: () => ({\n              ref,\n              spec,\n            }),\n          }),\n        }),\n        spec,\n      }),\n    },\n    search: {\n      init() {\n        return Promise.resolve();\n      },\n      getVisibleEngines: () =>\n        Promise.resolve([{ identifier: \"google\" }, { identifier: \"bing\" }]),\n      defaultEngine: {\n        identifier: \"google\",\n        searchForm:\n          \"https://www.google.com/search?q=&ie=utf-8&oe=utf-8&client=firefox-b\",\n        wrappedJSObject: {\n          __internalAliases: [\"@google\"],\n        },\n      },\n      defaultPrivateEngine: {\n        identifier: \"bing\",\n        searchForm: \"https://www.bing.com\",\n        wrappedJSObject: {\n          __internalAliases: [\"@bing\"],\n        },\n      },\n    },\n    scriptSecurityManager: {\n      createNullPrincipal() {},\n      getSystemPrincipal() {},\n    },\n    wm: {\n      getMostRecentWindow: () => window,\n      getMostRecentBrowserWindow: () => window,\n      getEnumerator: () => [],\n    },\n    ww: { registerNotification() {}, unregisterNotification() {} },\n    appinfo: { appBuildID: \"20180710100040\", version: \"69.0a1\" },\n  },\n  XPCOMUtils: {\n    defineLazyGetter(object, name, f) {\n      if (object && name) {\n        object[name] = f();\n      } else {\n        f();\n      }\n    },\n    defineLazyGlobalGetters() {},\n    defineLazyModuleGetter() {},\n    defineLazyModuleGetters() {},\n    defineLazyServiceGetter() {},\n    defineLazyServiceGetters() {},\n    defineLazyPreferenceGetter(obj, name) {\n      Object.defineProperty(obj, name, {\n        configurable: true,\n        get: () => \"\",\n      });\n    },\n    generateQI() {\n      return {};\n    },\n  },\n  EventEmitter,\n  ShellService: { isDefaultBrowser: () => true },\n  FilterExpressions: {\n    eval() {\n      return Promise.resolve(false);\n    },\n  },\n  RemoteSettings(name) {\n    return {\n      get() {\n        if (name === \"attachment\") {\n          return Promise.resolve([{ attachment: {} }]);\n        }\n        return Promise.resolve([]);\n      },\n      on() {},\n    };\n  },\n  Localization: class {\n    async formatMessages(stringsIds) {\n      return Promise.resolve(\n        stringsIds.map(({ id, args }) => ({ value: { string_id: id, args } }))\n      );\n    }\n  },\n  FxAccountsConfig: {\n    promiseConnectAccountURI(id) {\n      return Promise.resolve(id);\n    },\n  },\n  TelemetryEnvironment: {\n    setExperimentActive() {},\n  },\n  Sampling: {\n    ratioSample(seed, ratios) {\n      return Promise.resolve(0);\n    },\n  },\n  BrowserHandler: {\n    get kiosk() {\n      return false;\n    },\n  },\n};\noverrider.set(TEST_GLOBAL);\n\ndescribe(\"activity-stream\", () => {\n  after(() => overrider.restore());\n  files.forEach(file => req(file));\n});\n"
  },
  {
    "path": "test/unit/utils.js",
    "content": "/**\n * GlobalOverrider - Utility that allows you to override properties on the global object.\n *                   See unit-entry.js for example usage.\n */\nexport class GlobalOverrider {\n  constructor() {\n    this.originalGlobals = new Map();\n    this.sandbox = sinon.createSandbox();\n  }\n\n  /**\n   * _override - Internal method to override properties on the global object.\n   *             The first time a given key is overridden, we cache the original\n   *             value in this.originalGlobals so that later it can be restored.\n   *\n   * @param  {string} key The identifier of the property\n   * @param  {any} value The value to which the property should be reassigned\n   */\n  _override(key, value) {\n    if (!this.originalGlobals.has(key)) {\n      this.originalGlobals.set(key, global[key]);\n    }\n    global[key] = value;\n  }\n\n  /**\n   * set - Override a given property, or all properties on an object\n   *\n   * @param  {string|object} key If a string, the identifier of the property\n   *                             If an object, a number of properties and values to which they should be reassigned.\n   * @param  {any} value The value to which the property should be reassigned\n   * @return {type}       description\n   */\n  set(key, value) {\n    if (!value && typeof key === \"object\") {\n      const overrides = key;\n      Object.keys(overrides).forEach(k => this._override(k, overrides[k]));\n    } else {\n      this._override(key, value);\n    }\n    return value;\n  }\n\n  /**\n   * reset - Reset the global sandbox, so all state on spies, stubs etc. is cleared.\n   *         You probably want to call this after each test.\n   */\n  reset() {\n    this.sandbox.reset();\n  }\n\n  /**\n   * restore - Restore the global sandbox and reset all overriden properties to\n   *           their original values. You should call this after all tests have completed.\n   */\n  restore() {\n    this.sandbox.restore();\n    this.originalGlobals.forEach((value, key) => {\n      global[key] = value;\n    });\n  }\n}\n\n/**\n * Very simple fake for the most basic semantics of nsIPrefBranch. Lots of\n * things aren't yet supported.  Feel free to add them in.\n *\n * @param {Object} args - optional arguments\n * @param {Function} args.initHook - if present, will be called back\n *                   inside the constructor. Typically used from tests\n *                   to save off a pointer to the created instance so that\n *                   stubs and spies can be inspected by the test code.\n */\nexport class FakensIPrefBranch {\n  constructor(args) {\n    if (args) {\n      if (\"initHook\" in args) {\n        args.initHook.call(this);\n      }\n      if (args.defaultBranch) {\n        this.prefs = {};\n      }\n    }\n    this._prefBranch = {};\n    this.observers = {};\n  }\n  addObserver(prefName, callback) {\n    this.observers[prefName] = callback;\n  }\n  removeObserver(prefName, callback) {\n    if (prefName in this.observers) {\n      delete this.observers[prefName];\n    }\n  }\n  observeBranch(listener) {}\n  ignoreBranch(listener) {}\n  setStringPref(prefName) {}\n\n  getStringPref(prefName) {\n    return this.get(prefName);\n  }\n  getBoolPref(prefName) {\n    return this.get(prefName);\n  }\n  get(prefName) {\n    return this.prefs[prefName];\n  }\n  setBoolPref(prefName, value) {\n    this.prefs[prefName] = value;\n\n    if (prefName in this.observers) {\n      this.observers[prefName](\"\", \"\", prefName);\n    }\n  }\n}\nFakensIPrefBranch.prototype.prefs = {};\n\n/**\n * Very simple fake for the most basic semantics of Preferences.jsm.\n * Extends FakensIPrefBranch.\n */\nexport class FakePrefs extends FakensIPrefBranch {\n  observe(prefName, callback) {\n    super.addObserver(prefName, callback);\n  }\n  ignore(prefName, callback) {\n    super.removeObserver(prefName, callback);\n  }\n  set(prefName, value) {\n    this.prefs[prefName] = value;\n\n    if (prefName in this.observers) {\n      this.observers[prefName](value);\n    }\n  }\n}\n\n/**\n * Slimmed down version of toolkit/modules/EventEmitter.jsm\n */\nexport function EventEmitter() {}\nEventEmitter.decorate = function(objectToDecorate) {\n  let emitter = new EventEmitter();\n  objectToDecorate.on = emitter.on.bind(emitter);\n  objectToDecorate.off = emitter.off.bind(emitter);\n  objectToDecorate.once = emitter.once.bind(emitter);\n  objectToDecorate.emit = emitter.emit.bind(emitter);\n};\nEventEmitter.prototype = {\n  on(event, listener) {\n    if (!this._eventEmitterListeners) {\n      this._eventEmitterListeners = new Map();\n    }\n    if (!this._eventEmitterListeners.has(event)) {\n      this._eventEmitterListeners.set(event, []);\n    }\n    this._eventEmitterListeners.get(event).push(listener);\n  },\n  off(event, listener) {\n    if (!this._eventEmitterListeners) {\n      return;\n    }\n    let listeners = this._eventEmitterListeners.get(event);\n    if (listeners) {\n      this._eventEmitterListeners.set(\n        event,\n        listeners.filter(\n          l => l !== listener && l._originalListener !== listener\n        )\n      );\n    }\n  },\n  once(event, listener) {\n    return new Promise(resolve => {\n      let handler = (_, first, ...rest) => {\n        this.off(event, handler);\n        if (listener) {\n          listener(event, first, ...rest);\n        }\n        resolve(first);\n      };\n\n      handler._originalListener = listener;\n      this.on(event, handler);\n    });\n  },\n  // All arguments to this method will be sent to listeners\n  emit(event, ...args) {\n    if (\n      !this._eventEmitterListeners ||\n      !this._eventEmitterListeners.has(event)\n    ) {\n      return;\n    }\n    let originalListeners = this._eventEmitterListeners.get(event);\n    for (let listener of this._eventEmitterListeners.get(event)) {\n      // If the object was destroyed during event emission, stop\n      // emitting.\n      if (!this._eventEmitterListeners) {\n        break;\n      }\n      // If listeners were removed during emission, make sure the\n      // event handler we're going to fire wasn't removed.\n      if (\n        originalListeners === this._eventEmitterListeners.get(event) ||\n        this._eventEmitterListeners.get(event).some(l => l === listener)\n      ) {\n        try {\n          listener(event, ...args);\n        } catch (ex) {\n          // error with a listener\n        }\n      }\n    }\n  },\n};\n\nexport function FakePerformance() {}\nFakePerformance.prototype = {\n  marks: new Map(),\n  now() {\n    return window.performance.now();\n  },\n  timing: { navigationStart: 222222.123 },\n  get timeOrigin() {\n    return 10000.234;\n  },\n  // XXX assumes type == \"mark\"\n  getEntriesByName(name, type) {\n    if (this.marks.has(name)) {\n      return this.marks.get(name);\n    }\n    return [];\n  },\n  callsToMark: 0,\n\n  /**\n   * @note The \"startTime\" for each mark is simply the number of times mark\n   * has been called in this object.\n   */\n  mark(name) {\n    let markObj = {\n      name,\n      entryType: \"mark\",\n      startTime: ++this.callsToMark,\n      duration: 0,\n    };\n\n    if (this.marks.has(name)) {\n      this.marks.get(name).push(markObj);\n      return;\n    }\n\n    this.marks.set(name, [markObj]);\n  },\n};\n\n/**\n * addNumberReducer - a simple dummy reducer for testing that adds a number\n */\nexport function addNumberReducer(prevState = 0, action) {\n  return action.type === \"ADD\" ? prevState + action.data : prevState;\n}\n"
  },
  {
    "path": "test/xpcshell/test_ASRouterTargeting_attribution.js",
    "content": "/* Any copyright is dedicated to the Public Domain.\n * http://creativecommons.org/publicdomain/zero/1.0/\n */\n\n\"use strict\";\n\nconst { Services } = ChromeUtils.import(\"resource://gre/modules/Services.jsm\");\nconst { AttributionCode } = ChromeUtils.import(\n  \"resource:///modules/AttributionCode.jsm\"\n);\nconst { ASRouterTargeting } = ChromeUtils.import(\n  \"resource://activity-stream/lib/ASRouterTargeting.jsm\"\n);\n\nadd_task(async function check_attribution_data() {\n  // Some setup to fake the correct attribution data\n  const appPath = Services.dirsvc.get(\"GreD\", Ci.nsIFile).parent.parent.path;\n  const attributionSvc = Cc[\"@mozilla.org/mac-attribution;1\"].getService(\n    Ci.nsIMacAttributionService\n  );\n  const campaign = \"non-fx-button\";\n  const source = \"addons.mozilla.org\";\n  const referrer = `https://allizom.org/anything/?utm_campaign=${campaign}&utm_source=${source}`;\n  attributionSvc.setReferrerUrl(appPath, referrer, true);\n  AttributionCode._clearCache();\n  AttributionCode.getAttrDataAsync();\n\n  const {\n    campaign: attributionCampain,\n    source: attributionSource,\n  } = ASRouterTargeting.Environment.attributionData;\n  equal(\n    attributionCampain,\n    campaign,\n    \"should get the correct campaign out of attributionData\"\n  );\n  equal(\n    attributionSource,\n    source,\n    \"should get the correct source out of attributionData\"\n  );\n\n  const messages = [\n    {\n      id: \"foo1\",\n      targeting:\n        \"attributionData.campaign == 'back_to_school' && attributionData.source == 'addons.mozilla.org'\",\n    },\n    {\n      id: \"foo2\",\n      targeting:\n        \"attributionData.campaign == 'non-fx-button' && attributionData.source == 'addons.mozilla.org'\",\n    },\n  ];\n\n  equal(\n    await ASRouterTargeting.findMatchingMessage({ messages }),\n    messages[1],\n    \"should select the message with the correct campaign and source\"\n  );\n  AttributionCode._clearCache();\n});\n"
  },
  {
    "path": "test/xpcshell/test_AboutNewTabService.js",
    "content": "/* Any copyright is dedicated to the Public Domain.\n * http://creativecommons.org/publicdomain/zero/1.0/\n */\n\n\"use strict\";\n\nconst { Services } = ChromeUtils.import(\"resource://gre/modules/Services.jsm\");\nconst { XPCOMUtils } = ChromeUtils.import(\n  \"resource://gre/modules/XPCOMUtils.jsm\"\n);\nconst { AppConstants } = ChromeUtils.import(\n  \"resource://gre/modules/AppConstants.jsm\"\n);\nXPCOMUtils.defineLazyServiceGetter(\n  this,\n  \"aboutNewTabService\",\n  \"@mozilla.org/browser/aboutnewtab-service;1\",\n  \"nsIAboutNewTabService\"\n);\n\nconst IS_RELEASE_OR_BETA = AppConstants.RELEASE_OR_BETA;\n\nconst DOWNLOADS_URL =\n  \"chrome://browser/content/downloads/contentAreaDownloadsView.xul\";\nconst SEPARATE_PRIVILEGED_CONTENT_PROCESS_PREF =\n  \"browser.tabs.remote.separatePrivilegedContentProcess\";\nconst ACTIVITY_STREAM_DEBUG_PREF = \"browser.newtabpage.activity-stream.debug\";\n\nfunction cleanup() {\n  Services.prefs.clearUserPref(SEPARATE_PRIVILEGED_CONTENT_PROCESS_PREF);\n  Services.prefs.clearUserPref(ACTIVITY_STREAM_DEBUG_PREF);\n  aboutNewTabService.resetNewTabURL();\n}\n\nregisterCleanupFunction(cleanup);\n\nlet ACTIVITY_STREAM_URL;\nlet ACTIVITY_STREAM_DEBUG_URL;\n\nfunction setExpectedUrlsWithScripts() {\n  ACTIVITY_STREAM_URL =\n    \"resource://activity-stream/prerendered/activity-stream.html\";\n  ACTIVITY_STREAM_DEBUG_URL =\n    \"resource://activity-stream/prerendered/activity-stream-debug.html\";\n}\n\nfunction setExpectedUrlsWithoutScripts() {\n  ACTIVITY_STREAM_URL =\n    \"resource://activity-stream/prerendered/activity-stream-noscripts.html\";\n\n  // Debug urls are the same as non-debug because debug scripts load dynamically\n  ACTIVITY_STREAM_DEBUG_URL = ACTIVITY_STREAM_URL;\n}\n\nfunction nextChangeNotificationPromise(aNewURL, testMessage) {\n  return new Promise(resolve => {\n    Services.obs.addObserver(function observer(aSubject, aTopic, aData) {\n      // jshint unused:false\n      Services.obs.removeObserver(observer, aTopic);\n      Assert.equal(aData, aNewURL, testMessage);\n      resolve();\n    }, \"newtab-url-changed\");\n  });\n}\n\nfunction setPrivilegedContentProcessPref(usePrivilegedContentProcess) {\n  if (\n    usePrivilegedContentProcess ===\n    Services.prefs.getBoolPref(SEPARATE_PRIVILEGED_CONTENT_PROCESS_PREF)\n  ) {\n    return Promise.resolve();\n  }\n\n  let notificationPromise = nextChangeNotificationPromise(\"about:newtab\");\n  Services.prefs.setBoolPref(\n    SEPARATE_PRIVILEGED_CONTENT_PROCESS_PREF,\n    usePrivilegedContentProcess\n  );\n  return notificationPromise;\n}\n\n// Default expected URLs to files with scripts in them.\nsetExpectedUrlsWithScripts();\n\nfunction addTestsWithPrivilegedContentProcessPref(test) {\n  add_task(async () => {\n    await setPrivilegedContentProcessPref(true);\n    setExpectedUrlsWithoutScripts();\n    await test();\n  });\n  add_task(async () => {\n    await setPrivilegedContentProcessPref(false);\n    setExpectedUrlsWithScripts();\n    await test();\n  });\n}\n\nfunction setBoolPrefAndWaitForChange(pref, value, testMessage) {\n  return new Promise(resolve => {\n    Services.obs.addObserver(function observer(aSubject, aTopic, aData) {\n      // jshint unused:false\n      Services.obs.removeObserver(observer, aTopic);\n      Assert.equal(aData, aboutNewTabService.newTabURL, testMessage);\n      resolve();\n    }, \"newtab-url-changed\");\n\n    Services.prefs.setBoolPref(pref, value);\n  });\n}\n\nadd_task(async function test_as_initial_values() {\n  Assert.ok(\n    aboutNewTabService.activityStreamEnabled,\n    \".activityStreamEnabled should be set to the correct initial value\"\n  );\n  // This pref isn't defined on release or beta, so we fall back to false\n  Assert.equal(\n    aboutNewTabService.activityStreamDebug,\n    Services.prefs.getBoolPref(ACTIVITY_STREAM_DEBUG_PREF, false),\n    \".activityStreamDebug should be set to the correct initial value\"\n  );\n});\n\n/**\n * Test the overriding of the default URL\n */\nadd_task(async function test_override_activity_stream_disabled() {\n  let notificationPromise;\n\n  // override with some remote URL\n  let url = \"http://example.com/\";\n  notificationPromise = nextChangeNotificationPromise(url);\n  aboutNewTabService.newTabURL = url;\n  await notificationPromise;\n  Assert.ok(aboutNewTabService.overridden, \"Newtab URL should be overridden\");\n  Assert.ok(\n    !aboutNewTabService.activityStreamEnabled,\n    \"Newtab activity stream should not be enabled\"\n  );\n  Assert.equal(\n    aboutNewTabService.newTabURL,\n    url,\n    \"Newtab URL should be the custom URL\"\n  );\n\n  // test reset with activity stream disabled\n  notificationPromise = nextChangeNotificationPromise(\"about:newtab\");\n  aboutNewTabService.resetNewTabURL();\n  await notificationPromise;\n  Assert.ok(\n    !aboutNewTabService.overridden,\n    \"Newtab URL should not be overridden\"\n  );\n  Assert.equal(\n    aboutNewTabService.newTabURL,\n    \"about:newtab\",\n    \"Newtab URL should be the default\"\n  );\n\n  // test override to a chrome URL\n  notificationPromise = nextChangeNotificationPromise(DOWNLOADS_URL);\n  aboutNewTabService.newTabURL = DOWNLOADS_URL;\n  await notificationPromise;\n  Assert.ok(aboutNewTabService.overridden, \"Newtab URL should be overridden\");\n  Assert.equal(\n    aboutNewTabService.newTabURL,\n    DOWNLOADS_URL,\n    \"Newtab URL should be the custom URL\"\n  );\n\n  cleanup();\n});\n\naddTestsWithPrivilegedContentProcessPref(\n  async function test_override_activity_stream_enabled() {\n    Assert.equal(\n      aboutNewTabService.defaultURL,\n      ACTIVITY_STREAM_URL,\n      \"Newtab URL should be the default activity stream URL\"\n    );\n    Assert.ok(\n      !aboutNewTabService.overridden,\n      \"Newtab URL should not be overridden\"\n    );\n    Assert.ok(\n      aboutNewTabService.activityStreamEnabled,\n      \"Activity Stream should be enabled\"\n    );\n\n    // change to a chrome URL while activity stream is enabled\n    let notificationPromise = nextChangeNotificationPromise(DOWNLOADS_URL);\n    aboutNewTabService.newTabURL = DOWNLOADS_URL;\n    await notificationPromise;\n    Assert.equal(\n      aboutNewTabService.newTabURL,\n      DOWNLOADS_URL,\n      \"Newtab URL set to chrome url\"\n    );\n    Assert.equal(\n      aboutNewTabService.defaultURL,\n      ACTIVITY_STREAM_URL,\n      \"Newtab URL defaultURL still set to the default activity stream URL\"\n    );\n    Assert.ok(aboutNewTabService.overridden, \"Newtab URL should be overridden\");\n    Assert.ok(\n      !aboutNewTabService.activityStreamEnabled,\n      \"Activity Stream should not be enabled\"\n    );\n\n    cleanup();\n  }\n);\n\naddTestsWithPrivilegedContentProcessPref(async function test_default_url() {\n  Assert.equal(\n    aboutNewTabService.defaultURL,\n    ACTIVITY_STREAM_URL,\n    \"Newtab defaultURL initially set to AS url\"\n  );\n\n  // Only debug variants aren't available on release/beta\n  if (!IS_RELEASE_OR_BETA) {\n    await setBoolPrefAndWaitForChange(\n      ACTIVITY_STREAM_DEBUG_PREF,\n      true,\n      \"A notification occurs after changing the debug pref to true\"\n    );\n    Assert.equal(\n      aboutNewTabService.activityStreamDebug,\n      true,\n      \"the .activityStreamDebug property is set to true\"\n    );\n    Assert.equal(\n      aboutNewTabService.defaultURL,\n      ACTIVITY_STREAM_DEBUG_URL,\n      \"Newtab defaultURL set to debug AS url after the pref has been changed\"\n    );\n    await setBoolPrefAndWaitForChange(\n      ACTIVITY_STREAM_DEBUG_PREF,\n      false,\n      \"A notification occurs after changing the debug pref to false\"\n    );\n  } else {\n    Services.prefs.setBoolPref(ACTIVITY_STREAM_DEBUG_PREF, true);\n\n    Assert.equal(\n      aboutNewTabService.activityStreamDebug,\n      false,\n      \"the .activityStreamDebug property is remains false\"\n    );\n  }\n\n  Assert.equal(\n    aboutNewTabService.defaultURL,\n    ACTIVITY_STREAM_URL,\n    \"Newtab defaultURL set to un-prerendered AS if prerender is false and debug is false\"\n  );\n\n  cleanup();\n});\n\naddTestsWithPrivilegedContentProcessPref(async function test_welcome_url() {\n  Assert.equal(\n    aboutNewTabService.welcomeURL,\n    ACTIVITY_STREAM_URL,\n    \"Newtab welcomeURL set to un-prerendered AS when debug disabled.\"\n  );\n  Assert.equal(\n    aboutNewTabService.welcomeURL,\n    aboutNewTabService.defaultURL,\n    \"Newtab welcomeURL is equal to defaultURL when prerendering disabled and debug disabled.\"\n  );\n\n  // Only debug variants aren't available on release/beta\n  if (!IS_RELEASE_OR_BETA) {\n    await setBoolPrefAndWaitForChange(\n      ACTIVITY_STREAM_DEBUG_PREF,\n      true,\n      \"A notification occurs after changing the debug pref to true.\"\n    );\n    Assert.equal(\n      aboutNewTabService.welcomeURL,\n      ACTIVITY_STREAM_DEBUG_URL,\n      \"Newtab welcomeURL set to un-prerendered debug AS when debug enabled\"\n    );\n  }\n\n  cleanup();\n});\n\n/**\n * Tests response to updates to prefs\n */\naddTestsWithPrivilegedContentProcessPref(async function test_updates() {\n  // Simulates a \"cold-boot\" situation, with some pref already set before testing a series\n  // of changes.\n  aboutNewTabService.resetNewTabURL(); // need to set manually because pref notifs are off\n  let notificationPromise;\n\n  // test update fires on override and reset\n  let testURL = \"https://example.com/\";\n  notificationPromise = nextChangeNotificationPromise(\n    testURL,\n    \"a notification occurs on override\"\n  );\n  aboutNewTabService.newTabURL = testURL;\n  await notificationPromise;\n\n  // from overridden to default\n  notificationPromise = nextChangeNotificationPromise(\n    \"about:newtab\",\n    \"a notification occurs on reset\"\n  );\n  aboutNewTabService.resetNewTabURL();\n  Assert.ok(\n    aboutNewTabService.activityStreamEnabled,\n    \"Activity Stream should be enabled\"\n  );\n  Assert.equal(\n    aboutNewTabService.defaultURL,\n    ACTIVITY_STREAM_URL,\n    \"Default URL should be the activity stream page\"\n  );\n  await notificationPromise;\n\n  // reset twice, only one notification for default URL\n  notificationPromise = nextChangeNotificationPromise(\n    \"about:newtab\",\n    \"reset occurs\"\n  );\n  aboutNewTabService.resetNewTabURL();\n  await notificationPromise;\n\n  cleanup();\n});\n"
  },
  {
    "path": "test/xpcshell/xpcshell.ini",
    "content": "[DEFAULT]\nhead =\nfirefox-appdir = browser\nskip-if = toolkit == 'android'\n\n[test_AboutNewTabService.js]\n[test_ASRouterTargeting_attribution.js]\nskip-if = toolkit != \"cocoa\" # osx specific tests\n"
  },
  {
    "path": "vendor/PROP_TYPES_LICENSE",
    "content": "MIT License\n\nCopyright (c) 2013-present, Facebook, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "vendor/REACT_AND_REACT_DOM_LICENSE",
    "content": "MIT License\n\nCopyright (c) Facebook, Inc. and its affiliates.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "vendor/REACT_REDUX_LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2015-present Dan Abramov\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "vendor/REACT_TRANSITION_GROUP_LICENSE",
    "content": "BSD 3-Clause License\n\nCopyright (c) 2018, React Community\nForked from React (https://github.com/facebook/react) Copyright 2013-present, Facebook, Inc.\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n  list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n  this list of conditions and the following disclaimer in the documentation\n  and/or other materials provided with the distribution.\n\n* Neither the name of the copyright holder nor the names of its\n  contributors may be used to endorse or promote products derived from\n  this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "vendor/REDUX_LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2015-present Dan Abramov\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "vendor/Redux.jsm",
    "content": "/**\n * Redux v.4.0.1\n *\n * This file was imported from https://unpkg.com/redux@4.0.1/dist/redux.js\n * and reformatted as a Javascript Core Module\n */\nvar EXPORTED_SYMBOLS = [\"redux\"];\nvar self = this;\n\nthis.redux = (function (global, factory) {\n  var exports = {};\n  factory(exports);\n  return exports;\n  }(this, (function (exports) { 'use strict';\n\n  function symbolObservablePonyfill(root) {\n    var result;\n    var Symbol = root.Symbol;\n\n    if (typeof Symbol === 'function') {\n      if (Symbol.observable) {\n        result = Symbol.observable;\n      } else {\n        result = Symbol('observable');\n        Symbol.observable = result;\n      }\n    } else {\n      result = '@@observable';\n    }\n\n    return result;\n  }\n\n  /* global window */\n\n  // This is edited to prevent Function being present in this code.\n  // See https://bugzilla.mozilla.org/show_bug.cgi?id=1486375\n  var root;\n\n  if (typeof self !== 'undefined') {\n    root = self;\n  } else if (typeof global !== 'undefined') {\n    root = global;\n  }\n\n  var result = symbolObservablePonyfill(root);\n\n  /**\n   * These are private action types reserved by Redux.\n   * For any unknown actions, you must return the current state.\n   * If the current state is undefined, you must return the initial state.\n   * Do not reference these action types directly in your code.\n   */\n  var randomString = function randomString() {\n    return Math.random().toString(36).substring(7).split('').join('.');\n  };\n\n  var ActionTypes = {\n    INIT: \"@@redux/INIT\" + randomString(),\n    REPLACE: \"@@redux/REPLACE\" + randomString(),\n    PROBE_UNKNOWN_ACTION: function PROBE_UNKNOWN_ACTION() {\n      return \"@@redux/PROBE_UNKNOWN_ACTION\" + randomString();\n    }\n  };\n\n  /**\n   * @param {any} obj The object to inspect.\n   * @returns {boolean} True if the argument appears to be a plain object.\n   */\n  function isPlainObject(obj) {\n    if (typeof obj !== 'object' || obj === null) return false;\n    var proto = obj;\n\n    while (Object.getPrototypeOf(proto) !== null) {\n      proto = Object.getPrototypeOf(proto);\n    }\n\n    return Object.getPrototypeOf(obj) === proto;\n  }\n\n  /**\n   * Creates a Redux store that holds the state tree.\n   * The only way to change the data in the store is to call `dispatch()` on it.\n   *\n   * There should only be a single store in your app. To specify how different\n   * parts of the state tree respond to actions, you may combine several reducers\n   * into a single reducer function by using `combineReducers`.\n   *\n   * @param {Function} reducer A function that returns the next state tree, given\n   * the current state tree and the action to handle.\n   *\n   * @param {any} [preloadedState] The initial state. You may optionally specify it\n   * to hydrate the state from the server in universal apps, or to restore a\n   * previously serialized user session.\n   * If you use `combineReducers` to produce the root reducer function, this must be\n   * an object with the same shape as `combineReducers` keys.\n   *\n   * @param {Function} [enhancer] The store enhancer. You may optionally specify it\n   * to enhance the store with third-party capabilities such as middleware,\n   * time travel, persistence, etc. The only store enhancer that ships with Redux\n   * is `applyMiddleware()`.\n   *\n   * @returns {Store} A Redux store that lets you read the state, dispatch actions\n   * and subscribe to changes.\n   */\n\n  function createStore(reducer, preloadedState, enhancer) {\n    var _ref2;\n\n    if (typeof preloadedState === 'function' && typeof enhancer === 'function' || typeof enhancer === 'function' && typeof arguments[3] === 'function') {\n      throw new Error('It looks like you are passing several store enhancers to ' + 'createStore(). This is not supported. Instead, compose them ' + 'together to a single function');\n    }\n\n    if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {\n      enhancer = preloadedState;\n      preloadedState = undefined;\n    }\n\n    if (typeof enhancer !== 'undefined') {\n      if (typeof enhancer !== 'function') {\n        throw new Error('Expected the enhancer to be a function.');\n      }\n\n      return enhancer(createStore)(reducer, preloadedState);\n    }\n\n    if (typeof reducer !== 'function') {\n      throw new Error('Expected the reducer to be a function.');\n    }\n\n    var currentReducer = reducer;\n    var currentState = preloadedState;\n    var currentListeners = [];\n    var nextListeners = currentListeners;\n    var isDispatching = false;\n\n    function ensureCanMutateNextListeners() {\n      if (nextListeners === currentListeners) {\n        nextListeners = currentListeners.slice();\n      }\n    }\n    /**\n     * Reads the state tree managed by the store.\n     *\n     * @returns {any} The current state tree of your application.\n     */\n\n\n    function getState() {\n      if (isDispatching) {\n        throw new Error('You may not call store.getState() while the reducer is executing. ' + 'The reducer has already received the state as an argument. ' + 'Pass it down from the top reducer instead of reading it from the store.');\n      }\n\n      return currentState;\n    }\n    /**\n     * Adds a change listener. It will be called any time an action is dispatched,\n     * and some part of the state tree may potentially have changed. You may then\n     * call `getState()` to read the current state tree inside the callback.\n     *\n     * You may call `dispatch()` from a change listener, with the following\n     * caveats:\n     *\n     * 1. The subscriptions are snapshotted just before every `dispatch()` call.\n     * If you subscribe or unsubscribe while the listeners are being invoked, this\n     * will not have any effect on the `dispatch()` that is currently in progress.\n     * However, the next `dispatch()` call, whether nested or not, will use a more\n     * recent snapshot of the subscription list.\n     *\n     * 2. The listener should not expect to see all state changes, as the state\n     * might have been updated multiple times during a nested `dispatch()` before\n     * the listener is called. It is, however, guaranteed that all subscribers\n     * registered before the `dispatch()` started will be called with the latest\n     * state by the time it exits.\n     *\n     * @param {Function} listener A callback to be invoked on every dispatch.\n     * @returns {Function} A function to remove this change listener.\n     */\n\n\n    function subscribe(listener) {\n      if (typeof listener !== 'function') {\n        throw new Error('Expected the listener to be a function.');\n      }\n\n      if (isDispatching) {\n        throw new Error('You may not call store.subscribe() while the reducer is executing. ' + 'If you would like to be notified after the store has been updated, subscribe from a ' + 'component and invoke store.getState() in the callback to access the latest state. ' + 'See https://redux.js.org/api-reference/store#subscribe(listener) for more details.');\n      }\n\n      var isSubscribed = true;\n      ensureCanMutateNextListeners();\n      nextListeners.push(listener);\n      return function unsubscribe() {\n        if (!isSubscribed) {\n          return;\n        }\n\n        if (isDispatching) {\n          throw new Error('You may not unsubscribe from a store listener while the reducer is executing. ' + 'See https://redux.js.org/api-reference/store#subscribe(listener) for more details.');\n        }\n\n        isSubscribed = false;\n        ensureCanMutateNextListeners();\n        var index = nextListeners.indexOf(listener);\n        nextListeners.splice(index, 1);\n      };\n    }\n    /**\n     * Dispatches an action. It is the only way to trigger a state change.\n     *\n     * The `reducer` function, used to create the store, will be called with the\n     * current state tree and the given `action`. Its return value will\n     * be considered the **next** state of the tree, and the change listeners\n     * will be notified.\n     *\n     * The base implementation only supports plain object actions. If you want to\n     * dispatch a Promise, an Observable, a thunk, or something else, you need to\n     * wrap your store creating function into the corresponding middleware. For\n     * example, see the documentation for the `redux-thunk` package. Even the\n     * middleware will eventually dispatch plain object actions using this method.\n     *\n     * @param {Object} action A plain object representing “what changed”. It is\n     * a good idea to keep actions serializable so you can record and replay user\n     * sessions, or use the time travelling `redux-devtools`. An action must have\n     * a `type` property which may not be `undefined`. It is a good idea to use\n     * string constants for action types.\n     *\n     * @returns {Object} For convenience, the same action object you dispatched.\n     *\n     * Note that, if you use a custom middleware, it may wrap `dispatch()` to\n     * return something else (for example, a Promise you can await).\n     */\n\n\n    function dispatch(action) {\n      if (!isPlainObject(action)) {\n        throw new Error('Actions must be plain objects. ' + 'Use custom middleware for async actions.');\n      }\n\n      if (typeof action.type === 'undefined') {\n        throw new Error('Actions may not have an undefined \"type\" property. ' + 'Have you misspelled a constant?');\n      }\n\n      if (isDispatching) {\n        throw new Error('Reducers may not dispatch actions.');\n      }\n\n      try {\n        isDispatching = true;\n        currentState = currentReducer(currentState, action);\n      } finally {\n        isDispatching = false;\n      }\n\n      var listeners = currentListeners = nextListeners;\n\n      for (var i = 0; i < listeners.length; i++) {\n        var listener = listeners[i];\n        listener();\n      }\n\n      return action;\n    }\n    /**\n     * Replaces the reducer currently used by the store to calculate the state.\n     *\n     * You might need this if your app implements code splitting and you want to\n     * load some of the reducers dynamically. You might also need this if you\n     * implement a hot reloading mechanism for Redux.\n     *\n     * @param {Function} nextReducer The reducer for the store to use instead.\n     * @returns {void}\n     */\n\n\n    function replaceReducer(nextReducer) {\n      if (typeof nextReducer !== 'function') {\n        throw new Error('Expected the nextReducer to be a function.');\n      }\n\n      currentReducer = nextReducer;\n      dispatch({\n        type: ActionTypes.REPLACE\n      });\n    }\n    /**\n     * Interoperability point for observable/reactive libraries.\n     * @returns {observable} A minimal observable of state changes.\n     * For more information, see the observable proposal:\n     * https://github.com/tc39/proposal-observable\n     */\n\n\n    function observable() {\n      var _ref;\n\n      var outerSubscribe = subscribe;\n      return _ref = {\n        /**\n         * The minimal observable subscription method.\n         * @param {Object} observer Any object that can be used as an observer.\n         * The observer object should have a `next` method.\n         * @returns {subscription} An object with an `unsubscribe` method that can\n         * be used to unsubscribe the observable from the store, and prevent further\n         * emission of values from the observable.\n         */\n        subscribe: function subscribe(observer) {\n          if (typeof observer !== 'object' || observer === null) {\n            throw new TypeError('Expected the observer to be an object.');\n          }\n\n          function observeState() {\n            if (observer.next) {\n              observer.next(getState());\n            }\n          }\n\n          observeState();\n          var unsubscribe = outerSubscribe(observeState);\n          return {\n            unsubscribe: unsubscribe\n          };\n        }\n      }, _ref[result] = function () {\n        return this;\n      }, _ref;\n    } // When a store is created, an \"INIT\" action is dispatched so that every\n    // reducer returns their initial state. This effectively populates\n    // the initial state tree.\n\n\n    dispatch({\n      type: ActionTypes.INIT\n    });\n    return _ref2 = {\n      dispatch: dispatch,\n      subscribe: subscribe,\n      getState: getState,\n      replaceReducer: replaceReducer\n    }, _ref2[result] = observable, _ref2;\n  }\n\n  /**\n   * Prints a warning in the console if it exists.\n   *\n   * @param {String} message The warning message.\n   * @returns {void}\n   */\n  function warning(message) {\n    /* eslint-disable no-console */\n    if (typeof console !== 'undefined' && typeof console.error === 'function') {\n      console.error(message);\n    }\n    /* eslint-enable no-console */\n\n\n    try {\n      // This error was thrown as a convenience so that if you enable\n      // \"break on all exceptions\" in your console,\n      // it would pause the execution at this line.\n      throw new Error(message);\n    } catch (e) {} // eslint-disable-line no-empty\n\n  }\n\n  function getUndefinedStateErrorMessage(key, action) {\n    var actionType = action && action.type;\n    var actionDescription = actionType && \"action \\\"\" + String(actionType) + \"\\\"\" || 'an action';\n    return \"Given \" + actionDescription + \", reducer \\\"\" + key + \"\\\" returned undefined. \" + \"To ignore an action, you must explicitly return the previous state. \" + \"If you want this reducer to hold no value, you can return null instead of undefined.\";\n  }\n\n  function getUnexpectedStateShapeWarningMessage(inputState, reducers, action, unexpectedKeyCache) {\n    var reducerKeys = Object.keys(reducers);\n    var argumentName = action && action.type === ActionTypes.INIT ? 'preloadedState argument passed to createStore' : 'previous state received by the reducer';\n\n    if (reducerKeys.length === 0) {\n      return 'Store does not have a valid reducer. Make sure the argument passed ' + 'to combineReducers is an object whose values are reducers.';\n    }\n\n    if (!isPlainObject(inputState)) {\n      return \"The \" + argumentName + \" has unexpected type of \\\"\" + {}.toString.call(inputState).match(/\\s([a-z|A-Z]+)/)[1] + \"\\\". Expected argument to be an object with the following \" + (\"keys: \\\"\" + reducerKeys.join('\", \"') + \"\\\"\");\n    }\n\n    var unexpectedKeys = Object.keys(inputState).filter(function (key) {\n      return !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key];\n    });\n    unexpectedKeys.forEach(function (key) {\n      unexpectedKeyCache[key] = true;\n    });\n    if (action && action.type === ActionTypes.REPLACE) return;\n\n    if (unexpectedKeys.length > 0) {\n      return \"Unexpected \" + (unexpectedKeys.length > 1 ? 'keys' : 'key') + \" \" + (\"\\\"\" + unexpectedKeys.join('\", \"') + \"\\\" found in \" + argumentName + \". \") + \"Expected to find one of the known reducer keys instead: \" + (\"\\\"\" + reducerKeys.join('\", \"') + \"\\\". Unexpected keys will be ignored.\");\n    }\n  }\n\n  function assertReducerShape(reducers) {\n    Object.keys(reducers).forEach(function (key) {\n      var reducer = reducers[key];\n      var initialState = reducer(undefined, {\n        type: ActionTypes.INIT\n      });\n\n      if (typeof initialState === 'undefined') {\n        throw new Error(\"Reducer \\\"\" + key + \"\\\" returned undefined during initialization. \" + \"If the state passed to the reducer is undefined, you must \" + \"explicitly return the initial state. The initial state may \" + \"not be undefined. If you don't want to set a value for this reducer, \" + \"you can use null instead of undefined.\");\n      }\n\n      if (typeof reducer(undefined, {\n        type: ActionTypes.PROBE_UNKNOWN_ACTION()\n      }) === 'undefined') {\n        throw new Error(\"Reducer \\\"\" + key + \"\\\" returned undefined when probed with a random type. \" + (\"Don't try to handle \" + ActionTypes.INIT + \" or other actions in \\\"redux/*\\\" \") + \"namespace. They are considered private. Instead, you must return the \" + \"current state for any unknown actions, unless it is undefined, \" + \"in which case you must return the initial state, regardless of the \" + \"action type. The initial state may not be undefined, but can be null.\");\n      }\n    });\n  }\n  /**\n   * Turns an object whose values are different reducer functions, into a single\n   * reducer function. It will call every child reducer, and gather their results\n   * into a single state object, whose keys correspond to the keys of the passed\n   * reducer functions.\n   *\n   * @param {Object} reducers An object whose values correspond to different\n   * reducer functions that need to be combined into one. One handy way to obtain\n   * it is to use ES6 `import * as reducers` syntax. The reducers may never return\n   * undefined for any action. Instead, they should return their initial state\n   * if the state passed to them was undefined, and the current state for any\n   * unrecognized action.\n   *\n   * @returns {Function} A reducer function that invokes every reducer inside the\n   * passed object, and builds a state object with the same shape.\n   */\n\n\n  function combineReducers(reducers) {\n    var reducerKeys = Object.keys(reducers);\n    var finalReducers = {};\n\n    for (var i = 0; i < reducerKeys.length; i++) {\n      var key = reducerKeys[i];\n\n      {\n        if (typeof reducers[key] === 'undefined') {\n          warning(\"No reducer provided for key \\\"\" + key + \"\\\"\");\n        }\n      }\n\n      if (typeof reducers[key] === 'function') {\n        finalReducers[key] = reducers[key];\n      }\n    }\n\n    var finalReducerKeys = Object.keys(finalReducers);\n    var unexpectedKeyCache;\n\n    {\n      unexpectedKeyCache = {};\n    }\n\n    var shapeAssertionError;\n\n    try {\n      assertReducerShape(finalReducers);\n    } catch (e) {\n      shapeAssertionError = e;\n    }\n\n    return function combination(state, action) {\n      if (state === void 0) {\n        state = {};\n      }\n\n      if (shapeAssertionError) {\n        throw shapeAssertionError;\n      }\n\n      {\n        var warningMessage = getUnexpectedStateShapeWarningMessage(state, finalReducers, action, unexpectedKeyCache);\n\n        if (warningMessage) {\n          warning(warningMessage);\n        }\n      }\n\n      var hasChanged = false;\n      var nextState = {};\n\n      for (var _i = 0; _i < finalReducerKeys.length; _i++) {\n        var _key = finalReducerKeys[_i];\n        var reducer = finalReducers[_key];\n        var previousStateForKey = state[_key];\n        var nextStateForKey = reducer(previousStateForKey, action);\n\n        if (typeof nextStateForKey === 'undefined') {\n          var errorMessage = getUndefinedStateErrorMessage(_key, action);\n          throw new Error(errorMessage);\n        }\n\n        nextState[_key] = nextStateForKey;\n        hasChanged = hasChanged || nextStateForKey !== previousStateForKey;\n      }\n\n      return hasChanged ? nextState : state;\n    };\n  }\n\n  function bindActionCreator(actionCreator, dispatch) {\n    return function () {\n      return dispatch(actionCreator.apply(this, arguments));\n    };\n  }\n  /**\n   * Turns an object whose values are action creators, into an object with the\n   * same keys, but with every function wrapped into a `dispatch` call so they\n   * may be invoked directly. This is just a convenience method, as you can call\n   * `store.dispatch(MyActionCreators.doSomething())` yourself just fine.\n   *\n   * For convenience, you can also pass a single function as the first argument,\n   * and get a function in return.\n   *\n   * @param {Function|Object} actionCreators An object whose values are action\n   * creator functions. One handy way to obtain it is to use ES6 `import * as`\n   * syntax. You may also pass a single function.\n   *\n   * @param {Function} dispatch The `dispatch` function available on your Redux\n   * store.\n   *\n   * @returns {Function|Object} The object mimicking the original object, but with\n   * every action creator wrapped into the `dispatch` call. If you passed a\n   * function as `actionCreators`, the return value will also be a single\n   * function.\n   */\n\n\n  function bindActionCreators(actionCreators, dispatch) {\n    if (typeof actionCreators === 'function') {\n      return bindActionCreator(actionCreators, dispatch);\n    }\n\n    if (typeof actionCreators !== 'object' || actionCreators === null) {\n      throw new Error(\"bindActionCreators expected an object or a function, instead received \" + (actionCreators === null ? 'null' : typeof actionCreators) + \". \" + \"Did you write \\\"import ActionCreators from\\\" instead of \\\"import * as ActionCreators from\\\"?\");\n    }\n\n    var keys = Object.keys(actionCreators);\n    var boundActionCreators = {};\n\n    for (var i = 0; i < keys.length; i++) {\n      var key = keys[i];\n      var actionCreator = actionCreators[key];\n\n      if (typeof actionCreator === 'function') {\n        boundActionCreators[key] = bindActionCreator(actionCreator, dispatch);\n      }\n    }\n\n    return boundActionCreators;\n  }\n\n  function _defineProperty(obj, key, value) {\n    if (key in obj) {\n      Object.defineProperty(obj, key, {\n        value: value,\n        enumerable: true,\n        configurable: true,\n        writable: true\n      });\n    } else {\n      obj[key] = value;\n    }\n\n    return obj;\n  }\n\n  function _objectSpread(target) {\n    for (var i = 1; i < arguments.length; i++) {\n      var source = arguments[i] != null ? arguments[i] : {};\n      var ownKeys = Object.keys(source);\n\n      if (typeof Object.getOwnPropertySymbols === 'function') {\n        ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) {\n          return Object.getOwnPropertyDescriptor(source, sym).enumerable;\n        }));\n      }\n\n      ownKeys.forEach(function (key) {\n        _defineProperty(target, key, source[key]);\n      });\n    }\n\n    return target;\n  }\n\n  /**\n   * Composes single-argument functions from right to left. The rightmost\n   * function can take multiple arguments as it provides the signature for\n   * the resulting composite function.\n   *\n   * @param {...Function} funcs The functions to compose.\n   * @returns {Function} A function obtained by composing the argument functions\n   * from right to left. For example, compose(f, g, h) is identical to doing\n   * (...args) => f(g(h(...args))).\n   */\n  function compose() {\n    for (var _len = arguments.length, funcs = new Array(_len), _key = 0; _key < _len; _key++) {\n      funcs[_key] = arguments[_key];\n    }\n\n    if (funcs.length === 0) {\n      return function (arg) {\n        return arg;\n      };\n    }\n\n    if (funcs.length === 1) {\n      return funcs[0];\n    }\n\n    return funcs.reduce(function (a, b) {\n      return function () {\n        return a(b.apply(void 0, arguments));\n      };\n    });\n  }\n\n  /**\n   * Creates a store enhancer that applies middleware to the dispatch method\n   * of the Redux store. This is handy for a variety of tasks, such as expressing\n   * asynchronous actions in a concise manner, or logging every action payload.\n   *\n   * See `redux-thunk` package as an example of the Redux middleware.\n   *\n   * Because middleware is potentially asynchronous, this should be the first\n   * store enhancer in the composition chain.\n   *\n   * Note that each middleware will be given the `dispatch` and `getState` functions\n   * as named arguments.\n   *\n   * @param {...Function} middlewares The middleware chain to be applied.\n   * @returns {Function} A store enhancer applying the middleware.\n   */\n\n  function applyMiddleware() {\n    for (var _len = arguments.length, middlewares = new Array(_len), _key = 0; _key < _len; _key++) {\n      middlewares[_key] = arguments[_key];\n    }\n\n    return function (createStore) {\n      return function () {\n        var store = createStore.apply(void 0, arguments);\n\n        var _dispatch = function dispatch() {\n          throw new Error(\"Dispatching while constructing your middleware is not allowed. \" + \"Other middleware would not be applied to this dispatch.\");\n        };\n\n        var middlewareAPI = {\n          getState: store.getState,\n          dispatch: function dispatch() {\n            return _dispatch.apply(void 0, arguments);\n          }\n        };\n        var chain = middlewares.map(function (middleware) {\n          return middleware(middlewareAPI);\n        });\n        _dispatch = compose.apply(void 0, chain)(store.dispatch);\n        return _objectSpread({}, store, {\n          dispatch: _dispatch\n        });\n      };\n    };\n  }\n\n  /*\n   * This is a dummy function to check if the function name has been altered by minification.\n   * If the function has been minified and NODE_ENV !== 'production', warn the user.\n   */\n\n  function isCrushed() {}\n\n  if (typeof isCrushed.name === 'string' && isCrushed.name !== 'isCrushed') {\n    warning('You are currently using minified code outside of NODE_ENV === \"production\". ' + 'This means that you are running a slower development build of Redux. ' + 'You can use loose-envify (https://github.com/zertosh/loose-envify) for browserify ' + 'or setting mode to production in webpack (https://webpack.js.org/concepts/mode/) ' + 'to ensure you have the correct code for your production build.');\n  }\n\n  exports.createStore = createStore;\n  exports.combineReducers = combineReducers;\n  exports.bindActionCreators = bindActionCreators;\n  exports.applyMiddleware = applyMiddleware;\n  exports.compose = compose;\n  exports.__DO_NOT_USE__ActionTypes = ActionTypes;\n\n  Object.defineProperty(exports, '__esModule', { value: true });\n\n  })));\n"
  },
  {
    "path": "vendor/prop-types.js",
    "content": "!function(f){if(\"object\"==typeof exports&&\"undefined\"!=typeof module)module.exports=f();else if(\"function\"==typeof define&&define.amd)define([],f);else{var g;g=\"undefined\"!=typeof window?window:\"undefined\"!=typeof global?global:\"undefined\"!=typeof self?self:this,g.PropTypes=f()}}(function(){return function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=\"function\"==typeof require&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error(\"Cannot find module '\"+o+\"'\");throw f.code=\"MODULE_NOT_FOUND\",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}for(var i=\"function\"==typeof require&&require,o=0;o<r.length;o++)s(r[o]);return s}({1:[function(require,module){\"use strict\";var emptyFunction=require(4),invariant=require(5),ReactPropTypesSecret=require(3);module.exports=function(){function e(e,r,t,n,p,o){o!==ReactPropTypesSecret&&invariant(!1,\"Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types\")}function r(){return e}e.isRequired=e;var t={array:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:r,element:e,instanceOf:r,node:e,objectOf:r,oneOf:r,oneOfType:r,shape:r,exact:r};return t.checkPropTypes=emptyFunction,t.PropTypes=t,t}},{3:3,4:4,5:5}],2:[function(require,module){module.exports=require(1)()},{1:1}],3:[function(require,module){\"use strict\";var ReactPropTypesSecret=\"SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED\";module.exports=ReactPropTypesSecret},{}],4:[function(require,module){\"use strict\";function makeEmptyFunction(arg){return function(){return arg}}var emptyFunction=function(){};emptyFunction.thatReturns=makeEmptyFunction,emptyFunction.thatReturnsFalse=makeEmptyFunction(!1),emptyFunction.thatReturnsTrue=makeEmptyFunction(!0),emptyFunction.thatReturnsNull=makeEmptyFunction(null),emptyFunction.thatReturnsThis=function(){return this},emptyFunction.thatReturnsArgument=function(arg){return arg},module.exports=emptyFunction},{}],5:[function(require,module){\"use strict\";function invariant(condition,format,a,b,c,d,e,f){if(validateFormat(format),!condition){var error;if(void 0===format)error=new Error(\"Minified exception occurred; use the non-minified dev environment for the full error message and additional helpful warnings.\");else{var args=[a,b,c,d,e,f],argIndex=0;error=new Error(format.replace(/%s/g,function(){return args[argIndex++]})),error.name=\"Invariant Violation\"}throw error.framesToPop=1,error}}var validateFormat=function(){};module.exports=invariant},{}]},{},[2])(2)});"
  },
  {
    "path": "vendor/react-dev.js",
    "content": "/** @license React v16.8.6\n * react.development.js\n *\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n'use strict';\n\n(function (global, factory) {\n\ttypeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :\n\ttypeof define === 'function' && define.amd ? define(factory) :\n\t(global.React = factory());\n}(this, (function () { 'use strict';\n\n// TODO: this is special because it gets imported during build.\n\nvar ReactVersion = '16.8.6';\n\n// The Symbol used to tag the ReactElement-like types. If there is no native Symbol\n// nor polyfill, then a plain number is used for performance.\nvar hasSymbol = typeof Symbol === 'function' && Symbol.for;\n\nvar REACT_ELEMENT_TYPE = hasSymbol ? Symbol.for('react.element') : 0xeac7;\nvar REACT_PORTAL_TYPE = hasSymbol ? Symbol.for('react.portal') : 0xeaca;\nvar REACT_FRAGMENT_TYPE = hasSymbol ? Symbol.for('react.fragment') : 0xeacb;\nvar REACT_STRICT_MODE_TYPE = hasSymbol ? Symbol.for('react.strict_mode') : 0xeacc;\nvar REACT_PROFILER_TYPE = hasSymbol ? Symbol.for('react.profiler') : 0xead2;\nvar REACT_PROVIDER_TYPE = hasSymbol ? Symbol.for('react.provider') : 0xeacd;\nvar REACT_CONTEXT_TYPE = hasSymbol ? Symbol.for('react.context') : 0xeace;\n\nvar REACT_CONCURRENT_MODE_TYPE = hasSymbol ? Symbol.for('react.concurrent_mode') : 0xeacf;\nvar REACT_FORWARD_REF_TYPE = hasSymbol ? Symbol.for('react.forward_ref') : 0xead0;\nvar REACT_SUSPENSE_TYPE = hasSymbol ? Symbol.for('react.suspense') : 0xead1;\nvar REACT_MEMO_TYPE = hasSymbol ? Symbol.for('react.memo') : 0xead3;\nvar REACT_LAZY_TYPE = hasSymbol ? Symbol.for('react.lazy') : 0xead4;\n\nvar MAYBE_ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator;\nvar FAUX_ITERATOR_SYMBOL = '@@iterator';\n\nfunction getIteratorFn(maybeIterable) {\n  if (maybeIterable === null || typeof maybeIterable !== 'object') {\n    return null;\n  }\n  var maybeIterator = MAYBE_ITERATOR_SYMBOL && maybeIterable[MAYBE_ITERATOR_SYMBOL] || maybeIterable[FAUX_ITERATOR_SYMBOL];\n  if (typeof maybeIterator === 'function') {\n    return maybeIterator;\n  }\n  return null;\n}\n\n/*\nobject-assign\n(c) Sindre Sorhus\n@license MIT\n*/\n\n\n/* eslint-disable no-unused-vars */\nvar getOwnPropertySymbols = Object.getOwnPropertySymbols;\nvar hasOwnProperty = Object.prototype.hasOwnProperty;\nvar propIsEnumerable = Object.prototype.propertyIsEnumerable;\n\nfunction toObject(val) {\n\tif (val === null || val === undefined) {\n\t\tthrow new TypeError('Object.assign cannot be called with null or undefined');\n\t}\n\n\treturn Object(val);\n}\n\nfunction shouldUseNative() {\n\ttry {\n\t\tif (!Object.assign) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Detect buggy property enumeration order in older V8 versions.\n\n\t\t// https://bugs.chromium.org/p/v8/issues/detail?id=4118\n\t\tvar test1 = new String('abc');  // eslint-disable-line no-new-wrappers\n\t\ttest1[5] = 'de';\n\t\tif (Object.getOwnPropertyNames(test1)[0] === '5') {\n\t\t\treturn false;\n\t\t}\n\n\t\t// https://bugs.chromium.org/p/v8/issues/detail?id=3056\n\t\tvar test2 = {};\n\t\tfor (var i = 0; i < 10; i++) {\n\t\t\ttest2['_' + String.fromCharCode(i)] = i;\n\t\t}\n\t\tvar order2 = Object.getOwnPropertyNames(test2).map(function (n) {\n\t\t\treturn test2[n];\n\t\t});\n\t\tif (order2.join('') !== '0123456789') {\n\t\t\treturn false;\n\t\t}\n\n\t\t// https://bugs.chromium.org/p/v8/issues/detail?id=3056\n\t\tvar test3 = {};\n\t\t'abcdefghijklmnopqrst'.split('').forEach(function (letter) {\n\t\t\ttest3[letter] = letter;\n\t\t});\n\t\tif (Object.keys(Object.assign({}, test3)).join('') !==\n\t\t\t\t'abcdefghijklmnopqrst') {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn true;\n\t} catch (err) {\n\t\t// We don't expect any of the above to throw, but better to be safe.\n\t\treturn false;\n\t}\n}\n\nvar objectAssign = shouldUseNative() ? Object.assign : function (target, source) {\n\tvar from;\n\tvar to = toObject(target);\n\tvar symbols;\n\n\tfor (var s = 1; s < arguments.length; s++) {\n\t\tfrom = Object(arguments[s]);\n\n\t\tfor (var key in from) {\n\t\t\tif (hasOwnProperty.call(from, key)) {\n\t\t\t\tto[key] = from[key];\n\t\t\t}\n\t\t}\n\n\t\tif (getOwnPropertySymbols) {\n\t\t\tsymbols = getOwnPropertySymbols(from);\n\t\t\tfor (var i = 0; i < symbols.length; i++) {\n\t\t\t\tif (propIsEnumerable.call(from, symbols[i])) {\n\t\t\t\t\tto[symbols[i]] = from[symbols[i]];\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn to;\n};\n\n/**\n * Use invariant() to assert state which your program assumes to be true.\n *\n * Provide sprintf-style format (only %s is supported) and arguments\n * to provide information about what broke and what you were\n * expecting.\n *\n * The invariant message will be stripped in production, but the invariant\n * will remain to ensure logic does not differ in production.\n */\n\nvar validateFormat = function () {};\n\n{\n  validateFormat = function (format) {\n    if (format === undefined) {\n      throw new Error('invariant requires an error message argument');\n    }\n  };\n}\n\nfunction invariant(condition, format, a, b, c, d, e, f) {\n  validateFormat(format);\n\n  if (!condition) {\n    var error = void 0;\n    if (format === undefined) {\n      error = new Error('Minified exception occurred; use the non-minified dev environment ' + 'for the full error message and additional helpful warnings.');\n    } else {\n      var args = [a, b, c, d, e, f];\n      var argIndex = 0;\n      error = new Error(format.replace(/%s/g, function () {\n        return args[argIndex++];\n      }));\n      error.name = 'Invariant Violation';\n    }\n\n    error.framesToPop = 1; // we don't care about invariant's own frame\n    throw error;\n  }\n}\n\n// Relying on the `invariant()` implementation lets us\n// preserve the format and params in the www builds.\n\n/**\n * Forked from fbjs/warning:\n * https://github.com/facebook/fbjs/blob/e66ba20ad5be433eb54423f2b097d829324d9de6/packages/fbjs/src/__forks__/warning.js\n *\n * Only change is we use console.warn instead of console.error,\n * and do nothing when 'console' is not supported.\n * This really simplifies the code.\n * ---\n * Similar to invariant but only logs a warning if the condition is not met.\n * This can be used to log issues in development environments in critical\n * paths. Removing the logging code for production environments will keep the\n * same logic and follow the same code paths.\n */\n\nvar lowPriorityWarning = function () {};\n\n{\n  var printWarning = function (format) {\n    for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {\n      args[_key - 1] = arguments[_key];\n    }\n\n    var argIndex = 0;\n    var message = 'Warning: ' + format.replace(/%s/g, function () {\n      return args[argIndex++];\n    });\n    if (typeof console !== 'undefined') {\n      console.warn(message);\n    }\n    try {\n      // --- Welcome to debugging React ---\n      // This error was thrown as a convenience so that you can use this stack\n      // to find the callsite that caused this warning to fire.\n      throw new Error(message);\n    } catch (x) {}\n  };\n\n  lowPriorityWarning = function (condition, format) {\n    if (format === undefined) {\n      throw new Error('`lowPriorityWarning(condition, format, ...args)` requires a warning ' + 'message argument');\n    }\n    if (!condition) {\n      for (var _len2 = arguments.length, args = Array(_len2 > 2 ? _len2 - 2 : 0), _key2 = 2; _key2 < _len2; _key2++) {\n        args[_key2 - 2] = arguments[_key2];\n      }\n\n      printWarning.apply(undefined, [format].concat(args));\n    }\n  };\n}\n\nvar lowPriorityWarning$1 = lowPriorityWarning;\n\n/**\n * Similar to invariant but only logs a warning if the condition is not met.\n * This can be used to log issues in development environments in critical\n * paths. Removing the logging code for production environments will keep the\n * same logic and follow the same code paths.\n */\n\nvar warningWithoutStack = function () {};\n\n{\n  warningWithoutStack = function (condition, format) {\n    for (var _len = arguments.length, args = Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {\n      args[_key - 2] = arguments[_key];\n    }\n\n    if (format === undefined) {\n      throw new Error('`warningWithoutStack(condition, format, ...args)` requires a warning ' + 'message argument');\n    }\n    if (args.length > 8) {\n      // Check before the condition to catch violations early.\n      throw new Error('warningWithoutStack() currently supports at most 8 arguments.');\n    }\n    if (condition) {\n      return;\n    }\n    if (typeof console !== 'undefined') {\n      var argsWithFormat = args.map(function (item) {\n        return '' + item;\n      });\n      argsWithFormat.unshift('Warning: ' + format);\n\n      // We intentionally don't use spread (or .apply) directly because it\n      // breaks IE9: https://github.com/facebook/react/issues/13610\n      Function.prototype.apply.call(console.error, console, argsWithFormat);\n    }\n    try {\n      // --- Welcome to debugging React ---\n      // This error was thrown as a convenience so that you can use this stack\n      // to find the callsite that caused this warning to fire.\n      var argIndex = 0;\n      var message = 'Warning: ' + format.replace(/%s/g, function () {\n        return args[argIndex++];\n      });\n      throw new Error(message);\n    } catch (x) {}\n  };\n}\n\nvar warningWithoutStack$1 = warningWithoutStack;\n\nvar didWarnStateUpdateForUnmountedComponent = {};\n\nfunction warnNoop(publicInstance, callerName) {\n  {\n    var _constructor = publicInstance.constructor;\n    var componentName = _constructor && (_constructor.displayName || _constructor.name) || 'ReactClass';\n    var warningKey = componentName + '.' + callerName;\n    if (didWarnStateUpdateForUnmountedComponent[warningKey]) {\n      return;\n    }\n    warningWithoutStack$1(false, \"Can't call %s on a component that is not yet mounted. \" + 'This is a no-op, but it might indicate a bug in your application. ' + 'Instead, assign to `this.state` directly or define a `state = {};` ' + 'class property with the desired state in the %s component.', callerName, componentName);\n    didWarnStateUpdateForUnmountedComponent[warningKey] = true;\n  }\n}\n\n/**\n * This is the abstract API for an update queue.\n */\nvar ReactNoopUpdateQueue = {\n  /**\n   * Checks whether or not this composite component is mounted.\n   * @param {ReactClass} publicInstance The instance we want to test.\n   * @return {boolean} True if mounted, false otherwise.\n   * @protected\n   * @final\n   */\n  isMounted: function (publicInstance) {\n    return false;\n  },\n\n  /**\n   * Forces an update. This should only be invoked when it is known with\n   * certainty that we are **not** in a DOM transaction.\n   *\n   * You may want to call this when you know that some deeper aspect of the\n   * component's state has changed but `setState` was not called.\n   *\n   * This will not invoke `shouldComponentUpdate`, but it will invoke\n   * `componentWillUpdate` and `componentDidUpdate`.\n   *\n   * @param {ReactClass} publicInstance The instance that should rerender.\n   * @param {?function} callback Called after component is updated.\n   * @param {?string} callerName name of the calling function in the public API.\n   * @internal\n   */\n  enqueueForceUpdate: function (publicInstance, callback, callerName) {\n    warnNoop(publicInstance, 'forceUpdate');\n  },\n\n  /**\n   * Replaces all of the state. Always use this or `setState` to mutate state.\n   * You should treat `this.state` as immutable.\n   *\n   * There is no guarantee that `this.state` will be immediately updated, so\n   * accessing `this.state` after calling this method may return the old value.\n   *\n   * @param {ReactClass} publicInstance The instance that should rerender.\n   * @param {object} completeState Next state.\n   * @param {?function} callback Called after component is updated.\n   * @param {?string} callerName name of the calling function in the public API.\n   * @internal\n   */\n  enqueueReplaceState: function (publicInstance, completeState, callback, callerName) {\n    warnNoop(publicInstance, 'replaceState');\n  },\n\n  /**\n   * Sets a subset of the state. This only exists because _pendingState is\n   * internal. This provides a merging strategy that is not available to deep\n   * properties which is confusing. TODO: Expose pendingState or don't use it\n   * during the merge.\n   *\n   * @param {ReactClass} publicInstance The instance that should rerender.\n   * @param {object} partialState Next partial state to be merged with state.\n   * @param {?function} callback Called after component is updated.\n   * @param {?string} Name of the calling function in the public API.\n   * @internal\n   */\n  enqueueSetState: function (publicInstance, partialState, callback, callerName) {\n    warnNoop(publicInstance, 'setState');\n  }\n};\n\nvar emptyObject = {};\n{\n  Object.freeze(emptyObject);\n}\n\n/**\n * Base class helpers for the updating state of a component.\n */\nfunction Component(props, context, updater) {\n  this.props = props;\n  this.context = context;\n  // If a component has string refs, we will assign a different object later.\n  this.refs = emptyObject;\n  // We initialize the default updater but the real one gets injected by the\n  // renderer.\n  this.updater = updater || ReactNoopUpdateQueue;\n}\n\nComponent.prototype.isReactComponent = {};\n\n/**\n * Sets a subset of the state. Always use this to mutate\n * state. You should treat `this.state` as immutable.\n *\n * There is no guarantee that `this.state` will be immediately updated, so\n * accessing `this.state` after calling this method may return the old value.\n *\n * There is no guarantee that calls to `setState` will run synchronously,\n * as they may eventually be batched together.  You can provide an optional\n * callback that will be executed when the call to setState is actually\n * completed.\n *\n * When a function is provided to setState, it will be called at some point in\n * the future (not synchronously). It will be called with the up to date\n * component arguments (state, props, context). These values can be different\n * from this.* because your function may be called after receiveProps but before\n * shouldComponentUpdate, and this new state, props, and context will not yet be\n * assigned to this.\n *\n * @param {object|function} partialState Next partial state or function to\n *        produce next partial state to be merged with current state.\n * @param {?function} callback Called after state is updated.\n * @final\n * @protected\n */\nComponent.prototype.setState = function (partialState, callback) {\n  !(typeof partialState === 'object' || typeof partialState === 'function' || partialState == null) ? invariant(false, 'setState(...): takes an object of state variables to update or a function which returns an object of state variables.') : void 0;\n  this.updater.enqueueSetState(this, partialState, callback, 'setState');\n};\n\n/**\n * Forces an update. This should only be invoked when it is known with\n * certainty that we are **not** in a DOM transaction.\n *\n * You may want to call this when you know that some deeper aspect of the\n * component's state has changed but `setState` was not called.\n *\n * This will not invoke `shouldComponentUpdate`, but it will invoke\n * `componentWillUpdate` and `componentDidUpdate`.\n *\n * @param {?function} callback Called after update is complete.\n * @final\n * @protected\n */\nComponent.prototype.forceUpdate = function (callback) {\n  this.updater.enqueueForceUpdate(this, callback, 'forceUpdate');\n};\n\n/**\n * Deprecated APIs. These APIs used to exist on classic React classes but since\n * we would like to deprecate them, we're not going to move them over to this\n * modern base class. Instead, we define a getter that warns if it's accessed.\n */\n{\n  var deprecatedAPIs = {\n    isMounted: ['isMounted', 'Instead, make sure to clean up subscriptions and pending requests in ' + 'componentWillUnmount to prevent memory leaks.'],\n    replaceState: ['replaceState', 'Refactor your code to use setState instead (see ' + 'https://github.com/facebook/react/issues/3236).']\n  };\n  var defineDeprecationWarning = function (methodName, info) {\n    Object.defineProperty(Component.prototype, methodName, {\n      get: function () {\n        lowPriorityWarning$1(false, '%s(...) is deprecated in plain JavaScript React classes. %s', info[0], info[1]);\n        return undefined;\n      }\n    });\n  };\n  for (var fnName in deprecatedAPIs) {\n    if (deprecatedAPIs.hasOwnProperty(fnName)) {\n      defineDeprecationWarning(fnName, deprecatedAPIs[fnName]);\n    }\n  }\n}\n\nfunction ComponentDummy() {}\nComponentDummy.prototype = Component.prototype;\n\n/**\n * Convenience component with default shallow equality check for sCU.\n */\nfunction PureComponent(props, context, updater) {\n  this.props = props;\n  this.context = context;\n  // If a component has string refs, we will assign a different object later.\n  this.refs = emptyObject;\n  this.updater = updater || ReactNoopUpdateQueue;\n}\n\nvar pureComponentPrototype = PureComponent.prototype = new ComponentDummy();\npureComponentPrototype.constructor = PureComponent;\n// Avoid an extra prototype jump for these methods.\nobjectAssign(pureComponentPrototype, Component.prototype);\npureComponentPrototype.isPureReactComponent = true;\n\n// an immutable object with a single mutable value\nfunction createRef() {\n  var refObject = {\n    current: null\n  };\n  {\n    Object.seal(refObject);\n  }\n  return refObject;\n}\n\nvar enableSchedulerDebugging = false;\n\n/* eslint-disable no-var */\n\n// TODO: Use symbols?\nvar ImmediatePriority = 1;\nvar UserBlockingPriority = 2;\nvar NormalPriority = 3;\nvar LowPriority = 4;\nvar IdlePriority = 5;\n\n// Max 31 bit integer. The max integer size in V8 for 32-bit systems.\n// Math.pow(2, 30) - 1\n// 0b111111111111111111111111111111\nvar maxSigned31BitInt = 1073741823;\n\n// Times out immediately\nvar IMMEDIATE_PRIORITY_TIMEOUT = -1;\n// Eventually times out\nvar USER_BLOCKING_PRIORITY = 250;\nvar NORMAL_PRIORITY_TIMEOUT = 5000;\nvar LOW_PRIORITY_TIMEOUT = 10000;\n// Never times out\nvar IDLE_PRIORITY = maxSigned31BitInt;\n\n// Callbacks are stored as a circular, doubly linked list.\nvar firstCallbackNode = null;\n\nvar currentDidTimeout = false;\n// Pausing the scheduler is useful for debugging.\nvar isSchedulerPaused = false;\n\nvar currentPriorityLevel = NormalPriority;\nvar currentEventStartTime = -1;\nvar currentExpirationTime = -1;\n\n// This is set when a callback is being executed, to prevent re-entrancy.\nvar isExecutingCallback = false;\n\nvar isHostCallbackScheduled = false;\n\nvar hasNativePerformanceNow = typeof performance === 'object' && typeof performance.now === 'function';\n\nfunction ensureHostCallbackIsScheduled() {\n  if (isExecutingCallback) {\n    // Don't schedule work yet; wait until the next time we yield.\n    return;\n  }\n  // Schedule the host callback using the earliest expiration in the list.\n  var expirationTime = firstCallbackNode.expirationTime;\n  if (!isHostCallbackScheduled) {\n    isHostCallbackScheduled = true;\n  } else {\n    // Cancel the existing host callback.\n    cancelHostCallback();\n  }\n  requestHostCallback(flushWork, expirationTime);\n}\n\nfunction flushFirstCallback() {\n  var flushedNode = firstCallbackNode;\n\n  // Remove the node from the list before calling the callback. That way the\n  // list is in a consistent state even if the callback throws.\n  var next = firstCallbackNode.next;\n  if (firstCallbackNode === next) {\n    // This is the last callback in the list.\n    firstCallbackNode = null;\n    next = null;\n  } else {\n    var lastCallbackNode = firstCallbackNode.previous;\n    firstCallbackNode = lastCallbackNode.next = next;\n    next.previous = lastCallbackNode;\n  }\n\n  flushedNode.next = flushedNode.previous = null;\n\n  // Now it's safe to call the callback.\n  var callback = flushedNode.callback;\n  var expirationTime = flushedNode.expirationTime;\n  var priorityLevel = flushedNode.priorityLevel;\n  var previousPriorityLevel = currentPriorityLevel;\n  var previousExpirationTime = currentExpirationTime;\n  currentPriorityLevel = priorityLevel;\n  currentExpirationTime = expirationTime;\n  var continuationCallback;\n  try {\n    continuationCallback = callback();\n  } finally {\n    currentPriorityLevel = previousPriorityLevel;\n    currentExpirationTime = previousExpirationTime;\n  }\n\n  // A callback may return a continuation. The continuation should be scheduled\n  // with the same priority and expiration as the just-finished callback.\n  if (typeof continuationCallback === 'function') {\n    var continuationNode = {\n      callback: continuationCallback,\n      priorityLevel: priorityLevel,\n      expirationTime: expirationTime,\n      next: null,\n      previous: null\n    };\n\n    // Insert the new callback into the list, sorted by its expiration. This is\n    // almost the same as the code in `scheduleCallback`, except the callback\n    // is inserted into the list *before* callbacks of equal expiration instead\n    // of after.\n    if (firstCallbackNode === null) {\n      // This is the first callback in the list.\n      firstCallbackNode = continuationNode.next = continuationNode.previous = continuationNode;\n    } else {\n      var nextAfterContinuation = null;\n      var node = firstCallbackNode;\n      do {\n        if (node.expirationTime >= expirationTime) {\n          // This callback expires at or after the continuation. We will insert\n          // the continuation *before* this callback.\n          nextAfterContinuation = node;\n          break;\n        }\n        node = node.next;\n      } while (node !== firstCallbackNode);\n\n      if (nextAfterContinuation === null) {\n        // No equal or lower priority callback was found, which means the new\n        // callback is the lowest priority callback in the list.\n        nextAfterContinuation = firstCallbackNode;\n      } else if (nextAfterContinuation === firstCallbackNode) {\n        // The new callback is the highest priority callback in the list.\n        firstCallbackNode = continuationNode;\n        ensureHostCallbackIsScheduled();\n      }\n\n      var previous = nextAfterContinuation.previous;\n      previous.next = nextAfterContinuation.previous = continuationNode;\n      continuationNode.next = nextAfterContinuation;\n      continuationNode.previous = previous;\n    }\n  }\n}\n\nfunction flushImmediateWork() {\n  if (\n  // Confirm we've exited the outer most event handler\n  currentEventStartTime === -1 && firstCallbackNode !== null && firstCallbackNode.priorityLevel === ImmediatePriority) {\n    isExecutingCallback = true;\n    try {\n      do {\n        flushFirstCallback();\n      } while (\n      // Keep flushing until there are no more immediate callbacks\n      firstCallbackNode !== null && firstCallbackNode.priorityLevel === ImmediatePriority);\n    } finally {\n      isExecutingCallback = false;\n      if (firstCallbackNode !== null) {\n        // There's still work remaining. Request another callback.\n        ensureHostCallbackIsScheduled();\n      } else {\n        isHostCallbackScheduled = false;\n      }\n    }\n  }\n}\n\nfunction flushWork(didTimeout) {\n  // Exit right away if we're currently paused\n\n  if (enableSchedulerDebugging && isSchedulerPaused) {\n    return;\n  }\n\n  isExecutingCallback = true;\n  var previousDidTimeout = currentDidTimeout;\n  currentDidTimeout = didTimeout;\n  try {\n    if (didTimeout) {\n      // Flush all the expired callbacks without yielding.\n      while (firstCallbackNode !== null && !(enableSchedulerDebugging && isSchedulerPaused)) {\n        // TODO Wrap in feature flag\n        // Read the current time. Flush all the callbacks that expire at or\n        // earlier than that time. Then read the current time again and repeat.\n        // This optimizes for as few performance.now calls as possible.\n        var currentTime = getCurrentTime();\n        if (firstCallbackNode.expirationTime <= currentTime) {\n          do {\n            flushFirstCallback();\n          } while (firstCallbackNode !== null && firstCallbackNode.expirationTime <= currentTime && !(enableSchedulerDebugging && isSchedulerPaused));\n          continue;\n        }\n        break;\n      }\n    } else {\n      // Keep flushing callbacks until we run out of time in the frame.\n      if (firstCallbackNode !== null) {\n        do {\n          if (enableSchedulerDebugging && isSchedulerPaused) {\n            break;\n          }\n          flushFirstCallback();\n        } while (firstCallbackNode !== null && !shouldYieldToHost());\n      }\n    }\n  } finally {\n    isExecutingCallback = false;\n    currentDidTimeout = previousDidTimeout;\n    if (firstCallbackNode !== null) {\n      // There's still work remaining. Request another callback.\n      ensureHostCallbackIsScheduled();\n    } else {\n      isHostCallbackScheduled = false;\n    }\n    // Before exiting, flush all the immediate work that was scheduled.\n    flushImmediateWork();\n  }\n}\n\nfunction unstable_runWithPriority(priorityLevel, eventHandler) {\n  switch (priorityLevel) {\n    case ImmediatePriority:\n    case UserBlockingPriority:\n    case NormalPriority:\n    case LowPriority:\n    case IdlePriority:\n      break;\n    default:\n      priorityLevel = NormalPriority;\n  }\n\n  var previousPriorityLevel = currentPriorityLevel;\n  var previousEventStartTime = currentEventStartTime;\n  currentPriorityLevel = priorityLevel;\n  currentEventStartTime = getCurrentTime();\n\n  try {\n    return eventHandler();\n  } finally {\n    currentPriorityLevel = previousPriorityLevel;\n    currentEventStartTime = previousEventStartTime;\n\n    // Before exiting, flush all the immediate work that was scheduled.\n    flushImmediateWork();\n  }\n}\n\nfunction unstable_next(eventHandler) {\n  var priorityLevel = void 0;\n  switch (currentPriorityLevel) {\n    case ImmediatePriority:\n    case UserBlockingPriority:\n    case NormalPriority:\n      // Shift down to normal priority\n      priorityLevel = NormalPriority;\n      break;\n    default:\n      // Anything lower than normal priority should remain at the current level.\n      priorityLevel = currentPriorityLevel;\n      break;\n  }\n\n  var previousPriorityLevel = currentPriorityLevel;\n  var previousEventStartTime = currentEventStartTime;\n  currentPriorityLevel = priorityLevel;\n  currentEventStartTime = getCurrentTime();\n\n  try {\n    return eventHandler();\n  } finally {\n    currentPriorityLevel = previousPriorityLevel;\n    currentEventStartTime = previousEventStartTime;\n\n    // Before exiting, flush all the immediate work that was scheduled.\n    flushImmediateWork();\n  }\n}\n\nfunction unstable_wrapCallback(callback) {\n  var parentPriorityLevel = currentPriorityLevel;\n  return function () {\n    // This is a fork of runWithPriority, inlined for performance.\n    var previousPriorityLevel = currentPriorityLevel;\n    var previousEventStartTime = currentEventStartTime;\n    currentPriorityLevel = parentPriorityLevel;\n    currentEventStartTime = getCurrentTime();\n\n    try {\n      return callback.apply(this, arguments);\n    } finally {\n      currentPriorityLevel = previousPriorityLevel;\n      currentEventStartTime = previousEventStartTime;\n      flushImmediateWork();\n    }\n  };\n}\n\nfunction unstable_scheduleCallback(callback, deprecated_options) {\n  var startTime = currentEventStartTime !== -1 ? currentEventStartTime : getCurrentTime();\n\n  var expirationTime;\n  if (typeof deprecated_options === 'object' && deprecated_options !== null && typeof deprecated_options.timeout === 'number') {\n    // FIXME: Remove this branch once we lift expiration times out of React.\n    expirationTime = startTime + deprecated_options.timeout;\n  } else {\n    switch (currentPriorityLevel) {\n      case ImmediatePriority:\n        expirationTime = startTime + IMMEDIATE_PRIORITY_TIMEOUT;\n        break;\n      case UserBlockingPriority:\n        expirationTime = startTime + USER_BLOCKING_PRIORITY;\n        break;\n      case IdlePriority:\n        expirationTime = startTime + IDLE_PRIORITY;\n        break;\n      case LowPriority:\n        expirationTime = startTime + LOW_PRIORITY_TIMEOUT;\n        break;\n      case NormalPriority:\n      default:\n        expirationTime = startTime + NORMAL_PRIORITY_TIMEOUT;\n    }\n  }\n\n  var newNode = {\n    callback: callback,\n    priorityLevel: currentPriorityLevel,\n    expirationTime: expirationTime,\n    next: null,\n    previous: null\n  };\n\n  // Insert the new callback into the list, ordered first by expiration, then\n  // by insertion. So the new callback is inserted any other callback with\n  // equal expiration.\n  if (firstCallbackNode === null) {\n    // This is the first callback in the list.\n    firstCallbackNode = newNode.next = newNode.previous = newNode;\n    ensureHostCallbackIsScheduled();\n  } else {\n    var next = null;\n    var node = firstCallbackNode;\n    do {\n      if (node.expirationTime > expirationTime) {\n        // The new callback expires before this one.\n        next = node;\n        break;\n      }\n      node = node.next;\n    } while (node !== firstCallbackNode);\n\n    if (next === null) {\n      // No callback with a later expiration was found, which means the new\n      // callback has the latest expiration in the list.\n      next = firstCallbackNode;\n    } else if (next === firstCallbackNode) {\n      // The new callback has the earliest expiration in the entire list.\n      firstCallbackNode = newNode;\n      ensureHostCallbackIsScheduled();\n    }\n\n    var previous = next.previous;\n    previous.next = next.previous = newNode;\n    newNode.next = next;\n    newNode.previous = previous;\n  }\n\n  return newNode;\n}\n\nfunction unstable_pauseExecution() {\n  isSchedulerPaused = true;\n}\n\nfunction unstable_continueExecution() {\n  isSchedulerPaused = false;\n  if (firstCallbackNode !== null) {\n    ensureHostCallbackIsScheduled();\n  }\n}\n\nfunction unstable_getFirstCallbackNode() {\n  return firstCallbackNode;\n}\n\nfunction unstable_cancelCallback(callbackNode) {\n  var next = callbackNode.next;\n  if (next === null) {\n    // Already cancelled.\n    return;\n  }\n\n  if (next === callbackNode) {\n    // This is the only scheduled callback. Clear the list.\n    firstCallbackNode = null;\n  } else {\n    // Remove the callback from its position in the list.\n    if (callbackNode === firstCallbackNode) {\n      firstCallbackNode = next;\n    }\n    var previous = callbackNode.previous;\n    previous.next = next;\n    next.previous = previous;\n  }\n\n  callbackNode.next = callbackNode.previous = null;\n}\n\nfunction unstable_getCurrentPriorityLevel() {\n  return currentPriorityLevel;\n}\n\nfunction unstable_shouldYield() {\n  return !currentDidTimeout && (firstCallbackNode !== null && firstCallbackNode.expirationTime < currentExpirationTime || shouldYieldToHost());\n}\n\n// The remaining code is essentially a polyfill for requestIdleCallback. It\n// works by scheduling a requestAnimationFrame, storing the time for the start\n// of the frame, then scheduling a postMessage which gets scheduled after paint.\n// Within the postMessage handler do as much work as possible until time + frame\n// rate. By separating the idle call into a separate event tick we ensure that\n// layout, paint and other browser work is counted against the available time.\n// The frame rate is dynamically adjusted.\n\n// We capture a local reference to any global, in case it gets polyfilled after\n// this module is initially evaluated. We want to be using a\n// consistent implementation.\nvar localDate = Date;\n\n// This initialization code may run even on server environments if a component\n// just imports ReactDOM (e.g. for findDOMNode). Some environments might not\n// have setTimeout or clearTimeout. However, we always expect them to be defined\n// on the client. https://github.com/facebook/react/pull/13088\nvar localSetTimeout = typeof setTimeout === 'function' ? setTimeout : undefined;\nvar localClearTimeout = typeof clearTimeout === 'function' ? clearTimeout : undefined;\n\n// We don't expect either of these to necessarily be defined, but we will error\n// later if they are missing on the client.\nvar localRequestAnimationFrame = typeof requestAnimationFrame === 'function' ? requestAnimationFrame : undefined;\nvar localCancelAnimationFrame = typeof cancelAnimationFrame === 'function' ? cancelAnimationFrame : undefined;\n\nvar getCurrentTime;\n\n// requestAnimationFrame does not run when the tab is in the background. If\n// we're backgrounded we prefer for that work to happen so that the page\n// continues to load in the background. So we also schedule a 'setTimeout' as\n// a fallback.\n// TODO: Need a better heuristic for backgrounded work.\nvar ANIMATION_FRAME_TIMEOUT = 100;\nvar rAFID;\nvar rAFTimeoutID;\nvar requestAnimationFrameWithTimeout = function (callback) {\n  // schedule rAF and also a setTimeout\n  rAFID = localRequestAnimationFrame(function (timestamp) {\n    // cancel the setTimeout\n    localClearTimeout(rAFTimeoutID);\n    callback(timestamp);\n  });\n  rAFTimeoutID = localSetTimeout(function () {\n    // cancel the requestAnimationFrame\n    localCancelAnimationFrame(rAFID);\n    callback(getCurrentTime());\n  }, ANIMATION_FRAME_TIMEOUT);\n};\n\nif (hasNativePerformanceNow) {\n  var Performance = performance;\n  getCurrentTime = function () {\n    return Performance.now();\n  };\n} else {\n  getCurrentTime = function () {\n    return localDate.now();\n  };\n}\n\nvar requestHostCallback;\nvar cancelHostCallback;\nvar shouldYieldToHost;\n\nvar globalValue = null;\nif (typeof window !== 'undefined') {\n  globalValue = window;\n} else if (typeof global !== 'undefined') {\n  globalValue = global;\n}\n\nif (globalValue && globalValue._schedMock) {\n  // Dynamic injection, only for testing purposes.\n  var globalImpl = globalValue._schedMock;\n  requestHostCallback = globalImpl[0];\n  cancelHostCallback = globalImpl[1];\n  shouldYieldToHost = globalImpl[2];\n  getCurrentTime = globalImpl[3];\n} else if (\n// If Scheduler runs in a non-DOM environment, it falls back to a naive\n// implementation using setTimeout.\ntypeof window === 'undefined' ||\n// Check if MessageChannel is supported, too.\ntypeof MessageChannel !== 'function') {\n  // If this accidentally gets imported in a non-browser environment, e.g. JavaScriptCore,\n  // fallback to a naive implementation.\n  var _callback = null;\n  var _flushCallback = function (didTimeout) {\n    if (_callback !== null) {\n      try {\n        _callback(didTimeout);\n      } finally {\n        _callback = null;\n      }\n    }\n  };\n  requestHostCallback = function (cb, ms) {\n    if (_callback !== null) {\n      // Protect against re-entrancy.\n      setTimeout(requestHostCallback, 0, cb);\n    } else {\n      _callback = cb;\n      setTimeout(_flushCallback, 0, false);\n    }\n  };\n  cancelHostCallback = function () {\n    _callback = null;\n  };\n  shouldYieldToHost = function () {\n    return false;\n  };\n} else {\n  if (typeof console !== 'undefined') {\n    // TODO: Remove fb.me link\n    if (typeof localRequestAnimationFrame !== 'function') {\n      console.error(\"This browser doesn't support requestAnimationFrame. \" + 'Make sure that you load a ' + 'polyfill in older browsers. https://fb.me/react-polyfills');\n    }\n    if (typeof localCancelAnimationFrame !== 'function') {\n      console.error(\"This browser doesn't support cancelAnimationFrame. \" + 'Make sure that you load a ' + 'polyfill in older browsers. https://fb.me/react-polyfills');\n    }\n  }\n\n  var scheduledHostCallback = null;\n  var isMessageEventScheduled = false;\n  var timeoutTime = -1;\n\n  var isAnimationFrameScheduled = false;\n\n  var isFlushingHostCallback = false;\n\n  var frameDeadline = 0;\n  // We start out assuming that we run at 30fps but then the heuristic tracking\n  // will adjust this value to a faster fps if we get more frequent animation\n  // frames.\n  var previousFrameTime = 33;\n  var activeFrameTime = 33;\n\n  shouldYieldToHost = function () {\n    return frameDeadline <= getCurrentTime();\n  };\n\n  // We use the postMessage trick to defer idle work until after the repaint.\n  var channel = new MessageChannel();\n  var port = channel.port2;\n  channel.port1.onmessage = function (event) {\n    isMessageEventScheduled = false;\n\n    var prevScheduledCallback = scheduledHostCallback;\n    var prevTimeoutTime = timeoutTime;\n    scheduledHostCallback = null;\n    timeoutTime = -1;\n\n    var currentTime = getCurrentTime();\n\n    var didTimeout = false;\n    if (frameDeadline - currentTime <= 0) {\n      // There's no time left in this idle period. Check if the callback has\n      // a timeout and whether it's been exceeded.\n      if (prevTimeoutTime !== -1 && prevTimeoutTime <= currentTime) {\n        // Exceeded the timeout. Invoke the callback even though there's no\n        // time left.\n        didTimeout = true;\n      } else {\n        // No timeout.\n        if (!isAnimationFrameScheduled) {\n          // Schedule another animation callback so we retry later.\n          isAnimationFrameScheduled = true;\n          requestAnimationFrameWithTimeout(animationTick);\n        }\n        // Exit without invoking the callback.\n        scheduledHostCallback = prevScheduledCallback;\n        timeoutTime = prevTimeoutTime;\n        return;\n      }\n    }\n\n    if (prevScheduledCallback !== null) {\n      isFlushingHostCallback = true;\n      try {\n        prevScheduledCallback(didTimeout);\n      } finally {\n        isFlushingHostCallback = false;\n      }\n    }\n  };\n\n  var animationTick = function (rafTime) {\n    if (scheduledHostCallback !== null) {\n      // Eagerly schedule the next animation callback at the beginning of the\n      // frame. If the scheduler queue is not empty at the end of the frame, it\n      // will continue flushing inside that callback. If the queue *is* empty,\n      // then it will exit immediately. Posting the callback at the start of the\n      // frame ensures it's fired within the earliest possible frame. If we\n      // waited until the end of the frame to post the callback, we risk the\n      // browser skipping a frame and not firing the callback until the frame\n      // after that.\n      requestAnimationFrameWithTimeout(animationTick);\n    } else {\n      // No pending work. Exit.\n      isAnimationFrameScheduled = false;\n      return;\n    }\n\n    var nextFrameTime = rafTime - frameDeadline + activeFrameTime;\n    if (nextFrameTime < activeFrameTime && previousFrameTime < activeFrameTime) {\n      if (nextFrameTime < 8) {\n        // Defensive coding. We don't support higher frame rates than 120hz.\n        // If the calculated frame time gets lower than 8, it is probably a bug.\n        nextFrameTime = 8;\n      }\n      // If one frame goes long, then the next one can be short to catch up.\n      // If two frames are short in a row, then that's an indication that we\n      // actually have a higher frame rate than what we're currently optimizing.\n      // We adjust our heuristic dynamically accordingly. For example, if we're\n      // running on 120hz display or 90hz VR display.\n      // Take the max of the two in case one of them was an anomaly due to\n      // missed frame deadlines.\n      activeFrameTime = nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime;\n    } else {\n      previousFrameTime = nextFrameTime;\n    }\n    frameDeadline = rafTime + activeFrameTime;\n    if (!isMessageEventScheduled) {\n      isMessageEventScheduled = true;\n      port.postMessage(undefined);\n    }\n  };\n\n  requestHostCallback = function (callback, absoluteTimeout) {\n    scheduledHostCallback = callback;\n    timeoutTime = absoluteTimeout;\n    if (isFlushingHostCallback || absoluteTimeout < 0) {\n      // Don't wait for the next frame. Continue working ASAP, in a new event.\n      port.postMessage(undefined);\n    } else if (!isAnimationFrameScheduled) {\n      // If rAF didn't already schedule one, we need to schedule a frame.\n      // TODO: If this rAF doesn't materialize because the browser throttles, we\n      // might want to still have setTimeout trigger rIC as a backup to ensure\n      // that we keep performing work.\n      isAnimationFrameScheduled = true;\n      requestAnimationFrameWithTimeout(animationTick);\n    }\n  };\n\n  cancelHostCallback = function () {\n    scheduledHostCallback = null;\n    isMessageEventScheduled = false;\n    timeoutTime = -1;\n  };\n}\n\n// Helps identify side effects in begin-phase lifecycle hooks and setState reducers:\n\n\n// In some cases, StrictMode should also double-render lifecycles.\n// This can be confusing for tests though,\n// And it can be bad for performance in production.\n// This feature flag can be used to control the behavior:\n\n\n// To preserve the \"Pause on caught exceptions\" behavior of the debugger, we\n// replay the begin phase of a failed component inside invokeGuardedCallback.\n\n\n// Warn about deprecated, async-unsafe lifecycles; relates to RFC #6:\n\n\n// Gather advanced timing metrics for Profiler subtrees.\n\n\n// Trace which interactions trigger each commit.\nvar enableSchedulerTracing = true;\n\n// Only used in www builds.\n // TODO: true? Here it might just be false.\n\n// Only used in www builds.\n\n\n// Only used in www builds.\n\n\n// React Fire: prevent the value and checked attributes from syncing\n// with their related DOM properties\n\n\n// These APIs will no longer be \"unstable\" in the upcoming 16.7 release,\n// Control this behavior with a flag to support 16.6 minor releases in the meanwhile.\nvar enableStableConcurrentModeAPIs = false;\n\nvar DEFAULT_THREAD_ID = 0;\n\n// Counters used to generate unique IDs.\nvar interactionIDCounter = 0;\nvar threadIDCounter = 0;\n\n// Set of currently traced interactions.\n// Interactions \"stack\"–\n// Meaning that newly traced interactions are appended to the previously active set.\n// When an interaction goes out of scope, the previous set (if any) is restored.\nvar interactionsRef = null;\n\n// Listener(s) to notify when interactions begin and end.\nvar subscriberRef = null;\n\nif (enableSchedulerTracing) {\n  interactionsRef = {\n    current: new Set()\n  };\n  subscriberRef = {\n    current: null\n  };\n}\n\nfunction unstable_clear(callback) {\n  if (!enableSchedulerTracing) {\n    return callback();\n  }\n\n  var prevInteractions = interactionsRef.current;\n  interactionsRef.current = new Set();\n\n  try {\n    return callback();\n  } finally {\n    interactionsRef.current = prevInteractions;\n  }\n}\n\nfunction unstable_getCurrent() {\n  if (!enableSchedulerTracing) {\n    return null;\n  } else {\n    return interactionsRef.current;\n  }\n}\n\nfunction unstable_getThreadID() {\n  return ++threadIDCounter;\n}\n\nfunction unstable_trace(name, timestamp, callback) {\n  var threadID = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : DEFAULT_THREAD_ID;\n\n  if (!enableSchedulerTracing) {\n    return callback();\n  }\n\n  var interaction = {\n    __count: 1,\n    id: interactionIDCounter++,\n    name: name,\n    timestamp: timestamp\n  };\n\n  var prevInteractions = interactionsRef.current;\n\n  // Traced interactions should stack/accumulate.\n  // To do that, clone the current interactions.\n  // The previous set will be restored upon completion.\n  var interactions = new Set(prevInteractions);\n  interactions.add(interaction);\n  interactionsRef.current = interactions;\n\n  var subscriber = subscriberRef.current;\n  var returnValue = void 0;\n\n  try {\n    if (subscriber !== null) {\n      subscriber.onInteractionTraced(interaction);\n    }\n  } finally {\n    try {\n      if (subscriber !== null) {\n        subscriber.onWorkStarted(interactions, threadID);\n      }\n    } finally {\n      try {\n        returnValue = callback();\n      } finally {\n        interactionsRef.current = prevInteractions;\n\n        try {\n          if (subscriber !== null) {\n            subscriber.onWorkStopped(interactions, threadID);\n          }\n        } finally {\n          interaction.__count--;\n\n          // If no async work was scheduled for this interaction,\n          // Notify subscribers that it's completed.\n          if (subscriber !== null && interaction.__count === 0) {\n            subscriber.onInteractionScheduledWorkCompleted(interaction);\n          }\n        }\n      }\n    }\n  }\n\n  return returnValue;\n}\n\nfunction unstable_wrap(callback) {\n  var threadID = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : DEFAULT_THREAD_ID;\n\n  if (!enableSchedulerTracing) {\n    return callback;\n  }\n\n  var wrappedInteractions = interactionsRef.current;\n\n  var subscriber = subscriberRef.current;\n  if (subscriber !== null) {\n    subscriber.onWorkScheduled(wrappedInteractions, threadID);\n  }\n\n  // Update the pending async work count for the current interactions.\n  // Update after calling subscribers in case of error.\n  wrappedInteractions.forEach(function (interaction) {\n    interaction.__count++;\n  });\n\n  var hasRun = false;\n\n  function wrapped() {\n    var prevInteractions = interactionsRef.current;\n    interactionsRef.current = wrappedInteractions;\n\n    subscriber = subscriberRef.current;\n\n    try {\n      var returnValue = void 0;\n\n      try {\n        if (subscriber !== null) {\n          subscriber.onWorkStarted(wrappedInteractions, threadID);\n        }\n      } finally {\n        try {\n          returnValue = callback.apply(undefined, arguments);\n        } finally {\n          interactionsRef.current = prevInteractions;\n\n          if (subscriber !== null) {\n            subscriber.onWorkStopped(wrappedInteractions, threadID);\n          }\n        }\n      }\n\n      return returnValue;\n    } finally {\n      if (!hasRun) {\n        // We only expect a wrapped function to be executed once,\n        // But in the event that it's executed more than once–\n        // Only decrement the outstanding interaction counts once.\n        hasRun = true;\n\n        // Update pending async counts for all wrapped interactions.\n        // If this was the last scheduled async work for any of them,\n        // Mark them as completed.\n        wrappedInteractions.forEach(function (interaction) {\n          interaction.__count--;\n\n          if (subscriber !== null && interaction.__count === 0) {\n            subscriber.onInteractionScheduledWorkCompleted(interaction);\n          }\n        });\n      }\n    }\n  }\n\n  wrapped.cancel = function cancel() {\n    subscriber = subscriberRef.current;\n\n    try {\n      if (subscriber !== null) {\n        subscriber.onWorkCanceled(wrappedInteractions, threadID);\n      }\n    } finally {\n      // Update pending async counts for all wrapped interactions.\n      // If this was the last scheduled async work for any of them,\n      // Mark them as completed.\n      wrappedInteractions.forEach(function (interaction) {\n        interaction.__count--;\n\n        if (subscriber && interaction.__count === 0) {\n          subscriber.onInteractionScheduledWorkCompleted(interaction);\n        }\n      });\n    }\n  };\n\n  return wrapped;\n}\n\nvar subscribers = null;\nif (enableSchedulerTracing) {\n  subscribers = new Set();\n}\n\nfunction unstable_subscribe(subscriber) {\n  if (enableSchedulerTracing) {\n    subscribers.add(subscriber);\n\n    if (subscribers.size === 1) {\n      subscriberRef.current = {\n        onInteractionScheduledWorkCompleted: onInteractionScheduledWorkCompleted,\n        onInteractionTraced: onInteractionTraced,\n        onWorkCanceled: onWorkCanceled,\n        onWorkScheduled: onWorkScheduled,\n        onWorkStarted: onWorkStarted,\n        onWorkStopped: onWorkStopped\n      };\n    }\n  }\n}\n\nfunction unstable_unsubscribe(subscriber) {\n  if (enableSchedulerTracing) {\n    subscribers.delete(subscriber);\n\n    if (subscribers.size === 0) {\n      subscriberRef.current = null;\n    }\n  }\n}\n\nfunction onInteractionTraced(interaction) {\n  var didCatchError = false;\n  var caughtError = null;\n\n  subscribers.forEach(function (subscriber) {\n    try {\n      subscriber.onInteractionTraced(interaction);\n    } catch (error) {\n      if (!didCatchError) {\n        didCatchError = true;\n        caughtError = error;\n      }\n    }\n  });\n\n  if (didCatchError) {\n    throw caughtError;\n  }\n}\n\nfunction onInteractionScheduledWorkCompleted(interaction) {\n  var didCatchError = false;\n  var caughtError = null;\n\n  subscribers.forEach(function (subscriber) {\n    try {\n      subscriber.onInteractionScheduledWorkCompleted(interaction);\n    } catch (error) {\n      if (!didCatchError) {\n        didCatchError = true;\n        caughtError = error;\n      }\n    }\n  });\n\n  if (didCatchError) {\n    throw caughtError;\n  }\n}\n\nfunction onWorkScheduled(interactions, threadID) {\n  var didCatchError = false;\n  var caughtError = null;\n\n  subscribers.forEach(function (subscriber) {\n    try {\n      subscriber.onWorkScheduled(interactions, threadID);\n    } catch (error) {\n      if (!didCatchError) {\n        didCatchError = true;\n        caughtError = error;\n      }\n    }\n  });\n\n  if (didCatchError) {\n    throw caughtError;\n  }\n}\n\nfunction onWorkStarted(interactions, threadID) {\n  var didCatchError = false;\n  var caughtError = null;\n\n  subscribers.forEach(function (subscriber) {\n    try {\n      subscriber.onWorkStarted(interactions, threadID);\n    } catch (error) {\n      if (!didCatchError) {\n        didCatchError = true;\n        caughtError = error;\n      }\n    }\n  });\n\n  if (didCatchError) {\n    throw caughtError;\n  }\n}\n\nfunction onWorkStopped(interactions, threadID) {\n  var didCatchError = false;\n  var caughtError = null;\n\n  subscribers.forEach(function (subscriber) {\n    try {\n      subscriber.onWorkStopped(interactions, threadID);\n    } catch (error) {\n      if (!didCatchError) {\n        didCatchError = true;\n        caughtError = error;\n      }\n    }\n  });\n\n  if (didCatchError) {\n    throw caughtError;\n  }\n}\n\nfunction onWorkCanceled(interactions, threadID) {\n  var didCatchError = false;\n  var caughtError = null;\n\n  subscribers.forEach(function (subscriber) {\n    try {\n      subscriber.onWorkCanceled(interactions, threadID);\n    } catch (error) {\n      if (!didCatchError) {\n        didCatchError = true;\n        caughtError = error;\n      }\n    }\n  });\n\n  if (didCatchError) {\n    throw caughtError;\n  }\n}\n\n/**\n * Keeps track of the current dispatcher.\n */\nvar ReactCurrentDispatcher = {\n  /**\n   * @internal\n   * @type {ReactComponent}\n   */\n  current: null\n};\n\n/**\n * Keeps track of the current owner.\n *\n * The current owner is the component who should own any components that are\n * currently being constructed.\n */\nvar ReactCurrentOwner = {\n  /**\n   * @internal\n   * @type {ReactComponent}\n   */\n  current: null\n};\n\nvar BEFORE_SLASH_RE = /^(.*)[\\\\\\/]/;\n\nvar describeComponentFrame = function (name, source, ownerName) {\n  var sourceInfo = '';\n  if (source) {\n    var path = source.fileName;\n    var fileName = path.replace(BEFORE_SLASH_RE, '');\n    {\n      // In DEV, include code for a common special case:\n      // prefer \"folder/index.js\" instead of just \"index.js\".\n      if (/^index\\./.test(fileName)) {\n        var match = path.match(BEFORE_SLASH_RE);\n        if (match) {\n          var pathBeforeSlash = match[1];\n          if (pathBeforeSlash) {\n            var folderName = pathBeforeSlash.replace(BEFORE_SLASH_RE, '');\n            fileName = folderName + '/' + fileName;\n          }\n        }\n      }\n    }\n    sourceInfo = ' (at ' + fileName + ':' + source.lineNumber + ')';\n  } else if (ownerName) {\n    sourceInfo = ' (created by ' + ownerName + ')';\n  }\n  return '\\n    in ' + (name || 'Unknown') + sourceInfo;\n};\n\nvar Resolved = 1;\n\n\nfunction refineResolvedLazyComponent(lazyComponent) {\n  return lazyComponent._status === Resolved ? lazyComponent._result : null;\n}\n\nfunction getWrappedName(outerType, innerType, wrapperName) {\n  var functionName = innerType.displayName || innerType.name || '';\n  return outerType.displayName || (functionName !== '' ? wrapperName + '(' + functionName + ')' : wrapperName);\n}\n\nfunction getComponentName(type) {\n  if (type == null) {\n    // Host root, text node or just invalid type.\n    return null;\n  }\n  {\n    if (typeof type.tag === 'number') {\n      warningWithoutStack$1(false, 'Received an unexpected object in getComponentName(). ' + 'This is likely a bug in React. Please file an issue.');\n    }\n  }\n  if (typeof type === 'function') {\n    return type.displayName || type.name || null;\n  }\n  if (typeof type === 'string') {\n    return type;\n  }\n  switch (type) {\n    case REACT_CONCURRENT_MODE_TYPE:\n      return 'ConcurrentMode';\n    case REACT_FRAGMENT_TYPE:\n      return 'Fragment';\n    case REACT_PORTAL_TYPE:\n      return 'Portal';\n    case REACT_PROFILER_TYPE:\n      return 'Profiler';\n    case REACT_STRICT_MODE_TYPE:\n      return 'StrictMode';\n    case REACT_SUSPENSE_TYPE:\n      return 'Suspense';\n  }\n  if (typeof type === 'object') {\n    switch (type.$$typeof) {\n      case REACT_CONTEXT_TYPE:\n        return 'Context.Consumer';\n      case REACT_PROVIDER_TYPE:\n        return 'Context.Provider';\n      case REACT_FORWARD_REF_TYPE:\n        return getWrappedName(type, type.render, 'ForwardRef');\n      case REACT_MEMO_TYPE:\n        return getComponentName(type.type);\n      case REACT_LAZY_TYPE:\n        {\n          var thenable = type;\n          var resolvedThenable = refineResolvedLazyComponent(thenable);\n          if (resolvedThenable) {\n            return getComponentName(resolvedThenable);\n          }\n        }\n    }\n  }\n  return null;\n}\n\nvar ReactDebugCurrentFrame = {};\n\nvar currentlyValidatingElement = null;\n\nfunction setCurrentlyValidatingElement(element) {\n  {\n    currentlyValidatingElement = element;\n  }\n}\n\n{\n  // Stack implementation injected by the current renderer.\n  ReactDebugCurrentFrame.getCurrentStack = null;\n\n  ReactDebugCurrentFrame.getStackAddendum = function () {\n    var stack = '';\n\n    // Add an extra top frame while an element is being validated\n    if (currentlyValidatingElement) {\n      var name = getComponentName(currentlyValidatingElement.type);\n      var owner = currentlyValidatingElement._owner;\n      stack += describeComponentFrame(name, currentlyValidatingElement._source, owner && getComponentName(owner.type));\n    }\n\n    // Delegate to the injected renderer-specific implementation\n    var impl = ReactDebugCurrentFrame.getCurrentStack;\n    if (impl) {\n      stack += impl() || '';\n    }\n\n    return stack;\n  };\n}\n\nvar ReactSharedInternals = {\n  ReactCurrentDispatcher: ReactCurrentDispatcher,\n  ReactCurrentOwner: ReactCurrentOwner,\n  // Used by renderers to avoid bundling object-assign twice in UMD bundles:\n  assign: objectAssign\n};\n\n{\n  // Re-export the schedule API(s) for UMD bundles.\n  // This avoids introducing a dependency on a new UMD global in a minor update,\n  // Since that would be a breaking change (e.g. for all existing CodeSandboxes).\n  // This re-export is only required for UMD bundles;\n  // CJS bundles use the shared NPM package.\n  objectAssign(ReactSharedInternals, {\n    Scheduler: {\n      unstable_cancelCallback: unstable_cancelCallback,\n      unstable_shouldYield: unstable_shouldYield,\n      unstable_now: getCurrentTime,\n      unstable_scheduleCallback: unstable_scheduleCallback,\n      unstable_runWithPriority: unstable_runWithPriority,\n      unstable_next: unstable_next,\n      unstable_wrapCallback: unstable_wrapCallback,\n      unstable_getFirstCallbackNode: unstable_getFirstCallbackNode,\n      unstable_pauseExecution: unstable_pauseExecution,\n      unstable_continueExecution: unstable_continueExecution,\n      unstable_getCurrentPriorityLevel: unstable_getCurrentPriorityLevel,\n      unstable_IdlePriority: IdlePriority,\n      unstable_ImmediatePriority: ImmediatePriority,\n      unstable_LowPriority: LowPriority,\n      unstable_NormalPriority: NormalPriority,\n      unstable_UserBlockingPriority: UserBlockingPriority\n    },\n    SchedulerTracing: {\n      __interactionsRef: interactionsRef,\n      __subscriberRef: subscriberRef,\n      unstable_clear: unstable_clear,\n      unstable_getCurrent: unstable_getCurrent,\n      unstable_getThreadID: unstable_getThreadID,\n      unstable_subscribe: unstable_subscribe,\n      unstable_trace: unstable_trace,\n      unstable_unsubscribe: unstable_unsubscribe,\n      unstable_wrap: unstable_wrap\n    }\n  });\n}\n\n{\n  objectAssign(ReactSharedInternals, {\n    // These should not be included in production.\n    ReactDebugCurrentFrame: ReactDebugCurrentFrame,\n    // Shim for React DOM 16.0.0 which still destructured (but not used) this.\n    // TODO: remove in React 17.0.\n    ReactComponentTreeHook: {}\n  });\n}\n\n/**\n * Similar to invariant but only logs a warning if the condition is not met.\n * This can be used to log issues in development environments in critical\n * paths. Removing the logging code for production environments will keep the\n * same logic and follow the same code paths.\n */\n\nvar warning = warningWithoutStack$1;\n\n{\n  warning = function (condition, format) {\n    if (condition) {\n      return;\n    }\n    var ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame;\n    var stack = ReactDebugCurrentFrame.getStackAddendum();\n    // eslint-disable-next-line react-internal/warning-and-invariant-args\n\n    for (var _len = arguments.length, args = Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {\n      args[_key - 2] = arguments[_key];\n    }\n\n    warningWithoutStack$1.apply(undefined, [false, format + '%s'].concat(args, [stack]));\n  };\n}\n\nvar warning$1 = warning;\n\nvar hasOwnProperty$1 = Object.prototype.hasOwnProperty;\n\nvar RESERVED_PROPS = {\n  key: true,\n  ref: true,\n  __self: true,\n  __source: true\n};\n\nvar specialPropKeyWarningShown = void 0;\nvar specialPropRefWarningShown = void 0;\n\nfunction hasValidRef(config) {\n  {\n    if (hasOwnProperty$1.call(config, 'ref')) {\n      var getter = Object.getOwnPropertyDescriptor(config, 'ref').get;\n      if (getter && getter.isReactWarning) {\n        return false;\n      }\n    }\n  }\n  return config.ref !== undefined;\n}\n\nfunction hasValidKey(config) {\n  {\n    if (hasOwnProperty$1.call(config, 'key')) {\n      var getter = Object.getOwnPropertyDescriptor(config, 'key').get;\n      if (getter && getter.isReactWarning) {\n        return false;\n      }\n    }\n  }\n  return config.key !== undefined;\n}\n\nfunction defineKeyPropWarningGetter(props, displayName) {\n  var warnAboutAccessingKey = function () {\n    if (!specialPropKeyWarningShown) {\n      specialPropKeyWarningShown = true;\n      warningWithoutStack$1(false, '%s: `key` is not a prop. Trying to access it will result ' + 'in `undefined` being returned. If you need to access the same ' + 'value within the child component, you should pass it as a different ' + 'prop. (https://fb.me/react-special-props)', displayName);\n    }\n  };\n  warnAboutAccessingKey.isReactWarning = true;\n  Object.defineProperty(props, 'key', {\n    get: warnAboutAccessingKey,\n    configurable: true\n  });\n}\n\nfunction defineRefPropWarningGetter(props, displayName) {\n  var warnAboutAccessingRef = function () {\n    if (!specialPropRefWarningShown) {\n      specialPropRefWarningShown = true;\n      warningWithoutStack$1(false, '%s: `ref` is not a prop. Trying to access it will result ' + 'in `undefined` being returned. If you need to access the same ' + 'value within the child component, you should pass it as a different ' + 'prop. (https://fb.me/react-special-props)', displayName);\n    }\n  };\n  warnAboutAccessingRef.isReactWarning = true;\n  Object.defineProperty(props, 'ref', {\n    get: warnAboutAccessingRef,\n    configurable: true\n  });\n}\n\n/**\n * Factory method to create a new React element. This no longer adheres to\n * the class pattern, so do not use new to call it. Also, no instanceof check\n * will work. Instead test $$typeof field against Symbol.for('react.element') to check\n * if something is a React Element.\n *\n * @param {*} type\n * @param {*} key\n * @param {string|object} ref\n * @param {*} self A *temporary* helper to detect places where `this` is\n * different from the `owner` when React.createElement is called, so that we\n * can warn. We want to get rid of owner and replace string `ref`s with arrow\n * functions, and as long as `this` and owner are the same, there will be no\n * change in behavior.\n * @param {*} source An annotation object (added by a transpiler or otherwise)\n * indicating filename, line number, and/or other information.\n * @param {*} owner\n * @param {*} props\n * @internal\n */\nvar ReactElement = function (type, key, ref, self, source, owner, props) {\n  var element = {\n    // This tag allows us to uniquely identify this as a React Element\n    $$typeof: REACT_ELEMENT_TYPE,\n\n    // Built-in properties that belong on the element\n    type: type,\n    key: key,\n    ref: ref,\n    props: props,\n\n    // Record the component responsible for creating this element.\n    _owner: owner\n  };\n\n  {\n    // The validation flag is currently mutative. We put it on\n    // an external backing store so that we can freeze the whole object.\n    // This can be replaced with a WeakMap once they are implemented in\n    // commonly used development environments.\n    element._store = {};\n\n    // To make comparing ReactElements easier for testing purposes, we make\n    // the validation flag non-enumerable (where possible, which should\n    // include every environment we run tests in), so the test framework\n    // ignores it.\n    Object.defineProperty(element._store, 'validated', {\n      configurable: false,\n      enumerable: false,\n      writable: true,\n      value: false\n    });\n    // self and source are DEV only properties.\n    Object.defineProperty(element, '_self', {\n      configurable: false,\n      enumerable: false,\n      writable: false,\n      value: self\n    });\n    // Two elements created in two different places should be considered\n    // equal for testing purposes and therefore we hide it from enumeration.\n    Object.defineProperty(element, '_source', {\n      configurable: false,\n      enumerable: false,\n      writable: false,\n      value: source\n    });\n    if (Object.freeze) {\n      Object.freeze(element.props);\n      Object.freeze(element);\n    }\n  }\n\n  return element;\n};\n\n/**\n * Create and return a new ReactElement of the given type.\n * See https://reactjs.org/docs/react-api.html#createelement\n */\nfunction createElement(type, config, children) {\n  var propName = void 0;\n\n  // Reserved names are extracted\n  var props = {};\n\n  var key = null;\n  var ref = null;\n  var self = null;\n  var source = null;\n\n  if (config != null) {\n    if (hasValidRef(config)) {\n      ref = config.ref;\n    }\n    if (hasValidKey(config)) {\n      key = '' + config.key;\n    }\n\n    self = config.__self === undefined ? null : config.__self;\n    source = config.__source === undefined ? null : config.__source;\n    // Remaining properties are added to a new props object\n    for (propName in config) {\n      if (hasOwnProperty$1.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {\n        props[propName] = config[propName];\n      }\n    }\n  }\n\n  // Children can be more than one argument, and those are transferred onto\n  // the newly allocated props object.\n  var childrenLength = arguments.length - 2;\n  if (childrenLength === 1) {\n    props.children = children;\n  } else if (childrenLength > 1) {\n    var childArray = Array(childrenLength);\n    for (var i = 0; i < childrenLength; i++) {\n      childArray[i] = arguments[i + 2];\n    }\n    {\n      if (Object.freeze) {\n        Object.freeze(childArray);\n      }\n    }\n    props.children = childArray;\n  }\n\n  // Resolve default props\n  if (type && type.defaultProps) {\n    var defaultProps = type.defaultProps;\n    for (propName in defaultProps) {\n      if (props[propName] === undefined) {\n        props[propName] = defaultProps[propName];\n      }\n    }\n  }\n  {\n    if (key || ref) {\n      var displayName = typeof type === 'function' ? type.displayName || type.name || 'Unknown' : type;\n      if (key) {\n        defineKeyPropWarningGetter(props, displayName);\n      }\n      if (ref) {\n        defineRefPropWarningGetter(props, displayName);\n      }\n    }\n  }\n  return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props);\n}\n\n/**\n * Return a function that produces ReactElements of a given type.\n * See https://reactjs.org/docs/react-api.html#createfactory\n */\n\n\nfunction cloneAndReplaceKey(oldElement, newKey) {\n  var newElement = ReactElement(oldElement.type, newKey, oldElement.ref, oldElement._self, oldElement._source, oldElement._owner, oldElement.props);\n\n  return newElement;\n}\n\n/**\n * Clone and return a new ReactElement using element as the starting point.\n * See https://reactjs.org/docs/react-api.html#cloneelement\n */\nfunction cloneElement(element, config, children) {\n  !!(element === null || element === undefined) ? invariant(false, 'React.cloneElement(...): The argument must be a React element, but you passed %s.', element) : void 0;\n\n  var propName = void 0;\n\n  // Original props are copied\n  var props = objectAssign({}, element.props);\n\n  // Reserved names are extracted\n  var key = element.key;\n  var ref = element.ref;\n  // Self is preserved since the owner is preserved.\n  var self = element._self;\n  // Source is preserved since cloneElement is unlikely to be targeted by a\n  // transpiler, and the original source is probably a better indicator of the\n  // true owner.\n  var source = element._source;\n\n  // Owner will be preserved, unless ref is overridden\n  var owner = element._owner;\n\n  if (config != null) {\n    if (hasValidRef(config)) {\n      // Silently steal the ref from the parent.\n      ref = config.ref;\n      owner = ReactCurrentOwner.current;\n    }\n    if (hasValidKey(config)) {\n      key = '' + config.key;\n    }\n\n    // Remaining properties override existing props\n    var defaultProps = void 0;\n    if (element.type && element.type.defaultProps) {\n      defaultProps = element.type.defaultProps;\n    }\n    for (propName in config) {\n      if (hasOwnProperty$1.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {\n        if (config[propName] === undefined && defaultProps !== undefined) {\n          // Resolve default props\n          props[propName] = defaultProps[propName];\n        } else {\n          props[propName] = config[propName];\n        }\n      }\n    }\n  }\n\n  // Children can be more than one argument, and those are transferred onto\n  // the newly allocated props object.\n  var childrenLength = arguments.length - 2;\n  if (childrenLength === 1) {\n    props.children = children;\n  } else if (childrenLength > 1) {\n    var childArray = Array(childrenLength);\n    for (var i = 0; i < childrenLength; i++) {\n      childArray[i] = arguments[i + 2];\n    }\n    props.children = childArray;\n  }\n\n  return ReactElement(element.type, key, ref, self, source, owner, props);\n}\n\n/**\n * Verifies the object is a ReactElement.\n * See https://reactjs.org/docs/react-api.html#isvalidelement\n * @param {?object} object\n * @return {boolean} True if `object` is a ReactElement.\n * @final\n */\nfunction isValidElement(object) {\n  return typeof object === 'object' && object !== null && object.$$typeof === REACT_ELEMENT_TYPE;\n}\n\nvar SEPARATOR = '.';\nvar SUBSEPARATOR = ':';\n\n/**\n * Escape and wrap key so it is safe to use as a reactid\n *\n * @param {string} key to be escaped.\n * @return {string} the escaped key.\n */\nfunction escape(key) {\n  var escapeRegex = /[=:]/g;\n  var escaperLookup = {\n    '=': '=0',\n    ':': '=2'\n  };\n  var escapedString = ('' + key).replace(escapeRegex, function (match) {\n    return escaperLookup[match];\n  });\n\n  return '$' + escapedString;\n}\n\n/**\n * TODO: Test that a single child and an array with one item have the same key\n * pattern.\n */\n\nvar didWarnAboutMaps = false;\n\nvar userProvidedKeyEscapeRegex = /\\/+/g;\nfunction escapeUserProvidedKey(text) {\n  return ('' + text).replace(userProvidedKeyEscapeRegex, '$&/');\n}\n\nvar POOL_SIZE = 10;\nvar traverseContextPool = [];\nfunction getPooledTraverseContext(mapResult, keyPrefix, mapFunction, mapContext) {\n  if (traverseContextPool.length) {\n    var traverseContext = traverseContextPool.pop();\n    traverseContext.result = mapResult;\n    traverseContext.keyPrefix = keyPrefix;\n    traverseContext.func = mapFunction;\n    traverseContext.context = mapContext;\n    traverseContext.count = 0;\n    return traverseContext;\n  } else {\n    return {\n      result: mapResult,\n      keyPrefix: keyPrefix,\n      func: mapFunction,\n      context: mapContext,\n      count: 0\n    };\n  }\n}\n\nfunction releaseTraverseContext(traverseContext) {\n  traverseContext.result = null;\n  traverseContext.keyPrefix = null;\n  traverseContext.func = null;\n  traverseContext.context = null;\n  traverseContext.count = 0;\n  if (traverseContextPool.length < POOL_SIZE) {\n    traverseContextPool.push(traverseContext);\n  }\n}\n\n/**\n * @param {?*} children Children tree container.\n * @param {!string} nameSoFar Name of the key path so far.\n * @param {!function} callback Callback to invoke with each child found.\n * @param {?*} traverseContext Used to pass information throughout the traversal\n * process.\n * @return {!number} The number of children in this subtree.\n */\nfunction traverseAllChildrenImpl(children, nameSoFar, callback, traverseContext) {\n  var type = typeof children;\n\n  if (type === 'undefined' || type === 'boolean') {\n    // All of the above are perceived as null.\n    children = null;\n  }\n\n  var invokeCallback = false;\n\n  if (children === null) {\n    invokeCallback = true;\n  } else {\n    switch (type) {\n      case 'string':\n      case 'number':\n        invokeCallback = true;\n        break;\n      case 'object':\n        switch (children.$$typeof) {\n          case REACT_ELEMENT_TYPE:\n          case REACT_PORTAL_TYPE:\n            invokeCallback = true;\n        }\n    }\n  }\n\n  if (invokeCallback) {\n    callback(traverseContext, children,\n    // If it's the only child, treat the name as if it was wrapped in an array\n    // so that it's consistent if the number of children grows.\n    nameSoFar === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar);\n    return 1;\n  }\n\n  var child = void 0;\n  var nextName = void 0;\n  var subtreeCount = 0; // Count of children found in the current subtree.\n  var nextNamePrefix = nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR;\n\n  if (Array.isArray(children)) {\n    for (var i = 0; i < children.length; i++) {\n      child = children[i];\n      nextName = nextNamePrefix + getComponentKey(child, i);\n      subtreeCount += traverseAllChildrenImpl(child, nextName, callback, traverseContext);\n    }\n  } else {\n    var iteratorFn = getIteratorFn(children);\n    if (typeof iteratorFn === 'function') {\n      {\n        // Warn about using Maps as children\n        if (iteratorFn === children.entries) {\n          !didWarnAboutMaps ? warning$1(false, 'Using Maps as children is unsupported and will likely yield ' + 'unexpected results. Convert it to a sequence/iterable of keyed ' + 'ReactElements instead.') : void 0;\n          didWarnAboutMaps = true;\n        }\n      }\n\n      var iterator = iteratorFn.call(children);\n      var step = void 0;\n      var ii = 0;\n      while (!(step = iterator.next()).done) {\n        child = step.value;\n        nextName = nextNamePrefix + getComponentKey(child, ii++);\n        subtreeCount += traverseAllChildrenImpl(child, nextName, callback, traverseContext);\n      }\n    } else if (type === 'object') {\n      var addendum = '';\n      {\n        addendum = ' If you meant to render a collection of children, use an array ' + 'instead.' + ReactDebugCurrentFrame.getStackAddendum();\n      }\n      var childrenString = '' + children;\n      invariant(false, 'Objects are not valid as a React child (found: %s).%s', childrenString === '[object Object]' ? 'object with keys {' + Object.keys(children).join(', ') + '}' : childrenString, addendum);\n    }\n  }\n\n  return subtreeCount;\n}\n\n/**\n * Traverses children that are typically specified as `props.children`, but\n * might also be specified through attributes:\n *\n * - `traverseAllChildren(this.props.children, ...)`\n * - `traverseAllChildren(this.props.leftPanelChildren, ...)`\n *\n * The `traverseContext` is an optional argument that is passed through the\n * entire traversal. It can be used to store accumulations or anything else that\n * the callback might find relevant.\n *\n * @param {?*} children Children tree object.\n * @param {!function} callback To invoke upon traversing each child.\n * @param {?*} traverseContext Context for traversal.\n * @return {!number} The number of children in this subtree.\n */\nfunction traverseAllChildren(children, callback, traverseContext) {\n  if (children == null) {\n    return 0;\n  }\n\n  return traverseAllChildrenImpl(children, '', callback, traverseContext);\n}\n\n/**\n * Generate a key string that identifies a component within a set.\n *\n * @param {*} component A component that could contain a manual key.\n * @param {number} index Index that is used if a manual key is not provided.\n * @return {string}\n */\nfunction getComponentKey(component, index) {\n  // Do some typechecking here since we call this blindly. We want to ensure\n  // that we don't block potential future ES APIs.\n  if (typeof component === 'object' && component !== null && component.key != null) {\n    // Explicit key\n    return escape(component.key);\n  }\n  // Implicit key determined by the index in the set\n  return index.toString(36);\n}\n\nfunction forEachSingleChild(bookKeeping, child, name) {\n  var func = bookKeeping.func,\n      context = bookKeeping.context;\n\n  func.call(context, child, bookKeeping.count++);\n}\n\n/**\n * Iterates through children that are typically specified as `props.children`.\n *\n * See https://reactjs.org/docs/react-api.html#reactchildrenforeach\n *\n * The provided forEachFunc(child, index) will be called for each\n * leaf child.\n *\n * @param {?*} children Children tree container.\n * @param {function(*, int)} forEachFunc\n * @param {*} forEachContext Context for forEachContext.\n */\nfunction forEachChildren(children, forEachFunc, forEachContext) {\n  if (children == null) {\n    return children;\n  }\n  var traverseContext = getPooledTraverseContext(null, null, forEachFunc, forEachContext);\n  traverseAllChildren(children, forEachSingleChild, traverseContext);\n  releaseTraverseContext(traverseContext);\n}\n\nfunction mapSingleChildIntoContext(bookKeeping, child, childKey) {\n  var result = bookKeeping.result,\n      keyPrefix = bookKeeping.keyPrefix,\n      func = bookKeeping.func,\n      context = bookKeeping.context;\n\n\n  var mappedChild = func.call(context, child, bookKeeping.count++);\n  if (Array.isArray(mappedChild)) {\n    mapIntoWithKeyPrefixInternal(mappedChild, result, childKey, function (c) {\n      return c;\n    });\n  } else if (mappedChild != null) {\n    if (isValidElement(mappedChild)) {\n      mappedChild = cloneAndReplaceKey(mappedChild,\n      // Keep both the (mapped) and old keys if they differ, just as\n      // traverseAllChildren used to do for objects as children\n      keyPrefix + (mappedChild.key && (!child || child.key !== mappedChild.key) ? escapeUserProvidedKey(mappedChild.key) + '/' : '') + childKey);\n    }\n    result.push(mappedChild);\n  }\n}\n\nfunction mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) {\n  var escapedPrefix = '';\n  if (prefix != null) {\n    escapedPrefix = escapeUserProvidedKey(prefix) + '/';\n  }\n  var traverseContext = getPooledTraverseContext(array, escapedPrefix, func, context);\n  traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);\n  releaseTraverseContext(traverseContext);\n}\n\n/**\n * Maps children that are typically specified as `props.children`.\n *\n * See https://reactjs.org/docs/react-api.html#reactchildrenmap\n *\n * The provided mapFunction(child, key, index) will be called for each\n * leaf child.\n *\n * @param {?*} children Children tree container.\n * @param {function(*, int)} func The map function.\n * @param {*} context Context for mapFunction.\n * @return {object} Object containing the ordered map of results.\n */\nfunction mapChildren(children, func, context) {\n  if (children == null) {\n    return children;\n  }\n  var result = [];\n  mapIntoWithKeyPrefixInternal(children, result, null, func, context);\n  return result;\n}\n\n/**\n * Count the number of children that are typically specified as\n * `props.children`.\n *\n * See https://reactjs.org/docs/react-api.html#reactchildrencount\n *\n * @param {?*} children Children tree container.\n * @return {number} The number of children.\n */\nfunction countChildren(children) {\n  return traverseAllChildren(children, function () {\n    return null;\n  }, null);\n}\n\n/**\n * Flatten a children object (typically specified as `props.children`) and\n * return an array with appropriately re-keyed children.\n *\n * See https://reactjs.org/docs/react-api.html#reactchildrentoarray\n */\nfunction toArray(children) {\n  var result = [];\n  mapIntoWithKeyPrefixInternal(children, result, null, function (child) {\n    return child;\n  });\n  return result;\n}\n\n/**\n * Returns the first child in a collection of children and verifies that there\n * is only one child in the collection.\n *\n * See https://reactjs.org/docs/react-api.html#reactchildrenonly\n *\n * The current implementation of this function assumes that a single child gets\n * passed without a wrapper, but the purpose of this helper function is to\n * abstract away the particular structure of children.\n *\n * @param {?object} children Child collection structure.\n * @return {ReactElement} The first and only `ReactElement` contained in the\n * structure.\n */\nfunction onlyChild(children) {\n  !isValidElement(children) ? invariant(false, 'React.Children.only expected to receive a single React element child.') : void 0;\n  return children;\n}\n\nfunction createContext(defaultValue, calculateChangedBits) {\n  if (calculateChangedBits === undefined) {\n    calculateChangedBits = null;\n  } else {\n    {\n      !(calculateChangedBits === null || typeof calculateChangedBits === 'function') ? warningWithoutStack$1(false, 'createContext: Expected the optional second argument to be a ' + 'function. Instead received: %s', calculateChangedBits) : void 0;\n    }\n  }\n\n  var context = {\n    $$typeof: REACT_CONTEXT_TYPE,\n    _calculateChangedBits: calculateChangedBits,\n    // As a workaround to support multiple concurrent renderers, we categorize\n    // some renderers as primary and others as secondary. We only expect\n    // there to be two concurrent renderers at most: React Native (primary) and\n    // Fabric (secondary); React DOM (primary) and React ART (secondary).\n    // Secondary renderers store their context values on separate fields.\n    _currentValue: defaultValue,\n    _currentValue2: defaultValue,\n    // Used to track how many concurrent renderers this context currently\n    // supports within in a single renderer. Such as parallel server rendering.\n    _threadCount: 0,\n    // These are circular\n    Provider: null,\n    Consumer: null\n  };\n\n  context.Provider = {\n    $$typeof: REACT_PROVIDER_TYPE,\n    _context: context\n  };\n\n  var hasWarnedAboutUsingNestedContextConsumers = false;\n  var hasWarnedAboutUsingConsumerProvider = false;\n\n  {\n    // A separate object, but proxies back to the original context object for\n    // backwards compatibility. It has a different $$typeof, so we can properly\n    // warn for the incorrect usage of Context as a Consumer.\n    var Consumer = {\n      $$typeof: REACT_CONTEXT_TYPE,\n      _context: context,\n      _calculateChangedBits: context._calculateChangedBits\n    };\n    // $FlowFixMe: Flow complains about not setting a value, which is intentional here\n    Object.defineProperties(Consumer, {\n      Provider: {\n        get: function () {\n          if (!hasWarnedAboutUsingConsumerProvider) {\n            hasWarnedAboutUsingConsumerProvider = true;\n            warning$1(false, 'Rendering <Context.Consumer.Provider> is not supported and will be removed in ' + 'a future major release. Did you mean to render <Context.Provider> instead?');\n          }\n          return context.Provider;\n        },\n        set: function (_Provider) {\n          context.Provider = _Provider;\n        }\n      },\n      _currentValue: {\n        get: function () {\n          return context._currentValue;\n        },\n        set: function (_currentValue) {\n          context._currentValue = _currentValue;\n        }\n      },\n      _currentValue2: {\n        get: function () {\n          return context._currentValue2;\n        },\n        set: function (_currentValue2) {\n          context._currentValue2 = _currentValue2;\n        }\n      },\n      _threadCount: {\n        get: function () {\n          return context._threadCount;\n        },\n        set: function (_threadCount) {\n          context._threadCount = _threadCount;\n        }\n      },\n      Consumer: {\n        get: function () {\n          if (!hasWarnedAboutUsingNestedContextConsumers) {\n            hasWarnedAboutUsingNestedContextConsumers = true;\n            warning$1(false, 'Rendering <Context.Consumer.Consumer> is not supported and will be removed in ' + 'a future major release. Did you mean to render <Context.Consumer> instead?');\n          }\n          return context.Consumer;\n        }\n      }\n    });\n    // $FlowFixMe: Flow complains about missing properties because it doesn't understand defineProperty\n    context.Consumer = Consumer;\n  }\n\n  {\n    context._currentRenderer = null;\n    context._currentRenderer2 = null;\n  }\n\n  return context;\n}\n\nfunction lazy(ctor) {\n  var lazyType = {\n    $$typeof: REACT_LAZY_TYPE,\n    _ctor: ctor,\n    // React uses these fields to store the result.\n    _status: -1,\n    _result: null\n  };\n\n  {\n    // In production, this would just set it on the object.\n    var defaultProps = void 0;\n    var propTypes = void 0;\n    Object.defineProperties(lazyType, {\n      defaultProps: {\n        configurable: true,\n        get: function () {\n          return defaultProps;\n        },\n        set: function (newDefaultProps) {\n          warning$1(false, 'React.lazy(...): It is not supported to assign `defaultProps` to ' + 'a lazy component import. Either specify them where the component ' + 'is defined, or create a wrapping component around it.');\n          defaultProps = newDefaultProps;\n          // Match production behavior more closely:\n          Object.defineProperty(lazyType, 'defaultProps', {\n            enumerable: true\n          });\n        }\n      },\n      propTypes: {\n        configurable: true,\n        get: function () {\n          return propTypes;\n        },\n        set: function (newPropTypes) {\n          warning$1(false, 'React.lazy(...): It is not supported to assign `propTypes` to ' + 'a lazy component import. Either specify them where the component ' + 'is defined, or create a wrapping component around it.');\n          propTypes = newPropTypes;\n          // Match production behavior more closely:\n          Object.defineProperty(lazyType, 'propTypes', {\n            enumerable: true\n          });\n        }\n      }\n    });\n  }\n\n  return lazyType;\n}\n\nfunction forwardRef(render) {\n  {\n    if (render != null && render.$$typeof === REACT_MEMO_TYPE) {\n      warningWithoutStack$1(false, 'forwardRef requires a render function but received a `memo` ' + 'component. Instead of forwardRef(memo(...)), use ' + 'memo(forwardRef(...)).');\n    } else if (typeof render !== 'function') {\n      warningWithoutStack$1(false, 'forwardRef requires a render function but was given %s.', render === null ? 'null' : typeof render);\n    } else {\n      !(\n      // Do not warn for 0 arguments because it could be due to usage of the 'arguments' object\n      render.length === 0 || render.length === 2) ? warningWithoutStack$1(false, 'forwardRef render functions accept exactly two parameters: props and ref. %s', render.length === 1 ? 'Did you forget to use the ref parameter?' : 'Any additional parameter will be undefined.') : void 0;\n    }\n\n    if (render != null) {\n      !(render.defaultProps == null && render.propTypes == null) ? warningWithoutStack$1(false, 'forwardRef render functions do not support propTypes or defaultProps. ' + 'Did you accidentally pass a React component?') : void 0;\n    }\n  }\n\n  return {\n    $$typeof: REACT_FORWARD_REF_TYPE,\n    render: render\n  };\n}\n\nfunction isValidElementType(type) {\n  return typeof type === 'string' || typeof type === 'function' ||\n  // Note: its typeof might be other than 'symbol' or 'number' if it's a polyfill.\n  type === REACT_FRAGMENT_TYPE || type === REACT_CONCURRENT_MODE_TYPE || type === REACT_PROFILER_TYPE || type === REACT_STRICT_MODE_TYPE || type === REACT_SUSPENSE_TYPE || typeof type === 'object' && type !== null && (type.$$typeof === REACT_LAZY_TYPE || type.$$typeof === REACT_MEMO_TYPE || type.$$typeof === REACT_PROVIDER_TYPE || type.$$typeof === REACT_CONTEXT_TYPE || type.$$typeof === REACT_FORWARD_REF_TYPE);\n}\n\nfunction memo(type, compare) {\n  {\n    if (!isValidElementType(type)) {\n      warningWithoutStack$1(false, 'memo: The first argument must be a component. Instead ' + 'received: %s', type === null ? 'null' : typeof type);\n    }\n  }\n  return {\n    $$typeof: REACT_MEMO_TYPE,\n    type: type,\n    compare: compare === undefined ? null : compare\n  };\n}\n\nfunction resolveDispatcher() {\n  var dispatcher = ReactCurrentDispatcher.current;\n  !(dispatcher !== null) ? invariant(false, 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:\\n1. You might have mismatching versions of React and the renderer (such as React DOM)\\n2. You might be breaking the Rules of Hooks\\n3. You might have more than one copy of React in the same app\\nSee https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.') : void 0;\n  return dispatcher;\n}\n\nfunction useContext(Context, unstable_observedBits) {\n  var dispatcher = resolveDispatcher();\n  {\n    !(unstable_observedBits === undefined) ? warning$1(false, 'useContext() second argument is reserved for future ' + 'use in React. Passing it is not supported. ' + 'You passed: %s.%s', unstable_observedBits, typeof unstable_observedBits === 'number' && Array.isArray(arguments[2]) ? '\\n\\nDid you call array.map(useContext)? ' + 'Calling Hooks inside a loop is not supported. ' + 'Learn more at https://fb.me/rules-of-hooks' : '') : void 0;\n\n    // TODO: add a more generic warning for invalid values.\n    if (Context._context !== undefined) {\n      var realContext = Context._context;\n      // Don't deduplicate because this legitimately causes bugs\n      // and nobody should be using this in existing code.\n      if (realContext.Consumer === Context) {\n        warning$1(false, 'Calling useContext(Context.Consumer) is not supported, may cause bugs, and will be ' + 'removed in a future major release. Did you mean to call useContext(Context) instead?');\n      } else if (realContext.Provider === Context) {\n        warning$1(false, 'Calling useContext(Context.Provider) is not supported. ' + 'Did you mean to call useContext(Context) instead?');\n      }\n    }\n  }\n  return dispatcher.useContext(Context, unstable_observedBits);\n}\n\nfunction useState(initialState) {\n  var dispatcher = resolveDispatcher();\n  return dispatcher.useState(initialState);\n}\n\nfunction useReducer(reducer, initialArg, init) {\n  var dispatcher = resolveDispatcher();\n  return dispatcher.useReducer(reducer, initialArg, init);\n}\n\nfunction useRef(initialValue) {\n  var dispatcher = resolveDispatcher();\n  return dispatcher.useRef(initialValue);\n}\n\nfunction useEffect(create, inputs) {\n  var dispatcher = resolveDispatcher();\n  return dispatcher.useEffect(create, inputs);\n}\n\nfunction useLayoutEffect(create, inputs) {\n  var dispatcher = resolveDispatcher();\n  return dispatcher.useLayoutEffect(create, inputs);\n}\n\nfunction useCallback(callback, inputs) {\n  var dispatcher = resolveDispatcher();\n  return dispatcher.useCallback(callback, inputs);\n}\n\nfunction useMemo(create, inputs) {\n  var dispatcher = resolveDispatcher();\n  return dispatcher.useMemo(create, inputs);\n}\n\nfunction useImperativeHandle(ref, create, inputs) {\n  var dispatcher = resolveDispatcher();\n  return dispatcher.useImperativeHandle(ref, create, inputs);\n}\n\nfunction useDebugValue(value, formatterFn) {\n  {\n    var dispatcher = resolveDispatcher();\n    return dispatcher.useDebugValue(value, formatterFn);\n  }\n}\n\n/**\n * Copyright (c) 2013-present, Facebook, Inc.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n\n\nvar ReactPropTypesSecret$1 = 'SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED';\n\nvar ReactPropTypesSecret_1 = ReactPropTypesSecret$1;\n\n/**\n * Copyright (c) 2013-present, Facebook, Inc.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n\n\nvar printWarning$1 = function() {};\n\n{\n  var ReactPropTypesSecret = ReactPropTypesSecret_1;\n  var loggedTypeFailures = {};\n\n  printWarning$1 = function(text) {\n    var message = 'Warning: ' + text;\n    if (typeof console !== 'undefined') {\n      console.error(message);\n    }\n    try {\n      // --- Welcome to debugging React ---\n      // This error was thrown as a convenience so that you can use this stack\n      // to find the callsite that caused this warning to fire.\n      throw new Error(message);\n    } catch (x) {}\n  };\n}\n\n/**\n * Assert that the values match with the type specs.\n * Error messages are memorized and will only be shown once.\n *\n * @param {object} typeSpecs Map of name to a ReactPropType\n * @param {object} values Runtime values that need to be type-checked\n * @param {string} location e.g. \"prop\", \"context\", \"child context\"\n * @param {string} componentName Name of the component for error messages.\n * @param {?Function} getStack Returns the component stack.\n * @private\n */\nfunction checkPropTypes(typeSpecs, values, location, componentName, getStack) {\n  {\n    for (var typeSpecName in typeSpecs) {\n      if (typeSpecs.hasOwnProperty(typeSpecName)) {\n        var error;\n        // Prop type validation may throw. In case they do, we don't want to\n        // fail the render phase where it didn't fail before. So we log it.\n        // After these have been cleaned up, we'll let them throw.\n        try {\n          // This is intentionally an invariant that gets caught. It's the same\n          // behavior as without this statement except with a better message.\n          if (typeof typeSpecs[typeSpecName] !== 'function') {\n            var err = Error(\n              (componentName || 'React class') + ': ' + location + ' type `' + typeSpecName + '` is invalid; ' +\n              'it must be a function, usually from the `prop-types` package, but received `' + typeof typeSpecs[typeSpecName] + '`.'\n            );\n            err.name = 'Invariant Violation';\n            throw err;\n          }\n          error = typeSpecs[typeSpecName](values, typeSpecName, componentName, location, null, ReactPropTypesSecret);\n        } catch (ex) {\n          error = ex;\n        }\n        if (error && !(error instanceof Error)) {\n          printWarning$1(\n            (componentName || 'React class') + ': type specification of ' +\n            location + ' `' + typeSpecName + '` is invalid; the type checker ' +\n            'function must return `null` or an `Error` but returned a ' + typeof error + '. ' +\n            'You may have forgotten to pass an argument to the type checker ' +\n            'creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and ' +\n            'shape all require an argument).'\n          );\n\n        }\n        if (error instanceof Error && !(error.message in loggedTypeFailures)) {\n          // Only monitor this failure once because there tends to be a lot of the\n          // same error.\n          loggedTypeFailures[error.message] = true;\n\n          var stack = getStack ? getStack() : '';\n\n          printWarning$1(\n            'Failed ' + location + ' type: ' + error.message + (stack != null ? stack : '')\n          );\n        }\n      }\n    }\n  }\n}\n\nvar checkPropTypes_1 = checkPropTypes;\n\n/**\n * ReactElementValidator provides a wrapper around a element factory\n * which validates the props passed to the element. This is intended to be\n * used only in DEV and could be replaced by a static type checker for languages\n * that support it.\n */\n\nvar propTypesMisspellWarningShown = void 0;\n\n{\n  propTypesMisspellWarningShown = false;\n}\n\nfunction getDeclarationErrorAddendum() {\n  if (ReactCurrentOwner.current) {\n    var name = getComponentName(ReactCurrentOwner.current.type);\n    if (name) {\n      return '\\n\\nCheck the render method of `' + name + '`.';\n    }\n  }\n  return '';\n}\n\nfunction getSourceInfoErrorAddendum(elementProps) {\n  if (elementProps !== null && elementProps !== undefined && elementProps.__source !== undefined) {\n    var source = elementProps.__source;\n    var fileName = source.fileName.replace(/^.*[\\\\\\/]/, '');\n    var lineNumber = source.lineNumber;\n    return '\\n\\nCheck your code at ' + fileName + ':' + lineNumber + '.';\n  }\n  return '';\n}\n\n/**\n * Warn if there's no key explicitly set on dynamic arrays of children or\n * object keys are not valid. This allows us to keep track of children between\n * updates.\n */\nvar ownerHasKeyUseWarning = {};\n\nfunction getCurrentComponentErrorInfo(parentType) {\n  var info = getDeclarationErrorAddendum();\n\n  if (!info) {\n    var parentName = typeof parentType === 'string' ? parentType : parentType.displayName || parentType.name;\n    if (parentName) {\n      info = '\\n\\nCheck the top-level render call using <' + parentName + '>.';\n    }\n  }\n  return info;\n}\n\n/**\n * Warn if the element doesn't have an explicit key assigned to it.\n * This element is in an array. The array could grow and shrink or be\n * reordered. All children that haven't already been validated are required to\n * have a \"key\" property assigned to it. Error statuses are cached so a warning\n * will only be shown once.\n *\n * @internal\n * @param {ReactElement} element Element that requires a key.\n * @param {*} parentType element's parent's type.\n */\nfunction validateExplicitKey(element, parentType) {\n  if (!element._store || element._store.validated || element.key != null) {\n    return;\n  }\n  element._store.validated = true;\n\n  var currentComponentErrorInfo = getCurrentComponentErrorInfo(parentType);\n  if (ownerHasKeyUseWarning[currentComponentErrorInfo]) {\n    return;\n  }\n  ownerHasKeyUseWarning[currentComponentErrorInfo] = true;\n\n  // Usually the current owner is the offender, but if it accepts children as a\n  // property, it may be the creator of the child that's responsible for\n  // assigning it a key.\n  var childOwner = '';\n  if (element && element._owner && element._owner !== ReactCurrentOwner.current) {\n    // Give the component that originally created this child.\n    childOwner = ' It was passed a child from ' + getComponentName(element._owner.type) + '.';\n  }\n\n  setCurrentlyValidatingElement(element);\n  {\n    warning$1(false, 'Each child in a list should have a unique \"key\" prop.' + '%s%s See https://fb.me/react-warning-keys for more information.', currentComponentErrorInfo, childOwner);\n  }\n  setCurrentlyValidatingElement(null);\n}\n\n/**\n * Ensure that every element either is passed in a static location, in an\n * array with an explicit keys property defined, or in an object literal\n * with valid key property.\n *\n * @internal\n * @param {ReactNode} node Statically passed child of any type.\n * @param {*} parentType node's parent's type.\n */\nfunction validateChildKeys(node, parentType) {\n  if (typeof node !== 'object') {\n    return;\n  }\n  if (Array.isArray(node)) {\n    for (var i = 0; i < node.length; i++) {\n      var child = node[i];\n      if (isValidElement(child)) {\n        validateExplicitKey(child, parentType);\n      }\n    }\n  } else if (isValidElement(node)) {\n    // This element was passed in a valid location.\n    if (node._store) {\n      node._store.validated = true;\n    }\n  } else if (node) {\n    var iteratorFn = getIteratorFn(node);\n    if (typeof iteratorFn === 'function') {\n      // Entry iterators used to provide implicit keys,\n      // but now we print a separate warning for them later.\n      if (iteratorFn !== node.entries) {\n        var iterator = iteratorFn.call(node);\n        var step = void 0;\n        while (!(step = iterator.next()).done) {\n          if (isValidElement(step.value)) {\n            validateExplicitKey(step.value, parentType);\n          }\n        }\n      }\n    }\n  }\n}\n\n/**\n * Given an element, validate that its props follow the propTypes definition,\n * provided by the type.\n *\n * @param {ReactElement} element\n */\nfunction validatePropTypes(element) {\n  var type = element.type;\n  if (type === null || type === undefined || typeof type === 'string') {\n    return;\n  }\n  var name = getComponentName(type);\n  var propTypes = void 0;\n  if (typeof type === 'function') {\n    propTypes = type.propTypes;\n  } else if (typeof type === 'object' && (type.$$typeof === REACT_FORWARD_REF_TYPE ||\n  // Note: Memo only checks outer props here.\n  // Inner props are checked in the reconciler.\n  type.$$typeof === REACT_MEMO_TYPE)) {\n    propTypes = type.propTypes;\n  } else {\n    return;\n  }\n  if (propTypes) {\n    setCurrentlyValidatingElement(element);\n    checkPropTypes_1(propTypes, element.props, 'prop', name, ReactDebugCurrentFrame.getStackAddendum);\n    setCurrentlyValidatingElement(null);\n  } else if (type.PropTypes !== undefined && !propTypesMisspellWarningShown) {\n    propTypesMisspellWarningShown = true;\n    warningWithoutStack$1(false, 'Component %s declared `PropTypes` instead of `propTypes`. Did you misspell the property assignment?', name || 'Unknown');\n  }\n  if (typeof type.getDefaultProps === 'function') {\n    !type.getDefaultProps.isReactClassApproved ? warningWithoutStack$1(false, 'getDefaultProps is only used on classic React.createClass ' + 'definitions. Use a static property named `defaultProps` instead.') : void 0;\n  }\n}\n\n/**\n * Given a fragment, validate that it can only be provided with fragment props\n * @param {ReactElement} fragment\n */\nfunction validateFragmentProps(fragment) {\n  setCurrentlyValidatingElement(fragment);\n\n  var keys = Object.keys(fragment.props);\n  for (var i = 0; i < keys.length; i++) {\n    var key = keys[i];\n    if (key !== 'children' && key !== 'key') {\n      warning$1(false, 'Invalid prop `%s` supplied to `React.Fragment`. ' + 'React.Fragment can only have `key` and `children` props.', key);\n      break;\n    }\n  }\n\n  if (fragment.ref !== null) {\n    warning$1(false, 'Invalid attribute `ref` supplied to `React.Fragment`.');\n  }\n\n  setCurrentlyValidatingElement(null);\n}\n\nfunction createElementWithValidation(type, props, children) {\n  var validType = isValidElementType(type);\n\n  // We warn in this case but don't throw. We expect the element creation to\n  // succeed and there will likely be errors in render.\n  if (!validType) {\n    var info = '';\n    if (type === undefined || typeof type === 'object' && type !== null && Object.keys(type).length === 0) {\n      info += ' You likely forgot to export your component from the file ' + \"it's defined in, or you might have mixed up default and named imports.\";\n    }\n\n    var sourceInfo = getSourceInfoErrorAddendum(props);\n    if (sourceInfo) {\n      info += sourceInfo;\n    } else {\n      info += getDeclarationErrorAddendum();\n    }\n\n    var typeString = void 0;\n    if (type === null) {\n      typeString = 'null';\n    } else if (Array.isArray(type)) {\n      typeString = 'array';\n    } else if (type !== undefined && type.$$typeof === REACT_ELEMENT_TYPE) {\n      typeString = '<' + (getComponentName(type.type) || 'Unknown') + ' />';\n      info = ' Did you accidentally export a JSX literal instead of a component?';\n    } else {\n      typeString = typeof type;\n    }\n\n    warning$1(false, 'React.createElement: type is invalid -- expected a string (for ' + 'built-in components) or a class/function (for composite ' + 'components) but got: %s.%s', typeString, info);\n  }\n\n  var element = createElement.apply(this, arguments);\n\n  // The result can be nullish if a mock or a custom function is used.\n  // TODO: Drop this when these are no longer allowed as the type argument.\n  if (element == null) {\n    return element;\n  }\n\n  // Skip key warning if the type isn't valid since our key validation logic\n  // doesn't expect a non-string/function type and can throw confusing errors.\n  // We don't want exception behavior to differ between dev and prod.\n  // (Rendering will throw with a helpful message and as soon as the type is\n  // fixed, the key warnings will appear.)\n  if (validType) {\n    for (var i = 2; i < arguments.length; i++) {\n      validateChildKeys(arguments[i], type);\n    }\n  }\n\n  if (type === REACT_FRAGMENT_TYPE) {\n    validateFragmentProps(element);\n  } else {\n    validatePropTypes(element);\n  }\n\n  return element;\n}\n\nfunction createFactoryWithValidation(type) {\n  var validatedFactory = createElementWithValidation.bind(null, type);\n  validatedFactory.type = type;\n  // Legacy hook: remove it\n  {\n    Object.defineProperty(validatedFactory, 'type', {\n      enumerable: false,\n      get: function () {\n        lowPriorityWarning$1(false, 'Factory.type is deprecated. Access the class directly ' + 'before passing it to createFactory.');\n        Object.defineProperty(this, 'type', {\n          value: type\n        });\n        return type;\n      }\n    });\n  }\n\n  return validatedFactory;\n}\n\nfunction cloneElementWithValidation(element, props, children) {\n  var newElement = cloneElement.apply(this, arguments);\n  for (var i = 2; i < arguments.length; i++) {\n    validateChildKeys(arguments[i], newElement.type);\n  }\n  validatePropTypes(newElement);\n  return newElement;\n}\n\nvar React = {\n  Children: {\n    map: mapChildren,\n    forEach: forEachChildren,\n    count: countChildren,\n    toArray: toArray,\n    only: onlyChild\n  },\n\n  createRef: createRef,\n  Component: Component,\n  PureComponent: PureComponent,\n\n  createContext: createContext,\n  forwardRef: forwardRef,\n  lazy: lazy,\n  memo: memo,\n\n  useCallback: useCallback,\n  useContext: useContext,\n  useEffect: useEffect,\n  useImperativeHandle: useImperativeHandle,\n  useDebugValue: useDebugValue,\n  useLayoutEffect: useLayoutEffect,\n  useMemo: useMemo,\n  useReducer: useReducer,\n  useRef: useRef,\n  useState: useState,\n\n  Fragment: REACT_FRAGMENT_TYPE,\n  StrictMode: REACT_STRICT_MODE_TYPE,\n  Suspense: REACT_SUSPENSE_TYPE,\n\n  createElement: createElementWithValidation,\n  cloneElement: cloneElementWithValidation,\n  createFactory: createFactoryWithValidation,\n  isValidElement: isValidElement,\n\n  version: ReactVersion,\n\n  unstable_ConcurrentMode: REACT_CONCURRENT_MODE_TYPE,\n  unstable_Profiler: REACT_PROFILER_TYPE,\n\n  __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: ReactSharedInternals\n};\n\n// Note: some APIs are added with feature flags.\n// Make sure that stable builds for open source\n// don't modify the React object to avoid deopts.\n// Also let's not expose their names in stable builds.\n\nif (enableStableConcurrentModeAPIs) {\n  React.ConcurrentMode = REACT_CONCURRENT_MODE_TYPE;\n  React.Profiler = REACT_PROFILER_TYPE;\n  React.unstable_ConcurrentMode = undefined;\n  React.unstable_Profiler = undefined;\n}\n\n\n\nvar React$2 = Object.freeze({\n\tdefault: React\n});\n\nvar React$3 = ( React$2 && React ) || React$2;\n\n// TODO: decide on the top-level export form.\n// This is hacky but makes it work with both Rollup and Jest.\nvar react = React$3.default || React$3;\n\nreturn react;\n\n})));\n"
  },
  {
    "path": "vendor/react-dom-dev.js",
    "content": "/** @license React v16.8.6\n * react-dom.development.js\n *\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n'use strict';\n\n(function (global, factory) {\n\ttypeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('react')) :\n\ttypeof define === 'function' && define.amd ? define(['react'], factory) :\n\t(global.ReactDOM = factory(global.React));\n}(this, (function (React) { 'use strict';\n\n/**\n * Use invariant() to assert state which your program assumes to be true.\n *\n * Provide sprintf-style format (only %s is supported) and arguments\n * to provide information about what broke and what you were\n * expecting.\n *\n * The invariant message will be stripped in production, but the invariant\n * will remain to ensure logic does not differ in production.\n */\n\nvar validateFormat = function () {};\n\n{\n  validateFormat = function (format) {\n    if (format === undefined) {\n      throw new Error('invariant requires an error message argument');\n    }\n  };\n}\n\nfunction invariant(condition, format, a, b, c, d, e, f) {\n  validateFormat(format);\n\n  if (!condition) {\n    var error = void 0;\n    if (format === undefined) {\n      error = new Error('Minified exception occurred; use the non-minified dev environment ' + 'for the full error message and additional helpful warnings.');\n    } else {\n      var args = [a, b, c, d, e, f];\n      var argIndex = 0;\n      error = new Error(format.replace(/%s/g, function () {\n        return args[argIndex++];\n      }));\n      error.name = 'Invariant Violation';\n    }\n\n    error.framesToPop = 1; // we don't care about invariant's own frame\n    throw error;\n  }\n}\n\n// Relying on the `invariant()` implementation lets us\n// preserve the format and params in the www builds.\n\n!React ? invariant(false, 'ReactDOM was loaded before React. Make sure you load the React package before loading ReactDOM.') : void 0;\n\nvar invokeGuardedCallbackImpl = function (name, func, context, a, b, c, d, e, f) {\n  var funcArgs = Array.prototype.slice.call(arguments, 3);\n  try {\n    func.apply(context, funcArgs);\n  } catch (error) {\n    this.onError(error);\n  }\n};\n\n{\n  // In DEV mode, we swap out invokeGuardedCallback for a special version\n  // that plays more nicely with the browser's DevTools. The idea is to preserve\n  // \"Pause on exceptions\" behavior. Because React wraps all user-provided\n  // functions in invokeGuardedCallback, and the production version of\n  // invokeGuardedCallback uses a try-catch, all user exceptions are treated\n  // like caught exceptions, and the DevTools won't pause unless the developer\n  // takes the extra step of enabling pause on caught exceptions. This is\n  // unintuitive, though, because even though React has caught the error, from\n  // the developer's perspective, the error is uncaught.\n  //\n  // To preserve the expected \"Pause on exceptions\" behavior, we don't use a\n  // try-catch in DEV. Instead, we synchronously dispatch a fake event to a fake\n  // DOM node, and call the user-provided callback from inside an event handler\n  // for that fake event. If the callback throws, the error is \"captured\" using\n  // a global event handler. But because the error happens in a different\n  // event loop context, it does not interrupt the normal program flow.\n  // Effectively, this gives us try-catch behavior without actually using\n  // try-catch. Neat!\n\n  // Check that the browser supports the APIs we need to implement our special\n  // DEV version of invokeGuardedCallback\n  if (typeof window !== 'undefined' && typeof window.dispatchEvent === 'function' && typeof document !== 'undefined' && typeof document.createEvent === 'function') {\n    var fakeNode = document.createElement('react');\n\n    var invokeGuardedCallbackDev = function (name, func, context, a, b, c, d, e, f) {\n      // If document doesn't exist we know for sure we will crash in this method\n      // when we call document.createEvent(). However this can cause confusing\n      // errors: https://github.com/facebookincubator/create-react-app/issues/3482\n      // So we preemptively throw with a better message instead.\n      !(typeof document !== 'undefined') ? invariant(false, 'The `document` global was defined when React was initialized, but is not defined anymore. This can happen in a test environment if a component schedules an update from an asynchronous callback, but the test has already finished running. To solve this, you can either unmount the component at the end of your test (and ensure that any asynchronous operations get canceled in `componentWillUnmount`), or you can change the test itself to be asynchronous.') : void 0;\n      var evt = document.createEvent('Event');\n\n      // Keeps track of whether the user-provided callback threw an error. We\n      // set this to true at the beginning, then set it to false right after\n      // calling the function. If the function errors, `didError` will never be\n      // set to false. This strategy works even if the browser is flaky and\n      // fails to call our global error handler, because it doesn't rely on\n      // the error event at all.\n      var didError = true;\n\n      // Keeps track of the value of window.event so that we can reset it\n      // during the callback to let user code access window.event in the\n      // browsers that support it.\n      var windowEvent = window.event;\n\n      // Keeps track of the descriptor of window.event to restore it after event\n      // dispatching: https://github.com/facebook/react/issues/13688\n      var windowEventDescriptor = Object.getOwnPropertyDescriptor(window, 'event');\n\n      // Create an event handler for our fake event. We will synchronously\n      // dispatch our fake event using `dispatchEvent`. Inside the handler, we\n      // call the user-provided callback.\n      var funcArgs = Array.prototype.slice.call(arguments, 3);\n      function callCallback() {\n        // We immediately remove the callback from event listeners so that\n        // nested `invokeGuardedCallback` calls do not clash. Otherwise, a\n        // nested call would trigger the fake event handlers of any call higher\n        // in the stack.\n        fakeNode.removeEventListener(evtType, callCallback, false);\n\n        // We check for window.hasOwnProperty('event') to prevent the\n        // window.event assignment in both IE <= 10 as they throw an error\n        // \"Member not found\" in strict mode, and in Firefox which does not\n        // support window.event.\n        if (typeof window.event !== 'undefined' && window.hasOwnProperty('event')) {\n          window.event = windowEvent;\n        }\n\n        func.apply(context, funcArgs);\n        didError = false;\n      }\n\n      // Create a global error event handler. We use this to capture the value\n      // that was thrown. It's possible that this error handler will fire more\n      // than once; for example, if non-React code also calls `dispatchEvent`\n      // and a handler for that event throws. We should be resilient to most of\n      // those cases. Even if our error event handler fires more than once, the\n      // last error event is always used. If the callback actually does error,\n      // we know that the last error event is the correct one, because it's not\n      // possible for anything else to have happened in between our callback\n      // erroring and the code that follows the `dispatchEvent` call below. If\n      // the callback doesn't error, but the error event was fired, we know to\n      // ignore it because `didError` will be false, as described above.\n      var error = void 0;\n      // Use this to track whether the error event is ever called.\n      var didSetError = false;\n      var isCrossOriginError = false;\n\n      function handleWindowError(event) {\n        error = event.error;\n        didSetError = true;\n        if (error === null && event.colno === 0 && event.lineno === 0) {\n          isCrossOriginError = true;\n        }\n        if (event.defaultPrevented) {\n          // Some other error handler has prevented default.\n          // Browsers silence the error report if this happens.\n          // We'll remember this to later decide whether to log it or not.\n          if (error != null && typeof error === 'object') {\n            try {\n              error._suppressLogging = true;\n            } catch (inner) {\n              // Ignore.\n            }\n          }\n        }\n      }\n\n      // Create a fake event type.\n      var evtType = 'react-' + (name ? name : 'invokeguardedcallback');\n\n      // Attach our event handlers\n      window.addEventListener('error', handleWindowError);\n      fakeNode.addEventListener(evtType, callCallback, false);\n\n      // Synchronously dispatch our fake event. If the user-provided function\n      // errors, it will trigger our global error handler.\n      evt.initEvent(evtType, false, false);\n      fakeNode.dispatchEvent(evt);\n\n      if (windowEventDescriptor) {\n        Object.defineProperty(window, 'event', windowEventDescriptor);\n      }\n\n      if (didError) {\n        if (!didSetError) {\n          // The callback errored, but the error event never fired.\n          error = new Error('An error was thrown inside one of your components, but React ' + \"doesn't know what it was. This is likely due to browser \" + 'flakiness. React does its best to preserve the \"Pause on ' + 'exceptions\" behavior of the DevTools, which requires some ' + \"DEV-mode only tricks. It's possible that these don't work in \" + 'your browser. Try triggering the error in production mode, ' + 'or switching to a modern browser. If you suspect that this is ' + 'actually an issue with React, please file an issue.');\n        } else if (isCrossOriginError) {\n          error = new Error(\"A cross-origin error was thrown. React doesn't have access to \" + 'the actual error object in development. ' + 'See https://fb.me/react-crossorigin-error for more information.');\n        }\n        this.onError(error);\n      }\n\n      // Remove our event listeners\n      window.removeEventListener('error', handleWindowError);\n    };\n\n    invokeGuardedCallbackImpl = invokeGuardedCallbackDev;\n  }\n}\n\nvar invokeGuardedCallbackImpl$1 = invokeGuardedCallbackImpl;\n\n// Used by Fiber to simulate a try-catch.\nvar hasError = false;\nvar caughtError = null;\n\n// Used by event system to capture/rethrow the first error.\nvar hasRethrowError = false;\nvar rethrowError = null;\n\nvar reporter = {\n  onError: function (error) {\n    hasError = true;\n    caughtError = error;\n  }\n};\n\n/**\n * Call a function while guarding against errors that happens within it.\n * Returns an error if it throws, otherwise null.\n *\n * In production, this is implemented using a try-catch. The reason we don't\n * use a try-catch directly is so that we can swap out a different\n * implementation in DEV mode.\n *\n * @param {String} name of the guard to use for logging or debugging\n * @param {Function} func The function to invoke\n * @param {*} context The context to use when calling the function\n * @param {...*} args Arguments for function\n */\nfunction invokeGuardedCallback(name, func, context, a, b, c, d, e, f) {\n  hasError = false;\n  caughtError = null;\n  invokeGuardedCallbackImpl$1.apply(reporter, arguments);\n}\n\n/**\n * Same as invokeGuardedCallback, but instead of returning an error, it stores\n * it in a global so it can be rethrown by `rethrowCaughtError` later.\n * TODO: See if caughtError and rethrowError can be unified.\n *\n * @param {String} name of the guard to use for logging or debugging\n * @param {Function} func The function to invoke\n * @param {*} context The context to use when calling the function\n * @param {...*} args Arguments for function\n */\nfunction invokeGuardedCallbackAndCatchFirstError(name, func, context, a, b, c, d, e, f) {\n  invokeGuardedCallback.apply(this, arguments);\n  if (hasError) {\n    var error = clearCaughtError();\n    if (!hasRethrowError) {\n      hasRethrowError = true;\n      rethrowError = error;\n    }\n  }\n}\n\n/**\n * During execution of guarded functions we will capture the first error which\n * we will rethrow to be handled by the top level error handler.\n */\nfunction rethrowCaughtError() {\n  if (hasRethrowError) {\n    var error = rethrowError;\n    hasRethrowError = false;\n    rethrowError = null;\n    throw error;\n  }\n}\n\nfunction hasCaughtError() {\n  return hasError;\n}\n\nfunction clearCaughtError() {\n  if (hasError) {\n    var error = caughtError;\n    hasError = false;\n    caughtError = null;\n    return error;\n  } else {\n    invariant(false, 'clearCaughtError was called but no error was captured. This error is likely caused by a bug in React. Please file an issue.');\n  }\n}\n\n/**\n * Injectable ordering of event plugins.\n */\nvar eventPluginOrder = null;\n\n/**\n * Injectable mapping from names to event plugin modules.\n */\nvar namesToPlugins = {};\n\n/**\n * Recomputes the plugin list using the injected plugins and plugin ordering.\n *\n * @private\n */\nfunction recomputePluginOrdering() {\n  if (!eventPluginOrder) {\n    // Wait until an `eventPluginOrder` is injected.\n    return;\n  }\n  for (var pluginName in namesToPlugins) {\n    var pluginModule = namesToPlugins[pluginName];\n    var pluginIndex = eventPluginOrder.indexOf(pluginName);\n    !(pluginIndex > -1) ? invariant(false, 'EventPluginRegistry: Cannot inject event plugins that do not exist in the plugin ordering, `%s`.', pluginName) : void 0;\n    if (plugins[pluginIndex]) {\n      continue;\n    }\n    !pluginModule.extractEvents ? invariant(false, 'EventPluginRegistry: Event plugins must implement an `extractEvents` method, but `%s` does not.', pluginName) : void 0;\n    plugins[pluginIndex] = pluginModule;\n    var publishedEvents = pluginModule.eventTypes;\n    for (var eventName in publishedEvents) {\n      !publishEventForPlugin(publishedEvents[eventName], pluginModule, eventName) ? invariant(false, 'EventPluginRegistry: Failed to publish event `%s` for plugin `%s`.', eventName, pluginName) : void 0;\n    }\n  }\n}\n\n/**\n * Publishes an event so that it can be dispatched by the supplied plugin.\n *\n * @param {object} dispatchConfig Dispatch configuration for the event.\n * @param {object} PluginModule Plugin publishing the event.\n * @return {boolean} True if the event was successfully published.\n * @private\n */\nfunction publishEventForPlugin(dispatchConfig, pluginModule, eventName) {\n  !!eventNameDispatchConfigs.hasOwnProperty(eventName) ? invariant(false, 'EventPluginHub: More than one plugin attempted to publish the same event name, `%s`.', eventName) : void 0;\n  eventNameDispatchConfigs[eventName] = dispatchConfig;\n\n  var phasedRegistrationNames = dispatchConfig.phasedRegistrationNames;\n  if (phasedRegistrationNames) {\n    for (var phaseName in phasedRegistrationNames) {\n      if (phasedRegistrationNames.hasOwnProperty(phaseName)) {\n        var phasedRegistrationName = phasedRegistrationNames[phaseName];\n        publishRegistrationName(phasedRegistrationName, pluginModule, eventName);\n      }\n    }\n    return true;\n  } else if (dispatchConfig.registrationName) {\n    publishRegistrationName(dispatchConfig.registrationName, pluginModule, eventName);\n    return true;\n  }\n  return false;\n}\n\n/**\n * Publishes a registration name that is used to identify dispatched events.\n *\n * @param {string} registrationName Registration name to add.\n * @param {object} PluginModule Plugin publishing the event.\n * @private\n */\nfunction publishRegistrationName(registrationName, pluginModule, eventName) {\n  !!registrationNameModules[registrationName] ? invariant(false, 'EventPluginHub: More than one plugin attempted to publish the same registration name, `%s`.', registrationName) : void 0;\n  registrationNameModules[registrationName] = pluginModule;\n  registrationNameDependencies[registrationName] = pluginModule.eventTypes[eventName].dependencies;\n\n  {\n    var lowerCasedName = registrationName.toLowerCase();\n    possibleRegistrationNames[lowerCasedName] = registrationName;\n\n    if (registrationName === 'onDoubleClick') {\n      possibleRegistrationNames.ondblclick = registrationName;\n    }\n  }\n}\n\n/**\n * Registers plugins so that they can extract and dispatch events.\n *\n * @see {EventPluginHub}\n */\n\n/**\n * Ordered list of injected plugins.\n */\nvar plugins = [];\n\n/**\n * Mapping from event name to dispatch config\n */\nvar eventNameDispatchConfigs = {};\n\n/**\n * Mapping from registration name to plugin module\n */\nvar registrationNameModules = {};\n\n/**\n * Mapping from registration name to event name\n */\nvar registrationNameDependencies = {};\n\n/**\n * Mapping from lowercase registration names to the properly cased version,\n * used to warn in the case of missing event handlers. Available\n * only in true.\n * @type {Object}\n */\nvar possibleRegistrationNames = {};\n// Trust the developer to only use possibleRegistrationNames in true\n\n/**\n * Injects an ordering of plugins (by plugin name). This allows the ordering\n * to be decoupled from injection of the actual plugins so that ordering is\n * always deterministic regardless of packaging, on-the-fly injection, etc.\n *\n * @param {array} InjectedEventPluginOrder\n * @internal\n * @see {EventPluginHub.injection.injectEventPluginOrder}\n */\nfunction injectEventPluginOrder(injectedEventPluginOrder) {\n  !!eventPluginOrder ? invariant(false, 'EventPluginRegistry: Cannot inject event plugin ordering more than once. You are likely trying to load more than one copy of React.') : void 0;\n  // Clone the ordering so it cannot be dynamically mutated.\n  eventPluginOrder = Array.prototype.slice.call(injectedEventPluginOrder);\n  recomputePluginOrdering();\n}\n\n/**\n * Injects plugins to be used by `EventPluginHub`. The plugin names must be\n * in the ordering injected by `injectEventPluginOrder`.\n *\n * Plugins can be injected as part of page initialization or on-the-fly.\n *\n * @param {object} injectedNamesToPlugins Map from names to plugin modules.\n * @internal\n * @see {EventPluginHub.injection.injectEventPluginsByName}\n */\nfunction injectEventPluginsByName(injectedNamesToPlugins) {\n  var isOrderingDirty = false;\n  for (var pluginName in injectedNamesToPlugins) {\n    if (!injectedNamesToPlugins.hasOwnProperty(pluginName)) {\n      continue;\n    }\n    var pluginModule = injectedNamesToPlugins[pluginName];\n    if (!namesToPlugins.hasOwnProperty(pluginName) || namesToPlugins[pluginName] !== pluginModule) {\n      !!namesToPlugins[pluginName] ? invariant(false, 'EventPluginRegistry: Cannot inject two different event plugins using the same name, `%s`.', pluginName) : void 0;\n      namesToPlugins[pluginName] = pluginModule;\n      isOrderingDirty = true;\n    }\n  }\n  if (isOrderingDirty) {\n    recomputePluginOrdering();\n  }\n}\n\n/**\n * Similar to invariant but only logs a warning if the condition is not met.\n * This can be used to log issues in development environments in critical\n * paths. Removing the logging code for production environments will keep the\n * same logic and follow the same code paths.\n */\n\nvar warningWithoutStack = function () {};\n\n{\n  warningWithoutStack = function (condition, format) {\n    for (var _len = arguments.length, args = Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {\n      args[_key - 2] = arguments[_key];\n    }\n\n    if (format === undefined) {\n      throw new Error('`warningWithoutStack(condition, format, ...args)` requires a warning ' + 'message argument');\n    }\n    if (args.length > 8) {\n      // Check before the condition to catch violations early.\n      throw new Error('warningWithoutStack() currently supports at most 8 arguments.');\n    }\n    if (condition) {\n      return;\n    }\n    if (typeof console !== 'undefined') {\n      var argsWithFormat = args.map(function (item) {\n        return '' + item;\n      });\n      argsWithFormat.unshift('Warning: ' + format);\n\n      // We intentionally don't use spread (or .apply) directly because it\n      // breaks IE9: https://github.com/facebook/react/issues/13610\n      Function.prototype.apply.call(console.error, console, argsWithFormat);\n    }\n    try {\n      // --- Welcome to debugging React ---\n      // This error was thrown as a convenience so that you can use this stack\n      // to find the callsite that caused this warning to fire.\n      var argIndex = 0;\n      var message = 'Warning: ' + format.replace(/%s/g, function () {\n        return args[argIndex++];\n      });\n      throw new Error(message);\n    } catch (x) {}\n  };\n}\n\nvar warningWithoutStack$1 = warningWithoutStack;\n\nvar getFiberCurrentPropsFromNode = null;\nvar getInstanceFromNode = null;\nvar getNodeFromInstance = null;\n\nfunction setComponentTree(getFiberCurrentPropsFromNodeImpl, getInstanceFromNodeImpl, getNodeFromInstanceImpl) {\n  getFiberCurrentPropsFromNode = getFiberCurrentPropsFromNodeImpl;\n  getInstanceFromNode = getInstanceFromNodeImpl;\n  getNodeFromInstance = getNodeFromInstanceImpl;\n  {\n    !(getNodeFromInstance && getInstanceFromNode) ? warningWithoutStack$1(false, 'EventPluginUtils.setComponentTree(...): Injected ' + 'module is missing getNodeFromInstance or getInstanceFromNode.') : void 0;\n  }\n}\n\nvar validateEventDispatches = void 0;\n{\n  validateEventDispatches = function (event) {\n    var dispatchListeners = event._dispatchListeners;\n    var dispatchInstances = event._dispatchInstances;\n\n    var listenersIsArr = Array.isArray(dispatchListeners);\n    var listenersLen = listenersIsArr ? dispatchListeners.length : dispatchListeners ? 1 : 0;\n\n    var instancesIsArr = Array.isArray(dispatchInstances);\n    var instancesLen = instancesIsArr ? dispatchInstances.length : dispatchInstances ? 1 : 0;\n\n    !(instancesIsArr === listenersIsArr && instancesLen === listenersLen) ? warningWithoutStack$1(false, 'EventPluginUtils: Invalid `event`.') : void 0;\n  };\n}\n\n/**\n * Dispatch the event to the listener.\n * @param {SyntheticEvent} event SyntheticEvent to handle\n * @param {function} listener Application-level callback\n * @param {*} inst Internal component instance\n */\nfunction executeDispatch(event, listener, inst) {\n  var type = event.type || 'unknown-event';\n  event.currentTarget = getNodeFromInstance(inst);\n  invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);\n  event.currentTarget = null;\n}\n\n/**\n * Standard/simple iteration through an event's collected dispatches.\n */\nfunction executeDispatchesInOrder(event) {\n  var dispatchListeners = event._dispatchListeners;\n  var dispatchInstances = event._dispatchInstances;\n  {\n    validateEventDispatches(event);\n  }\n  if (Array.isArray(dispatchListeners)) {\n    for (var i = 0; i < dispatchListeners.length; i++) {\n      if (event.isPropagationStopped()) {\n        break;\n      }\n      // Listeners and Instances are two parallel arrays that are always in sync.\n      executeDispatch(event, dispatchListeners[i], dispatchInstances[i]);\n    }\n  } else if (dispatchListeners) {\n    executeDispatch(event, dispatchListeners, dispatchInstances);\n  }\n  event._dispatchListeners = null;\n  event._dispatchInstances = null;\n}\n\n/**\n * @see executeDispatchesInOrderStopAtTrueImpl\n */\n\n\n/**\n * Execution of a \"direct\" dispatch - there must be at most one dispatch\n * accumulated on the event or it is considered an error. It doesn't really make\n * sense for an event with multiple dispatches (bubbled) to keep track of the\n * return values at each dispatch execution, but it does tend to make sense when\n * dealing with \"direct\" dispatches.\n *\n * @return {*} The return value of executing the single dispatch.\n */\n\n\n/**\n * @param {SyntheticEvent} event\n * @return {boolean} True iff number of dispatches accumulated is greater than 0.\n */\n\n/**\n * Accumulates items that must not be null or undefined into the first one. This\n * is used to conserve memory by avoiding array allocations, and thus sacrifices\n * API cleanness. Since `current` can be null before being passed in and not\n * null after this function, make sure to assign it back to `current`:\n *\n * `a = accumulateInto(a, b);`\n *\n * This API should be sparingly used. Try `accumulate` for something cleaner.\n *\n * @return {*|array<*>} An accumulation of items.\n */\n\nfunction accumulateInto(current, next) {\n  !(next != null) ? invariant(false, 'accumulateInto(...): Accumulated items must not be null or undefined.') : void 0;\n\n  if (current == null) {\n    return next;\n  }\n\n  // Both are not empty. Warning: Never call x.concat(y) when you are not\n  // certain that x is an Array (x could be a string with concat method).\n  if (Array.isArray(current)) {\n    if (Array.isArray(next)) {\n      current.push.apply(current, next);\n      return current;\n    }\n    current.push(next);\n    return current;\n  }\n\n  if (Array.isArray(next)) {\n    // A bit too dangerous to mutate `next`.\n    return [current].concat(next);\n  }\n\n  return [current, next];\n}\n\n/**\n * @param {array} arr an \"accumulation\" of items which is either an Array or\n * a single item. Useful when paired with the `accumulate` module. This is a\n * simple utility that allows us to reason about a collection of items, but\n * handling the case when there is exactly one item (and we do not need to\n * allocate an array).\n * @param {function} cb Callback invoked with each element or a collection.\n * @param {?} [scope] Scope used as `this` in a callback.\n */\nfunction forEachAccumulated(arr, cb, scope) {\n  if (Array.isArray(arr)) {\n    arr.forEach(cb, scope);\n  } else if (arr) {\n    cb.call(scope, arr);\n  }\n}\n\n/**\n * Internal queue of events that have accumulated their dispatches and are\n * waiting to have their dispatches executed.\n */\nvar eventQueue = null;\n\n/**\n * Dispatches an event and releases it back into the pool, unless persistent.\n *\n * @param {?object} event Synthetic event to be dispatched.\n * @private\n */\nvar executeDispatchesAndRelease = function (event) {\n  if (event) {\n    executeDispatchesInOrder(event);\n\n    if (!event.isPersistent()) {\n      event.constructor.release(event);\n    }\n  }\n};\nvar executeDispatchesAndReleaseTopLevel = function (e) {\n  return executeDispatchesAndRelease(e);\n};\n\nfunction isInteractive(tag) {\n  return tag === 'button' || tag === 'input' || tag === 'select' || tag === 'textarea';\n}\n\nfunction shouldPreventMouseEvent(name, type, props) {\n  switch (name) {\n    case 'onClick':\n    case 'onClickCapture':\n    case 'onDoubleClick':\n    case 'onDoubleClickCapture':\n    case 'onMouseDown':\n    case 'onMouseDownCapture':\n    case 'onMouseMove':\n    case 'onMouseMoveCapture':\n    case 'onMouseUp':\n    case 'onMouseUpCapture':\n      return !!(props.disabled && isInteractive(type));\n    default:\n      return false;\n  }\n}\n\n/**\n * This is a unified interface for event plugins to be installed and configured.\n *\n * Event plugins can implement the following properties:\n *\n *   `extractEvents` {function(string, DOMEventTarget, string, object): *}\n *     Required. When a top-level event is fired, this method is expected to\n *     extract synthetic events that will in turn be queued and dispatched.\n *\n *   `eventTypes` {object}\n *     Optional, plugins that fire events must publish a mapping of registration\n *     names that are used to register listeners. Values of this mapping must\n *     be objects that contain `registrationName` or `phasedRegistrationNames`.\n *\n *   `executeDispatch` {function(object, function, string)}\n *     Optional, allows plugins to override how an event gets dispatched. By\n *     default, the listener is simply invoked.\n *\n * Each plugin that is injected into `EventsPluginHub` is immediately operable.\n *\n * @public\n */\n\n/**\n * Methods for injecting dependencies.\n */\nvar injection = {\n  /**\n   * @param {array} InjectedEventPluginOrder\n   * @public\n   */\n  injectEventPluginOrder: injectEventPluginOrder,\n\n  /**\n   * @param {object} injectedNamesToPlugins Map from names to plugin modules.\n   */\n  injectEventPluginsByName: injectEventPluginsByName\n};\n\n/**\n * @param {object} inst The instance, which is the source of events.\n * @param {string} registrationName Name of listener (e.g. `onClick`).\n * @return {?function} The stored callback.\n */\nfunction getListener(inst, registrationName) {\n  var listener = void 0;\n\n  // TODO: shouldPreventMouseEvent is DOM-specific and definitely should not\n  // live here; needs to be moved to a better place soon\n  var stateNode = inst.stateNode;\n  if (!stateNode) {\n    // Work in progress (ex: onload events in incremental mode).\n    return null;\n  }\n  var props = getFiberCurrentPropsFromNode(stateNode);\n  if (!props) {\n    // Work in progress.\n    return null;\n  }\n  listener = props[registrationName];\n  if (shouldPreventMouseEvent(registrationName, inst.type, props)) {\n    return null;\n  }\n  !(!listener || typeof listener === 'function') ? invariant(false, 'Expected `%s` listener to be a function, instead got a value of `%s` type.', registrationName, typeof listener) : void 0;\n  return listener;\n}\n\n/**\n * Allows registered plugins an opportunity to extract events from top-level\n * native browser events.\n *\n * @return {*} An accumulation of synthetic events.\n * @internal\n */\nfunction extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget) {\n  var events = null;\n  for (var i = 0; i < plugins.length; i++) {\n    // Not every plugin in the ordering may be loaded at runtime.\n    var possiblePlugin = plugins[i];\n    if (possiblePlugin) {\n      var extractedEvents = possiblePlugin.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);\n      if (extractedEvents) {\n        events = accumulateInto(events, extractedEvents);\n      }\n    }\n  }\n  return events;\n}\n\nfunction runEventsInBatch(events) {\n  if (events !== null) {\n    eventQueue = accumulateInto(eventQueue, events);\n  }\n\n  // Set `eventQueue` to null before processing it so that we can tell if more\n  // events get enqueued while processing.\n  var processingEventQueue = eventQueue;\n  eventQueue = null;\n\n  if (!processingEventQueue) {\n    return;\n  }\n\n  forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);\n  !!eventQueue ? invariant(false, 'processEventQueue(): Additional events were enqueued while processing an event queue. Support for this has not yet been implemented.') : void 0;\n  // This would be a good time to rethrow if any of the event handlers threw.\n  rethrowCaughtError();\n}\n\nfunction runExtractedEventsInBatch(topLevelType, targetInst, nativeEvent, nativeEventTarget) {\n  var events = extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);\n  runEventsInBatch(events);\n}\n\nvar FunctionComponent = 0;\nvar ClassComponent = 1;\nvar IndeterminateComponent = 2; // Before we know whether it is function or class\nvar HostRoot = 3; // Root of a host tree. Could be nested inside another node.\nvar HostPortal = 4; // A subtree. Could be an entry point to a different renderer.\nvar HostComponent = 5;\nvar HostText = 6;\nvar Fragment = 7;\nvar Mode = 8;\nvar ContextConsumer = 9;\nvar ContextProvider = 10;\nvar ForwardRef = 11;\nvar Profiler = 12;\nvar SuspenseComponent = 13;\nvar MemoComponent = 14;\nvar SimpleMemoComponent = 15;\nvar LazyComponent = 16;\nvar IncompleteClassComponent = 17;\nvar DehydratedSuspenseComponent = 18;\n\nvar randomKey = Math.random().toString(36).slice(2);\nvar internalInstanceKey = '__reactInternalInstance$' + randomKey;\nvar internalEventHandlersKey = '__reactEventHandlers$' + randomKey;\n\nfunction precacheFiberNode(hostInst, node) {\n  node[internalInstanceKey] = hostInst;\n}\n\n/**\n * Given a DOM node, return the closest ReactDOMComponent or\n * ReactDOMTextComponent instance ancestor.\n */\nfunction getClosestInstanceFromNode(node) {\n  if (node[internalInstanceKey]) {\n    return node[internalInstanceKey];\n  }\n\n  while (!node[internalInstanceKey]) {\n    if (node.parentNode) {\n      node = node.parentNode;\n    } else {\n      // Top of the tree. This node must not be part of a React tree (or is\n      // unmounted, potentially).\n      return null;\n    }\n  }\n\n  var inst = node[internalInstanceKey];\n  if (inst.tag === HostComponent || inst.tag === HostText) {\n    // In Fiber, this will always be the deepest root.\n    return inst;\n  }\n\n  return null;\n}\n\n/**\n * Given a DOM node, return the ReactDOMComponent or ReactDOMTextComponent\n * instance, or null if the node was not rendered by this React.\n */\nfunction getInstanceFromNode$1(node) {\n  var inst = node[internalInstanceKey];\n  if (inst) {\n    if (inst.tag === HostComponent || inst.tag === HostText) {\n      return inst;\n    } else {\n      return null;\n    }\n  }\n  return null;\n}\n\n/**\n * Given a ReactDOMComponent or ReactDOMTextComponent, return the corresponding\n * DOM node.\n */\nfunction getNodeFromInstance$1(inst) {\n  if (inst.tag === HostComponent || inst.tag === HostText) {\n    // In Fiber this, is just the state node right now. We assume it will be\n    // a host component or host text.\n    return inst.stateNode;\n  }\n\n  // Without this first invariant, passing a non-DOM-component triggers the next\n  // invariant for a missing parent, which is super confusing.\n  invariant(false, 'getNodeFromInstance: Invalid argument.');\n}\n\nfunction getFiberCurrentPropsFromNode$1(node) {\n  return node[internalEventHandlersKey] || null;\n}\n\nfunction updateFiberProps(node, props) {\n  node[internalEventHandlersKey] = props;\n}\n\nfunction getParent(inst) {\n  do {\n    inst = inst.return;\n    // TODO: If this is a HostRoot we might want to bail out.\n    // That is depending on if we want nested subtrees (layers) to bubble\n    // events to their parent. We could also go through parentNode on the\n    // host node but that wouldn't work for React Native and doesn't let us\n    // do the portal feature.\n  } while (inst && inst.tag !== HostComponent);\n  if (inst) {\n    return inst;\n  }\n  return null;\n}\n\n/**\n * Return the lowest common ancestor of A and B, or null if they are in\n * different trees.\n */\nfunction getLowestCommonAncestor(instA, instB) {\n  var depthA = 0;\n  for (var tempA = instA; tempA; tempA = getParent(tempA)) {\n    depthA++;\n  }\n  var depthB = 0;\n  for (var tempB = instB; tempB; tempB = getParent(tempB)) {\n    depthB++;\n  }\n\n  // If A is deeper, crawl up.\n  while (depthA - depthB > 0) {\n    instA = getParent(instA);\n    depthA--;\n  }\n\n  // If B is deeper, crawl up.\n  while (depthB - depthA > 0) {\n    instB = getParent(instB);\n    depthB--;\n  }\n\n  // Walk in lockstep until we find a match.\n  var depth = depthA;\n  while (depth--) {\n    if (instA === instB || instA === instB.alternate) {\n      return instA;\n    }\n    instA = getParent(instA);\n    instB = getParent(instB);\n  }\n  return null;\n}\n\n/**\n * Return if A is an ancestor of B.\n */\n\n\n/**\n * Return the parent instance of the passed-in instance.\n */\n\n\n/**\n * Simulates the traversal of a two-phase, capture/bubble event dispatch.\n */\nfunction traverseTwoPhase(inst, fn, arg) {\n  var path = [];\n  while (inst) {\n    path.push(inst);\n    inst = getParent(inst);\n  }\n  var i = void 0;\n  for (i = path.length; i-- > 0;) {\n    fn(path[i], 'captured', arg);\n  }\n  for (i = 0; i < path.length; i++) {\n    fn(path[i], 'bubbled', arg);\n  }\n}\n\n/**\n * Traverses the ID hierarchy and invokes the supplied `cb` on any IDs that\n * should would receive a `mouseEnter` or `mouseLeave` event.\n *\n * Does not invoke the callback on the nearest common ancestor because nothing\n * \"entered\" or \"left\" that element.\n */\nfunction traverseEnterLeave(from, to, fn, argFrom, argTo) {\n  var common = from && to ? getLowestCommonAncestor(from, to) : null;\n  var pathFrom = [];\n  while (true) {\n    if (!from) {\n      break;\n    }\n    if (from === common) {\n      break;\n    }\n    var alternate = from.alternate;\n    if (alternate !== null && alternate === common) {\n      break;\n    }\n    pathFrom.push(from);\n    from = getParent(from);\n  }\n  var pathTo = [];\n  while (true) {\n    if (!to) {\n      break;\n    }\n    if (to === common) {\n      break;\n    }\n    var _alternate = to.alternate;\n    if (_alternate !== null && _alternate === common) {\n      break;\n    }\n    pathTo.push(to);\n    to = getParent(to);\n  }\n  for (var i = 0; i < pathFrom.length; i++) {\n    fn(pathFrom[i], 'bubbled', argFrom);\n  }\n  for (var _i = pathTo.length; _i-- > 0;) {\n    fn(pathTo[_i], 'captured', argTo);\n  }\n}\n\n/**\n * Some event types have a notion of different registration names for different\n * \"phases\" of propagation. This finds listeners by a given phase.\n */\nfunction listenerAtPhase(inst, event, propagationPhase) {\n  var registrationName = event.dispatchConfig.phasedRegistrationNames[propagationPhase];\n  return getListener(inst, registrationName);\n}\n\n/**\n * A small set of propagation patterns, each of which will accept a small amount\n * of information, and generate a set of \"dispatch ready event objects\" - which\n * are sets of events that have already been annotated with a set of dispatched\n * listener functions/ids. The API is designed this way to discourage these\n * propagation strategies from actually executing the dispatches, since we\n * always want to collect the entire set of dispatches before executing even a\n * single one.\n */\n\n/**\n * Tags a `SyntheticEvent` with dispatched listeners. Creating this function\n * here, allows us to not have to bind or create functions for each event.\n * Mutating the event's members allows us to not have to create a wrapping\n * \"dispatch\" object that pairs the event with the listener.\n */\nfunction accumulateDirectionalDispatches(inst, phase, event) {\n  {\n    !inst ? warningWithoutStack$1(false, 'Dispatching inst must not be null') : void 0;\n  }\n  var listener = listenerAtPhase(inst, event, phase);\n  if (listener) {\n    event._dispatchListeners = accumulateInto(event._dispatchListeners, listener);\n    event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);\n  }\n}\n\n/**\n * Collect dispatches (must be entirely collected before dispatching - see unit\n * tests). Lazily allocate the array to conserve memory.  We must loop through\n * each event and perform the traversal for each one. We cannot perform a\n * single traversal for the entire collection of events because each event may\n * have a different target.\n */\nfunction accumulateTwoPhaseDispatchesSingle(event) {\n  if (event && event.dispatchConfig.phasedRegistrationNames) {\n    traverseTwoPhase(event._targetInst, accumulateDirectionalDispatches, event);\n  }\n}\n\n/**\n * Accumulates without regard to direction, does not look for phased\n * registration names. Same as `accumulateDirectDispatchesSingle` but without\n * requiring that the `dispatchMarker` be the same as the dispatched ID.\n */\nfunction accumulateDispatches(inst, ignoredDirection, event) {\n  if (inst && event && event.dispatchConfig.registrationName) {\n    var registrationName = event.dispatchConfig.registrationName;\n    var listener = getListener(inst, registrationName);\n    if (listener) {\n      event._dispatchListeners = accumulateInto(event._dispatchListeners, listener);\n      event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);\n    }\n  }\n}\n\n/**\n * Accumulates dispatches on an `SyntheticEvent`, but only for the\n * `dispatchMarker`.\n * @param {SyntheticEvent} event\n */\nfunction accumulateDirectDispatchesSingle(event) {\n  if (event && event.dispatchConfig.registrationName) {\n    accumulateDispatches(event._targetInst, null, event);\n  }\n}\n\nfunction accumulateTwoPhaseDispatches(events) {\n  forEachAccumulated(events, accumulateTwoPhaseDispatchesSingle);\n}\n\n\n\nfunction accumulateEnterLeaveDispatches(leave, enter, from, to) {\n  traverseEnterLeave(from, to, accumulateDispatches, leave, enter);\n}\n\nfunction accumulateDirectDispatches(events) {\n  forEachAccumulated(events, accumulateDirectDispatchesSingle);\n}\n\nvar canUseDOM = !!(typeof window !== 'undefined' && window.document && window.document.createElement);\n\n// Do not uses the below two methods directly!\n// Instead use constants exported from DOMTopLevelEventTypes in ReactDOM.\n// (It is the only module that is allowed to access these methods.)\n\nfunction unsafeCastStringToDOMTopLevelType(topLevelType) {\n  return topLevelType;\n}\n\nfunction unsafeCastDOMTopLevelTypeToString(topLevelType) {\n  return topLevelType;\n}\n\n/**\n * Generate a mapping of standard vendor prefixes using the defined style property and event name.\n *\n * @param {string} styleProp\n * @param {string} eventName\n * @returns {object}\n */\nfunction makePrefixMap(styleProp, eventName) {\n  var prefixes = {};\n\n  prefixes[styleProp.toLowerCase()] = eventName.toLowerCase();\n  prefixes['Webkit' + styleProp] = 'webkit' + eventName;\n  prefixes['Moz' + styleProp] = 'moz' + eventName;\n\n  return prefixes;\n}\n\n/**\n * A list of event names to a configurable list of vendor prefixes.\n */\nvar vendorPrefixes = {\n  animationend: makePrefixMap('Animation', 'AnimationEnd'),\n  animationiteration: makePrefixMap('Animation', 'AnimationIteration'),\n  animationstart: makePrefixMap('Animation', 'AnimationStart'),\n  transitionend: makePrefixMap('Transition', 'TransitionEnd')\n};\n\n/**\n * Event names that have already been detected and prefixed (if applicable).\n */\nvar prefixedEventNames = {};\n\n/**\n * Element to check for prefixes on.\n */\nvar style = {};\n\n/**\n * Bootstrap if a DOM exists.\n */\nif (canUseDOM) {\n  style = document.createElement('div').style;\n\n  // On some platforms, in particular some releases of Android 4.x,\n  // the un-prefixed \"animation\" and \"transition\" properties are defined on the\n  // style object but the events that fire will still be prefixed, so we need\n  // to check if the un-prefixed events are usable, and if not remove them from the map.\n  if (!('AnimationEvent' in window)) {\n    delete vendorPrefixes.animationend.animation;\n    delete vendorPrefixes.animationiteration.animation;\n    delete vendorPrefixes.animationstart.animation;\n  }\n\n  // Same as above\n  if (!('TransitionEvent' in window)) {\n    delete vendorPrefixes.transitionend.transition;\n  }\n}\n\n/**\n * Attempts to determine the correct vendor prefixed event name.\n *\n * @param {string} eventName\n * @returns {string}\n */\nfunction getVendorPrefixedEventName(eventName) {\n  if (prefixedEventNames[eventName]) {\n    return prefixedEventNames[eventName];\n  } else if (!vendorPrefixes[eventName]) {\n    return eventName;\n  }\n\n  var prefixMap = vendorPrefixes[eventName];\n\n  for (var styleProp in prefixMap) {\n    if (prefixMap.hasOwnProperty(styleProp) && styleProp in style) {\n      return prefixedEventNames[eventName] = prefixMap[styleProp];\n    }\n  }\n\n  return eventName;\n}\n\n/**\n * To identify top level events in ReactDOM, we use constants defined by this\n * module. This is the only module that uses the unsafe* methods to express\n * that the constants actually correspond to the browser event names. This lets\n * us save some bundle size by avoiding a top level type -> event name map.\n * The rest of ReactDOM code should import top level types from this file.\n */\nvar TOP_ABORT = unsafeCastStringToDOMTopLevelType('abort');\nvar TOP_ANIMATION_END = unsafeCastStringToDOMTopLevelType(getVendorPrefixedEventName('animationend'));\nvar TOP_ANIMATION_ITERATION = unsafeCastStringToDOMTopLevelType(getVendorPrefixedEventName('animationiteration'));\nvar TOP_ANIMATION_START = unsafeCastStringToDOMTopLevelType(getVendorPrefixedEventName('animationstart'));\nvar TOP_BLUR = unsafeCastStringToDOMTopLevelType('blur');\nvar TOP_CAN_PLAY = unsafeCastStringToDOMTopLevelType('canplay');\nvar TOP_CAN_PLAY_THROUGH = unsafeCastStringToDOMTopLevelType('canplaythrough');\nvar TOP_CANCEL = unsafeCastStringToDOMTopLevelType('cancel');\nvar TOP_CHANGE = unsafeCastStringToDOMTopLevelType('change');\nvar TOP_CLICK = unsafeCastStringToDOMTopLevelType('click');\nvar TOP_CLOSE = unsafeCastStringToDOMTopLevelType('close');\nvar TOP_COMPOSITION_END = unsafeCastStringToDOMTopLevelType('compositionend');\nvar TOP_COMPOSITION_START = unsafeCastStringToDOMTopLevelType('compositionstart');\nvar TOP_COMPOSITION_UPDATE = unsafeCastStringToDOMTopLevelType('compositionupdate');\nvar TOP_CONTEXT_MENU = unsafeCastStringToDOMTopLevelType('contextmenu');\nvar TOP_COPY = unsafeCastStringToDOMTopLevelType('copy');\nvar TOP_CUT = unsafeCastStringToDOMTopLevelType('cut');\nvar TOP_DOUBLE_CLICK = unsafeCastStringToDOMTopLevelType('dblclick');\nvar TOP_AUX_CLICK = unsafeCastStringToDOMTopLevelType('auxclick');\nvar TOP_DRAG = unsafeCastStringToDOMTopLevelType('drag');\nvar TOP_DRAG_END = unsafeCastStringToDOMTopLevelType('dragend');\nvar TOP_DRAG_ENTER = unsafeCastStringToDOMTopLevelType('dragenter');\nvar TOP_DRAG_EXIT = unsafeCastStringToDOMTopLevelType('dragexit');\nvar TOP_DRAG_LEAVE = unsafeCastStringToDOMTopLevelType('dragleave');\nvar TOP_DRAG_OVER = unsafeCastStringToDOMTopLevelType('dragover');\nvar TOP_DRAG_START = unsafeCastStringToDOMTopLevelType('dragstart');\nvar TOP_DROP = unsafeCastStringToDOMTopLevelType('drop');\nvar TOP_DURATION_CHANGE = unsafeCastStringToDOMTopLevelType('durationchange');\nvar TOP_EMPTIED = unsafeCastStringToDOMTopLevelType('emptied');\nvar TOP_ENCRYPTED = unsafeCastStringToDOMTopLevelType('encrypted');\nvar TOP_ENDED = unsafeCastStringToDOMTopLevelType('ended');\nvar TOP_ERROR = unsafeCastStringToDOMTopLevelType('error');\nvar TOP_FOCUS = unsafeCastStringToDOMTopLevelType('focus');\nvar TOP_GOT_POINTER_CAPTURE = unsafeCastStringToDOMTopLevelType('gotpointercapture');\nvar TOP_INPUT = unsafeCastStringToDOMTopLevelType('input');\nvar TOP_INVALID = unsafeCastStringToDOMTopLevelType('invalid');\nvar TOP_KEY_DOWN = unsafeCastStringToDOMTopLevelType('keydown');\nvar TOP_KEY_PRESS = unsafeCastStringToDOMTopLevelType('keypress');\nvar TOP_KEY_UP = unsafeCastStringToDOMTopLevelType('keyup');\nvar TOP_LOAD = unsafeCastStringToDOMTopLevelType('load');\nvar TOP_LOAD_START = unsafeCastStringToDOMTopLevelType('loadstart');\nvar TOP_LOADED_DATA = unsafeCastStringToDOMTopLevelType('loadeddata');\nvar TOP_LOADED_METADATA = unsafeCastStringToDOMTopLevelType('loadedmetadata');\nvar TOP_LOST_POINTER_CAPTURE = unsafeCastStringToDOMTopLevelType('lostpointercapture');\nvar TOP_MOUSE_DOWN = unsafeCastStringToDOMTopLevelType('mousedown');\nvar TOP_MOUSE_MOVE = unsafeCastStringToDOMTopLevelType('mousemove');\nvar TOP_MOUSE_OUT = unsafeCastStringToDOMTopLevelType('mouseout');\nvar TOP_MOUSE_OVER = unsafeCastStringToDOMTopLevelType('mouseover');\nvar TOP_MOUSE_UP = unsafeCastStringToDOMTopLevelType('mouseup');\nvar TOP_PASTE = unsafeCastStringToDOMTopLevelType('paste');\nvar TOP_PAUSE = unsafeCastStringToDOMTopLevelType('pause');\nvar TOP_PLAY = unsafeCastStringToDOMTopLevelType('play');\nvar TOP_PLAYING = unsafeCastStringToDOMTopLevelType('playing');\nvar TOP_POINTER_CANCEL = unsafeCastStringToDOMTopLevelType('pointercancel');\nvar TOP_POINTER_DOWN = unsafeCastStringToDOMTopLevelType('pointerdown');\n\n\nvar TOP_POINTER_MOVE = unsafeCastStringToDOMTopLevelType('pointermove');\nvar TOP_POINTER_OUT = unsafeCastStringToDOMTopLevelType('pointerout');\nvar TOP_POINTER_OVER = unsafeCastStringToDOMTopLevelType('pointerover');\nvar TOP_POINTER_UP = unsafeCastStringToDOMTopLevelType('pointerup');\nvar TOP_PROGRESS = unsafeCastStringToDOMTopLevelType('progress');\nvar TOP_RATE_CHANGE = unsafeCastStringToDOMTopLevelType('ratechange');\nvar TOP_RESET = unsafeCastStringToDOMTopLevelType('reset');\nvar TOP_SCROLL = unsafeCastStringToDOMTopLevelType('scroll');\nvar TOP_SEEKED = unsafeCastStringToDOMTopLevelType('seeked');\nvar TOP_SEEKING = unsafeCastStringToDOMTopLevelType('seeking');\nvar TOP_SELECTION_CHANGE = unsafeCastStringToDOMTopLevelType('selectionchange');\nvar TOP_STALLED = unsafeCastStringToDOMTopLevelType('stalled');\nvar TOP_SUBMIT = unsafeCastStringToDOMTopLevelType('submit');\nvar TOP_SUSPEND = unsafeCastStringToDOMTopLevelType('suspend');\nvar TOP_TEXT_INPUT = unsafeCastStringToDOMTopLevelType('textInput');\nvar TOP_TIME_UPDATE = unsafeCastStringToDOMTopLevelType('timeupdate');\nvar TOP_TOGGLE = unsafeCastStringToDOMTopLevelType('toggle');\nvar TOP_TOUCH_CANCEL = unsafeCastStringToDOMTopLevelType('touchcancel');\nvar TOP_TOUCH_END = unsafeCastStringToDOMTopLevelType('touchend');\nvar TOP_TOUCH_MOVE = unsafeCastStringToDOMTopLevelType('touchmove');\nvar TOP_TOUCH_START = unsafeCastStringToDOMTopLevelType('touchstart');\nvar TOP_TRANSITION_END = unsafeCastStringToDOMTopLevelType(getVendorPrefixedEventName('transitionend'));\nvar TOP_VOLUME_CHANGE = unsafeCastStringToDOMTopLevelType('volumechange');\nvar TOP_WAITING = unsafeCastStringToDOMTopLevelType('waiting');\nvar TOP_WHEEL = unsafeCastStringToDOMTopLevelType('wheel');\n\n// List of events that need to be individually attached to media elements.\n// Note that events in this list will *not* be listened to at the top level\n// unless they're explicitly whitelisted in `ReactBrowserEventEmitter.listenTo`.\nvar mediaEventTypes = [TOP_ABORT, TOP_CAN_PLAY, TOP_CAN_PLAY_THROUGH, TOP_DURATION_CHANGE, TOP_EMPTIED, TOP_ENCRYPTED, TOP_ENDED, TOP_ERROR, TOP_LOADED_DATA, TOP_LOADED_METADATA, TOP_LOAD_START, TOP_PAUSE, TOP_PLAY, TOP_PLAYING, TOP_PROGRESS, TOP_RATE_CHANGE, TOP_SEEKED, TOP_SEEKING, TOP_STALLED, TOP_SUSPEND, TOP_TIME_UPDATE, TOP_VOLUME_CHANGE, TOP_WAITING];\n\nfunction getRawEventName(topLevelType) {\n  return unsafeCastDOMTopLevelTypeToString(topLevelType);\n}\n\n/**\n * These variables store information about text content of a target node,\n * allowing comparison of content before and after a given event.\n *\n * Identify the node where selection currently begins, then observe\n * both its text content and its current position in the DOM. Since the\n * browser may natively replace the target node during composition, we can\n * use its position to find its replacement.\n *\n *\n */\n\nvar root = null;\nvar startText = null;\nvar fallbackText = null;\n\nfunction initialize(nativeEventTarget) {\n  root = nativeEventTarget;\n  startText = getText();\n  return true;\n}\n\nfunction reset() {\n  root = null;\n  startText = null;\n  fallbackText = null;\n}\n\nfunction getData() {\n  if (fallbackText) {\n    return fallbackText;\n  }\n\n  var start = void 0;\n  var startValue = startText;\n  var startLength = startValue.length;\n  var end = void 0;\n  var endValue = getText();\n  var endLength = endValue.length;\n\n  for (start = 0; start < startLength; start++) {\n    if (startValue[start] !== endValue[start]) {\n      break;\n    }\n  }\n\n  var minEnd = startLength - start;\n  for (end = 1; end <= minEnd; end++) {\n    if (startValue[startLength - end] !== endValue[endLength - end]) {\n      break;\n    }\n  }\n\n  var sliceTail = end > 1 ? 1 - end : undefined;\n  fallbackText = endValue.slice(start, sliceTail);\n  return fallbackText;\n}\n\nfunction getText() {\n  if ('value' in root) {\n    return root.value;\n  }\n  return root.textContent;\n}\n\nvar ReactInternals = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;\n\nvar _assign = ReactInternals.assign;\n\n/* eslint valid-typeof: 0 */\n\nvar EVENT_POOL_SIZE = 10;\n\n/**\n * @interface Event\n * @see http://www.w3.org/TR/DOM-Level-3-Events/\n */\nvar EventInterface = {\n  type: null,\n  target: null,\n  // currentTarget is set when dispatching; no use in copying it here\n  currentTarget: function () {\n    return null;\n  },\n  eventPhase: null,\n  bubbles: null,\n  cancelable: null,\n  timeStamp: function (event) {\n    return event.timeStamp || Date.now();\n  },\n  defaultPrevented: null,\n  isTrusted: null\n};\n\nfunction functionThatReturnsTrue() {\n  return true;\n}\n\nfunction functionThatReturnsFalse() {\n  return false;\n}\n\n/**\n * Synthetic events are dispatched by event plugins, typically in response to a\n * top-level event delegation handler.\n *\n * These systems should generally use pooling to reduce the frequency of garbage\n * collection. The system should check `isPersistent` to determine whether the\n * event should be released into the pool after being dispatched. Users that\n * need a persisted event should invoke `persist`.\n *\n * Synthetic events (and subclasses) implement the DOM Level 3 Events API by\n * normalizing browser quirks. Subclasses do not necessarily have to implement a\n * DOM interface; custom application-specific events can also subclass this.\n *\n * @param {object} dispatchConfig Configuration used to dispatch this event.\n * @param {*} targetInst Marker identifying the event target.\n * @param {object} nativeEvent Native browser event.\n * @param {DOMEventTarget} nativeEventTarget Target node.\n */\nfunction SyntheticEvent(dispatchConfig, targetInst, nativeEvent, nativeEventTarget) {\n  {\n    // these have a getter/setter for warnings\n    delete this.nativeEvent;\n    delete this.preventDefault;\n    delete this.stopPropagation;\n    delete this.isDefaultPrevented;\n    delete this.isPropagationStopped;\n  }\n\n  this.dispatchConfig = dispatchConfig;\n  this._targetInst = targetInst;\n  this.nativeEvent = nativeEvent;\n\n  var Interface = this.constructor.Interface;\n  for (var propName in Interface) {\n    if (!Interface.hasOwnProperty(propName)) {\n      continue;\n    }\n    {\n      delete this[propName]; // this has a getter/setter for warnings\n    }\n    var normalize = Interface[propName];\n    if (normalize) {\n      this[propName] = normalize(nativeEvent);\n    } else {\n      if (propName === 'target') {\n        this.target = nativeEventTarget;\n      } else {\n        this[propName] = nativeEvent[propName];\n      }\n    }\n  }\n\n  var defaultPrevented = nativeEvent.defaultPrevented != null ? nativeEvent.defaultPrevented : nativeEvent.returnValue === false;\n  if (defaultPrevented) {\n    this.isDefaultPrevented = functionThatReturnsTrue;\n  } else {\n    this.isDefaultPrevented = functionThatReturnsFalse;\n  }\n  this.isPropagationStopped = functionThatReturnsFalse;\n  return this;\n}\n\n_assign(SyntheticEvent.prototype, {\n  preventDefault: function () {\n    this.defaultPrevented = true;\n    var event = this.nativeEvent;\n    if (!event) {\n      return;\n    }\n\n    if (event.preventDefault) {\n      event.preventDefault();\n    } else if (typeof event.returnValue !== 'unknown') {\n      event.returnValue = false;\n    }\n    this.isDefaultPrevented = functionThatReturnsTrue;\n  },\n\n  stopPropagation: function () {\n    var event = this.nativeEvent;\n    if (!event) {\n      return;\n    }\n\n    if (event.stopPropagation) {\n      event.stopPropagation();\n    } else if (typeof event.cancelBubble !== 'unknown') {\n      // The ChangeEventPlugin registers a \"propertychange\" event for\n      // IE. This event does not support bubbling or cancelling, and\n      // any references to cancelBubble throw \"Member not found\".  A\n      // typeof check of \"unknown\" circumvents this issue (and is also\n      // IE specific).\n      event.cancelBubble = true;\n    }\n\n    this.isPropagationStopped = functionThatReturnsTrue;\n  },\n\n  /**\n   * We release all dispatched `SyntheticEvent`s after each event loop, adding\n   * them back into the pool. This allows a way to hold onto a reference that\n   * won't be added back into the pool.\n   */\n  persist: function () {\n    this.isPersistent = functionThatReturnsTrue;\n  },\n\n  /**\n   * Checks if this event should be released back into the pool.\n   *\n   * @return {boolean} True if this should not be released, false otherwise.\n   */\n  isPersistent: functionThatReturnsFalse,\n\n  /**\n   * `PooledClass` looks for `destructor` on each instance it releases.\n   */\n  destructor: function () {\n    var Interface = this.constructor.Interface;\n    for (var propName in Interface) {\n      {\n        Object.defineProperty(this, propName, getPooledWarningPropertyDefinition(propName, Interface[propName]));\n      }\n    }\n    this.dispatchConfig = null;\n    this._targetInst = null;\n    this.nativeEvent = null;\n    this.isDefaultPrevented = functionThatReturnsFalse;\n    this.isPropagationStopped = functionThatReturnsFalse;\n    this._dispatchListeners = null;\n    this._dispatchInstances = null;\n    {\n      Object.defineProperty(this, 'nativeEvent', getPooledWarningPropertyDefinition('nativeEvent', null));\n      Object.defineProperty(this, 'isDefaultPrevented', getPooledWarningPropertyDefinition('isDefaultPrevented', functionThatReturnsFalse));\n      Object.defineProperty(this, 'isPropagationStopped', getPooledWarningPropertyDefinition('isPropagationStopped', functionThatReturnsFalse));\n      Object.defineProperty(this, 'preventDefault', getPooledWarningPropertyDefinition('preventDefault', function () {}));\n      Object.defineProperty(this, 'stopPropagation', getPooledWarningPropertyDefinition('stopPropagation', function () {}));\n    }\n  }\n});\n\nSyntheticEvent.Interface = EventInterface;\n\n/**\n * Helper to reduce boilerplate when creating subclasses.\n */\nSyntheticEvent.extend = function (Interface) {\n  var Super = this;\n\n  var E = function () {};\n  E.prototype = Super.prototype;\n  var prototype = new E();\n\n  function Class() {\n    return Super.apply(this, arguments);\n  }\n  _assign(prototype, Class.prototype);\n  Class.prototype = prototype;\n  Class.prototype.constructor = Class;\n\n  Class.Interface = _assign({}, Super.Interface, Interface);\n  Class.extend = Super.extend;\n  addEventPoolingTo(Class);\n\n  return Class;\n};\n\naddEventPoolingTo(SyntheticEvent);\n\n/**\n * Helper to nullify syntheticEvent instance properties when destructing\n *\n * @param {String} propName\n * @param {?object} getVal\n * @return {object} defineProperty object\n */\nfunction getPooledWarningPropertyDefinition(propName, getVal) {\n  var isFunction = typeof getVal === 'function';\n  return {\n    configurable: true,\n    set: set,\n    get: get\n  };\n\n  function set(val) {\n    var action = isFunction ? 'setting the method' : 'setting the property';\n    warn(action, 'This is effectively a no-op');\n    return val;\n  }\n\n  function get() {\n    var action = isFunction ? 'accessing the method' : 'accessing the property';\n    var result = isFunction ? 'This is a no-op function' : 'This is set to null';\n    warn(action, result);\n    return getVal;\n  }\n\n  function warn(action, result) {\n    var warningCondition = false;\n    !warningCondition ? warningWithoutStack$1(false, \"This synthetic event is reused for performance reasons. If you're seeing this, \" + \"you're %s `%s` on a released/nullified synthetic event. %s. \" + 'If you must keep the original synthetic event around, use event.persist(). ' + 'See https://fb.me/react-event-pooling for more information.', action, propName, result) : void 0;\n  }\n}\n\nfunction getPooledEvent(dispatchConfig, targetInst, nativeEvent, nativeInst) {\n  var EventConstructor = this;\n  if (EventConstructor.eventPool.length) {\n    var instance = EventConstructor.eventPool.pop();\n    EventConstructor.call(instance, dispatchConfig, targetInst, nativeEvent, nativeInst);\n    return instance;\n  }\n  return new EventConstructor(dispatchConfig, targetInst, nativeEvent, nativeInst);\n}\n\nfunction releasePooledEvent(event) {\n  var EventConstructor = this;\n  !(event instanceof EventConstructor) ? invariant(false, 'Trying to release an event instance into a pool of a different type.') : void 0;\n  event.destructor();\n  if (EventConstructor.eventPool.length < EVENT_POOL_SIZE) {\n    EventConstructor.eventPool.push(event);\n  }\n}\n\nfunction addEventPoolingTo(EventConstructor) {\n  EventConstructor.eventPool = [];\n  EventConstructor.getPooled = getPooledEvent;\n  EventConstructor.release = releasePooledEvent;\n}\n\n/**\n * @interface Event\n * @see http://www.w3.org/TR/DOM-Level-3-Events/#events-compositionevents\n */\nvar SyntheticCompositionEvent = SyntheticEvent.extend({\n  data: null\n});\n\n/**\n * @interface Event\n * @see http://www.w3.org/TR/2013/WD-DOM-Level-3-Events-20131105\n *      /#events-inputevents\n */\nvar SyntheticInputEvent = SyntheticEvent.extend({\n  data: null\n});\n\nvar END_KEYCODES = [9, 13, 27, 32]; // Tab, Return, Esc, Space\nvar START_KEYCODE = 229;\n\nvar canUseCompositionEvent = canUseDOM && 'CompositionEvent' in window;\n\nvar documentMode = null;\nif (canUseDOM && 'documentMode' in document) {\n  documentMode = document.documentMode;\n}\n\n// Webkit offers a very useful `textInput` event that can be used to\n// directly represent `beforeInput`. The IE `textinput` event is not as\n// useful, so we don't use it.\nvar canUseTextInputEvent = canUseDOM && 'TextEvent' in window && !documentMode;\n\n// In IE9+, we have access to composition events, but the data supplied\n// by the native compositionend event may be incorrect. Japanese ideographic\n// spaces, for instance (\\u3000) are not recorded correctly.\nvar useFallbackCompositionData = canUseDOM && (!canUseCompositionEvent || documentMode && documentMode > 8 && documentMode <= 11);\n\nvar SPACEBAR_CODE = 32;\nvar SPACEBAR_CHAR = String.fromCharCode(SPACEBAR_CODE);\n\n// Events and their corresponding property names.\nvar eventTypes = {\n  beforeInput: {\n    phasedRegistrationNames: {\n      bubbled: 'onBeforeInput',\n      captured: 'onBeforeInputCapture'\n    },\n    dependencies: [TOP_COMPOSITION_END, TOP_KEY_PRESS, TOP_TEXT_INPUT, TOP_PASTE]\n  },\n  compositionEnd: {\n    phasedRegistrationNames: {\n      bubbled: 'onCompositionEnd',\n      captured: 'onCompositionEndCapture'\n    },\n    dependencies: [TOP_BLUR, TOP_COMPOSITION_END, TOP_KEY_DOWN, TOP_KEY_PRESS, TOP_KEY_UP, TOP_MOUSE_DOWN]\n  },\n  compositionStart: {\n    phasedRegistrationNames: {\n      bubbled: 'onCompositionStart',\n      captured: 'onCompositionStartCapture'\n    },\n    dependencies: [TOP_BLUR, TOP_COMPOSITION_START, TOP_KEY_DOWN, TOP_KEY_PRESS, TOP_KEY_UP, TOP_MOUSE_DOWN]\n  },\n  compositionUpdate: {\n    phasedRegistrationNames: {\n      bubbled: 'onCompositionUpdate',\n      captured: 'onCompositionUpdateCapture'\n    },\n    dependencies: [TOP_BLUR, TOP_COMPOSITION_UPDATE, TOP_KEY_DOWN, TOP_KEY_PRESS, TOP_KEY_UP, TOP_MOUSE_DOWN]\n  }\n};\n\n// Track whether we've ever handled a keypress on the space key.\nvar hasSpaceKeypress = false;\n\n/**\n * Return whether a native keypress event is assumed to be a command.\n * This is required because Firefox fires `keypress` events for key commands\n * (cut, copy, select-all, etc.) even though no character is inserted.\n */\nfunction isKeypressCommand(nativeEvent) {\n  return (nativeEvent.ctrlKey || nativeEvent.altKey || nativeEvent.metaKey) &&\n  // ctrlKey && altKey is equivalent to AltGr, and is not a command.\n  !(nativeEvent.ctrlKey && nativeEvent.altKey);\n}\n\n/**\n * Translate native top level events into event types.\n *\n * @param {string} topLevelType\n * @return {object}\n */\nfunction getCompositionEventType(topLevelType) {\n  switch (topLevelType) {\n    case TOP_COMPOSITION_START:\n      return eventTypes.compositionStart;\n    case TOP_COMPOSITION_END:\n      return eventTypes.compositionEnd;\n    case TOP_COMPOSITION_UPDATE:\n      return eventTypes.compositionUpdate;\n  }\n}\n\n/**\n * Does our fallback best-guess model think this event signifies that\n * composition has begun?\n *\n * @param {string} topLevelType\n * @param {object} nativeEvent\n * @return {boolean}\n */\nfunction isFallbackCompositionStart(topLevelType, nativeEvent) {\n  return topLevelType === TOP_KEY_DOWN && nativeEvent.keyCode === START_KEYCODE;\n}\n\n/**\n * Does our fallback mode think that this event is the end of composition?\n *\n * @param {string} topLevelType\n * @param {object} nativeEvent\n * @return {boolean}\n */\nfunction isFallbackCompositionEnd(topLevelType, nativeEvent) {\n  switch (topLevelType) {\n    case TOP_KEY_UP:\n      // Command keys insert or clear IME input.\n      return END_KEYCODES.indexOf(nativeEvent.keyCode) !== -1;\n    case TOP_KEY_DOWN:\n      // Expect IME keyCode on each keydown. If we get any other\n      // code we must have exited earlier.\n      return nativeEvent.keyCode !== START_KEYCODE;\n    case TOP_KEY_PRESS:\n    case TOP_MOUSE_DOWN:\n    case TOP_BLUR:\n      // Events are not possible without cancelling IME.\n      return true;\n    default:\n      return false;\n  }\n}\n\n/**\n * Google Input Tools provides composition data via a CustomEvent,\n * with the `data` property populated in the `detail` object. If this\n * is available on the event object, use it. If not, this is a plain\n * composition event and we have nothing special to extract.\n *\n * @param {object} nativeEvent\n * @return {?string}\n */\nfunction getDataFromCustomEvent(nativeEvent) {\n  var detail = nativeEvent.detail;\n  if (typeof detail === 'object' && 'data' in detail) {\n    return detail.data;\n  }\n  return null;\n}\n\n/**\n * Check if a composition event was triggered by Korean IME.\n * Our fallback mode does not work well with IE's Korean IME,\n * so just use native composition events when Korean IME is used.\n * Although CompositionEvent.locale property is deprecated,\n * it is available in IE, where our fallback mode is enabled.\n *\n * @param {object} nativeEvent\n * @return {boolean}\n */\nfunction isUsingKoreanIME(nativeEvent) {\n  return nativeEvent.locale === 'ko';\n}\n\n// Track the current IME composition status, if any.\nvar isComposing = false;\n\n/**\n * @return {?object} A SyntheticCompositionEvent.\n */\nfunction extractCompositionEvent(topLevelType, targetInst, nativeEvent, nativeEventTarget) {\n  var eventType = void 0;\n  var fallbackData = void 0;\n\n  if (canUseCompositionEvent) {\n    eventType = getCompositionEventType(topLevelType);\n  } else if (!isComposing) {\n    if (isFallbackCompositionStart(topLevelType, nativeEvent)) {\n      eventType = eventTypes.compositionStart;\n    }\n  } else if (isFallbackCompositionEnd(topLevelType, nativeEvent)) {\n    eventType = eventTypes.compositionEnd;\n  }\n\n  if (!eventType) {\n    return null;\n  }\n\n  if (useFallbackCompositionData && !isUsingKoreanIME(nativeEvent)) {\n    // The current composition is stored statically and must not be\n    // overwritten while composition continues.\n    if (!isComposing && eventType === eventTypes.compositionStart) {\n      isComposing = initialize(nativeEventTarget);\n    } else if (eventType === eventTypes.compositionEnd) {\n      if (isComposing) {\n        fallbackData = getData();\n      }\n    }\n  }\n\n  var event = SyntheticCompositionEvent.getPooled(eventType, targetInst, nativeEvent, nativeEventTarget);\n\n  if (fallbackData) {\n    // Inject data generated from fallback path into the synthetic event.\n    // This matches the property of native CompositionEventInterface.\n    event.data = fallbackData;\n  } else {\n    var customData = getDataFromCustomEvent(nativeEvent);\n    if (customData !== null) {\n      event.data = customData;\n    }\n  }\n\n  accumulateTwoPhaseDispatches(event);\n  return event;\n}\n\n/**\n * @param {TopLevelType} topLevelType Number from `TopLevelType`.\n * @param {object} nativeEvent Native browser event.\n * @return {?string} The string corresponding to this `beforeInput` event.\n */\nfunction getNativeBeforeInputChars(topLevelType, nativeEvent) {\n  switch (topLevelType) {\n    case TOP_COMPOSITION_END:\n      return getDataFromCustomEvent(nativeEvent);\n    case TOP_KEY_PRESS:\n      /**\n       * If native `textInput` events are available, our goal is to make\n       * use of them. However, there is a special case: the spacebar key.\n       * In Webkit, preventing default on a spacebar `textInput` event\n       * cancels character insertion, but it *also* causes the browser\n       * to fall back to its default spacebar behavior of scrolling the\n       * page.\n       *\n       * Tracking at:\n       * https://code.google.com/p/chromium/issues/detail?id=355103\n       *\n       * To avoid this issue, use the keypress event as if no `textInput`\n       * event is available.\n       */\n      var which = nativeEvent.which;\n      if (which !== SPACEBAR_CODE) {\n        return null;\n      }\n\n      hasSpaceKeypress = true;\n      return SPACEBAR_CHAR;\n\n    case TOP_TEXT_INPUT:\n      // Record the characters to be added to the DOM.\n      var chars = nativeEvent.data;\n\n      // If it's a spacebar character, assume that we have already handled\n      // it at the keypress level and bail immediately. Android Chrome\n      // doesn't give us keycodes, so we need to ignore it.\n      if (chars === SPACEBAR_CHAR && hasSpaceKeypress) {\n        return null;\n      }\n\n      return chars;\n\n    default:\n      // For other native event types, do nothing.\n      return null;\n  }\n}\n\n/**\n * For browsers that do not provide the `textInput` event, extract the\n * appropriate string to use for SyntheticInputEvent.\n *\n * @param {number} topLevelType Number from `TopLevelEventTypes`.\n * @param {object} nativeEvent Native browser event.\n * @return {?string} The fallback string for this `beforeInput` event.\n */\nfunction getFallbackBeforeInputChars(topLevelType, nativeEvent) {\n  // If we are currently composing (IME) and using a fallback to do so,\n  // try to extract the composed characters from the fallback object.\n  // If composition event is available, we extract a string only at\n  // compositionevent, otherwise extract it at fallback events.\n  if (isComposing) {\n    if (topLevelType === TOP_COMPOSITION_END || !canUseCompositionEvent && isFallbackCompositionEnd(topLevelType, nativeEvent)) {\n      var chars = getData();\n      reset();\n      isComposing = false;\n      return chars;\n    }\n    return null;\n  }\n\n  switch (topLevelType) {\n    case TOP_PASTE:\n      // If a paste event occurs after a keypress, throw out the input\n      // chars. Paste events should not lead to BeforeInput events.\n      return null;\n    case TOP_KEY_PRESS:\n      /**\n       * As of v27, Firefox may fire keypress events even when no character\n       * will be inserted. A few possibilities:\n       *\n       * - `which` is `0`. Arrow keys, Esc key, etc.\n       *\n       * - `which` is the pressed key code, but no char is available.\n       *   Ex: 'AltGr + d` in Polish. There is no modified character for\n       *   this key combination and no character is inserted into the\n       *   document, but FF fires the keypress for char code `100` anyway.\n       *   No `input` event will occur.\n       *\n       * - `which` is the pressed key code, but a command combination is\n       *   being used. Ex: `Cmd+C`. No character is inserted, and no\n       *   `input` event will occur.\n       */\n      if (!isKeypressCommand(nativeEvent)) {\n        // IE fires the `keypress` event when a user types an emoji via\n        // Touch keyboard of Windows.  In such a case, the `char` property\n        // holds an emoji character like `\\uD83D\\uDE0A`.  Because its length\n        // is 2, the property `which` does not represent an emoji correctly.\n        // In such a case, we directly return the `char` property instead of\n        // using `which`.\n        if (nativeEvent.char && nativeEvent.char.length > 1) {\n          return nativeEvent.char;\n        } else if (nativeEvent.which) {\n          return String.fromCharCode(nativeEvent.which);\n        }\n      }\n      return null;\n    case TOP_COMPOSITION_END:\n      return useFallbackCompositionData && !isUsingKoreanIME(nativeEvent) ? null : nativeEvent.data;\n    default:\n      return null;\n  }\n}\n\n/**\n * Extract a SyntheticInputEvent for `beforeInput`, based on either native\n * `textInput` or fallback behavior.\n *\n * @return {?object} A SyntheticInputEvent.\n */\nfunction extractBeforeInputEvent(topLevelType, targetInst, nativeEvent, nativeEventTarget) {\n  var chars = void 0;\n\n  if (canUseTextInputEvent) {\n    chars = getNativeBeforeInputChars(topLevelType, nativeEvent);\n  } else {\n    chars = getFallbackBeforeInputChars(topLevelType, nativeEvent);\n  }\n\n  // If no characters are being inserted, no BeforeInput event should\n  // be fired.\n  if (!chars) {\n    return null;\n  }\n\n  var event = SyntheticInputEvent.getPooled(eventTypes.beforeInput, targetInst, nativeEvent, nativeEventTarget);\n\n  event.data = chars;\n  accumulateTwoPhaseDispatches(event);\n  return event;\n}\n\n/**\n * Create an `onBeforeInput` event to match\n * http://www.w3.org/TR/2013/WD-DOM-Level-3-Events-20131105/#events-inputevents.\n *\n * This event plugin is based on the native `textInput` event\n * available in Chrome, Safari, Opera, and IE. This event fires after\n * `onKeyPress` and `onCompositionEnd`, but before `onInput`.\n *\n * `beforeInput` is spec'd but not implemented in any browsers, and\n * the `input` event does not provide any useful information about what has\n * actually been added, contrary to the spec. Thus, `textInput` is the best\n * available event to identify the characters that have actually been inserted\n * into the target node.\n *\n * This plugin is also responsible for emitting `composition` events, thus\n * allowing us to share composition fallback code for both `beforeInput` and\n * `composition` event types.\n */\nvar BeforeInputEventPlugin = {\n  eventTypes: eventTypes,\n\n  extractEvents: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {\n    var composition = extractCompositionEvent(topLevelType, targetInst, nativeEvent, nativeEventTarget);\n\n    var beforeInput = extractBeforeInputEvent(topLevelType, targetInst, nativeEvent, nativeEventTarget);\n\n    if (composition === null) {\n      return beforeInput;\n    }\n\n    if (beforeInput === null) {\n      return composition;\n    }\n\n    return [composition, beforeInput];\n  }\n};\n\n// Use to restore controlled state after a change event has fired.\n\nvar restoreImpl = null;\nvar restoreTarget = null;\nvar restoreQueue = null;\n\nfunction restoreStateOfTarget(target) {\n  // We perform this translation at the end of the event loop so that we\n  // always receive the correct fiber here\n  var internalInstance = getInstanceFromNode(target);\n  if (!internalInstance) {\n    // Unmounted\n    return;\n  }\n  !(typeof restoreImpl === 'function') ? invariant(false, 'setRestoreImplementation() needs to be called to handle a target for controlled events. This error is likely caused by a bug in React. Please file an issue.') : void 0;\n  var props = getFiberCurrentPropsFromNode(internalInstance.stateNode);\n  restoreImpl(internalInstance.stateNode, internalInstance.type, props);\n}\n\nfunction setRestoreImplementation(impl) {\n  restoreImpl = impl;\n}\n\nfunction enqueueStateRestore(target) {\n  if (restoreTarget) {\n    if (restoreQueue) {\n      restoreQueue.push(target);\n    } else {\n      restoreQueue = [target];\n    }\n  } else {\n    restoreTarget = target;\n  }\n}\n\nfunction needsStateRestore() {\n  return restoreTarget !== null || restoreQueue !== null;\n}\n\nfunction restoreStateIfNeeded() {\n  if (!restoreTarget) {\n    return;\n  }\n  var target = restoreTarget;\n  var queuedTargets = restoreQueue;\n  restoreTarget = null;\n  restoreQueue = null;\n\n  restoreStateOfTarget(target);\n  if (queuedTargets) {\n    for (var i = 0; i < queuedTargets.length; i++) {\n      restoreStateOfTarget(queuedTargets[i]);\n    }\n  }\n}\n\n// Used as a way to call batchedUpdates when we don't have a reference to\n// the renderer. Such as when we're dispatching events or if third party\n// libraries need to call batchedUpdates. Eventually, this API will go away when\n// everything is batched by default. We'll then have a similar API to opt-out of\n// scheduled work and instead do synchronous work.\n\n// Defaults\nvar _batchedUpdatesImpl = function (fn, bookkeeping) {\n  return fn(bookkeeping);\n};\nvar _interactiveUpdatesImpl = function (fn, a, b) {\n  return fn(a, b);\n};\nvar _flushInteractiveUpdatesImpl = function () {};\n\nvar isBatching = false;\nfunction batchedUpdates(fn, bookkeeping) {\n  if (isBatching) {\n    // If we are currently inside another batch, we need to wait until it\n    // fully completes before restoring state.\n    return fn(bookkeeping);\n  }\n  isBatching = true;\n  try {\n    return _batchedUpdatesImpl(fn, bookkeeping);\n  } finally {\n    // Here we wait until all updates have propagated, which is important\n    // when using controlled components within layers:\n    // https://github.com/facebook/react/issues/1698\n    // Then we restore state of any controlled component.\n    isBatching = false;\n    var controlledComponentsHavePendingUpdates = needsStateRestore();\n    if (controlledComponentsHavePendingUpdates) {\n      // If a controlled event was fired, we may need to restore the state of\n      // the DOM node back to the controlled value. This is necessary when React\n      // bails out of the update without touching the DOM.\n      _flushInteractiveUpdatesImpl();\n      restoreStateIfNeeded();\n    }\n  }\n}\n\nfunction interactiveUpdates(fn, a, b) {\n  return _interactiveUpdatesImpl(fn, a, b);\n}\n\n\n\nfunction setBatchingImplementation(batchedUpdatesImpl, interactiveUpdatesImpl, flushInteractiveUpdatesImpl) {\n  _batchedUpdatesImpl = batchedUpdatesImpl;\n  _interactiveUpdatesImpl = interactiveUpdatesImpl;\n  _flushInteractiveUpdatesImpl = flushInteractiveUpdatesImpl;\n}\n\n/**\n * @see http://www.whatwg.org/specs/web-apps/current-work/multipage/the-input-element.html#input-type-attr-summary\n */\nvar supportedInputTypes = {\n  color: true,\n  date: true,\n  datetime: true,\n  'datetime-local': true,\n  email: true,\n  month: true,\n  number: true,\n  password: true,\n  range: true,\n  search: true,\n  tel: true,\n  text: true,\n  time: true,\n  url: true,\n  week: true\n};\n\nfunction isTextInputElement(elem) {\n  var nodeName = elem && elem.nodeName && elem.nodeName.toLowerCase();\n\n  if (nodeName === 'input') {\n    return !!supportedInputTypes[elem.type];\n  }\n\n  if (nodeName === 'textarea') {\n    return true;\n  }\n\n  return false;\n}\n\n/**\n * HTML nodeType values that represent the type of the node\n */\n\nvar ELEMENT_NODE = 1;\nvar TEXT_NODE = 3;\nvar COMMENT_NODE = 8;\nvar DOCUMENT_NODE = 9;\nvar DOCUMENT_FRAGMENT_NODE = 11;\n\n/**\n * Gets the target node from a native browser event by accounting for\n * inconsistencies in browser DOM APIs.\n *\n * @param {object} nativeEvent Native browser event.\n * @return {DOMEventTarget} Target node.\n */\nfunction getEventTarget(nativeEvent) {\n  // Fallback to nativeEvent.srcElement for IE9\n  // https://github.com/facebook/react/issues/12506\n  var target = nativeEvent.target || nativeEvent.srcElement || window;\n\n  // Normalize SVG <use> element events #4963\n  if (target.correspondingUseElement) {\n    target = target.correspondingUseElement;\n  }\n\n  // Safari may fire events on text nodes (Node.TEXT_NODE is 3).\n  // @see http://www.quirksmode.org/js/events_properties.html\n  return target.nodeType === TEXT_NODE ? target.parentNode : target;\n}\n\n/**\n * Checks if an event is supported in the current execution environment.\n *\n * NOTE: This will not work correctly for non-generic events such as `change`,\n * `reset`, `load`, `error`, and `select`.\n *\n * Borrows from Modernizr.\n *\n * @param {string} eventNameSuffix Event name, e.g. \"click\".\n * @return {boolean} True if the event is supported.\n * @internal\n * @license Modernizr 3.0.0pre (Custom Build) | MIT\n */\nfunction isEventSupported(eventNameSuffix) {\n  if (!canUseDOM) {\n    return false;\n  }\n\n  var eventName = 'on' + eventNameSuffix;\n  var isSupported = eventName in document;\n\n  if (!isSupported) {\n    var element = document.createElement('div');\n    element.setAttribute(eventName, 'return;');\n    isSupported = typeof element[eventName] === 'function';\n  }\n\n  return isSupported;\n}\n\nfunction isCheckable(elem) {\n  var type = elem.type;\n  var nodeName = elem.nodeName;\n  return nodeName && nodeName.toLowerCase() === 'input' && (type === 'checkbox' || type === 'radio');\n}\n\nfunction getTracker(node) {\n  return node._valueTracker;\n}\n\nfunction detachTracker(node) {\n  node._valueTracker = null;\n}\n\nfunction getValueFromNode(node) {\n  var value = '';\n  if (!node) {\n    return value;\n  }\n\n  if (isCheckable(node)) {\n    value = node.checked ? 'true' : 'false';\n  } else {\n    value = node.value;\n  }\n\n  return value;\n}\n\nfunction trackValueOnNode(node) {\n  var valueField = isCheckable(node) ? 'checked' : 'value';\n  var descriptor = Object.getOwnPropertyDescriptor(node.constructor.prototype, valueField);\n\n  var currentValue = '' + node[valueField];\n\n  // if someone has already defined a value or Safari, then bail\n  // and don't track value will cause over reporting of changes,\n  // but it's better then a hard failure\n  // (needed for certain tests that spyOn input values and Safari)\n  if (node.hasOwnProperty(valueField) || typeof descriptor === 'undefined' || typeof descriptor.get !== 'function' || typeof descriptor.set !== 'function') {\n    return;\n  }\n  var get = descriptor.get,\n      set = descriptor.set;\n\n  Object.defineProperty(node, valueField, {\n    configurable: true,\n    get: function () {\n      return get.call(this);\n    },\n    set: function (value) {\n      currentValue = '' + value;\n      set.call(this, value);\n    }\n  });\n  // We could've passed this the first time\n  // but it triggers a bug in IE11 and Edge 14/15.\n  // Calling defineProperty() again should be equivalent.\n  // https://github.com/facebook/react/issues/11768\n  Object.defineProperty(node, valueField, {\n    enumerable: descriptor.enumerable\n  });\n\n  var tracker = {\n    getValue: function () {\n      return currentValue;\n    },\n    setValue: function (value) {\n      currentValue = '' + value;\n    },\n    stopTracking: function () {\n      detachTracker(node);\n      delete node[valueField];\n    }\n  };\n  return tracker;\n}\n\nfunction track(node) {\n  if (getTracker(node)) {\n    return;\n  }\n\n  // TODO: Once it's just Fiber we can move this to node._wrapperState\n  node._valueTracker = trackValueOnNode(node);\n}\n\nfunction updateValueIfChanged(node) {\n  if (!node) {\n    return false;\n  }\n\n  var tracker = getTracker(node);\n  // if there is no tracker at this point it's unlikely\n  // that trying again will succeed\n  if (!tracker) {\n    return true;\n  }\n\n  var lastValue = tracker.getValue();\n  var nextValue = getValueFromNode(node);\n  if (nextValue !== lastValue) {\n    tracker.setValue(nextValue);\n    return true;\n  }\n  return false;\n}\n\nvar ReactSharedInternals = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;\n\n// Prevent newer renderers from RTE when used with older react package versions.\n// Current owner and dispatcher used to share the same ref,\n// but PR #14548 split them out to better support the react-debug-tools package.\nif (!ReactSharedInternals.hasOwnProperty('ReactCurrentDispatcher')) {\n  ReactSharedInternals.ReactCurrentDispatcher = {\n    current: null\n  };\n}\n\nvar BEFORE_SLASH_RE = /^(.*)[\\\\\\/]/;\n\nvar describeComponentFrame = function (name, source, ownerName) {\n  var sourceInfo = '';\n  if (source) {\n    var path = source.fileName;\n    var fileName = path.replace(BEFORE_SLASH_RE, '');\n    {\n      // In DEV, include code for a common special case:\n      // prefer \"folder/index.js\" instead of just \"index.js\".\n      if (/^index\\./.test(fileName)) {\n        var match = path.match(BEFORE_SLASH_RE);\n        if (match) {\n          var pathBeforeSlash = match[1];\n          if (pathBeforeSlash) {\n            var folderName = pathBeforeSlash.replace(BEFORE_SLASH_RE, '');\n            fileName = folderName + '/' + fileName;\n          }\n        }\n      }\n    }\n    sourceInfo = ' (at ' + fileName + ':' + source.lineNumber + ')';\n  } else if (ownerName) {\n    sourceInfo = ' (created by ' + ownerName + ')';\n  }\n  return '\\n    in ' + (name || 'Unknown') + sourceInfo;\n};\n\n// The Symbol used to tag the ReactElement-like types. If there is no native Symbol\n// nor polyfill, then a plain number is used for performance.\nvar hasSymbol = typeof Symbol === 'function' && Symbol.for;\n\nvar REACT_ELEMENT_TYPE = hasSymbol ? Symbol.for('react.element') : 0xeac7;\nvar REACT_PORTAL_TYPE = hasSymbol ? Symbol.for('react.portal') : 0xeaca;\nvar REACT_FRAGMENT_TYPE = hasSymbol ? Symbol.for('react.fragment') : 0xeacb;\nvar REACT_STRICT_MODE_TYPE = hasSymbol ? Symbol.for('react.strict_mode') : 0xeacc;\nvar REACT_PROFILER_TYPE = hasSymbol ? Symbol.for('react.profiler') : 0xead2;\nvar REACT_PROVIDER_TYPE = hasSymbol ? Symbol.for('react.provider') : 0xeacd;\nvar REACT_CONTEXT_TYPE = hasSymbol ? Symbol.for('react.context') : 0xeace;\n\nvar REACT_CONCURRENT_MODE_TYPE = hasSymbol ? Symbol.for('react.concurrent_mode') : 0xeacf;\nvar REACT_FORWARD_REF_TYPE = hasSymbol ? Symbol.for('react.forward_ref') : 0xead0;\nvar REACT_SUSPENSE_TYPE = hasSymbol ? Symbol.for('react.suspense') : 0xead1;\nvar REACT_MEMO_TYPE = hasSymbol ? Symbol.for('react.memo') : 0xead3;\nvar REACT_LAZY_TYPE = hasSymbol ? Symbol.for('react.lazy') : 0xead4;\n\nvar MAYBE_ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator;\nvar FAUX_ITERATOR_SYMBOL = '@@iterator';\n\nfunction getIteratorFn(maybeIterable) {\n  if (maybeIterable === null || typeof maybeIterable !== 'object') {\n    return null;\n  }\n  var maybeIterator = MAYBE_ITERATOR_SYMBOL && maybeIterable[MAYBE_ITERATOR_SYMBOL] || maybeIterable[FAUX_ITERATOR_SYMBOL];\n  if (typeof maybeIterator === 'function') {\n    return maybeIterator;\n  }\n  return null;\n}\n\nvar Pending = 0;\nvar Resolved = 1;\nvar Rejected = 2;\n\nfunction refineResolvedLazyComponent(lazyComponent) {\n  return lazyComponent._status === Resolved ? lazyComponent._result : null;\n}\n\nfunction getWrappedName(outerType, innerType, wrapperName) {\n  var functionName = innerType.displayName || innerType.name || '';\n  return outerType.displayName || (functionName !== '' ? wrapperName + '(' + functionName + ')' : wrapperName);\n}\n\nfunction getComponentName(type) {\n  if (type == null) {\n    // Host root, text node or just invalid type.\n    return null;\n  }\n  {\n    if (typeof type.tag === 'number') {\n      warningWithoutStack$1(false, 'Received an unexpected object in getComponentName(). ' + 'This is likely a bug in React. Please file an issue.');\n    }\n  }\n  if (typeof type === 'function') {\n    return type.displayName || type.name || null;\n  }\n  if (typeof type === 'string') {\n    return type;\n  }\n  switch (type) {\n    case REACT_CONCURRENT_MODE_TYPE:\n      return 'ConcurrentMode';\n    case REACT_FRAGMENT_TYPE:\n      return 'Fragment';\n    case REACT_PORTAL_TYPE:\n      return 'Portal';\n    case REACT_PROFILER_TYPE:\n      return 'Profiler';\n    case REACT_STRICT_MODE_TYPE:\n      return 'StrictMode';\n    case REACT_SUSPENSE_TYPE:\n      return 'Suspense';\n  }\n  if (typeof type === 'object') {\n    switch (type.$$typeof) {\n      case REACT_CONTEXT_TYPE:\n        return 'Context.Consumer';\n      case REACT_PROVIDER_TYPE:\n        return 'Context.Provider';\n      case REACT_FORWARD_REF_TYPE:\n        return getWrappedName(type, type.render, 'ForwardRef');\n      case REACT_MEMO_TYPE:\n        return getComponentName(type.type);\n      case REACT_LAZY_TYPE:\n        {\n          var thenable = type;\n          var resolvedThenable = refineResolvedLazyComponent(thenable);\n          if (resolvedThenable) {\n            return getComponentName(resolvedThenable);\n          }\n        }\n    }\n  }\n  return null;\n}\n\nvar ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame;\n\nfunction describeFiber(fiber) {\n  switch (fiber.tag) {\n    case HostRoot:\n    case HostPortal:\n    case HostText:\n    case Fragment:\n    case ContextProvider:\n    case ContextConsumer:\n      return '';\n    default:\n      var owner = fiber._debugOwner;\n      var source = fiber._debugSource;\n      var name = getComponentName(fiber.type);\n      var ownerName = null;\n      if (owner) {\n        ownerName = getComponentName(owner.type);\n      }\n      return describeComponentFrame(name, source, ownerName);\n  }\n}\n\nfunction getStackByFiberInDevAndProd(workInProgress) {\n  var info = '';\n  var node = workInProgress;\n  do {\n    info += describeFiber(node);\n    node = node.return;\n  } while (node);\n  return info;\n}\n\nvar current = null;\nvar phase = null;\n\nfunction getCurrentFiberOwnerNameInDevOrNull() {\n  {\n    if (current === null) {\n      return null;\n    }\n    var owner = current._debugOwner;\n    if (owner !== null && typeof owner !== 'undefined') {\n      return getComponentName(owner.type);\n    }\n  }\n  return null;\n}\n\nfunction getCurrentFiberStackInDev() {\n  {\n    if (current === null) {\n      return '';\n    }\n    // Safe because if current fiber exists, we are reconciling,\n    // and it is guaranteed to be the work-in-progress version.\n    return getStackByFiberInDevAndProd(current);\n  }\n  return '';\n}\n\nfunction resetCurrentFiber() {\n  {\n    ReactDebugCurrentFrame.getCurrentStack = null;\n    current = null;\n    phase = null;\n  }\n}\n\nfunction setCurrentFiber(fiber) {\n  {\n    ReactDebugCurrentFrame.getCurrentStack = getCurrentFiberStackInDev;\n    current = fiber;\n    phase = null;\n  }\n}\n\nfunction setCurrentPhase(lifeCyclePhase) {\n  {\n    phase = lifeCyclePhase;\n  }\n}\n\n/**\n * Similar to invariant but only logs a warning if the condition is not met.\n * This can be used to log issues in development environments in critical\n * paths. Removing the logging code for production environments will keep the\n * same logic and follow the same code paths.\n */\n\nvar warning = warningWithoutStack$1;\n\n{\n  warning = function (condition, format) {\n    if (condition) {\n      return;\n    }\n    var ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame;\n    var stack = ReactDebugCurrentFrame.getStackAddendum();\n    // eslint-disable-next-line react-internal/warning-and-invariant-args\n\n    for (var _len = arguments.length, args = Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {\n      args[_key - 2] = arguments[_key];\n    }\n\n    warningWithoutStack$1.apply(undefined, [false, format + '%s'].concat(args, [stack]));\n  };\n}\n\nvar warning$1 = warning;\n\n// A reserved attribute.\n// It is handled by React separately and shouldn't be written to the DOM.\nvar RESERVED = 0;\n\n// A simple string attribute.\n// Attributes that aren't in the whitelist are presumed to have this type.\nvar STRING = 1;\n\n// A string attribute that accepts booleans in React. In HTML, these are called\n// \"enumerated\" attributes with \"true\" and \"false\" as possible values.\n// When true, it should be set to a \"true\" string.\n// When false, it should be set to a \"false\" string.\nvar BOOLEANISH_STRING = 2;\n\n// A real boolean attribute.\n// When true, it should be present (set either to an empty string or its name).\n// When false, it should be omitted.\nvar BOOLEAN = 3;\n\n// An attribute that can be used as a flag as well as with a value.\n// When true, it should be present (set either to an empty string or its name).\n// When false, it should be omitted.\n// For any other value, should be present with that value.\nvar OVERLOADED_BOOLEAN = 4;\n\n// An attribute that must be numeric or parse as a numeric.\n// When falsy, it should be removed.\nvar NUMERIC = 5;\n\n// An attribute that must be positive numeric or parse as a positive numeric.\n// When falsy, it should be removed.\nvar POSITIVE_NUMERIC = 6;\n\n/* eslint-disable max-len */\nvar ATTRIBUTE_NAME_START_CHAR = ':A-Z_a-z\\\\u00C0-\\\\u00D6\\\\u00D8-\\\\u00F6\\\\u00F8-\\\\u02FF\\\\u0370-\\\\u037D\\\\u037F-\\\\u1FFF\\\\u200C-\\\\u200D\\\\u2070-\\\\u218F\\\\u2C00-\\\\u2FEF\\\\u3001-\\\\uD7FF\\\\uF900-\\\\uFDCF\\\\uFDF0-\\\\uFFFD';\n/* eslint-enable max-len */\nvar ATTRIBUTE_NAME_CHAR = ATTRIBUTE_NAME_START_CHAR + '\\\\-.0-9\\\\u00B7\\\\u0300-\\\\u036F\\\\u203F-\\\\u2040';\n\n\nvar ROOT_ATTRIBUTE_NAME = 'data-reactroot';\nvar VALID_ATTRIBUTE_NAME_REGEX = new RegExp('^[' + ATTRIBUTE_NAME_START_CHAR + '][' + ATTRIBUTE_NAME_CHAR + ']*$');\n\nvar hasOwnProperty = Object.prototype.hasOwnProperty;\nvar illegalAttributeNameCache = {};\nvar validatedAttributeNameCache = {};\n\nfunction isAttributeNameSafe(attributeName) {\n  if (hasOwnProperty.call(validatedAttributeNameCache, attributeName)) {\n    return true;\n  }\n  if (hasOwnProperty.call(illegalAttributeNameCache, attributeName)) {\n    return false;\n  }\n  if (VALID_ATTRIBUTE_NAME_REGEX.test(attributeName)) {\n    validatedAttributeNameCache[attributeName] = true;\n    return true;\n  }\n  illegalAttributeNameCache[attributeName] = true;\n  {\n    warning$1(false, 'Invalid attribute name: `%s`', attributeName);\n  }\n  return false;\n}\n\nfunction shouldIgnoreAttribute(name, propertyInfo, isCustomComponentTag) {\n  if (propertyInfo !== null) {\n    return propertyInfo.type === RESERVED;\n  }\n  if (isCustomComponentTag) {\n    return false;\n  }\n  if (name.length > 2 && (name[0] === 'o' || name[0] === 'O') && (name[1] === 'n' || name[1] === 'N')) {\n    return true;\n  }\n  return false;\n}\n\nfunction shouldRemoveAttributeWithWarning(name, value, propertyInfo, isCustomComponentTag) {\n  if (propertyInfo !== null && propertyInfo.type === RESERVED) {\n    return false;\n  }\n  switch (typeof value) {\n    case 'function':\n    // $FlowIssue symbol is perfectly valid here\n    case 'symbol':\n      // eslint-disable-line\n      return true;\n    case 'boolean':\n      {\n        if (isCustomComponentTag) {\n          return false;\n        }\n        if (propertyInfo !== null) {\n          return !propertyInfo.acceptsBooleans;\n        } else {\n          var prefix = name.toLowerCase().slice(0, 5);\n          return prefix !== 'data-' && prefix !== 'aria-';\n        }\n      }\n    default:\n      return false;\n  }\n}\n\nfunction shouldRemoveAttribute(name, value, propertyInfo, isCustomComponentTag) {\n  if (value === null || typeof value === 'undefined') {\n    return true;\n  }\n  if (shouldRemoveAttributeWithWarning(name, value, propertyInfo, isCustomComponentTag)) {\n    return true;\n  }\n  if (isCustomComponentTag) {\n    return false;\n  }\n  if (propertyInfo !== null) {\n    switch (propertyInfo.type) {\n      case BOOLEAN:\n        return !value;\n      case OVERLOADED_BOOLEAN:\n        return value === false;\n      case NUMERIC:\n        return isNaN(value);\n      case POSITIVE_NUMERIC:\n        return isNaN(value) || value < 1;\n    }\n  }\n  return false;\n}\n\nfunction getPropertyInfo(name) {\n  return properties.hasOwnProperty(name) ? properties[name] : null;\n}\n\nfunction PropertyInfoRecord(name, type, mustUseProperty, attributeName, attributeNamespace) {\n  this.acceptsBooleans = type === BOOLEANISH_STRING || type === BOOLEAN || type === OVERLOADED_BOOLEAN;\n  this.attributeName = attributeName;\n  this.attributeNamespace = attributeNamespace;\n  this.mustUseProperty = mustUseProperty;\n  this.propertyName = name;\n  this.type = type;\n}\n\n// When adding attributes to this list, be sure to also add them to\n// the `possibleStandardNames` module to ensure casing and incorrect\n// name warnings.\nvar properties = {};\n\n// These props are reserved by React. They shouldn't be written to the DOM.\n['children', 'dangerouslySetInnerHTML',\n// TODO: This prevents the assignment of defaultValue to regular\n// elements (not just inputs). Now that ReactDOMInput assigns to the\n// defaultValue property -- do we need this?\n'defaultValue', 'defaultChecked', 'innerHTML', 'suppressContentEditableWarning', 'suppressHydrationWarning', 'style'].forEach(function (name) {\n  properties[name] = new PropertyInfoRecord(name, RESERVED, false, // mustUseProperty\n  name, // attributeName\n  null);\n} // attributeNamespace\n);\n\n// A few React string attributes have a different name.\n// This is a mapping from React prop names to the attribute names.\n[['acceptCharset', 'accept-charset'], ['className', 'class'], ['htmlFor', 'for'], ['httpEquiv', 'http-equiv']].forEach(function (_ref) {\n  var name = _ref[0],\n      attributeName = _ref[1];\n\n  properties[name] = new PropertyInfoRecord(name, STRING, false, // mustUseProperty\n  attributeName, // attributeName\n  null);\n} // attributeNamespace\n);\n\n// These are \"enumerated\" HTML attributes that accept \"true\" and \"false\".\n// In React, we let users pass `true` and `false` even though technically\n// these aren't boolean attributes (they are coerced to strings).\n['contentEditable', 'draggable', 'spellCheck', 'value'].forEach(function (name) {\n  properties[name] = new PropertyInfoRecord(name, BOOLEANISH_STRING, false, // mustUseProperty\n  name.toLowerCase(), // attributeName\n  null);\n} // attributeNamespace\n);\n\n// These are \"enumerated\" SVG attributes that accept \"true\" and \"false\".\n// In React, we let users pass `true` and `false` even though technically\n// these aren't boolean attributes (they are coerced to strings).\n// Since these are SVG attributes, their attribute names are case-sensitive.\n['autoReverse', 'externalResourcesRequired', 'focusable', 'preserveAlpha'].forEach(function (name) {\n  properties[name] = new PropertyInfoRecord(name, BOOLEANISH_STRING, false, // mustUseProperty\n  name, // attributeName\n  null);\n} // attributeNamespace\n);\n\n// These are HTML boolean attributes.\n['allowFullScreen', 'async',\n// Note: there is a special case that prevents it from being written to the DOM\n// on the client side because the browsers are inconsistent. Instead we call focus().\n'autoFocus', 'autoPlay', 'controls', 'default', 'defer', 'disabled', 'formNoValidate', 'hidden', 'loop', 'noModule', 'noValidate', 'open', 'playsInline', 'readOnly', 'required', 'reversed', 'scoped', 'seamless',\n// Microdata\n'itemScope'].forEach(function (name) {\n  properties[name] = new PropertyInfoRecord(name, BOOLEAN, false, // mustUseProperty\n  name.toLowerCase(), // attributeName\n  null);\n} // attributeNamespace\n);\n\n// These are the few React props that we set as DOM properties\n// rather than attributes. These are all booleans.\n['checked',\n// Note: `option.selected` is not updated if `select.multiple` is\n// disabled with `removeAttribute`. We have special logic for handling this.\n'multiple', 'muted', 'selected'].forEach(function (name) {\n  properties[name] = new PropertyInfoRecord(name, BOOLEAN, true, // mustUseProperty\n  name, // attributeName\n  null);\n} // attributeNamespace\n);\n\n// These are HTML attributes that are \"overloaded booleans\": they behave like\n// booleans, but can also accept a string value.\n['capture', 'download'].forEach(function (name) {\n  properties[name] = new PropertyInfoRecord(name, OVERLOADED_BOOLEAN, false, // mustUseProperty\n  name, // attributeName\n  null);\n} // attributeNamespace\n);\n\n// These are HTML attributes that must be positive numbers.\n['cols', 'rows', 'size', 'span'].forEach(function (name) {\n  properties[name] = new PropertyInfoRecord(name, POSITIVE_NUMERIC, false, // mustUseProperty\n  name, // attributeName\n  null);\n} // attributeNamespace\n);\n\n// These are HTML attributes that must be numbers.\n['rowSpan', 'start'].forEach(function (name) {\n  properties[name] = new PropertyInfoRecord(name, NUMERIC, false, // mustUseProperty\n  name.toLowerCase(), // attributeName\n  null);\n} // attributeNamespace\n);\n\nvar CAMELIZE = /[\\-\\:]([a-z])/g;\nvar capitalize = function (token) {\n  return token[1].toUpperCase();\n};\n\n// This is a list of all SVG attributes that need special casing, namespacing,\n// or boolean value assignment. Regular attributes that just accept strings\n// and have the same names are omitted, just like in the HTML whitelist.\n// Some of these attributes can be hard to find. This list was created by\n// scrapping the MDN documentation.\n['accent-height', 'alignment-baseline', 'arabic-form', 'baseline-shift', 'cap-height', 'clip-path', 'clip-rule', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'dominant-baseline', 'enable-background', 'fill-opacity', 'fill-rule', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'glyph-name', 'glyph-orientation-horizontal', 'glyph-orientation-vertical', 'horiz-adv-x', 'horiz-origin-x', 'image-rendering', 'letter-spacing', 'lighting-color', 'marker-end', 'marker-mid', 'marker-start', 'overline-position', 'overline-thickness', 'paint-order', 'panose-1', 'pointer-events', 'rendering-intent', 'shape-rendering', 'stop-color', 'stop-opacity', 'strikethrough-position', 'strikethrough-thickness', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'text-anchor', 'text-decoration', 'text-rendering', 'underline-position', 'underline-thickness', 'unicode-bidi', 'unicode-range', 'units-per-em', 'v-alphabetic', 'v-hanging', 'v-ideographic', 'v-mathematical', 'vector-effect', 'vert-adv-y', 'vert-origin-x', 'vert-origin-y', 'word-spacing', 'writing-mode', 'xmlns:xlink', 'x-height'].forEach(function (attributeName) {\n  var name = attributeName.replace(CAMELIZE, capitalize);\n  properties[name] = new PropertyInfoRecord(name, STRING, false, // mustUseProperty\n  attributeName, null);\n} // attributeNamespace\n);\n\n// String SVG attributes with the xlink namespace.\n['xlink:actuate', 'xlink:arcrole', 'xlink:href', 'xlink:role', 'xlink:show', 'xlink:title', 'xlink:type'].forEach(function (attributeName) {\n  var name = attributeName.replace(CAMELIZE, capitalize);\n  properties[name] = new PropertyInfoRecord(name, STRING, false, // mustUseProperty\n  attributeName, 'http://www.w3.org/1999/xlink');\n});\n\n// String SVG attributes with the xml namespace.\n['xml:base', 'xml:lang', 'xml:space'].forEach(function (attributeName) {\n  var name = attributeName.replace(CAMELIZE, capitalize);\n  properties[name] = new PropertyInfoRecord(name, STRING, false, // mustUseProperty\n  attributeName, 'http://www.w3.org/XML/1998/namespace');\n});\n\n// These attribute exists both in HTML and SVG.\n// The attribute name is case-sensitive in SVG so we can't just use\n// the React name like we do for attributes that exist only in HTML.\n['tabIndex', 'crossOrigin'].forEach(function (attributeName) {\n  properties[attributeName] = new PropertyInfoRecord(attributeName, STRING, false, // mustUseProperty\n  attributeName.toLowerCase(), // attributeName\n  null);\n} // attributeNamespace\n);\n\n/**\n * Get the value for a property on a node. Only used in DEV for SSR validation.\n * The \"expected\" argument is used as a hint of what the expected value is.\n * Some properties have multiple equivalent values.\n */\nfunction getValueForProperty(node, name, expected, propertyInfo) {\n  {\n    if (propertyInfo.mustUseProperty) {\n      var propertyName = propertyInfo.propertyName;\n\n      return node[propertyName];\n    } else {\n      var attributeName = propertyInfo.attributeName;\n\n      var stringValue = null;\n\n      if (propertyInfo.type === OVERLOADED_BOOLEAN) {\n        if (node.hasAttribute(attributeName)) {\n          var value = node.getAttribute(attributeName);\n          if (value === '') {\n            return true;\n          }\n          if (shouldRemoveAttribute(name, expected, propertyInfo, false)) {\n            return value;\n          }\n          if (value === '' + expected) {\n            return expected;\n          }\n          return value;\n        }\n      } else if (node.hasAttribute(attributeName)) {\n        if (shouldRemoveAttribute(name, expected, propertyInfo, false)) {\n          // We had an attribute but shouldn't have had one, so read it\n          // for the error message.\n          return node.getAttribute(attributeName);\n        }\n        if (propertyInfo.type === BOOLEAN) {\n          // If this was a boolean, it doesn't matter what the value is\n          // the fact that we have it is the same as the expected.\n          return expected;\n        }\n        // Even if this property uses a namespace we use getAttribute\n        // because we assume its namespaced name is the same as our config.\n        // To use getAttributeNS we need the local name which we don't have\n        // in our config atm.\n        stringValue = node.getAttribute(attributeName);\n      }\n\n      if (shouldRemoveAttribute(name, expected, propertyInfo, false)) {\n        return stringValue === null ? expected : stringValue;\n      } else if (stringValue === '' + expected) {\n        return expected;\n      } else {\n        return stringValue;\n      }\n    }\n  }\n}\n\n/**\n * Get the value for a attribute on a node. Only used in DEV for SSR validation.\n * The third argument is used as a hint of what the expected value is. Some\n * attributes have multiple equivalent values.\n */\nfunction getValueForAttribute(node, name, expected) {\n  {\n    if (!isAttributeNameSafe(name)) {\n      return;\n    }\n    if (!node.hasAttribute(name)) {\n      return expected === undefined ? undefined : null;\n    }\n    var value = node.getAttribute(name);\n    if (value === '' + expected) {\n      return expected;\n    }\n    return value;\n  }\n}\n\n/**\n * Sets the value for a property on a node.\n *\n * @param {DOMElement} node\n * @param {string} name\n * @param {*} value\n */\nfunction setValueForProperty(node, name, value, isCustomComponentTag) {\n  var propertyInfo = getPropertyInfo(name);\n  if (shouldIgnoreAttribute(name, propertyInfo, isCustomComponentTag)) {\n    return;\n  }\n  if (shouldRemoveAttribute(name, value, propertyInfo, isCustomComponentTag)) {\n    value = null;\n  }\n  // If the prop isn't in the special list, treat it as a simple attribute.\n  if (isCustomComponentTag || propertyInfo === null) {\n    if (isAttributeNameSafe(name)) {\n      var _attributeName = name;\n      if (value === null) {\n        node.removeAttribute(_attributeName);\n      } else {\n        node.setAttribute(_attributeName, '' + value);\n      }\n    }\n    return;\n  }\n  var mustUseProperty = propertyInfo.mustUseProperty;\n\n  if (mustUseProperty) {\n    var propertyName = propertyInfo.propertyName;\n\n    if (value === null) {\n      var type = propertyInfo.type;\n\n      node[propertyName] = type === BOOLEAN ? false : '';\n    } else {\n      // Contrary to `setAttribute`, object properties are properly\n      // `toString`ed by IE8/9.\n      node[propertyName] = value;\n    }\n    return;\n  }\n  // The rest are treated as attributes with special cases.\n  var attributeName = propertyInfo.attributeName,\n      attributeNamespace = propertyInfo.attributeNamespace;\n\n  if (value === null) {\n    node.removeAttribute(attributeName);\n  } else {\n    var _type = propertyInfo.type;\n\n    var attributeValue = void 0;\n    if (_type === BOOLEAN || _type === OVERLOADED_BOOLEAN && value === true) {\n      attributeValue = '';\n    } else {\n      // `setAttribute` with objects becomes only `[object]` in IE8/9,\n      // ('' + value) makes it output the correct toString()-value.\n      attributeValue = '' + value;\n    }\n    if (attributeNamespace) {\n      node.setAttributeNS(attributeNamespace, attributeName, attributeValue);\n    } else {\n      node.setAttribute(attributeName, attributeValue);\n    }\n  }\n}\n\n// Flow does not allow string concatenation of most non-string types. To work\n// around this limitation, we use an opaque type that can only be obtained by\n// passing the value through getToStringValue first.\nfunction toString(value) {\n  return '' + value;\n}\n\nfunction getToStringValue(value) {\n  switch (typeof value) {\n    case 'boolean':\n    case 'number':\n    case 'object':\n    case 'string':\n    case 'undefined':\n      return value;\n    default:\n      // function, symbol are assigned as empty strings\n      return '';\n  }\n}\n\n/**\n * Copyright (c) 2013-present, Facebook, Inc.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n\n\nvar ReactPropTypesSecret$1 = 'SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED';\n\nvar ReactPropTypesSecret_1 = ReactPropTypesSecret$1;\n\n/**\n * Copyright (c) 2013-present, Facebook, Inc.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n\n\nvar printWarning = function() {};\n\n{\n  var ReactPropTypesSecret = ReactPropTypesSecret_1;\n  var loggedTypeFailures = {};\n\n  printWarning = function(text) {\n    var message = 'Warning: ' + text;\n    if (typeof console !== 'undefined') {\n      console.error(message);\n    }\n    try {\n      // --- Welcome to debugging React ---\n      // This error was thrown as a convenience so that you can use this stack\n      // to find the callsite that caused this warning to fire.\n      throw new Error(message);\n    } catch (x) {}\n  };\n}\n\n/**\n * Assert that the values match with the type specs.\n * Error messages are memorized and will only be shown once.\n *\n * @param {object} typeSpecs Map of name to a ReactPropType\n * @param {object} values Runtime values that need to be type-checked\n * @param {string} location e.g. \"prop\", \"context\", \"child context\"\n * @param {string} componentName Name of the component for error messages.\n * @param {?Function} getStack Returns the component stack.\n * @private\n */\nfunction checkPropTypes(typeSpecs, values, location, componentName, getStack) {\n  {\n    for (var typeSpecName in typeSpecs) {\n      if (typeSpecs.hasOwnProperty(typeSpecName)) {\n        var error;\n        // Prop type validation may throw. In case they do, we don't want to\n        // fail the render phase where it didn't fail before. So we log it.\n        // After these have been cleaned up, we'll let them throw.\n        try {\n          // This is intentionally an invariant that gets caught. It's the same\n          // behavior as without this statement except with a better message.\n          if (typeof typeSpecs[typeSpecName] !== 'function') {\n            var err = Error(\n              (componentName || 'React class') + ': ' + location + ' type `' + typeSpecName + '` is invalid; ' +\n              'it must be a function, usually from the `prop-types` package, but received `' + typeof typeSpecs[typeSpecName] + '`.'\n            );\n            err.name = 'Invariant Violation';\n            throw err;\n          }\n          error = typeSpecs[typeSpecName](values, typeSpecName, componentName, location, null, ReactPropTypesSecret);\n        } catch (ex) {\n          error = ex;\n        }\n        if (error && !(error instanceof Error)) {\n          printWarning(\n            (componentName || 'React class') + ': type specification of ' +\n            location + ' `' + typeSpecName + '` is invalid; the type checker ' +\n            'function must return `null` or an `Error` but returned a ' + typeof error + '. ' +\n            'You may have forgotten to pass an argument to the type checker ' +\n            'creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and ' +\n            'shape all require an argument).'\n          );\n\n        }\n        if (error instanceof Error && !(error.message in loggedTypeFailures)) {\n          // Only monitor this failure once because there tends to be a lot of the\n          // same error.\n          loggedTypeFailures[error.message] = true;\n\n          var stack = getStack ? getStack() : '';\n\n          printWarning(\n            'Failed ' + location + ' type: ' + error.message + (stack != null ? stack : '')\n          );\n        }\n      }\n    }\n  }\n}\n\nvar checkPropTypes_1 = checkPropTypes;\n\nvar ReactDebugCurrentFrame$1 = null;\n\nvar ReactControlledValuePropTypes = {\n  checkPropTypes: null\n};\n\n{\n  ReactDebugCurrentFrame$1 = ReactSharedInternals.ReactDebugCurrentFrame;\n\n  var hasReadOnlyValue = {\n    button: true,\n    checkbox: true,\n    image: true,\n    hidden: true,\n    radio: true,\n    reset: true,\n    submit: true\n  };\n\n  var propTypes = {\n    value: function (props, propName, componentName) {\n      if (hasReadOnlyValue[props.type] || props.onChange || props.readOnly || props.disabled || props[propName] == null) {\n        return null;\n      }\n      return new Error('You provided a `value` prop to a form field without an ' + '`onChange` handler. This will render a read-only field. If ' + 'the field should be mutable use `defaultValue`. Otherwise, ' + 'set either `onChange` or `readOnly`.');\n    },\n    checked: function (props, propName, componentName) {\n      if (props.onChange || props.readOnly || props.disabled || props[propName] == null) {\n        return null;\n      }\n      return new Error('You provided a `checked` prop to a form field without an ' + '`onChange` handler. This will render a read-only field. If ' + 'the field should be mutable use `defaultChecked`. Otherwise, ' + 'set either `onChange` or `readOnly`.');\n    }\n  };\n\n  /**\n   * Provide a linked `value` attribute for controlled forms. You should not use\n   * this outside of the ReactDOM controlled form components.\n   */\n  ReactControlledValuePropTypes.checkPropTypes = function (tagName, props) {\n    checkPropTypes_1(propTypes, props, 'prop', tagName, ReactDebugCurrentFrame$1.getStackAddendum);\n  };\n}\n\nvar enableUserTimingAPI = true;\n\n// Helps identify side effects in begin-phase lifecycle hooks and setState reducers:\nvar debugRenderPhaseSideEffects = false;\n\n// In some cases, StrictMode should also double-render lifecycles.\n// This can be confusing for tests though,\n// And it can be bad for performance in production.\n// This feature flag can be used to control the behavior:\nvar debugRenderPhaseSideEffectsForStrictMode = true;\n\n// To preserve the \"Pause on caught exceptions\" behavior of the debugger, we\n// replay the begin phase of a failed component inside invokeGuardedCallback.\nvar replayFailedUnitOfWorkWithInvokeGuardedCallback = true;\n\n// Warn about deprecated, async-unsafe lifecycles; relates to RFC #6:\nvar warnAboutDeprecatedLifecycles = false;\n\n// Gather advanced timing metrics for Profiler subtrees.\nvar enableProfilerTimer = true;\n\n// Trace which interactions trigger each commit.\nvar enableSchedulerTracing = true;\n\n// Only used in www builds.\nvar enableSuspenseServerRenderer = false; // TODO: true? Here it might just be false.\n\n// Only used in www builds.\n\n\n// Only used in www builds.\n\n\n// React Fire: prevent the value and checked attributes from syncing\n// with their related DOM properties\nvar disableInputAttributeSyncing = false;\n\n// These APIs will no longer be \"unstable\" in the upcoming 16.7 release,\n// Control this behavior with a flag to support 16.6 minor releases in the meanwhile.\nvar enableStableConcurrentModeAPIs = false;\n\nvar warnAboutShorthandPropertyCollision = false;\n\n// TODO: direct imports like some-package/src/* are bad. Fix me.\nvar didWarnValueDefaultValue = false;\nvar didWarnCheckedDefaultChecked = false;\nvar didWarnControlledToUncontrolled = false;\nvar didWarnUncontrolledToControlled = false;\n\nfunction isControlled(props) {\n  var usesChecked = props.type === 'checkbox' || props.type === 'radio';\n  return usesChecked ? props.checked != null : props.value != null;\n}\n\n/**\n * Implements an <input> host component that allows setting these optional\n * props: `checked`, `value`, `defaultChecked`, and `defaultValue`.\n *\n * If `checked` or `value` are not supplied (or null/undefined), user actions\n * that affect the checked state or value will trigger updates to the element.\n *\n * If they are supplied (and not null/undefined), the rendered element will not\n * trigger updates to the element. Instead, the props must change in order for\n * the rendered element to be updated.\n *\n * The rendered element will be initialized as unchecked (or `defaultChecked`)\n * with an empty value (or `defaultValue`).\n *\n * See http://www.w3.org/TR/2012/WD-html5-20121025/the-input-element.html\n */\n\nfunction getHostProps(element, props) {\n  var node = element;\n  var checked = props.checked;\n\n  var hostProps = _assign({}, props, {\n    defaultChecked: undefined,\n    defaultValue: undefined,\n    value: undefined,\n    checked: checked != null ? checked : node._wrapperState.initialChecked\n  });\n\n  return hostProps;\n}\n\nfunction initWrapperState(element, props) {\n  {\n    ReactControlledValuePropTypes.checkPropTypes('input', props);\n\n    if (props.checked !== undefined && props.defaultChecked !== undefined && !didWarnCheckedDefaultChecked) {\n      warning$1(false, '%s contains an input of type %s with both checked and defaultChecked props. ' + 'Input elements must be either controlled or uncontrolled ' + '(specify either the checked prop, or the defaultChecked prop, but not ' + 'both). Decide between using a controlled or uncontrolled input ' + 'element and remove one of these props. More info: ' + 'https://fb.me/react-controlled-components', getCurrentFiberOwnerNameInDevOrNull() || 'A component', props.type);\n      didWarnCheckedDefaultChecked = true;\n    }\n    if (props.value !== undefined && props.defaultValue !== undefined && !didWarnValueDefaultValue) {\n      warning$1(false, '%s contains an input of type %s with both value and defaultValue props. ' + 'Input elements must be either controlled or uncontrolled ' + '(specify either the value prop, or the defaultValue prop, but not ' + 'both). Decide between using a controlled or uncontrolled input ' + 'element and remove one of these props. More info: ' + 'https://fb.me/react-controlled-components', getCurrentFiberOwnerNameInDevOrNull() || 'A component', props.type);\n      didWarnValueDefaultValue = true;\n    }\n  }\n\n  var node = element;\n  var defaultValue = props.defaultValue == null ? '' : props.defaultValue;\n\n  node._wrapperState = {\n    initialChecked: props.checked != null ? props.checked : props.defaultChecked,\n    initialValue: getToStringValue(props.value != null ? props.value : defaultValue),\n    controlled: isControlled(props)\n  };\n}\n\nfunction updateChecked(element, props) {\n  var node = element;\n  var checked = props.checked;\n  if (checked != null) {\n    setValueForProperty(node, 'checked', checked, false);\n  }\n}\n\nfunction updateWrapper(element, props) {\n  var node = element;\n  {\n    var _controlled = isControlled(props);\n\n    if (!node._wrapperState.controlled && _controlled && !didWarnUncontrolledToControlled) {\n      warning$1(false, 'A component is changing an uncontrolled input of type %s to be controlled. ' + 'Input elements should not switch from uncontrolled to controlled (or vice versa). ' + 'Decide between using a controlled or uncontrolled input ' + 'element for the lifetime of the component. More info: https://fb.me/react-controlled-components', props.type);\n      didWarnUncontrolledToControlled = true;\n    }\n    if (node._wrapperState.controlled && !_controlled && !didWarnControlledToUncontrolled) {\n      warning$1(false, 'A component is changing a controlled input of type %s to be uncontrolled. ' + 'Input elements should not switch from controlled to uncontrolled (or vice versa). ' + 'Decide between using a controlled or uncontrolled input ' + 'element for the lifetime of the component. More info: https://fb.me/react-controlled-components', props.type);\n      didWarnControlledToUncontrolled = true;\n    }\n  }\n\n  updateChecked(element, props);\n\n  var value = getToStringValue(props.value);\n  var type = props.type;\n\n  if (value != null) {\n    if (type === 'number') {\n      if (value === 0 && node.value === '' ||\n      // We explicitly want to coerce to number here if possible.\n      // eslint-disable-next-line\n      node.value != value) {\n        node.value = toString(value);\n      }\n    } else if (node.value !== toString(value)) {\n      node.value = toString(value);\n    }\n  } else if (type === 'submit' || type === 'reset') {\n    // Submit/reset inputs need the attribute removed completely to avoid\n    // blank-text buttons.\n    node.removeAttribute('value');\n    return;\n  }\n\n  if (disableInputAttributeSyncing) {\n    // When not syncing the value attribute, React only assigns a new value\n    // whenever the defaultValue React prop has changed. When not present,\n    // React does nothing\n    if (props.hasOwnProperty('defaultValue')) {\n      setDefaultValue(node, props.type, getToStringValue(props.defaultValue));\n    }\n  } else {\n    // When syncing the value attribute, the value comes from a cascade of\n    // properties:\n    //  1. The value React property\n    //  2. The defaultValue React property\n    //  3. Otherwise there should be no change\n    if (props.hasOwnProperty('value')) {\n      setDefaultValue(node, props.type, value);\n    } else if (props.hasOwnProperty('defaultValue')) {\n      setDefaultValue(node, props.type, getToStringValue(props.defaultValue));\n    }\n  }\n\n  if (disableInputAttributeSyncing) {\n    // When not syncing the checked attribute, the attribute is directly\n    // controllable from the defaultValue React property. It needs to be\n    // updated as new props come in.\n    if (props.defaultChecked == null) {\n      node.removeAttribute('checked');\n    } else {\n      node.defaultChecked = !!props.defaultChecked;\n    }\n  } else {\n    // When syncing the checked attribute, it only changes when it needs\n    // to be removed, such as transitioning from a checkbox into a text input\n    if (props.checked == null && props.defaultChecked != null) {\n      node.defaultChecked = !!props.defaultChecked;\n    }\n  }\n}\n\nfunction postMountWrapper(element, props, isHydrating) {\n  var node = element;\n\n  // Do not assign value if it is already set. This prevents user text input\n  // from being lost during SSR hydration.\n  if (props.hasOwnProperty('value') || props.hasOwnProperty('defaultValue')) {\n    var type = props.type;\n    var isButton = type === 'submit' || type === 'reset';\n\n    // Avoid setting value attribute on submit/reset inputs as it overrides the\n    // default value provided by the browser. See: #12872\n    if (isButton && (props.value === undefined || props.value === null)) {\n      return;\n    }\n\n    var _initialValue = toString(node._wrapperState.initialValue);\n\n    // Do not assign value if it is already set. This prevents user text input\n    // from being lost during SSR hydration.\n    if (!isHydrating) {\n      if (disableInputAttributeSyncing) {\n        var value = getToStringValue(props.value);\n\n        // When not syncing the value attribute, the value property points\n        // directly to the React prop. Only assign it if it exists.\n        if (value != null) {\n          // Always assign on buttons so that it is possible to assign an\n          // empty string to clear button text.\n          //\n          // Otherwise, do not re-assign the value property if is empty. This\n          // potentially avoids a DOM write and prevents Firefox (~60.0.1) from\n          // prematurely marking required inputs as invalid. Equality is compared\n          // to the current value in case the browser provided value is not an\n          // empty string.\n          if (isButton || value !== node.value) {\n            node.value = toString(value);\n          }\n        }\n      } else {\n        // When syncing the value attribute, the value property should use\n        // the wrapperState._initialValue property. This uses:\n        //\n        //   1. The value React property when present\n        //   2. The defaultValue React property when present\n        //   3. An empty string\n        if (_initialValue !== node.value) {\n          node.value = _initialValue;\n        }\n      }\n    }\n\n    if (disableInputAttributeSyncing) {\n      // When not syncing the value attribute, assign the value attribute\n      // directly from the defaultValue React property (when present)\n      var defaultValue = getToStringValue(props.defaultValue);\n      if (defaultValue != null) {\n        node.defaultValue = toString(defaultValue);\n      }\n    } else {\n      // Otherwise, the value attribute is synchronized to the property,\n      // so we assign defaultValue to the same thing as the value property\n      // assignment step above.\n      node.defaultValue = _initialValue;\n    }\n  }\n\n  // Normally, we'd just do `node.checked = node.checked` upon initial mount, less this bug\n  // this is needed to work around a chrome bug where setting defaultChecked\n  // will sometimes influence the value of checked (even after detachment).\n  // Reference: https://bugs.chromium.org/p/chromium/issues/detail?id=608416\n  // We need to temporarily unset name to avoid disrupting radio button groups.\n  var name = node.name;\n  if (name !== '') {\n    node.name = '';\n  }\n\n  if (disableInputAttributeSyncing) {\n    // When not syncing the checked attribute, the checked property\n    // never gets assigned. It must be manually set. We don't want\n    // to do this when hydrating so that existing user input isn't\n    // modified\n    if (!isHydrating) {\n      updateChecked(element, props);\n    }\n\n    // Only assign the checked attribute if it is defined. This saves\n    // a DOM write when controlling the checked attribute isn't needed\n    // (text inputs, submit/reset)\n    if (props.hasOwnProperty('defaultChecked')) {\n      node.defaultChecked = !node.defaultChecked;\n      node.defaultChecked = !!props.defaultChecked;\n    }\n  } else {\n    // When syncing the checked attribute, both the checked property and\n    // attribute are assigned at the same time using defaultChecked. This uses:\n    //\n    //   1. The checked React property when present\n    //   2. The defaultChecked React property when present\n    //   3. Otherwise, false\n    node.defaultChecked = !node.defaultChecked;\n    node.defaultChecked = !!node._wrapperState.initialChecked;\n  }\n\n  if (name !== '') {\n    node.name = name;\n  }\n}\n\nfunction restoreControlledState(element, props) {\n  var node = element;\n  updateWrapper(node, props);\n  updateNamedCousins(node, props);\n}\n\nfunction updateNamedCousins(rootNode, props) {\n  var name = props.name;\n  if (props.type === 'radio' && name != null) {\n    var queryRoot = rootNode;\n\n    while (queryRoot.parentNode) {\n      queryRoot = queryRoot.parentNode;\n    }\n\n    // If `rootNode.form` was non-null, then we could try `form.elements`,\n    // but that sometimes behaves strangely in IE8. We could also try using\n    // `form.getElementsByName`, but that will only return direct children\n    // and won't include inputs that use the HTML5 `form=` attribute. Since\n    // the input might not even be in a form. It might not even be in the\n    // document. Let's just use the local `querySelectorAll` to ensure we don't\n    // miss anything.\n    var group = queryRoot.querySelectorAll('input[name=' + JSON.stringify('' + name) + '][type=\"radio\"]');\n\n    for (var i = 0; i < group.length; i++) {\n      var otherNode = group[i];\n      if (otherNode === rootNode || otherNode.form !== rootNode.form) {\n        continue;\n      }\n      // This will throw if radio buttons rendered by different copies of React\n      // and the same name are rendered into the same form (same as #1939).\n      // That's probably okay; we don't support it just as we don't support\n      // mixing React radio buttons with non-React ones.\n      var otherProps = getFiberCurrentPropsFromNode$1(otherNode);\n      !otherProps ? invariant(false, 'ReactDOMInput: Mixing React and non-React radio inputs with the same `name` is not supported.') : void 0;\n\n      // We need update the tracked value on the named cousin since the value\n      // was changed but the input saw no event or value set\n      updateValueIfChanged(otherNode);\n\n      // If this is a controlled radio button group, forcing the input that\n      // was previously checked to update will cause it to be come re-checked\n      // as appropriate.\n      updateWrapper(otherNode, otherProps);\n    }\n  }\n}\n\n// In Chrome, assigning defaultValue to certain input types triggers input validation.\n// For number inputs, the display value loses trailing decimal points. For email inputs,\n// Chrome raises \"The specified value <x> is not a valid email address\".\n//\n// Here we check to see if the defaultValue has actually changed, avoiding these problems\n// when the user is inputting text\n//\n// https://github.com/facebook/react/issues/7253\nfunction setDefaultValue(node, type, value) {\n  if (\n  // Focused number inputs synchronize on blur. See ChangeEventPlugin.js\n  type !== 'number' || node.ownerDocument.activeElement !== node) {\n    if (value == null) {\n      node.defaultValue = toString(node._wrapperState.initialValue);\n    } else if (node.defaultValue !== toString(value)) {\n      node.defaultValue = toString(value);\n    }\n  }\n}\n\nvar eventTypes$1 = {\n  change: {\n    phasedRegistrationNames: {\n      bubbled: 'onChange',\n      captured: 'onChangeCapture'\n    },\n    dependencies: [TOP_BLUR, TOP_CHANGE, TOP_CLICK, TOP_FOCUS, TOP_INPUT, TOP_KEY_DOWN, TOP_KEY_UP, TOP_SELECTION_CHANGE]\n  }\n};\n\nfunction createAndAccumulateChangeEvent(inst, nativeEvent, target) {\n  var event = SyntheticEvent.getPooled(eventTypes$1.change, inst, nativeEvent, target);\n  event.type = 'change';\n  // Flag this event loop as needing state restore.\n  enqueueStateRestore(target);\n  accumulateTwoPhaseDispatches(event);\n  return event;\n}\n/**\n * For IE shims\n */\nvar activeElement = null;\nvar activeElementInst = null;\n\n/**\n * SECTION: handle `change` event\n */\nfunction shouldUseChangeEvent(elem) {\n  var nodeName = elem.nodeName && elem.nodeName.toLowerCase();\n  return nodeName === 'select' || nodeName === 'input' && elem.type === 'file';\n}\n\nfunction manualDispatchChangeEvent(nativeEvent) {\n  var event = createAndAccumulateChangeEvent(activeElementInst, nativeEvent, getEventTarget(nativeEvent));\n\n  // If change and propertychange bubbled, we'd just bind to it like all the\n  // other events and have it go through ReactBrowserEventEmitter. Since it\n  // doesn't, we manually listen for the events and so we have to enqueue and\n  // process the abstract event manually.\n  //\n  // Batching is necessary here in order to ensure that all event handlers run\n  // before the next rerender (including event handlers attached to ancestor\n  // elements instead of directly on the input). Without this, controlled\n  // components don't work properly in conjunction with event bubbling because\n  // the component is rerendered and the value reverted before all the event\n  // handlers can run. See https://github.com/facebook/react/issues/708.\n  batchedUpdates(runEventInBatch, event);\n}\n\nfunction runEventInBatch(event) {\n  runEventsInBatch(event);\n}\n\nfunction getInstIfValueChanged(targetInst) {\n  var targetNode = getNodeFromInstance$1(targetInst);\n  if (updateValueIfChanged(targetNode)) {\n    return targetInst;\n  }\n}\n\nfunction getTargetInstForChangeEvent(topLevelType, targetInst) {\n  if (topLevelType === TOP_CHANGE) {\n    return targetInst;\n  }\n}\n\n/**\n * SECTION: handle `input` event\n */\nvar isInputEventSupported = false;\nif (canUseDOM) {\n  // IE9 claims to support the input event but fails to trigger it when\n  // deleting text, so we ignore its input events.\n  isInputEventSupported = isEventSupported('input') && (!document.documentMode || document.documentMode > 9);\n}\n\n/**\n * (For IE <=9) Starts tracking propertychange events on the passed-in element\n * and override the value property so that we can distinguish user events from\n * value changes in JS.\n */\nfunction startWatchingForValueChange(target, targetInst) {\n  activeElement = target;\n  activeElementInst = targetInst;\n  activeElement.attachEvent('onpropertychange', handlePropertyChange);\n}\n\n/**\n * (For IE <=9) Removes the event listeners from the currently-tracked element,\n * if any exists.\n */\nfunction stopWatchingForValueChange() {\n  if (!activeElement) {\n    return;\n  }\n  activeElement.detachEvent('onpropertychange', handlePropertyChange);\n  activeElement = null;\n  activeElementInst = null;\n}\n\n/**\n * (For IE <=9) Handles a propertychange event, sending a `change` event if\n * the value of the active element has changed.\n */\nfunction handlePropertyChange(nativeEvent) {\n  if (nativeEvent.propertyName !== 'value') {\n    return;\n  }\n  if (getInstIfValueChanged(activeElementInst)) {\n    manualDispatchChangeEvent(nativeEvent);\n  }\n}\n\nfunction handleEventsForInputEventPolyfill(topLevelType, target, targetInst) {\n  if (topLevelType === TOP_FOCUS) {\n    // In IE9, propertychange fires for most input events but is buggy and\n    // doesn't fire when text is deleted, but conveniently, selectionchange\n    // appears to fire in all of the remaining cases so we catch those and\n    // forward the event if the value has changed\n    // In either case, we don't want to call the event handler if the value\n    // is changed from JS so we redefine a setter for `.value` that updates\n    // our activeElementValue variable, allowing us to ignore those changes\n    //\n    // stopWatching() should be a noop here but we call it just in case we\n    // missed a blur event somehow.\n    stopWatchingForValueChange();\n    startWatchingForValueChange(target, targetInst);\n  } else if (topLevelType === TOP_BLUR) {\n    stopWatchingForValueChange();\n  }\n}\n\n// For IE8 and IE9.\nfunction getTargetInstForInputEventPolyfill(topLevelType, targetInst) {\n  if (topLevelType === TOP_SELECTION_CHANGE || topLevelType === TOP_KEY_UP || topLevelType === TOP_KEY_DOWN) {\n    // On the selectionchange event, the target is just document which isn't\n    // helpful for us so just check activeElement instead.\n    //\n    // 99% of the time, keydown and keyup aren't necessary. IE8 fails to fire\n    // propertychange on the first input event after setting `value` from a\n    // script and fires only keydown, keypress, keyup. Catching keyup usually\n    // gets it and catching keydown lets us fire an event for the first\n    // keystroke if user does a key repeat (it'll be a little delayed: right\n    // before the second keystroke). Other input methods (e.g., paste) seem to\n    // fire selectionchange normally.\n    return getInstIfValueChanged(activeElementInst);\n  }\n}\n\n/**\n * SECTION: handle `click` event\n */\nfunction shouldUseClickEvent(elem) {\n  // Use the `click` event to detect changes to checkbox and radio inputs.\n  // This approach works across all browsers, whereas `change` does not fire\n  // until `blur` in IE8.\n  var nodeName = elem.nodeName;\n  return nodeName && nodeName.toLowerCase() === 'input' && (elem.type === 'checkbox' || elem.type === 'radio');\n}\n\nfunction getTargetInstForClickEvent(topLevelType, targetInst) {\n  if (topLevelType === TOP_CLICK) {\n    return getInstIfValueChanged(targetInst);\n  }\n}\n\nfunction getTargetInstForInputOrChangeEvent(topLevelType, targetInst) {\n  if (topLevelType === TOP_INPUT || topLevelType === TOP_CHANGE) {\n    return getInstIfValueChanged(targetInst);\n  }\n}\n\nfunction handleControlledInputBlur(node) {\n  var state = node._wrapperState;\n\n  if (!state || !state.controlled || node.type !== 'number') {\n    return;\n  }\n\n  if (!disableInputAttributeSyncing) {\n    // If controlled, assign the value attribute to the current value on blur\n    setDefaultValue(node, 'number', node.value);\n  }\n}\n\n/**\n * This plugin creates an `onChange` event that normalizes change events\n * across form elements. This event fires at a time when it's possible to\n * change the element's value without seeing a flicker.\n *\n * Supported elements are:\n * - input (see `isTextInputElement`)\n * - textarea\n * - select\n */\nvar ChangeEventPlugin = {\n  eventTypes: eventTypes$1,\n\n  _isInputEventSupported: isInputEventSupported,\n\n  extractEvents: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {\n    var targetNode = targetInst ? getNodeFromInstance$1(targetInst) : window;\n\n    var getTargetInstFunc = void 0,\n        handleEventFunc = void 0;\n    if (shouldUseChangeEvent(targetNode)) {\n      getTargetInstFunc = getTargetInstForChangeEvent;\n    } else if (isTextInputElement(targetNode)) {\n      if (isInputEventSupported) {\n        getTargetInstFunc = getTargetInstForInputOrChangeEvent;\n      } else {\n        getTargetInstFunc = getTargetInstForInputEventPolyfill;\n        handleEventFunc = handleEventsForInputEventPolyfill;\n      }\n    } else if (shouldUseClickEvent(targetNode)) {\n      getTargetInstFunc = getTargetInstForClickEvent;\n    }\n\n    if (getTargetInstFunc) {\n      var inst = getTargetInstFunc(topLevelType, targetInst);\n      if (inst) {\n        var event = createAndAccumulateChangeEvent(inst, nativeEvent, nativeEventTarget);\n        return event;\n      }\n    }\n\n    if (handleEventFunc) {\n      handleEventFunc(topLevelType, targetNode, targetInst);\n    }\n\n    // When blurring, set the value attribute for number inputs\n    if (topLevelType === TOP_BLUR) {\n      handleControlledInputBlur(targetNode);\n    }\n  }\n};\n\n/**\n * Module that is injectable into `EventPluginHub`, that specifies a\n * deterministic ordering of `EventPlugin`s. A convenient way to reason about\n * plugins, without having to package every one of them. This is better than\n * having plugins be ordered in the same order that they are injected because\n * that ordering would be influenced by the packaging order.\n * `ResponderEventPlugin` must occur before `SimpleEventPlugin` so that\n * preventing default on events is convenient in `SimpleEventPlugin` handlers.\n */\nvar DOMEventPluginOrder = ['ResponderEventPlugin', 'SimpleEventPlugin', 'EnterLeaveEventPlugin', 'ChangeEventPlugin', 'SelectEventPlugin', 'BeforeInputEventPlugin'];\n\nvar SyntheticUIEvent = SyntheticEvent.extend({\n  view: null,\n  detail: null\n});\n\nvar modifierKeyToProp = {\n  Alt: 'altKey',\n  Control: 'ctrlKey',\n  Meta: 'metaKey',\n  Shift: 'shiftKey'\n};\n\n// Older browsers (Safari <= 10, iOS Safari <= 10.2) do not support\n// getModifierState. If getModifierState is not supported, we map it to a set of\n// modifier keys exposed by the event. In this case, Lock-keys are not supported.\n/**\n * Translation from modifier key to the associated property in the event.\n * @see http://www.w3.org/TR/DOM-Level-3-Events/#keys-Modifiers\n */\n\nfunction modifierStateGetter(keyArg) {\n  var syntheticEvent = this;\n  var nativeEvent = syntheticEvent.nativeEvent;\n  if (nativeEvent.getModifierState) {\n    return nativeEvent.getModifierState(keyArg);\n  }\n  var keyProp = modifierKeyToProp[keyArg];\n  return keyProp ? !!nativeEvent[keyProp] : false;\n}\n\nfunction getEventModifierState(nativeEvent) {\n  return modifierStateGetter;\n}\n\nvar previousScreenX = 0;\nvar previousScreenY = 0;\n// Use flags to signal movementX/Y has already been set\nvar isMovementXSet = false;\nvar isMovementYSet = false;\n\n/**\n * @interface MouseEvent\n * @see http://www.w3.org/TR/DOM-Level-3-Events/\n */\nvar SyntheticMouseEvent = SyntheticUIEvent.extend({\n  screenX: null,\n  screenY: null,\n  clientX: null,\n  clientY: null,\n  pageX: null,\n  pageY: null,\n  ctrlKey: null,\n  shiftKey: null,\n  altKey: null,\n  metaKey: null,\n  getModifierState: getEventModifierState,\n  button: null,\n  buttons: null,\n  relatedTarget: function (event) {\n    return event.relatedTarget || (event.fromElement === event.srcElement ? event.toElement : event.fromElement);\n  },\n  movementX: function (event) {\n    if ('movementX' in event) {\n      return event.movementX;\n    }\n\n    var screenX = previousScreenX;\n    previousScreenX = event.screenX;\n\n    if (!isMovementXSet) {\n      isMovementXSet = true;\n      return 0;\n    }\n\n    return event.type === 'mousemove' ? event.screenX - screenX : 0;\n  },\n  movementY: function (event) {\n    if ('movementY' in event) {\n      return event.movementY;\n    }\n\n    var screenY = previousScreenY;\n    previousScreenY = event.screenY;\n\n    if (!isMovementYSet) {\n      isMovementYSet = true;\n      return 0;\n    }\n\n    return event.type === 'mousemove' ? event.screenY - screenY : 0;\n  }\n});\n\n/**\n * @interface PointerEvent\n * @see http://www.w3.org/TR/pointerevents/\n */\nvar SyntheticPointerEvent = SyntheticMouseEvent.extend({\n  pointerId: null,\n  width: null,\n  height: null,\n  pressure: null,\n  tangentialPressure: null,\n  tiltX: null,\n  tiltY: null,\n  twist: null,\n  pointerType: null,\n  isPrimary: null\n});\n\nvar eventTypes$2 = {\n  mouseEnter: {\n    registrationName: 'onMouseEnter',\n    dependencies: [TOP_MOUSE_OUT, TOP_MOUSE_OVER]\n  },\n  mouseLeave: {\n    registrationName: 'onMouseLeave',\n    dependencies: [TOP_MOUSE_OUT, TOP_MOUSE_OVER]\n  },\n  pointerEnter: {\n    registrationName: 'onPointerEnter',\n    dependencies: [TOP_POINTER_OUT, TOP_POINTER_OVER]\n  },\n  pointerLeave: {\n    registrationName: 'onPointerLeave',\n    dependencies: [TOP_POINTER_OUT, TOP_POINTER_OVER]\n  }\n};\n\nvar EnterLeaveEventPlugin = {\n  eventTypes: eventTypes$2,\n\n  /**\n   * For almost every interaction we care about, there will be both a top-level\n   * `mouseover` and `mouseout` event that occurs. Only use `mouseout` so that\n   * we do not extract duplicate events. However, moving the mouse into the\n   * browser from outside will not fire a `mouseout` event. In this case, we use\n   * the `mouseover` top-level event.\n   */\n  extractEvents: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {\n    var isOverEvent = topLevelType === TOP_MOUSE_OVER || topLevelType === TOP_POINTER_OVER;\n    var isOutEvent = topLevelType === TOP_MOUSE_OUT || topLevelType === TOP_POINTER_OUT;\n\n    if (isOverEvent && (nativeEvent.relatedTarget || nativeEvent.fromElement)) {\n      return null;\n    }\n\n    if (!isOutEvent && !isOverEvent) {\n      // Must not be a mouse or pointer in or out - ignoring.\n      return null;\n    }\n\n    var win = void 0;\n    if (nativeEventTarget.window === nativeEventTarget) {\n      // `nativeEventTarget` is probably a window object.\n      win = nativeEventTarget;\n    } else {\n      // TODO: Figure out why `ownerDocument` is sometimes undefined in IE8.\n      var doc = nativeEventTarget.ownerDocument;\n      if (doc) {\n        win = doc.defaultView || doc.parentWindow;\n      } else {\n        win = window;\n      }\n    }\n\n    var from = void 0;\n    var to = void 0;\n    if (isOutEvent) {\n      from = targetInst;\n      var related = nativeEvent.relatedTarget || nativeEvent.toElement;\n      to = related ? getClosestInstanceFromNode(related) : null;\n    } else {\n      // Moving to a node from outside the window.\n      from = null;\n      to = targetInst;\n    }\n\n    if (from === to) {\n      // Nothing pertains to our managed components.\n      return null;\n    }\n\n    var eventInterface = void 0,\n        leaveEventType = void 0,\n        enterEventType = void 0,\n        eventTypePrefix = void 0;\n\n    if (topLevelType === TOP_MOUSE_OUT || topLevelType === TOP_MOUSE_OVER) {\n      eventInterface = SyntheticMouseEvent;\n      leaveEventType = eventTypes$2.mouseLeave;\n      enterEventType = eventTypes$2.mouseEnter;\n      eventTypePrefix = 'mouse';\n    } else if (topLevelType === TOP_POINTER_OUT || topLevelType === TOP_POINTER_OVER) {\n      eventInterface = SyntheticPointerEvent;\n      leaveEventType = eventTypes$2.pointerLeave;\n      enterEventType = eventTypes$2.pointerEnter;\n      eventTypePrefix = 'pointer';\n    }\n\n    var fromNode = from == null ? win : getNodeFromInstance$1(from);\n    var toNode = to == null ? win : getNodeFromInstance$1(to);\n\n    var leave = eventInterface.getPooled(leaveEventType, from, nativeEvent, nativeEventTarget);\n    leave.type = eventTypePrefix + 'leave';\n    leave.target = fromNode;\n    leave.relatedTarget = toNode;\n\n    var enter = eventInterface.getPooled(enterEventType, to, nativeEvent, nativeEventTarget);\n    enter.type = eventTypePrefix + 'enter';\n    enter.target = toNode;\n    enter.relatedTarget = fromNode;\n\n    accumulateEnterLeaveDispatches(leave, enter, from, to);\n\n    return [leave, enter];\n  }\n};\n\n/**\n * inlined Object.is polyfill to avoid requiring consumers ship their own\n * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is\n */\nfunction is(x, y) {\n  return x === y && (x !== 0 || 1 / x === 1 / y) || x !== x && y !== y // eslint-disable-line no-self-compare\n  ;\n}\n\nvar hasOwnProperty$1 = Object.prototype.hasOwnProperty;\n\n/**\n * Performs equality by iterating through keys on an object and returning false\n * when any key has values which are not strictly equal between the arguments.\n * Returns true when the values of all keys are strictly equal.\n */\nfunction shallowEqual(objA, objB) {\n  if (is(objA, objB)) {\n    return true;\n  }\n\n  if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) {\n    return false;\n  }\n\n  var keysA = Object.keys(objA);\n  var keysB = Object.keys(objB);\n\n  if (keysA.length !== keysB.length) {\n    return false;\n  }\n\n  // Test for A's keys different from B.\n  for (var i = 0; i < keysA.length; i++) {\n    if (!hasOwnProperty$1.call(objB, keysA[i]) || !is(objA[keysA[i]], objB[keysA[i]])) {\n      return false;\n    }\n  }\n\n  return true;\n}\n\n/**\n * `ReactInstanceMap` maintains a mapping from a public facing stateful\n * instance (key) and the internal representation (value). This allows public\n * methods to accept the user facing instance as an argument and map them back\n * to internal methods.\n *\n * Note that this module is currently shared and assumed to be stateless.\n * If this becomes an actual Map, that will break.\n */\n\n/**\n * This API should be called `delete` but we'd have to make sure to always\n * transform these to strings for IE support. When this transform is fully\n * supported we can rename it.\n */\n\n\nfunction get(key) {\n  return key._reactInternalFiber;\n}\n\nfunction has(key) {\n  return key._reactInternalFiber !== undefined;\n}\n\nfunction set(key, value) {\n  key._reactInternalFiber = value;\n}\n\n// Don't change these two values. They're used by React Dev Tools.\nvar NoEffect = /*              */0;\nvar PerformedWork = /*         */1;\n\n// You can change the rest (and add more).\nvar Placement = /*             */2;\nvar Update = /*                */4;\nvar PlacementAndUpdate = /*    */6;\nvar Deletion = /*              */8;\nvar ContentReset = /*          */16;\nvar Callback = /*              */32;\nvar DidCapture = /*            */64;\nvar Ref = /*                   */128;\nvar Snapshot = /*              */256;\nvar Passive = /*               */512;\n\n// Passive & Update & Callback & Ref & Snapshot\nvar LifecycleEffectMask = /*   */932;\n\n// Union of all host effects\nvar HostEffectMask = /*        */1023;\n\nvar Incomplete = /*            */1024;\nvar ShouldCapture = /*         */2048;\n\nvar ReactCurrentOwner$1 = ReactSharedInternals.ReactCurrentOwner;\n\nvar MOUNTING = 1;\nvar MOUNTED = 2;\nvar UNMOUNTED = 3;\n\nfunction isFiberMountedImpl(fiber) {\n  var node = fiber;\n  if (!fiber.alternate) {\n    // If there is no alternate, this might be a new tree that isn't inserted\n    // yet. If it is, then it will have a pending insertion effect on it.\n    if ((node.effectTag & Placement) !== NoEffect) {\n      return MOUNTING;\n    }\n    while (node.return) {\n      node = node.return;\n      if ((node.effectTag & Placement) !== NoEffect) {\n        return MOUNTING;\n      }\n    }\n  } else {\n    while (node.return) {\n      node = node.return;\n    }\n  }\n  if (node.tag === HostRoot) {\n    // TODO: Check if this was a nested HostRoot when used with\n    // renderContainerIntoSubtree.\n    return MOUNTED;\n  }\n  // If we didn't hit the root, that means that we're in an disconnected tree\n  // that has been unmounted.\n  return UNMOUNTED;\n}\n\nfunction isFiberMounted(fiber) {\n  return isFiberMountedImpl(fiber) === MOUNTED;\n}\n\nfunction isMounted(component) {\n  {\n    var owner = ReactCurrentOwner$1.current;\n    if (owner !== null && owner.tag === ClassComponent) {\n      var ownerFiber = owner;\n      var instance = ownerFiber.stateNode;\n      !instance._warnedAboutRefsInRender ? warningWithoutStack$1(false, '%s is accessing isMounted inside its render() function. ' + 'render() should be a pure function of props and state. It should ' + 'never access something that requires stale data from the previous ' + 'render, such as refs. Move this logic to componentDidMount and ' + 'componentDidUpdate instead.', getComponentName(ownerFiber.type) || 'A component') : void 0;\n      instance._warnedAboutRefsInRender = true;\n    }\n  }\n\n  var fiber = get(component);\n  if (!fiber) {\n    return false;\n  }\n  return isFiberMountedImpl(fiber) === MOUNTED;\n}\n\nfunction assertIsMounted(fiber) {\n  !(isFiberMountedImpl(fiber) === MOUNTED) ? invariant(false, 'Unable to find node on an unmounted component.') : void 0;\n}\n\nfunction findCurrentFiberUsingSlowPath(fiber) {\n  var alternate = fiber.alternate;\n  if (!alternate) {\n    // If there is no alternate, then we only need to check if it is mounted.\n    var state = isFiberMountedImpl(fiber);\n    !(state !== UNMOUNTED) ? invariant(false, 'Unable to find node on an unmounted component.') : void 0;\n    if (state === MOUNTING) {\n      return null;\n    }\n    return fiber;\n  }\n  // If we have two possible branches, we'll walk backwards up to the root\n  // to see what path the root points to. On the way we may hit one of the\n  // special cases and we'll deal with them.\n  var a = fiber;\n  var b = alternate;\n  while (true) {\n    var parentA = a.return;\n    var parentB = parentA ? parentA.alternate : null;\n    if (!parentA || !parentB) {\n      // We're at the root.\n      break;\n    }\n\n    // If both copies of the parent fiber point to the same child, we can\n    // assume that the child is current. This happens when we bailout on low\n    // priority: the bailed out fiber's child reuses the current child.\n    if (parentA.child === parentB.child) {\n      var child = parentA.child;\n      while (child) {\n        if (child === a) {\n          // We've determined that A is the current branch.\n          assertIsMounted(parentA);\n          return fiber;\n        }\n        if (child === b) {\n          // We've determined that B is the current branch.\n          assertIsMounted(parentA);\n          return alternate;\n        }\n        child = child.sibling;\n      }\n      // We should never have an alternate for any mounting node. So the only\n      // way this could possibly happen is if this was unmounted, if at all.\n      invariant(false, 'Unable to find node on an unmounted component.');\n    }\n\n    if (a.return !== b.return) {\n      // The return pointer of A and the return pointer of B point to different\n      // fibers. We assume that return pointers never criss-cross, so A must\n      // belong to the child set of A.return, and B must belong to the child\n      // set of B.return.\n      a = parentA;\n      b = parentB;\n    } else {\n      // The return pointers point to the same fiber. We'll have to use the\n      // default, slow path: scan the child sets of each parent alternate to see\n      // which child belongs to which set.\n      //\n      // Search parent A's child set\n      var didFindChild = false;\n      var _child = parentA.child;\n      while (_child) {\n        if (_child === a) {\n          didFindChild = true;\n          a = parentA;\n          b = parentB;\n          break;\n        }\n        if (_child === b) {\n          didFindChild = true;\n          b = parentA;\n          a = parentB;\n          break;\n        }\n        _child = _child.sibling;\n      }\n      if (!didFindChild) {\n        // Search parent B's child set\n        _child = parentB.child;\n        while (_child) {\n          if (_child === a) {\n            didFindChild = true;\n            a = parentB;\n            b = parentA;\n            break;\n          }\n          if (_child === b) {\n            didFindChild = true;\n            b = parentB;\n            a = parentA;\n            break;\n          }\n          _child = _child.sibling;\n        }\n        !didFindChild ? invariant(false, 'Child was not found in either parent set. This indicates a bug in React related to the return pointer. Please file an issue.') : void 0;\n      }\n    }\n\n    !(a.alternate === b) ? invariant(false, 'Return fibers should always be each others\\' alternates. This error is likely caused by a bug in React. Please file an issue.') : void 0;\n  }\n  // If the root is not a host container, we're in a disconnected tree. I.e.\n  // unmounted.\n  !(a.tag === HostRoot) ? invariant(false, 'Unable to find node on an unmounted component.') : void 0;\n  if (a.stateNode.current === a) {\n    // We've determined that A is the current branch.\n    return fiber;\n  }\n  // Otherwise B has to be current branch.\n  return alternate;\n}\n\nfunction findCurrentHostFiber(parent) {\n  var currentParent = findCurrentFiberUsingSlowPath(parent);\n  if (!currentParent) {\n    return null;\n  }\n\n  // Next we'll drill down this component to find the first HostComponent/Text.\n  var node = currentParent;\n  while (true) {\n    if (node.tag === HostComponent || node.tag === HostText) {\n      return node;\n    } else if (node.child) {\n      node.child.return = node;\n      node = node.child;\n      continue;\n    }\n    if (node === currentParent) {\n      return null;\n    }\n    while (!node.sibling) {\n      if (!node.return || node.return === currentParent) {\n        return null;\n      }\n      node = node.return;\n    }\n    node.sibling.return = node.return;\n    node = node.sibling;\n  }\n  // Flow needs the return null here, but ESLint complains about it.\n  // eslint-disable-next-line no-unreachable\n  return null;\n}\n\nfunction findCurrentHostFiberWithNoPortals(parent) {\n  var currentParent = findCurrentFiberUsingSlowPath(parent);\n  if (!currentParent) {\n    return null;\n  }\n\n  // Next we'll drill down this component to find the first HostComponent/Text.\n  var node = currentParent;\n  while (true) {\n    if (node.tag === HostComponent || node.tag === HostText) {\n      return node;\n    } else if (node.child && node.tag !== HostPortal) {\n      node.child.return = node;\n      node = node.child;\n      continue;\n    }\n    if (node === currentParent) {\n      return null;\n    }\n    while (!node.sibling) {\n      if (!node.return || node.return === currentParent) {\n        return null;\n      }\n      node = node.return;\n    }\n    node.sibling.return = node.return;\n    node = node.sibling;\n  }\n  // Flow needs the return null here, but ESLint complains about it.\n  // eslint-disable-next-line no-unreachable\n  return null;\n}\n\nfunction addEventBubbleListener(element, eventType, listener) {\n  element.addEventListener(eventType, listener, false);\n}\n\nfunction addEventCaptureListener(element, eventType, listener) {\n  element.addEventListener(eventType, listener, true);\n}\n\n/**\n * @interface Event\n * @see http://www.w3.org/TR/css3-animations/#AnimationEvent-interface\n * @see https://developer.mozilla.org/en-US/docs/Web/API/AnimationEvent\n */\nvar SyntheticAnimationEvent = SyntheticEvent.extend({\n  animationName: null,\n  elapsedTime: null,\n  pseudoElement: null\n});\n\n/**\n * @interface Event\n * @see http://www.w3.org/TR/clipboard-apis/\n */\nvar SyntheticClipboardEvent = SyntheticEvent.extend({\n  clipboardData: function (event) {\n    return 'clipboardData' in event ? event.clipboardData : window.clipboardData;\n  }\n});\n\n/**\n * @interface FocusEvent\n * @see http://www.w3.org/TR/DOM-Level-3-Events/\n */\nvar SyntheticFocusEvent = SyntheticUIEvent.extend({\n  relatedTarget: null\n});\n\n/**\n * `charCode` represents the actual \"character code\" and is safe to use with\n * `String.fromCharCode`. As such, only keys that correspond to printable\n * characters produce a valid `charCode`, the only exception to this is Enter.\n * The Tab-key is considered non-printable and does not have a `charCode`,\n * presumably because it does not produce a tab-character in browsers.\n *\n * @param {object} nativeEvent Native browser event.\n * @return {number} Normalized `charCode` property.\n */\nfunction getEventCharCode(nativeEvent) {\n  var charCode = void 0;\n  var keyCode = nativeEvent.keyCode;\n\n  if ('charCode' in nativeEvent) {\n    charCode = nativeEvent.charCode;\n\n    // FF does not set `charCode` for the Enter-key, check against `keyCode`.\n    if (charCode === 0 && keyCode === 13) {\n      charCode = 13;\n    }\n  } else {\n    // IE8 does not implement `charCode`, but `keyCode` has the correct value.\n    charCode = keyCode;\n  }\n\n  // IE and Edge (on Windows) and Chrome / Safari (on Windows and Linux)\n  // report Enter as charCode 10 when ctrl is pressed.\n  if (charCode === 10) {\n    charCode = 13;\n  }\n\n  // Some non-printable keys are reported in `charCode`/`keyCode`, discard them.\n  // Must not discard the (non-)printable Enter-key.\n  if (charCode >= 32 || charCode === 13) {\n    return charCode;\n  }\n\n  return 0;\n}\n\n/**\n * Normalization of deprecated HTML5 `key` values\n * @see https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent#Key_names\n */\nvar normalizeKey = {\n  Esc: 'Escape',\n  Spacebar: ' ',\n  Left: 'ArrowLeft',\n  Up: 'ArrowUp',\n  Right: 'ArrowRight',\n  Down: 'ArrowDown',\n  Del: 'Delete',\n  Win: 'OS',\n  Menu: 'ContextMenu',\n  Apps: 'ContextMenu',\n  Scroll: 'ScrollLock',\n  MozPrintableKey: 'Unidentified'\n};\n\n/**\n * Translation from legacy `keyCode` to HTML5 `key`\n * Only special keys supported, all others depend on keyboard layout or browser\n * @see https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent#Key_names\n */\nvar translateToKey = {\n  '8': 'Backspace',\n  '9': 'Tab',\n  '12': 'Clear',\n  '13': 'Enter',\n  '16': 'Shift',\n  '17': 'Control',\n  '18': 'Alt',\n  '19': 'Pause',\n  '20': 'CapsLock',\n  '27': 'Escape',\n  '32': ' ',\n  '33': 'PageUp',\n  '34': 'PageDown',\n  '35': 'End',\n  '36': 'Home',\n  '37': 'ArrowLeft',\n  '38': 'ArrowUp',\n  '39': 'ArrowRight',\n  '40': 'ArrowDown',\n  '45': 'Insert',\n  '46': 'Delete',\n  '112': 'F1',\n  '113': 'F2',\n  '114': 'F3',\n  '115': 'F4',\n  '116': 'F5',\n  '117': 'F6',\n  '118': 'F7',\n  '119': 'F8',\n  '120': 'F9',\n  '121': 'F10',\n  '122': 'F11',\n  '123': 'F12',\n  '144': 'NumLock',\n  '145': 'ScrollLock',\n  '224': 'Meta'\n};\n\n/**\n * @param {object} nativeEvent Native browser event.\n * @return {string} Normalized `key` property.\n */\nfunction getEventKey(nativeEvent) {\n  if (nativeEvent.key) {\n    // Normalize inconsistent values reported by browsers due to\n    // implementations of a working draft specification.\n\n    // FireFox implements `key` but returns `MozPrintableKey` for all\n    // printable characters (normalized to `Unidentified`), ignore it.\n    var key = normalizeKey[nativeEvent.key] || nativeEvent.key;\n    if (key !== 'Unidentified') {\n      return key;\n    }\n  }\n\n  // Browser does not implement `key`, polyfill as much of it as we can.\n  if (nativeEvent.type === 'keypress') {\n    var charCode = getEventCharCode(nativeEvent);\n\n    // The enter-key is technically both printable and non-printable and can\n    // thus be captured by `keypress`, no other non-printable key should.\n    return charCode === 13 ? 'Enter' : String.fromCharCode(charCode);\n  }\n  if (nativeEvent.type === 'keydown' || nativeEvent.type === 'keyup') {\n    // While user keyboard layout determines the actual meaning of each\n    // `keyCode` value, almost all function keys have a universal value.\n    return translateToKey[nativeEvent.keyCode] || 'Unidentified';\n  }\n  return '';\n}\n\n/**\n * @interface KeyboardEvent\n * @see http://www.w3.org/TR/DOM-Level-3-Events/\n */\nvar SyntheticKeyboardEvent = SyntheticUIEvent.extend({\n  key: getEventKey,\n  location: null,\n  ctrlKey: null,\n  shiftKey: null,\n  altKey: null,\n  metaKey: null,\n  repeat: null,\n  locale: null,\n  getModifierState: getEventModifierState,\n  // Legacy Interface\n  charCode: function (event) {\n    // `charCode` is the result of a KeyPress event and represents the value of\n    // the actual printable character.\n\n    // KeyPress is deprecated, but its replacement is not yet final and not\n    // implemented in any major browser. Only KeyPress has charCode.\n    if (event.type === 'keypress') {\n      return getEventCharCode(event);\n    }\n    return 0;\n  },\n  keyCode: function (event) {\n    // `keyCode` is the result of a KeyDown/Up event and represents the value of\n    // physical keyboard key.\n\n    // The actual meaning of the value depends on the users' keyboard layout\n    // which cannot be detected. Assuming that it is a US keyboard layout\n    // provides a surprisingly accurate mapping for US and European users.\n    // Due to this, it is left to the user to implement at this time.\n    if (event.type === 'keydown' || event.type === 'keyup') {\n      return event.keyCode;\n    }\n    return 0;\n  },\n  which: function (event) {\n    // `which` is an alias for either `keyCode` or `charCode` depending on the\n    // type of the event.\n    if (event.type === 'keypress') {\n      return getEventCharCode(event);\n    }\n    if (event.type === 'keydown' || event.type === 'keyup') {\n      return event.keyCode;\n    }\n    return 0;\n  }\n});\n\n/**\n * @interface DragEvent\n * @see http://www.w3.org/TR/DOM-Level-3-Events/\n */\nvar SyntheticDragEvent = SyntheticMouseEvent.extend({\n  dataTransfer: null\n});\n\n/**\n * @interface TouchEvent\n * @see http://www.w3.org/TR/touch-events/\n */\nvar SyntheticTouchEvent = SyntheticUIEvent.extend({\n  touches: null,\n  targetTouches: null,\n  changedTouches: null,\n  altKey: null,\n  metaKey: null,\n  ctrlKey: null,\n  shiftKey: null,\n  getModifierState: getEventModifierState\n});\n\n/**\n * @interface Event\n * @see http://www.w3.org/TR/2009/WD-css3-transitions-20090320/#transition-events-\n * @see https://developer.mozilla.org/en-US/docs/Web/API/TransitionEvent\n */\nvar SyntheticTransitionEvent = SyntheticEvent.extend({\n  propertyName: null,\n  elapsedTime: null,\n  pseudoElement: null\n});\n\n/**\n * @interface WheelEvent\n * @see http://www.w3.org/TR/DOM-Level-3-Events/\n */\nvar SyntheticWheelEvent = SyntheticMouseEvent.extend({\n  deltaX: function (event) {\n    return 'deltaX' in event ? event.deltaX : // Fallback to `wheelDeltaX` for Webkit and normalize (right is positive).\n    'wheelDeltaX' in event ? -event.wheelDeltaX : 0;\n  },\n  deltaY: function (event) {\n    return 'deltaY' in event ? event.deltaY : // Fallback to `wheelDeltaY` for Webkit and normalize (down is positive).\n    'wheelDeltaY' in event ? -event.wheelDeltaY : // Fallback to `wheelDelta` for IE<9 and normalize (down is positive).\n    'wheelDelta' in event ? -event.wheelDelta : 0;\n  },\n\n  deltaZ: null,\n\n  // Browsers without \"deltaMode\" is reporting in raw wheel delta where one\n  // notch on the scroll is always +/- 120, roughly equivalent to pixels.\n  // A good approximation of DOM_DELTA_LINE (1) is 5% of viewport size or\n  // ~40 pixels, for DOM_DELTA_SCREEN (2) it is 87.5% of viewport size.\n  deltaMode: null\n});\n\n/**\n * Turns\n * ['abort', ...]\n * into\n * eventTypes = {\n *   'abort': {\n *     phasedRegistrationNames: {\n *       bubbled: 'onAbort',\n *       captured: 'onAbortCapture',\n *     },\n *     dependencies: [TOP_ABORT],\n *   },\n *   ...\n * };\n * topLevelEventsToDispatchConfig = new Map([\n *   [TOP_ABORT, { sameConfig }],\n * ]);\n */\n\nvar interactiveEventTypeNames = [[TOP_BLUR, 'blur'], [TOP_CANCEL, 'cancel'], [TOP_CLICK, 'click'], [TOP_CLOSE, 'close'], [TOP_CONTEXT_MENU, 'contextMenu'], [TOP_COPY, 'copy'], [TOP_CUT, 'cut'], [TOP_AUX_CLICK, 'auxClick'], [TOP_DOUBLE_CLICK, 'doubleClick'], [TOP_DRAG_END, 'dragEnd'], [TOP_DRAG_START, 'dragStart'], [TOP_DROP, 'drop'], [TOP_FOCUS, 'focus'], [TOP_INPUT, 'input'], [TOP_INVALID, 'invalid'], [TOP_KEY_DOWN, 'keyDown'], [TOP_KEY_PRESS, 'keyPress'], [TOP_KEY_UP, 'keyUp'], [TOP_MOUSE_DOWN, 'mouseDown'], [TOP_MOUSE_UP, 'mouseUp'], [TOP_PASTE, 'paste'], [TOP_PAUSE, 'pause'], [TOP_PLAY, 'play'], [TOP_POINTER_CANCEL, 'pointerCancel'], [TOP_POINTER_DOWN, 'pointerDown'], [TOP_POINTER_UP, 'pointerUp'], [TOP_RATE_CHANGE, 'rateChange'], [TOP_RESET, 'reset'], [TOP_SEEKED, 'seeked'], [TOP_SUBMIT, 'submit'], [TOP_TOUCH_CANCEL, 'touchCancel'], [TOP_TOUCH_END, 'touchEnd'], [TOP_TOUCH_START, 'touchStart'], [TOP_VOLUME_CHANGE, 'volumeChange']];\nvar nonInteractiveEventTypeNames = [[TOP_ABORT, 'abort'], [TOP_ANIMATION_END, 'animationEnd'], [TOP_ANIMATION_ITERATION, 'animationIteration'], [TOP_ANIMATION_START, 'animationStart'], [TOP_CAN_PLAY, 'canPlay'], [TOP_CAN_PLAY_THROUGH, 'canPlayThrough'], [TOP_DRAG, 'drag'], [TOP_DRAG_ENTER, 'dragEnter'], [TOP_DRAG_EXIT, 'dragExit'], [TOP_DRAG_LEAVE, 'dragLeave'], [TOP_DRAG_OVER, 'dragOver'], [TOP_DURATION_CHANGE, 'durationChange'], [TOP_EMPTIED, 'emptied'], [TOP_ENCRYPTED, 'encrypted'], [TOP_ENDED, 'ended'], [TOP_ERROR, 'error'], [TOP_GOT_POINTER_CAPTURE, 'gotPointerCapture'], [TOP_LOAD, 'load'], [TOP_LOADED_DATA, 'loadedData'], [TOP_LOADED_METADATA, 'loadedMetadata'], [TOP_LOAD_START, 'loadStart'], [TOP_LOST_POINTER_CAPTURE, 'lostPointerCapture'], [TOP_MOUSE_MOVE, 'mouseMove'], [TOP_MOUSE_OUT, 'mouseOut'], [TOP_MOUSE_OVER, 'mouseOver'], [TOP_PLAYING, 'playing'], [TOP_POINTER_MOVE, 'pointerMove'], [TOP_POINTER_OUT, 'pointerOut'], [TOP_POINTER_OVER, 'pointerOver'], [TOP_PROGRESS, 'progress'], [TOP_SCROLL, 'scroll'], [TOP_SEEKING, 'seeking'], [TOP_STALLED, 'stalled'], [TOP_SUSPEND, 'suspend'], [TOP_TIME_UPDATE, 'timeUpdate'], [TOP_TOGGLE, 'toggle'], [TOP_TOUCH_MOVE, 'touchMove'], [TOP_TRANSITION_END, 'transitionEnd'], [TOP_WAITING, 'waiting'], [TOP_WHEEL, 'wheel']];\n\nvar eventTypes$4 = {};\nvar topLevelEventsToDispatchConfig = {};\n\nfunction addEventTypeNameToConfig(_ref, isInteractive) {\n  var topEvent = _ref[0],\n      event = _ref[1];\n\n  var capitalizedEvent = event[0].toUpperCase() + event.slice(1);\n  var onEvent = 'on' + capitalizedEvent;\n\n  var type = {\n    phasedRegistrationNames: {\n      bubbled: onEvent,\n      captured: onEvent + 'Capture'\n    },\n    dependencies: [topEvent],\n    isInteractive: isInteractive\n  };\n  eventTypes$4[event] = type;\n  topLevelEventsToDispatchConfig[topEvent] = type;\n}\n\ninteractiveEventTypeNames.forEach(function (eventTuple) {\n  addEventTypeNameToConfig(eventTuple, true);\n});\nnonInteractiveEventTypeNames.forEach(function (eventTuple) {\n  addEventTypeNameToConfig(eventTuple, false);\n});\n\n// Only used in DEV for exhaustiveness validation.\nvar knownHTMLTopLevelTypes = [TOP_ABORT, TOP_CANCEL, TOP_CAN_PLAY, TOP_CAN_PLAY_THROUGH, TOP_CLOSE, TOP_DURATION_CHANGE, TOP_EMPTIED, TOP_ENCRYPTED, TOP_ENDED, TOP_ERROR, TOP_INPUT, TOP_INVALID, TOP_LOAD, TOP_LOADED_DATA, TOP_LOADED_METADATA, TOP_LOAD_START, TOP_PAUSE, TOP_PLAY, TOP_PLAYING, TOP_PROGRESS, TOP_RATE_CHANGE, TOP_RESET, TOP_SEEKED, TOP_SEEKING, TOP_STALLED, TOP_SUBMIT, TOP_SUSPEND, TOP_TIME_UPDATE, TOP_TOGGLE, TOP_VOLUME_CHANGE, TOP_WAITING];\n\nvar SimpleEventPlugin = {\n  eventTypes: eventTypes$4,\n\n  isInteractiveTopLevelEventType: function (topLevelType) {\n    var config = topLevelEventsToDispatchConfig[topLevelType];\n    return config !== undefined && config.isInteractive === true;\n  },\n\n\n  extractEvents: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {\n    var dispatchConfig = topLevelEventsToDispatchConfig[topLevelType];\n    if (!dispatchConfig) {\n      return null;\n    }\n    var EventConstructor = void 0;\n    switch (topLevelType) {\n      case TOP_KEY_PRESS:\n        // Firefox creates a keypress event for function keys too. This removes\n        // the unwanted keypress events. Enter is however both printable and\n        // non-printable. One would expect Tab to be as well (but it isn't).\n        if (getEventCharCode(nativeEvent) === 0) {\n          return null;\n        }\n      /* falls through */\n      case TOP_KEY_DOWN:\n      case TOP_KEY_UP:\n        EventConstructor = SyntheticKeyboardEvent;\n        break;\n      case TOP_BLUR:\n      case TOP_FOCUS:\n        EventConstructor = SyntheticFocusEvent;\n        break;\n      case TOP_CLICK:\n        // Firefox creates a click event on right mouse clicks. This removes the\n        // unwanted click events.\n        if (nativeEvent.button === 2) {\n          return null;\n        }\n      /* falls through */\n      case TOP_AUX_CLICK:\n      case TOP_DOUBLE_CLICK:\n      case TOP_MOUSE_DOWN:\n      case TOP_MOUSE_MOVE:\n      case TOP_MOUSE_UP:\n      // TODO: Disabled elements should not respond to mouse events\n      /* falls through */\n      case TOP_MOUSE_OUT:\n      case TOP_MOUSE_OVER:\n      case TOP_CONTEXT_MENU:\n        EventConstructor = SyntheticMouseEvent;\n        break;\n      case TOP_DRAG:\n      case TOP_DRAG_END:\n      case TOP_DRAG_ENTER:\n      case TOP_DRAG_EXIT:\n      case TOP_DRAG_LEAVE:\n      case TOP_DRAG_OVER:\n      case TOP_DRAG_START:\n      case TOP_DROP:\n        EventConstructor = SyntheticDragEvent;\n        break;\n      case TOP_TOUCH_CANCEL:\n      case TOP_TOUCH_END:\n      case TOP_TOUCH_MOVE:\n      case TOP_TOUCH_START:\n        EventConstructor = SyntheticTouchEvent;\n        break;\n      case TOP_ANIMATION_END:\n      case TOP_ANIMATION_ITERATION:\n      case TOP_ANIMATION_START:\n        EventConstructor = SyntheticAnimationEvent;\n        break;\n      case TOP_TRANSITION_END:\n        EventConstructor = SyntheticTransitionEvent;\n        break;\n      case TOP_SCROLL:\n        EventConstructor = SyntheticUIEvent;\n        break;\n      case TOP_WHEEL:\n        EventConstructor = SyntheticWheelEvent;\n        break;\n      case TOP_COPY:\n      case TOP_CUT:\n      case TOP_PASTE:\n        EventConstructor = SyntheticClipboardEvent;\n        break;\n      case TOP_GOT_POINTER_CAPTURE:\n      case TOP_LOST_POINTER_CAPTURE:\n      case TOP_POINTER_CANCEL:\n      case TOP_POINTER_DOWN:\n      case TOP_POINTER_MOVE:\n      case TOP_POINTER_OUT:\n      case TOP_POINTER_OVER:\n      case TOP_POINTER_UP:\n        EventConstructor = SyntheticPointerEvent;\n        break;\n      default:\n        {\n          if (knownHTMLTopLevelTypes.indexOf(topLevelType) === -1) {\n            warningWithoutStack$1(false, 'SimpleEventPlugin: Unhandled event type, `%s`. This warning ' + 'is likely caused by a bug in React. Please file an issue.', topLevelType);\n          }\n        }\n        // HTML Events\n        // @see http://www.w3.org/TR/html5/index.html#events-0\n        EventConstructor = SyntheticEvent;\n        break;\n    }\n    var event = EventConstructor.getPooled(dispatchConfig, targetInst, nativeEvent, nativeEventTarget);\n    accumulateTwoPhaseDispatches(event);\n    return event;\n  }\n};\n\nvar isInteractiveTopLevelEventType = SimpleEventPlugin.isInteractiveTopLevelEventType;\n\n\nvar CALLBACK_BOOKKEEPING_POOL_SIZE = 10;\nvar callbackBookkeepingPool = [];\n\n/**\n * Find the deepest React component completely containing the root of the\n * passed-in instance (for use when entire React trees are nested within each\n * other). If React trees are not nested, returns null.\n */\nfunction findRootContainerNode(inst) {\n  // TODO: It may be a good idea to cache this to prevent unnecessary DOM\n  // traversal, but caching is difficult to do correctly without using a\n  // mutation observer to listen for all DOM changes.\n  while (inst.return) {\n    inst = inst.return;\n  }\n  if (inst.tag !== HostRoot) {\n    // This can happen if we're in a detached tree.\n    return null;\n  }\n  return inst.stateNode.containerInfo;\n}\n\n// Used to store ancestor hierarchy in top level callback\nfunction getTopLevelCallbackBookKeeping(topLevelType, nativeEvent, targetInst) {\n  if (callbackBookkeepingPool.length) {\n    var instance = callbackBookkeepingPool.pop();\n    instance.topLevelType = topLevelType;\n    instance.nativeEvent = nativeEvent;\n    instance.targetInst = targetInst;\n    return instance;\n  }\n  return {\n    topLevelType: topLevelType,\n    nativeEvent: nativeEvent,\n    targetInst: targetInst,\n    ancestors: []\n  };\n}\n\nfunction releaseTopLevelCallbackBookKeeping(instance) {\n  instance.topLevelType = null;\n  instance.nativeEvent = null;\n  instance.targetInst = null;\n  instance.ancestors.length = 0;\n  if (callbackBookkeepingPool.length < CALLBACK_BOOKKEEPING_POOL_SIZE) {\n    callbackBookkeepingPool.push(instance);\n  }\n}\n\nfunction handleTopLevel(bookKeeping) {\n  var targetInst = bookKeeping.targetInst;\n\n  // Loop through the hierarchy, in case there's any nested components.\n  // It's important that we build the array of ancestors before calling any\n  // event handlers, because event handlers can modify the DOM, leading to\n  // inconsistencies with ReactMount's node cache. See #1105.\n  var ancestor = targetInst;\n  do {\n    if (!ancestor) {\n      bookKeeping.ancestors.push(ancestor);\n      break;\n    }\n    var root = findRootContainerNode(ancestor);\n    if (!root) {\n      break;\n    }\n    bookKeeping.ancestors.push(ancestor);\n    ancestor = getClosestInstanceFromNode(root);\n  } while (ancestor);\n\n  for (var i = 0; i < bookKeeping.ancestors.length; i++) {\n    targetInst = bookKeeping.ancestors[i];\n    runExtractedEventsInBatch(bookKeeping.topLevelType, targetInst, bookKeeping.nativeEvent, getEventTarget(bookKeeping.nativeEvent));\n  }\n}\n\n// TODO: can we stop exporting these?\nvar _enabled = true;\n\nfunction setEnabled(enabled) {\n  _enabled = !!enabled;\n}\n\nfunction isEnabled() {\n  return _enabled;\n}\n\n/**\n * Traps top-level events by using event bubbling.\n *\n * @param {number} topLevelType Number from `TopLevelEventTypes`.\n * @param {object} element Element on which to attach listener.\n * @return {?object} An object with a remove function which will forcefully\n *                  remove the listener.\n * @internal\n */\nfunction trapBubbledEvent(topLevelType, element) {\n  if (!element) {\n    return null;\n  }\n  var dispatch = isInteractiveTopLevelEventType(topLevelType) ? dispatchInteractiveEvent : dispatchEvent;\n\n  addEventBubbleListener(element, getRawEventName(topLevelType),\n  // Check if interactive and wrap in interactiveUpdates\n  dispatch.bind(null, topLevelType));\n}\n\n/**\n * Traps a top-level event by using event capturing.\n *\n * @param {number} topLevelType Number from `TopLevelEventTypes`.\n * @param {object} element Element on which to attach listener.\n * @return {?object} An object with a remove function which will forcefully\n *                  remove the listener.\n * @internal\n */\nfunction trapCapturedEvent(topLevelType, element) {\n  if (!element) {\n    return null;\n  }\n  var dispatch = isInteractiveTopLevelEventType(topLevelType) ? dispatchInteractiveEvent : dispatchEvent;\n\n  addEventCaptureListener(element, getRawEventName(topLevelType),\n  // Check if interactive and wrap in interactiveUpdates\n  dispatch.bind(null, topLevelType));\n}\n\nfunction dispatchInteractiveEvent(topLevelType, nativeEvent) {\n  interactiveUpdates(dispatchEvent, topLevelType, nativeEvent);\n}\n\nfunction dispatchEvent(topLevelType, nativeEvent) {\n  if (!_enabled) {\n    return;\n  }\n\n  var nativeEventTarget = getEventTarget(nativeEvent);\n  var targetInst = getClosestInstanceFromNode(nativeEventTarget);\n  if (targetInst !== null && typeof targetInst.tag === 'number' && !isFiberMounted(targetInst)) {\n    // If we get an event (ex: img onload) before committing that\n    // component's mount, ignore it for now (that is, treat it as if it was an\n    // event on a non-React tree). We might also consider queueing events and\n    // dispatching them after the mount.\n    targetInst = null;\n  }\n\n  var bookKeeping = getTopLevelCallbackBookKeeping(topLevelType, nativeEvent, targetInst);\n\n  try {\n    // Event queue being processed in the same cycle allows\n    // `preventDefault`.\n    batchedUpdates(handleTopLevel, bookKeeping);\n  } finally {\n    releaseTopLevelCallbackBookKeeping(bookKeeping);\n  }\n}\n\n/**\n * Summary of `ReactBrowserEventEmitter` event handling:\n *\n *  - Top-level delegation is used to trap most native browser events. This\n *    may only occur in the main thread and is the responsibility of\n *    ReactDOMEventListener, which is injected and can therefore support\n *    pluggable event sources. This is the only work that occurs in the main\n *    thread.\n *\n *  - We normalize and de-duplicate events to account for browser quirks. This\n *    may be done in the worker thread.\n *\n *  - Forward these native events (with the associated top-level type used to\n *    trap it) to `EventPluginHub`, which in turn will ask plugins if they want\n *    to extract any synthetic events.\n *\n *  - The `EventPluginHub` will then process each event by annotating them with\n *    \"dispatches\", a sequence of listeners and IDs that care about that event.\n *\n *  - The `EventPluginHub` then dispatches the events.\n *\n * Overview of React and the event system:\n *\n * +------------+    .\n * |    DOM     |    .\n * +------------+    .\n *       |           .\n *       v           .\n * +------------+    .\n * | ReactEvent |    .\n * |  Listener  |    .\n * +------------+    .                         +-----------+\n *       |           .               +--------+|SimpleEvent|\n *       |           .               |         |Plugin     |\n * +-----|------+    .               v         +-----------+\n * |     |      |    .    +--------------+                    +------------+\n * |     +-----------.--->|EventPluginHub|                    |    Event   |\n * |            |    .    |              |     +-----------+  | Propagators|\n * | ReactEvent |    .    |              |     |TapEvent   |  |------------|\n * |  Emitter   |    .    |              |<---+|Plugin     |  |other plugin|\n * |            |    .    |              |     +-----------+  |  utilities |\n * |     +-----------.--->|              |                    +------------+\n * |     |      |    .    +--------------+\n * +-----|------+    .                ^        +-----------+\n *       |           .                |        |Enter/Leave|\n *       +           .                +-------+|Plugin     |\n * +-------------+   .                         +-----------+\n * | application |   .\n * |-------------|   .\n * |             |   .\n * |             |   .\n * +-------------+   .\n *                   .\n *    React Core     .  General Purpose Event Plugin System\n */\n\nvar alreadyListeningTo = {};\nvar reactTopListenersCounter = 0;\n\n/**\n * To ensure no conflicts with other potential React instances on the page\n */\nvar topListenersIDKey = '_reactListenersID' + ('' + Math.random()).slice(2);\n\nfunction getListeningForDocument(mountAt) {\n  // In IE8, `mountAt` is a host object and doesn't have `hasOwnProperty`\n  // directly.\n  if (!Object.prototype.hasOwnProperty.call(mountAt, topListenersIDKey)) {\n    mountAt[topListenersIDKey] = reactTopListenersCounter++;\n    alreadyListeningTo[mountAt[topListenersIDKey]] = {};\n  }\n  return alreadyListeningTo[mountAt[topListenersIDKey]];\n}\n\n/**\n * We listen for bubbled touch events on the document object.\n *\n * Firefox v8.01 (and possibly others) exhibited strange behavior when\n * mounting `onmousemove` events at some node that was not the document\n * element. The symptoms were that if your mouse is not moving over something\n * contained within that mount point (for example on the background) the\n * top-level listeners for `onmousemove` won't be called. However, if you\n * register the `mousemove` on the document object, then it will of course\n * catch all `mousemove`s. This along with iOS quirks, justifies restricting\n * top-level listeners to the document object only, at least for these\n * movement types of events and possibly all events.\n *\n * @see http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html\n *\n * Also, `keyup`/`keypress`/`keydown` do not bubble to the window on IE, but\n * they bubble to document.\n *\n * @param {string} registrationName Name of listener (e.g. `onClick`).\n * @param {object} mountAt Container where to mount the listener\n */\nfunction listenTo(registrationName, mountAt) {\n  var isListening = getListeningForDocument(mountAt);\n  var dependencies = registrationNameDependencies[registrationName];\n\n  for (var i = 0; i < dependencies.length; i++) {\n    var dependency = dependencies[i];\n    if (!(isListening.hasOwnProperty(dependency) && isListening[dependency])) {\n      switch (dependency) {\n        case TOP_SCROLL:\n          trapCapturedEvent(TOP_SCROLL, mountAt);\n          break;\n        case TOP_FOCUS:\n        case TOP_BLUR:\n          trapCapturedEvent(TOP_FOCUS, mountAt);\n          trapCapturedEvent(TOP_BLUR, mountAt);\n          // We set the flag for a single dependency later in this function,\n          // but this ensures we mark both as attached rather than just one.\n          isListening[TOP_BLUR] = true;\n          isListening[TOP_FOCUS] = true;\n          break;\n        case TOP_CANCEL:\n        case TOP_CLOSE:\n          if (isEventSupported(getRawEventName(dependency))) {\n            trapCapturedEvent(dependency, mountAt);\n          }\n          break;\n        case TOP_INVALID:\n        case TOP_SUBMIT:\n        case TOP_RESET:\n          // We listen to them on the target DOM elements.\n          // Some of them bubble so we don't want them to fire twice.\n          break;\n        default:\n          // By default, listen on the top level to all non-media events.\n          // Media events don't bubble so adding the listener wouldn't do anything.\n          var isMediaEvent = mediaEventTypes.indexOf(dependency) !== -1;\n          if (!isMediaEvent) {\n            trapBubbledEvent(dependency, mountAt);\n          }\n          break;\n      }\n      isListening[dependency] = true;\n    }\n  }\n}\n\nfunction isListeningToAllDependencies(registrationName, mountAt) {\n  var isListening = getListeningForDocument(mountAt);\n  var dependencies = registrationNameDependencies[registrationName];\n  for (var i = 0; i < dependencies.length; i++) {\n    var dependency = dependencies[i];\n    if (!(isListening.hasOwnProperty(dependency) && isListening[dependency])) {\n      return false;\n    }\n  }\n  return true;\n}\n\nfunction getActiveElement(doc) {\n  doc = doc || (typeof document !== 'undefined' ? document : undefined);\n  if (typeof doc === 'undefined') {\n    return null;\n  }\n  try {\n    return doc.activeElement || doc.body;\n  } catch (e) {\n    return doc.body;\n  }\n}\n\n/**\n * Given any node return the first leaf node without children.\n *\n * @param {DOMElement|DOMTextNode} node\n * @return {DOMElement|DOMTextNode}\n */\nfunction getLeafNode(node) {\n  while (node && node.firstChild) {\n    node = node.firstChild;\n  }\n  return node;\n}\n\n/**\n * Get the next sibling within a container. This will walk up the\n * DOM if a node's siblings have been exhausted.\n *\n * @param {DOMElement|DOMTextNode} node\n * @return {?DOMElement|DOMTextNode}\n */\nfunction getSiblingNode(node) {\n  while (node) {\n    if (node.nextSibling) {\n      return node.nextSibling;\n    }\n    node = node.parentNode;\n  }\n}\n\n/**\n * Get object describing the nodes which contain characters at offset.\n *\n * @param {DOMElement|DOMTextNode} root\n * @param {number} offset\n * @return {?object}\n */\nfunction getNodeForCharacterOffset(root, offset) {\n  var node = getLeafNode(root);\n  var nodeStart = 0;\n  var nodeEnd = 0;\n\n  while (node) {\n    if (node.nodeType === TEXT_NODE) {\n      nodeEnd = nodeStart + node.textContent.length;\n\n      if (nodeStart <= offset && nodeEnd >= offset) {\n        return {\n          node: node,\n          offset: offset - nodeStart\n        };\n      }\n\n      nodeStart = nodeEnd;\n    }\n\n    node = getLeafNode(getSiblingNode(node));\n  }\n}\n\n/**\n * @param {DOMElement} outerNode\n * @return {?object}\n */\nfunction getOffsets(outerNode) {\n  var ownerDocument = outerNode.ownerDocument;\n\n  var win = ownerDocument && ownerDocument.defaultView || window;\n  var selection = win.getSelection && win.getSelection();\n\n  if (!selection || selection.rangeCount === 0) {\n    return null;\n  }\n\n  var anchorNode = selection.anchorNode,\n      anchorOffset = selection.anchorOffset,\n      focusNode = selection.focusNode,\n      focusOffset = selection.focusOffset;\n\n  // In Firefox, anchorNode and focusNode can be \"anonymous divs\", e.g. the\n  // up/down buttons on an <input type=\"number\">. Anonymous divs do not seem to\n  // expose properties, triggering a \"Permission denied error\" if any of its\n  // properties are accessed. The only seemingly possible way to avoid erroring\n  // is to access a property that typically works for non-anonymous divs and\n  // catch any error that may otherwise arise. See\n  // https://bugzilla.mozilla.org/show_bug.cgi?id=208427\n\n  try {\n    /* eslint-disable no-unused-expressions */\n    anchorNode.nodeType;\n    focusNode.nodeType;\n    /* eslint-enable no-unused-expressions */\n  } catch (e) {\n    return null;\n  }\n\n  return getModernOffsetsFromPoints(outerNode, anchorNode, anchorOffset, focusNode, focusOffset);\n}\n\n/**\n * Returns {start, end} where `start` is the character/codepoint index of\n * (anchorNode, anchorOffset) within the textContent of `outerNode`, and\n * `end` is the index of (focusNode, focusOffset).\n *\n * Returns null if you pass in garbage input but we should probably just crash.\n *\n * Exported only for testing.\n */\nfunction getModernOffsetsFromPoints(outerNode, anchorNode, anchorOffset, focusNode, focusOffset) {\n  var length = 0;\n  var start = -1;\n  var end = -1;\n  var indexWithinAnchor = 0;\n  var indexWithinFocus = 0;\n  var node = outerNode;\n  var parentNode = null;\n\n  outer: while (true) {\n    var next = null;\n\n    while (true) {\n      if (node === anchorNode && (anchorOffset === 0 || node.nodeType === TEXT_NODE)) {\n        start = length + anchorOffset;\n      }\n      if (node === focusNode && (focusOffset === 0 || node.nodeType === TEXT_NODE)) {\n        end = length + focusOffset;\n      }\n\n      if (node.nodeType === TEXT_NODE) {\n        length += node.nodeValue.length;\n      }\n\n      if ((next = node.firstChild) === null) {\n        break;\n      }\n      // Moving from `node` to its first child `next`.\n      parentNode = node;\n      node = next;\n    }\n\n    while (true) {\n      if (node === outerNode) {\n        // If `outerNode` has children, this is always the second time visiting\n        // it. If it has no children, this is still the first loop, and the only\n        // valid selection is anchorNode and focusNode both equal to this node\n        // and both offsets 0, in which case we will have handled above.\n        break outer;\n      }\n      if (parentNode === anchorNode && ++indexWithinAnchor === anchorOffset) {\n        start = length;\n      }\n      if (parentNode === focusNode && ++indexWithinFocus === focusOffset) {\n        end = length;\n      }\n      if ((next = node.nextSibling) !== null) {\n        break;\n      }\n      node = parentNode;\n      parentNode = node.parentNode;\n    }\n\n    // Moving from `node` to its next sibling `next`.\n    node = next;\n  }\n\n  if (start === -1 || end === -1) {\n    // This should never happen. (Would happen if the anchor/focus nodes aren't\n    // actually inside the passed-in node.)\n    return null;\n  }\n\n  return {\n    start: start,\n    end: end\n  };\n}\n\n/**\n * In modern non-IE browsers, we can support both forward and backward\n * selections.\n *\n * Note: IE10+ supports the Selection object, but it does not support\n * the `extend` method, which means that even in modern IE, it's not possible\n * to programmatically create a backward selection. Thus, for all IE\n * versions, we use the old IE API to create our selections.\n *\n * @param {DOMElement|DOMTextNode} node\n * @param {object} offsets\n */\nfunction setOffsets(node, offsets) {\n  var doc = node.ownerDocument || document;\n  var win = doc && doc.defaultView || window;\n\n  // Edge fails with \"Object expected\" in some scenarios.\n  // (For instance: TinyMCE editor used in a list component that supports pasting to add more,\n  // fails when pasting 100+ items)\n  if (!win.getSelection) {\n    return;\n  }\n\n  var selection = win.getSelection();\n  var length = node.textContent.length;\n  var start = Math.min(offsets.start, length);\n  var end = offsets.end === undefined ? start : Math.min(offsets.end, length);\n\n  // IE 11 uses modern selection, but doesn't support the extend method.\n  // Flip backward selections, so we can set with a single range.\n  if (!selection.extend && start > end) {\n    var temp = end;\n    end = start;\n    start = temp;\n  }\n\n  var startMarker = getNodeForCharacterOffset(node, start);\n  var endMarker = getNodeForCharacterOffset(node, end);\n\n  if (startMarker && endMarker) {\n    if (selection.rangeCount === 1 && selection.anchorNode === startMarker.node && selection.anchorOffset === startMarker.offset && selection.focusNode === endMarker.node && selection.focusOffset === endMarker.offset) {\n      return;\n    }\n    var range = doc.createRange();\n    range.setStart(startMarker.node, startMarker.offset);\n    selection.removeAllRanges();\n\n    if (start > end) {\n      selection.addRange(range);\n      selection.extend(endMarker.node, endMarker.offset);\n    } else {\n      range.setEnd(endMarker.node, endMarker.offset);\n      selection.addRange(range);\n    }\n  }\n}\n\nfunction isTextNode(node) {\n  return node && node.nodeType === TEXT_NODE;\n}\n\nfunction containsNode(outerNode, innerNode) {\n  if (!outerNode || !innerNode) {\n    return false;\n  } else if (outerNode === innerNode) {\n    return true;\n  } else if (isTextNode(outerNode)) {\n    return false;\n  } else if (isTextNode(innerNode)) {\n    return containsNode(outerNode, innerNode.parentNode);\n  } else if ('contains' in outerNode) {\n    return outerNode.contains(innerNode);\n  } else if (outerNode.compareDocumentPosition) {\n    return !!(outerNode.compareDocumentPosition(innerNode) & 16);\n  } else {\n    return false;\n  }\n}\n\nfunction isInDocument(node) {\n  return node && node.ownerDocument && containsNode(node.ownerDocument.documentElement, node);\n}\n\nfunction isSameOriginFrame(iframe) {\n  try {\n    // Accessing the contentDocument of a HTMLIframeElement can cause the browser\n    // to throw, e.g. if it has a cross-origin src attribute.\n    // Safari will show an error in the console when the access results in \"Blocked a frame with origin\". e.g:\n    // iframe.contentDocument.defaultView;\n    // A safety way is to access one of the cross origin properties: Window or Location\n    // Which might result in \"SecurityError\" DOM Exception and it is compatible to Safari.\n    // https://html.spec.whatwg.org/multipage/browsers.html#integration-with-idl\n\n    return typeof iframe.contentWindow.location.href === 'string';\n  } catch (err) {\n    return false;\n  }\n}\n\nfunction getActiveElementDeep() {\n  var win = window;\n  var element = getActiveElement();\n  while (element instanceof win.HTMLIFrameElement) {\n    if (isSameOriginFrame(element)) {\n      win = element.contentWindow;\n    } else {\n      return element;\n    }\n    element = getActiveElement(win.document);\n  }\n  return element;\n}\n\n/**\n * @ReactInputSelection: React input selection module. Based on Selection.js,\n * but modified to be suitable for react and has a couple of bug fixes (doesn't\n * assume buttons have range selections allowed).\n * Input selection module for React.\n */\n\n/**\n * @hasSelectionCapabilities: we get the element types that support selection\n * from https://html.spec.whatwg.org/#do-not-apply, looking at `selectionStart`\n * and `selectionEnd` rows.\n */\nfunction hasSelectionCapabilities(elem) {\n  var nodeName = elem && elem.nodeName && elem.nodeName.toLowerCase();\n  return nodeName && (nodeName === 'input' && (elem.type === 'text' || elem.type === 'search' || elem.type === 'tel' || elem.type === 'url' || elem.type === 'password') || nodeName === 'textarea' || elem.contentEditable === 'true');\n}\n\nfunction getSelectionInformation() {\n  var focusedElem = getActiveElementDeep();\n  return {\n    focusedElem: focusedElem,\n    selectionRange: hasSelectionCapabilities(focusedElem) ? getSelection$1(focusedElem) : null\n  };\n}\n\n/**\n * @restoreSelection: If any selection information was potentially lost,\n * restore it. This is useful when performing operations that could remove dom\n * nodes and place them back in, resulting in focus being lost.\n */\nfunction restoreSelection(priorSelectionInformation) {\n  var curFocusedElem = getActiveElementDeep();\n  var priorFocusedElem = priorSelectionInformation.focusedElem;\n  var priorSelectionRange = priorSelectionInformation.selectionRange;\n  if (curFocusedElem !== priorFocusedElem && isInDocument(priorFocusedElem)) {\n    if (priorSelectionRange !== null && hasSelectionCapabilities(priorFocusedElem)) {\n      setSelection(priorFocusedElem, priorSelectionRange);\n    }\n\n    // Focusing a node can change the scroll position, which is undesirable\n    var ancestors = [];\n    var ancestor = priorFocusedElem;\n    while (ancestor = ancestor.parentNode) {\n      if (ancestor.nodeType === ELEMENT_NODE) {\n        ancestors.push({\n          element: ancestor,\n          left: ancestor.scrollLeft,\n          top: ancestor.scrollTop\n        });\n      }\n    }\n\n    if (typeof priorFocusedElem.focus === 'function') {\n      priorFocusedElem.focus();\n    }\n\n    for (var i = 0; i < ancestors.length; i++) {\n      var info = ancestors[i];\n      info.element.scrollLeft = info.left;\n      info.element.scrollTop = info.top;\n    }\n  }\n}\n\n/**\n * @getSelection: Gets the selection bounds of a focused textarea, input or\n * contentEditable node.\n * -@input: Look up selection bounds of this input\n * -@return {start: selectionStart, end: selectionEnd}\n */\nfunction getSelection$1(input) {\n  var selection = void 0;\n\n  if ('selectionStart' in input) {\n    // Modern browser with input or textarea.\n    selection = {\n      start: input.selectionStart,\n      end: input.selectionEnd\n    };\n  } else {\n    // Content editable or old IE textarea.\n    selection = getOffsets(input);\n  }\n\n  return selection || { start: 0, end: 0 };\n}\n\n/**\n * @setSelection: Sets the selection bounds of a textarea or input and focuses\n * the input.\n * -@input     Set selection bounds of this input or textarea\n * -@offsets   Object of same form that is returned from get*\n */\nfunction setSelection(input, offsets) {\n  var start = offsets.start,\n      end = offsets.end;\n\n  if (end === undefined) {\n    end = start;\n  }\n\n  if ('selectionStart' in input) {\n    input.selectionStart = start;\n    input.selectionEnd = Math.min(end, input.value.length);\n  } else {\n    setOffsets(input, offsets);\n  }\n}\n\nvar skipSelectionChangeEvent = canUseDOM && 'documentMode' in document && document.documentMode <= 11;\n\nvar eventTypes$3 = {\n  select: {\n    phasedRegistrationNames: {\n      bubbled: 'onSelect',\n      captured: 'onSelectCapture'\n    },\n    dependencies: [TOP_BLUR, TOP_CONTEXT_MENU, TOP_DRAG_END, TOP_FOCUS, TOP_KEY_DOWN, TOP_KEY_UP, TOP_MOUSE_DOWN, TOP_MOUSE_UP, TOP_SELECTION_CHANGE]\n  }\n};\n\nvar activeElement$1 = null;\nvar activeElementInst$1 = null;\nvar lastSelection = null;\nvar mouseDown = false;\n\n/**\n * Get an object which is a unique representation of the current selection.\n *\n * The return value will not be consistent across nodes or browsers, but\n * two identical selections on the same node will return identical objects.\n *\n * @param {DOMElement} node\n * @return {object}\n */\nfunction getSelection(node) {\n  if ('selectionStart' in node && hasSelectionCapabilities(node)) {\n    return {\n      start: node.selectionStart,\n      end: node.selectionEnd\n    };\n  } else {\n    var win = node.ownerDocument && node.ownerDocument.defaultView || window;\n    var selection = win.getSelection();\n    return {\n      anchorNode: selection.anchorNode,\n      anchorOffset: selection.anchorOffset,\n      focusNode: selection.focusNode,\n      focusOffset: selection.focusOffset\n    };\n  }\n}\n\n/**\n * Get document associated with the event target.\n *\n * @param {object} nativeEventTarget\n * @return {Document}\n */\nfunction getEventTargetDocument(eventTarget) {\n  return eventTarget.window === eventTarget ? eventTarget.document : eventTarget.nodeType === DOCUMENT_NODE ? eventTarget : eventTarget.ownerDocument;\n}\n\n/**\n * Poll selection to see whether it's changed.\n *\n * @param {object} nativeEvent\n * @param {object} nativeEventTarget\n * @return {?SyntheticEvent}\n */\nfunction constructSelectEvent(nativeEvent, nativeEventTarget) {\n  // Ensure we have the right element, and that the user is not dragging a\n  // selection (this matches native `select` event behavior). In HTML5, select\n  // fires only on input and textarea thus if there's no focused element we\n  // won't dispatch.\n  var doc = getEventTargetDocument(nativeEventTarget);\n\n  if (mouseDown || activeElement$1 == null || activeElement$1 !== getActiveElement(doc)) {\n    return null;\n  }\n\n  // Only fire when selection has actually changed.\n  var currentSelection = getSelection(activeElement$1);\n  if (!lastSelection || !shallowEqual(lastSelection, currentSelection)) {\n    lastSelection = currentSelection;\n\n    var syntheticEvent = SyntheticEvent.getPooled(eventTypes$3.select, activeElementInst$1, nativeEvent, nativeEventTarget);\n\n    syntheticEvent.type = 'select';\n    syntheticEvent.target = activeElement$1;\n\n    accumulateTwoPhaseDispatches(syntheticEvent);\n\n    return syntheticEvent;\n  }\n\n  return null;\n}\n\n/**\n * This plugin creates an `onSelect` event that normalizes select events\n * across form elements.\n *\n * Supported elements are:\n * - input (see `isTextInputElement`)\n * - textarea\n * - contentEditable\n *\n * This differs from native browser implementations in the following ways:\n * - Fires on contentEditable fields as well as inputs.\n * - Fires for collapsed selection.\n * - Fires after user input.\n */\nvar SelectEventPlugin = {\n  eventTypes: eventTypes$3,\n\n  extractEvents: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {\n    var doc = getEventTargetDocument(nativeEventTarget);\n    // Track whether all listeners exists for this plugin. If none exist, we do\n    // not extract events. See #3639.\n    if (!doc || !isListeningToAllDependencies('onSelect', doc)) {\n      return null;\n    }\n\n    var targetNode = targetInst ? getNodeFromInstance$1(targetInst) : window;\n\n    switch (topLevelType) {\n      // Track the input node that has focus.\n      case TOP_FOCUS:\n        if (isTextInputElement(targetNode) || targetNode.contentEditable === 'true') {\n          activeElement$1 = targetNode;\n          activeElementInst$1 = targetInst;\n          lastSelection = null;\n        }\n        break;\n      case TOP_BLUR:\n        activeElement$1 = null;\n        activeElementInst$1 = null;\n        lastSelection = null;\n        break;\n      // Don't fire the event while the user is dragging. This matches the\n      // semantics of the native select event.\n      case TOP_MOUSE_DOWN:\n        mouseDown = true;\n        break;\n      case TOP_CONTEXT_MENU:\n      case TOP_MOUSE_UP:\n      case TOP_DRAG_END:\n        mouseDown = false;\n        return constructSelectEvent(nativeEvent, nativeEventTarget);\n      // Chrome and IE fire non-standard event when selection is changed (and\n      // sometimes when it hasn't). IE's event fires out of order with respect\n      // to key and input events on deletion, so we discard it.\n      //\n      // Firefox doesn't support selectionchange, so check selection status\n      // after each key entry. The selection changes after keydown and before\n      // keyup, but we check on keydown as well in the case of holding down a\n      // key, when multiple keydown events are fired but only one keyup is.\n      // This is also our approach for IE handling, for the reason above.\n      case TOP_SELECTION_CHANGE:\n        if (skipSelectionChangeEvent) {\n          break;\n        }\n      // falls through\n      case TOP_KEY_DOWN:\n      case TOP_KEY_UP:\n        return constructSelectEvent(nativeEvent, nativeEventTarget);\n    }\n\n    return null;\n  }\n};\n\n/**\n * Inject modules for resolving DOM hierarchy and plugin ordering.\n */\ninjection.injectEventPluginOrder(DOMEventPluginOrder);\nsetComponentTree(getFiberCurrentPropsFromNode$1, getInstanceFromNode$1, getNodeFromInstance$1);\n\n/**\n * Some important event plugins included by default (without having to require\n * them).\n */\ninjection.injectEventPluginsByName({\n  SimpleEventPlugin: SimpleEventPlugin,\n  EnterLeaveEventPlugin: EnterLeaveEventPlugin,\n  ChangeEventPlugin: ChangeEventPlugin,\n  SelectEventPlugin: SelectEventPlugin,\n  BeforeInputEventPlugin: BeforeInputEventPlugin\n});\n\nvar didWarnSelectedSetOnOption = false;\nvar didWarnInvalidChild = false;\n\nfunction flattenChildren(children) {\n  var content = '';\n\n  // Flatten children. We'll warn if they are invalid\n  // during validateProps() which runs for hydration too.\n  // Note that this would throw on non-element objects.\n  // Elements are stringified (which is normally irrelevant\n  // but matters for <fbt>).\n  React.Children.forEach(children, function (child) {\n    if (child == null) {\n      return;\n    }\n    content += child;\n    // Note: we don't warn about invalid children here.\n    // Instead, this is done separately below so that\n    // it happens during the hydration codepath too.\n  });\n\n  return content;\n}\n\n/**\n * Implements an <option> host component that warns when `selected` is set.\n */\n\nfunction validateProps(element, props) {\n  {\n    // This mirrors the codepath above, but runs for hydration too.\n    // Warn about invalid children here so that client and hydration are consistent.\n    // TODO: this seems like it could cause a DEV-only throw for hydration\n    // if children contains a non-element object. We should try to avoid that.\n    if (typeof props.children === 'object' && props.children !== null) {\n      React.Children.forEach(props.children, function (child) {\n        if (child == null) {\n          return;\n        }\n        if (typeof child === 'string' || typeof child === 'number') {\n          return;\n        }\n        if (typeof child.type !== 'string') {\n          return;\n        }\n        if (!didWarnInvalidChild) {\n          didWarnInvalidChild = true;\n          warning$1(false, 'Only strings and numbers are supported as <option> children.');\n        }\n      });\n    }\n\n    // TODO: Remove support for `selected` in <option>.\n    if (props.selected != null && !didWarnSelectedSetOnOption) {\n      warning$1(false, 'Use the `defaultValue` or `value` props on <select> instead of ' + 'setting `selected` on <option>.');\n      didWarnSelectedSetOnOption = true;\n    }\n  }\n}\n\nfunction postMountWrapper$1(element, props) {\n  // value=\"\" should make a value attribute (#6219)\n  if (props.value != null) {\n    element.setAttribute('value', toString(getToStringValue(props.value)));\n  }\n}\n\nfunction getHostProps$1(element, props) {\n  var hostProps = _assign({ children: undefined }, props);\n  var content = flattenChildren(props.children);\n\n  if (content) {\n    hostProps.children = content;\n  }\n\n  return hostProps;\n}\n\n// TODO: direct imports like some-package/src/* are bad. Fix me.\nvar didWarnValueDefaultValue$1 = void 0;\n\n{\n  didWarnValueDefaultValue$1 = false;\n}\n\nfunction getDeclarationErrorAddendum() {\n  var ownerName = getCurrentFiberOwnerNameInDevOrNull();\n  if (ownerName) {\n    return '\\n\\nCheck the render method of `' + ownerName + '`.';\n  }\n  return '';\n}\n\nvar valuePropNames = ['value', 'defaultValue'];\n\n/**\n * Validation function for `value` and `defaultValue`.\n */\nfunction checkSelectPropTypes(props) {\n  ReactControlledValuePropTypes.checkPropTypes('select', props);\n\n  for (var i = 0; i < valuePropNames.length; i++) {\n    var propName = valuePropNames[i];\n    if (props[propName] == null) {\n      continue;\n    }\n    var isArray = Array.isArray(props[propName]);\n    if (props.multiple && !isArray) {\n      warning$1(false, 'The `%s` prop supplied to <select> must be an array if ' + '`multiple` is true.%s', propName, getDeclarationErrorAddendum());\n    } else if (!props.multiple && isArray) {\n      warning$1(false, 'The `%s` prop supplied to <select> must be a scalar ' + 'value if `multiple` is false.%s', propName, getDeclarationErrorAddendum());\n    }\n  }\n}\n\nfunction updateOptions(node, multiple, propValue, setDefaultSelected) {\n  var options = node.options;\n\n  if (multiple) {\n    var selectedValues = propValue;\n    var selectedValue = {};\n    for (var i = 0; i < selectedValues.length; i++) {\n      // Prefix to avoid chaos with special keys.\n      selectedValue['$' + selectedValues[i]] = true;\n    }\n    for (var _i = 0; _i < options.length; _i++) {\n      var selected = selectedValue.hasOwnProperty('$' + options[_i].value);\n      if (options[_i].selected !== selected) {\n        options[_i].selected = selected;\n      }\n      if (selected && setDefaultSelected) {\n        options[_i].defaultSelected = true;\n      }\n    }\n  } else {\n    // Do not set `select.value` as exact behavior isn't consistent across all\n    // browsers for all cases.\n    var _selectedValue = toString(getToStringValue(propValue));\n    var defaultSelected = null;\n    for (var _i2 = 0; _i2 < options.length; _i2++) {\n      if (options[_i2].value === _selectedValue) {\n        options[_i2].selected = true;\n        if (setDefaultSelected) {\n          options[_i2].defaultSelected = true;\n        }\n        return;\n      }\n      if (defaultSelected === null && !options[_i2].disabled) {\n        defaultSelected = options[_i2];\n      }\n    }\n    if (defaultSelected !== null) {\n      defaultSelected.selected = true;\n    }\n  }\n}\n\n/**\n * Implements a <select> host component that allows optionally setting the\n * props `value` and `defaultValue`. If `multiple` is false, the prop must be a\n * stringable. If `multiple` is true, the prop must be an array of stringables.\n *\n * If `value` is not supplied (or null/undefined), user actions that change the\n * selected option will trigger updates to the rendered options.\n *\n * If it is supplied (and not null/undefined), the rendered options will not\n * update in response to user actions. Instead, the `value` prop must change in\n * order for the rendered options to update.\n *\n * If `defaultValue` is provided, any options with the supplied values will be\n * selected.\n */\n\nfunction getHostProps$2(element, props) {\n  return _assign({}, props, {\n    value: undefined\n  });\n}\n\nfunction initWrapperState$1(element, props) {\n  var node = element;\n  {\n    checkSelectPropTypes(props);\n  }\n\n  node._wrapperState = {\n    wasMultiple: !!props.multiple\n  };\n\n  {\n    if (props.value !== undefined && props.defaultValue !== undefined && !didWarnValueDefaultValue$1) {\n      warning$1(false, 'Select elements must be either controlled or uncontrolled ' + '(specify either the value prop, or the defaultValue prop, but not ' + 'both). Decide between using a controlled or uncontrolled select ' + 'element and remove one of these props. More info: ' + 'https://fb.me/react-controlled-components');\n      didWarnValueDefaultValue$1 = true;\n    }\n  }\n}\n\nfunction postMountWrapper$2(element, props) {\n  var node = element;\n  node.multiple = !!props.multiple;\n  var value = props.value;\n  if (value != null) {\n    updateOptions(node, !!props.multiple, value, false);\n  } else if (props.defaultValue != null) {\n    updateOptions(node, !!props.multiple, props.defaultValue, true);\n  }\n}\n\nfunction postUpdateWrapper(element, props) {\n  var node = element;\n  var wasMultiple = node._wrapperState.wasMultiple;\n  node._wrapperState.wasMultiple = !!props.multiple;\n\n  var value = props.value;\n  if (value != null) {\n    updateOptions(node, !!props.multiple, value, false);\n  } else if (wasMultiple !== !!props.multiple) {\n    // For simplicity, reapply `defaultValue` if `multiple` is toggled.\n    if (props.defaultValue != null) {\n      updateOptions(node, !!props.multiple, props.defaultValue, true);\n    } else {\n      // Revert the select back to its default unselected state.\n      updateOptions(node, !!props.multiple, props.multiple ? [] : '', false);\n    }\n  }\n}\n\nfunction restoreControlledState$2(element, props) {\n  var node = element;\n  var value = props.value;\n\n  if (value != null) {\n    updateOptions(node, !!props.multiple, value, false);\n  }\n}\n\nvar didWarnValDefaultVal = false;\n\n/**\n * Implements a <textarea> host component that allows setting `value`, and\n * `defaultValue`. This differs from the traditional DOM API because value is\n * usually set as PCDATA children.\n *\n * If `value` is not supplied (or null/undefined), user actions that affect the\n * value will trigger updates to the element.\n *\n * If `value` is supplied (and not null/undefined), the rendered element will\n * not trigger updates to the element. Instead, the `value` prop must change in\n * order for the rendered element to be updated.\n *\n * The rendered element will be initialized with an empty value, the prop\n * `defaultValue` if specified, or the children content (deprecated).\n */\n\nfunction getHostProps$3(element, props) {\n  var node = element;\n  !(props.dangerouslySetInnerHTML == null) ? invariant(false, '`dangerouslySetInnerHTML` does not make sense on <textarea>.') : void 0;\n\n  // Always set children to the same thing. In IE9, the selection range will\n  // get reset if `textContent` is mutated.  We could add a check in setTextContent\n  // to only set the value if/when the value differs from the node value (which would\n  // completely solve this IE9 bug), but Sebastian+Sophie seemed to like this\n  // solution. The value can be a boolean or object so that's why it's forced\n  // to be a string.\n  var hostProps = _assign({}, props, {\n    value: undefined,\n    defaultValue: undefined,\n    children: toString(node._wrapperState.initialValue)\n  });\n\n  return hostProps;\n}\n\nfunction initWrapperState$2(element, props) {\n  var node = element;\n  {\n    ReactControlledValuePropTypes.checkPropTypes('textarea', props);\n    if (props.value !== undefined && props.defaultValue !== undefined && !didWarnValDefaultVal) {\n      warning$1(false, '%s contains a textarea with both value and defaultValue props. ' + 'Textarea elements must be either controlled or uncontrolled ' + '(specify either the value prop, or the defaultValue prop, but not ' + 'both). Decide between using a controlled or uncontrolled textarea ' + 'and remove one of these props. More info: ' + 'https://fb.me/react-controlled-components', getCurrentFiberOwnerNameInDevOrNull() || 'A component');\n      didWarnValDefaultVal = true;\n    }\n  }\n\n  var initialValue = props.value;\n\n  // Only bother fetching default value if we're going to use it\n  if (initialValue == null) {\n    var defaultValue = props.defaultValue;\n    // TODO (yungsters): Remove support for children content in <textarea>.\n    var children = props.children;\n    if (children != null) {\n      {\n        warning$1(false, 'Use the `defaultValue` or `value` props instead of setting ' + 'children on <textarea>.');\n      }\n      !(defaultValue == null) ? invariant(false, 'If you supply `defaultValue` on a <textarea>, do not pass children.') : void 0;\n      if (Array.isArray(children)) {\n        !(children.length <= 1) ? invariant(false, '<textarea> can only have at most one child.') : void 0;\n        children = children[0];\n      }\n\n      defaultValue = children;\n    }\n    if (defaultValue == null) {\n      defaultValue = '';\n    }\n    initialValue = defaultValue;\n  }\n\n  node._wrapperState = {\n    initialValue: getToStringValue(initialValue)\n  };\n}\n\nfunction updateWrapper$1(element, props) {\n  var node = element;\n  var value = getToStringValue(props.value);\n  var defaultValue = getToStringValue(props.defaultValue);\n  if (value != null) {\n    // Cast `value` to a string to ensure the value is set correctly. While\n    // browsers typically do this as necessary, jsdom doesn't.\n    var newValue = toString(value);\n    // To avoid side effects (such as losing text selection), only set value if changed\n    if (newValue !== node.value) {\n      node.value = newValue;\n    }\n    if (props.defaultValue == null && node.defaultValue !== newValue) {\n      node.defaultValue = newValue;\n    }\n  }\n  if (defaultValue != null) {\n    node.defaultValue = toString(defaultValue);\n  }\n}\n\nfunction postMountWrapper$3(element, props) {\n  var node = element;\n  // This is in postMount because we need access to the DOM node, which is not\n  // available until after the component has mounted.\n  var textContent = node.textContent;\n\n  // Only set node.value if textContent is equal to the expected\n  // initial value. In IE10/IE11 there is a bug where the placeholder attribute\n  // will populate textContent as well.\n  // https://developer.microsoft.com/microsoft-edge/platform/issues/101525/\n  if (textContent === node._wrapperState.initialValue) {\n    node.value = textContent;\n  }\n}\n\nfunction restoreControlledState$3(element, props) {\n  // DOM component is still mounted; update\n  updateWrapper$1(element, props);\n}\n\nvar HTML_NAMESPACE$1 = 'http://www.w3.org/1999/xhtml';\nvar MATH_NAMESPACE = 'http://www.w3.org/1998/Math/MathML';\nvar SVG_NAMESPACE = 'http://www.w3.org/2000/svg';\n\nvar Namespaces = {\n  html: HTML_NAMESPACE$1,\n  mathml: MATH_NAMESPACE,\n  svg: SVG_NAMESPACE\n};\n\n// Assumes there is no parent namespace.\nfunction getIntrinsicNamespace(type) {\n  switch (type) {\n    case 'svg':\n      return SVG_NAMESPACE;\n    case 'math':\n      return MATH_NAMESPACE;\n    default:\n      return HTML_NAMESPACE$1;\n  }\n}\n\nfunction getChildNamespace(parentNamespace, type) {\n  if (parentNamespace == null || parentNamespace === HTML_NAMESPACE$1) {\n    // No (or default) parent namespace: potential entry point.\n    return getIntrinsicNamespace(type);\n  }\n  if (parentNamespace === SVG_NAMESPACE && type === 'foreignObject') {\n    // We're leaving SVG.\n    return HTML_NAMESPACE$1;\n  }\n  // By default, pass namespace below.\n  return parentNamespace;\n}\n\n/* globals MSApp */\n\n/**\n * Create a function which has 'unsafe' privileges (required by windows8 apps)\n */\nvar createMicrosoftUnsafeLocalFunction = function (func) {\n  if (typeof MSApp !== 'undefined' && MSApp.execUnsafeLocalFunction) {\n    return function (arg0, arg1, arg2, arg3) {\n      MSApp.execUnsafeLocalFunction(function () {\n        return func(arg0, arg1, arg2, arg3);\n      });\n    };\n  } else {\n    return func;\n  }\n};\n\n// SVG temp container for IE lacking innerHTML\nvar reusableSVGContainer = void 0;\n\n/**\n * Set the innerHTML property of a node\n *\n * @param {DOMElement} node\n * @param {string} html\n * @internal\n */\nvar setInnerHTML = createMicrosoftUnsafeLocalFunction(function (node, html) {\n  // IE does not have innerHTML for SVG nodes, so instead we inject the\n  // new markup in a temp node and then move the child nodes across into\n  // the target node\n\n  if (node.namespaceURI === Namespaces.svg && !('innerHTML' in node)) {\n    reusableSVGContainer = reusableSVGContainer || document.createElement('div');\n    reusableSVGContainer.innerHTML = '<svg>' + html + '</svg>';\n    var svgNode = reusableSVGContainer.firstChild;\n    while (node.firstChild) {\n      node.removeChild(node.firstChild);\n    }\n    while (svgNode.firstChild) {\n      node.appendChild(svgNode.firstChild);\n    }\n  } else {\n    node.innerHTML = html;\n  }\n});\n\n/**\n * Set the textContent property of a node. For text updates, it's faster\n * to set the `nodeValue` of the Text node directly instead of using\n * `.textContent` which will remove the existing node and create a new one.\n *\n * @param {DOMElement} node\n * @param {string} text\n * @internal\n */\nvar setTextContent = function (node, text) {\n  if (text) {\n    var firstChild = node.firstChild;\n\n    if (firstChild && firstChild === node.lastChild && firstChild.nodeType === TEXT_NODE) {\n      firstChild.nodeValue = text;\n      return;\n    }\n  }\n  node.textContent = text;\n};\n\n// List derived from Gecko source code:\n// https://github.com/mozilla/gecko-dev/blob/4e638efc71/layout/style/test/property_database.js\nvar shorthandToLonghand = {\n  animation: ['animationDelay', 'animationDirection', 'animationDuration', 'animationFillMode', 'animationIterationCount', 'animationName', 'animationPlayState', 'animationTimingFunction'],\n  background: ['backgroundAttachment', 'backgroundClip', 'backgroundColor', 'backgroundImage', 'backgroundOrigin', 'backgroundPositionX', 'backgroundPositionY', 'backgroundRepeat', 'backgroundSize'],\n  backgroundPosition: ['backgroundPositionX', 'backgroundPositionY'],\n  border: ['borderBottomColor', 'borderBottomStyle', 'borderBottomWidth', 'borderImageOutset', 'borderImageRepeat', 'borderImageSlice', 'borderImageSource', 'borderImageWidth', 'borderLeftColor', 'borderLeftStyle', 'borderLeftWidth', 'borderRightColor', 'borderRightStyle', 'borderRightWidth', 'borderTopColor', 'borderTopStyle', 'borderTopWidth'],\n  borderBlockEnd: ['borderBlockEndColor', 'borderBlockEndStyle', 'borderBlockEndWidth'],\n  borderBlockStart: ['borderBlockStartColor', 'borderBlockStartStyle', 'borderBlockStartWidth'],\n  borderBottom: ['borderBottomColor', 'borderBottomStyle', 'borderBottomWidth'],\n  borderColor: ['borderBottomColor', 'borderLeftColor', 'borderRightColor', 'borderTopColor'],\n  borderImage: ['borderImageOutset', 'borderImageRepeat', 'borderImageSlice', 'borderImageSource', 'borderImageWidth'],\n  borderInlineEnd: ['borderInlineEndColor', 'borderInlineEndStyle', 'borderInlineEndWidth'],\n  borderInlineStart: ['borderInlineStartColor', 'borderInlineStartStyle', 'borderInlineStartWidth'],\n  borderLeft: ['borderLeftColor', 'borderLeftStyle', 'borderLeftWidth'],\n  borderRadius: ['borderBottomLeftRadius', 'borderBottomRightRadius', 'borderTopLeftRadius', 'borderTopRightRadius'],\n  borderRight: ['borderRightColor', 'borderRightStyle', 'borderRightWidth'],\n  borderStyle: ['borderBottomStyle', 'borderLeftStyle', 'borderRightStyle', 'borderTopStyle'],\n  borderTop: ['borderTopColor', 'borderTopStyle', 'borderTopWidth'],\n  borderWidth: ['borderBottomWidth', 'borderLeftWidth', 'borderRightWidth', 'borderTopWidth'],\n  columnRule: ['columnRuleColor', 'columnRuleStyle', 'columnRuleWidth'],\n  columns: ['columnCount', 'columnWidth'],\n  flex: ['flexBasis', 'flexGrow', 'flexShrink'],\n  flexFlow: ['flexDirection', 'flexWrap'],\n  font: ['fontFamily', 'fontFeatureSettings', 'fontKerning', 'fontLanguageOverride', 'fontSize', 'fontSizeAdjust', 'fontStretch', 'fontStyle', 'fontVariant', 'fontVariantAlternates', 'fontVariantCaps', 'fontVariantEastAsian', 'fontVariantLigatures', 'fontVariantNumeric', 'fontVariantPosition', 'fontWeight', 'lineHeight'],\n  fontVariant: ['fontVariantAlternates', 'fontVariantCaps', 'fontVariantEastAsian', 'fontVariantLigatures', 'fontVariantNumeric', 'fontVariantPosition'],\n  gap: ['columnGap', 'rowGap'],\n  grid: ['gridAutoColumns', 'gridAutoFlow', 'gridAutoRows', 'gridTemplateAreas', 'gridTemplateColumns', 'gridTemplateRows'],\n  gridArea: ['gridColumnEnd', 'gridColumnStart', 'gridRowEnd', 'gridRowStart'],\n  gridColumn: ['gridColumnEnd', 'gridColumnStart'],\n  gridColumnGap: ['columnGap'],\n  gridGap: ['columnGap', 'rowGap'],\n  gridRow: ['gridRowEnd', 'gridRowStart'],\n  gridRowGap: ['rowGap'],\n  gridTemplate: ['gridTemplateAreas', 'gridTemplateColumns', 'gridTemplateRows'],\n  listStyle: ['listStyleImage', 'listStylePosition', 'listStyleType'],\n  margin: ['marginBottom', 'marginLeft', 'marginRight', 'marginTop'],\n  marker: ['markerEnd', 'markerMid', 'markerStart'],\n  mask: ['maskClip', 'maskComposite', 'maskImage', 'maskMode', 'maskOrigin', 'maskPositionX', 'maskPositionY', 'maskRepeat', 'maskSize'],\n  maskPosition: ['maskPositionX', 'maskPositionY'],\n  outline: ['outlineColor', 'outlineStyle', 'outlineWidth'],\n  overflow: ['overflowX', 'overflowY'],\n  padding: ['paddingBottom', 'paddingLeft', 'paddingRight', 'paddingTop'],\n  placeContent: ['alignContent', 'justifyContent'],\n  placeItems: ['alignItems', 'justifyItems'],\n  placeSelf: ['alignSelf', 'justifySelf'],\n  textDecoration: ['textDecorationColor', 'textDecorationLine', 'textDecorationStyle'],\n  textEmphasis: ['textEmphasisColor', 'textEmphasisStyle'],\n  transition: ['transitionDelay', 'transitionDuration', 'transitionProperty', 'transitionTimingFunction'],\n  wordWrap: ['overflowWrap']\n};\n\n/**\n * CSS properties which accept numbers but are not in units of \"px\".\n */\nvar isUnitlessNumber = {\n  animationIterationCount: true,\n  borderImageOutset: true,\n  borderImageSlice: true,\n  borderImageWidth: true,\n  boxFlex: true,\n  boxFlexGroup: true,\n  boxOrdinalGroup: true,\n  columnCount: true,\n  columns: true,\n  flex: true,\n  flexGrow: true,\n  flexPositive: true,\n  flexShrink: true,\n  flexNegative: true,\n  flexOrder: true,\n  gridArea: true,\n  gridRow: true,\n  gridRowEnd: true,\n  gridRowSpan: true,\n  gridRowStart: true,\n  gridColumn: true,\n  gridColumnEnd: true,\n  gridColumnSpan: true,\n  gridColumnStart: true,\n  fontWeight: true,\n  lineClamp: true,\n  lineHeight: true,\n  opacity: true,\n  order: true,\n  orphans: true,\n  tabSize: true,\n  widows: true,\n  zIndex: true,\n  zoom: true,\n\n  // SVG-related properties\n  fillOpacity: true,\n  floodOpacity: true,\n  stopOpacity: true,\n  strokeDasharray: true,\n  strokeDashoffset: true,\n  strokeMiterlimit: true,\n  strokeOpacity: true,\n  strokeWidth: true\n};\n\n/**\n * @param {string} prefix vendor-specific prefix, eg: Webkit\n * @param {string} key style name, eg: transitionDuration\n * @return {string} style name prefixed with `prefix`, properly camelCased, eg:\n * WebkitTransitionDuration\n */\nfunction prefixKey(prefix, key) {\n  return prefix + key.charAt(0).toUpperCase() + key.substring(1);\n}\n\n/**\n * Support style names that may come passed in prefixed by adding permutations\n * of vendor prefixes.\n */\nvar prefixes = ['Webkit', 'ms', 'Moz', 'O'];\n\n// Using Object.keys here, or else the vanilla for-in loop makes IE8 go into an\n// infinite loop, because it iterates over the newly added props too.\nObject.keys(isUnitlessNumber).forEach(function (prop) {\n  prefixes.forEach(function (prefix) {\n    isUnitlessNumber[prefixKey(prefix, prop)] = isUnitlessNumber[prop];\n  });\n});\n\n/**\n * Convert a value into the proper css writable value. The style name `name`\n * should be logical (no hyphens), as specified\n * in `CSSProperty.isUnitlessNumber`.\n *\n * @param {string} name CSS property name such as `topMargin`.\n * @param {*} value CSS property value such as `10px`.\n * @return {string} Normalized style value with dimensions applied.\n */\nfunction dangerousStyleValue(name, value, isCustomProperty) {\n  // Note that we've removed escapeTextForBrowser() calls here since the\n  // whole string will be escaped when the attribute is injected into\n  // the markup. If you provide unsafe user data here they can inject\n  // arbitrary CSS which may be problematic (I couldn't repro this):\n  // https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet\n  // http://www.thespanner.co.uk/2007/11/26/ultimate-xss-css-injection/\n  // This is not an XSS hole but instead a potential CSS injection issue\n  // which has lead to a greater discussion about how we're going to\n  // trust URLs moving forward. See #2115901\n\n  var isEmpty = value == null || typeof value === 'boolean' || value === '';\n  if (isEmpty) {\n    return '';\n  }\n\n  if (!isCustomProperty && typeof value === 'number' && value !== 0 && !(isUnitlessNumber.hasOwnProperty(name) && isUnitlessNumber[name])) {\n    return value + 'px'; // Presumes implicit 'px' suffix for unitless numbers\n  }\n\n  return ('' + value).trim();\n}\n\nvar uppercasePattern = /([A-Z])/g;\nvar msPattern = /^ms-/;\n\n/**\n * Hyphenates a camelcased CSS property name, for example:\n *\n *   > hyphenateStyleName('backgroundColor')\n *   < \"background-color\"\n *   > hyphenateStyleName('MozTransition')\n *   < \"-moz-transition\"\n *   > hyphenateStyleName('msTransition')\n *   < \"-ms-transition\"\n *\n * As Modernizr suggests (http://modernizr.com/docs/#prefixed), an `ms` prefix\n * is converted to `-ms-`.\n */\nfunction hyphenateStyleName(name) {\n  return name.replace(uppercasePattern, '-$1').toLowerCase().replace(msPattern, '-ms-');\n}\n\nvar warnValidStyle = function () {};\n\n{\n  // 'msTransform' is correct, but the other prefixes should be capitalized\n  var badVendoredStyleNamePattern = /^(?:webkit|moz|o)[A-Z]/;\n  var msPattern$1 = /^-ms-/;\n  var hyphenPattern = /-(.)/g;\n\n  // style values shouldn't contain a semicolon\n  var badStyleValueWithSemicolonPattern = /;\\s*$/;\n\n  var warnedStyleNames = {};\n  var warnedStyleValues = {};\n  var warnedForNaNValue = false;\n  var warnedForInfinityValue = false;\n\n  var camelize = function (string) {\n    return string.replace(hyphenPattern, function (_, character) {\n      return character.toUpperCase();\n    });\n  };\n\n  var warnHyphenatedStyleName = function (name) {\n    if (warnedStyleNames.hasOwnProperty(name) && warnedStyleNames[name]) {\n      return;\n    }\n\n    warnedStyleNames[name] = true;\n    warning$1(false, 'Unsupported style property %s. Did you mean %s?', name,\n    // As Andi Smith suggests\n    // (http://www.andismith.com/blog/2012/02/modernizr-prefixed/), an `-ms` prefix\n    // is converted to lowercase `ms`.\n    camelize(name.replace(msPattern$1, 'ms-')));\n  };\n\n  var warnBadVendoredStyleName = function (name) {\n    if (warnedStyleNames.hasOwnProperty(name) && warnedStyleNames[name]) {\n      return;\n    }\n\n    warnedStyleNames[name] = true;\n    warning$1(false, 'Unsupported vendor-prefixed style property %s. Did you mean %s?', name, name.charAt(0).toUpperCase() + name.slice(1));\n  };\n\n  var warnStyleValueWithSemicolon = function (name, value) {\n    if (warnedStyleValues.hasOwnProperty(value) && warnedStyleValues[value]) {\n      return;\n    }\n\n    warnedStyleValues[value] = true;\n    warning$1(false, \"Style property values shouldn't contain a semicolon. \" + 'Try \"%s: %s\" instead.', name, value.replace(badStyleValueWithSemicolonPattern, ''));\n  };\n\n  var warnStyleValueIsNaN = function (name, value) {\n    if (warnedForNaNValue) {\n      return;\n    }\n\n    warnedForNaNValue = true;\n    warning$1(false, '`NaN` is an invalid value for the `%s` css style property.', name);\n  };\n\n  var warnStyleValueIsInfinity = function (name, value) {\n    if (warnedForInfinityValue) {\n      return;\n    }\n\n    warnedForInfinityValue = true;\n    warning$1(false, '`Infinity` is an invalid value for the `%s` css style property.', name);\n  };\n\n  warnValidStyle = function (name, value) {\n    if (name.indexOf('-') > -1) {\n      warnHyphenatedStyleName(name);\n    } else if (badVendoredStyleNamePattern.test(name)) {\n      warnBadVendoredStyleName(name);\n    } else if (badStyleValueWithSemicolonPattern.test(value)) {\n      warnStyleValueWithSemicolon(name, value);\n    }\n\n    if (typeof value === 'number') {\n      if (isNaN(value)) {\n        warnStyleValueIsNaN(name, value);\n      } else if (!isFinite(value)) {\n        warnStyleValueIsInfinity(name, value);\n      }\n    }\n  };\n}\n\nvar warnValidStyle$1 = warnValidStyle;\n\n/**\n * Operations for dealing with CSS properties.\n */\n\n/**\n * This creates a string that is expected to be equivalent to the style\n * attribute generated by server-side rendering. It by-passes warnings and\n * security checks so it's not safe to use this value for anything other than\n * comparison. It is only used in DEV for SSR validation.\n */\nfunction createDangerousStringForStyles(styles) {\n  {\n    var serialized = '';\n    var delimiter = '';\n    for (var styleName in styles) {\n      if (!styles.hasOwnProperty(styleName)) {\n        continue;\n      }\n      var styleValue = styles[styleName];\n      if (styleValue != null) {\n        var isCustomProperty = styleName.indexOf('--') === 0;\n        serialized += delimiter + hyphenateStyleName(styleName) + ':';\n        serialized += dangerousStyleValue(styleName, styleValue, isCustomProperty);\n\n        delimiter = ';';\n      }\n    }\n    return serialized || null;\n  }\n}\n\n/**\n * Sets the value for multiple styles on a node.  If a value is specified as\n * '' (empty string), the corresponding style property will be unset.\n *\n * @param {DOMElement} node\n * @param {object} styles\n */\nfunction setValueForStyles(node, styles) {\n  var style = node.style;\n  for (var styleName in styles) {\n    if (!styles.hasOwnProperty(styleName)) {\n      continue;\n    }\n    var isCustomProperty = styleName.indexOf('--') === 0;\n    {\n      if (!isCustomProperty) {\n        warnValidStyle$1(styleName, styles[styleName]);\n      }\n    }\n    var styleValue = dangerousStyleValue(styleName, styles[styleName], isCustomProperty);\n    if (styleName === 'float') {\n      styleName = 'cssFloat';\n    }\n    if (isCustomProperty) {\n      style.setProperty(styleName, styleValue);\n    } else {\n      style[styleName] = styleValue;\n    }\n  }\n}\n\nfunction isValueEmpty(value) {\n  return value == null || typeof value === 'boolean' || value === '';\n}\n\n/**\n * Given {color: 'red', overflow: 'hidden'} returns {\n *   color: 'color',\n *   overflowX: 'overflow',\n *   overflowY: 'overflow',\n * }. This can be read as \"the overflowY property was set by the overflow\n * shorthand\". That is, the values are the property that each was derived from.\n */\nfunction expandShorthandMap(styles) {\n  var expanded = {};\n  for (var key in styles) {\n    var longhands = shorthandToLonghand[key] || [key];\n    for (var i = 0; i < longhands.length; i++) {\n      expanded[longhands[i]] = key;\n    }\n  }\n  return expanded;\n}\n\n/**\n * When mixing shorthand and longhand property names, we warn during updates if\n * we expect an incorrect result to occur. In particular, we warn for:\n *\n * Updating a shorthand property (longhand gets overwritten):\n *   {font: 'foo', fontVariant: 'bar'} -> {font: 'baz', fontVariant: 'bar'}\n *   becomes .style.font = 'baz'\n * Removing a shorthand property (longhand gets lost too):\n *   {font: 'foo', fontVariant: 'bar'} -> {fontVariant: 'bar'}\n *   becomes .style.font = ''\n * Removing a longhand property (should revert to shorthand; doesn't):\n *   {font: 'foo', fontVariant: 'bar'} -> {font: 'foo'}\n *   becomes .style.fontVariant = ''\n */\nfunction validateShorthandPropertyCollisionInDev(styleUpdates, nextStyles) {\n  if (!warnAboutShorthandPropertyCollision) {\n    return;\n  }\n\n  if (!nextStyles) {\n    return;\n  }\n\n  var expandedUpdates = expandShorthandMap(styleUpdates);\n  var expandedStyles = expandShorthandMap(nextStyles);\n  var warnedAbout = {};\n  for (var key in expandedUpdates) {\n    var originalKey = expandedUpdates[key];\n    var correctOriginalKey = expandedStyles[key];\n    if (correctOriginalKey && originalKey !== correctOriginalKey) {\n      var warningKey = originalKey + ',' + correctOriginalKey;\n      if (warnedAbout[warningKey]) {\n        continue;\n      }\n      warnedAbout[warningKey] = true;\n      warning$1(false, '%s a style property during rerender (%s) when a ' + 'conflicting property is set (%s) can lead to styling bugs. To ' + \"avoid this, don't mix shorthand and non-shorthand properties \" + 'for the same value; instead, replace the shorthand with ' + 'separate values.', isValueEmpty(styleUpdates[originalKey]) ? 'Removing' : 'Updating', originalKey, correctOriginalKey);\n    }\n  }\n}\n\n// For HTML, certain tags should omit their close tag. We keep a whitelist for\n// those special-case tags.\n\nvar omittedCloseTags = {\n  area: true,\n  base: true,\n  br: true,\n  col: true,\n  embed: true,\n  hr: true,\n  img: true,\n  input: true,\n  keygen: true,\n  link: true,\n  meta: true,\n  param: true,\n  source: true,\n  track: true,\n  wbr: true\n  // NOTE: menuitem's close tag should be omitted, but that causes problems.\n};\n\n// For HTML, certain tags cannot have children. This has the same purpose as\n// `omittedCloseTags` except that `menuitem` should still have its closing tag.\n\nvar voidElementTags = _assign({\n  menuitem: true\n}, omittedCloseTags);\n\n// TODO: We can remove this if we add invariantWithStack()\n// or add stack by default to invariants where possible.\nvar HTML$1 = '__html';\n\nvar ReactDebugCurrentFrame$2 = null;\n{\n  ReactDebugCurrentFrame$2 = ReactSharedInternals.ReactDebugCurrentFrame;\n}\n\nfunction assertValidProps(tag, props) {\n  if (!props) {\n    return;\n  }\n  // Note the use of `==` which checks for null or undefined.\n  if (voidElementTags[tag]) {\n    !(props.children == null && props.dangerouslySetInnerHTML == null) ? invariant(false, '%s is a void element tag and must neither have `children` nor use `dangerouslySetInnerHTML`.%s', tag, ReactDebugCurrentFrame$2.getStackAddendum()) : void 0;\n  }\n  if (props.dangerouslySetInnerHTML != null) {\n    !(props.children == null) ? invariant(false, 'Can only set one of `children` or `props.dangerouslySetInnerHTML`.') : void 0;\n    !(typeof props.dangerouslySetInnerHTML === 'object' && HTML$1 in props.dangerouslySetInnerHTML) ? invariant(false, '`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. Please visit https://fb.me/react-invariant-dangerously-set-inner-html for more information.') : void 0;\n  }\n  {\n    !(props.suppressContentEditableWarning || !props.contentEditable || props.children == null) ? warning$1(false, 'A component is `contentEditable` and contains `children` managed by ' + 'React. It is now your responsibility to guarantee that none of ' + 'those nodes are unexpectedly modified or duplicated. This is ' + 'probably not intentional.') : void 0;\n  }\n  !(props.style == null || typeof props.style === 'object') ? invariant(false, 'The `style` prop expects a mapping from style properties to values, not a string. For example, style={{marginRight: spacing + \\'em\\'}} when using JSX.%s', ReactDebugCurrentFrame$2.getStackAddendum()) : void 0;\n}\n\nfunction isCustomComponent(tagName, props) {\n  if (tagName.indexOf('-') === -1) {\n    return typeof props.is === 'string';\n  }\n  switch (tagName) {\n    // These are reserved SVG and MathML elements.\n    // We don't mind this whitelist too much because we expect it to never grow.\n    // The alternative is to track the namespace in a few places which is convoluted.\n    // https://w3c.github.io/webcomponents/spec/custom/#custom-elements-core-concepts\n    case 'annotation-xml':\n    case 'color-profile':\n    case 'font-face':\n    case 'font-face-src':\n    case 'font-face-uri':\n    case 'font-face-format':\n    case 'font-face-name':\n    case 'missing-glyph':\n      return false;\n    default:\n      return true;\n  }\n}\n\n// When adding attributes to the HTML or SVG whitelist, be sure to\n// also add them to this module to ensure casing and incorrect name\n// warnings.\nvar possibleStandardNames = {\n  // HTML\n  accept: 'accept',\n  acceptcharset: 'acceptCharset',\n  'accept-charset': 'acceptCharset',\n  accesskey: 'accessKey',\n  action: 'action',\n  allowfullscreen: 'allowFullScreen',\n  alt: 'alt',\n  as: 'as',\n  async: 'async',\n  autocapitalize: 'autoCapitalize',\n  autocomplete: 'autoComplete',\n  autocorrect: 'autoCorrect',\n  autofocus: 'autoFocus',\n  autoplay: 'autoPlay',\n  autosave: 'autoSave',\n  capture: 'capture',\n  cellpadding: 'cellPadding',\n  cellspacing: 'cellSpacing',\n  challenge: 'challenge',\n  charset: 'charSet',\n  checked: 'checked',\n  children: 'children',\n  cite: 'cite',\n  class: 'className',\n  classid: 'classID',\n  classname: 'className',\n  cols: 'cols',\n  colspan: 'colSpan',\n  content: 'content',\n  contenteditable: 'contentEditable',\n  contextmenu: 'contextMenu',\n  controls: 'controls',\n  controlslist: 'controlsList',\n  coords: 'coords',\n  crossorigin: 'crossOrigin',\n  dangerouslysetinnerhtml: 'dangerouslySetInnerHTML',\n  data: 'data',\n  datetime: 'dateTime',\n  default: 'default',\n  defaultchecked: 'defaultChecked',\n  defaultvalue: 'defaultValue',\n  defer: 'defer',\n  dir: 'dir',\n  disabled: 'disabled',\n  download: 'download',\n  draggable: 'draggable',\n  enctype: 'encType',\n  for: 'htmlFor',\n  form: 'form',\n  formmethod: 'formMethod',\n  formaction: 'formAction',\n  formenctype: 'formEncType',\n  formnovalidate: 'formNoValidate',\n  formtarget: 'formTarget',\n  frameborder: 'frameBorder',\n  headers: 'headers',\n  height: 'height',\n  hidden: 'hidden',\n  high: 'high',\n  href: 'href',\n  hreflang: 'hrefLang',\n  htmlfor: 'htmlFor',\n  httpequiv: 'httpEquiv',\n  'http-equiv': 'httpEquiv',\n  icon: 'icon',\n  id: 'id',\n  innerhtml: 'innerHTML',\n  inputmode: 'inputMode',\n  integrity: 'integrity',\n  is: 'is',\n  itemid: 'itemID',\n  itemprop: 'itemProp',\n  itemref: 'itemRef',\n  itemscope: 'itemScope',\n  itemtype: 'itemType',\n  keyparams: 'keyParams',\n  keytype: 'keyType',\n  kind: 'kind',\n  label: 'label',\n  lang: 'lang',\n  list: 'list',\n  loop: 'loop',\n  low: 'low',\n  manifest: 'manifest',\n  marginwidth: 'marginWidth',\n  marginheight: 'marginHeight',\n  max: 'max',\n  maxlength: 'maxLength',\n  media: 'media',\n  mediagroup: 'mediaGroup',\n  method: 'method',\n  min: 'min',\n  minlength: 'minLength',\n  multiple: 'multiple',\n  muted: 'muted',\n  name: 'name',\n  nomodule: 'noModule',\n  nonce: 'nonce',\n  novalidate: 'noValidate',\n  open: 'open',\n  optimum: 'optimum',\n  pattern: 'pattern',\n  placeholder: 'placeholder',\n  playsinline: 'playsInline',\n  poster: 'poster',\n  preload: 'preload',\n  profile: 'profile',\n  radiogroup: 'radioGroup',\n  readonly: 'readOnly',\n  referrerpolicy: 'referrerPolicy',\n  rel: 'rel',\n  required: 'required',\n  reversed: 'reversed',\n  role: 'role',\n  rows: 'rows',\n  rowspan: 'rowSpan',\n  sandbox: 'sandbox',\n  scope: 'scope',\n  scoped: 'scoped',\n  scrolling: 'scrolling',\n  seamless: 'seamless',\n  selected: 'selected',\n  shape: 'shape',\n  size: 'size',\n  sizes: 'sizes',\n  span: 'span',\n  spellcheck: 'spellCheck',\n  src: 'src',\n  srcdoc: 'srcDoc',\n  srclang: 'srcLang',\n  srcset: 'srcSet',\n  start: 'start',\n  step: 'step',\n  style: 'style',\n  summary: 'summary',\n  tabindex: 'tabIndex',\n  target: 'target',\n  title: 'title',\n  type: 'type',\n  usemap: 'useMap',\n  value: 'value',\n  width: 'width',\n  wmode: 'wmode',\n  wrap: 'wrap',\n\n  // SVG\n  about: 'about',\n  accentheight: 'accentHeight',\n  'accent-height': 'accentHeight',\n  accumulate: 'accumulate',\n  additive: 'additive',\n  alignmentbaseline: 'alignmentBaseline',\n  'alignment-baseline': 'alignmentBaseline',\n  allowreorder: 'allowReorder',\n  alphabetic: 'alphabetic',\n  amplitude: 'amplitude',\n  arabicform: 'arabicForm',\n  'arabic-form': 'arabicForm',\n  ascent: 'ascent',\n  attributename: 'attributeName',\n  attributetype: 'attributeType',\n  autoreverse: 'autoReverse',\n  azimuth: 'azimuth',\n  basefrequency: 'baseFrequency',\n  baselineshift: 'baselineShift',\n  'baseline-shift': 'baselineShift',\n  baseprofile: 'baseProfile',\n  bbox: 'bbox',\n  begin: 'begin',\n  bias: 'bias',\n  by: 'by',\n  calcmode: 'calcMode',\n  capheight: 'capHeight',\n  'cap-height': 'capHeight',\n  clip: 'clip',\n  clippath: 'clipPath',\n  'clip-path': 'clipPath',\n  clippathunits: 'clipPathUnits',\n  cliprule: 'clipRule',\n  'clip-rule': 'clipRule',\n  color: 'color',\n  colorinterpolation: 'colorInterpolation',\n  'color-interpolation': 'colorInterpolation',\n  colorinterpolationfilters: 'colorInterpolationFilters',\n  'color-interpolation-filters': 'colorInterpolationFilters',\n  colorprofile: 'colorProfile',\n  'color-profile': 'colorProfile',\n  colorrendering: 'colorRendering',\n  'color-rendering': 'colorRendering',\n  contentscripttype: 'contentScriptType',\n  contentstyletype: 'contentStyleType',\n  cursor: 'cursor',\n  cx: 'cx',\n  cy: 'cy',\n  d: 'd',\n  datatype: 'datatype',\n  decelerate: 'decelerate',\n  descent: 'descent',\n  diffuseconstant: 'diffuseConstant',\n  direction: 'direction',\n  display: 'display',\n  divisor: 'divisor',\n  dominantbaseline: 'dominantBaseline',\n  'dominant-baseline': 'dominantBaseline',\n  dur: 'dur',\n  dx: 'dx',\n  dy: 'dy',\n  edgemode: 'edgeMode',\n  elevation: 'elevation',\n  enablebackground: 'enableBackground',\n  'enable-background': 'enableBackground',\n  end: 'end',\n  exponent: 'exponent',\n  externalresourcesrequired: 'externalResourcesRequired',\n  fill: 'fill',\n  fillopacity: 'fillOpacity',\n  'fill-opacity': 'fillOpacity',\n  fillrule: 'fillRule',\n  'fill-rule': 'fillRule',\n  filter: 'filter',\n  filterres: 'filterRes',\n  filterunits: 'filterUnits',\n  floodopacity: 'floodOpacity',\n  'flood-opacity': 'floodOpacity',\n  floodcolor: 'floodColor',\n  'flood-color': 'floodColor',\n  focusable: 'focusable',\n  fontfamily: 'fontFamily',\n  'font-family': 'fontFamily',\n  fontsize: 'fontSize',\n  'font-size': 'fontSize',\n  fontsizeadjust: 'fontSizeAdjust',\n  'font-size-adjust': 'fontSizeAdjust',\n  fontstretch: 'fontStretch',\n  'font-stretch': 'fontStretch',\n  fontstyle: 'fontStyle',\n  'font-style': 'fontStyle',\n  fontvariant: 'fontVariant',\n  'font-variant': 'fontVariant',\n  fontweight: 'fontWeight',\n  'font-weight': 'fontWeight',\n  format: 'format',\n  from: 'from',\n  fx: 'fx',\n  fy: 'fy',\n  g1: 'g1',\n  g2: 'g2',\n  glyphname: 'glyphName',\n  'glyph-name': 'glyphName',\n  glyphorientationhorizontal: 'glyphOrientationHorizontal',\n  'glyph-orientation-horizontal': 'glyphOrientationHorizontal',\n  glyphorientationvertical: 'glyphOrientationVertical',\n  'glyph-orientation-vertical': 'glyphOrientationVertical',\n  glyphref: 'glyphRef',\n  gradienttransform: 'gradientTransform',\n  gradientunits: 'gradientUnits',\n  hanging: 'hanging',\n  horizadvx: 'horizAdvX',\n  'horiz-adv-x': 'horizAdvX',\n  horizoriginx: 'horizOriginX',\n  'horiz-origin-x': 'horizOriginX',\n  ideographic: 'ideographic',\n  imagerendering: 'imageRendering',\n  'image-rendering': 'imageRendering',\n  in2: 'in2',\n  in: 'in',\n  inlist: 'inlist',\n  intercept: 'intercept',\n  k1: 'k1',\n  k2: 'k2',\n  k3: 'k3',\n  k4: 'k4',\n  k: 'k',\n  kernelmatrix: 'kernelMatrix',\n  kernelunitlength: 'kernelUnitLength',\n  kerning: 'kerning',\n  keypoints: 'keyPoints',\n  keysplines: 'keySplines',\n  keytimes: 'keyTimes',\n  lengthadjust: 'lengthAdjust',\n  letterspacing: 'letterSpacing',\n  'letter-spacing': 'letterSpacing',\n  lightingcolor: 'lightingColor',\n  'lighting-color': 'lightingColor',\n  limitingconeangle: 'limitingConeAngle',\n  local: 'local',\n  markerend: 'markerEnd',\n  'marker-end': 'markerEnd',\n  markerheight: 'markerHeight',\n  markermid: 'markerMid',\n  'marker-mid': 'markerMid',\n  markerstart: 'markerStart',\n  'marker-start': 'markerStart',\n  markerunits: 'markerUnits',\n  markerwidth: 'markerWidth',\n  mask: 'mask',\n  maskcontentunits: 'maskContentUnits',\n  maskunits: 'maskUnits',\n  mathematical: 'mathematical',\n  mode: 'mode',\n  numoctaves: 'numOctaves',\n  offset: 'offset',\n  opacity: 'opacity',\n  operator: 'operator',\n  order: 'order',\n  orient: 'orient',\n  orientation: 'orientation',\n  origin: 'origin',\n  overflow: 'overflow',\n  overlineposition: 'overlinePosition',\n  'overline-position': 'overlinePosition',\n  overlinethickness: 'overlineThickness',\n  'overline-thickness': 'overlineThickness',\n  paintorder: 'paintOrder',\n  'paint-order': 'paintOrder',\n  panose1: 'panose1',\n  'panose-1': 'panose1',\n  pathlength: 'pathLength',\n  patterncontentunits: 'patternContentUnits',\n  patterntransform: 'patternTransform',\n  patternunits: 'patternUnits',\n  pointerevents: 'pointerEvents',\n  'pointer-events': 'pointerEvents',\n  points: 'points',\n  pointsatx: 'pointsAtX',\n  pointsaty: 'pointsAtY',\n  pointsatz: 'pointsAtZ',\n  prefix: 'prefix',\n  preservealpha: 'preserveAlpha',\n  preserveaspectratio: 'preserveAspectRatio',\n  primitiveunits: 'primitiveUnits',\n  property: 'property',\n  r: 'r',\n  radius: 'radius',\n  refx: 'refX',\n  refy: 'refY',\n  renderingintent: 'renderingIntent',\n  'rendering-intent': 'renderingIntent',\n  repeatcount: 'repeatCount',\n  repeatdur: 'repeatDur',\n  requiredextensions: 'requiredExtensions',\n  requiredfeatures: 'requiredFeatures',\n  resource: 'resource',\n  restart: 'restart',\n  result: 'result',\n  results: 'results',\n  rotate: 'rotate',\n  rx: 'rx',\n  ry: 'ry',\n  scale: 'scale',\n  security: 'security',\n  seed: 'seed',\n  shaperendering: 'shapeRendering',\n  'shape-rendering': 'shapeRendering',\n  slope: 'slope',\n  spacing: 'spacing',\n  specularconstant: 'specularConstant',\n  specularexponent: 'specularExponent',\n  speed: 'speed',\n  spreadmethod: 'spreadMethod',\n  startoffset: 'startOffset',\n  stddeviation: 'stdDeviation',\n  stemh: 'stemh',\n  stemv: 'stemv',\n  stitchtiles: 'stitchTiles',\n  stopcolor: 'stopColor',\n  'stop-color': 'stopColor',\n  stopopacity: 'stopOpacity',\n  'stop-opacity': 'stopOpacity',\n  strikethroughposition: 'strikethroughPosition',\n  'strikethrough-position': 'strikethroughPosition',\n  strikethroughthickness: 'strikethroughThickness',\n  'strikethrough-thickness': 'strikethroughThickness',\n  string: 'string',\n  stroke: 'stroke',\n  strokedasharray: 'strokeDasharray',\n  'stroke-dasharray': 'strokeDasharray',\n  strokedashoffset: 'strokeDashoffset',\n  'stroke-dashoffset': 'strokeDashoffset',\n  strokelinecap: 'strokeLinecap',\n  'stroke-linecap': 'strokeLinecap',\n  strokelinejoin: 'strokeLinejoin',\n  'stroke-linejoin': 'strokeLinejoin',\n  strokemiterlimit: 'strokeMiterlimit',\n  'stroke-miterlimit': 'strokeMiterlimit',\n  strokewidth: 'strokeWidth',\n  'stroke-width': 'strokeWidth',\n  strokeopacity: 'strokeOpacity',\n  'stroke-opacity': 'strokeOpacity',\n  suppresscontenteditablewarning: 'suppressContentEditableWarning',\n  suppresshydrationwarning: 'suppressHydrationWarning',\n  surfacescale: 'surfaceScale',\n  systemlanguage: 'systemLanguage',\n  tablevalues: 'tableValues',\n  targetx: 'targetX',\n  targety: 'targetY',\n  textanchor: 'textAnchor',\n  'text-anchor': 'textAnchor',\n  textdecoration: 'textDecoration',\n  'text-decoration': 'textDecoration',\n  textlength: 'textLength',\n  textrendering: 'textRendering',\n  'text-rendering': 'textRendering',\n  to: 'to',\n  transform: 'transform',\n  typeof: 'typeof',\n  u1: 'u1',\n  u2: 'u2',\n  underlineposition: 'underlinePosition',\n  'underline-position': 'underlinePosition',\n  underlinethickness: 'underlineThickness',\n  'underline-thickness': 'underlineThickness',\n  unicode: 'unicode',\n  unicodebidi: 'unicodeBidi',\n  'unicode-bidi': 'unicodeBidi',\n  unicoderange: 'unicodeRange',\n  'unicode-range': 'unicodeRange',\n  unitsperem: 'unitsPerEm',\n  'units-per-em': 'unitsPerEm',\n  unselectable: 'unselectable',\n  valphabetic: 'vAlphabetic',\n  'v-alphabetic': 'vAlphabetic',\n  values: 'values',\n  vectoreffect: 'vectorEffect',\n  'vector-effect': 'vectorEffect',\n  version: 'version',\n  vertadvy: 'vertAdvY',\n  'vert-adv-y': 'vertAdvY',\n  vertoriginx: 'vertOriginX',\n  'vert-origin-x': 'vertOriginX',\n  vertoriginy: 'vertOriginY',\n  'vert-origin-y': 'vertOriginY',\n  vhanging: 'vHanging',\n  'v-hanging': 'vHanging',\n  videographic: 'vIdeographic',\n  'v-ideographic': 'vIdeographic',\n  viewbox: 'viewBox',\n  viewtarget: 'viewTarget',\n  visibility: 'visibility',\n  vmathematical: 'vMathematical',\n  'v-mathematical': 'vMathematical',\n  vocab: 'vocab',\n  widths: 'widths',\n  wordspacing: 'wordSpacing',\n  'word-spacing': 'wordSpacing',\n  writingmode: 'writingMode',\n  'writing-mode': 'writingMode',\n  x1: 'x1',\n  x2: 'x2',\n  x: 'x',\n  xchannelselector: 'xChannelSelector',\n  xheight: 'xHeight',\n  'x-height': 'xHeight',\n  xlinkactuate: 'xlinkActuate',\n  'xlink:actuate': 'xlinkActuate',\n  xlinkarcrole: 'xlinkArcrole',\n  'xlink:arcrole': 'xlinkArcrole',\n  xlinkhref: 'xlinkHref',\n  'xlink:href': 'xlinkHref',\n  xlinkrole: 'xlinkRole',\n  'xlink:role': 'xlinkRole',\n  xlinkshow: 'xlinkShow',\n  'xlink:show': 'xlinkShow',\n  xlinktitle: 'xlinkTitle',\n  'xlink:title': 'xlinkTitle',\n  xlinktype: 'xlinkType',\n  'xlink:type': 'xlinkType',\n  xmlbase: 'xmlBase',\n  'xml:base': 'xmlBase',\n  xmllang: 'xmlLang',\n  'xml:lang': 'xmlLang',\n  xmlns: 'xmlns',\n  'xml:space': 'xmlSpace',\n  xmlnsxlink: 'xmlnsXlink',\n  'xmlns:xlink': 'xmlnsXlink',\n  xmlspace: 'xmlSpace',\n  y1: 'y1',\n  y2: 'y2',\n  y: 'y',\n  ychannelselector: 'yChannelSelector',\n  z: 'z',\n  zoomandpan: 'zoomAndPan'\n};\n\nvar ariaProperties = {\n  'aria-current': 0, // state\n  'aria-details': 0,\n  'aria-disabled': 0, // state\n  'aria-hidden': 0, // state\n  'aria-invalid': 0, // state\n  'aria-keyshortcuts': 0,\n  'aria-label': 0,\n  'aria-roledescription': 0,\n  // Widget Attributes\n  'aria-autocomplete': 0,\n  'aria-checked': 0,\n  'aria-expanded': 0,\n  'aria-haspopup': 0,\n  'aria-level': 0,\n  'aria-modal': 0,\n  'aria-multiline': 0,\n  'aria-multiselectable': 0,\n  'aria-orientation': 0,\n  'aria-placeholder': 0,\n  'aria-pressed': 0,\n  'aria-readonly': 0,\n  'aria-required': 0,\n  'aria-selected': 0,\n  'aria-sort': 0,\n  'aria-valuemax': 0,\n  'aria-valuemin': 0,\n  'aria-valuenow': 0,\n  'aria-valuetext': 0,\n  // Live Region Attributes\n  'aria-atomic': 0,\n  'aria-busy': 0,\n  'aria-live': 0,\n  'aria-relevant': 0,\n  // Drag-and-Drop Attributes\n  'aria-dropeffect': 0,\n  'aria-grabbed': 0,\n  // Relationship Attributes\n  'aria-activedescendant': 0,\n  'aria-colcount': 0,\n  'aria-colindex': 0,\n  'aria-colspan': 0,\n  'aria-controls': 0,\n  'aria-describedby': 0,\n  'aria-errormessage': 0,\n  'aria-flowto': 0,\n  'aria-labelledby': 0,\n  'aria-owns': 0,\n  'aria-posinset': 0,\n  'aria-rowcount': 0,\n  'aria-rowindex': 0,\n  'aria-rowspan': 0,\n  'aria-setsize': 0\n};\n\nvar warnedProperties = {};\nvar rARIA = new RegExp('^(aria)-[' + ATTRIBUTE_NAME_CHAR + ']*$');\nvar rARIACamel = new RegExp('^(aria)[A-Z][' + ATTRIBUTE_NAME_CHAR + ']*$');\n\nvar hasOwnProperty$2 = Object.prototype.hasOwnProperty;\n\nfunction validateProperty(tagName, name) {\n  if (hasOwnProperty$2.call(warnedProperties, name) && warnedProperties[name]) {\n    return true;\n  }\n\n  if (rARIACamel.test(name)) {\n    var ariaName = 'aria-' + name.slice(4).toLowerCase();\n    var correctName = ariaProperties.hasOwnProperty(ariaName) ? ariaName : null;\n\n    // If this is an aria-* attribute, but is not listed in the known DOM\n    // DOM properties, then it is an invalid aria-* attribute.\n    if (correctName == null) {\n      warning$1(false, 'Invalid ARIA attribute `%s`. ARIA attributes follow the pattern aria-* and must be lowercase.', name);\n      warnedProperties[name] = true;\n      return true;\n    }\n    // aria-* attributes should be lowercase; suggest the lowercase version.\n    if (name !== correctName) {\n      warning$1(false, 'Invalid ARIA attribute `%s`. Did you mean `%s`?', name, correctName);\n      warnedProperties[name] = true;\n      return true;\n    }\n  }\n\n  if (rARIA.test(name)) {\n    var lowerCasedName = name.toLowerCase();\n    var standardName = ariaProperties.hasOwnProperty(lowerCasedName) ? lowerCasedName : null;\n\n    // If this is an aria-* attribute, but is not listed in the known DOM\n    // DOM properties, then it is an invalid aria-* attribute.\n    if (standardName == null) {\n      warnedProperties[name] = true;\n      return false;\n    }\n    // aria-* attributes should be lowercase; suggest the lowercase version.\n    if (name !== standardName) {\n      warning$1(false, 'Unknown ARIA attribute `%s`. Did you mean `%s`?', name, standardName);\n      warnedProperties[name] = true;\n      return true;\n    }\n  }\n\n  return true;\n}\n\nfunction warnInvalidARIAProps(type, props) {\n  var invalidProps = [];\n\n  for (var key in props) {\n    var isValid = validateProperty(type, key);\n    if (!isValid) {\n      invalidProps.push(key);\n    }\n  }\n\n  var unknownPropString = invalidProps.map(function (prop) {\n    return '`' + prop + '`';\n  }).join(', ');\n\n  if (invalidProps.length === 1) {\n    warning$1(false, 'Invalid aria prop %s on <%s> tag. ' + 'For details, see https://fb.me/invalid-aria-prop', unknownPropString, type);\n  } else if (invalidProps.length > 1) {\n    warning$1(false, 'Invalid aria props %s on <%s> tag. ' + 'For details, see https://fb.me/invalid-aria-prop', unknownPropString, type);\n  }\n}\n\nfunction validateProperties(type, props) {\n  if (isCustomComponent(type, props)) {\n    return;\n  }\n  warnInvalidARIAProps(type, props);\n}\n\nvar didWarnValueNull = false;\n\nfunction validateProperties$1(type, props) {\n  if (type !== 'input' && type !== 'textarea' && type !== 'select') {\n    return;\n  }\n\n  if (props != null && props.value === null && !didWarnValueNull) {\n    didWarnValueNull = true;\n    if (type === 'select' && props.multiple) {\n      warning$1(false, '`value` prop on `%s` should not be null. ' + 'Consider using an empty array when `multiple` is set to `true` ' + 'to clear the component or `undefined` for uncontrolled components.', type);\n    } else {\n      warning$1(false, '`value` prop on `%s` should not be null. ' + 'Consider using an empty string to clear the component or `undefined` ' + 'for uncontrolled components.', type);\n    }\n  }\n}\n\nvar validateProperty$1 = function () {};\n\n{\n  var warnedProperties$1 = {};\n  var _hasOwnProperty = Object.prototype.hasOwnProperty;\n  var EVENT_NAME_REGEX = /^on./;\n  var INVALID_EVENT_NAME_REGEX = /^on[^A-Z]/;\n  var rARIA$1 = new RegExp('^(aria)-[' + ATTRIBUTE_NAME_CHAR + ']*$');\n  var rARIACamel$1 = new RegExp('^(aria)[A-Z][' + ATTRIBUTE_NAME_CHAR + ']*$');\n\n  validateProperty$1 = function (tagName, name, value, canUseEventSystem) {\n    if (_hasOwnProperty.call(warnedProperties$1, name) && warnedProperties$1[name]) {\n      return true;\n    }\n\n    var lowerCasedName = name.toLowerCase();\n    if (lowerCasedName === 'onfocusin' || lowerCasedName === 'onfocusout') {\n      warning$1(false, 'React uses onFocus and onBlur instead of onFocusIn and onFocusOut. ' + 'All React events are normalized to bubble, so onFocusIn and onFocusOut ' + 'are not needed/supported by React.');\n      warnedProperties$1[name] = true;\n      return true;\n    }\n\n    // We can't rely on the event system being injected on the server.\n    if (canUseEventSystem) {\n      if (registrationNameModules.hasOwnProperty(name)) {\n        return true;\n      }\n      var registrationName = possibleRegistrationNames.hasOwnProperty(lowerCasedName) ? possibleRegistrationNames[lowerCasedName] : null;\n      if (registrationName != null) {\n        warning$1(false, 'Invalid event handler property `%s`. Did you mean `%s`?', name, registrationName);\n        warnedProperties$1[name] = true;\n        return true;\n      }\n      if (EVENT_NAME_REGEX.test(name)) {\n        warning$1(false, 'Unknown event handler property `%s`. It will be ignored.', name);\n        warnedProperties$1[name] = true;\n        return true;\n      }\n    } else if (EVENT_NAME_REGEX.test(name)) {\n      // If no event plugins have been injected, we are in a server environment.\n      // So we can't tell if the event name is correct for sure, but we can filter\n      // out known bad ones like `onclick`. We can't suggest a specific replacement though.\n      if (INVALID_EVENT_NAME_REGEX.test(name)) {\n        warning$1(false, 'Invalid event handler property `%s`. ' + 'React events use the camelCase naming convention, for example `onClick`.', name);\n      }\n      warnedProperties$1[name] = true;\n      return true;\n    }\n\n    // Let the ARIA attribute hook validate ARIA attributes\n    if (rARIA$1.test(name) || rARIACamel$1.test(name)) {\n      return true;\n    }\n\n    if (lowerCasedName === 'innerhtml') {\n      warning$1(false, 'Directly setting property `innerHTML` is not permitted. ' + 'For more information, lookup documentation on `dangerouslySetInnerHTML`.');\n      warnedProperties$1[name] = true;\n      return true;\n    }\n\n    if (lowerCasedName === 'aria') {\n      warning$1(false, 'The `aria` attribute is reserved for future use in React. ' + 'Pass individual `aria-` attributes instead.');\n      warnedProperties$1[name] = true;\n      return true;\n    }\n\n    if (lowerCasedName === 'is' && value !== null && value !== undefined && typeof value !== 'string') {\n      warning$1(false, 'Received a `%s` for a string attribute `is`. If this is expected, cast ' + 'the value to a string.', typeof value);\n      warnedProperties$1[name] = true;\n      return true;\n    }\n\n    if (typeof value === 'number' && isNaN(value)) {\n      warning$1(false, 'Received NaN for the `%s` attribute. If this is expected, cast ' + 'the value to a string.', name);\n      warnedProperties$1[name] = true;\n      return true;\n    }\n\n    var propertyInfo = getPropertyInfo(name);\n    var isReserved = propertyInfo !== null && propertyInfo.type === RESERVED;\n\n    // Known attributes should match the casing specified in the property config.\n    if (possibleStandardNames.hasOwnProperty(lowerCasedName)) {\n      var standardName = possibleStandardNames[lowerCasedName];\n      if (standardName !== name) {\n        warning$1(false, 'Invalid DOM property `%s`. Did you mean `%s`?', name, standardName);\n        warnedProperties$1[name] = true;\n        return true;\n      }\n    } else if (!isReserved && name !== lowerCasedName) {\n      // Unknown attributes should have lowercase casing since that's how they\n      // will be cased anyway with server rendering.\n      warning$1(false, 'React does not recognize the `%s` prop on a DOM element. If you ' + 'intentionally want it to appear in the DOM as a custom ' + 'attribute, spell it as lowercase `%s` instead. ' + 'If you accidentally passed it from a parent component, remove ' + 'it from the DOM element.', name, lowerCasedName);\n      warnedProperties$1[name] = true;\n      return true;\n    }\n\n    if (typeof value === 'boolean' && shouldRemoveAttributeWithWarning(name, value, propertyInfo, false)) {\n      if (value) {\n        warning$1(false, 'Received `%s` for a non-boolean attribute `%s`.\\n\\n' + 'If you want to write it to the DOM, pass a string instead: ' + '%s=\"%s\" or %s={value.toString()}.', value, name, name, value, name);\n      } else {\n        warning$1(false, 'Received `%s` for a non-boolean attribute `%s`.\\n\\n' + 'If you want to write it to the DOM, pass a string instead: ' + '%s=\"%s\" or %s={value.toString()}.\\n\\n' + 'If you used to conditionally omit it with %s={condition && value}, ' + 'pass %s={condition ? value : undefined} instead.', value, name, name, value, name, name, name);\n      }\n      warnedProperties$1[name] = true;\n      return true;\n    }\n\n    // Now that we've validated casing, do not validate\n    // data types for reserved props\n    if (isReserved) {\n      return true;\n    }\n\n    // Warn when a known attribute is a bad type\n    if (shouldRemoveAttributeWithWarning(name, value, propertyInfo, false)) {\n      warnedProperties$1[name] = true;\n      return false;\n    }\n\n    // Warn when passing the strings 'false' or 'true' into a boolean prop\n    if ((value === 'false' || value === 'true') && propertyInfo !== null && propertyInfo.type === BOOLEAN) {\n      warning$1(false, 'Received the string `%s` for the boolean attribute `%s`. ' + '%s ' + 'Did you mean %s={%s}?', value, name, value === 'false' ? 'The browser will interpret it as a truthy value.' : 'Although this works, it will not work as expected if you pass the string \"false\".', name, value);\n      warnedProperties$1[name] = true;\n      return true;\n    }\n\n    return true;\n  };\n}\n\nvar warnUnknownProperties = function (type, props, canUseEventSystem) {\n  var unknownProps = [];\n  for (var key in props) {\n    var isValid = validateProperty$1(type, key, props[key], canUseEventSystem);\n    if (!isValid) {\n      unknownProps.push(key);\n    }\n  }\n\n  var unknownPropString = unknownProps.map(function (prop) {\n    return '`' + prop + '`';\n  }).join(', ');\n  if (unknownProps.length === 1) {\n    warning$1(false, 'Invalid value for prop %s on <%s> tag. Either remove it from the element, ' + 'or pass a string or number value to keep it in the DOM. ' + 'For details, see https://fb.me/react-attribute-behavior', unknownPropString, type);\n  } else if (unknownProps.length > 1) {\n    warning$1(false, 'Invalid values for props %s on <%s> tag. Either remove them from the element, ' + 'or pass a string or number value to keep them in the DOM. ' + 'For details, see https://fb.me/react-attribute-behavior', unknownPropString, type);\n  }\n};\n\nfunction validateProperties$2(type, props, canUseEventSystem) {\n  if (isCustomComponent(type, props)) {\n    return;\n  }\n  warnUnknownProperties(type, props, canUseEventSystem);\n}\n\n// TODO: direct imports like some-package/src/* are bad. Fix me.\nvar didWarnInvalidHydration = false;\nvar didWarnShadyDOM = false;\n\nvar DANGEROUSLY_SET_INNER_HTML = 'dangerouslySetInnerHTML';\nvar SUPPRESS_CONTENT_EDITABLE_WARNING = 'suppressContentEditableWarning';\nvar SUPPRESS_HYDRATION_WARNING$1 = 'suppressHydrationWarning';\nvar AUTOFOCUS = 'autoFocus';\nvar CHILDREN = 'children';\nvar STYLE$1 = 'style';\nvar HTML = '__html';\n\nvar HTML_NAMESPACE = Namespaces.html;\n\n\nvar warnedUnknownTags = void 0;\nvar suppressHydrationWarning = void 0;\n\nvar validatePropertiesInDevelopment = void 0;\nvar warnForTextDifference = void 0;\nvar warnForPropDifference = void 0;\nvar warnForExtraAttributes = void 0;\nvar warnForInvalidEventListener = void 0;\nvar canDiffStyleForHydrationWarning = void 0;\n\nvar normalizeMarkupForTextOrAttribute = void 0;\nvar normalizeHTML = void 0;\n\n{\n  warnedUnknownTags = {\n    // Chrome is the only major browser not shipping <time>. But as of July\n    // 2017 it intends to ship it due to widespread usage. We intentionally\n    // *don't* warn for <time> even if it's unrecognized by Chrome because\n    // it soon will be, and many apps have been using it anyway.\n    time: true,\n    // There are working polyfills for <dialog>. Let people use it.\n    dialog: true,\n    // Electron ships a custom <webview> tag to display external web content in\n    // an isolated frame and process.\n    // This tag is not present in non Electron environments such as JSDom which\n    // is often used for testing purposes.\n    // @see https://electronjs.org/docs/api/webview-tag\n    webview: true\n  };\n\n  validatePropertiesInDevelopment = function (type, props) {\n    validateProperties(type, props);\n    validateProperties$1(type, props);\n    validateProperties$2(type, props, /* canUseEventSystem */true);\n  };\n\n  // IE 11 parses & normalizes the style attribute as opposed to other\n  // browsers. It adds spaces and sorts the properties in some\n  // non-alphabetical order. Handling that would require sorting CSS\n  // properties in the client & server versions or applying\n  // `expectedStyle` to a temporary DOM node to read its `style` attribute\n  // normalized. Since it only affects IE, we're skipping style warnings\n  // in that browser completely in favor of doing all that work.\n  // See https://github.com/facebook/react/issues/11807\n  canDiffStyleForHydrationWarning = canUseDOM && !document.documentMode;\n\n  // HTML parsing normalizes CR and CRLF to LF.\n  // It also can turn \\u0000 into \\uFFFD inside attributes.\n  // https://www.w3.org/TR/html5/single-page.html#preprocessing-the-input-stream\n  // If we have a mismatch, it might be caused by that.\n  // We will still patch up in this case but not fire the warning.\n  var NORMALIZE_NEWLINES_REGEX = /\\r\\n?/g;\n  var NORMALIZE_NULL_AND_REPLACEMENT_REGEX = /\\u0000|\\uFFFD/g;\n\n  normalizeMarkupForTextOrAttribute = function (markup) {\n    var markupString = typeof markup === 'string' ? markup : '' + markup;\n    return markupString.replace(NORMALIZE_NEWLINES_REGEX, '\\n').replace(NORMALIZE_NULL_AND_REPLACEMENT_REGEX, '');\n  };\n\n  warnForTextDifference = function (serverText, clientText) {\n    if (didWarnInvalidHydration) {\n      return;\n    }\n    var normalizedClientText = normalizeMarkupForTextOrAttribute(clientText);\n    var normalizedServerText = normalizeMarkupForTextOrAttribute(serverText);\n    if (normalizedServerText === normalizedClientText) {\n      return;\n    }\n    didWarnInvalidHydration = true;\n    warningWithoutStack$1(false, 'Text content did not match. Server: \"%s\" Client: \"%s\"', normalizedServerText, normalizedClientText);\n  };\n\n  warnForPropDifference = function (propName, serverValue, clientValue) {\n    if (didWarnInvalidHydration) {\n      return;\n    }\n    var normalizedClientValue = normalizeMarkupForTextOrAttribute(clientValue);\n    var normalizedServerValue = normalizeMarkupForTextOrAttribute(serverValue);\n    if (normalizedServerValue === normalizedClientValue) {\n      return;\n    }\n    didWarnInvalidHydration = true;\n    warningWithoutStack$1(false, 'Prop `%s` did not match. Server: %s Client: %s', propName, JSON.stringify(normalizedServerValue), JSON.stringify(normalizedClientValue));\n  };\n\n  warnForExtraAttributes = function (attributeNames) {\n    if (didWarnInvalidHydration) {\n      return;\n    }\n    didWarnInvalidHydration = true;\n    var names = [];\n    attributeNames.forEach(function (name) {\n      names.push(name);\n    });\n    warningWithoutStack$1(false, 'Extra attributes from the server: %s', names);\n  };\n\n  warnForInvalidEventListener = function (registrationName, listener) {\n    if (listener === false) {\n      warning$1(false, 'Expected `%s` listener to be a function, instead got `false`.\\n\\n' + 'If you used to conditionally omit it with %s={condition && value}, ' + 'pass %s={condition ? value : undefined} instead.', registrationName, registrationName, registrationName);\n    } else {\n      warning$1(false, 'Expected `%s` listener to be a function, instead got a value of `%s` type.', registrationName, typeof listener);\n    }\n  };\n\n  // Parse the HTML and read it back to normalize the HTML string so that it\n  // can be used for comparison.\n  normalizeHTML = function (parent, html) {\n    // We could have created a separate document here to avoid\n    // re-initializing custom elements if they exist. But this breaks\n    // how <noscript> is being handled. So we use the same document.\n    // See the discussion in https://github.com/facebook/react/pull/11157.\n    var testElement = parent.namespaceURI === HTML_NAMESPACE ? parent.ownerDocument.createElement(parent.tagName) : parent.ownerDocument.createElementNS(parent.namespaceURI, parent.tagName);\n    testElement.innerHTML = html;\n    return testElement.innerHTML;\n  };\n}\n\nfunction ensureListeningTo(rootContainerElement, registrationName) {\n  var isDocumentOrFragment = rootContainerElement.nodeType === DOCUMENT_NODE || rootContainerElement.nodeType === DOCUMENT_FRAGMENT_NODE;\n  var doc = isDocumentOrFragment ? rootContainerElement : rootContainerElement.ownerDocument;\n  listenTo(registrationName, doc);\n}\n\nfunction getOwnerDocumentFromRootContainer(rootContainerElement) {\n  return rootContainerElement.nodeType === DOCUMENT_NODE ? rootContainerElement : rootContainerElement.ownerDocument;\n}\n\nfunction noop() {}\n\nfunction trapClickOnNonInteractiveElement(node) {\n  // Mobile Safari does not fire properly bubble click events on\n  // non-interactive elements, which means delegated click listeners do not\n  // fire. The workaround for this bug involves attaching an empty click\n  // listener on the target node.\n  // http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html\n  // Just set it using the onclick property so that we don't have to manage any\n  // bookkeeping for it. Not sure if we need to clear it when the listener is\n  // removed.\n  // TODO: Only do this for the relevant Safaris maybe?\n  node.onclick = noop;\n}\n\nfunction setInitialDOMProperties(tag, domElement, rootContainerElement, nextProps, isCustomComponentTag) {\n  for (var propKey in nextProps) {\n    if (!nextProps.hasOwnProperty(propKey)) {\n      continue;\n    }\n    var nextProp = nextProps[propKey];\n    if (propKey === STYLE$1) {\n      {\n        if (nextProp) {\n          // Freeze the next style object so that we can assume it won't be\n          // mutated. We have already warned for this in the past.\n          Object.freeze(nextProp);\n        }\n      }\n      // Relies on `updateStylesByID` not mutating `styleUpdates`.\n      setValueForStyles(domElement, nextProp);\n    } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {\n      var nextHtml = nextProp ? nextProp[HTML] : undefined;\n      if (nextHtml != null) {\n        setInnerHTML(domElement, nextHtml);\n      }\n    } else if (propKey === CHILDREN) {\n      if (typeof nextProp === 'string') {\n        // Avoid setting initial textContent when the text is empty. In IE11 setting\n        // textContent on a <textarea> will cause the placeholder to not\n        // show within the <textarea> until it has been focused and blurred again.\n        // https://github.com/facebook/react/issues/6731#issuecomment-254874553\n        var canSetTextContent = tag !== 'textarea' || nextProp !== '';\n        if (canSetTextContent) {\n          setTextContent(domElement, nextProp);\n        }\n      } else if (typeof nextProp === 'number') {\n        setTextContent(domElement, '' + nextProp);\n      }\n    } else if (propKey === SUPPRESS_CONTENT_EDITABLE_WARNING || propKey === SUPPRESS_HYDRATION_WARNING$1) {\n      // Noop\n    } else if (propKey === AUTOFOCUS) {\n      // We polyfill it separately on the client during commit.\n      // We could have excluded it in the property list instead of\n      // adding a special case here, but then it wouldn't be emitted\n      // on server rendering (but we *do* want to emit it in SSR).\n    } else if (registrationNameModules.hasOwnProperty(propKey)) {\n      if (nextProp != null) {\n        if (true && typeof nextProp !== 'function') {\n          warnForInvalidEventListener(propKey, nextProp);\n        }\n        ensureListeningTo(rootContainerElement, propKey);\n      }\n    } else if (nextProp != null) {\n      setValueForProperty(domElement, propKey, nextProp, isCustomComponentTag);\n    }\n  }\n}\n\nfunction updateDOMProperties(domElement, updatePayload, wasCustomComponentTag, isCustomComponentTag) {\n  // TODO: Handle wasCustomComponentTag\n  for (var i = 0; i < updatePayload.length; i += 2) {\n    var propKey = updatePayload[i];\n    var propValue = updatePayload[i + 1];\n    if (propKey === STYLE$1) {\n      setValueForStyles(domElement, propValue);\n    } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {\n      setInnerHTML(domElement, propValue);\n    } else if (propKey === CHILDREN) {\n      setTextContent(domElement, propValue);\n    } else {\n      setValueForProperty(domElement, propKey, propValue, isCustomComponentTag);\n    }\n  }\n}\n\nfunction createElement(type, props, rootContainerElement, parentNamespace) {\n  var isCustomComponentTag = void 0;\n\n  // We create tags in the namespace of their parent container, except HTML\n  // tags get no namespace.\n  var ownerDocument = getOwnerDocumentFromRootContainer(rootContainerElement);\n  var domElement = void 0;\n  var namespaceURI = parentNamespace;\n  if (namespaceURI === HTML_NAMESPACE) {\n    namespaceURI = getIntrinsicNamespace(type);\n  }\n  if (namespaceURI === HTML_NAMESPACE) {\n    {\n      isCustomComponentTag = isCustomComponent(type, props);\n      // Should this check be gated by parent namespace? Not sure we want to\n      // allow <SVG> or <mATH>.\n      !(isCustomComponentTag || type === type.toLowerCase()) ? warning$1(false, '<%s /> is using incorrect casing. ' + 'Use PascalCase for React components, ' + 'or lowercase for HTML elements.', type) : void 0;\n    }\n\n    if (type === 'script') {\n      // Create the script via .innerHTML so its \"parser-inserted\" flag is\n      // set to true and it does not execute\n      var div = ownerDocument.createElement('div');\n      div.innerHTML = '<script><' + '/script>'; // eslint-disable-line\n      // This is guaranteed to yield a script element.\n      var firstChild = div.firstChild;\n      domElement = div.removeChild(firstChild);\n    } else if (typeof props.is === 'string') {\n      // $FlowIssue `createElement` should be updated for Web Components\n      domElement = ownerDocument.createElement(type, { is: props.is });\n    } else {\n      // Separate else branch instead of using `props.is || undefined` above because of a Firefox bug.\n      // See discussion in https://github.com/facebook/react/pull/6896\n      // and discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=1276240\n      domElement = ownerDocument.createElement(type);\n      // Normally attributes are assigned in `setInitialDOMProperties`, however the `multiple` and `size`\n      // attributes on `select`s needs to be added before `option`s are inserted.\n      // This prevents:\n      // - a bug where the `select` does not scroll to the correct option because singular\n      //  `select` elements automatically pick the first item #13222\n      // - a bug where the `select` set the first item as selected despite the `size` attribute #14239\n      // See https://github.com/facebook/react/issues/13222\n      // and https://github.com/facebook/react/issues/14239\n      if (type === 'select') {\n        var node = domElement;\n        if (props.multiple) {\n          node.multiple = true;\n        } else if (props.size) {\n          // Setting a size greater than 1 causes a select to behave like `multiple=true`, where\n          // it is possible that no option is selected.\n          //\n          // This is only necessary when a select in \"single selection mode\".\n          node.size = props.size;\n        }\n      }\n    }\n  } else {\n    domElement = ownerDocument.createElementNS(namespaceURI, type);\n  }\n\n  {\n    if (namespaceURI === HTML_NAMESPACE) {\n      if (!isCustomComponentTag && Object.prototype.toString.call(domElement) === '[object HTMLUnknownElement]' && !Object.prototype.hasOwnProperty.call(warnedUnknownTags, type)) {\n        warnedUnknownTags[type] = true;\n        warning$1(false, 'The tag <%s> is unrecognized in this browser. ' + 'If you meant to render a React component, start its name with ' + 'an uppercase letter.', type);\n      }\n    }\n  }\n\n  return domElement;\n}\n\nfunction createTextNode(text, rootContainerElement) {\n  return getOwnerDocumentFromRootContainer(rootContainerElement).createTextNode(text);\n}\n\nfunction setInitialProperties(domElement, tag, rawProps, rootContainerElement) {\n  var isCustomComponentTag = isCustomComponent(tag, rawProps);\n  {\n    validatePropertiesInDevelopment(tag, rawProps);\n    if (isCustomComponentTag && !didWarnShadyDOM && domElement.shadyRoot) {\n      warning$1(false, '%s is using shady DOM. Using shady DOM with React can ' + 'cause things to break subtly.', getCurrentFiberOwnerNameInDevOrNull() || 'A component');\n      didWarnShadyDOM = true;\n    }\n  }\n\n  // TODO: Make sure that we check isMounted before firing any of these events.\n  var props = void 0;\n  switch (tag) {\n    case 'iframe':\n    case 'object':\n      trapBubbledEvent(TOP_LOAD, domElement);\n      props = rawProps;\n      break;\n    case 'video':\n    case 'audio':\n      // Create listener for each media event\n      for (var i = 0; i < mediaEventTypes.length; i++) {\n        trapBubbledEvent(mediaEventTypes[i], domElement);\n      }\n      props = rawProps;\n      break;\n    case 'source':\n      trapBubbledEvent(TOP_ERROR, domElement);\n      props = rawProps;\n      break;\n    case 'img':\n    case 'image':\n    case 'link':\n      trapBubbledEvent(TOP_ERROR, domElement);\n      trapBubbledEvent(TOP_LOAD, domElement);\n      props = rawProps;\n      break;\n    case 'form':\n      trapBubbledEvent(TOP_RESET, domElement);\n      trapBubbledEvent(TOP_SUBMIT, domElement);\n      props = rawProps;\n      break;\n    case 'details':\n      trapBubbledEvent(TOP_TOGGLE, domElement);\n      props = rawProps;\n      break;\n    case 'input':\n      initWrapperState(domElement, rawProps);\n      props = getHostProps(domElement, rawProps);\n      trapBubbledEvent(TOP_INVALID, domElement);\n      // For controlled components we always need to ensure we're listening\n      // to onChange. Even if there is no listener.\n      ensureListeningTo(rootContainerElement, 'onChange');\n      break;\n    case 'option':\n      validateProps(domElement, rawProps);\n      props = getHostProps$1(domElement, rawProps);\n      break;\n    case 'select':\n      initWrapperState$1(domElement, rawProps);\n      props = getHostProps$2(domElement, rawProps);\n      trapBubbledEvent(TOP_INVALID, domElement);\n      // For controlled components we always need to ensure we're listening\n      // to onChange. Even if there is no listener.\n      ensureListeningTo(rootContainerElement, 'onChange');\n      break;\n    case 'textarea':\n      initWrapperState$2(domElement, rawProps);\n      props = getHostProps$3(domElement, rawProps);\n      trapBubbledEvent(TOP_INVALID, domElement);\n      // For controlled components we always need to ensure we're listening\n      // to onChange. Even if there is no listener.\n      ensureListeningTo(rootContainerElement, 'onChange');\n      break;\n    default:\n      props = rawProps;\n  }\n\n  assertValidProps(tag, props);\n\n  setInitialDOMProperties(tag, domElement, rootContainerElement, props, isCustomComponentTag);\n\n  switch (tag) {\n    case 'input':\n      // TODO: Make sure we check if this is still unmounted or do any clean\n      // up necessary since we never stop tracking anymore.\n      track(domElement);\n      postMountWrapper(domElement, rawProps, false);\n      break;\n    case 'textarea':\n      // TODO: Make sure we check if this is still unmounted or do any clean\n      // up necessary since we never stop tracking anymore.\n      track(domElement);\n      postMountWrapper$3(domElement, rawProps);\n      break;\n    case 'option':\n      postMountWrapper$1(domElement, rawProps);\n      break;\n    case 'select':\n      postMountWrapper$2(domElement, rawProps);\n      break;\n    default:\n      if (typeof props.onClick === 'function') {\n        // TODO: This cast may not be sound for SVG, MathML or custom elements.\n        trapClickOnNonInteractiveElement(domElement);\n      }\n      break;\n  }\n}\n\n// Calculate the diff between the two objects.\nfunction diffProperties(domElement, tag, lastRawProps, nextRawProps, rootContainerElement) {\n  {\n    validatePropertiesInDevelopment(tag, nextRawProps);\n  }\n\n  var updatePayload = null;\n\n  var lastProps = void 0;\n  var nextProps = void 0;\n  switch (tag) {\n    case 'input':\n      lastProps = getHostProps(domElement, lastRawProps);\n      nextProps = getHostProps(domElement, nextRawProps);\n      updatePayload = [];\n      break;\n    case 'option':\n      lastProps = getHostProps$1(domElement, lastRawProps);\n      nextProps = getHostProps$1(domElement, nextRawProps);\n      updatePayload = [];\n      break;\n    case 'select':\n      lastProps = getHostProps$2(domElement, lastRawProps);\n      nextProps = getHostProps$2(domElement, nextRawProps);\n      updatePayload = [];\n      break;\n    case 'textarea':\n      lastProps = getHostProps$3(domElement, lastRawProps);\n      nextProps = getHostProps$3(domElement, nextRawProps);\n      updatePayload = [];\n      break;\n    default:\n      lastProps = lastRawProps;\n      nextProps = nextRawProps;\n      if (typeof lastProps.onClick !== 'function' && typeof nextProps.onClick === 'function') {\n        // TODO: This cast may not be sound for SVG, MathML or custom elements.\n        trapClickOnNonInteractiveElement(domElement);\n      }\n      break;\n  }\n\n  assertValidProps(tag, nextProps);\n\n  var propKey = void 0;\n  var styleName = void 0;\n  var styleUpdates = null;\n  for (propKey in lastProps) {\n    if (nextProps.hasOwnProperty(propKey) || !lastProps.hasOwnProperty(propKey) || lastProps[propKey] == null) {\n      continue;\n    }\n    if (propKey === STYLE$1) {\n      var lastStyle = lastProps[propKey];\n      for (styleName in lastStyle) {\n        if (lastStyle.hasOwnProperty(styleName)) {\n          if (!styleUpdates) {\n            styleUpdates = {};\n          }\n          styleUpdates[styleName] = '';\n        }\n      }\n    } else if (propKey === DANGEROUSLY_SET_INNER_HTML || propKey === CHILDREN) {\n      // Noop. This is handled by the clear text mechanism.\n    } else if (propKey === SUPPRESS_CONTENT_EDITABLE_WARNING || propKey === SUPPRESS_HYDRATION_WARNING$1) {\n      // Noop\n    } else if (propKey === AUTOFOCUS) {\n      // Noop. It doesn't work on updates anyway.\n    } else if (registrationNameModules.hasOwnProperty(propKey)) {\n      // This is a special case. If any listener updates we need to ensure\n      // that the \"current\" fiber pointer gets updated so we need a commit\n      // to update this element.\n      if (!updatePayload) {\n        updatePayload = [];\n      }\n    } else {\n      // For all other deleted properties we add it to the queue. We use\n      // the whitelist in the commit phase instead.\n      (updatePayload = updatePayload || []).push(propKey, null);\n    }\n  }\n  for (propKey in nextProps) {\n    var nextProp = nextProps[propKey];\n    var lastProp = lastProps != null ? lastProps[propKey] : undefined;\n    if (!nextProps.hasOwnProperty(propKey) || nextProp === lastProp || nextProp == null && lastProp == null) {\n      continue;\n    }\n    if (propKey === STYLE$1) {\n      {\n        if (nextProp) {\n          // Freeze the next style object so that we can assume it won't be\n          // mutated. We have already warned for this in the past.\n          Object.freeze(nextProp);\n        }\n      }\n      if (lastProp) {\n        // Unset styles on `lastProp` but not on `nextProp`.\n        for (styleName in lastProp) {\n          if (lastProp.hasOwnProperty(styleName) && (!nextProp || !nextProp.hasOwnProperty(styleName))) {\n            if (!styleUpdates) {\n              styleUpdates = {};\n            }\n            styleUpdates[styleName] = '';\n          }\n        }\n        // Update styles that changed since `lastProp`.\n        for (styleName in nextProp) {\n          if (nextProp.hasOwnProperty(styleName) && lastProp[styleName] !== nextProp[styleName]) {\n            if (!styleUpdates) {\n              styleUpdates = {};\n            }\n            styleUpdates[styleName] = nextProp[styleName];\n          }\n        }\n      } else {\n        // Relies on `updateStylesByID` not mutating `styleUpdates`.\n        if (!styleUpdates) {\n          if (!updatePayload) {\n            updatePayload = [];\n          }\n          updatePayload.push(propKey, styleUpdates);\n        }\n        styleUpdates = nextProp;\n      }\n    } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {\n      var nextHtml = nextProp ? nextProp[HTML] : undefined;\n      var lastHtml = lastProp ? lastProp[HTML] : undefined;\n      if (nextHtml != null) {\n        if (lastHtml !== nextHtml) {\n          (updatePayload = updatePayload || []).push(propKey, '' + nextHtml);\n        }\n      } else {\n        // TODO: It might be too late to clear this if we have children\n        // inserted already.\n      }\n    } else if (propKey === CHILDREN) {\n      if (lastProp !== nextProp && (typeof nextProp === 'string' || typeof nextProp === 'number')) {\n        (updatePayload = updatePayload || []).push(propKey, '' + nextProp);\n      }\n    } else if (propKey === SUPPRESS_CONTENT_EDITABLE_WARNING || propKey === SUPPRESS_HYDRATION_WARNING$1) {\n      // Noop\n    } else if (registrationNameModules.hasOwnProperty(propKey)) {\n      if (nextProp != null) {\n        // We eagerly listen to this even though we haven't committed yet.\n        if (true && typeof nextProp !== 'function') {\n          warnForInvalidEventListener(propKey, nextProp);\n        }\n        ensureListeningTo(rootContainerElement, propKey);\n      }\n      if (!updatePayload && lastProp !== nextProp) {\n        // This is a special case. If any listener updates we need to ensure\n        // that the \"current\" props pointer gets updated so we need a commit\n        // to update this element.\n        updatePayload = [];\n      }\n    } else {\n      // For any other property we always add it to the queue and then we\n      // filter it out using the whitelist during the commit.\n      (updatePayload = updatePayload || []).push(propKey, nextProp);\n    }\n  }\n  if (styleUpdates) {\n    {\n      validateShorthandPropertyCollisionInDev(styleUpdates, nextProps[STYLE$1]);\n    }\n    (updatePayload = updatePayload || []).push(STYLE$1, styleUpdates);\n  }\n  return updatePayload;\n}\n\n// Apply the diff.\nfunction updateProperties(domElement, updatePayload, tag, lastRawProps, nextRawProps) {\n  // Update checked *before* name.\n  // In the middle of an update, it is possible to have multiple checked.\n  // When a checked radio tries to change name, browser makes another radio's checked false.\n  if (tag === 'input' && nextRawProps.type === 'radio' && nextRawProps.name != null) {\n    updateChecked(domElement, nextRawProps);\n  }\n\n  var wasCustomComponentTag = isCustomComponent(tag, lastRawProps);\n  var isCustomComponentTag = isCustomComponent(tag, nextRawProps);\n  // Apply the diff.\n  updateDOMProperties(domElement, updatePayload, wasCustomComponentTag, isCustomComponentTag);\n\n  // TODO: Ensure that an update gets scheduled if any of the special props\n  // changed.\n  switch (tag) {\n    case 'input':\n      // Update the wrapper around inputs *after* updating props. This has to\n      // happen after `updateDOMProperties`. Otherwise HTML5 input validations\n      // raise warnings and prevent the new value from being assigned.\n      updateWrapper(domElement, nextRawProps);\n      break;\n    case 'textarea':\n      updateWrapper$1(domElement, nextRawProps);\n      break;\n    case 'select':\n      // <select> value update needs to occur after <option> children\n      // reconciliation\n      postUpdateWrapper(domElement, nextRawProps);\n      break;\n  }\n}\n\nfunction getPossibleStandardName(propName) {\n  {\n    var lowerCasedName = propName.toLowerCase();\n    if (!possibleStandardNames.hasOwnProperty(lowerCasedName)) {\n      return null;\n    }\n    return possibleStandardNames[lowerCasedName] || null;\n  }\n  return null;\n}\n\nfunction diffHydratedProperties(domElement, tag, rawProps, parentNamespace, rootContainerElement) {\n  var isCustomComponentTag = void 0;\n  var extraAttributeNames = void 0;\n\n  {\n    suppressHydrationWarning = rawProps[SUPPRESS_HYDRATION_WARNING$1] === true;\n    isCustomComponentTag = isCustomComponent(tag, rawProps);\n    validatePropertiesInDevelopment(tag, rawProps);\n    if (isCustomComponentTag && !didWarnShadyDOM && domElement.shadyRoot) {\n      warning$1(false, '%s is using shady DOM. Using shady DOM with React can ' + 'cause things to break subtly.', getCurrentFiberOwnerNameInDevOrNull() || 'A component');\n      didWarnShadyDOM = true;\n    }\n  }\n\n  // TODO: Make sure that we check isMounted before firing any of these events.\n  switch (tag) {\n    case 'iframe':\n    case 'object':\n      trapBubbledEvent(TOP_LOAD, domElement);\n      break;\n    case 'video':\n    case 'audio':\n      // Create listener for each media event\n      for (var i = 0; i < mediaEventTypes.length; i++) {\n        trapBubbledEvent(mediaEventTypes[i], domElement);\n      }\n      break;\n    case 'source':\n      trapBubbledEvent(TOP_ERROR, domElement);\n      break;\n    case 'img':\n    case 'image':\n    case 'link':\n      trapBubbledEvent(TOP_ERROR, domElement);\n      trapBubbledEvent(TOP_LOAD, domElement);\n      break;\n    case 'form':\n      trapBubbledEvent(TOP_RESET, domElement);\n      trapBubbledEvent(TOP_SUBMIT, domElement);\n      break;\n    case 'details':\n      trapBubbledEvent(TOP_TOGGLE, domElement);\n      break;\n    case 'input':\n      initWrapperState(domElement, rawProps);\n      trapBubbledEvent(TOP_INVALID, domElement);\n      // For controlled components we always need to ensure we're listening\n      // to onChange. Even if there is no listener.\n      ensureListeningTo(rootContainerElement, 'onChange');\n      break;\n    case 'option':\n      validateProps(domElement, rawProps);\n      break;\n    case 'select':\n      initWrapperState$1(domElement, rawProps);\n      trapBubbledEvent(TOP_INVALID, domElement);\n      // For controlled components we always need to ensure we're listening\n      // to onChange. Even if there is no listener.\n      ensureListeningTo(rootContainerElement, 'onChange');\n      break;\n    case 'textarea':\n      initWrapperState$2(domElement, rawProps);\n      trapBubbledEvent(TOP_INVALID, domElement);\n      // For controlled components we always need to ensure we're listening\n      // to onChange. Even if there is no listener.\n      ensureListeningTo(rootContainerElement, 'onChange');\n      break;\n  }\n\n  assertValidProps(tag, rawProps);\n\n  {\n    extraAttributeNames = new Set();\n    var attributes = domElement.attributes;\n    for (var _i = 0; _i < attributes.length; _i++) {\n      var name = attributes[_i].name.toLowerCase();\n      switch (name) {\n        // Built-in SSR attribute is whitelisted\n        case 'data-reactroot':\n          break;\n        // Controlled attributes are not validated\n        // TODO: Only ignore them on controlled tags.\n        case 'value':\n          break;\n        case 'checked':\n          break;\n        case 'selected':\n          break;\n        default:\n          // Intentionally use the original name.\n          // See discussion in https://github.com/facebook/react/pull/10676.\n          extraAttributeNames.add(attributes[_i].name);\n      }\n    }\n  }\n\n  var updatePayload = null;\n  for (var propKey in rawProps) {\n    if (!rawProps.hasOwnProperty(propKey)) {\n      continue;\n    }\n    var nextProp = rawProps[propKey];\n    if (propKey === CHILDREN) {\n      // For text content children we compare against textContent. This\n      // might match additional HTML that is hidden when we read it using\n      // textContent. E.g. \"foo\" will match \"f<span>oo</span>\" but that still\n      // satisfies our requirement. Our requirement is not to produce perfect\n      // HTML and attributes. Ideally we should preserve structure but it's\n      // ok not to if the visible content is still enough to indicate what\n      // even listeners these nodes might be wired up to.\n      // TODO: Warn if there is more than a single textNode as a child.\n      // TODO: Should we use domElement.firstChild.nodeValue to compare?\n      if (typeof nextProp === 'string') {\n        if (domElement.textContent !== nextProp) {\n          if (true && !suppressHydrationWarning) {\n            warnForTextDifference(domElement.textContent, nextProp);\n          }\n          updatePayload = [CHILDREN, nextProp];\n        }\n      } else if (typeof nextProp === 'number') {\n        if (domElement.textContent !== '' + nextProp) {\n          if (true && !suppressHydrationWarning) {\n            warnForTextDifference(domElement.textContent, nextProp);\n          }\n          updatePayload = [CHILDREN, '' + nextProp];\n        }\n      }\n    } else if (registrationNameModules.hasOwnProperty(propKey)) {\n      if (nextProp != null) {\n        if (true && typeof nextProp !== 'function') {\n          warnForInvalidEventListener(propKey, nextProp);\n        }\n        ensureListeningTo(rootContainerElement, propKey);\n      }\n    } else if (true &&\n    // Convince Flow we've calculated it (it's DEV-only in this method.)\n    typeof isCustomComponentTag === 'boolean') {\n      // Validate that the properties correspond to their expected values.\n      var serverValue = void 0;\n      var propertyInfo = getPropertyInfo(propKey);\n      if (suppressHydrationWarning) {\n        // Don't bother comparing. We're ignoring all these warnings.\n      } else if (propKey === SUPPRESS_CONTENT_EDITABLE_WARNING || propKey === SUPPRESS_HYDRATION_WARNING$1 ||\n      // Controlled attributes are not validated\n      // TODO: Only ignore them on controlled tags.\n      propKey === 'value' || propKey === 'checked' || propKey === 'selected') {\n        // Noop\n      } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {\n        var serverHTML = domElement.innerHTML;\n        var nextHtml = nextProp ? nextProp[HTML] : undefined;\n        var expectedHTML = normalizeHTML(domElement, nextHtml != null ? nextHtml : '');\n        if (expectedHTML !== serverHTML) {\n          warnForPropDifference(propKey, serverHTML, expectedHTML);\n        }\n      } else if (propKey === STYLE$1) {\n        // $FlowFixMe - Should be inferred as not undefined.\n        extraAttributeNames.delete(propKey);\n\n        if (canDiffStyleForHydrationWarning) {\n          var expectedStyle = createDangerousStringForStyles(nextProp);\n          serverValue = domElement.getAttribute('style');\n          if (expectedStyle !== serverValue) {\n            warnForPropDifference(propKey, serverValue, expectedStyle);\n          }\n        }\n      } else if (isCustomComponentTag) {\n        // $FlowFixMe - Should be inferred as not undefined.\n        extraAttributeNames.delete(propKey.toLowerCase());\n        serverValue = getValueForAttribute(domElement, propKey, nextProp);\n\n        if (nextProp !== serverValue) {\n          warnForPropDifference(propKey, serverValue, nextProp);\n        }\n      } else if (!shouldIgnoreAttribute(propKey, propertyInfo, isCustomComponentTag) && !shouldRemoveAttribute(propKey, nextProp, propertyInfo, isCustomComponentTag)) {\n        var isMismatchDueToBadCasing = false;\n        if (propertyInfo !== null) {\n          // $FlowFixMe - Should be inferred as not undefined.\n          extraAttributeNames.delete(propertyInfo.attributeName);\n          serverValue = getValueForProperty(domElement, propKey, nextProp, propertyInfo);\n        } else {\n          var ownNamespace = parentNamespace;\n          if (ownNamespace === HTML_NAMESPACE) {\n            ownNamespace = getIntrinsicNamespace(tag);\n          }\n          if (ownNamespace === HTML_NAMESPACE) {\n            // $FlowFixMe - Should be inferred as not undefined.\n            extraAttributeNames.delete(propKey.toLowerCase());\n          } else {\n            var standardName = getPossibleStandardName(propKey);\n            if (standardName !== null && standardName !== propKey) {\n              // If an SVG prop is supplied with bad casing, it will\n              // be successfully parsed from HTML, but will produce a mismatch\n              // (and would be incorrectly rendered on the client).\n              // However, we already warn about bad casing elsewhere.\n              // So we'll skip the misleading extra mismatch warning in this case.\n              isMismatchDueToBadCasing = true;\n              // $FlowFixMe - Should be inferred as not undefined.\n              extraAttributeNames.delete(standardName);\n            }\n            // $FlowFixMe - Should be inferred as not undefined.\n            extraAttributeNames.delete(propKey);\n          }\n          serverValue = getValueForAttribute(domElement, propKey, nextProp);\n        }\n\n        if (nextProp !== serverValue && !isMismatchDueToBadCasing) {\n          warnForPropDifference(propKey, serverValue, nextProp);\n        }\n      }\n    }\n  }\n\n  {\n    // $FlowFixMe - Should be inferred as not undefined.\n    if (extraAttributeNames.size > 0 && !suppressHydrationWarning) {\n      // $FlowFixMe - Should be inferred as not undefined.\n      warnForExtraAttributes(extraAttributeNames);\n    }\n  }\n\n  switch (tag) {\n    case 'input':\n      // TODO: Make sure we check if this is still unmounted or do any clean\n      // up necessary since we never stop tracking anymore.\n      track(domElement);\n      postMountWrapper(domElement, rawProps, true);\n      break;\n    case 'textarea':\n      // TODO: Make sure we check if this is still unmounted or do any clean\n      // up necessary since we never stop tracking anymore.\n      track(domElement);\n      postMountWrapper$3(domElement, rawProps);\n      break;\n    case 'select':\n    case 'option':\n      // For input and textarea we current always set the value property at\n      // post mount to force it to diverge from attributes. However, for\n      // option and select we don't quite do the same thing and select\n      // is not resilient to the DOM state changing so we don't do that here.\n      // TODO: Consider not doing this for input and textarea.\n      break;\n    default:\n      if (typeof rawProps.onClick === 'function') {\n        // TODO: This cast may not be sound for SVG, MathML or custom elements.\n        trapClickOnNonInteractiveElement(domElement);\n      }\n      break;\n  }\n\n  return updatePayload;\n}\n\nfunction diffHydratedText(textNode, text) {\n  var isDifferent = textNode.nodeValue !== text;\n  return isDifferent;\n}\n\nfunction warnForUnmatchedText(textNode, text) {\n  {\n    warnForTextDifference(textNode.nodeValue, text);\n  }\n}\n\nfunction warnForDeletedHydratableElement(parentNode, child) {\n  {\n    if (didWarnInvalidHydration) {\n      return;\n    }\n    didWarnInvalidHydration = true;\n    warningWithoutStack$1(false, 'Did not expect server HTML to contain a <%s> in <%s>.', child.nodeName.toLowerCase(), parentNode.nodeName.toLowerCase());\n  }\n}\n\nfunction warnForDeletedHydratableText(parentNode, child) {\n  {\n    if (didWarnInvalidHydration) {\n      return;\n    }\n    didWarnInvalidHydration = true;\n    warningWithoutStack$1(false, 'Did not expect server HTML to contain the text node \"%s\" in <%s>.', child.nodeValue, parentNode.nodeName.toLowerCase());\n  }\n}\n\nfunction warnForInsertedHydratedElement(parentNode, tag, props) {\n  {\n    if (didWarnInvalidHydration) {\n      return;\n    }\n    didWarnInvalidHydration = true;\n    warningWithoutStack$1(false, 'Expected server HTML to contain a matching <%s> in <%s>.', tag, parentNode.nodeName.toLowerCase());\n  }\n}\n\nfunction warnForInsertedHydratedText(parentNode, text) {\n  {\n    if (text === '') {\n      // We expect to insert empty text nodes since they're not represented in\n      // the HTML.\n      // TODO: Remove this special case if we can just avoid inserting empty\n      // text nodes.\n      return;\n    }\n    if (didWarnInvalidHydration) {\n      return;\n    }\n    didWarnInvalidHydration = true;\n    warningWithoutStack$1(false, 'Expected server HTML to contain a matching text node for \"%s\" in <%s>.', text, parentNode.nodeName.toLowerCase());\n  }\n}\n\nfunction restoreControlledState$1(domElement, tag, props) {\n  switch (tag) {\n    case 'input':\n      restoreControlledState(domElement, props);\n      return;\n    case 'textarea':\n      restoreControlledState$3(domElement, props);\n      return;\n    case 'select':\n      restoreControlledState$2(domElement, props);\n      return;\n  }\n}\n\n// TODO: direct imports like some-package/src/* are bad. Fix me.\nvar validateDOMNesting = function () {};\nvar updatedAncestorInfo = function () {};\n\n{\n  // This validation code was written based on the HTML5 parsing spec:\n  // https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-scope\n  //\n  // Note: this does not catch all invalid nesting, nor does it try to (as it's\n  // not clear what practical benefit doing so provides); instead, we warn only\n  // for cases where the parser will give a parse tree differing from what React\n  // intended. For example, <b><div></div></b> is invalid but we don't warn\n  // because it still parses correctly; we do warn for other cases like nested\n  // <p> tags where the beginning of the second element implicitly closes the\n  // first, causing a confusing mess.\n\n  // https://html.spec.whatwg.org/multipage/syntax.html#special\n  var specialTags = ['address', 'applet', 'area', 'article', 'aside', 'base', 'basefont', 'bgsound', 'blockquote', 'body', 'br', 'button', 'caption', 'center', 'col', 'colgroup', 'dd', 'details', 'dir', 'div', 'dl', 'dt', 'embed', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'iframe', 'img', 'input', 'isindex', 'li', 'link', 'listing', 'main', 'marquee', 'menu', 'menuitem', 'meta', 'nav', 'noembed', 'noframes', 'noscript', 'object', 'ol', 'p', 'param', 'plaintext', 'pre', 'script', 'section', 'select', 'source', 'style', 'summary', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'title', 'tr', 'track', 'ul', 'wbr', 'xmp'];\n\n  // https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-scope\n  var inScopeTags = ['applet', 'caption', 'html', 'table', 'td', 'th', 'marquee', 'object', 'template',\n\n  // https://html.spec.whatwg.org/multipage/syntax.html#html-integration-point\n  // TODO: Distinguish by namespace here -- for <title>, including it here\n  // errs on the side of fewer warnings\n  'foreignObject', 'desc', 'title'];\n\n  // https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-button-scope\n  var buttonScopeTags = inScopeTags.concat(['button']);\n\n  // https://html.spec.whatwg.org/multipage/syntax.html#generate-implied-end-tags\n  var impliedEndTags = ['dd', 'dt', 'li', 'option', 'optgroup', 'p', 'rp', 'rt'];\n\n  var emptyAncestorInfo = {\n    current: null,\n\n    formTag: null,\n    aTagInScope: null,\n    buttonTagInScope: null,\n    nobrTagInScope: null,\n    pTagInButtonScope: null,\n\n    listItemTagAutoclosing: null,\n    dlItemTagAutoclosing: null\n  };\n\n  updatedAncestorInfo = function (oldInfo, tag) {\n    var ancestorInfo = _assign({}, oldInfo || emptyAncestorInfo);\n    var info = { tag: tag };\n\n    if (inScopeTags.indexOf(tag) !== -1) {\n      ancestorInfo.aTagInScope = null;\n      ancestorInfo.buttonTagInScope = null;\n      ancestorInfo.nobrTagInScope = null;\n    }\n    if (buttonScopeTags.indexOf(tag) !== -1) {\n      ancestorInfo.pTagInButtonScope = null;\n    }\n\n    // See rules for 'li', 'dd', 'dt' start tags in\n    // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inbody\n    if (specialTags.indexOf(tag) !== -1 && tag !== 'address' && tag !== 'div' && tag !== 'p') {\n      ancestorInfo.listItemTagAutoclosing = null;\n      ancestorInfo.dlItemTagAutoclosing = null;\n    }\n\n    ancestorInfo.current = info;\n\n    if (tag === 'form') {\n      ancestorInfo.formTag = info;\n    }\n    if (tag === 'a') {\n      ancestorInfo.aTagInScope = info;\n    }\n    if (tag === 'button') {\n      ancestorInfo.buttonTagInScope = info;\n    }\n    if (tag === 'nobr') {\n      ancestorInfo.nobrTagInScope = info;\n    }\n    if (tag === 'p') {\n      ancestorInfo.pTagInButtonScope = info;\n    }\n    if (tag === 'li') {\n      ancestorInfo.listItemTagAutoclosing = info;\n    }\n    if (tag === 'dd' || tag === 'dt') {\n      ancestorInfo.dlItemTagAutoclosing = info;\n    }\n\n    return ancestorInfo;\n  };\n\n  /**\n   * Returns whether\n   */\n  var isTagValidWithParent = function (tag, parentTag) {\n    // First, let's check if we're in an unusual parsing mode...\n    switch (parentTag) {\n      // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inselect\n      case 'select':\n        return tag === 'option' || tag === 'optgroup' || tag === '#text';\n      case 'optgroup':\n        return tag === 'option' || tag === '#text';\n      // Strictly speaking, seeing an <option> doesn't mean we're in a <select>\n      // but\n      case 'option':\n        return tag === '#text';\n      // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intd\n      // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-incaption\n      // No special behavior since these rules fall back to \"in body\" mode for\n      // all except special table nodes which cause bad parsing behavior anyway.\n\n      // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intr\n      case 'tr':\n        return tag === 'th' || tag === 'td' || tag === 'style' || tag === 'script' || tag === 'template';\n      // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intbody\n      case 'tbody':\n      case 'thead':\n      case 'tfoot':\n        return tag === 'tr' || tag === 'style' || tag === 'script' || tag === 'template';\n      // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-incolgroup\n      case 'colgroup':\n        return tag === 'col' || tag === 'template';\n      // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intable\n      case 'table':\n        return tag === 'caption' || tag === 'colgroup' || tag === 'tbody' || tag === 'tfoot' || tag === 'thead' || tag === 'style' || tag === 'script' || tag === 'template';\n      // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inhead\n      case 'head':\n        return tag === 'base' || tag === 'basefont' || tag === 'bgsound' || tag === 'link' || tag === 'meta' || tag === 'title' || tag === 'noscript' || tag === 'noframes' || tag === 'style' || tag === 'script' || tag === 'template';\n      // https://html.spec.whatwg.org/multipage/semantics.html#the-html-element\n      case 'html':\n        return tag === 'head' || tag === 'body';\n      case '#document':\n        return tag === 'html';\n    }\n\n    // Probably in the \"in body\" parsing mode, so we outlaw only tag combos\n    // where the parsing rules cause implicit opens or closes to be added.\n    // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inbody\n    switch (tag) {\n      case 'h1':\n      case 'h2':\n      case 'h3':\n      case 'h4':\n      case 'h5':\n      case 'h6':\n        return parentTag !== 'h1' && parentTag !== 'h2' && parentTag !== 'h3' && parentTag !== 'h4' && parentTag !== 'h5' && parentTag !== 'h6';\n\n      case 'rp':\n      case 'rt':\n        return impliedEndTags.indexOf(parentTag) === -1;\n\n      case 'body':\n      case 'caption':\n      case 'col':\n      case 'colgroup':\n      case 'frame':\n      case 'head':\n      case 'html':\n      case 'tbody':\n      case 'td':\n      case 'tfoot':\n      case 'th':\n      case 'thead':\n      case 'tr':\n        // These tags are only valid with a few parents that have special child\n        // parsing rules -- if we're down here, then none of those matched and\n        // so we allow it only if we don't know what the parent is, as all other\n        // cases are invalid.\n        return parentTag == null;\n    }\n\n    return true;\n  };\n\n  /**\n   * Returns whether\n   */\n  var findInvalidAncestorForTag = function (tag, ancestorInfo) {\n    switch (tag) {\n      case 'address':\n      case 'article':\n      case 'aside':\n      case 'blockquote':\n      case 'center':\n      case 'details':\n      case 'dialog':\n      case 'dir':\n      case 'div':\n      case 'dl':\n      case 'fieldset':\n      case 'figcaption':\n      case 'figure':\n      case 'footer':\n      case 'header':\n      case 'hgroup':\n      case 'main':\n      case 'menu':\n      case 'nav':\n      case 'ol':\n      case 'p':\n      case 'section':\n      case 'summary':\n      case 'ul':\n      case 'pre':\n      case 'listing':\n      case 'table':\n      case 'hr':\n      case 'xmp':\n      case 'h1':\n      case 'h2':\n      case 'h3':\n      case 'h4':\n      case 'h5':\n      case 'h6':\n        return ancestorInfo.pTagInButtonScope;\n\n      case 'form':\n        return ancestorInfo.formTag || ancestorInfo.pTagInButtonScope;\n\n      case 'li':\n        return ancestorInfo.listItemTagAutoclosing;\n\n      case 'dd':\n      case 'dt':\n        return ancestorInfo.dlItemTagAutoclosing;\n\n      case 'button':\n        return ancestorInfo.buttonTagInScope;\n\n      case 'a':\n        // Spec says something about storing a list of markers, but it sounds\n        // equivalent to this check.\n        return ancestorInfo.aTagInScope;\n\n      case 'nobr':\n        return ancestorInfo.nobrTagInScope;\n    }\n\n    return null;\n  };\n\n  var didWarn = {};\n\n  validateDOMNesting = function (childTag, childText, ancestorInfo) {\n    ancestorInfo = ancestorInfo || emptyAncestorInfo;\n    var parentInfo = ancestorInfo.current;\n    var parentTag = parentInfo && parentInfo.tag;\n\n    if (childText != null) {\n      !(childTag == null) ? warningWithoutStack$1(false, 'validateDOMNesting: when childText is passed, childTag should be null') : void 0;\n      childTag = '#text';\n    }\n\n    var invalidParent = isTagValidWithParent(childTag, parentTag) ? null : parentInfo;\n    var invalidAncestor = invalidParent ? null : findInvalidAncestorForTag(childTag, ancestorInfo);\n    var invalidParentOrAncestor = invalidParent || invalidAncestor;\n    if (!invalidParentOrAncestor) {\n      return;\n    }\n\n    var ancestorTag = invalidParentOrAncestor.tag;\n    var addendum = getCurrentFiberStackInDev();\n\n    var warnKey = !!invalidParent + '|' + childTag + '|' + ancestorTag + '|' + addendum;\n    if (didWarn[warnKey]) {\n      return;\n    }\n    didWarn[warnKey] = true;\n\n    var tagDisplayName = childTag;\n    var whitespaceInfo = '';\n    if (childTag === '#text') {\n      if (/\\S/.test(childText)) {\n        tagDisplayName = 'Text nodes';\n      } else {\n        tagDisplayName = 'Whitespace text nodes';\n        whitespaceInfo = \" Make sure you don't have any extra whitespace between tags on \" + 'each line of your source code.';\n      }\n    } else {\n      tagDisplayName = '<' + childTag + '>';\n    }\n\n    if (invalidParent) {\n      var info = '';\n      if (ancestorTag === 'table' && childTag === 'tr') {\n        info += ' Add a <tbody> to your code to match the DOM tree generated by ' + 'the browser.';\n      }\n      warningWithoutStack$1(false, 'validateDOMNesting(...): %s cannot appear as a child of <%s>.%s%s%s', tagDisplayName, ancestorTag, whitespaceInfo, info, addendum);\n    } else {\n      warningWithoutStack$1(false, 'validateDOMNesting(...): %s cannot appear as a descendant of ' + '<%s>.%s', tagDisplayName, ancestorTag, addendum);\n    }\n  };\n}\n\nvar ReactInternals$1 = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;\n\nvar _ReactInternals$Sched = ReactInternals$1.Scheduler;\nvar unstable_cancelCallback = _ReactInternals$Sched.unstable_cancelCallback;\nvar unstable_now = _ReactInternals$Sched.unstable_now;\nvar unstable_scheduleCallback = _ReactInternals$Sched.unstable_scheduleCallback;\nvar unstable_shouldYield = _ReactInternals$Sched.unstable_shouldYield;\nvar unstable_getFirstCallbackNode = _ReactInternals$Sched.unstable_getFirstCallbackNode;\nvar unstable_runWithPriority = _ReactInternals$Sched.unstable_runWithPriority;\nvar unstable_next = _ReactInternals$Sched.unstable_next;\nvar unstable_continueExecution = _ReactInternals$Sched.unstable_continueExecution;\nvar unstable_pauseExecution = _ReactInternals$Sched.unstable_pauseExecution;\nvar unstable_getCurrentPriorityLevel = _ReactInternals$Sched.unstable_getCurrentPriorityLevel;\nvar unstable_ImmediatePriority = _ReactInternals$Sched.unstable_ImmediatePriority;\nvar unstable_UserBlockingPriority = _ReactInternals$Sched.unstable_UserBlockingPriority;\nvar unstable_NormalPriority = _ReactInternals$Sched.unstable_NormalPriority;\nvar unstable_LowPriority = _ReactInternals$Sched.unstable_LowPriority;\nvar unstable_IdlePriority = _ReactInternals$Sched.unstable_IdlePriority;\n\n// Renderers that don't support persistence\n// can re-export everything from this module.\n\nfunction shim() {\n  invariant(false, 'The current renderer does not support persistence. This error is likely caused by a bug in React. Please file an issue.');\n}\n\n// Persistence (when unsupported)\nvar supportsPersistence = false;\nvar cloneInstance = shim;\nvar createContainerChildSet = shim;\nvar appendChildToContainerChildSet = shim;\nvar finalizeContainerChildren = shim;\nvar replaceContainerChildren = shim;\nvar cloneHiddenInstance = shim;\nvar cloneUnhiddenInstance = shim;\nvar createHiddenTextInstance = shim;\n\nvar SUPPRESS_HYDRATION_WARNING = void 0;\n{\n  SUPPRESS_HYDRATION_WARNING = 'suppressHydrationWarning';\n}\n\nvar SUSPENSE_START_DATA = '$';\nvar SUSPENSE_END_DATA = '/$';\n\nvar STYLE = 'style';\n\nvar eventsEnabled = null;\nvar selectionInformation = null;\n\nfunction shouldAutoFocusHostComponent(type, props) {\n  switch (type) {\n    case 'button':\n    case 'input':\n    case 'select':\n    case 'textarea':\n      return !!props.autoFocus;\n  }\n  return false;\n}\n\nfunction getRootHostContext(rootContainerInstance) {\n  var type = void 0;\n  var namespace = void 0;\n  var nodeType = rootContainerInstance.nodeType;\n  switch (nodeType) {\n    case DOCUMENT_NODE:\n    case DOCUMENT_FRAGMENT_NODE:\n      {\n        type = nodeType === DOCUMENT_NODE ? '#document' : '#fragment';\n        var root = rootContainerInstance.documentElement;\n        namespace = root ? root.namespaceURI : getChildNamespace(null, '');\n        break;\n      }\n    default:\n      {\n        var container = nodeType === COMMENT_NODE ? rootContainerInstance.parentNode : rootContainerInstance;\n        var ownNamespace = container.namespaceURI || null;\n        type = container.tagName;\n        namespace = getChildNamespace(ownNamespace, type);\n        break;\n      }\n  }\n  {\n    var validatedTag = type.toLowerCase();\n    var _ancestorInfo = updatedAncestorInfo(null, validatedTag);\n    return { namespace: namespace, ancestorInfo: _ancestorInfo };\n  }\n  return namespace;\n}\n\nfunction getChildHostContext(parentHostContext, type, rootContainerInstance) {\n  {\n    var parentHostContextDev = parentHostContext;\n    var _namespace = getChildNamespace(parentHostContextDev.namespace, type);\n    var _ancestorInfo2 = updatedAncestorInfo(parentHostContextDev.ancestorInfo, type);\n    return { namespace: _namespace, ancestorInfo: _ancestorInfo2 };\n  }\n  var parentNamespace = parentHostContext;\n  return getChildNamespace(parentNamespace, type);\n}\n\nfunction getPublicInstance(instance) {\n  return instance;\n}\n\nfunction prepareForCommit(containerInfo) {\n  eventsEnabled = isEnabled();\n  selectionInformation = getSelectionInformation();\n  setEnabled(false);\n}\n\nfunction resetAfterCommit(containerInfo) {\n  restoreSelection(selectionInformation);\n  selectionInformation = null;\n  setEnabled(eventsEnabled);\n  eventsEnabled = null;\n}\n\nfunction createInstance(type, props, rootContainerInstance, hostContext, internalInstanceHandle) {\n  var parentNamespace = void 0;\n  {\n    // TODO: take namespace into account when validating.\n    var hostContextDev = hostContext;\n    validateDOMNesting(type, null, hostContextDev.ancestorInfo);\n    if (typeof props.children === 'string' || typeof props.children === 'number') {\n      var string = '' + props.children;\n      var ownAncestorInfo = updatedAncestorInfo(hostContextDev.ancestorInfo, type);\n      validateDOMNesting(null, string, ownAncestorInfo);\n    }\n    parentNamespace = hostContextDev.namespace;\n  }\n  var domElement = createElement(type, props, rootContainerInstance, parentNamespace);\n  precacheFiberNode(internalInstanceHandle, domElement);\n  updateFiberProps(domElement, props);\n  return domElement;\n}\n\nfunction appendInitialChild(parentInstance, child) {\n  parentInstance.appendChild(child);\n}\n\nfunction finalizeInitialChildren(domElement, type, props, rootContainerInstance, hostContext) {\n  setInitialProperties(domElement, type, props, rootContainerInstance);\n  return shouldAutoFocusHostComponent(type, props);\n}\n\nfunction prepareUpdate(domElement, type, oldProps, newProps, rootContainerInstance, hostContext) {\n  {\n    var hostContextDev = hostContext;\n    if (typeof newProps.children !== typeof oldProps.children && (typeof newProps.children === 'string' || typeof newProps.children === 'number')) {\n      var string = '' + newProps.children;\n      var ownAncestorInfo = updatedAncestorInfo(hostContextDev.ancestorInfo, type);\n      validateDOMNesting(null, string, ownAncestorInfo);\n    }\n  }\n  return diffProperties(domElement, type, oldProps, newProps, rootContainerInstance);\n}\n\nfunction shouldSetTextContent(type, props) {\n  return type === 'textarea' || type === 'option' || type === 'noscript' || typeof props.children === 'string' || typeof props.children === 'number' || typeof props.dangerouslySetInnerHTML === 'object' && props.dangerouslySetInnerHTML !== null && props.dangerouslySetInnerHTML.__html != null;\n}\n\nfunction shouldDeprioritizeSubtree(type, props) {\n  return !!props.hidden;\n}\n\nfunction createTextInstance(text, rootContainerInstance, hostContext, internalInstanceHandle) {\n  {\n    var hostContextDev = hostContext;\n    validateDOMNesting(null, text, hostContextDev.ancestorInfo);\n  }\n  var textNode = createTextNode(text, rootContainerInstance);\n  precacheFiberNode(internalInstanceHandle, textNode);\n  return textNode;\n}\n\nvar isPrimaryRenderer = true;\n// This initialization code may run even on server environments\n// if a component just imports ReactDOM (e.g. for findDOMNode).\n// Some environments might not have setTimeout or clearTimeout.\nvar scheduleTimeout = typeof setTimeout === 'function' ? setTimeout : undefined;\nvar cancelTimeout = typeof clearTimeout === 'function' ? clearTimeout : undefined;\nvar noTimeout = -1;\nvar schedulePassiveEffects = unstable_scheduleCallback;\nvar cancelPassiveEffects = unstable_cancelCallback;\n\n// -------------------\n//     Mutation\n// -------------------\n\nvar supportsMutation = true;\n\nfunction commitMount(domElement, type, newProps, internalInstanceHandle) {\n  // Despite the naming that might imply otherwise, this method only\n  // fires if there is an `Update` effect scheduled during mounting.\n  // This happens if `finalizeInitialChildren` returns `true` (which it\n  // does to implement the `autoFocus` attribute on the client). But\n  // there are also other cases when this might happen (such as patching\n  // up text content during hydration mismatch). So we'll check this again.\n  if (shouldAutoFocusHostComponent(type, newProps)) {\n    domElement.focus();\n  }\n}\n\nfunction commitUpdate(domElement, updatePayload, type, oldProps, newProps, internalInstanceHandle) {\n  // Update the props handle so that we know which props are the ones with\n  // with current event handlers.\n  updateFiberProps(domElement, newProps);\n  // Apply the diff to the DOM node.\n  updateProperties(domElement, updatePayload, type, oldProps, newProps);\n}\n\nfunction resetTextContent(domElement) {\n  setTextContent(domElement, '');\n}\n\nfunction commitTextUpdate(textInstance, oldText, newText) {\n  textInstance.nodeValue = newText;\n}\n\nfunction appendChild(parentInstance, child) {\n  parentInstance.appendChild(child);\n}\n\nfunction appendChildToContainer(container, child) {\n  var parentNode = void 0;\n  if (container.nodeType === COMMENT_NODE) {\n    parentNode = container.parentNode;\n    parentNode.insertBefore(child, container);\n  } else {\n    parentNode = container;\n    parentNode.appendChild(child);\n  }\n  // This container might be used for a portal.\n  // If something inside a portal is clicked, that click should bubble\n  // through the React tree. However, on Mobile Safari the click would\n  // never bubble through the *DOM* tree unless an ancestor with onclick\n  // event exists. So we wouldn't see it and dispatch it.\n  // This is why we ensure that non React root containers have inline onclick\n  // defined.\n  // https://github.com/facebook/react/issues/11918\n  var reactRootContainer = container._reactRootContainer;\n  if ((reactRootContainer === null || reactRootContainer === undefined) && parentNode.onclick === null) {\n    // TODO: This cast may not be sound for SVG, MathML or custom elements.\n    trapClickOnNonInteractiveElement(parentNode);\n  }\n}\n\nfunction insertBefore(parentInstance, child, beforeChild) {\n  parentInstance.insertBefore(child, beforeChild);\n}\n\nfunction insertInContainerBefore(container, child, beforeChild) {\n  if (container.nodeType === COMMENT_NODE) {\n    container.parentNode.insertBefore(child, beforeChild);\n  } else {\n    container.insertBefore(child, beforeChild);\n  }\n}\n\nfunction removeChild(parentInstance, child) {\n  parentInstance.removeChild(child);\n}\n\nfunction removeChildFromContainer(container, child) {\n  if (container.nodeType === COMMENT_NODE) {\n    container.parentNode.removeChild(child);\n  } else {\n    container.removeChild(child);\n  }\n}\n\nfunction clearSuspenseBoundary(parentInstance, suspenseInstance) {\n  var node = suspenseInstance;\n  // Delete all nodes within this suspense boundary.\n  // There might be nested nodes so we need to keep track of how\n  // deep we are and only break out when we're back on top.\n  var depth = 0;\n  do {\n    var nextNode = node.nextSibling;\n    parentInstance.removeChild(node);\n    if (nextNode && nextNode.nodeType === COMMENT_NODE) {\n      var data = nextNode.data;\n      if (data === SUSPENSE_END_DATA) {\n        if (depth === 0) {\n          parentInstance.removeChild(nextNode);\n          return;\n        } else {\n          depth--;\n        }\n      } else if (data === SUSPENSE_START_DATA) {\n        depth++;\n      }\n    }\n    node = nextNode;\n  } while (node);\n  // TODO: Warn, we didn't find the end comment boundary.\n}\n\nfunction clearSuspenseBoundaryFromContainer(container, suspenseInstance) {\n  if (container.nodeType === COMMENT_NODE) {\n    clearSuspenseBoundary(container.parentNode, suspenseInstance);\n  } else if (container.nodeType === ELEMENT_NODE) {\n    clearSuspenseBoundary(container, suspenseInstance);\n  } else {\n    // Document nodes should never contain suspense boundaries.\n  }\n}\n\nfunction hideInstance(instance) {\n  // TODO: Does this work for all element types? What about MathML? Should we\n  // pass host context to this method?\n  instance = instance;\n  instance.style.display = 'none';\n}\n\nfunction hideTextInstance(textInstance) {\n  textInstance.nodeValue = '';\n}\n\nfunction unhideInstance(instance, props) {\n  instance = instance;\n  var styleProp = props[STYLE];\n  var display = styleProp !== undefined && styleProp !== null && styleProp.hasOwnProperty('display') ? styleProp.display : null;\n  instance.style.display = dangerousStyleValue('display', display);\n}\n\nfunction unhideTextInstance(textInstance, text) {\n  textInstance.nodeValue = text;\n}\n\n// -------------------\n//     Hydration\n// -------------------\n\nvar supportsHydration = true;\n\nfunction canHydrateInstance(instance, type, props) {\n  if (instance.nodeType !== ELEMENT_NODE || type.toLowerCase() !== instance.nodeName.toLowerCase()) {\n    return null;\n  }\n  // This has now been refined to an element node.\n  return instance;\n}\n\nfunction canHydrateTextInstance(instance, text) {\n  if (text === '' || instance.nodeType !== TEXT_NODE) {\n    // Empty strings are not parsed by HTML so there won't be a correct match here.\n    return null;\n  }\n  // This has now been refined to a text node.\n  return instance;\n}\n\nfunction canHydrateSuspenseInstance(instance) {\n  if (instance.nodeType !== COMMENT_NODE) {\n    // Empty strings are not parsed by HTML so there won't be a correct match here.\n    return null;\n  }\n  // This has now been refined to a suspense node.\n  return instance;\n}\n\nfunction getNextHydratableSibling(instance) {\n  var node = instance.nextSibling;\n  // Skip non-hydratable nodes.\n  while (node && node.nodeType !== ELEMENT_NODE && node.nodeType !== TEXT_NODE && (!enableSuspenseServerRenderer || node.nodeType !== COMMENT_NODE || node.data !== SUSPENSE_START_DATA)) {\n    node = node.nextSibling;\n  }\n  return node;\n}\n\nfunction getFirstHydratableChild(parentInstance) {\n  var next = parentInstance.firstChild;\n  // Skip non-hydratable nodes.\n  while (next && next.nodeType !== ELEMENT_NODE && next.nodeType !== TEXT_NODE && (!enableSuspenseServerRenderer || next.nodeType !== COMMENT_NODE || next.data !== SUSPENSE_START_DATA)) {\n    next = next.nextSibling;\n  }\n  return next;\n}\n\nfunction hydrateInstance(instance, type, props, rootContainerInstance, hostContext, internalInstanceHandle) {\n  precacheFiberNode(internalInstanceHandle, instance);\n  // TODO: Possibly defer this until the commit phase where all the events\n  // get attached.\n  updateFiberProps(instance, props);\n  var parentNamespace = void 0;\n  {\n    var hostContextDev = hostContext;\n    parentNamespace = hostContextDev.namespace;\n  }\n  return diffHydratedProperties(instance, type, props, parentNamespace, rootContainerInstance);\n}\n\nfunction hydrateTextInstance(textInstance, text, internalInstanceHandle) {\n  precacheFiberNode(internalInstanceHandle, textInstance);\n  return diffHydratedText(textInstance, text);\n}\n\nfunction getNextHydratableInstanceAfterSuspenseInstance(suspenseInstance) {\n  var node = suspenseInstance.nextSibling;\n  // Skip past all nodes within this suspense boundary.\n  // There might be nested nodes so we need to keep track of how\n  // deep we are and only break out when we're back on top.\n  var depth = 0;\n  while (node) {\n    if (node.nodeType === COMMENT_NODE) {\n      var data = node.data;\n      if (data === SUSPENSE_END_DATA) {\n        if (depth === 0) {\n          return getNextHydratableSibling(node);\n        } else {\n          depth--;\n        }\n      } else if (data === SUSPENSE_START_DATA) {\n        depth++;\n      }\n    }\n    node = node.nextSibling;\n  }\n  // TODO: Warn, we didn't find the end comment boundary.\n  return null;\n}\n\nfunction didNotMatchHydratedContainerTextInstance(parentContainer, textInstance, text) {\n  {\n    warnForUnmatchedText(textInstance, text);\n  }\n}\n\nfunction didNotMatchHydratedTextInstance(parentType, parentProps, parentInstance, textInstance, text) {\n  if (true && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) {\n    warnForUnmatchedText(textInstance, text);\n  }\n}\n\nfunction didNotHydrateContainerInstance(parentContainer, instance) {\n  {\n    if (instance.nodeType === ELEMENT_NODE) {\n      warnForDeletedHydratableElement(parentContainer, instance);\n    } else if (instance.nodeType === COMMENT_NODE) {\n      // TODO: warnForDeletedHydratableSuspenseBoundary\n    } else {\n      warnForDeletedHydratableText(parentContainer, instance);\n    }\n  }\n}\n\nfunction didNotHydrateInstance(parentType, parentProps, parentInstance, instance) {\n  if (true && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) {\n    if (instance.nodeType === ELEMENT_NODE) {\n      warnForDeletedHydratableElement(parentInstance, instance);\n    } else if (instance.nodeType === COMMENT_NODE) {\n      // TODO: warnForDeletedHydratableSuspenseBoundary\n    } else {\n      warnForDeletedHydratableText(parentInstance, instance);\n    }\n  }\n}\n\nfunction didNotFindHydratableContainerInstance(parentContainer, type, props) {\n  {\n    warnForInsertedHydratedElement(parentContainer, type, props);\n  }\n}\n\nfunction didNotFindHydratableContainerTextInstance(parentContainer, text) {\n  {\n    warnForInsertedHydratedText(parentContainer, text);\n  }\n}\n\n\n\nfunction didNotFindHydratableInstance(parentType, parentProps, parentInstance, type, props) {\n  if (true && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) {\n    warnForInsertedHydratedElement(parentInstance, type, props);\n  }\n}\n\nfunction didNotFindHydratableTextInstance(parentType, parentProps, parentInstance, text) {\n  if (true && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) {\n    warnForInsertedHydratedText(parentInstance, text);\n  }\n}\n\nfunction didNotFindHydratableSuspenseInstance(parentType, parentProps, parentInstance) {\n  if (true && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) {\n    // TODO: warnForInsertedHydratedSuspense(parentInstance);\n  }\n}\n\n// Prefix measurements so that it's possible to filter them.\n// Longer prefixes are hard to read in DevTools.\nvar reactEmoji = '\\u269B';\nvar warningEmoji = '\\u26D4';\nvar supportsUserTiming = typeof performance !== 'undefined' && typeof performance.mark === 'function' && typeof performance.clearMarks === 'function' && typeof performance.measure === 'function' && typeof performance.clearMeasures === 'function';\n\n// Keep track of current fiber so that we know the path to unwind on pause.\n// TODO: this looks the same as nextUnitOfWork in scheduler. Can we unify them?\nvar currentFiber = null;\n// If we're in the middle of user code, which fiber and method is it?\n// Reusing `currentFiber` would be confusing for this because user code fiber\n// can change during commit phase too, but we don't need to unwind it (since\n// lifecycles in the commit phase don't resemble a tree).\nvar currentPhase = null;\nvar currentPhaseFiber = null;\n// Did lifecycle hook schedule an update? This is often a performance problem,\n// so we will keep track of it, and include it in the report.\n// Track commits caused by cascading updates.\nvar isCommitting = false;\nvar hasScheduledUpdateInCurrentCommit = false;\nvar hasScheduledUpdateInCurrentPhase = false;\nvar commitCountInCurrentWorkLoop = 0;\nvar effectCountInCurrentCommit = 0;\nvar isWaitingForCallback = false;\n// During commits, we only show a measurement once per method name\n// to avoid stretch the commit phase with measurement overhead.\nvar labelsInCurrentCommit = new Set();\n\nvar formatMarkName = function (markName) {\n  return reactEmoji + ' ' + markName;\n};\n\nvar formatLabel = function (label, warning) {\n  var prefix = warning ? warningEmoji + ' ' : reactEmoji + ' ';\n  var suffix = warning ? ' Warning: ' + warning : '';\n  return '' + prefix + label + suffix;\n};\n\nvar beginMark = function (markName) {\n  performance.mark(formatMarkName(markName));\n};\n\nvar clearMark = function (markName) {\n  performance.clearMarks(formatMarkName(markName));\n};\n\nvar endMark = function (label, markName, warning) {\n  var formattedMarkName = formatMarkName(markName);\n  var formattedLabel = formatLabel(label, warning);\n  try {\n    performance.measure(formattedLabel, formattedMarkName);\n  } catch (err) {}\n  // If previous mark was missing for some reason, this will throw.\n  // This could only happen if React crashed in an unexpected place earlier.\n  // Don't pile on with more errors.\n\n  // Clear marks immediately to avoid growing buffer.\n  performance.clearMarks(formattedMarkName);\n  performance.clearMeasures(formattedLabel);\n};\n\nvar getFiberMarkName = function (label, debugID) {\n  return label + ' (#' + debugID + ')';\n};\n\nvar getFiberLabel = function (componentName, isMounted, phase) {\n  if (phase === null) {\n    // These are composite component total time measurements.\n    return componentName + ' [' + (isMounted ? 'update' : 'mount') + ']';\n  } else {\n    // Composite component methods.\n    return componentName + '.' + phase;\n  }\n};\n\nvar beginFiberMark = function (fiber, phase) {\n  var componentName = getComponentName(fiber.type) || 'Unknown';\n  var debugID = fiber._debugID;\n  var isMounted = fiber.alternate !== null;\n  var label = getFiberLabel(componentName, isMounted, phase);\n\n  if (isCommitting && labelsInCurrentCommit.has(label)) {\n    // During the commit phase, we don't show duplicate labels because\n    // there is a fixed overhead for every measurement, and we don't\n    // want to stretch the commit phase beyond necessary.\n    return false;\n  }\n  labelsInCurrentCommit.add(label);\n\n  var markName = getFiberMarkName(label, debugID);\n  beginMark(markName);\n  return true;\n};\n\nvar clearFiberMark = function (fiber, phase) {\n  var componentName = getComponentName(fiber.type) || 'Unknown';\n  var debugID = fiber._debugID;\n  var isMounted = fiber.alternate !== null;\n  var label = getFiberLabel(componentName, isMounted, phase);\n  var markName = getFiberMarkName(label, debugID);\n  clearMark(markName);\n};\n\nvar endFiberMark = function (fiber, phase, warning) {\n  var componentName = getComponentName(fiber.type) || 'Unknown';\n  var debugID = fiber._debugID;\n  var isMounted = fiber.alternate !== null;\n  var label = getFiberLabel(componentName, isMounted, phase);\n  var markName = getFiberMarkName(label, debugID);\n  endMark(label, markName, warning);\n};\n\nvar shouldIgnoreFiber = function (fiber) {\n  // Host components should be skipped in the timeline.\n  // We could check typeof fiber.type, but does this work with RN?\n  switch (fiber.tag) {\n    case HostRoot:\n    case HostComponent:\n    case HostText:\n    case HostPortal:\n    case Fragment:\n    case ContextProvider:\n    case ContextConsumer:\n    case Mode:\n      return true;\n    default:\n      return false;\n  }\n};\n\nvar clearPendingPhaseMeasurement = function () {\n  if (currentPhase !== null && currentPhaseFiber !== null) {\n    clearFiberMark(currentPhaseFiber, currentPhase);\n  }\n  currentPhaseFiber = null;\n  currentPhase = null;\n  hasScheduledUpdateInCurrentPhase = false;\n};\n\nvar pauseTimers = function () {\n  // Stops all currently active measurements so that they can be resumed\n  // if we continue in a later deferred loop from the same unit of work.\n  var fiber = currentFiber;\n  while (fiber) {\n    if (fiber._debugIsCurrentlyTiming) {\n      endFiberMark(fiber, null, null);\n    }\n    fiber = fiber.return;\n  }\n};\n\nvar resumeTimersRecursively = function (fiber) {\n  if (fiber.return !== null) {\n    resumeTimersRecursively(fiber.return);\n  }\n  if (fiber._debugIsCurrentlyTiming) {\n    beginFiberMark(fiber, null);\n  }\n};\n\nvar resumeTimers = function () {\n  // Resumes all measurements that were active during the last deferred loop.\n  if (currentFiber !== null) {\n    resumeTimersRecursively(currentFiber);\n  }\n};\n\nfunction recordEffect() {\n  if (enableUserTimingAPI) {\n    effectCountInCurrentCommit++;\n  }\n}\n\nfunction recordScheduleUpdate() {\n  if (enableUserTimingAPI) {\n    if (isCommitting) {\n      hasScheduledUpdateInCurrentCommit = true;\n    }\n    if (currentPhase !== null && currentPhase !== 'componentWillMount' && currentPhase !== 'componentWillReceiveProps') {\n      hasScheduledUpdateInCurrentPhase = true;\n    }\n  }\n}\n\nfunction startRequestCallbackTimer() {\n  if (enableUserTimingAPI) {\n    if (supportsUserTiming && !isWaitingForCallback) {\n      isWaitingForCallback = true;\n      beginMark('(Waiting for async callback...)');\n    }\n  }\n}\n\nfunction stopRequestCallbackTimer(didExpire, expirationTime) {\n  if (enableUserTimingAPI) {\n    if (supportsUserTiming) {\n      isWaitingForCallback = false;\n      var warning = didExpire ? 'React was blocked by main thread' : null;\n      endMark('(Waiting for async callback... will force flush in ' + expirationTime + ' ms)', '(Waiting for async callback...)', warning);\n    }\n  }\n}\n\nfunction startWorkTimer(fiber) {\n  if (enableUserTimingAPI) {\n    if (!supportsUserTiming || shouldIgnoreFiber(fiber)) {\n      return;\n    }\n    // If we pause, this is the fiber to unwind from.\n    currentFiber = fiber;\n    if (!beginFiberMark(fiber, null)) {\n      return;\n    }\n    fiber._debugIsCurrentlyTiming = true;\n  }\n}\n\nfunction cancelWorkTimer(fiber) {\n  if (enableUserTimingAPI) {\n    if (!supportsUserTiming || shouldIgnoreFiber(fiber)) {\n      return;\n    }\n    // Remember we shouldn't complete measurement for this fiber.\n    // Otherwise flamechart will be deep even for small updates.\n    fiber._debugIsCurrentlyTiming = false;\n    clearFiberMark(fiber, null);\n  }\n}\n\nfunction stopWorkTimer(fiber) {\n  if (enableUserTimingAPI) {\n    if (!supportsUserTiming || shouldIgnoreFiber(fiber)) {\n      return;\n    }\n    // If we pause, its parent is the fiber to unwind from.\n    currentFiber = fiber.return;\n    if (!fiber._debugIsCurrentlyTiming) {\n      return;\n    }\n    fiber._debugIsCurrentlyTiming = false;\n    endFiberMark(fiber, null, null);\n  }\n}\n\nfunction stopFailedWorkTimer(fiber) {\n  if (enableUserTimingAPI) {\n    if (!supportsUserTiming || shouldIgnoreFiber(fiber)) {\n      return;\n    }\n    // If we pause, its parent is the fiber to unwind from.\n    currentFiber = fiber.return;\n    if (!fiber._debugIsCurrentlyTiming) {\n      return;\n    }\n    fiber._debugIsCurrentlyTiming = false;\n    var warning = fiber.tag === SuspenseComponent || fiber.tag === DehydratedSuspenseComponent ? 'Rendering was suspended' : 'An error was thrown inside this error boundary';\n    endFiberMark(fiber, null, warning);\n  }\n}\n\nfunction startPhaseTimer(fiber, phase) {\n  if (enableUserTimingAPI) {\n    if (!supportsUserTiming) {\n      return;\n    }\n    clearPendingPhaseMeasurement();\n    if (!beginFiberMark(fiber, phase)) {\n      return;\n    }\n    currentPhaseFiber = fiber;\n    currentPhase = phase;\n  }\n}\n\nfunction stopPhaseTimer() {\n  if (enableUserTimingAPI) {\n    if (!supportsUserTiming) {\n      return;\n    }\n    if (currentPhase !== null && currentPhaseFiber !== null) {\n      var warning = hasScheduledUpdateInCurrentPhase ? 'Scheduled a cascading update' : null;\n      endFiberMark(currentPhaseFiber, currentPhase, warning);\n    }\n    currentPhase = null;\n    currentPhaseFiber = null;\n  }\n}\n\nfunction startWorkLoopTimer(nextUnitOfWork) {\n  if (enableUserTimingAPI) {\n    currentFiber = nextUnitOfWork;\n    if (!supportsUserTiming) {\n      return;\n    }\n    commitCountInCurrentWorkLoop = 0;\n    // This is top level call.\n    // Any other measurements are performed within.\n    beginMark('(React Tree Reconciliation)');\n    // Resume any measurements that were in progress during the last loop.\n    resumeTimers();\n  }\n}\n\nfunction stopWorkLoopTimer(interruptedBy, didCompleteRoot) {\n  if (enableUserTimingAPI) {\n    if (!supportsUserTiming) {\n      return;\n    }\n    var warning = null;\n    if (interruptedBy !== null) {\n      if (interruptedBy.tag === HostRoot) {\n        warning = 'A top-level update interrupted the previous render';\n      } else {\n        var componentName = getComponentName(interruptedBy.type) || 'Unknown';\n        warning = 'An update to ' + componentName + ' interrupted the previous render';\n      }\n    } else if (commitCountInCurrentWorkLoop > 1) {\n      warning = 'There were cascading updates';\n    }\n    commitCountInCurrentWorkLoop = 0;\n    var label = didCompleteRoot ? '(React Tree Reconciliation: Completed Root)' : '(React Tree Reconciliation: Yielded)';\n    // Pause any measurements until the next loop.\n    pauseTimers();\n    endMark(label, '(React Tree Reconciliation)', warning);\n  }\n}\n\nfunction startCommitTimer() {\n  if (enableUserTimingAPI) {\n    if (!supportsUserTiming) {\n      return;\n    }\n    isCommitting = true;\n    hasScheduledUpdateInCurrentCommit = false;\n    labelsInCurrentCommit.clear();\n    beginMark('(Committing Changes)');\n  }\n}\n\nfunction stopCommitTimer() {\n  if (enableUserTimingAPI) {\n    if (!supportsUserTiming) {\n      return;\n    }\n\n    var warning = null;\n    if (hasScheduledUpdateInCurrentCommit) {\n      warning = 'Lifecycle hook scheduled a cascading update';\n    } else if (commitCountInCurrentWorkLoop > 0) {\n      warning = 'Caused by a cascading update in earlier commit';\n    }\n    hasScheduledUpdateInCurrentCommit = false;\n    commitCountInCurrentWorkLoop++;\n    isCommitting = false;\n    labelsInCurrentCommit.clear();\n\n    endMark('(Committing Changes)', '(Committing Changes)', warning);\n  }\n}\n\nfunction startCommitSnapshotEffectsTimer() {\n  if (enableUserTimingAPI) {\n    if (!supportsUserTiming) {\n      return;\n    }\n    effectCountInCurrentCommit = 0;\n    beginMark('(Committing Snapshot Effects)');\n  }\n}\n\nfunction stopCommitSnapshotEffectsTimer() {\n  if (enableUserTimingAPI) {\n    if (!supportsUserTiming) {\n      return;\n    }\n    var count = effectCountInCurrentCommit;\n    effectCountInCurrentCommit = 0;\n    endMark('(Committing Snapshot Effects: ' + count + ' Total)', '(Committing Snapshot Effects)', null);\n  }\n}\n\nfunction startCommitHostEffectsTimer() {\n  if (enableUserTimingAPI) {\n    if (!supportsUserTiming) {\n      return;\n    }\n    effectCountInCurrentCommit = 0;\n    beginMark('(Committing Host Effects)');\n  }\n}\n\nfunction stopCommitHostEffectsTimer() {\n  if (enableUserTimingAPI) {\n    if (!supportsUserTiming) {\n      return;\n    }\n    var count = effectCountInCurrentCommit;\n    effectCountInCurrentCommit = 0;\n    endMark('(Committing Host Effects: ' + count + ' Total)', '(Committing Host Effects)', null);\n  }\n}\n\nfunction startCommitLifeCyclesTimer() {\n  if (enableUserTimingAPI) {\n    if (!supportsUserTiming) {\n      return;\n    }\n    effectCountInCurrentCommit = 0;\n    beginMark('(Calling Lifecycle Methods)');\n  }\n}\n\nfunction stopCommitLifeCyclesTimer() {\n  if (enableUserTimingAPI) {\n    if (!supportsUserTiming) {\n      return;\n    }\n    var count = effectCountInCurrentCommit;\n    effectCountInCurrentCommit = 0;\n    endMark('(Calling Lifecycle Methods: ' + count + ' Total)', '(Calling Lifecycle Methods)', null);\n  }\n}\n\nvar valueStack = [];\n\nvar fiberStack = void 0;\n\n{\n  fiberStack = [];\n}\n\nvar index = -1;\n\nfunction createCursor(defaultValue) {\n  return {\n    current: defaultValue\n  };\n}\n\nfunction pop(cursor, fiber) {\n  if (index < 0) {\n    {\n      warningWithoutStack$1(false, 'Unexpected pop.');\n    }\n    return;\n  }\n\n  {\n    if (fiber !== fiberStack[index]) {\n      warningWithoutStack$1(false, 'Unexpected Fiber popped.');\n    }\n  }\n\n  cursor.current = valueStack[index];\n\n  valueStack[index] = null;\n\n  {\n    fiberStack[index] = null;\n  }\n\n  index--;\n}\n\nfunction push(cursor, value, fiber) {\n  index++;\n\n  valueStack[index] = cursor.current;\n\n  {\n    fiberStack[index] = fiber;\n  }\n\n  cursor.current = value;\n}\n\nfunction checkThatStackIsEmpty() {\n  {\n    if (index !== -1) {\n      warningWithoutStack$1(false, 'Expected an empty stack. Something was not reset properly.');\n    }\n  }\n}\n\nfunction resetStackAfterFatalErrorInDev() {\n  {\n    index = -1;\n    valueStack.length = 0;\n    fiberStack.length = 0;\n  }\n}\n\nvar warnedAboutMissingGetChildContext = void 0;\n\n{\n  warnedAboutMissingGetChildContext = {};\n}\n\nvar emptyContextObject = {};\n{\n  Object.freeze(emptyContextObject);\n}\n\n// A cursor to the current merged context object on the stack.\nvar contextStackCursor = createCursor(emptyContextObject);\n// A cursor to a boolean indicating whether the context has changed.\nvar didPerformWorkStackCursor = createCursor(false);\n// Keep track of the previous context object that was on the stack.\n// We use this to get access to the parent context after we have already\n// pushed the next context provider, and now need to merge their contexts.\nvar previousContext = emptyContextObject;\n\nfunction getUnmaskedContext(workInProgress, Component, didPushOwnContextIfProvider) {\n  if (didPushOwnContextIfProvider && isContextProvider(Component)) {\n    // If the fiber is a context provider itself, when we read its context\n    // we may have already pushed its own child context on the stack. A context\n    // provider should not \"see\" its own child context. Therefore we read the\n    // previous (parent) context instead for a context provider.\n    return previousContext;\n  }\n  return contextStackCursor.current;\n}\n\nfunction cacheContext(workInProgress, unmaskedContext, maskedContext) {\n  var instance = workInProgress.stateNode;\n  instance.__reactInternalMemoizedUnmaskedChildContext = unmaskedContext;\n  instance.__reactInternalMemoizedMaskedChildContext = maskedContext;\n}\n\nfunction getMaskedContext(workInProgress, unmaskedContext) {\n  var type = workInProgress.type;\n  var contextTypes = type.contextTypes;\n  if (!contextTypes) {\n    return emptyContextObject;\n  }\n\n  // Avoid recreating masked context unless unmasked context has changed.\n  // Failing to do this will result in unnecessary calls to componentWillReceiveProps.\n  // This may trigger infinite loops if componentWillReceiveProps calls setState.\n  var instance = workInProgress.stateNode;\n  if (instance && instance.__reactInternalMemoizedUnmaskedChildContext === unmaskedContext) {\n    return instance.__reactInternalMemoizedMaskedChildContext;\n  }\n\n  var context = {};\n  for (var key in contextTypes) {\n    context[key] = unmaskedContext[key];\n  }\n\n  {\n    var name = getComponentName(type) || 'Unknown';\n    checkPropTypes_1(contextTypes, context, 'context', name, getCurrentFiberStackInDev);\n  }\n\n  // Cache unmasked context so we can avoid recreating masked context unless necessary.\n  // Context is created before the class component is instantiated so check for instance.\n  if (instance) {\n    cacheContext(workInProgress, unmaskedContext, context);\n  }\n\n  return context;\n}\n\nfunction hasContextChanged() {\n  return didPerformWorkStackCursor.current;\n}\n\nfunction isContextProvider(type) {\n  var childContextTypes = type.childContextTypes;\n  return childContextTypes !== null && childContextTypes !== undefined;\n}\n\nfunction popContext(fiber) {\n  pop(didPerformWorkStackCursor, fiber);\n  pop(contextStackCursor, fiber);\n}\n\nfunction popTopLevelContextObject(fiber) {\n  pop(didPerformWorkStackCursor, fiber);\n  pop(contextStackCursor, fiber);\n}\n\nfunction pushTopLevelContextObject(fiber, context, didChange) {\n  !(contextStackCursor.current === emptyContextObject) ? invariant(false, 'Unexpected context found on stack. This error is likely caused by a bug in React. Please file an issue.') : void 0;\n\n  push(contextStackCursor, context, fiber);\n  push(didPerformWorkStackCursor, didChange, fiber);\n}\n\nfunction processChildContext(fiber, type, parentContext) {\n  var instance = fiber.stateNode;\n  var childContextTypes = type.childContextTypes;\n\n  // TODO (bvaughn) Replace this behavior with an invariant() in the future.\n  // It has only been added in Fiber to match the (unintentional) behavior in Stack.\n  if (typeof instance.getChildContext !== 'function') {\n    {\n      var componentName = getComponentName(type) || 'Unknown';\n\n      if (!warnedAboutMissingGetChildContext[componentName]) {\n        warnedAboutMissingGetChildContext[componentName] = true;\n        warningWithoutStack$1(false, '%s.childContextTypes is specified but there is no getChildContext() method ' + 'on the instance. You can either define getChildContext() on %s or remove ' + 'childContextTypes from it.', componentName, componentName);\n      }\n    }\n    return parentContext;\n  }\n\n  var childContext = void 0;\n  {\n    setCurrentPhase('getChildContext');\n  }\n  startPhaseTimer(fiber, 'getChildContext');\n  childContext = instance.getChildContext();\n  stopPhaseTimer();\n  {\n    setCurrentPhase(null);\n  }\n  for (var contextKey in childContext) {\n    !(contextKey in childContextTypes) ? invariant(false, '%s.getChildContext(): key \"%s\" is not defined in childContextTypes.', getComponentName(type) || 'Unknown', contextKey) : void 0;\n  }\n  {\n    var name = getComponentName(type) || 'Unknown';\n    checkPropTypes_1(childContextTypes, childContext, 'child context', name,\n    // In practice, there is one case in which we won't get a stack. It's when\n    // somebody calls unstable_renderSubtreeIntoContainer() and we process\n    // context from the parent component instance. The stack will be missing\n    // because it's outside of the reconciliation, and so the pointer has not\n    // been set. This is rare and doesn't matter. We'll also remove that API.\n    getCurrentFiberStackInDev);\n  }\n\n  return _assign({}, parentContext, childContext);\n}\n\nfunction pushContextProvider(workInProgress) {\n  var instance = workInProgress.stateNode;\n  // We push the context as early as possible to ensure stack integrity.\n  // If the instance does not exist yet, we will push null at first,\n  // and replace it on the stack later when invalidating the context.\n  var memoizedMergedChildContext = instance && instance.__reactInternalMemoizedMergedChildContext || emptyContextObject;\n\n  // Remember the parent context so we can merge with it later.\n  // Inherit the parent's did-perform-work value to avoid inadvertently blocking updates.\n  previousContext = contextStackCursor.current;\n  push(contextStackCursor, memoizedMergedChildContext, workInProgress);\n  push(didPerformWorkStackCursor, didPerformWorkStackCursor.current, workInProgress);\n\n  return true;\n}\n\nfunction invalidateContextProvider(workInProgress, type, didChange) {\n  var instance = workInProgress.stateNode;\n  !instance ? invariant(false, 'Expected to have an instance by this point. This error is likely caused by a bug in React. Please file an issue.') : void 0;\n\n  if (didChange) {\n    // Merge parent and own context.\n    // Skip this if we're not updating due to sCU.\n    // This avoids unnecessarily recomputing memoized values.\n    var mergedContext = processChildContext(workInProgress, type, previousContext);\n    instance.__reactInternalMemoizedMergedChildContext = mergedContext;\n\n    // Replace the old (or empty) context with the new one.\n    // It is important to unwind the context in the reverse order.\n    pop(didPerformWorkStackCursor, workInProgress);\n    pop(contextStackCursor, workInProgress);\n    // Now push the new context and mark that it has changed.\n    push(contextStackCursor, mergedContext, workInProgress);\n    push(didPerformWorkStackCursor, didChange, workInProgress);\n  } else {\n    pop(didPerformWorkStackCursor, workInProgress);\n    push(didPerformWorkStackCursor, didChange, workInProgress);\n  }\n}\n\nfunction findCurrentUnmaskedContext(fiber) {\n  // Currently this is only used with renderSubtreeIntoContainer; not sure if it\n  // makes sense elsewhere\n  !(isFiberMounted(fiber) && fiber.tag === ClassComponent) ? invariant(false, 'Expected subtree parent to be a mounted class component. This error is likely caused by a bug in React. Please file an issue.') : void 0;\n\n  var node = fiber;\n  do {\n    switch (node.tag) {\n      case HostRoot:\n        return node.stateNode.context;\n      case ClassComponent:\n        {\n          var Component = node.type;\n          if (isContextProvider(Component)) {\n            return node.stateNode.__reactInternalMemoizedMergedChildContext;\n          }\n          break;\n        }\n    }\n    node = node.return;\n  } while (node !== null);\n  invariant(false, 'Found unexpected detached subtree parent. This error is likely caused by a bug in React. Please file an issue.');\n}\n\nvar onCommitFiberRoot = null;\nvar onCommitFiberUnmount = null;\nvar hasLoggedError = false;\n\nfunction catchErrors(fn) {\n  return function (arg) {\n    try {\n      return fn(arg);\n    } catch (err) {\n      if (true && !hasLoggedError) {\n        hasLoggedError = true;\n        warningWithoutStack$1(false, 'React DevTools encountered an error: %s', err);\n      }\n    }\n  };\n}\n\nvar isDevToolsPresent = typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== 'undefined';\n\nfunction injectInternals(internals) {\n  if (typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ === 'undefined') {\n    // No DevTools\n    return false;\n  }\n  var hook = __REACT_DEVTOOLS_GLOBAL_HOOK__;\n  if (hook.isDisabled) {\n    // This isn't a real property on the hook, but it can be set to opt out\n    // of DevTools integration and associated warnings and logs.\n    // https://github.com/facebook/react/issues/3877\n    return true;\n  }\n  if (!hook.supportsFiber) {\n    {\n      warningWithoutStack$1(false, 'The installed version of React DevTools is too old and will not work ' + 'with the current version of React. Please update React DevTools. ' + 'https://fb.me/react-devtools');\n    }\n    // DevTools exists, even though it doesn't support Fiber.\n    return true;\n  }\n  try {\n    var rendererID = hook.inject(internals);\n    // We have successfully injected, so now it is safe to set up hooks.\n    onCommitFiberRoot = catchErrors(function (root) {\n      return hook.onCommitFiberRoot(rendererID, root);\n    });\n    onCommitFiberUnmount = catchErrors(function (fiber) {\n      return hook.onCommitFiberUnmount(rendererID, fiber);\n    });\n  } catch (err) {\n    // Catch all errors because it is unsafe to throw during initialization.\n    {\n      warningWithoutStack$1(false, 'React DevTools encountered an error: %s.', err);\n    }\n  }\n  // DevTools exists\n  return true;\n}\n\nfunction onCommitRoot(root) {\n  if (typeof onCommitFiberRoot === 'function') {\n    onCommitFiberRoot(root);\n  }\n}\n\nfunction onCommitUnmount(fiber) {\n  if (typeof onCommitFiberUnmount === 'function') {\n    onCommitFiberUnmount(fiber);\n  }\n}\n\n// Max 31 bit integer. The max integer size in V8 for 32-bit systems.\n// Math.pow(2, 30) - 1\n// 0b111111111111111111111111111111\nvar maxSigned31BitInt = 1073741823;\n\nvar NoWork = 0;\nvar Never = 1;\nvar Sync = maxSigned31BitInt;\n\nvar UNIT_SIZE = 10;\nvar MAGIC_NUMBER_OFFSET = maxSigned31BitInt - 1;\n\n// 1 unit of expiration time represents 10ms.\nfunction msToExpirationTime(ms) {\n  // Always add an offset so that we don't clash with the magic number for NoWork.\n  return MAGIC_NUMBER_OFFSET - (ms / UNIT_SIZE | 0);\n}\n\nfunction expirationTimeToMs(expirationTime) {\n  return (MAGIC_NUMBER_OFFSET - expirationTime) * UNIT_SIZE;\n}\n\nfunction ceiling(num, precision) {\n  return ((num / precision | 0) + 1) * precision;\n}\n\nfunction computeExpirationBucket(currentTime, expirationInMs, bucketSizeMs) {\n  return MAGIC_NUMBER_OFFSET - ceiling(MAGIC_NUMBER_OFFSET - currentTime + expirationInMs / UNIT_SIZE, bucketSizeMs / UNIT_SIZE);\n}\n\nvar LOW_PRIORITY_EXPIRATION = 5000;\nvar LOW_PRIORITY_BATCH_SIZE = 250;\n\nfunction computeAsyncExpiration(currentTime) {\n  return computeExpirationBucket(currentTime, LOW_PRIORITY_EXPIRATION, LOW_PRIORITY_BATCH_SIZE);\n}\n\n// We intentionally set a higher expiration time for interactive updates in\n// dev than in production.\n//\n// If the main thread is being blocked so long that you hit the expiration,\n// it's a problem that could be solved with better scheduling.\n//\n// People will be more likely to notice this and fix it with the long\n// expiration time in development.\n//\n// In production we opt for better UX at the risk of masking scheduling\n// problems, by expiring fast.\nvar HIGH_PRIORITY_EXPIRATION = 500;\nvar HIGH_PRIORITY_BATCH_SIZE = 100;\n\nfunction computeInteractiveExpiration(currentTime) {\n  return computeExpirationBucket(currentTime, HIGH_PRIORITY_EXPIRATION, HIGH_PRIORITY_BATCH_SIZE);\n}\n\nvar NoContext = 0;\nvar ConcurrentMode = 1;\nvar StrictMode = 2;\nvar ProfileMode = 4;\n\nvar hasBadMapPolyfill = void 0;\n\n{\n  hasBadMapPolyfill = false;\n  try {\n    var nonExtensibleObject = Object.preventExtensions({});\n    var testMap = new Map([[nonExtensibleObject, null]]);\n    var testSet = new Set([nonExtensibleObject]);\n    // This is necessary for Rollup to not consider these unused.\n    // https://github.com/rollup/rollup/issues/1771\n    // TODO: we can remove these if Rollup fixes the bug.\n    testMap.set(0, 0);\n    testSet.add(0);\n  } catch (e) {\n    // TODO: Consider warning about bad polyfills\n    hasBadMapPolyfill = true;\n  }\n}\n\n// A Fiber is work on a Component that needs to be done or was done. There can\n// be more than one per component.\n\n\nvar debugCounter = void 0;\n\n{\n  debugCounter = 1;\n}\n\nfunction FiberNode(tag, pendingProps, key, mode) {\n  // Instance\n  this.tag = tag;\n  this.key = key;\n  this.elementType = null;\n  this.type = null;\n  this.stateNode = null;\n\n  // Fiber\n  this.return = null;\n  this.child = null;\n  this.sibling = null;\n  this.index = 0;\n\n  this.ref = null;\n\n  this.pendingProps = pendingProps;\n  this.memoizedProps = null;\n  this.updateQueue = null;\n  this.memoizedState = null;\n  this.contextDependencies = null;\n\n  this.mode = mode;\n\n  // Effects\n  this.effectTag = NoEffect;\n  this.nextEffect = null;\n\n  this.firstEffect = null;\n  this.lastEffect = null;\n\n  this.expirationTime = NoWork;\n  this.childExpirationTime = NoWork;\n\n  this.alternate = null;\n\n  if (enableProfilerTimer) {\n    // Note: The following is done to avoid a v8 performance cliff.\n    //\n    // Initializing the fields below to smis and later updating them with\n    // double values will cause Fibers to end up having separate shapes.\n    // This behavior/bug has something to do with Object.preventExtension().\n    // Fortunately this only impacts DEV builds.\n    // Unfortunately it makes React unusably slow for some applications.\n    // To work around this, initialize the fields below with doubles.\n    //\n    // Learn more about this here:\n    // https://github.com/facebook/react/issues/14365\n    // https://bugs.chromium.org/p/v8/issues/detail?id=8538\n    this.actualDuration = Number.NaN;\n    this.actualStartTime = Number.NaN;\n    this.selfBaseDuration = Number.NaN;\n    this.treeBaseDuration = Number.NaN;\n\n    // It's okay to replace the initial doubles with smis after initialization.\n    // This won't trigger the performance cliff mentioned above,\n    // and it simplifies other profiler code (including DevTools).\n    this.actualDuration = 0;\n    this.actualStartTime = -1;\n    this.selfBaseDuration = 0;\n    this.treeBaseDuration = 0;\n  }\n\n  {\n    this._debugID = debugCounter++;\n    this._debugSource = null;\n    this._debugOwner = null;\n    this._debugIsCurrentlyTiming = false;\n    this._debugHookTypes = null;\n    if (!hasBadMapPolyfill && typeof Object.preventExtensions === 'function') {\n      Object.preventExtensions(this);\n    }\n  }\n}\n\n// This is a constructor function, rather than a POJO constructor, still\n// please ensure we do the following:\n// 1) Nobody should add any instance methods on this. Instance methods can be\n//    more difficult to predict when they get optimized and they are almost\n//    never inlined properly in static compilers.\n// 2) Nobody should rely on `instanceof Fiber` for type testing. We should\n//    always know when it is a fiber.\n// 3) We might want to experiment with using numeric keys since they are easier\n//    to optimize in a non-JIT environment.\n// 4) We can easily go from a constructor to a createFiber object literal if that\n//    is faster.\n// 5) It should be easy to port this to a C struct and keep a C implementation\n//    compatible.\nvar createFiber = function (tag, pendingProps, key, mode) {\n  // $FlowFixMe: the shapes are exact here but Flow doesn't like constructors\n  return new FiberNode(tag, pendingProps, key, mode);\n};\n\nfunction shouldConstruct(Component) {\n  var prototype = Component.prototype;\n  return !!(prototype && prototype.isReactComponent);\n}\n\nfunction isSimpleFunctionComponent(type) {\n  return typeof type === 'function' && !shouldConstruct(type) && type.defaultProps === undefined;\n}\n\nfunction resolveLazyComponentTag(Component) {\n  if (typeof Component === 'function') {\n    return shouldConstruct(Component) ? ClassComponent : FunctionComponent;\n  } else if (Component !== undefined && Component !== null) {\n    var $$typeof = Component.$$typeof;\n    if ($$typeof === REACT_FORWARD_REF_TYPE) {\n      return ForwardRef;\n    }\n    if ($$typeof === REACT_MEMO_TYPE) {\n      return MemoComponent;\n    }\n  }\n  return IndeterminateComponent;\n}\n\n// This is used to create an alternate fiber to do work on.\nfunction createWorkInProgress(current, pendingProps, expirationTime) {\n  var workInProgress = current.alternate;\n  if (workInProgress === null) {\n    // We use a double buffering pooling technique because we know that we'll\n    // only ever need at most two versions of a tree. We pool the \"other\" unused\n    // node that we're free to reuse. This is lazily created to avoid allocating\n    // extra objects for things that are never updated. It also allow us to\n    // reclaim the extra memory if needed.\n    workInProgress = createFiber(current.tag, pendingProps, current.key, current.mode);\n    workInProgress.elementType = current.elementType;\n    workInProgress.type = current.type;\n    workInProgress.stateNode = current.stateNode;\n\n    {\n      // DEV-only fields\n      workInProgress._debugID = current._debugID;\n      workInProgress._debugSource = current._debugSource;\n      workInProgress._debugOwner = current._debugOwner;\n      workInProgress._debugHookTypes = current._debugHookTypes;\n    }\n\n    workInProgress.alternate = current;\n    current.alternate = workInProgress;\n  } else {\n    workInProgress.pendingProps = pendingProps;\n\n    // We already have an alternate.\n    // Reset the effect tag.\n    workInProgress.effectTag = NoEffect;\n\n    // The effect list is no longer valid.\n    workInProgress.nextEffect = null;\n    workInProgress.firstEffect = null;\n    workInProgress.lastEffect = null;\n\n    if (enableProfilerTimer) {\n      // We intentionally reset, rather than copy, actualDuration & actualStartTime.\n      // This prevents time from endlessly accumulating in new commits.\n      // This has the downside of resetting values for different priority renders,\n      // But works for yielding (the common case) and should support resuming.\n      workInProgress.actualDuration = 0;\n      workInProgress.actualStartTime = -1;\n    }\n  }\n\n  workInProgress.childExpirationTime = current.childExpirationTime;\n  workInProgress.expirationTime = current.expirationTime;\n\n  workInProgress.child = current.child;\n  workInProgress.memoizedProps = current.memoizedProps;\n  workInProgress.memoizedState = current.memoizedState;\n  workInProgress.updateQueue = current.updateQueue;\n  workInProgress.contextDependencies = current.contextDependencies;\n\n  // These will be overridden during the parent's reconciliation\n  workInProgress.sibling = current.sibling;\n  workInProgress.index = current.index;\n  workInProgress.ref = current.ref;\n\n  if (enableProfilerTimer) {\n    workInProgress.selfBaseDuration = current.selfBaseDuration;\n    workInProgress.treeBaseDuration = current.treeBaseDuration;\n  }\n\n  return workInProgress;\n}\n\nfunction createHostRootFiber(isConcurrent) {\n  var mode = isConcurrent ? ConcurrentMode | StrictMode : NoContext;\n\n  if (enableProfilerTimer && isDevToolsPresent) {\n    // Always collect profile timings when DevTools are present.\n    // This enables DevTools to start capturing timing at any point–\n    // Without some nodes in the tree having empty base times.\n    mode |= ProfileMode;\n  }\n\n  return createFiber(HostRoot, null, null, mode);\n}\n\nfunction createFiberFromTypeAndProps(type, // React$ElementType\nkey, pendingProps, owner, mode, expirationTime) {\n  var fiber = void 0;\n\n  var fiberTag = IndeterminateComponent;\n  // The resolved type is set if we know what the final type will be. I.e. it's not lazy.\n  var resolvedType = type;\n  if (typeof type === 'function') {\n    if (shouldConstruct(type)) {\n      fiberTag = ClassComponent;\n    }\n  } else if (typeof type === 'string') {\n    fiberTag = HostComponent;\n  } else {\n    getTag: switch (type) {\n      case REACT_FRAGMENT_TYPE:\n        return createFiberFromFragment(pendingProps.children, mode, expirationTime, key);\n      case REACT_CONCURRENT_MODE_TYPE:\n        return createFiberFromMode(pendingProps, mode | ConcurrentMode | StrictMode, expirationTime, key);\n      case REACT_STRICT_MODE_TYPE:\n        return createFiberFromMode(pendingProps, mode | StrictMode, expirationTime, key);\n      case REACT_PROFILER_TYPE:\n        return createFiberFromProfiler(pendingProps, mode, expirationTime, key);\n      case REACT_SUSPENSE_TYPE:\n        return createFiberFromSuspense(pendingProps, mode, expirationTime, key);\n      default:\n        {\n          if (typeof type === 'object' && type !== null) {\n            switch (type.$$typeof) {\n              case REACT_PROVIDER_TYPE:\n                fiberTag = ContextProvider;\n                break getTag;\n              case REACT_CONTEXT_TYPE:\n                // This is a consumer\n                fiberTag = ContextConsumer;\n                break getTag;\n              case REACT_FORWARD_REF_TYPE:\n                fiberTag = ForwardRef;\n                break getTag;\n              case REACT_MEMO_TYPE:\n                fiberTag = MemoComponent;\n                break getTag;\n              case REACT_LAZY_TYPE:\n                fiberTag = LazyComponent;\n                resolvedType = null;\n                break getTag;\n            }\n          }\n          var info = '';\n          {\n            if (type === undefined || typeof type === 'object' && type !== null && Object.keys(type).length === 0) {\n              info += ' You likely forgot to export your component from the file ' + \"it's defined in, or you might have mixed up default and \" + 'named imports.';\n            }\n            var ownerName = owner ? getComponentName(owner.type) : null;\n            if (ownerName) {\n              info += '\\n\\nCheck the render method of `' + ownerName + '`.';\n            }\n          }\n          invariant(false, 'Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: %s.%s', type == null ? type : typeof type, info);\n        }\n    }\n  }\n\n  fiber = createFiber(fiberTag, pendingProps, key, mode);\n  fiber.elementType = type;\n  fiber.type = resolvedType;\n  fiber.expirationTime = expirationTime;\n\n  return fiber;\n}\n\nfunction createFiberFromElement(element, mode, expirationTime) {\n  var owner = null;\n  {\n    owner = element._owner;\n  }\n  var type = element.type;\n  var key = element.key;\n  var pendingProps = element.props;\n  var fiber = createFiberFromTypeAndProps(type, key, pendingProps, owner, mode, expirationTime);\n  {\n    fiber._debugSource = element._source;\n    fiber._debugOwner = element._owner;\n  }\n  return fiber;\n}\n\nfunction createFiberFromFragment(elements, mode, expirationTime, key) {\n  var fiber = createFiber(Fragment, elements, key, mode);\n  fiber.expirationTime = expirationTime;\n  return fiber;\n}\n\nfunction createFiberFromProfiler(pendingProps, mode, expirationTime, key) {\n  {\n    if (typeof pendingProps.id !== 'string' || typeof pendingProps.onRender !== 'function') {\n      warningWithoutStack$1(false, 'Profiler must specify an \"id\" string and \"onRender\" function as props');\n    }\n  }\n\n  var fiber = createFiber(Profiler, pendingProps, key, mode | ProfileMode);\n  // TODO: The Profiler fiber shouldn't have a type. It has a tag.\n  fiber.elementType = REACT_PROFILER_TYPE;\n  fiber.type = REACT_PROFILER_TYPE;\n  fiber.expirationTime = expirationTime;\n\n  return fiber;\n}\n\nfunction createFiberFromMode(pendingProps, mode, expirationTime, key) {\n  var fiber = createFiber(Mode, pendingProps, key, mode);\n\n  // TODO: The Mode fiber shouldn't have a type. It has a tag.\n  var type = (mode & ConcurrentMode) === NoContext ? REACT_STRICT_MODE_TYPE : REACT_CONCURRENT_MODE_TYPE;\n  fiber.elementType = type;\n  fiber.type = type;\n\n  fiber.expirationTime = expirationTime;\n  return fiber;\n}\n\nfunction createFiberFromSuspense(pendingProps, mode, expirationTime, key) {\n  var fiber = createFiber(SuspenseComponent, pendingProps, key, mode);\n\n  // TODO: The SuspenseComponent fiber shouldn't have a type. It has a tag.\n  var type = REACT_SUSPENSE_TYPE;\n  fiber.elementType = type;\n  fiber.type = type;\n\n  fiber.expirationTime = expirationTime;\n  return fiber;\n}\n\nfunction createFiberFromText(content, mode, expirationTime) {\n  var fiber = createFiber(HostText, content, null, mode);\n  fiber.expirationTime = expirationTime;\n  return fiber;\n}\n\nfunction createFiberFromHostInstanceForDeletion() {\n  var fiber = createFiber(HostComponent, null, null, NoContext);\n  // TODO: These should not need a type.\n  fiber.elementType = 'DELETED';\n  fiber.type = 'DELETED';\n  return fiber;\n}\n\nfunction createFiberFromPortal(portal, mode, expirationTime) {\n  var pendingProps = portal.children !== null ? portal.children : [];\n  var fiber = createFiber(HostPortal, pendingProps, portal.key, mode);\n  fiber.expirationTime = expirationTime;\n  fiber.stateNode = {\n    containerInfo: portal.containerInfo,\n    pendingChildren: null, // Used by persistent updates\n    implementation: portal.implementation\n  };\n  return fiber;\n}\n\n// Used for stashing WIP properties to replay failed work in DEV.\nfunction assignFiberPropertiesInDEV(target, source) {\n  if (target === null) {\n    // This Fiber's initial properties will always be overwritten.\n    // We only use a Fiber to ensure the same hidden class so DEV isn't slow.\n    target = createFiber(IndeterminateComponent, null, null, NoContext);\n  }\n\n  // This is intentionally written as a list of all properties.\n  // We tried to use Object.assign() instead but this is called in\n  // the hottest path, and Object.assign() was too slow:\n  // https://github.com/facebook/react/issues/12502\n  // This code is DEV-only so size is not a concern.\n\n  target.tag = source.tag;\n  target.key = source.key;\n  target.elementType = source.elementType;\n  target.type = source.type;\n  target.stateNode = source.stateNode;\n  target.return = source.return;\n  target.child = source.child;\n  target.sibling = source.sibling;\n  target.index = source.index;\n  target.ref = source.ref;\n  target.pendingProps = source.pendingProps;\n  target.memoizedProps = source.memoizedProps;\n  target.updateQueue = source.updateQueue;\n  target.memoizedState = source.memoizedState;\n  target.contextDependencies = source.contextDependencies;\n  target.mode = source.mode;\n  target.effectTag = source.effectTag;\n  target.nextEffect = source.nextEffect;\n  target.firstEffect = source.firstEffect;\n  target.lastEffect = source.lastEffect;\n  target.expirationTime = source.expirationTime;\n  target.childExpirationTime = source.childExpirationTime;\n  target.alternate = source.alternate;\n  if (enableProfilerTimer) {\n    target.actualDuration = source.actualDuration;\n    target.actualStartTime = source.actualStartTime;\n    target.selfBaseDuration = source.selfBaseDuration;\n    target.treeBaseDuration = source.treeBaseDuration;\n  }\n  target._debugID = source._debugID;\n  target._debugSource = source._debugSource;\n  target._debugOwner = source._debugOwner;\n  target._debugIsCurrentlyTiming = source._debugIsCurrentlyTiming;\n  target._debugHookTypes = source._debugHookTypes;\n  return target;\n}\n\nvar ReactInternals$2 = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;\n\nvar _ReactInternals$Sched$1 = ReactInternals$2.SchedulerTracing;\nvar __interactionsRef = _ReactInternals$Sched$1.__interactionsRef;\nvar __subscriberRef = _ReactInternals$Sched$1.__subscriberRef;\nvar unstable_clear = _ReactInternals$Sched$1.unstable_clear;\nvar unstable_getCurrent = _ReactInternals$Sched$1.unstable_getCurrent;\nvar unstable_getThreadID = _ReactInternals$Sched$1.unstable_getThreadID;\nvar unstable_subscribe = _ReactInternals$Sched$1.unstable_subscribe;\nvar unstable_trace = _ReactInternals$Sched$1.unstable_trace;\nvar unstable_unsubscribe = _ReactInternals$Sched$1.unstable_unsubscribe;\nvar unstable_wrap = _ReactInternals$Sched$1.unstable_wrap;\n\n// TODO: This should be lifted into the renderer.\n\n\n// The following attributes are only used by interaction tracing builds.\n// They enable interactions to be associated with their async work,\n// And expose interaction metadata to the React DevTools Profiler plugin.\n// Note that these attributes are only defined when the enableSchedulerTracing flag is enabled.\n\n\n// Exported FiberRoot type includes all properties,\n// To avoid requiring potentially error-prone :any casts throughout the project.\n// Profiling properties are only safe to access in profiling builds (when enableSchedulerTracing is true).\n// The types are defined separately within this file to ensure they stay in sync.\n// (We don't have to use an inline :any cast when enableSchedulerTracing is disabled.)\n\n\nfunction createFiberRoot(containerInfo, isConcurrent, hydrate) {\n  // Cyclic construction. This cheats the type system right now because\n  // stateNode is any.\n  var uninitializedFiber = createHostRootFiber(isConcurrent);\n\n  var root = void 0;\n  if (enableSchedulerTracing) {\n    root = {\n      current: uninitializedFiber,\n      containerInfo: containerInfo,\n      pendingChildren: null,\n\n      earliestPendingTime: NoWork,\n      latestPendingTime: NoWork,\n      earliestSuspendedTime: NoWork,\n      latestSuspendedTime: NoWork,\n      latestPingedTime: NoWork,\n\n      pingCache: null,\n\n      didError: false,\n\n      pendingCommitExpirationTime: NoWork,\n      finishedWork: null,\n      timeoutHandle: noTimeout,\n      context: null,\n      pendingContext: null,\n      hydrate: hydrate,\n      nextExpirationTimeToWorkOn: NoWork,\n      expirationTime: NoWork,\n      firstBatch: null,\n      nextScheduledRoot: null,\n\n      interactionThreadID: unstable_getThreadID(),\n      memoizedInteractions: new Set(),\n      pendingInteractionMap: new Map()\n    };\n  } else {\n    root = {\n      current: uninitializedFiber,\n      containerInfo: containerInfo,\n      pendingChildren: null,\n\n      pingCache: null,\n\n      earliestPendingTime: NoWork,\n      latestPendingTime: NoWork,\n      earliestSuspendedTime: NoWork,\n      latestSuspendedTime: NoWork,\n      latestPingedTime: NoWork,\n\n      didError: false,\n\n      pendingCommitExpirationTime: NoWork,\n      finishedWork: null,\n      timeoutHandle: noTimeout,\n      context: null,\n      pendingContext: null,\n      hydrate: hydrate,\n      nextExpirationTimeToWorkOn: NoWork,\n      expirationTime: NoWork,\n      firstBatch: null,\n      nextScheduledRoot: null\n    };\n  }\n\n  uninitializedFiber.stateNode = root;\n\n  // The reason for the way the Flow types are structured in this file,\n  // Is to avoid needing :any casts everywhere interaction tracing fields are used.\n  // Unfortunately that requires an :any cast for non-interaction tracing capable builds.\n  // $FlowFixMe Remove this :any cast and replace it with something better.\n  return root;\n}\n\n/**\n * Forked from fbjs/warning:\n * https://github.com/facebook/fbjs/blob/e66ba20ad5be433eb54423f2b097d829324d9de6/packages/fbjs/src/__forks__/warning.js\n *\n * Only change is we use console.warn instead of console.error,\n * and do nothing when 'console' is not supported.\n * This really simplifies the code.\n * ---\n * Similar to invariant but only logs a warning if the condition is not met.\n * This can be used to log issues in development environments in critical\n * paths. Removing the logging code for production environments will keep the\n * same logic and follow the same code paths.\n */\n\nvar lowPriorityWarning = function () {};\n\n{\n  var printWarning$1 = function (format) {\n    for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {\n      args[_key - 1] = arguments[_key];\n    }\n\n    var argIndex = 0;\n    var message = 'Warning: ' + format.replace(/%s/g, function () {\n      return args[argIndex++];\n    });\n    if (typeof console !== 'undefined') {\n      console.warn(message);\n    }\n    try {\n      // --- Welcome to debugging React ---\n      // This error was thrown as a convenience so that you can use this stack\n      // to find the callsite that caused this warning to fire.\n      throw new Error(message);\n    } catch (x) {}\n  };\n\n  lowPriorityWarning = function (condition, format) {\n    if (format === undefined) {\n      throw new Error('`lowPriorityWarning(condition, format, ...args)` requires a warning ' + 'message argument');\n    }\n    if (!condition) {\n      for (var _len2 = arguments.length, args = Array(_len2 > 2 ? _len2 - 2 : 0), _key2 = 2; _key2 < _len2; _key2++) {\n        args[_key2 - 2] = arguments[_key2];\n      }\n\n      printWarning$1.apply(undefined, [format].concat(args));\n    }\n  };\n}\n\nvar lowPriorityWarning$1 = lowPriorityWarning;\n\nvar ReactStrictModeWarnings = {\n  discardPendingWarnings: function () {},\n  flushPendingDeprecationWarnings: function () {},\n  flushPendingUnsafeLifecycleWarnings: function () {},\n  recordDeprecationWarnings: function (fiber, instance) {},\n  recordUnsafeLifecycleWarnings: function (fiber, instance) {},\n  recordLegacyContextWarning: function (fiber, instance) {},\n  flushLegacyContextWarning: function () {}\n};\n\n{\n  var LIFECYCLE_SUGGESTIONS = {\n    UNSAFE_componentWillMount: 'componentDidMount',\n    UNSAFE_componentWillReceiveProps: 'static getDerivedStateFromProps',\n    UNSAFE_componentWillUpdate: 'componentDidUpdate'\n  };\n\n  var pendingComponentWillMountWarnings = [];\n  var pendingComponentWillReceivePropsWarnings = [];\n  var pendingComponentWillUpdateWarnings = [];\n  var pendingUnsafeLifecycleWarnings = new Map();\n  var pendingLegacyContextWarning = new Map();\n\n  // Tracks components we have already warned about.\n  var didWarnAboutDeprecatedLifecycles = new Set();\n  var didWarnAboutUnsafeLifecycles = new Set();\n  var didWarnAboutLegacyContext = new Set();\n\n  var setToSortedString = function (set) {\n    var array = [];\n    set.forEach(function (value) {\n      array.push(value);\n    });\n    return array.sort().join(', ');\n  };\n\n  ReactStrictModeWarnings.discardPendingWarnings = function () {\n    pendingComponentWillMountWarnings = [];\n    pendingComponentWillReceivePropsWarnings = [];\n    pendingComponentWillUpdateWarnings = [];\n    pendingUnsafeLifecycleWarnings = new Map();\n    pendingLegacyContextWarning = new Map();\n  };\n\n  ReactStrictModeWarnings.flushPendingUnsafeLifecycleWarnings = function () {\n    pendingUnsafeLifecycleWarnings.forEach(function (lifecycleWarningsMap, strictRoot) {\n      var lifecyclesWarningMessages = [];\n\n      Object.keys(lifecycleWarningsMap).forEach(function (lifecycle) {\n        var lifecycleWarnings = lifecycleWarningsMap[lifecycle];\n        if (lifecycleWarnings.length > 0) {\n          var componentNames = new Set();\n          lifecycleWarnings.forEach(function (fiber) {\n            componentNames.add(getComponentName(fiber.type) || 'Component');\n            didWarnAboutUnsafeLifecycles.add(fiber.type);\n          });\n\n          var formatted = lifecycle.replace('UNSAFE_', '');\n          var suggestion = LIFECYCLE_SUGGESTIONS[lifecycle];\n          var sortedComponentNames = setToSortedString(componentNames);\n\n          lifecyclesWarningMessages.push(formatted + ': Please update the following components to use ' + (suggestion + ' instead: ' + sortedComponentNames));\n        }\n      });\n\n      if (lifecyclesWarningMessages.length > 0) {\n        var strictRootComponentStack = getStackByFiberInDevAndProd(strictRoot);\n\n        warningWithoutStack$1(false, 'Unsafe lifecycle methods were found within a strict-mode tree:%s' + '\\n\\n%s' + '\\n\\nLearn more about this warning here:' + '\\nhttps://fb.me/react-strict-mode-warnings', strictRootComponentStack, lifecyclesWarningMessages.join('\\n\\n'));\n      }\n    });\n\n    pendingUnsafeLifecycleWarnings = new Map();\n  };\n\n  var findStrictRoot = function (fiber) {\n    var maybeStrictRoot = null;\n\n    var node = fiber;\n    while (node !== null) {\n      if (node.mode & StrictMode) {\n        maybeStrictRoot = node;\n      }\n      node = node.return;\n    }\n\n    return maybeStrictRoot;\n  };\n\n  ReactStrictModeWarnings.flushPendingDeprecationWarnings = function () {\n    if (pendingComponentWillMountWarnings.length > 0) {\n      var uniqueNames = new Set();\n      pendingComponentWillMountWarnings.forEach(function (fiber) {\n        uniqueNames.add(getComponentName(fiber.type) || 'Component');\n        didWarnAboutDeprecatedLifecycles.add(fiber.type);\n      });\n\n      var sortedNames = setToSortedString(uniqueNames);\n\n      lowPriorityWarning$1(false, 'componentWillMount is deprecated and will be removed in the next major version. ' + 'Use componentDidMount instead. As a temporary workaround, ' + 'you can rename to UNSAFE_componentWillMount.' + '\\n\\nPlease update the following components: %s' + '\\n\\nLearn more about this warning here:' + '\\nhttps://fb.me/react-async-component-lifecycle-hooks', sortedNames);\n\n      pendingComponentWillMountWarnings = [];\n    }\n\n    if (pendingComponentWillReceivePropsWarnings.length > 0) {\n      var _uniqueNames = new Set();\n      pendingComponentWillReceivePropsWarnings.forEach(function (fiber) {\n        _uniqueNames.add(getComponentName(fiber.type) || 'Component');\n        didWarnAboutDeprecatedLifecycles.add(fiber.type);\n      });\n\n      var _sortedNames = setToSortedString(_uniqueNames);\n\n      lowPriorityWarning$1(false, 'componentWillReceiveProps is deprecated and will be removed in the next major version. ' + 'Use static getDerivedStateFromProps instead.' + '\\n\\nPlease update the following components: %s' + '\\n\\nLearn more about this warning here:' + '\\nhttps://fb.me/react-async-component-lifecycle-hooks', _sortedNames);\n\n      pendingComponentWillReceivePropsWarnings = [];\n    }\n\n    if (pendingComponentWillUpdateWarnings.length > 0) {\n      var _uniqueNames2 = new Set();\n      pendingComponentWillUpdateWarnings.forEach(function (fiber) {\n        _uniqueNames2.add(getComponentName(fiber.type) || 'Component');\n        didWarnAboutDeprecatedLifecycles.add(fiber.type);\n      });\n\n      var _sortedNames2 = setToSortedString(_uniqueNames2);\n\n      lowPriorityWarning$1(false, 'componentWillUpdate is deprecated and will be removed in the next major version. ' + 'Use componentDidUpdate instead. As a temporary workaround, ' + 'you can rename to UNSAFE_componentWillUpdate.' + '\\n\\nPlease update the following components: %s' + '\\n\\nLearn more about this warning here:' + '\\nhttps://fb.me/react-async-component-lifecycle-hooks', _sortedNames2);\n\n      pendingComponentWillUpdateWarnings = [];\n    }\n  };\n\n  ReactStrictModeWarnings.recordDeprecationWarnings = function (fiber, instance) {\n    // Dedup strategy: Warn once per component.\n    if (didWarnAboutDeprecatedLifecycles.has(fiber.type)) {\n      return;\n    }\n\n    // Don't warn about react-lifecycles-compat polyfilled components.\n    if (typeof instance.componentWillMount === 'function' && instance.componentWillMount.__suppressDeprecationWarning !== true) {\n      pendingComponentWillMountWarnings.push(fiber);\n    }\n    if (typeof instance.componentWillReceiveProps === 'function' && instance.componentWillReceiveProps.__suppressDeprecationWarning !== true) {\n      pendingComponentWillReceivePropsWarnings.push(fiber);\n    }\n    if (typeof instance.componentWillUpdate === 'function' && instance.componentWillUpdate.__suppressDeprecationWarning !== true) {\n      pendingComponentWillUpdateWarnings.push(fiber);\n    }\n  };\n\n  ReactStrictModeWarnings.recordUnsafeLifecycleWarnings = function (fiber, instance) {\n    var strictRoot = findStrictRoot(fiber);\n    if (strictRoot === null) {\n      warningWithoutStack$1(false, 'Expected to find a StrictMode component in a strict mode tree. ' + 'This error is likely caused by a bug in React. Please file an issue.');\n      return;\n    }\n\n    // Dedup strategy: Warn once per component.\n    // This is difficult to track any other way since component names\n    // are often vague and are likely to collide between 3rd party libraries.\n    // An expand property is probably okay to use here since it's DEV-only,\n    // and will only be set in the event of serious warnings.\n    if (didWarnAboutUnsafeLifecycles.has(fiber.type)) {\n      return;\n    }\n\n    var warningsForRoot = void 0;\n    if (!pendingUnsafeLifecycleWarnings.has(strictRoot)) {\n      warningsForRoot = {\n        UNSAFE_componentWillMount: [],\n        UNSAFE_componentWillReceiveProps: [],\n        UNSAFE_componentWillUpdate: []\n      };\n\n      pendingUnsafeLifecycleWarnings.set(strictRoot, warningsForRoot);\n    } else {\n      warningsForRoot = pendingUnsafeLifecycleWarnings.get(strictRoot);\n    }\n\n    var unsafeLifecycles = [];\n    if (typeof instance.componentWillMount === 'function' && instance.componentWillMount.__suppressDeprecationWarning !== true || typeof instance.UNSAFE_componentWillMount === 'function') {\n      unsafeLifecycles.push('UNSAFE_componentWillMount');\n    }\n    if (typeof instance.componentWillReceiveProps === 'function' && instance.componentWillReceiveProps.__suppressDeprecationWarning !== true || typeof instance.UNSAFE_componentWillReceiveProps === 'function') {\n      unsafeLifecycles.push('UNSAFE_componentWillReceiveProps');\n    }\n    if (typeof instance.componentWillUpdate === 'function' && instance.componentWillUpdate.__suppressDeprecationWarning !== true || typeof instance.UNSAFE_componentWillUpdate === 'function') {\n      unsafeLifecycles.push('UNSAFE_componentWillUpdate');\n    }\n\n    if (unsafeLifecycles.length > 0) {\n      unsafeLifecycles.forEach(function (lifecycle) {\n        warningsForRoot[lifecycle].push(fiber);\n      });\n    }\n  };\n\n  ReactStrictModeWarnings.recordLegacyContextWarning = function (fiber, instance) {\n    var strictRoot = findStrictRoot(fiber);\n    if (strictRoot === null) {\n      warningWithoutStack$1(false, 'Expected to find a StrictMode component in a strict mode tree. ' + 'This error is likely caused by a bug in React. Please file an issue.');\n      return;\n    }\n\n    // Dedup strategy: Warn once per component.\n    if (didWarnAboutLegacyContext.has(fiber.type)) {\n      return;\n    }\n\n    var warningsForRoot = pendingLegacyContextWarning.get(strictRoot);\n\n    if (fiber.type.contextTypes != null || fiber.type.childContextTypes != null || instance !== null && typeof instance.getChildContext === 'function') {\n      if (warningsForRoot === undefined) {\n        warningsForRoot = [];\n        pendingLegacyContextWarning.set(strictRoot, warningsForRoot);\n      }\n      warningsForRoot.push(fiber);\n    }\n  };\n\n  ReactStrictModeWarnings.flushLegacyContextWarning = function () {\n    pendingLegacyContextWarning.forEach(function (fiberArray, strictRoot) {\n      var uniqueNames = new Set();\n      fiberArray.forEach(function (fiber) {\n        uniqueNames.add(getComponentName(fiber.type) || 'Component');\n        didWarnAboutLegacyContext.add(fiber.type);\n      });\n\n      var sortedNames = setToSortedString(uniqueNames);\n      var strictRootComponentStack = getStackByFiberInDevAndProd(strictRoot);\n\n      warningWithoutStack$1(false, 'Legacy context API has been detected within a strict-mode tree: %s' + '\\n\\nPlease update the following components: %s' + '\\n\\nLearn more about this warning here:' + '\\nhttps://fb.me/react-strict-mode-warnings', strictRootComponentStack, sortedNames);\n    });\n  };\n}\n\n// This lets us hook into Fiber to debug what it's doing.\n// See https://github.com/facebook/react/pull/8033.\n// This is not part of the public API, not even for React DevTools.\n// You may only inject a debugTool if you work on React Fiber itself.\nvar ReactFiberInstrumentation = {\n  debugTool: null\n};\n\nvar ReactFiberInstrumentation_1 = ReactFiberInstrumentation;\n\n// TODO: Offscreen updates should never suspend. However, a promise that\n// suspended inside an offscreen subtree should be able to ping at the priority\n// of the outer render.\n\nfunction markPendingPriorityLevel(root, expirationTime) {\n  // If there's a gap between completing a failed root and retrying it,\n  // additional updates may be scheduled. Clear `didError`, in case the update\n  // is sufficient to fix the error.\n  root.didError = false;\n\n  // Update the latest and earliest pending times\n  var earliestPendingTime = root.earliestPendingTime;\n  if (earliestPendingTime === NoWork) {\n    // No other pending updates.\n    root.earliestPendingTime = root.latestPendingTime = expirationTime;\n  } else {\n    if (earliestPendingTime < expirationTime) {\n      // This is the earliest pending update.\n      root.earliestPendingTime = expirationTime;\n    } else {\n      var latestPendingTime = root.latestPendingTime;\n      if (latestPendingTime > expirationTime) {\n        // This is the latest pending update\n        root.latestPendingTime = expirationTime;\n      }\n    }\n  }\n  findNextExpirationTimeToWorkOn(expirationTime, root);\n}\n\nfunction markCommittedPriorityLevels(root, earliestRemainingTime) {\n  root.didError = false;\n\n  if (earliestRemainingTime === NoWork) {\n    // Fast path. There's no remaining work. Clear everything.\n    root.earliestPendingTime = NoWork;\n    root.latestPendingTime = NoWork;\n    root.earliestSuspendedTime = NoWork;\n    root.latestSuspendedTime = NoWork;\n    root.latestPingedTime = NoWork;\n    findNextExpirationTimeToWorkOn(NoWork, root);\n    return;\n  }\n\n  if (earliestRemainingTime < root.latestPingedTime) {\n    root.latestPingedTime = NoWork;\n  }\n\n  // Let's see if the previous latest known pending level was just flushed.\n  var latestPendingTime = root.latestPendingTime;\n  if (latestPendingTime !== NoWork) {\n    if (latestPendingTime > earliestRemainingTime) {\n      // We've flushed all the known pending levels.\n      root.earliestPendingTime = root.latestPendingTime = NoWork;\n    } else {\n      var earliestPendingTime = root.earliestPendingTime;\n      if (earliestPendingTime > earliestRemainingTime) {\n        // We've flushed the earliest known pending level. Set this to the\n        // latest pending time.\n        root.earliestPendingTime = root.latestPendingTime;\n      }\n    }\n  }\n\n  // Now let's handle the earliest remaining level in the whole tree. We need to\n  // decide whether to treat it as a pending level or as suspended. Check\n  // it falls within the range of known suspended levels.\n\n  var earliestSuspendedTime = root.earliestSuspendedTime;\n  if (earliestSuspendedTime === NoWork) {\n    // There's no suspended work. Treat the earliest remaining level as a\n    // pending level.\n    markPendingPriorityLevel(root, earliestRemainingTime);\n    findNextExpirationTimeToWorkOn(NoWork, root);\n    return;\n  }\n\n  var latestSuspendedTime = root.latestSuspendedTime;\n  if (earliestRemainingTime < latestSuspendedTime) {\n    // The earliest remaining level is later than all the suspended work. That\n    // means we've flushed all the suspended work.\n    root.earliestSuspendedTime = NoWork;\n    root.latestSuspendedTime = NoWork;\n    root.latestPingedTime = NoWork;\n\n    // There's no suspended work. Treat the earliest remaining level as a\n    // pending level.\n    markPendingPriorityLevel(root, earliestRemainingTime);\n    findNextExpirationTimeToWorkOn(NoWork, root);\n    return;\n  }\n\n  if (earliestRemainingTime > earliestSuspendedTime) {\n    // The earliest remaining time is earlier than all the suspended work.\n    // Treat it as a pending update.\n    markPendingPriorityLevel(root, earliestRemainingTime);\n    findNextExpirationTimeToWorkOn(NoWork, root);\n    return;\n  }\n\n  // The earliest remaining time falls within the range of known suspended\n  // levels. We should treat this as suspended work.\n  findNextExpirationTimeToWorkOn(NoWork, root);\n}\n\nfunction hasLowerPriorityWork(root, erroredExpirationTime) {\n  var latestPendingTime = root.latestPendingTime;\n  var latestSuspendedTime = root.latestSuspendedTime;\n  var latestPingedTime = root.latestPingedTime;\n  return latestPendingTime !== NoWork && latestPendingTime < erroredExpirationTime || latestSuspendedTime !== NoWork && latestSuspendedTime < erroredExpirationTime || latestPingedTime !== NoWork && latestPingedTime < erroredExpirationTime;\n}\n\nfunction isPriorityLevelSuspended(root, expirationTime) {\n  var earliestSuspendedTime = root.earliestSuspendedTime;\n  var latestSuspendedTime = root.latestSuspendedTime;\n  return earliestSuspendedTime !== NoWork && expirationTime <= earliestSuspendedTime && expirationTime >= latestSuspendedTime;\n}\n\nfunction markSuspendedPriorityLevel(root, suspendedTime) {\n  root.didError = false;\n  clearPing(root, suspendedTime);\n\n  // First, check the known pending levels and update them if needed.\n  var earliestPendingTime = root.earliestPendingTime;\n  var latestPendingTime = root.latestPendingTime;\n  if (earliestPendingTime === suspendedTime) {\n    if (latestPendingTime === suspendedTime) {\n      // Both known pending levels were suspended. Clear them.\n      root.earliestPendingTime = root.latestPendingTime = NoWork;\n    } else {\n      // The earliest pending level was suspended. Clear by setting it to the\n      // latest pending level.\n      root.earliestPendingTime = latestPendingTime;\n    }\n  } else if (latestPendingTime === suspendedTime) {\n    // The latest pending level was suspended. Clear by setting it to the\n    // latest pending level.\n    root.latestPendingTime = earliestPendingTime;\n  }\n\n  // Finally, update the known suspended levels.\n  var earliestSuspendedTime = root.earliestSuspendedTime;\n  var latestSuspendedTime = root.latestSuspendedTime;\n  if (earliestSuspendedTime === NoWork) {\n    // No other suspended levels.\n    root.earliestSuspendedTime = root.latestSuspendedTime = suspendedTime;\n  } else {\n    if (earliestSuspendedTime < suspendedTime) {\n      // This is the earliest suspended level.\n      root.earliestSuspendedTime = suspendedTime;\n    } else if (latestSuspendedTime > suspendedTime) {\n      // This is the latest suspended level\n      root.latestSuspendedTime = suspendedTime;\n    }\n  }\n\n  findNextExpirationTimeToWorkOn(suspendedTime, root);\n}\n\nfunction markPingedPriorityLevel(root, pingedTime) {\n  root.didError = false;\n\n  // TODO: When we add back resuming, we need to ensure the progressed work\n  // is thrown out and not reused during the restarted render. One way to\n  // invalidate the progressed work is to restart at expirationTime + 1.\n  var latestPingedTime = root.latestPingedTime;\n  if (latestPingedTime === NoWork || latestPingedTime > pingedTime) {\n    root.latestPingedTime = pingedTime;\n  }\n  findNextExpirationTimeToWorkOn(pingedTime, root);\n}\n\nfunction clearPing(root, completedTime) {\n  var latestPingedTime = root.latestPingedTime;\n  if (latestPingedTime >= completedTime) {\n    root.latestPingedTime = NoWork;\n  }\n}\n\nfunction findEarliestOutstandingPriorityLevel(root, renderExpirationTime) {\n  var earliestExpirationTime = renderExpirationTime;\n\n  var earliestPendingTime = root.earliestPendingTime;\n  var earliestSuspendedTime = root.earliestSuspendedTime;\n  if (earliestPendingTime > earliestExpirationTime) {\n    earliestExpirationTime = earliestPendingTime;\n  }\n  if (earliestSuspendedTime > earliestExpirationTime) {\n    earliestExpirationTime = earliestSuspendedTime;\n  }\n  return earliestExpirationTime;\n}\n\nfunction didExpireAtExpirationTime(root, currentTime) {\n  var expirationTime = root.expirationTime;\n  if (expirationTime !== NoWork && currentTime <= expirationTime) {\n    // The root has expired. Flush all work up to the current time.\n    root.nextExpirationTimeToWorkOn = currentTime;\n  }\n}\n\nfunction findNextExpirationTimeToWorkOn(completedExpirationTime, root) {\n  var earliestSuspendedTime = root.earliestSuspendedTime;\n  var latestSuspendedTime = root.latestSuspendedTime;\n  var earliestPendingTime = root.earliestPendingTime;\n  var latestPingedTime = root.latestPingedTime;\n\n  // Work on the earliest pending time. Failing that, work on the latest\n  // pinged time.\n  var nextExpirationTimeToWorkOn = earliestPendingTime !== NoWork ? earliestPendingTime : latestPingedTime;\n\n  // If there is no pending or pinged work, check if there's suspended work\n  // that's lower priority than what we just completed.\n  if (nextExpirationTimeToWorkOn === NoWork && (completedExpirationTime === NoWork || latestSuspendedTime < completedExpirationTime)) {\n    // The lowest priority suspended work is the work most likely to be\n    // committed next. Let's start rendering it again, so that if it times out,\n    // it's ready to commit.\n    nextExpirationTimeToWorkOn = latestSuspendedTime;\n  }\n\n  var expirationTime = nextExpirationTimeToWorkOn;\n  if (expirationTime !== NoWork && earliestSuspendedTime > expirationTime) {\n    // Expire using the earliest known expiration time.\n    expirationTime = earliestSuspendedTime;\n  }\n\n  root.nextExpirationTimeToWorkOn = nextExpirationTimeToWorkOn;\n  root.expirationTime = expirationTime;\n}\n\nfunction resolveDefaultProps(Component, baseProps) {\n  if (Component && Component.defaultProps) {\n    // Resolve default props. Taken from ReactElement\n    var props = _assign({}, baseProps);\n    var defaultProps = Component.defaultProps;\n    for (var propName in defaultProps) {\n      if (props[propName] === undefined) {\n        props[propName] = defaultProps[propName];\n      }\n    }\n    return props;\n  }\n  return baseProps;\n}\n\nfunction readLazyComponentType(lazyComponent) {\n  var status = lazyComponent._status;\n  var result = lazyComponent._result;\n  switch (status) {\n    case Resolved:\n      {\n        var Component = result;\n        return Component;\n      }\n    case Rejected:\n      {\n        var error = result;\n        throw error;\n      }\n    case Pending:\n      {\n        var thenable = result;\n        throw thenable;\n      }\n    default:\n      {\n        lazyComponent._status = Pending;\n        var ctor = lazyComponent._ctor;\n        var _thenable = ctor();\n        _thenable.then(function (moduleObject) {\n          if (lazyComponent._status === Pending) {\n            var defaultExport = moduleObject.default;\n            {\n              if (defaultExport === undefined) {\n                warning$1(false, 'lazy: Expected the result of a dynamic import() call. ' + 'Instead received: %s\\n\\nYour code should look like: \\n  ' + \"const MyComponent = lazy(() => import('./MyComponent'))\", moduleObject);\n              }\n            }\n            lazyComponent._status = Resolved;\n            lazyComponent._result = defaultExport;\n          }\n        }, function (error) {\n          if (lazyComponent._status === Pending) {\n            lazyComponent._status = Rejected;\n            lazyComponent._result = error;\n          }\n        });\n        // Handle synchronous thenables.\n        switch (lazyComponent._status) {\n          case Resolved:\n            return lazyComponent._result;\n          case Rejected:\n            throw lazyComponent._result;\n        }\n        lazyComponent._result = _thenable;\n        throw _thenable;\n      }\n  }\n}\n\nvar fakeInternalInstance = {};\nvar isArray$1 = Array.isArray;\n\n// React.Component uses a shared frozen object by default.\n// We'll use it to determine whether we need to initialize legacy refs.\nvar emptyRefsObject = new React.Component().refs;\n\nvar didWarnAboutStateAssignmentForComponent = void 0;\nvar didWarnAboutUninitializedState = void 0;\nvar didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate = void 0;\nvar didWarnAboutLegacyLifecyclesAndDerivedState = void 0;\nvar didWarnAboutUndefinedDerivedState = void 0;\nvar warnOnUndefinedDerivedState = void 0;\nvar warnOnInvalidCallback$1 = void 0;\nvar didWarnAboutDirectlyAssigningPropsToState = void 0;\nvar didWarnAboutContextTypeAndContextTypes = void 0;\nvar didWarnAboutInvalidateContextType = void 0;\n\n{\n  didWarnAboutStateAssignmentForComponent = new Set();\n  didWarnAboutUninitializedState = new Set();\n  didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate = new Set();\n  didWarnAboutLegacyLifecyclesAndDerivedState = new Set();\n  didWarnAboutDirectlyAssigningPropsToState = new Set();\n  didWarnAboutUndefinedDerivedState = new Set();\n  didWarnAboutContextTypeAndContextTypes = new Set();\n  didWarnAboutInvalidateContextType = new Set();\n\n  var didWarnOnInvalidCallback = new Set();\n\n  warnOnInvalidCallback$1 = function (callback, callerName) {\n    if (callback === null || typeof callback === 'function') {\n      return;\n    }\n    var key = callerName + '_' + callback;\n    if (!didWarnOnInvalidCallback.has(key)) {\n      didWarnOnInvalidCallback.add(key);\n      warningWithoutStack$1(false, '%s(...): Expected the last optional `callback` argument to be a ' + 'function. Instead received: %s.', callerName, callback);\n    }\n  };\n\n  warnOnUndefinedDerivedState = function (type, partialState) {\n    if (partialState === undefined) {\n      var componentName = getComponentName(type) || 'Component';\n      if (!didWarnAboutUndefinedDerivedState.has(componentName)) {\n        didWarnAboutUndefinedDerivedState.add(componentName);\n        warningWithoutStack$1(false, '%s.getDerivedStateFromProps(): A valid state object (or null) must be returned. ' + 'You have returned undefined.', componentName);\n      }\n    }\n  };\n\n  // This is so gross but it's at least non-critical and can be removed if\n  // it causes problems. This is meant to give a nicer error message for\n  // ReactDOM15.unstable_renderSubtreeIntoContainer(reactDOM16Component,\n  // ...)) which otherwise throws a \"_processChildContext is not a function\"\n  // exception.\n  Object.defineProperty(fakeInternalInstance, '_processChildContext', {\n    enumerable: false,\n    value: function () {\n      invariant(false, '_processChildContext is not available in React 16+. This likely means you have multiple copies of React and are attempting to nest a React 15 tree inside a React 16 tree using unstable_renderSubtreeIntoContainer, which isn\\'t supported. Try to make sure you have only one copy of React (and ideally, switch to ReactDOM.createPortal).');\n    }\n  });\n  Object.freeze(fakeInternalInstance);\n}\n\nfunction applyDerivedStateFromProps(workInProgress, ctor, getDerivedStateFromProps, nextProps) {\n  var prevState = workInProgress.memoizedState;\n\n  {\n    if (debugRenderPhaseSideEffects || debugRenderPhaseSideEffectsForStrictMode && workInProgress.mode & StrictMode) {\n      // Invoke the function an extra time to help detect side-effects.\n      getDerivedStateFromProps(nextProps, prevState);\n    }\n  }\n\n  var partialState = getDerivedStateFromProps(nextProps, prevState);\n\n  {\n    warnOnUndefinedDerivedState(ctor, partialState);\n  }\n  // Merge the partial state and the previous state.\n  var memoizedState = partialState === null || partialState === undefined ? prevState : _assign({}, prevState, partialState);\n  workInProgress.memoizedState = memoizedState;\n\n  // Once the update queue is empty, persist the derived state onto the\n  // base state.\n  var updateQueue = workInProgress.updateQueue;\n  if (updateQueue !== null && workInProgress.expirationTime === NoWork) {\n    updateQueue.baseState = memoizedState;\n  }\n}\n\nvar classComponentUpdater = {\n  isMounted: isMounted,\n  enqueueSetState: function (inst, payload, callback) {\n    var fiber = get(inst);\n    var currentTime = requestCurrentTime();\n    var expirationTime = computeExpirationForFiber(currentTime, fiber);\n\n    var update = createUpdate(expirationTime);\n    update.payload = payload;\n    if (callback !== undefined && callback !== null) {\n      {\n        warnOnInvalidCallback$1(callback, 'setState');\n      }\n      update.callback = callback;\n    }\n\n    flushPassiveEffects();\n    enqueueUpdate(fiber, update);\n    scheduleWork(fiber, expirationTime);\n  },\n  enqueueReplaceState: function (inst, payload, callback) {\n    var fiber = get(inst);\n    var currentTime = requestCurrentTime();\n    var expirationTime = computeExpirationForFiber(currentTime, fiber);\n\n    var update = createUpdate(expirationTime);\n    update.tag = ReplaceState;\n    update.payload = payload;\n\n    if (callback !== undefined && callback !== null) {\n      {\n        warnOnInvalidCallback$1(callback, 'replaceState');\n      }\n      update.callback = callback;\n    }\n\n    flushPassiveEffects();\n    enqueueUpdate(fiber, update);\n    scheduleWork(fiber, expirationTime);\n  },\n  enqueueForceUpdate: function (inst, callback) {\n    var fiber = get(inst);\n    var currentTime = requestCurrentTime();\n    var expirationTime = computeExpirationForFiber(currentTime, fiber);\n\n    var update = createUpdate(expirationTime);\n    update.tag = ForceUpdate;\n\n    if (callback !== undefined && callback !== null) {\n      {\n        warnOnInvalidCallback$1(callback, 'forceUpdate');\n      }\n      update.callback = callback;\n    }\n\n    flushPassiveEffects();\n    enqueueUpdate(fiber, update);\n    scheduleWork(fiber, expirationTime);\n  }\n};\n\nfunction checkShouldComponentUpdate(workInProgress, ctor, oldProps, newProps, oldState, newState, nextContext) {\n  var instance = workInProgress.stateNode;\n  if (typeof instance.shouldComponentUpdate === 'function') {\n    startPhaseTimer(workInProgress, 'shouldComponentUpdate');\n    var shouldUpdate = instance.shouldComponentUpdate(newProps, newState, nextContext);\n    stopPhaseTimer();\n\n    {\n      !(shouldUpdate !== undefined) ? warningWithoutStack$1(false, '%s.shouldComponentUpdate(): Returned undefined instead of a ' + 'boolean value. Make sure to return true or false.', getComponentName(ctor) || 'Component') : void 0;\n    }\n\n    return shouldUpdate;\n  }\n\n  if (ctor.prototype && ctor.prototype.isPureReactComponent) {\n    return !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState);\n  }\n\n  return true;\n}\n\nfunction checkClassInstance(workInProgress, ctor, newProps) {\n  var instance = workInProgress.stateNode;\n  {\n    var name = getComponentName(ctor) || 'Component';\n    var renderPresent = instance.render;\n\n    if (!renderPresent) {\n      if (ctor.prototype && typeof ctor.prototype.render === 'function') {\n        warningWithoutStack$1(false, '%s(...): No `render` method found on the returned component ' + 'instance: did you accidentally return an object from the constructor?', name);\n      } else {\n        warningWithoutStack$1(false, '%s(...): No `render` method found on the returned component ' + 'instance: you may have forgotten to define `render`.', name);\n      }\n    }\n\n    var noGetInitialStateOnES6 = !instance.getInitialState || instance.getInitialState.isReactClassApproved || instance.state;\n    !noGetInitialStateOnES6 ? warningWithoutStack$1(false, 'getInitialState was defined on %s, a plain JavaScript class. ' + 'This is only supported for classes created using React.createClass. ' + 'Did you mean to define a state property instead?', name) : void 0;\n    var noGetDefaultPropsOnES6 = !instance.getDefaultProps || instance.getDefaultProps.isReactClassApproved;\n    !noGetDefaultPropsOnES6 ? warningWithoutStack$1(false, 'getDefaultProps was defined on %s, a plain JavaScript class. ' + 'This is only supported for classes created using React.createClass. ' + 'Use a static property to define defaultProps instead.', name) : void 0;\n    var noInstancePropTypes = !instance.propTypes;\n    !noInstancePropTypes ? warningWithoutStack$1(false, 'propTypes was defined as an instance property on %s. Use a static ' + 'property to define propTypes instead.', name) : void 0;\n    var noInstanceContextType = !instance.contextType;\n    !noInstanceContextType ? warningWithoutStack$1(false, 'contextType was defined as an instance property on %s. Use a static ' + 'property to define contextType instead.', name) : void 0;\n    var noInstanceContextTypes = !instance.contextTypes;\n    !noInstanceContextTypes ? warningWithoutStack$1(false, 'contextTypes was defined as an instance property on %s. Use a static ' + 'property to define contextTypes instead.', name) : void 0;\n\n    if (ctor.contextType && ctor.contextTypes && !didWarnAboutContextTypeAndContextTypes.has(ctor)) {\n      didWarnAboutContextTypeAndContextTypes.add(ctor);\n      warningWithoutStack$1(false, '%s declares both contextTypes and contextType static properties. ' + 'The legacy contextTypes property will be ignored.', name);\n    }\n\n    var noComponentShouldUpdate = typeof instance.componentShouldUpdate !== 'function';\n    !noComponentShouldUpdate ? warningWithoutStack$1(false, '%s has a method called ' + 'componentShouldUpdate(). Did you mean shouldComponentUpdate()? ' + 'The name is phrased as a question because the function is ' + 'expected to return a value.', name) : void 0;\n    if (ctor.prototype && ctor.prototype.isPureReactComponent && typeof instance.shouldComponentUpdate !== 'undefined') {\n      warningWithoutStack$1(false, '%s has a method called shouldComponentUpdate(). ' + 'shouldComponentUpdate should not be used when extending React.PureComponent. ' + 'Please extend React.Component if shouldComponentUpdate is used.', getComponentName(ctor) || 'A pure component');\n    }\n    var noComponentDidUnmount = typeof instance.componentDidUnmount !== 'function';\n    !noComponentDidUnmount ? warningWithoutStack$1(false, '%s has a method called ' + 'componentDidUnmount(). But there is no such lifecycle method. ' + 'Did you mean componentWillUnmount()?', name) : void 0;\n    var noComponentDidReceiveProps = typeof instance.componentDidReceiveProps !== 'function';\n    !noComponentDidReceiveProps ? warningWithoutStack$1(false, '%s has a method called ' + 'componentDidReceiveProps(). But there is no such lifecycle method. ' + 'If you meant to update the state in response to changing props, ' + 'use componentWillReceiveProps(). If you meant to fetch data or ' + 'run side-effects or mutations after React has updated the UI, use componentDidUpdate().', name) : void 0;\n    var noComponentWillRecieveProps = typeof instance.componentWillRecieveProps !== 'function';\n    !noComponentWillRecieveProps ? warningWithoutStack$1(false, '%s has a method called ' + 'componentWillRecieveProps(). Did you mean componentWillReceiveProps()?', name) : void 0;\n    var noUnsafeComponentWillRecieveProps = typeof instance.UNSAFE_componentWillRecieveProps !== 'function';\n    !noUnsafeComponentWillRecieveProps ? warningWithoutStack$1(false, '%s has a method called ' + 'UNSAFE_componentWillRecieveProps(). Did you mean UNSAFE_componentWillReceiveProps()?', name) : void 0;\n    var hasMutatedProps = instance.props !== newProps;\n    !(instance.props === undefined || !hasMutatedProps) ? warningWithoutStack$1(false, '%s(...): When calling super() in `%s`, make sure to pass ' + \"up the same props that your component's constructor was passed.\", name, name) : void 0;\n    var noInstanceDefaultProps = !instance.defaultProps;\n    !noInstanceDefaultProps ? warningWithoutStack$1(false, 'Setting defaultProps as an instance property on %s is not supported and will be ignored.' + ' Instead, define defaultProps as a static property on %s.', name, name) : void 0;\n\n    if (typeof instance.getSnapshotBeforeUpdate === 'function' && typeof instance.componentDidUpdate !== 'function' && !didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate.has(ctor)) {\n      didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate.add(ctor);\n      warningWithoutStack$1(false, '%s: getSnapshotBeforeUpdate() should be used with componentDidUpdate(). ' + 'This component defines getSnapshotBeforeUpdate() only.', getComponentName(ctor));\n    }\n\n    var noInstanceGetDerivedStateFromProps = typeof instance.getDerivedStateFromProps !== 'function';\n    !noInstanceGetDerivedStateFromProps ? warningWithoutStack$1(false, '%s: getDerivedStateFromProps() is defined as an instance method ' + 'and will be ignored. Instead, declare it as a static method.', name) : void 0;\n    var noInstanceGetDerivedStateFromCatch = typeof instance.getDerivedStateFromError !== 'function';\n    !noInstanceGetDerivedStateFromCatch ? warningWithoutStack$1(false, '%s: getDerivedStateFromError() is defined as an instance method ' + 'and will be ignored. Instead, declare it as a static method.', name) : void 0;\n    var noStaticGetSnapshotBeforeUpdate = typeof ctor.getSnapshotBeforeUpdate !== 'function';\n    !noStaticGetSnapshotBeforeUpdate ? warningWithoutStack$1(false, '%s: getSnapshotBeforeUpdate() is defined as a static method ' + 'and will be ignored. Instead, declare it as an instance method.', name) : void 0;\n    var _state = instance.state;\n    if (_state && (typeof _state !== 'object' || isArray$1(_state))) {\n      warningWithoutStack$1(false, '%s.state: must be set to an object or null', name);\n    }\n    if (typeof instance.getChildContext === 'function') {\n      !(typeof ctor.childContextTypes === 'object') ? warningWithoutStack$1(false, '%s.getChildContext(): childContextTypes must be defined in order to ' + 'use getChildContext().', name) : void 0;\n    }\n  }\n}\n\nfunction adoptClassInstance(workInProgress, instance) {\n  instance.updater = classComponentUpdater;\n  workInProgress.stateNode = instance;\n  // The instance needs access to the fiber so that it can schedule updates\n  set(instance, workInProgress);\n  {\n    instance._reactInternalInstance = fakeInternalInstance;\n  }\n}\n\nfunction constructClassInstance(workInProgress, ctor, props, renderExpirationTime) {\n  var isLegacyContextConsumer = false;\n  var unmaskedContext = emptyContextObject;\n  var context = null;\n  var contextType = ctor.contextType;\n\n  {\n    if ('contextType' in ctor) {\n      var isValid =\n      // Allow null for conditional declaration\n      contextType === null || contextType !== undefined && contextType.$$typeof === REACT_CONTEXT_TYPE && contextType._context === undefined; // Not a <Context.Consumer>\n\n      if (!isValid && !didWarnAboutInvalidateContextType.has(ctor)) {\n        didWarnAboutInvalidateContextType.add(ctor);\n\n        var addendum = '';\n        if (contextType === undefined) {\n          addendum = ' However, it is set to undefined. ' + 'This can be caused by a typo or by mixing up named and default imports. ' + 'This can also happen due to a circular dependency, so ' + 'try moving the createContext() call to a separate file.';\n        } else if (typeof contextType !== 'object') {\n          addendum = ' However, it is set to a ' + typeof contextType + '.';\n        } else if (contextType.$$typeof === REACT_PROVIDER_TYPE) {\n          addendum = ' Did you accidentally pass the Context.Provider instead?';\n        } else if (contextType._context !== undefined) {\n          // <Context.Consumer>\n          addendum = ' Did you accidentally pass the Context.Consumer instead?';\n        } else {\n          addendum = ' However, it is set to an object with keys {' + Object.keys(contextType).join(', ') + '}.';\n        }\n        warningWithoutStack$1(false, '%s defines an invalid contextType. ' + 'contextType should point to the Context object returned by React.createContext().%s', getComponentName(ctor) || 'Component', addendum);\n      }\n    }\n  }\n\n  if (typeof contextType === 'object' && contextType !== null) {\n    context = readContext(contextType);\n  } else {\n    unmaskedContext = getUnmaskedContext(workInProgress, ctor, true);\n    var contextTypes = ctor.contextTypes;\n    isLegacyContextConsumer = contextTypes !== null && contextTypes !== undefined;\n    context = isLegacyContextConsumer ? getMaskedContext(workInProgress, unmaskedContext) : emptyContextObject;\n  }\n\n  // Instantiate twice to help detect side-effects.\n  {\n    if (debugRenderPhaseSideEffects || debugRenderPhaseSideEffectsForStrictMode && workInProgress.mode & StrictMode) {\n      new ctor(props, context); // eslint-disable-line no-new\n    }\n  }\n\n  var instance = new ctor(props, context);\n  var state = workInProgress.memoizedState = instance.state !== null && instance.state !== undefined ? instance.state : null;\n  adoptClassInstance(workInProgress, instance);\n\n  {\n    if (typeof ctor.getDerivedStateFromProps === 'function' && state === null) {\n      var componentName = getComponentName(ctor) || 'Component';\n      if (!didWarnAboutUninitializedState.has(componentName)) {\n        didWarnAboutUninitializedState.add(componentName);\n        warningWithoutStack$1(false, '`%s` uses `getDerivedStateFromProps` but its initial state is ' + '%s. This is not recommended. Instead, define the initial state by ' + 'assigning an object to `this.state` in the constructor of `%s`. ' + 'This ensures that `getDerivedStateFromProps` arguments have a consistent shape.', componentName, instance.state === null ? 'null' : 'undefined', componentName);\n      }\n    }\n\n    // If new component APIs are defined, \"unsafe\" lifecycles won't be called.\n    // Warn about these lifecycles if they are present.\n    // Don't warn about react-lifecycles-compat polyfilled methods though.\n    if (typeof ctor.getDerivedStateFromProps === 'function' || typeof instance.getSnapshotBeforeUpdate === 'function') {\n      var foundWillMountName = null;\n      var foundWillReceivePropsName = null;\n      var foundWillUpdateName = null;\n      if (typeof instance.componentWillMount === 'function' && instance.componentWillMount.__suppressDeprecationWarning !== true) {\n        foundWillMountName = 'componentWillMount';\n      } else if (typeof instance.UNSAFE_componentWillMount === 'function') {\n        foundWillMountName = 'UNSAFE_componentWillMount';\n      }\n      if (typeof instance.componentWillReceiveProps === 'function' && instance.componentWillReceiveProps.__suppressDeprecationWarning !== true) {\n        foundWillReceivePropsName = 'componentWillReceiveProps';\n      } else if (typeof instance.UNSAFE_componentWillReceiveProps === 'function') {\n        foundWillReceivePropsName = 'UNSAFE_componentWillReceiveProps';\n      }\n      if (typeof instance.componentWillUpdate === 'function' && instance.componentWillUpdate.__suppressDeprecationWarning !== true) {\n        foundWillUpdateName = 'componentWillUpdate';\n      } else if (typeof instance.UNSAFE_componentWillUpdate === 'function') {\n        foundWillUpdateName = 'UNSAFE_componentWillUpdate';\n      }\n      if (foundWillMountName !== null || foundWillReceivePropsName !== null || foundWillUpdateName !== null) {\n        var _componentName = getComponentName(ctor) || 'Component';\n        var newApiName = typeof ctor.getDerivedStateFromProps === 'function' ? 'getDerivedStateFromProps()' : 'getSnapshotBeforeUpdate()';\n        if (!didWarnAboutLegacyLifecyclesAndDerivedState.has(_componentName)) {\n          didWarnAboutLegacyLifecyclesAndDerivedState.add(_componentName);\n          warningWithoutStack$1(false, 'Unsafe legacy lifecycles will not be called for components using new component APIs.\\n\\n' + '%s uses %s but also contains the following legacy lifecycles:%s%s%s\\n\\n' + 'The above lifecycles should be removed. Learn more about this warning here:\\n' + 'https://fb.me/react-async-component-lifecycle-hooks', _componentName, newApiName, foundWillMountName !== null ? '\\n  ' + foundWillMountName : '', foundWillReceivePropsName !== null ? '\\n  ' + foundWillReceivePropsName : '', foundWillUpdateName !== null ? '\\n  ' + foundWillUpdateName : '');\n        }\n      }\n    }\n  }\n\n  // Cache unmasked context so we can avoid recreating masked context unless necessary.\n  // ReactFiberContext usually updates this cache but can't for newly-created instances.\n  if (isLegacyContextConsumer) {\n    cacheContext(workInProgress, unmaskedContext, context);\n  }\n\n  return instance;\n}\n\nfunction callComponentWillMount(workInProgress, instance) {\n  startPhaseTimer(workInProgress, 'componentWillMount');\n  var oldState = instance.state;\n\n  if (typeof instance.componentWillMount === 'function') {\n    instance.componentWillMount();\n  }\n  if (typeof instance.UNSAFE_componentWillMount === 'function') {\n    instance.UNSAFE_componentWillMount();\n  }\n\n  stopPhaseTimer();\n\n  if (oldState !== instance.state) {\n    {\n      warningWithoutStack$1(false, '%s.componentWillMount(): Assigning directly to this.state is ' + \"deprecated (except inside a component's \" + 'constructor). Use setState instead.', getComponentName(workInProgress.type) || 'Component');\n    }\n    classComponentUpdater.enqueueReplaceState(instance, instance.state, null);\n  }\n}\n\nfunction callComponentWillReceiveProps(workInProgress, instance, newProps, nextContext) {\n  var oldState = instance.state;\n  startPhaseTimer(workInProgress, 'componentWillReceiveProps');\n  if (typeof instance.componentWillReceiveProps === 'function') {\n    instance.componentWillReceiveProps(newProps, nextContext);\n  }\n  if (typeof instance.UNSAFE_componentWillReceiveProps === 'function') {\n    instance.UNSAFE_componentWillReceiveProps(newProps, nextContext);\n  }\n  stopPhaseTimer();\n\n  if (instance.state !== oldState) {\n    {\n      var componentName = getComponentName(workInProgress.type) || 'Component';\n      if (!didWarnAboutStateAssignmentForComponent.has(componentName)) {\n        didWarnAboutStateAssignmentForComponent.add(componentName);\n        warningWithoutStack$1(false, '%s.componentWillReceiveProps(): Assigning directly to ' + \"this.state is deprecated (except inside a component's \" + 'constructor). Use setState instead.', componentName);\n      }\n    }\n    classComponentUpdater.enqueueReplaceState(instance, instance.state, null);\n  }\n}\n\n// Invokes the mount life-cycles on a previously never rendered instance.\nfunction mountClassInstance(workInProgress, ctor, newProps, renderExpirationTime) {\n  {\n    checkClassInstance(workInProgress, ctor, newProps);\n  }\n\n  var instance = workInProgress.stateNode;\n  instance.props = newProps;\n  instance.state = workInProgress.memoizedState;\n  instance.refs = emptyRefsObject;\n\n  var contextType = ctor.contextType;\n  if (typeof contextType === 'object' && contextType !== null) {\n    instance.context = readContext(contextType);\n  } else {\n    var unmaskedContext = getUnmaskedContext(workInProgress, ctor, true);\n    instance.context = getMaskedContext(workInProgress, unmaskedContext);\n  }\n\n  {\n    if (instance.state === newProps) {\n      var componentName = getComponentName(ctor) || 'Component';\n      if (!didWarnAboutDirectlyAssigningPropsToState.has(componentName)) {\n        didWarnAboutDirectlyAssigningPropsToState.add(componentName);\n        warningWithoutStack$1(false, '%s: It is not recommended to assign props directly to state ' + \"because updates to props won't be reflected in state. \" + 'In most cases, it is better to use props directly.', componentName);\n      }\n    }\n\n    if (workInProgress.mode & StrictMode) {\n      ReactStrictModeWarnings.recordUnsafeLifecycleWarnings(workInProgress, instance);\n\n      ReactStrictModeWarnings.recordLegacyContextWarning(workInProgress, instance);\n    }\n\n    if (warnAboutDeprecatedLifecycles) {\n      ReactStrictModeWarnings.recordDeprecationWarnings(workInProgress, instance);\n    }\n  }\n\n  var updateQueue = workInProgress.updateQueue;\n  if (updateQueue !== null) {\n    processUpdateQueue(workInProgress, updateQueue, newProps, instance, renderExpirationTime);\n    instance.state = workInProgress.memoizedState;\n  }\n\n  var getDerivedStateFromProps = ctor.getDerivedStateFromProps;\n  if (typeof getDerivedStateFromProps === 'function') {\n    applyDerivedStateFromProps(workInProgress, ctor, getDerivedStateFromProps, newProps);\n    instance.state = workInProgress.memoizedState;\n  }\n\n  // In order to support react-lifecycles-compat polyfilled components,\n  // Unsafe lifecycles should not be invoked for components using the new APIs.\n  if (typeof ctor.getDerivedStateFromProps !== 'function' && typeof instance.getSnapshotBeforeUpdate !== 'function' && (typeof instance.UNSAFE_componentWillMount === 'function' || typeof instance.componentWillMount === 'function')) {\n    callComponentWillMount(workInProgress, instance);\n    // If we had additional state updates during this life-cycle, let's\n    // process them now.\n    updateQueue = workInProgress.updateQueue;\n    if (updateQueue !== null) {\n      processUpdateQueue(workInProgress, updateQueue, newProps, instance, renderExpirationTime);\n      instance.state = workInProgress.memoizedState;\n    }\n  }\n\n  if (typeof instance.componentDidMount === 'function') {\n    workInProgress.effectTag |= Update;\n  }\n}\n\nfunction resumeMountClassInstance(workInProgress, ctor, newProps, renderExpirationTime) {\n  var instance = workInProgress.stateNode;\n\n  var oldProps = workInProgress.memoizedProps;\n  instance.props = oldProps;\n\n  var oldContext = instance.context;\n  var contextType = ctor.contextType;\n  var nextContext = void 0;\n  if (typeof contextType === 'object' && contextType !== null) {\n    nextContext = readContext(contextType);\n  } else {\n    var nextLegacyUnmaskedContext = getUnmaskedContext(workInProgress, ctor, true);\n    nextContext = getMaskedContext(workInProgress, nextLegacyUnmaskedContext);\n  }\n\n  var getDerivedStateFromProps = ctor.getDerivedStateFromProps;\n  var hasNewLifecycles = typeof getDerivedStateFromProps === 'function' || typeof instance.getSnapshotBeforeUpdate === 'function';\n\n  // Note: During these life-cycles, instance.props/instance.state are what\n  // ever the previously attempted to render - not the \"current\". However,\n  // during componentDidUpdate we pass the \"current\" props.\n\n  // In order to support react-lifecycles-compat polyfilled components,\n  // Unsafe lifecycles should not be invoked for components using the new APIs.\n  if (!hasNewLifecycles && (typeof instance.UNSAFE_componentWillReceiveProps === 'function' || typeof instance.componentWillReceiveProps === 'function')) {\n    if (oldProps !== newProps || oldContext !== nextContext) {\n      callComponentWillReceiveProps(workInProgress, instance, newProps, nextContext);\n    }\n  }\n\n  resetHasForceUpdateBeforeProcessing();\n\n  var oldState = workInProgress.memoizedState;\n  var newState = instance.state = oldState;\n  var updateQueue = workInProgress.updateQueue;\n  if (updateQueue !== null) {\n    processUpdateQueue(workInProgress, updateQueue, newProps, instance, renderExpirationTime);\n    newState = workInProgress.memoizedState;\n  }\n  if (oldProps === newProps && oldState === newState && !hasContextChanged() && !checkHasForceUpdateAfterProcessing()) {\n    // If an update was already in progress, we should schedule an Update\n    // effect even though we're bailing out, so that cWU/cDU are called.\n    if (typeof instance.componentDidMount === 'function') {\n      workInProgress.effectTag |= Update;\n    }\n    return false;\n  }\n\n  if (typeof getDerivedStateFromProps === 'function') {\n    applyDerivedStateFromProps(workInProgress, ctor, getDerivedStateFromProps, newProps);\n    newState = workInProgress.memoizedState;\n  }\n\n  var shouldUpdate = checkHasForceUpdateAfterProcessing() || checkShouldComponentUpdate(workInProgress, ctor, oldProps, newProps, oldState, newState, nextContext);\n\n  if (shouldUpdate) {\n    // In order to support react-lifecycles-compat polyfilled components,\n    // Unsafe lifecycles should not be invoked for components using the new APIs.\n    if (!hasNewLifecycles && (typeof instance.UNSAFE_componentWillMount === 'function' || typeof instance.componentWillMount === 'function')) {\n      startPhaseTimer(workInProgress, 'componentWillMount');\n      if (typeof instance.componentWillMount === 'function') {\n        instance.componentWillMount();\n      }\n      if (typeof instance.UNSAFE_componentWillMount === 'function') {\n        instance.UNSAFE_componentWillMount();\n      }\n      stopPhaseTimer();\n    }\n    if (typeof instance.componentDidMount === 'function') {\n      workInProgress.effectTag |= Update;\n    }\n  } else {\n    // If an update was already in progress, we should schedule an Update\n    // effect even though we're bailing out, so that cWU/cDU are called.\n    if (typeof instance.componentDidMount === 'function') {\n      workInProgress.effectTag |= Update;\n    }\n\n    // If shouldComponentUpdate returned false, we should still update the\n    // memoized state to indicate that this work can be reused.\n    workInProgress.memoizedProps = newProps;\n    workInProgress.memoizedState = newState;\n  }\n\n  // Update the existing instance's state, props, and context pointers even\n  // if shouldComponentUpdate returns false.\n  instance.props = newProps;\n  instance.state = newState;\n  instance.context = nextContext;\n\n  return shouldUpdate;\n}\n\n// Invokes the update life-cycles and returns false if it shouldn't rerender.\nfunction updateClassInstance(current, workInProgress, ctor, newProps, renderExpirationTime) {\n  var instance = workInProgress.stateNode;\n\n  var oldProps = workInProgress.memoizedProps;\n  instance.props = workInProgress.type === workInProgress.elementType ? oldProps : resolveDefaultProps(workInProgress.type, oldProps);\n\n  var oldContext = instance.context;\n  var contextType = ctor.contextType;\n  var nextContext = void 0;\n  if (typeof contextType === 'object' && contextType !== null) {\n    nextContext = readContext(contextType);\n  } else {\n    var nextUnmaskedContext = getUnmaskedContext(workInProgress, ctor, true);\n    nextContext = getMaskedContext(workInProgress, nextUnmaskedContext);\n  }\n\n  var getDerivedStateFromProps = ctor.getDerivedStateFromProps;\n  var hasNewLifecycles = typeof getDerivedStateFromProps === 'function' || typeof instance.getSnapshotBeforeUpdate === 'function';\n\n  // Note: During these life-cycles, instance.props/instance.state are what\n  // ever the previously attempted to render - not the \"current\". However,\n  // during componentDidUpdate we pass the \"current\" props.\n\n  // In order to support react-lifecycles-compat polyfilled components,\n  // Unsafe lifecycles should not be invoked for components using the new APIs.\n  if (!hasNewLifecycles && (typeof instance.UNSAFE_componentWillReceiveProps === 'function' || typeof instance.componentWillReceiveProps === 'function')) {\n    if (oldProps !== newProps || oldContext !== nextContext) {\n      callComponentWillReceiveProps(workInProgress, instance, newProps, nextContext);\n    }\n  }\n\n  resetHasForceUpdateBeforeProcessing();\n\n  var oldState = workInProgress.memoizedState;\n  var newState = instance.state = oldState;\n  var updateQueue = workInProgress.updateQueue;\n  if (updateQueue !== null) {\n    processUpdateQueue(workInProgress, updateQueue, newProps, instance, renderExpirationTime);\n    newState = workInProgress.memoizedState;\n  }\n\n  if (oldProps === newProps && oldState === newState && !hasContextChanged() && !checkHasForceUpdateAfterProcessing()) {\n    // If an update was already in progress, we should schedule an Update\n    // effect even though we're bailing out, so that cWU/cDU are called.\n    if (typeof instance.componentDidUpdate === 'function') {\n      if (oldProps !== current.memoizedProps || oldState !== current.memoizedState) {\n        workInProgress.effectTag |= Update;\n      }\n    }\n    if (typeof instance.getSnapshotBeforeUpdate === 'function') {\n      if (oldProps !== current.memoizedProps || oldState !== current.memoizedState) {\n        workInProgress.effectTag |= Snapshot;\n      }\n    }\n    return false;\n  }\n\n  if (typeof getDerivedStateFromProps === 'function') {\n    applyDerivedStateFromProps(workInProgress, ctor, getDerivedStateFromProps, newProps);\n    newState = workInProgress.memoizedState;\n  }\n\n  var shouldUpdate = checkHasForceUpdateAfterProcessing() || checkShouldComponentUpdate(workInProgress, ctor, oldProps, newProps, oldState, newState, nextContext);\n\n  if (shouldUpdate) {\n    // In order to support react-lifecycles-compat polyfilled components,\n    // Unsafe lifecycles should not be invoked for components using the new APIs.\n    if (!hasNewLifecycles && (typeof instance.UNSAFE_componentWillUpdate === 'function' || typeof instance.componentWillUpdate === 'function')) {\n      startPhaseTimer(workInProgress, 'componentWillUpdate');\n      if (typeof instance.componentWillUpdate === 'function') {\n        instance.componentWillUpdate(newProps, newState, nextContext);\n      }\n      if (typeof instance.UNSAFE_componentWillUpdate === 'function') {\n        instance.UNSAFE_componentWillUpdate(newProps, newState, nextContext);\n      }\n      stopPhaseTimer();\n    }\n    if (typeof instance.componentDidUpdate === 'function') {\n      workInProgress.effectTag |= Update;\n    }\n    if (typeof instance.getSnapshotBeforeUpdate === 'function') {\n      workInProgress.effectTag |= Snapshot;\n    }\n  } else {\n    // If an update was already in progress, we should schedule an Update\n    // effect even though we're bailing out, so that cWU/cDU are called.\n    if (typeof instance.componentDidUpdate === 'function') {\n      if (oldProps !== current.memoizedProps || oldState !== current.memoizedState) {\n        workInProgress.effectTag |= Update;\n      }\n    }\n    if (typeof instance.getSnapshotBeforeUpdate === 'function') {\n      if (oldProps !== current.memoizedProps || oldState !== current.memoizedState) {\n        workInProgress.effectTag |= Snapshot;\n      }\n    }\n\n    // If shouldComponentUpdate returned false, we should still update the\n    // memoized props/state to indicate that this work can be reused.\n    workInProgress.memoizedProps = newProps;\n    workInProgress.memoizedState = newState;\n  }\n\n  // Update the existing instance's state, props, and context pointers even\n  // if shouldComponentUpdate returns false.\n  instance.props = newProps;\n  instance.state = newState;\n  instance.context = nextContext;\n\n  return shouldUpdate;\n}\n\nvar didWarnAboutMaps = void 0;\nvar didWarnAboutGenerators = void 0;\nvar didWarnAboutStringRefInStrictMode = void 0;\nvar ownerHasKeyUseWarning = void 0;\nvar ownerHasFunctionTypeWarning = void 0;\nvar warnForMissingKey = function (child) {};\n\n{\n  didWarnAboutMaps = false;\n  didWarnAboutGenerators = false;\n  didWarnAboutStringRefInStrictMode = {};\n\n  /**\n   * Warn if there's no key explicitly set on dynamic arrays of children or\n   * object keys are not valid. This allows us to keep track of children between\n   * updates.\n   */\n  ownerHasKeyUseWarning = {};\n  ownerHasFunctionTypeWarning = {};\n\n  warnForMissingKey = function (child) {\n    if (child === null || typeof child !== 'object') {\n      return;\n    }\n    if (!child._store || child._store.validated || child.key != null) {\n      return;\n    }\n    !(typeof child._store === 'object') ? invariant(false, 'React Component in warnForMissingKey should have a _store. This error is likely caused by a bug in React. Please file an issue.') : void 0;\n    child._store.validated = true;\n\n    var currentComponentErrorInfo = 'Each child in a list should have a unique ' + '\"key\" prop. See https://fb.me/react-warning-keys for ' + 'more information.' + getCurrentFiberStackInDev();\n    if (ownerHasKeyUseWarning[currentComponentErrorInfo]) {\n      return;\n    }\n    ownerHasKeyUseWarning[currentComponentErrorInfo] = true;\n\n    warning$1(false, 'Each child in a list should have a unique ' + '\"key\" prop. See https://fb.me/react-warning-keys for ' + 'more information.');\n  };\n}\n\nvar isArray = Array.isArray;\n\nfunction coerceRef(returnFiber, current$$1, element) {\n  var mixedRef = element.ref;\n  if (mixedRef !== null && typeof mixedRef !== 'function' && typeof mixedRef !== 'object') {\n    {\n      if (returnFiber.mode & StrictMode) {\n        var componentName = getComponentName(returnFiber.type) || 'Component';\n        if (!didWarnAboutStringRefInStrictMode[componentName]) {\n          warningWithoutStack$1(false, 'A string ref, \"%s\", has been found within a strict mode tree. ' + 'String refs are a source of potential bugs and should be avoided. ' + 'We recommend using createRef() instead.' + '\\n%s' + '\\n\\nLearn more about using refs safely here:' + '\\nhttps://fb.me/react-strict-mode-string-ref', mixedRef, getStackByFiberInDevAndProd(returnFiber));\n          didWarnAboutStringRefInStrictMode[componentName] = true;\n        }\n      }\n    }\n\n    if (element._owner) {\n      var owner = element._owner;\n      var inst = void 0;\n      if (owner) {\n        var ownerFiber = owner;\n        !(ownerFiber.tag === ClassComponent) ? invariant(false, 'Function components cannot have refs. Did you mean to use React.forwardRef()?') : void 0;\n        inst = ownerFiber.stateNode;\n      }\n      !inst ? invariant(false, 'Missing owner for string ref %s. This error is likely caused by a bug in React. Please file an issue.', mixedRef) : void 0;\n      var stringRef = '' + mixedRef;\n      // Check if previous string ref matches new string ref\n      if (current$$1 !== null && current$$1.ref !== null && typeof current$$1.ref === 'function' && current$$1.ref._stringRef === stringRef) {\n        return current$$1.ref;\n      }\n      var ref = function (value) {\n        var refs = inst.refs;\n        if (refs === emptyRefsObject) {\n          // This is a lazy pooled frozen object, so we need to initialize.\n          refs = inst.refs = {};\n        }\n        if (value === null) {\n          delete refs[stringRef];\n        } else {\n          refs[stringRef] = value;\n        }\n      };\n      ref._stringRef = stringRef;\n      return ref;\n    } else {\n      !(typeof mixedRef === 'string') ? invariant(false, 'Expected ref to be a function, a string, an object returned by React.createRef(), or null.') : void 0;\n      !element._owner ? invariant(false, 'Element ref was specified as a string (%s) but no owner was set. This could happen for one of the following reasons:\\n1. You may be adding a ref to a function component\\n2. You may be adding a ref to a component that was not created inside a component\\'s render method\\n3. You have multiple copies of React loaded\\nSee https://fb.me/react-refs-must-have-owner for more information.', mixedRef) : void 0;\n    }\n  }\n  return mixedRef;\n}\n\nfunction throwOnInvalidObjectType(returnFiber, newChild) {\n  if (returnFiber.type !== 'textarea') {\n    var addendum = '';\n    {\n      addendum = ' If you meant to render a collection of children, use an array ' + 'instead.' + getCurrentFiberStackInDev();\n    }\n    invariant(false, 'Objects are not valid as a React child (found: %s).%s', Object.prototype.toString.call(newChild) === '[object Object]' ? 'object with keys {' + Object.keys(newChild).join(', ') + '}' : newChild, addendum);\n  }\n}\n\nfunction warnOnFunctionType() {\n  var currentComponentErrorInfo = 'Functions are not valid as a React child. This may happen if ' + 'you return a Component instead of <Component /> from render. ' + 'Or maybe you meant to call this function rather than return it.' + getCurrentFiberStackInDev();\n\n  if (ownerHasFunctionTypeWarning[currentComponentErrorInfo]) {\n    return;\n  }\n  ownerHasFunctionTypeWarning[currentComponentErrorInfo] = true;\n\n  warning$1(false, 'Functions are not valid as a React child. This may happen if ' + 'you return a Component instead of <Component /> from render. ' + 'Or maybe you meant to call this function rather than return it.');\n}\n\n// This wrapper function exists because I expect to clone the code in each path\n// to be able to optimize each path individually by branching early. This needs\n// a compiler or we can do it manually. Helpers that don't need this branching\n// live outside of this function.\nfunction ChildReconciler(shouldTrackSideEffects) {\n  function deleteChild(returnFiber, childToDelete) {\n    if (!shouldTrackSideEffects) {\n      // Noop.\n      return;\n    }\n    // Deletions are added in reversed order so we add it to the front.\n    // At this point, the return fiber's effect list is empty except for\n    // deletions, so we can just append the deletion to the list. The remaining\n    // effects aren't added until the complete phase. Once we implement\n    // resuming, this may not be true.\n    var last = returnFiber.lastEffect;\n    if (last !== null) {\n      last.nextEffect = childToDelete;\n      returnFiber.lastEffect = childToDelete;\n    } else {\n      returnFiber.firstEffect = returnFiber.lastEffect = childToDelete;\n    }\n    childToDelete.nextEffect = null;\n    childToDelete.effectTag = Deletion;\n  }\n\n  function deleteRemainingChildren(returnFiber, currentFirstChild) {\n    if (!shouldTrackSideEffects) {\n      // Noop.\n      return null;\n    }\n\n    // TODO: For the shouldClone case, this could be micro-optimized a bit by\n    // assuming that after the first child we've already added everything.\n    var childToDelete = currentFirstChild;\n    while (childToDelete !== null) {\n      deleteChild(returnFiber, childToDelete);\n      childToDelete = childToDelete.sibling;\n    }\n    return null;\n  }\n\n  function mapRemainingChildren(returnFiber, currentFirstChild) {\n    // Add the remaining children to a temporary map so that we can find them by\n    // keys quickly. Implicit (null) keys get added to this set with their index\n    var existingChildren = new Map();\n\n    var existingChild = currentFirstChild;\n    while (existingChild !== null) {\n      if (existingChild.key !== null) {\n        existingChildren.set(existingChild.key, existingChild);\n      } else {\n        existingChildren.set(existingChild.index, existingChild);\n      }\n      existingChild = existingChild.sibling;\n    }\n    return existingChildren;\n  }\n\n  function useFiber(fiber, pendingProps, expirationTime) {\n    // We currently set sibling to null and index to 0 here because it is easy\n    // to forget to do before returning it. E.g. for the single child case.\n    var clone = createWorkInProgress(fiber, pendingProps, expirationTime);\n    clone.index = 0;\n    clone.sibling = null;\n    return clone;\n  }\n\n  function placeChild(newFiber, lastPlacedIndex, newIndex) {\n    newFiber.index = newIndex;\n    if (!shouldTrackSideEffects) {\n      // Noop.\n      return lastPlacedIndex;\n    }\n    var current$$1 = newFiber.alternate;\n    if (current$$1 !== null) {\n      var oldIndex = current$$1.index;\n      if (oldIndex < lastPlacedIndex) {\n        // This is a move.\n        newFiber.effectTag = Placement;\n        return lastPlacedIndex;\n      } else {\n        // This item can stay in place.\n        return oldIndex;\n      }\n    } else {\n      // This is an insertion.\n      newFiber.effectTag = Placement;\n      return lastPlacedIndex;\n    }\n  }\n\n  function placeSingleChild(newFiber) {\n    // This is simpler for the single child case. We only need to do a\n    // placement for inserting new children.\n    if (shouldTrackSideEffects && newFiber.alternate === null) {\n      newFiber.effectTag = Placement;\n    }\n    return newFiber;\n  }\n\n  function updateTextNode(returnFiber, current$$1, textContent, expirationTime) {\n    if (current$$1 === null || current$$1.tag !== HostText) {\n      // Insert\n      var created = createFiberFromText(textContent, returnFiber.mode, expirationTime);\n      created.return = returnFiber;\n      return created;\n    } else {\n      // Update\n      var existing = useFiber(current$$1, textContent, expirationTime);\n      existing.return = returnFiber;\n      return existing;\n    }\n  }\n\n  function updateElement(returnFiber, current$$1, element, expirationTime) {\n    if (current$$1 !== null && current$$1.elementType === element.type) {\n      // Move based on index\n      var existing = useFiber(current$$1, element.props, expirationTime);\n      existing.ref = coerceRef(returnFiber, current$$1, element);\n      existing.return = returnFiber;\n      {\n        existing._debugSource = element._source;\n        existing._debugOwner = element._owner;\n      }\n      return existing;\n    } else {\n      // Insert\n      var created = createFiberFromElement(element, returnFiber.mode, expirationTime);\n      created.ref = coerceRef(returnFiber, current$$1, element);\n      created.return = returnFiber;\n      return created;\n    }\n  }\n\n  function updatePortal(returnFiber, current$$1, portal, expirationTime) {\n    if (current$$1 === null || current$$1.tag !== HostPortal || current$$1.stateNode.containerInfo !== portal.containerInfo || current$$1.stateNode.implementation !== portal.implementation) {\n      // Insert\n      var created = createFiberFromPortal(portal, returnFiber.mode, expirationTime);\n      created.return = returnFiber;\n      return created;\n    } else {\n      // Update\n      var existing = useFiber(current$$1, portal.children || [], expirationTime);\n      existing.return = returnFiber;\n      return existing;\n    }\n  }\n\n  function updateFragment(returnFiber, current$$1, fragment, expirationTime, key) {\n    if (current$$1 === null || current$$1.tag !== Fragment) {\n      // Insert\n      var created = createFiberFromFragment(fragment, returnFiber.mode, expirationTime, key);\n      created.return = returnFiber;\n      return created;\n    } else {\n      // Update\n      var existing = useFiber(current$$1, fragment, expirationTime);\n      existing.return = returnFiber;\n      return existing;\n    }\n  }\n\n  function createChild(returnFiber, newChild, expirationTime) {\n    if (typeof newChild === 'string' || typeof newChild === 'number') {\n      // Text nodes don't have keys. If the previous node is implicitly keyed\n      // we can continue to replace it without aborting even if it is not a text\n      // node.\n      var created = createFiberFromText('' + newChild, returnFiber.mode, expirationTime);\n      created.return = returnFiber;\n      return created;\n    }\n\n    if (typeof newChild === 'object' && newChild !== null) {\n      switch (newChild.$$typeof) {\n        case REACT_ELEMENT_TYPE:\n          {\n            var _created = createFiberFromElement(newChild, returnFiber.mode, expirationTime);\n            _created.ref = coerceRef(returnFiber, null, newChild);\n            _created.return = returnFiber;\n            return _created;\n          }\n        case REACT_PORTAL_TYPE:\n          {\n            var _created2 = createFiberFromPortal(newChild, returnFiber.mode, expirationTime);\n            _created2.return = returnFiber;\n            return _created2;\n          }\n      }\n\n      if (isArray(newChild) || getIteratorFn(newChild)) {\n        var _created3 = createFiberFromFragment(newChild, returnFiber.mode, expirationTime, null);\n        _created3.return = returnFiber;\n        return _created3;\n      }\n\n      throwOnInvalidObjectType(returnFiber, newChild);\n    }\n\n    {\n      if (typeof newChild === 'function') {\n        warnOnFunctionType();\n      }\n    }\n\n    return null;\n  }\n\n  function updateSlot(returnFiber, oldFiber, newChild, expirationTime) {\n    // Update the fiber if the keys match, otherwise return null.\n\n    var key = oldFiber !== null ? oldFiber.key : null;\n\n    if (typeof newChild === 'string' || typeof newChild === 'number') {\n      // Text nodes don't have keys. If the previous node is implicitly keyed\n      // we can continue to replace it without aborting even if it is not a text\n      // node.\n      if (key !== null) {\n        return null;\n      }\n      return updateTextNode(returnFiber, oldFiber, '' + newChild, expirationTime);\n    }\n\n    if (typeof newChild === 'object' && newChild !== null) {\n      switch (newChild.$$typeof) {\n        case REACT_ELEMENT_TYPE:\n          {\n            if (newChild.key === key) {\n              if (newChild.type === REACT_FRAGMENT_TYPE) {\n                return updateFragment(returnFiber, oldFiber, newChild.props.children, expirationTime, key);\n              }\n              return updateElement(returnFiber, oldFiber, newChild, expirationTime);\n            } else {\n              return null;\n            }\n          }\n        case REACT_PORTAL_TYPE:\n          {\n            if (newChild.key === key) {\n              return updatePortal(returnFiber, oldFiber, newChild, expirationTime);\n            } else {\n              return null;\n            }\n          }\n      }\n\n      if (isArray(newChild) || getIteratorFn(newChild)) {\n        if (key !== null) {\n          return null;\n        }\n\n        return updateFragment(returnFiber, oldFiber, newChild, expirationTime, null);\n      }\n\n      throwOnInvalidObjectType(returnFiber, newChild);\n    }\n\n    {\n      if (typeof newChild === 'function') {\n        warnOnFunctionType();\n      }\n    }\n\n    return null;\n  }\n\n  function updateFromMap(existingChildren, returnFiber, newIdx, newChild, expirationTime) {\n    if (typeof newChild === 'string' || typeof newChild === 'number') {\n      // Text nodes don't have keys, so we neither have to check the old nor\n      // new node for the key. If both are text nodes, they match.\n      var matchedFiber = existingChildren.get(newIdx) || null;\n      return updateTextNode(returnFiber, matchedFiber, '' + newChild, expirationTime);\n    }\n\n    if (typeof newChild === 'object' && newChild !== null) {\n      switch (newChild.$$typeof) {\n        case REACT_ELEMENT_TYPE:\n          {\n            var _matchedFiber = existingChildren.get(newChild.key === null ? newIdx : newChild.key) || null;\n            if (newChild.type === REACT_FRAGMENT_TYPE) {\n              return updateFragment(returnFiber, _matchedFiber, newChild.props.children, expirationTime, newChild.key);\n            }\n            return updateElement(returnFiber, _matchedFiber, newChild, expirationTime);\n          }\n        case REACT_PORTAL_TYPE:\n          {\n            var _matchedFiber2 = existingChildren.get(newChild.key === null ? newIdx : newChild.key) || null;\n            return updatePortal(returnFiber, _matchedFiber2, newChild, expirationTime);\n          }\n      }\n\n      if (isArray(newChild) || getIteratorFn(newChild)) {\n        var _matchedFiber3 = existingChildren.get(newIdx) || null;\n        return updateFragment(returnFiber, _matchedFiber3, newChild, expirationTime, null);\n      }\n\n      throwOnInvalidObjectType(returnFiber, newChild);\n    }\n\n    {\n      if (typeof newChild === 'function') {\n        warnOnFunctionType();\n      }\n    }\n\n    return null;\n  }\n\n  /**\n   * Warns if there is a duplicate or missing key\n   */\n  function warnOnInvalidKey(child, knownKeys) {\n    {\n      if (typeof child !== 'object' || child === null) {\n        return knownKeys;\n      }\n      switch (child.$$typeof) {\n        case REACT_ELEMENT_TYPE:\n        case REACT_PORTAL_TYPE:\n          warnForMissingKey(child);\n          var key = child.key;\n          if (typeof key !== 'string') {\n            break;\n          }\n          if (knownKeys === null) {\n            knownKeys = new Set();\n            knownKeys.add(key);\n            break;\n          }\n          if (!knownKeys.has(key)) {\n            knownKeys.add(key);\n            break;\n          }\n          warning$1(false, 'Encountered two children with the same key, `%s`. ' + 'Keys should be unique so that components maintain their identity ' + 'across updates. Non-unique keys may cause children to be ' + 'duplicated and/or omitted — the behavior is unsupported and ' + 'could change in a future version.', key);\n          break;\n        default:\n          break;\n      }\n    }\n    return knownKeys;\n  }\n\n  function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren, expirationTime) {\n    // This algorithm can't optimize by searching from both ends since we\n    // don't have backpointers on fibers. I'm trying to see how far we can get\n    // with that model. If it ends up not being worth the tradeoffs, we can\n    // add it later.\n\n    // Even with a two ended optimization, we'd want to optimize for the case\n    // where there are few changes and brute force the comparison instead of\n    // going for the Map. It'd like to explore hitting that path first in\n    // forward-only mode and only go for the Map once we notice that we need\n    // lots of look ahead. This doesn't handle reversal as well as two ended\n    // search but that's unusual. Besides, for the two ended optimization to\n    // work on Iterables, we'd need to copy the whole set.\n\n    // In this first iteration, we'll just live with hitting the bad case\n    // (adding everything to a Map) in for every insert/move.\n\n    // If you change this code, also update reconcileChildrenIterator() which\n    // uses the same algorithm.\n\n    {\n      // First, validate keys.\n      var knownKeys = null;\n      for (var i = 0; i < newChildren.length; i++) {\n        var child = newChildren[i];\n        knownKeys = warnOnInvalidKey(child, knownKeys);\n      }\n    }\n\n    var resultingFirstChild = null;\n    var previousNewFiber = null;\n\n    var oldFiber = currentFirstChild;\n    var lastPlacedIndex = 0;\n    var newIdx = 0;\n    var nextOldFiber = null;\n    for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {\n      if (oldFiber.index > newIdx) {\n        nextOldFiber = oldFiber;\n        oldFiber = null;\n      } else {\n        nextOldFiber = oldFiber.sibling;\n      }\n      var newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx], expirationTime);\n      if (newFiber === null) {\n        // TODO: This breaks on empty slots like null children. That's\n        // unfortunate because it triggers the slow path all the time. We need\n        // a better way to communicate whether this was a miss or null,\n        // boolean, undefined, etc.\n        if (oldFiber === null) {\n          oldFiber = nextOldFiber;\n        }\n        break;\n      }\n      if (shouldTrackSideEffects) {\n        if (oldFiber && newFiber.alternate === null) {\n          // We matched the slot, but we didn't reuse the existing fiber, so we\n          // need to delete the existing child.\n          deleteChild(returnFiber, oldFiber);\n        }\n      }\n      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);\n      if (previousNewFiber === null) {\n        // TODO: Move out of the loop. This only happens for the first run.\n        resultingFirstChild = newFiber;\n      } else {\n        // TODO: Defer siblings if we're not at the right index for this slot.\n        // I.e. if we had null values before, then we want to defer this\n        // for each null value. However, we also don't want to call updateSlot\n        // with the previous one.\n        previousNewFiber.sibling = newFiber;\n      }\n      previousNewFiber = newFiber;\n      oldFiber = nextOldFiber;\n    }\n\n    if (newIdx === newChildren.length) {\n      // We've reached the end of the new children. We can delete the rest.\n      deleteRemainingChildren(returnFiber, oldFiber);\n      return resultingFirstChild;\n    }\n\n    if (oldFiber === null) {\n      // If we don't have any more existing children we can choose a fast path\n      // since the rest will all be insertions.\n      for (; newIdx < newChildren.length; newIdx++) {\n        var _newFiber = createChild(returnFiber, newChildren[newIdx], expirationTime);\n        if (!_newFiber) {\n          continue;\n        }\n        lastPlacedIndex = placeChild(_newFiber, lastPlacedIndex, newIdx);\n        if (previousNewFiber === null) {\n          // TODO: Move out of the loop. This only happens for the first run.\n          resultingFirstChild = _newFiber;\n        } else {\n          previousNewFiber.sibling = _newFiber;\n        }\n        previousNewFiber = _newFiber;\n      }\n      return resultingFirstChild;\n    }\n\n    // Add all children to a key map for quick lookups.\n    var existingChildren = mapRemainingChildren(returnFiber, oldFiber);\n\n    // Keep scanning and use the map to restore deleted items as moves.\n    for (; newIdx < newChildren.length; newIdx++) {\n      var _newFiber2 = updateFromMap(existingChildren, returnFiber, newIdx, newChildren[newIdx], expirationTime);\n      if (_newFiber2) {\n        if (shouldTrackSideEffects) {\n          if (_newFiber2.alternate !== null) {\n            // The new fiber is a work in progress, but if there exists a\n            // current, that means that we reused the fiber. We need to delete\n            // it from the child list so that we don't add it to the deletion\n            // list.\n            existingChildren.delete(_newFiber2.key === null ? newIdx : _newFiber2.key);\n          }\n        }\n        lastPlacedIndex = placeChild(_newFiber2, lastPlacedIndex, newIdx);\n        if (previousNewFiber === null) {\n          resultingFirstChild = _newFiber2;\n        } else {\n          previousNewFiber.sibling = _newFiber2;\n        }\n        previousNewFiber = _newFiber2;\n      }\n    }\n\n    if (shouldTrackSideEffects) {\n      // Any existing children that weren't consumed above were deleted. We need\n      // to add them to the deletion list.\n      existingChildren.forEach(function (child) {\n        return deleteChild(returnFiber, child);\n      });\n    }\n\n    return resultingFirstChild;\n  }\n\n  function reconcileChildrenIterator(returnFiber, currentFirstChild, newChildrenIterable, expirationTime) {\n    // This is the same implementation as reconcileChildrenArray(),\n    // but using the iterator instead.\n\n    var iteratorFn = getIteratorFn(newChildrenIterable);\n    !(typeof iteratorFn === 'function') ? invariant(false, 'An object is not an iterable. This error is likely caused by a bug in React. Please file an issue.') : void 0;\n\n    {\n      // We don't support rendering Generators because it's a mutation.\n      // See https://github.com/facebook/react/issues/12995\n      if (typeof Symbol === 'function' &&\n      // $FlowFixMe Flow doesn't know about toStringTag\n      newChildrenIterable[Symbol.toStringTag] === 'Generator') {\n        !didWarnAboutGenerators ? warning$1(false, 'Using Generators as children is unsupported and will likely yield ' + 'unexpected results because enumerating a generator mutates it. ' + 'You may convert it to an array with `Array.from()` or the ' + '`[...spread]` operator before rendering. Keep in mind ' + 'you might need to polyfill these features for older browsers.') : void 0;\n        didWarnAboutGenerators = true;\n      }\n\n      // Warn about using Maps as children\n      if (newChildrenIterable.entries === iteratorFn) {\n        !didWarnAboutMaps ? warning$1(false, 'Using Maps as children is unsupported and will likely yield ' + 'unexpected results. Convert it to a sequence/iterable of keyed ' + 'ReactElements instead.') : void 0;\n        didWarnAboutMaps = true;\n      }\n\n      // First, validate keys.\n      // We'll get a different iterator later for the main pass.\n      var _newChildren = iteratorFn.call(newChildrenIterable);\n      if (_newChildren) {\n        var knownKeys = null;\n        var _step = _newChildren.next();\n        for (; !_step.done; _step = _newChildren.next()) {\n          var child = _step.value;\n          knownKeys = warnOnInvalidKey(child, knownKeys);\n        }\n      }\n    }\n\n    var newChildren = iteratorFn.call(newChildrenIterable);\n    !(newChildren != null) ? invariant(false, 'An iterable object provided no iterator.') : void 0;\n\n    var resultingFirstChild = null;\n    var previousNewFiber = null;\n\n    var oldFiber = currentFirstChild;\n    var lastPlacedIndex = 0;\n    var newIdx = 0;\n    var nextOldFiber = null;\n\n    var step = newChildren.next();\n    for (; oldFiber !== null && !step.done; newIdx++, step = newChildren.next()) {\n      if (oldFiber.index > newIdx) {\n        nextOldFiber = oldFiber;\n        oldFiber = null;\n      } else {\n        nextOldFiber = oldFiber.sibling;\n      }\n      var newFiber = updateSlot(returnFiber, oldFiber, step.value, expirationTime);\n      if (newFiber === null) {\n        // TODO: This breaks on empty slots like null children. That's\n        // unfortunate because it triggers the slow path all the time. We need\n        // a better way to communicate whether this was a miss or null,\n        // boolean, undefined, etc.\n        if (!oldFiber) {\n          oldFiber = nextOldFiber;\n        }\n        break;\n      }\n      if (shouldTrackSideEffects) {\n        if (oldFiber && newFiber.alternate === null) {\n          // We matched the slot, but we didn't reuse the existing fiber, so we\n          // need to delete the existing child.\n          deleteChild(returnFiber, oldFiber);\n        }\n      }\n      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);\n      if (previousNewFiber === null) {\n        // TODO: Move out of the loop. This only happens for the first run.\n        resultingFirstChild = newFiber;\n      } else {\n        // TODO: Defer siblings if we're not at the right index for this slot.\n        // I.e. if we had null values before, then we want to defer this\n        // for each null value. However, we also don't want to call updateSlot\n        // with the previous one.\n        previousNewFiber.sibling = newFiber;\n      }\n      previousNewFiber = newFiber;\n      oldFiber = nextOldFiber;\n    }\n\n    if (step.done) {\n      // We've reached the end of the new children. We can delete the rest.\n      deleteRemainingChildren(returnFiber, oldFiber);\n      return resultingFirstChild;\n    }\n\n    if (oldFiber === null) {\n      // If we don't have any more existing children we can choose a fast path\n      // since the rest will all be insertions.\n      for (; !step.done; newIdx++, step = newChildren.next()) {\n        var _newFiber3 = createChild(returnFiber, step.value, expirationTime);\n        if (_newFiber3 === null) {\n          continue;\n        }\n        lastPlacedIndex = placeChild(_newFiber3, lastPlacedIndex, newIdx);\n        if (previousNewFiber === null) {\n          // TODO: Move out of the loop. This only happens for the first run.\n          resultingFirstChild = _newFiber3;\n        } else {\n          previousNewFiber.sibling = _newFiber3;\n        }\n        previousNewFiber = _newFiber3;\n      }\n      return resultingFirstChild;\n    }\n\n    // Add all children to a key map for quick lookups.\n    var existingChildren = mapRemainingChildren(returnFiber, oldFiber);\n\n    // Keep scanning and use the map to restore deleted items as moves.\n    for (; !step.done; newIdx++, step = newChildren.next()) {\n      var _newFiber4 = updateFromMap(existingChildren, returnFiber, newIdx, step.value, expirationTime);\n      if (_newFiber4 !== null) {\n        if (shouldTrackSideEffects) {\n          if (_newFiber4.alternate !== null) {\n            // The new fiber is a work in progress, but if there exists a\n            // current, that means that we reused the fiber. We need to delete\n            // it from the child list so that we don't add it to the deletion\n            // list.\n            existingChildren.delete(_newFiber4.key === null ? newIdx : _newFiber4.key);\n          }\n        }\n        lastPlacedIndex = placeChild(_newFiber4, lastPlacedIndex, newIdx);\n        if (previousNewFiber === null) {\n          resultingFirstChild = _newFiber4;\n        } else {\n          previousNewFiber.sibling = _newFiber4;\n        }\n        previousNewFiber = _newFiber4;\n      }\n    }\n\n    if (shouldTrackSideEffects) {\n      // Any existing children that weren't consumed above were deleted. We need\n      // to add them to the deletion list.\n      existingChildren.forEach(function (child) {\n        return deleteChild(returnFiber, child);\n      });\n    }\n\n    return resultingFirstChild;\n  }\n\n  function reconcileSingleTextNode(returnFiber, currentFirstChild, textContent, expirationTime) {\n    // There's no need to check for keys on text nodes since we don't have a\n    // way to define them.\n    if (currentFirstChild !== null && currentFirstChild.tag === HostText) {\n      // We already have an existing node so let's just update it and delete\n      // the rest.\n      deleteRemainingChildren(returnFiber, currentFirstChild.sibling);\n      var existing = useFiber(currentFirstChild, textContent, expirationTime);\n      existing.return = returnFiber;\n      return existing;\n    }\n    // The existing first child is not a text node so we need to create one\n    // and delete the existing ones.\n    deleteRemainingChildren(returnFiber, currentFirstChild);\n    var created = createFiberFromText(textContent, returnFiber.mode, expirationTime);\n    created.return = returnFiber;\n    return created;\n  }\n\n  function reconcileSingleElement(returnFiber, currentFirstChild, element, expirationTime) {\n    var key = element.key;\n    var child = currentFirstChild;\n    while (child !== null) {\n      // TODO: If key === null and child.key === null, then this only applies to\n      // the first item in the list.\n      if (child.key === key) {\n        if (child.tag === Fragment ? element.type === REACT_FRAGMENT_TYPE : child.elementType === element.type) {\n          deleteRemainingChildren(returnFiber, child.sibling);\n          var existing = useFiber(child, element.type === REACT_FRAGMENT_TYPE ? element.props.children : element.props, expirationTime);\n          existing.ref = coerceRef(returnFiber, child, element);\n          existing.return = returnFiber;\n          {\n            existing._debugSource = element._source;\n            existing._debugOwner = element._owner;\n          }\n          return existing;\n        } else {\n          deleteRemainingChildren(returnFiber, child);\n          break;\n        }\n      } else {\n        deleteChild(returnFiber, child);\n      }\n      child = child.sibling;\n    }\n\n    if (element.type === REACT_FRAGMENT_TYPE) {\n      var created = createFiberFromFragment(element.props.children, returnFiber.mode, expirationTime, element.key);\n      created.return = returnFiber;\n      return created;\n    } else {\n      var _created4 = createFiberFromElement(element, returnFiber.mode, expirationTime);\n      _created4.ref = coerceRef(returnFiber, currentFirstChild, element);\n      _created4.return = returnFiber;\n      return _created4;\n    }\n  }\n\n  function reconcileSinglePortal(returnFiber, currentFirstChild, portal, expirationTime) {\n    var key = portal.key;\n    var child = currentFirstChild;\n    while (child !== null) {\n      // TODO: If key === null and child.key === null, then this only applies to\n      // the first item in the list.\n      if (child.key === key) {\n        if (child.tag === HostPortal && child.stateNode.containerInfo === portal.containerInfo && child.stateNode.implementation === portal.implementation) {\n          deleteRemainingChildren(returnFiber, child.sibling);\n          var existing = useFiber(child, portal.children || [], expirationTime);\n          existing.return = returnFiber;\n          return existing;\n        } else {\n          deleteRemainingChildren(returnFiber, child);\n          break;\n        }\n      } else {\n        deleteChild(returnFiber, child);\n      }\n      child = child.sibling;\n    }\n\n    var created = createFiberFromPortal(portal, returnFiber.mode, expirationTime);\n    created.return = returnFiber;\n    return created;\n  }\n\n  // This API will tag the children with the side-effect of the reconciliation\n  // itself. They will be added to the side-effect list as we pass through the\n  // children and the parent.\n  function reconcileChildFibers(returnFiber, currentFirstChild, newChild, expirationTime) {\n    // This function is not recursive.\n    // If the top level item is an array, we treat it as a set of children,\n    // not as a fragment. Nested arrays on the other hand will be treated as\n    // fragment nodes. Recursion happens at the normal flow.\n\n    // Handle top level unkeyed fragments as if they were arrays.\n    // This leads to an ambiguity between <>{[...]}</> and <>...</>.\n    // We treat the ambiguous cases above the same.\n    var isUnkeyedTopLevelFragment = typeof newChild === 'object' && newChild !== null && newChild.type === REACT_FRAGMENT_TYPE && newChild.key === null;\n    if (isUnkeyedTopLevelFragment) {\n      newChild = newChild.props.children;\n    }\n\n    // Handle object types\n    var isObject = typeof newChild === 'object' && newChild !== null;\n\n    if (isObject) {\n      switch (newChild.$$typeof) {\n        case REACT_ELEMENT_TYPE:\n          return placeSingleChild(reconcileSingleElement(returnFiber, currentFirstChild, newChild, expirationTime));\n        case REACT_PORTAL_TYPE:\n          return placeSingleChild(reconcileSinglePortal(returnFiber, currentFirstChild, newChild, expirationTime));\n      }\n    }\n\n    if (typeof newChild === 'string' || typeof newChild === 'number') {\n      return placeSingleChild(reconcileSingleTextNode(returnFiber, currentFirstChild, '' + newChild, expirationTime));\n    }\n\n    if (isArray(newChild)) {\n      return reconcileChildrenArray(returnFiber, currentFirstChild, newChild, expirationTime);\n    }\n\n    if (getIteratorFn(newChild)) {\n      return reconcileChildrenIterator(returnFiber, currentFirstChild, newChild, expirationTime);\n    }\n\n    if (isObject) {\n      throwOnInvalidObjectType(returnFiber, newChild);\n    }\n\n    {\n      if (typeof newChild === 'function') {\n        warnOnFunctionType();\n      }\n    }\n    if (typeof newChild === 'undefined' && !isUnkeyedTopLevelFragment) {\n      // If the new child is undefined, and the return fiber is a composite\n      // component, throw an error. If Fiber return types are disabled,\n      // we already threw above.\n      switch (returnFiber.tag) {\n        case ClassComponent:\n          {\n            {\n              var instance = returnFiber.stateNode;\n              if (instance.render._isMockFunction) {\n                // We allow auto-mocks to proceed as if they're returning null.\n                break;\n              }\n            }\n          }\n        // Intentionally fall through to the next case, which handles both\n        // functions and classes\n        // eslint-disable-next-lined no-fallthrough\n        case FunctionComponent:\n          {\n            var Component = returnFiber.type;\n            invariant(false, '%s(...): Nothing was returned from render. This usually means a return statement is missing. Or, to render nothing, return null.', Component.displayName || Component.name || 'Component');\n          }\n      }\n    }\n\n    // Remaining cases are all treated as empty.\n    return deleteRemainingChildren(returnFiber, currentFirstChild);\n  }\n\n  return reconcileChildFibers;\n}\n\nvar reconcileChildFibers = ChildReconciler(true);\nvar mountChildFibers = ChildReconciler(false);\n\nfunction cloneChildFibers(current$$1, workInProgress) {\n  !(current$$1 === null || workInProgress.child === current$$1.child) ? invariant(false, 'Resuming work not yet implemented.') : void 0;\n\n  if (workInProgress.child === null) {\n    return;\n  }\n\n  var currentChild = workInProgress.child;\n  var newChild = createWorkInProgress(currentChild, currentChild.pendingProps, currentChild.expirationTime);\n  workInProgress.child = newChild;\n\n  newChild.return = workInProgress;\n  while (currentChild.sibling !== null) {\n    currentChild = currentChild.sibling;\n    newChild = newChild.sibling = createWorkInProgress(currentChild, currentChild.pendingProps, currentChild.expirationTime);\n    newChild.return = workInProgress;\n  }\n  newChild.sibling = null;\n}\n\nvar NO_CONTEXT = {};\n\nvar contextStackCursor$1 = createCursor(NO_CONTEXT);\nvar contextFiberStackCursor = createCursor(NO_CONTEXT);\nvar rootInstanceStackCursor = createCursor(NO_CONTEXT);\n\nfunction requiredContext(c) {\n  !(c !== NO_CONTEXT) ? invariant(false, 'Expected host context to exist. This error is likely caused by a bug in React. Please file an issue.') : void 0;\n  return c;\n}\n\nfunction getRootHostContainer() {\n  var rootInstance = requiredContext(rootInstanceStackCursor.current);\n  return rootInstance;\n}\n\nfunction pushHostContainer(fiber, nextRootInstance) {\n  // Push current root instance onto the stack;\n  // This allows us to reset root when portals are popped.\n  push(rootInstanceStackCursor, nextRootInstance, fiber);\n  // Track the context and the Fiber that provided it.\n  // This enables us to pop only Fibers that provide unique contexts.\n  push(contextFiberStackCursor, fiber, fiber);\n\n  // Finally, we need to push the host context to the stack.\n  // However, we can't just call getRootHostContext() and push it because\n  // we'd have a different number of entries on the stack depending on\n  // whether getRootHostContext() throws somewhere in renderer code or not.\n  // So we push an empty value first. This lets us safely unwind on errors.\n  push(contextStackCursor$1, NO_CONTEXT, fiber);\n  var nextRootContext = getRootHostContext(nextRootInstance);\n  // Now that we know this function doesn't throw, replace it.\n  pop(contextStackCursor$1, fiber);\n  push(contextStackCursor$1, nextRootContext, fiber);\n}\n\nfunction popHostContainer(fiber) {\n  pop(contextStackCursor$1, fiber);\n  pop(contextFiberStackCursor, fiber);\n  pop(rootInstanceStackCursor, fiber);\n}\n\nfunction getHostContext() {\n  var context = requiredContext(contextStackCursor$1.current);\n  return context;\n}\n\nfunction pushHostContext(fiber) {\n  var rootInstance = requiredContext(rootInstanceStackCursor.current);\n  var context = requiredContext(contextStackCursor$1.current);\n  var nextContext = getChildHostContext(context, fiber.type, rootInstance);\n\n  // Don't push this Fiber's context unless it's unique.\n  if (context === nextContext) {\n    return;\n  }\n\n  // Track the context and the Fiber that provided it.\n  // This enables us to pop only Fibers that provide unique contexts.\n  push(contextFiberStackCursor, fiber, fiber);\n  push(contextStackCursor$1, nextContext, fiber);\n}\n\nfunction popHostContext(fiber) {\n  // Do not pop unless this Fiber provided the current context.\n  // pushHostContext() only pushes Fibers that provide unique contexts.\n  if (contextFiberStackCursor.current !== fiber) {\n    return;\n  }\n\n  pop(contextStackCursor$1, fiber);\n  pop(contextFiberStackCursor, fiber);\n}\n\nvar NoEffect$1 = /*             */0;\nvar UnmountSnapshot = /*      */2;\nvar UnmountMutation = /*      */4;\nvar MountMutation = /*        */8;\nvar UnmountLayout = /*        */16;\nvar MountLayout = /*          */32;\nvar MountPassive = /*         */64;\nvar UnmountPassive = /*       */128;\n\nvar ReactCurrentDispatcher$1 = ReactSharedInternals.ReactCurrentDispatcher;\n\n\nvar didWarnAboutMismatchedHooksForComponent = void 0;\n{\n  didWarnAboutMismatchedHooksForComponent = new Set();\n}\n\n// These are set right before calling the component.\nvar renderExpirationTime = NoWork;\n// The work-in-progress fiber. I've named it differently to distinguish it from\n// the work-in-progress hook.\nvar currentlyRenderingFiber$1 = null;\n\n// Hooks are stored as a linked list on the fiber's memoizedState field. The\n// current hook list is the list that belongs to the current fiber. The\n// work-in-progress hook list is a new list that will be added to the\n// work-in-progress fiber.\nvar currentHook = null;\nvar nextCurrentHook = null;\nvar firstWorkInProgressHook = null;\nvar workInProgressHook = null;\nvar nextWorkInProgressHook = null;\n\nvar remainingExpirationTime = NoWork;\nvar componentUpdateQueue = null;\nvar sideEffectTag = 0;\n\n// Updates scheduled during render will trigger an immediate re-render at the\n// end of the current pass. We can't store these updates on the normal queue,\n// because if the work is aborted, they should be discarded. Because this is\n// a relatively rare case, we also don't want to add an additional field to\n// either the hook or queue object types. So we store them in a lazily create\n// map of queue -> render-phase updates, which are discarded once the component\n// completes without re-rendering.\n\n// Whether an update was scheduled during the currently executing render pass.\nvar didScheduleRenderPhaseUpdate = false;\n// Lazily created map of render-phase updates\nvar renderPhaseUpdates = null;\n// Counter to prevent infinite loops.\nvar numberOfReRenders = 0;\nvar RE_RENDER_LIMIT = 25;\n\n// In DEV, this is the name of the currently executing primitive hook\nvar currentHookNameInDev = null;\n\n// In DEV, this list ensures that hooks are called in the same order between renders.\n// The list stores the order of hooks used during the initial render (mount).\n// Subsequent renders (updates) reference this list.\nvar hookTypesDev = null;\nvar hookTypesUpdateIndexDev = -1;\n\nfunction mountHookTypesDev() {\n  {\n    var hookName = currentHookNameInDev;\n\n    if (hookTypesDev === null) {\n      hookTypesDev = [hookName];\n    } else {\n      hookTypesDev.push(hookName);\n    }\n  }\n}\n\nfunction updateHookTypesDev() {\n  {\n    var hookName = currentHookNameInDev;\n\n    if (hookTypesDev !== null) {\n      hookTypesUpdateIndexDev++;\n      if (hookTypesDev[hookTypesUpdateIndexDev] !== hookName) {\n        warnOnHookMismatchInDev(hookName);\n      }\n    }\n  }\n}\n\nfunction warnOnHookMismatchInDev(currentHookName) {\n  {\n    var componentName = getComponentName(currentlyRenderingFiber$1.type);\n    if (!didWarnAboutMismatchedHooksForComponent.has(componentName)) {\n      didWarnAboutMismatchedHooksForComponent.add(componentName);\n\n      if (hookTypesDev !== null) {\n        var table = '';\n\n        var secondColumnStart = 30;\n\n        for (var i = 0; i <= hookTypesUpdateIndexDev; i++) {\n          var oldHookName = hookTypesDev[i];\n          var newHookName = i === hookTypesUpdateIndexDev ? currentHookName : oldHookName;\n\n          var row = i + 1 + '. ' + oldHookName;\n\n          // Extra space so second column lines up\n          // lol @ IE not supporting String#repeat\n          while (row.length < secondColumnStart) {\n            row += ' ';\n          }\n\n          row += newHookName + '\\n';\n\n          table += row;\n        }\n\n        warning$1(false, 'React has detected a change in the order of Hooks called by %s. ' + 'This will lead to bugs and errors if not fixed. ' + 'For more information, read the Rules of Hooks: https://fb.me/rules-of-hooks\\n\\n' + '   Previous render            Next render\\n' + '   ------------------------------------------------------\\n' + '%s' + '   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\\n', componentName, table);\n      }\n    }\n  }\n}\n\nfunction throwInvalidHookError() {\n  invariant(false, 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:\\n1. You might have mismatching versions of React and the renderer (such as React DOM)\\n2. You might be breaking the Rules of Hooks\\n3. You might have more than one copy of React in the same app\\nSee https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.');\n}\n\nfunction areHookInputsEqual(nextDeps, prevDeps) {\n  if (prevDeps === null) {\n    {\n      warning$1(false, '%s received a final argument during this render, but not during ' + 'the previous render. Even though the final argument is optional, ' + 'its type cannot change between renders.', currentHookNameInDev);\n    }\n    return false;\n  }\n\n  {\n    // Don't bother comparing lengths in prod because these arrays should be\n    // passed inline.\n    if (nextDeps.length !== prevDeps.length) {\n      warning$1(false, 'The final argument passed to %s changed size between renders. The ' + 'order and size of this array must remain constant.\\n\\n' + 'Previous: %s\\n' + 'Incoming: %s', currentHookNameInDev, '[' + nextDeps.join(', ') + ']', '[' + prevDeps.join(', ') + ']');\n    }\n  }\n  for (var i = 0; i < prevDeps.length && i < nextDeps.length; i++) {\n    if (is(nextDeps[i], prevDeps[i])) {\n      continue;\n    }\n    return false;\n  }\n  return true;\n}\n\nfunction renderWithHooks(current, workInProgress, Component, props, refOrContext, nextRenderExpirationTime) {\n  renderExpirationTime = nextRenderExpirationTime;\n  currentlyRenderingFiber$1 = workInProgress;\n  nextCurrentHook = current !== null ? current.memoizedState : null;\n\n  {\n    hookTypesDev = current !== null ? current._debugHookTypes : null;\n    hookTypesUpdateIndexDev = -1;\n  }\n\n  // The following should have already been reset\n  // currentHook = null;\n  // workInProgressHook = null;\n\n  // remainingExpirationTime = NoWork;\n  // componentUpdateQueue = null;\n\n  // didScheduleRenderPhaseUpdate = false;\n  // renderPhaseUpdates = null;\n  // numberOfReRenders = 0;\n  // sideEffectTag = 0;\n\n  // TODO Warn if no hooks are used at all during mount, then some are used during update.\n  // Currently we will identify the update render as a mount because nextCurrentHook === null.\n  // This is tricky because it's valid for certain types of components (e.g. React.lazy)\n\n  // Using nextCurrentHook to differentiate between mount/update only works if at least one stateful hook is used.\n  // Non-stateful hooks (e.g. context) don't get added to memoizedState,\n  // so nextCurrentHook would be null during updates and mounts.\n  {\n    if (nextCurrentHook !== null) {\n      ReactCurrentDispatcher$1.current = HooksDispatcherOnUpdateInDEV;\n    } else if (hookTypesDev !== null) {\n      // This dispatcher handles an edge case where a component is updating,\n      // but no stateful hooks have been used.\n      // We want to match the production code behavior (which will use HooksDispatcherOnMount),\n      // but with the extra DEV validation to ensure hooks ordering hasn't changed.\n      // This dispatcher does that.\n      ReactCurrentDispatcher$1.current = HooksDispatcherOnMountWithHookTypesInDEV;\n    } else {\n      ReactCurrentDispatcher$1.current = HooksDispatcherOnMountInDEV;\n    }\n  }\n\n  var children = Component(props, refOrContext);\n\n  if (didScheduleRenderPhaseUpdate) {\n    do {\n      didScheduleRenderPhaseUpdate = false;\n      numberOfReRenders += 1;\n\n      // Start over from the beginning of the list\n      nextCurrentHook = current !== null ? current.memoizedState : null;\n      nextWorkInProgressHook = firstWorkInProgressHook;\n\n      currentHook = null;\n      workInProgressHook = null;\n      componentUpdateQueue = null;\n\n      {\n        // Also validate hook order for cascading updates.\n        hookTypesUpdateIndexDev = -1;\n      }\n\n      ReactCurrentDispatcher$1.current = HooksDispatcherOnUpdateInDEV;\n\n      children = Component(props, refOrContext);\n    } while (didScheduleRenderPhaseUpdate);\n\n    renderPhaseUpdates = null;\n    numberOfReRenders = 0;\n  }\n\n  // We can assume the previous dispatcher is always this one, since we set it\n  // at the beginning of the render phase and there's no re-entrancy.\n  ReactCurrentDispatcher$1.current = ContextOnlyDispatcher;\n\n  var renderedWork = currentlyRenderingFiber$1;\n\n  renderedWork.memoizedState = firstWorkInProgressHook;\n  renderedWork.expirationTime = remainingExpirationTime;\n  renderedWork.updateQueue = componentUpdateQueue;\n  renderedWork.effectTag |= sideEffectTag;\n\n  {\n    renderedWork._debugHookTypes = hookTypesDev;\n  }\n\n  // This check uses currentHook so that it works the same in DEV and prod bundles.\n  // hookTypesDev could catch more cases (e.g. context) but only in DEV bundles.\n  var didRenderTooFewHooks = currentHook !== null && currentHook.next !== null;\n\n  renderExpirationTime = NoWork;\n  currentlyRenderingFiber$1 = null;\n\n  currentHook = null;\n  nextCurrentHook = null;\n  firstWorkInProgressHook = null;\n  workInProgressHook = null;\n  nextWorkInProgressHook = null;\n\n  {\n    currentHookNameInDev = null;\n    hookTypesDev = null;\n    hookTypesUpdateIndexDev = -1;\n  }\n\n  remainingExpirationTime = NoWork;\n  componentUpdateQueue = null;\n  sideEffectTag = 0;\n\n  // These were reset above\n  // didScheduleRenderPhaseUpdate = false;\n  // renderPhaseUpdates = null;\n  // numberOfReRenders = 0;\n\n  !!didRenderTooFewHooks ? invariant(false, 'Rendered fewer hooks than expected. This may be caused by an accidental early return statement.') : void 0;\n\n  return children;\n}\n\nfunction bailoutHooks(current, workInProgress, expirationTime) {\n  workInProgress.updateQueue = current.updateQueue;\n  workInProgress.effectTag &= ~(Passive | Update);\n  if (current.expirationTime <= expirationTime) {\n    current.expirationTime = NoWork;\n  }\n}\n\nfunction resetHooks() {\n  // We can assume the previous dispatcher is always this one, since we set it\n  // at the beginning of the render phase and there's no re-entrancy.\n  ReactCurrentDispatcher$1.current = ContextOnlyDispatcher;\n\n  // This is used to reset the state of this module when a component throws.\n  // It's also called inside mountIndeterminateComponent if we determine the\n  // component is a module-style component.\n  renderExpirationTime = NoWork;\n  currentlyRenderingFiber$1 = null;\n\n  currentHook = null;\n  nextCurrentHook = null;\n  firstWorkInProgressHook = null;\n  workInProgressHook = null;\n  nextWorkInProgressHook = null;\n\n  {\n    hookTypesDev = null;\n    hookTypesUpdateIndexDev = -1;\n\n    currentHookNameInDev = null;\n  }\n\n  remainingExpirationTime = NoWork;\n  componentUpdateQueue = null;\n  sideEffectTag = 0;\n\n  didScheduleRenderPhaseUpdate = false;\n  renderPhaseUpdates = null;\n  numberOfReRenders = 0;\n}\n\nfunction mountWorkInProgressHook() {\n  var hook = {\n    memoizedState: null,\n\n    baseState: null,\n    queue: null,\n    baseUpdate: null,\n\n    next: null\n  };\n\n  if (workInProgressHook === null) {\n    // This is the first hook in the list\n    firstWorkInProgressHook = workInProgressHook = hook;\n  } else {\n    // Append to the end of the list\n    workInProgressHook = workInProgressHook.next = hook;\n  }\n  return workInProgressHook;\n}\n\nfunction updateWorkInProgressHook() {\n  // This function is used both for updates and for re-renders triggered by a\n  // render phase update. It assumes there is either a current hook we can\n  // clone, or a work-in-progress hook from a previous render pass that we can\n  // use as a base. When we reach the end of the base list, we must switch to\n  // the dispatcher used for mounts.\n  if (nextWorkInProgressHook !== null) {\n    // There's already a work-in-progress. Reuse it.\n    workInProgressHook = nextWorkInProgressHook;\n    nextWorkInProgressHook = workInProgressHook.next;\n\n    currentHook = nextCurrentHook;\n    nextCurrentHook = currentHook !== null ? currentHook.next : null;\n  } else {\n    // Clone from the current hook.\n    !(nextCurrentHook !== null) ? invariant(false, 'Rendered more hooks than during the previous render.') : void 0;\n    currentHook = nextCurrentHook;\n\n    var newHook = {\n      memoizedState: currentHook.memoizedState,\n\n      baseState: currentHook.baseState,\n      queue: currentHook.queue,\n      baseUpdate: currentHook.baseUpdate,\n\n      next: null\n    };\n\n    if (workInProgressHook === null) {\n      // This is the first hook in the list.\n      workInProgressHook = firstWorkInProgressHook = newHook;\n    } else {\n      // Append to the end of the list.\n      workInProgressHook = workInProgressHook.next = newHook;\n    }\n    nextCurrentHook = currentHook.next;\n  }\n  return workInProgressHook;\n}\n\nfunction createFunctionComponentUpdateQueue() {\n  return {\n    lastEffect: null\n  };\n}\n\nfunction basicStateReducer(state, action) {\n  return typeof action === 'function' ? action(state) : action;\n}\n\nfunction mountReducer(reducer, initialArg, init) {\n  var hook = mountWorkInProgressHook();\n  var initialState = void 0;\n  if (init !== undefined) {\n    initialState = init(initialArg);\n  } else {\n    initialState = initialArg;\n  }\n  hook.memoizedState = hook.baseState = initialState;\n  var queue = hook.queue = {\n    last: null,\n    dispatch: null,\n    lastRenderedReducer: reducer,\n    lastRenderedState: initialState\n  };\n  var dispatch = queue.dispatch = dispatchAction.bind(null,\n  // Flow doesn't know this is non-null, but we do.\n  currentlyRenderingFiber$1, queue);\n  return [hook.memoizedState, dispatch];\n}\n\nfunction updateReducer(reducer, initialArg, init) {\n  var hook = updateWorkInProgressHook();\n  var queue = hook.queue;\n  !(queue !== null) ? invariant(false, 'Should have a queue. This is likely a bug in React. Please file an issue.') : void 0;\n\n  queue.lastRenderedReducer = reducer;\n\n  if (numberOfReRenders > 0) {\n    // This is a re-render. Apply the new render phase updates to the previous\n    var _dispatch = queue.dispatch;\n    if (renderPhaseUpdates !== null) {\n      // Render phase updates are stored in a map of queue -> linked list\n      var firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);\n      if (firstRenderPhaseUpdate !== undefined) {\n        renderPhaseUpdates.delete(queue);\n        var newState = hook.memoizedState;\n        var update = firstRenderPhaseUpdate;\n        do {\n          // Process this render phase update. We don't have to check the\n          // priority because it will always be the same as the current\n          // render's.\n          var _action = update.action;\n          newState = reducer(newState, _action);\n          update = update.next;\n        } while (update !== null);\n\n        // Mark that the fiber performed work, but only if the new state is\n        // different from the current state.\n        if (!is(newState, hook.memoizedState)) {\n          markWorkInProgressReceivedUpdate();\n        }\n\n        hook.memoizedState = newState;\n        // Don't persist the state accumlated from the render phase updates to\n        // the base state unless the queue is empty.\n        // TODO: Not sure if this is the desired semantics, but it's what we\n        // do for gDSFP. I can't remember why.\n        if (hook.baseUpdate === queue.last) {\n          hook.baseState = newState;\n        }\n\n        queue.lastRenderedState = newState;\n\n        return [newState, _dispatch];\n      }\n    }\n    return [hook.memoizedState, _dispatch];\n  }\n\n  // The last update in the entire queue\n  var last = queue.last;\n  // The last update that is part of the base state.\n  var baseUpdate = hook.baseUpdate;\n  var baseState = hook.baseState;\n\n  // Find the first unprocessed update.\n  var first = void 0;\n  if (baseUpdate !== null) {\n    if (last !== null) {\n      // For the first update, the queue is a circular linked list where\n      // `queue.last.next = queue.first`. Once the first update commits, and\n      // the `baseUpdate` is no longer empty, we can unravel the list.\n      last.next = null;\n    }\n    first = baseUpdate.next;\n  } else {\n    first = last !== null ? last.next : null;\n  }\n  if (first !== null) {\n    var _newState = baseState;\n    var newBaseState = null;\n    var newBaseUpdate = null;\n    var prevUpdate = baseUpdate;\n    var _update = first;\n    var didSkip = false;\n    do {\n      var updateExpirationTime = _update.expirationTime;\n      if (updateExpirationTime < renderExpirationTime) {\n        // Priority is insufficient. Skip this update. If this is the first\n        // skipped update, the previous update/state is the new base\n        // update/state.\n        if (!didSkip) {\n          didSkip = true;\n          newBaseUpdate = prevUpdate;\n          newBaseState = _newState;\n        }\n        // Update the remaining priority in the queue.\n        if (updateExpirationTime > remainingExpirationTime) {\n          remainingExpirationTime = updateExpirationTime;\n        }\n      } else {\n        // Process this update.\n        if (_update.eagerReducer === reducer) {\n          // If this update was processed eagerly, and its reducer matches the\n          // current reducer, we can use the eagerly computed state.\n          _newState = _update.eagerState;\n        } else {\n          var _action2 = _update.action;\n          _newState = reducer(_newState, _action2);\n        }\n      }\n      prevUpdate = _update;\n      _update = _update.next;\n    } while (_update !== null && _update !== first);\n\n    if (!didSkip) {\n      newBaseUpdate = prevUpdate;\n      newBaseState = _newState;\n    }\n\n    // Mark that the fiber performed work, but only if the new state is\n    // different from the current state.\n    if (!is(_newState, hook.memoizedState)) {\n      markWorkInProgressReceivedUpdate();\n    }\n\n    hook.memoizedState = _newState;\n    hook.baseUpdate = newBaseUpdate;\n    hook.baseState = newBaseState;\n\n    queue.lastRenderedState = _newState;\n  }\n\n  var dispatch = queue.dispatch;\n  return [hook.memoizedState, dispatch];\n}\n\nfunction mountState(initialState) {\n  var hook = mountWorkInProgressHook();\n  if (typeof initialState === 'function') {\n    initialState = initialState();\n  }\n  hook.memoizedState = hook.baseState = initialState;\n  var queue = hook.queue = {\n    last: null,\n    dispatch: null,\n    lastRenderedReducer: basicStateReducer,\n    lastRenderedState: initialState\n  };\n  var dispatch = queue.dispatch = dispatchAction.bind(null,\n  // Flow doesn't know this is non-null, but we do.\n  currentlyRenderingFiber$1, queue);\n  return [hook.memoizedState, dispatch];\n}\n\nfunction updateState(initialState) {\n  return updateReducer(basicStateReducer, initialState);\n}\n\nfunction pushEffect(tag, create, destroy, deps) {\n  var effect = {\n    tag: tag,\n    create: create,\n    destroy: destroy,\n    deps: deps,\n    // Circular\n    next: null\n  };\n  if (componentUpdateQueue === null) {\n    componentUpdateQueue = createFunctionComponentUpdateQueue();\n    componentUpdateQueue.lastEffect = effect.next = effect;\n  } else {\n    var _lastEffect = componentUpdateQueue.lastEffect;\n    if (_lastEffect === null) {\n      componentUpdateQueue.lastEffect = effect.next = effect;\n    } else {\n      var firstEffect = _lastEffect.next;\n      _lastEffect.next = effect;\n      effect.next = firstEffect;\n      componentUpdateQueue.lastEffect = effect;\n    }\n  }\n  return effect;\n}\n\nfunction mountRef(initialValue) {\n  var hook = mountWorkInProgressHook();\n  var ref = { current: initialValue };\n  {\n    Object.seal(ref);\n  }\n  hook.memoizedState = ref;\n  return ref;\n}\n\nfunction updateRef(initialValue) {\n  var hook = updateWorkInProgressHook();\n  return hook.memoizedState;\n}\n\nfunction mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps) {\n  var hook = mountWorkInProgressHook();\n  var nextDeps = deps === undefined ? null : deps;\n  sideEffectTag |= fiberEffectTag;\n  hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps);\n}\n\nfunction updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps) {\n  var hook = updateWorkInProgressHook();\n  var nextDeps = deps === undefined ? null : deps;\n  var destroy = undefined;\n\n  if (currentHook !== null) {\n    var prevEffect = currentHook.memoizedState;\n    destroy = prevEffect.destroy;\n    if (nextDeps !== null) {\n      var prevDeps = prevEffect.deps;\n      if (areHookInputsEqual(nextDeps, prevDeps)) {\n        pushEffect(NoEffect$1, create, destroy, nextDeps);\n        return;\n      }\n    }\n  }\n\n  sideEffectTag |= fiberEffectTag;\n  hook.memoizedState = pushEffect(hookEffectTag, create, destroy, nextDeps);\n}\n\nfunction mountEffect(create, deps) {\n  return mountEffectImpl(Update | Passive, UnmountPassive | MountPassive, create, deps);\n}\n\nfunction updateEffect(create, deps) {\n  return updateEffectImpl(Update | Passive, UnmountPassive | MountPassive, create, deps);\n}\n\nfunction mountLayoutEffect(create, deps) {\n  return mountEffectImpl(Update, UnmountMutation | MountLayout, create, deps);\n}\n\nfunction updateLayoutEffect(create, deps) {\n  return updateEffectImpl(Update, UnmountMutation | MountLayout, create, deps);\n}\n\nfunction imperativeHandleEffect(create, ref) {\n  if (typeof ref === 'function') {\n    var refCallback = ref;\n    var _inst = create();\n    refCallback(_inst);\n    return function () {\n      refCallback(null);\n    };\n  } else if (ref !== null && ref !== undefined) {\n    var refObject = ref;\n    {\n      !refObject.hasOwnProperty('current') ? warning$1(false, 'Expected useImperativeHandle() first argument to either be a ' + 'ref callback or React.createRef() object. Instead received: %s.', 'an object with keys {' + Object.keys(refObject).join(', ') + '}') : void 0;\n    }\n    var _inst2 = create();\n    refObject.current = _inst2;\n    return function () {\n      refObject.current = null;\n    };\n  }\n}\n\nfunction mountImperativeHandle(ref, create, deps) {\n  {\n    !(typeof create === 'function') ? warning$1(false, 'Expected useImperativeHandle() second argument to be a function ' + 'that creates a handle. Instead received: %s.', create !== null ? typeof create : 'null') : void 0;\n  }\n\n  // TODO: If deps are provided, should we skip comparing the ref itself?\n  var effectDeps = deps !== null && deps !== undefined ? deps.concat([ref]) : null;\n\n  return mountEffectImpl(Update, UnmountMutation | MountLayout, imperativeHandleEffect.bind(null, create, ref), effectDeps);\n}\n\nfunction updateImperativeHandle(ref, create, deps) {\n  {\n    !(typeof create === 'function') ? warning$1(false, 'Expected useImperativeHandle() second argument to be a function ' + 'that creates a handle. Instead received: %s.', create !== null ? typeof create : 'null') : void 0;\n  }\n\n  // TODO: If deps are provided, should we skip comparing the ref itself?\n  var effectDeps = deps !== null && deps !== undefined ? deps.concat([ref]) : null;\n\n  return updateEffectImpl(Update, UnmountMutation | MountLayout, imperativeHandleEffect.bind(null, create, ref), effectDeps);\n}\n\nfunction mountDebugValue(value, formatterFn) {\n  // This hook is normally a no-op.\n  // The react-debug-hooks package injects its own implementation\n  // so that e.g. DevTools can display custom hook values.\n}\n\nvar updateDebugValue = mountDebugValue;\n\nfunction mountCallback(callback, deps) {\n  var hook = mountWorkInProgressHook();\n  var nextDeps = deps === undefined ? null : deps;\n  hook.memoizedState = [callback, nextDeps];\n  return callback;\n}\n\nfunction updateCallback(callback, deps) {\n  var hook = updateWorkInProgressHook();\n  var nextDeps = deps === undefined ? null : deps;\n  var prevState = hook.memoizedState;\n  if (prevState !== null) {\n    if (nextDeps !== null) {\n      var prevDeps = prevState[1];\n      if (areHookInputsEqual(nextDeps, prevDeps)) {\n        return prevState[0];\n      }\n    }\n  }\n  hook.memoizedState = [callback, nextDeps];\n  return callback;\n}\n\nfunction mountMemo(nextCreate, deps) {\n  var hook = mountWorkInProgressHook();\n  var nextDeps = deps === undefined ? null : deps;\n  var nextValue = nextCreate();\n  hook.memoizedState = [nextValue, nextDeps];\n  return nextValue;\n}\n\nfunction updateMemo(nextCreate, deps) {\n  var hook = updateWorkInProgressHook();\n  var nextDeps = deps === undefined ? null : deps;\n  var prevState = hook.memoizedState;\n  if (prevState !== null) {\n    // Assume these are defined. If they're not, areHookInputsEqual will warn.\n    if (nextDeps !== null) {\n      var prevDeps = prevState[1];\n      if (areHookInputsEqual(nextDeps, prevDeps)) {\n        return prevState[0];\n      }\n    }\n  }\n  var nextValue = nextCreate();\n  hook.memoizedState = [nextValue, nextDeps];\n  return nextValue;\n}\n\n// in a test-like environment, we want to warn if dispatchAction()\n// is called outside of a batchedUpdates/TestUtils.act(...) call.\nvar shouldWarnForUnbatchedSetState = false;\n\n{\n  // jest isn't a 'global', it's just exposed to tests via a wrapped function\n  // further, this isn't a test file, so flow doesn't recognize the symbol. So...\n  // $FlowExpectedError - because requirements don't give a damn about your type sigs.\n  if ('undefined' !== typeof jest) {\n    shouldWarnForUnbatchedSetState = true;\n  }\n}\n\nfunction dispatchAction(fiber, queue, action) {\n  !(numberOfReRenders < RE_RENDER_LIMIT) ? invariant(false, 'Too many re-renders. React limits the number of renders to prevent an infinite loop.') : void 0;\n\n  {\n    !(arguments.length <= 3) ? warning$1(false, \"State updates from the useState() and useReducer() Hooks don't support the \" + 'second callback argument. To execute a side effect after ' + 'rendering, declare it in the component body with useEffect().') : void 0;\n  }\n\n  var alternate = fiber.alternate;\n  if (fiber === currentlyRenderingFiber$1 || alternate !== null && alternate === currentlyRenderingFiber$1) {\n    // This is a render phase update. Stash it in a lazily-created map of\n    // queue -> linked list of updates. After this render pass, we'll restart\n    // and apply the stashed updates on top of the work-in-progress hook.\n    didScheduleRenderPhaseUpdate = true;\n    var update = {\n      expirationTime: renderExpirationTime,\n      action: action,\n      eagerReducer: null,\n      eagerState: null,\n      next: null\n    };\n    if (renderPhaseUpdates === null) {\n      renderPhaseUpdates = new Map();\n    }\n    var firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);\n    if (firstRenderPhaseUpdate === undefined) {\n      renderPhaseUpdates.set(queue, update);\n    } else {\n      // Append the update to the end of the list.\n      var lastRenderPhaseUpdate = firstRenderPhaseUpdate;\n      while (lastRenderPhaseUpdate.next !== null) {\n        lastRenderPhaseUpdate = lastRenderPhaseUpdate.next;\n      }\n      lastRenderPhaseUpdate.next = update;\n    }\n  } else {\n    flushPassiveEffects();\n\n    var currentTime = requestCurrentTime();\n    var _expirationTime = computeExpirationForFiber(currentTime, fiber);\n\n    var _update2 = {\n      expirationTime: _expirationTime,\n      action: action,\n      eagerReducer: null,\n      eagerState: null,\n      next: null\n    };\n\n    // Append the update to the end of the list.\n    var _last = queue.last;\n    if (_last === null) {\n      // This is the first update. Create a circular list.\n      _update2.next = _update2;\n    } else {\n      var first = _last.next;\n      if (first !== null) {\n        // Still circular.\n        _update2.next = first;\n      }\n      _last.next = _update2;\n    }\n    queue.last = _update2;\n\n    if (fiber.expirationTime === NoWork && (alternate === null || alternate.expirationTime === NoWork)) {\n      // The queue is currently empty, which means we can eagerly compute the\n      // next state before entering the render phase. If the new state is the\n      // same as the current state, we may be able to bail out entirely.\n      var _lastRenderedReducer = queue.lastRenderedReducer;\n      if (_lastRenderedReducer !== null) {\n        var prevDispatcher = void 0;\n        {\n          prevDispatcher = ReactCurrentDispatcher$1.current;\n          ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV;\n        }\n        try {\n          var currentState = queue.lastRenderedState;\n          var _eagerState = _lastRenderedReducer(currentState, action);\n          // Stash the eagerly computed state, and the reducer used to compute\n          // it, on the update object. If the reducer hasn't changed by the\n          // time we enter the render phase, then the eager state can be used\n          // without calling the reducer again.\n          _update2.eagerReducer = _lastRenderedReducer;\n          _update2.eagerState = _eagerState;\n          if (is(_eagerState, currentState)) {\n            // Fast path. We can bail out without scheduling React to re-render.\n            // It's still possible that we'll need to rebase this update later,\n            // if the component re-renders for a different reason and by that\n            // time the reducer has changed.\n            return;\n          }\n        } catch (error) {\n          // Suppress the error. It will throw again in the render phase.\n        } finally {\n          {\n            ReactCurrentDispatcher$1.current = prevDispatcher;\n          }\n        }\n      }\n    }\n    {\n      if (shouldWarnForUnbatchedSetState === true) {\n        warnIfNotCurrentlyBatchingInDev(fiber);\n      }\n    }\n    scheduleWork(fiber, _expirationTime);\n  }\n}\n\nvar ContextOnlyDispatcher = {\n  readContext: readContext,\n\n  useCallback: throwInvalidHookError,\n  useContext: throwInvalidHookError,\n  useEffect: throwInvalidHookError,\n  useImperativeHandle: throwInvalidHookError,\n  useLayoutEffect: throwInvalidHookError,\n  useMemo: throwInvalidHookError,\n  useReducer: throwInvalidHookError,\n  useRef: throwInvalidHookError,\n  useState: throwInvalidHookError,\n  useDebugValue: throwInvalidHookError\n};\n\nvar HooksDispatcherOnMountInDEV = null;\nvar HooksDispatcherOnMountWithHookTypesInDEV = null;\nvar HooksDispatcherOnUpdateInDEV = null;\nvar InvalidNestedHooksDispatcherOnMountInDEV = null;\nvar InvalidNestedHooksDispatcherOnUpdateInDEV = null;\n\n{\n  var warnInvalidContextAccess = function () {\n    warning$1(false, 'Context can only be read while React is rendering. ' + 'In classes, you can read it in the render method or getDerivedStateFromProps. ' + 'In function components, you can read it directly in the function body, but not ' + 'inside Hooks like useReducer() or useMemo().');\n  };\n\n  var warnInvalidHookAccess = function () {\n    warning$1(false, 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks. ' + 'You can only call Hooks at the top level of your React function. ' + 'For more information, see ' + 'https://fb.me/rules-of-hooks');\n  };\n\n  HooksDispatcherOnMountInDEV = {\n    readContext: function (context, observedBits) {\n      return readContext(context, observedBits);\n    },\n    useCallback: function (callback, deps) {\n      currentHookNameInDev = 'useCallback';\n      mountHookTypesDev();\n      return mountCallback(callback, deps);\n    },\n    useContext: function (context, observedBits) {\n      currentHookNameInDev = 'useContext';\n      mountHookTypesDev();\n      return readContext(context, observedBits);\n    },\n    useEffect: function (create, deps) {\n      currentHookNameInDev = 'useEffect';\n      mountHookTypesDev();\n      return mountEffect(create, deps);\n    },\n    useImperativeHandle: function (ref, create, deps) {\n      currentHookNameInDev = 'useImperativeHandle';\n      mountHookTypesDev();\n      return mountImperativeHandle(ref, create, deps);\n    },\n    useLayoutEffect: function (create, deps) {\n      currentHookNameInDev = 'useLayoutEffect';\n      mountHookTypesDev();\n      return mountLayoutEffect(create, deps);\n    },\n    useMemo: function (create, deps) {\n      currentHookNameInDev = 'useMemo';\n      mountHookTypesDev();\n      var prevDispatcher = ReactCurrentDispatcher$1.current;\n      ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnMountInDEV;\n      try {\n        return mountMemo(create, deps);\n      } finally {\n        ReactCurrentDispatcher$1.current = prevDispatcher;\n      }\n    },\n    useReducer: function (reducer, initialArg, init) {\n      currentHookNameInDev = 'useReducer';\n      mountHookTypesDev();\n      var prevDispatcher = ReactCurrentDispatcher$1.current;\n      ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnMountInDEV;\n      try {\n        return mountReducer(reducer, initialArg, init);\n      } finally {\n        ReactCurrentDispatcher$1.current = prevDispatcher;\n      }\n    },\n    useRef: function (initialValue) {\n      currentHookNameInDev = 'useRef';\n      mountHookTypesDev();\n      return mountRef(initialValue);\n    },\n    useState: function (initialState) {\n      currentHookNameInDev = 'useState';\n      mountHookTypesDev();\n      var prevDispatcher = ReactCurrentDispatcher$1.current;\n      ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnMountInDEV;\n      try {\n        return mountState(initialState);\n      } finally {\n        ReactCurrentDispatcher$1.current = prevDispatcher;\n      }\n    },\n    useDebugValue: function (value, formatterFn) {\n      currentHookNameInDev = 'useDebugValue';\n      mountHookTypesDev();\n      return mountDebugValue(value, formatterFn);\n    }\n  };\n\n  HooksDispatcherOnMountWithHookTypesInDEV = {\n    readContext: function (context, observedBits) {\n      return readContext(context, observedBits);\n    },\n    useCallback: function (callback, deps) {\n      currentHookNameInDev = 'useCallback';\n      updateHookTypesDev();\n      return mountCallback(callback, deps);\n    },\n    useContext: function (context, observedBits) {\n      currentHookNameInDev = 'useContext';\n      updateHookTypesDev();\n      return readContext(context, observedBits);\n    },\n    useEffect: function (create, deps) {\n      currentHookNameInDev = 'useEffect';\n      updateHookTypesDev();\n      return mountEffect(create, deps);\n    },\n    useImperativeHandle: function (ref, create, deps) {\n      currentHookNameInDev = 'useImperativeHandle';\n      updateHookTypesDev();\n      return mountImperativeHandle(ref, create, deps);\n    },\n    useLayoutEffect: function (create, deps) {\n      currentHookNameInDev = 'useLayoutEffect';\n      updateHookTypesDev();\n      return mountLayoutEffect(create, deps);\n    },\n    useMemo: function (create, deps) {\n      currentHookNameInDev = 'useMemo';\n      updateHookTypesDev();\n      var prevDispatcher = ReactCurrentDispatcher$1.current;\n      ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnMountInDEV;\n      try {\n        return mountMemo(create, deps);\n      } finally {\n        ReactCurrentDispatcher$1.current = prevDispatcher;\n      }\n    },\n    useReducer: function (reducer, initialArg, init) {\n      currentHookNameInDev = 'useReducer';\n      updateHookTypesDev();\n      var prevDispatcher = ReactCurrentDispatcher$1.current;\n      ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnMountInDEV;\n      try {\n        return mountReducer(reducer, initialArg, init);\n      } finally {\n        ReactCurrentDispatcher$1.current = prevDispatcher;\n      }\n    },\n    useRef: function (initialValue) {\n      currentHookNameInDev = 'useRef';\n      updateHookTypesDev();\n      return mountRef(initialValue);\n    },\n    useState: function (initialState) {\n      currentHookNameInDev = 'useState';\n      updateHookTypesDev();\n      var prevDispatcher = ReactCurrentDispatcher$1.current;\n      ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnMountInDEV;\n      try {\n        return mountState(initialState);\n      } finally {\n        ReactCurrentDispatcher$1.current = prevDispatcher;\n      }\n    },\n    useDebugValue: function (value, formatterFn) {\n      currentHookNameInDev = 'useDebugValue';\n      updateHookTypesDev();\n      return mountDebugValue(value, formatterFn);\n    }\n  };\n\n  HooksDispatcherOnUpdateInDEV = {\n    readContext: function (context, observedBits) {\n      return readContext(context, observedBits);\n    },\n    useCallback: function (callback, deps) {\n      currentHookNameInDev = 'useCallback';\n      updateHookTypesDev();\n      return updateCallback(callback, deps);\n    },\n    useContext: function (context, observedBits) {\n      currentHookNameInDev = 'useContext';\n      updateHookTypesDev();\n      return readContext(context, observedBits);\n    },\n    useEffect: function (create, deps) {\n      currentHookNameInDev = 'useEffect';\n      updateHookTypesDev();\n      return updateEffect(create, deps);\n    },\n    useImperativeHandle: function (ref, create, deps) {\n      currentHookNameInDev = 'useImperativeHandle';\n      updateHookTypesDev();\n      return updateImperativeHandle(ref, create, deps);\n    },\n    useLayoutEffect: function (create, deps) {\n      currentHookNameInDev = 'useLayoutEffect';\n      updateHookTypesDev();\n      return updateLayoutEffect(create, deps);\n    },\n    useMemo: function (create, deps) {\n      currentHookNameInDev = 'useMemo';\n      updateHookTypesDev();\n      var prevDispatcher = ReactCurrentDispatcher$1.current;\n      ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV;\n      try {\n        return updateMemo(create, deps);\n      } finally {\n        ReactCurrentDispatcher$1.current = prevDispatcher;\n      }\n    },\n    useReducer: function (reducer, initialArg, init) {\n      currentHookNameInDev = 'useReducer';\n      updateHookTypesDev();\n      var prevDispatcher = ReactCurrentDispatcher$1.current;\n      ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV;\n      try {\n        return updateReducer(reducer, initialArg, init);\n      } finally {\n        ReactCurrentDispatcher$1.current = prevDispatcher;\n      }\n    },\n    useRef: function (initialValue) {\n      currentHookNameInDev = 'useRef';\n      updateHookTypesDev();\n      return updateRef(initialValue);\n    },\n    useState: function (initialState) {\n      currentHookNameInDev = 'useState';\n      updateHookTypesDev();\n      var prevDispatcher = ReactCurrentDispatcher$1.current;\n      ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV;\n      try {\n        return updateState(initialState);\n      } finally {\n        ReactCurrentDispatcher$1.current = prevDispatcher;\n      }\n    },\n    useDebugValue: function (value, formatterFn) {\n      currentHookNameInDev = 'useDebugValue';\n      updateHookTypesDev();\n      return updateDebugValue(value, formatterFn);\n    }\n  };\n\n  InvalidNestedHooksDispatcherOnMountInDEV = {\n    readContext: function (context, observedBits) {\n      warnInvalidContextAccess();\n      return readContext(context, observedBits);\n    },\n    useCallback: function (callback, deps) {\n      currentHookNameInDev = 'useCallback';\n      warnInvalidHookAccess();\n      mountHookTypesDev();\n      return mountCallback(callback, deps);\n    },\n    useContext: function (context, observedBits) {\n      currentHookNameInDev = 'useContext';\n      warnInvalidHookAccess();\n      mountHookTypesDev();\n      return readContext(context, observedBits);\n    },\n    useEffect: function (create, deps) {\n      currentHookNameInDev = 'useEffect';\n      warnInvalidHookAccess();\n      mountHookTypesDev();\n      return mountEffect(create, deps);\n    },\n    useImperativeHandle: function (ref, create, deps) {\n      currentHookNameInDev = 'useImperativeHandle';\n      warnInvalidHookAccess();\n      mountHookTypesDev();\n      return mountImperativeHandle(ref, create, deps);\n    },\n    useLayoutEffect: function (create, deps) {\n      currentHookNameInDev = 'useLayoutEffect';\n      warnInvalidHookAccess();\n      mountHookTypesDev();\n      return mountLayoutEffect(create, deps);\n    },\n    useMemo: function (create, deps) {\n      currentHookNameInDev = 'useMemo';\n      warnInvalidHookAccess();\n      mountHookTypesDev();\n      var prevDispatcher = ReactCurrentDispatcher$1.current;\n      ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnMountInDEV;\n      try {\n        return mountMemo(create, deps);\n      } finally {\n        ReactCurrentDispatcher$1.current = prevDispatcher;\n      }\n    },\n    useReducer: function (reducer, initialArg, init) {\n      currentHookNameInDev = 'useReducer';\n      warnInvalidHookAccess();\n      mountHookTypesDev();\n      var prevDispatcher = ReactCurrentDispatcher$1.current;\n      ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnMountInDEV;\n      try {\n        return mountReducer(reducer, initialArg, init);\n      } finally {\n        ReactCurrentDispatcher$1.current = prevDispatcher;\n      }\n    },\n    useRef: function (initialValue) {\n      currentHookNameInDev = 'useRef';\n      warnInvalidHookAccess();\n      mountHookTypesDev();\n      return mountRef(initialValue);\n    },\n    useState: function (initialState) {\n      currentHookNameInDev = 'useState';\n      warnInvalidHookAccess();\n      mountHookTypesDev();\n      var prevDispatcher = ReactCurrentDispatcher$1.current;\n      ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnMountInDEV;\n      try {\n        return mountState(initialState);\n      } finally {\n        ReactCurrentDispatcher$1.current = prevDispatcher;\n      }\n    },\n    useDebugValue: function (value, formatterFn) {\n      currentHookNameInDev = 'useDebugValue';\n      warnInvalidHookAccess();\n      mountHookTypesDev();\n      return mountDebugValue(value, formatterFn);\n    }\n  };\n\n  InvalidNestedHooksDispatcherOnUpdateInDEV = {\n    readContext: function (context, observedBits) {\n      warnInvalidContextAccess();\n      return readContext(context, observedBits);\n    },\n    useCallback: function (callback, deps) {\n      currentHookNameInDev = 'useCallback';\n      warnInvalidHookAccess();\n      updateHookTypesDev();\n      return updateCallback(callback, deps);\n    },\n    useContext: function (context, observedBits) {\n      currentHookNameInDev = 'useContext';\n      warnInvalidHookAccess();\n      updateHookTypesDev();\n      return readContext(context, observedBits);\n    },\n    useEffect: function (create, deps) {\n      currentHookNameInDev = 'useEffect';\n      warnInvalidHookAccess();\n      updateHookTypesDev();\n      return updateEffect(create, deps);\n    },\n    useImperativeHandle: function (ref, create, deps) {\n      currentHookNameInDev = 'useImperativeHandle';\n      warnInvalidHookAccess();\n      updateHookTypesDev();\n      return updateImperativeHandle(ref, create, deps);\n    },\n    useLayoutEffect: function (create, deps) {\n      currentHookNameInDev = 'useLayoutEffect';\n      warnInvalidHookAccess();\n      updateHookTypesDev();\n      return updateLayoutEffect(create, deps);\n    },\n    useMemo: function (create, deps) {\n      currentHookNameInDev = 'useMemo';\n      warnInvalidHookAccess();\n      updateHookTypesDev();\n      var prevDispatcher = ReactCurrentDispatcher$1.current;\n      ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV;\n      try {\n        return updateMemo(create, deps);\n      } finally {\n        ReactCurrentDispatcher$1.current = prevDispatcher;\n      }\n    },\n    useReducer: function (reducer, initialArg, init) {\n      currentHookNameInDev = 'useReducer';\n      warnInvalidHookAccess();\n      updateHookTypesDev();\n      var prevDispatcher = ReactCurrentDispatcher$1.current;\n      ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV;\n      try {\n        return updateReducer(reducer, initialArg, init);\n      } finally {\n        ReactCurrentDispatcher$1.current = prevDispatcher;\n      }\n    },\n    useRef: function (initialValue) {\n      currentHookNameInDev = 'useRef';\n      warnInvalidHookAccess();\n      updateHookTypesDev();\n      return updateRef(initialValue);\n    },\n    useState: function (initialState) {\n      currentHookNameInDev = 'useState';\n      warnInvalidHookAccess();\n      updateHookTypesDev();\n      var prevDispatcher = ReactCurrentDispatcher$1.current;\n      ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV;\n      try {\n        return updateState(initialState);\n      } finally {\n        ReactCurrentDispatcher$1.current = prevDispatcher;\n      }\n    },\n    useDebugValue: function (value, formatterFn) {\n      currentHookNameInDev = 'useDebugValue';\n      warnInvalidHookAccess();\n      updateHookTypesDev();\n      return updateDebugValue(value, formatterFn);\n    }\n  };\n}\n\nvar commitTime = 0;\nvar profilerStartTime = -1;\n\nfunction getCommitTime() {\n  return commitTime;\n}\n\nfunction recordCommitTime() {\n  if (!enableProfilerTimer) {\n    return;\n  }\n  commitTime = unstable_now();\n}\n\nfunction startProfilerTimer(fiber) {\n  if (!enableProfilerTimer) {\n    return;\n  }\n\n  profilerStartTime = unstable_now();\n\n  if (fiber.actualStartTime < 0) {\n    fiber.actualStartTime = unstable_now();\n  }\n}\n\nfunction stopProfilerTimerIfRunning(fiber) {\n  if (!enableProfilerTimer) {\n    return;\n  }\n  profilerStartTime = -1;\n}\n\nfunction stopProfilerTimerIfRunningAndRecordDelta(fiber, overrideBaseTime) {\n  if (!enableProfilerTimer) {\n    return;\n  }\n\n  if (profilerStartTime >= 0) {\n    var elapsedTime = unstable_now() - profilerStartTime;\n    fiber.actualDuration += elapsedTime;\n    if (overrideBaseTime) {\n      fiber.selfBaseDuration = elapsedTime;\n    }\n    profilerStartTime = -1;\n  }\n}\n\n// The deepest Fiber on the stack involved in a hydration context.\n// This may have been an insertion or a hydration.\nvar hydrationParentFiber = null;\nvar nextHydratableInstance = null;\nvar isHydrating = false;\n\nfunction enterHydrationState(fiber) {\n  if (!supportsHydration) {\n    return false;\n  }\n\n  var parentInstance = fiber.stateNode.containerInfo;\n  nextHydratableInstance = getFirstHydratableChild(parentInstance);\n  hydrationParentFiber = fiber;\n  isHydrating = true;\n  return true;\n}\n\nfunction reenterHydrationStateFromDehydratedSuspenseInstance(fiber) {\n  if (!supportsHydration) {\n    return false;\n  }\n\n  var suspenseInstance = fiber.stateNode;\n  nextHydratableInstance = getNextHydratableSibling(suspenseInstance);\n  popToNextHostParent(fiber);\n  isHydrating = true;\n  return true;\n}\n\nfunction deleteHydratableInstance(returnFiber, instance) {\n  {\n    switch (returnFiber.tag) {\n      case HostRoot:\n        didNotHydrateContainerInstance(returnFiber.stateNode.containerInfo, instance);\n        break;\n      case HostComponent:\n        didNotHydrateInstance(returnFiber.type, returnFiber.memoizedProps, returnFiber.stateNode, instance);\n        break;\n    }\n  }\n\n  var childToDelete = createFiberFromHostInstanceForDeletion();\n  childToDelete.stateNode = instance;\n  childToDelete.return = returnFiber;\n  childToDelete.effectTag = Deletion;\n\n  // This might seem like it belongs on progressedFirstDeletion. However,\n  // these children are not part of the reconciliation list of children.\n  // Even if we abort and rereconcile the children, that will try to hydrate\n  // again and the nodes are still in the host tree so these will be\n  // recreated.\n  if (returnFiber.lastEffect !== null) {\n    returnFiber.lastEffect.nextEffect = childToDelete;\n    returnFiber.lastEffect = childToDelete;\n  } else {\n    returnFiber.firstEffect = returnFiber.lastEffect = childToDelete;\n  }\n}\n\nfunction insertNonHydratedInstance(returnFiber, fiber) {\n  fiber.effectTag |= Placement;\n  {\n    switch (returnFiber.tag) {\n      case HostRoot:\n        {\n          var parentContainer = returnFiber.stateNode.containerInfo;\n          switch (fiber.tag) {\n            case HostComponent:\n              var type = fiber.type;\n              var props = fiber.pendingProps;\n              didNotFindHydratableContainerInstance(parentContainer, type, props);\n              break;\n            case HostText:\n              var text = fiber.pendingProps;\n              didNotFindHydratableContainerTextInstance(parentContainer, text);\n              break;\n            case SuspenseComponent:\n              \n              break;\n          }\n          break;\n        }\n      case HostComponent:\n        {\n          var parentType = returnFiber.type;\n          var parentProps = returnFiber.memoizedProps;\n          var parentInstance = returnFiber.stateNode;\n          switch (fiber.tag) {\n            case HostComponent:\n              var _type = fiber.type;\n              var _props = fiber.pendingProps;\n              didNotFindHydratableInstance(parentType, parentProps, parentInstance, _type, _props);\n              break;\n            case HostText:\n              var _text = fiber.pendingProps;\n              didNotFindHydratableTextInstance(parentType, parentProps, parentInstance, _text);\n              break;\n            case SuspenseComponent:\n              didNotFindHydratableSuspenseInstance(parentType, parentProps, parentInstance);\n              break;\n          }\n          break;\n        }\n      default:\n        return;\n    }\n  }\n}\n\nfunction tryHydrate(fiber, nextInstance) {\n  switch (fiber.tag) {\n    case HostComponent:\n      {\n        var type = fiber.type;\n        var props = fiber.pendingProps;\n        var instance = canHydrateInstance(nextInstance, type, props);\n        if (instance !== null) {\n          fiber.stateNode = instance;\n          return true;\n        }\n        return false;\n      }\n    case HostText:\n      {\n        var text = fiber.pendingProps;\n        var textInstance = canHydrateTextInstance(nextInstance, text);\n        if (textInstance !== null) {\n          fiber.stateNode = textInstance;\n          return true;\n        }\n        return false;\n      }\n    case SuspenseComponent:\n      {\n        if (enableSuspenseServerRenderer) {\n          var suspenseInstance = canHydrateSuspenseInstance(nextInstance);\n          if (suspenseInstance !== null) {\n            // Downgrade the tag to a dehydrated component until we've hydrated it.\n            fiber.tag = DehydratedSuspenseComponent;\n            fiber.stateNode = suspenseInstance;\n            return true;\n          }\n        }\n        return false;\n      }\n    default:\n      return false;\n  }\n}\n\nfunction tryToClaimNextHydratableInstance(fiber) {\n  if (!isHydrating) {\n    return;\n  }\n  var nextInstance = nextHydratableInstance;\n  if (!nextInstance) {\n    // Nothing to hydrate. Make it an insertion.\n    insertNonHydratedInstance(hydrationParentFiber, fiber);\n    isHydrating = false;\n    hydrationParentFiber = fiber;\n    return;\n  }\n  var firstAttemptedInstance = nextInstance;\n  if (!tryHydrate(fiber, nextInstance)) {\n    // If we can't hydrate this instance let's try the next one.\n    // We use this as a heuristic. It's based on intuition and not data so it\n    // might be flawed or unnecessary.\n    nextInstance = getNextHydratableSibling(firstAttemptedInstance);\n    if (!nextInstance || !tryHydrate(fiber, nextInstance)) {\n      // Nothing to hydrate. Make it an insertion.\n      insertNonHydratedInstance(hydrationParentFiber, fiber);\n      isHydrating = false;\n      hydrationParentFiber = fiber;\n      return;\n    }\n    // We matched the next one, we'll now assume that the first one was\n    // superfluous and we'll delete it. Since we can't eagerly delete it\n    // we'll have to schedule a deletion. To do that, this node needs a dummy\n    // fiber associated with it.\n    deleteHydratableInstance(hydrationParentFiber, firstAttemptedInstance);\n  }\n  hydrationParentFiber = fiber;\n  nextHydratableInstance = getFirstHydratableChild(nextInstance);\n}\n\nfunction prepareToHydrateHostInstance(fiber, rootContainerInstance, hostContext) {\n  if (!supportsHydration) {\n    invariant(false, 'Expected prepareToHydrateHostInstance() to never be called. This error is likely caused by a bug in React. Please file an issue.');\n  }\n\n  var instance = fiber.stateNode;\n  var updatePayload = hydrateInstance(instance, fiber.type, fiber.memoizedProps, rootContainerInstance, hostContext, fiber);\n  // TODO: Type this specific to this type of component.\n  fiber.updateQueue = updatePayload;\n  // If the update payload indicates that there is a change or if there\n  // is a new ref we mark this as an update.\n  if (updatePayload !== null) {\n    return true;\n  }\n  return false;\n}\n\nfunction prepareToHydrateHostTextInstance(fiber) {\n  if (!supportsHydration) {\n    invariant(false, 'Expected prepareToHydrateHostTextInstance() to never be called. This error is likely caused by a bug in React. Please file an issue.');\n  }\n\n  var textInstance = fiber.stateNode;\n  var textContent = fiber.memoizedProps;\n  var shouldUpdate = hydrateTextInstance(textInstance, textContent, fiber);\n  {\n    if (shouldUpdate) {\n      // We assume that prepareToHydrateHostTextInstance is called in a context where the\n      // hydration parent is the parent host component of this host text.\n      var returnFiber = hydrationParentFiber;\n      if (returnFiber !== null) {\n        switch (returnFiber.tag) {\n          case HostRoot:\n            {\n              var parentContainer = returnFiber.stateNode.containerInfo;\n              didNotMatchHydratedContainerTextInstance(parentContainer, textInstance, textContent);\n              break;\n            }\n          case HostComponent:\n            {\n              var parentType = returnFiber.type;\n              var parentProps = returnFiber.memoizedProps;\n              var parentInstance = returnFiber.stateNode;\n              didNotMatchHydratedTextInstance(parentType, parentProps, parentInstance, textInstance, textContent);\n              break;\n            }\n        }\n      }\n    }\n  }\n  return shouldUpdate;\n}\n\nfunction skipPastDehydratedSuspenseInstance(fiber) {\n  if (!supportsHydration) {\n    invariant(false, 'Expected skipPastDehydratedSuspenseInstance() to never be called. This error is likely caused by a bug in React. Please file an issue.');\n  }\n  var suspenseInstance = fiber.stateNode;\n  !suspenseInstance ? invariant(false, 'Expected to have a hydrated suspense instance. This error is likely caused by a bug in React. Please file an issue.') : void 0;\n  nextHydratableInstance = getNextHydratableInstanceAfterSuspenseInstance(suspenseInstance);\n}\n\nfunction popToNextHostParent(fiber) {\n  var parent = fiber.return;\n  while (parent !== null && parent.tag !== HostComponent && parent.tag !== HostRoot && parent.tag !== DehydratedSuspenseComponent) {\n    parent = parent.return;\n  }\n  hydrationParentFiber = parent;\n}\n\nfunction popHydrationState(fiber) {\n  if (!supportsHydration) {\n    return false;\n  }\n  if (fiber !== hydrationParentFiber) {\n    // We're deeper than the current hydration context, inside an inserted\n    // tree.\n    return false;\n  }\n  if (!isHydrating) {\n    // If we're not currently hydrating but we're in a hydration context, then\n    // we were an insertion and now need to pop up reenter hydration of our\n    // siblings.\n    popToNextHostParent(fiber);\n    isHydrating = true;\n    return false;\n  }\n\n  var type = fiber.type;\n\n  // If we have any remaining hydratable nodes, we need to delete them now.\n  // We only do this deeper than head and body since they tend to have random\n  // other nodes in them. We also ignore components with pure text content in\n  // side of them.\n  // TODO: Better heuristic.\n  if (fiber.tag !== HostComponent || type !== 'head' && type !== 'body' && !shouldSetTextContent(type, fiber.memoizedProps)) {\n    var nextInstance = nextHydratableInstance;\n    while (nextInstance) {\n      deleteHydratableInstance(fiber, nextInstance);\n      nextInstance = getNextHydratableSibling(nextInstance);\n    }\n  }\n\n  popToNextHostParent(fiber);\n  nextHydratableInstance = hydrationParentFiber ? getNextHydratableSibling(fiber.stateNode) : null;\n  return true;\n}\n\nfunction resetHydrationState() {\n  if (!supportsHydration) {\n    return;\n  }\n\n  hydrationParentFiber = null;\n  nextHydratableInstance = null;\n  isHydrating = false;\n}\n\nvar ReactCurrentOwner$3 = ReactSharedInternals.ReactCurrentOwner;\n\nvar didReceiveUpdate = false;\n\nvar didWarnAboutBadClass = void 0;\nvar didWarnAboutContextTypeOnFunctionComponent = void 0;\nvar didWarnAboutGetDerivedStateOnFunctionComponent = void 0;\nvar didWarnAboutFunctionRefs = void 0;\nvar didWarnAboutReassigningProps = void 0;\n\n{\n  didWarnAboutBadClass = {};\n  didWarnAboutContextTypeOnFunctionComponent = {};\n  didWarnAboutGetDerivedStateOnFunctionComponent = {};\n  didWarnAboutFunctionRefs = {};\n  didWarnAboutReassigningProps = false;\n}\n\nfunction reconcileChildren(current$$1, workInProgress, nextChildren, renderExpirationTime) {\n  if (current$$1 === null) {\n    // If this is a fresh new component that hasn't been rendered yet, we\n    // won't update its child set by applying minimal side-effects. Instead,\n    // we will add them all to the child before it gets rendered. That means\n    // we can optimize this reconciliation pass by not tracking side-effects.\n    workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderExpirationTime);\n  } else {\n    // If the current child is the same as the work in progress, it means that\n    // we haven't yet started any work on these children. Therefore, we use\n    // the clone algorithm to create a copy of all the current children.\n\n    // If we had any progressed work already, that is invalid at this point so\n    // let's throw it out.\n    workInProgress.child = reconcileChildFibers(workInProgress, current$$1.child, nextChildren, renderExpirationTime);\n  }\n}\n\nfunction forceUnmountCurrentAndReconcile(current$$1, workInProgress, nextChildren, renderExpirationTime) {\n  // This function is fork of reconcileChildren. It's used in cases where we\n  // want to reconcile without matching against the existing set. This has the\n  // effect of all current children being unmounted; even if the type and key\n  // are the same, the old child is unmounted and a new child is created.\n  //\n  // To do this, we're going to go through the reconcile algorithm twice. In\n  // the first pass, we schedule a deletion for all the current children by\n  // passing null.\n  workInProgress.child = reconcileChildFibers(workInProgress, current$$1.child, null, renderExpirationTime);\n  // In the second pass, we mount the new children. The trick here is that we\n  // pass null in place of where we usually pass the current child set. This has\n  // the effect of remounting all children regardless of whether their their\n  // identity matches.\n  workInProgress.child = reconcileChildFibers(workInProgress, null, nextChildren, renderExpirationTime);\n}\n\nfunction updateForwardRef(current$$1, workInProgress, Component, nextProps, renderExpirationTime) {\n  // TODO: current can be non-null here even if the component\n  // hasn't yet mounted. This happens after the first render suspends.\n  // We'll need to figure out if this is fine or can cause issues.\n\n  {\n    if (workInProgress.type !== workInProgress.elementType) {\n      // Lazy component props can't be validated in createElement\n      // because they're only guaranteed to be resolved here.\n      var innerPropTypes = Component.propTypes;\n      if (innerPropTypes) {\n        checkPropTypes_1(innerPropTypes, nextProps, // Resolved props\n        'prop', getComponentName(Component), getCurrentFiberStackInDev);\n      }\n    }\n  }\n\n  var render = Component.render;\n  var ref = workInProgress.ref;\n\n  // The rest is a fork of updateFunctionComponent\n  var nextChildren = void 0;\n  prepareToReadContext(workInProgress, renderExpirationTime);\n  {\n    ReactCurrentOwner$3.current = workInProgress;\n    setCurrentPhase('render');\n    nextChildren = renderWithHooks(current$$1, workInProgress, render, nextProps, ref, renderExpirationTime);\n    if (debugRenderPhaseSideEffects || debugRenderPhaseSideEffectsForStrictMode && workInProgress.mode & StrictMode) {\n      // Only double-render components with Hooks\n      if (workInProgress.memoizedState !== null) {\n        nextChildren = renderWithHooks(current$$1, workInProgress, render, nextProps, ref, renderExpirationTime);\n      }\n    }\n    setCurrentPhase(null);\n  }\n\n  if (current$$1 !== null && !didReceiveUpdate) {\n    bailoutHooks(current$$1, workInProgress, renderExpirationTime);\n    return bailoutOnAlreadyFinishedWork(current$$1, workInProgress, renderExpirationTime);\n  }\n\n  // React DevTools reads this flag.\n  workInProgress.effectTag |= PerformedWork;\n  reconcileChildren(current$$1, workInProgress, nextChildren, renderExpirationTime);\n  return workInProgress.child;\n}\n\nfunction updateMemoComponent(current$$1, workInProgress, Component, nextProps, updateExpirationTime, renderExpirationTime) {\n  if (current$$1 === null) {\n    var type = Component.type;\n    if (isSimpleFunctionComponent(type) && Component.compare === null &&\n    // SimpleMemoComponent codepath doesn't resolve outer props either.\n    Component.defaultProps === undefined) {\n      // If this is a plain function component without default props,\n      // and with only the default shallow comparison, we upgrade it\n      // to a SimpleMemoComponent to allow fast path updates.\n      workInProgress.tag = SimpleMemoComponent;\n      workInProgress.type = type;\n      {\n        validateFunctionComponentInDev(workInProgress, type);\n      }\n      return updateSimpleMemoComponent(current$$1, workInProgress, type, nextProps, updateExpirationTime, renderExpirationTime);\n    }\n    {\n      var innerPropTypes = type.propTypes;\n      if (innerPropTypes) {\n        // Inner memo component props aren't currently validated in createElement.\n        // We could move it there, but we'd still need this for lazy code path.\n        checkPropTypes_1(innerPropTypes, nextProps, // Resolved props\n        'prop', getComponentName(type), getCurrentFiberStackInDev);\n      }\n    }\n    var child = createFiberFromTypeAndProps(Component.type, null, nextProps, null, workInProgress.mode, renderExpirationTime);\n    child.ref = workInProgress.ref;\n    child.return = workInProgress;\n    workInProgress.child = child;\n    return child;\n  }\n  {\n    var _type = Component.type;\n    var _innerPropTypes = _type.propTypes;\n    if (_innerPropTypes) {\n      // Inner memo component props aren't currently validated in createElement.\n      // We could move it there, but we'd still need this for lazy code path.\n      checkPropTypes_1(_innerPropTypes, nextProps, // Resolved props\n      'prop', getComponentName(_type), getCurrentFiberStackInDev);\n    }\n  }\n  var currentChild = current$$1.child; // This is always exactly one child\n  if (updateExpirationTime < renderExpirationTime) {\n    // This will be the props with resolved defaultProps,\n    // unlike current.memoizedProps which will be the unresolved ones.\n    var prevProps = currentChild.memoizedProps;\n    // Default to shallow comparison\n    var compare = Component.compare;\n    compare = compare !== null ? compare : shallowEqual;\n    if (compare(prevProps, nextProps) && current$$1.ref === workInProgress.ref) {\n      return bailoutOnAlreadyFinishedWork(current$$1, workInProgress, renderExpirationTime);\n    }\n  }\n  // React DevTools reads this flag.\n  workInProgress.effectTag |= PerformedWork;\n  var newChild = createWorkInProgress(currentChild, nextProps, renderExpirationTime);\n  newChild.ref = workInProgress.ref;\n  newChild.return = workInProgress;\n  workInProgress.child = newChild;\n  return newChild;\n}\n\nfunction updateSimpleMemoComponent(current$$1, workInProgress, Component, nextProps, updateExpirationTime, renderExpirationTime) {\n  // TODO: current can be non-null here even if the component\n  // hasn't yet mounted. This happens when the inner render suspends.\n  // We'll need to figure out if this is fine or can cause issues.\n\n  {\n    if (workInProgress.type !== workInProgress.elementType) {\n      // Lazy component props can't be validated in createElement\n      // because they're only guaranteed to be resolved here.\n      var outerMemoType = workInProgress.elementType;\n      if (outerMemoType.$$typeof === REACT_LAZY_TYPE) {\n        // We warn when you define propTypes on lazy()\n        // so let's just skip over it to find memo() outer wrapper.\n        // Inner props for memo are validated later.\n        outerMemoType = refineResolvedLazyComponent(outerMemoType);\n      }\n      var outerPropTypes = outerMemoType && outerMemoType.propTypes;\n      if (outerPropTypes) {\n        checkPropTypes_1(outerPropTypes, nextProps, // Resolved (SimpleMemoComponent has no defaultProps)\n        'prop', getComponentName(outerMemoType), getCurrentFiberStackInDev);\n      }\n      // Inner propTypes will be validated in the function component path.\n    }\n  }\n  if (current$$1 !== null) {\n    var prevProps = current$$1.memoizedProps;\n    if (shallowEqual(prevProps, nextProps) && current$$1.ref === workInProgress.ref) {\n      didReceiveUpdate = false;\n      if (updateExpirationTime < renderExpirationTime) {\n        return bailoutOnAlreadyFinishedWork(current$$1, workInProgress, renderExpirationTime);\n      }\n    }\n  }\n  return updateFunctionComponent(current$$1, workInProgress, Component, nextProps, renderExpirationTime);\n}\n\nfunction updateFragment(current$$1, workInProgress, renderExpirationTime) {\n  var nextChildren = workInProgress.pendingProps;\n  reconcileChildren(current$$1, workInProgress, nextChildren, renderExpirationTime);\n  return workInProgress.child;\n}\n\nfunction updateMode(current$$1, workInProgress, renderExpirationTime) {\n  var nextChildren = workInProgress.pendingProps.children;\n  reconcileChildren(current$$1, workInProgress, nextChildren, renderExpirationTime);\n  return workInProgress.child;\n}\n\nfunction updateProfiler(current$$1, workInProgress, renderExpirationTime) {\n  if (enableProfilerTimer) {\n    workInProgress.effectTag |= Update;\n  }\n  var nextProps = workInProgress.pendingProps;\n  var nextChildren = nextProps.children;\n  reconcileChildren(current$$1, workInProgress, nextChildren, renderExpirationTime);\n  return workInProgress.child;\n}\n\nfunction markRef(current$$1, workInProgress) {\n  var ref = workInProgress.ref;\n  if (current$$1 === null && ref !== null || current$$1 !== null && current$$1.ref !== ref) {\n    // Schedule a Ref effect\n    workInProgress.effectTag |= Ref;\n  }\n}\n\nfunction updateFunctionComponent(current$$1, workInProgress, Component, nextProps, renderExpirationTime) {\n  {\n    if (workInProgress.type !== workInProgress.elementType) {\n      // Lazy component props can't be validated in createElement\n      // because they're only guaranteed to be resolved here.\n      var innerPropTypes = Component.propTypes;\n      if (innerPropTypes) {\n        checkPropTypes_1(innerPropTypes, nextProps, // Resolved props\n        'prop', getComponentName(Component), getCurrentFiberStackInDev);\n      }\n    }\n  }\n\n  var unmaskedContext = getUnmaskedContext(workInProgress, Component, true);\n  var context = getMaskedContext(workInProgress, unmaskedContext);\n\n  var nextChildren = void 0;\n  prepareToReadContext(workInProgress, renderExpirationTime);\n  {\n    ReactCurrentOwner$3.current = workInProgress;\n    setCurrentPhase('render');\n    nextChildren = renderWithHooks(current$$1, workInProgress, Component, nextProps, context, renderExpirationTime);\n    if (debugRenderPhaseSideEffects || debugRenderPhaseSideEffectsForStrictMode && workInProgress.mode & StrictMode) {\n      // Only double-render components with Hooks\n      if (workInProgress.memoizedState !== null) {\n        nextChildren = renderWithHooks(current$$1, workInProgress, Component, nextProps, context, renderExpirationTime);\n      }\n    }\n    setCurrentPhase(null);\n  }\n\n  if (current$$1 !== null && !didReceiveUpdate) {\n    bailoutHooks(current$$1, workInProgress, renderExpirationTime);\n    return bailoutOnAlreadyFinishedWork(current$$1, workInProgress, renderExpirationTime);\n  }\n\n  // React DevTools reads this flag.\n  workInProgress.effectTag |= PerformedWork;\n  reconcileChildren(current$$1, workInProgress, nextChildren, renderExpirationTime);\n  return workInProgress.child;\n}\n\nfunction updateClassComponent(current$$1, workInProgress, Component, nextProps, renderExpirationTime) {\n  {\n    if (workInProgress.type !== workInProgress.elementType) {\n      // Lazy component props can't be validated in createElement\n      // because they're only guaranteed to be resolved here.\n      var innerPropTypes = Component.propTypes;\n      if (innerPropTypes) {\n        checkPropTypes_1(innerPropTypes, nextProps, // Resolved props\n        'prop', getComponentName(Component), getCurrentFiberStackInDev);\n      }\n    }\n  }\n\n  // Push context providers early to prevent context stack mismatches.\n  // During mounting we don't know the child context yet as the instance doesn't exist.\n  // We will invalidate the child context in finishClassComponent() right after rendering.\n  var hasContext = void 0;\n  if (isContextProvider(Component)) {\n    hasContext = true;\n    pushContextProvider(workInProgress);\n  } else {\n    hasContext = false;\n  }\n  prepareToReadContext(workInProgress, renderExpirationTime);\n\n  var instance = workInProgress.stateNode;\n  var shouldUpdate = void 0;\n  if (instance === null) {\n    if (current$$1 !== null) {\n      // An class component without an instance only mounts if it suspended\n      // inside a non- concurrent tree, in an inconsistent state. We want to\n      // tree it like a new mount, even though an empty version of it already\n      // committed. Disconnect the alternate pointers.\n      current$$1.alternate = null;\n      workInProgress.alternate = null;\n      // Since this is conceptually a new fiber, schedule a Placement effect\n      workInProgress.effectTag |= Placement;\n    }\n    // In the initial pass we might need to construct the instance.\n    constructClassInstance(workInProgress, Component, nextProps, renderExpirationTime);\n    mountClassInstance(workInProgress, Component, nextProps, renderExpirationTime);\n    shouldUpdate = true;\n  } else if (current$$1 === null) {\n    // In a resume, we'll already have an instance we can reuse.\n    shouldUpdate = resumeMountClassInstance(workInProgress, Component, nextProps, renderExpirationTime);\n  } else {\n    shouldUpdate = updateClassInstance(current$$1, workInProgress, Component, nextProps, renderExpirationTime);\n  }\n  var nextUnitOfWork = finishClassComponent(current$$1, workInProgress, Component, shouldUpdate, hasContext, renderExpirationTime);\n  {\n    var inst = workInProgress.stateNode;\n    if (inst.props !== nextProps) {\n      !didWarnAboutReassigningProps ? warning$1(false, 'It looks like %s is reassigning its own `this.props` while rendering. ' + 'This is not supported and can lead to confusing bugs.', getComponentName(workInProgress.type) || 'a component') : void 0;\n      didWarnAboutReassigningProps = true;\n    }\n  }\n  return nextUnitOfWork;\n}\n\nfunction finishClassComponent(current$$1, workInProgress, Component, shouldUpdate, hasContext, renderExpirationTime) {\n  // Refs should update even if shouldComponentUpdate returns false\n  markRef(current$$1, workInProgress);\n\n  var didCaptureError = (workInProgress.effectTag & DidCapture) !== NoEffect;\n\n  if (!shouldUpdate && !didCaptureError) {\n    // Context providers should defer to sCU for rendering\n    if (hasContext) {\n      invalidateContextProvider(workInProgress, Component, false);\n    }\n\n    return bailoutOnAlreadyFinishedWork(current$$1, workInProgress, renderExpirationTime);\n  }\n\n  var instance = workInProgress.stateNode;\n\n  // Rerender\n  ReactCurrentOwner$3.current = workInProgress;\n  var nextChildren = void 0;\n  if (didCaptureError && typeof Component.getDerivedStateFromError !== 'function') {\n    // If we captured an error, but getDerivedStateFrom catch is not defined,\n    // unmount all the children. componentDidCatch will schedule an update to\n    // re-render a fallback. This is temporary until we migrate everyone to\n    // the new API.\n    // TODO: Warn in a future release.\n    nextChildren = null;\n\n    if (enableProfilerTimer) {\n      stopProfilerTimerIfRunning(workInProgress);\n    }\n  } else {\n    {\n      setCurrentPhase('render');\n      nextChildren = instance.render();\n      if (debugRenderPhaseSideEffects || debugRenderPhaseSideEffectsForStrictMode && workInProgress.mode & StrictMode) {\n        instance.render();\n      }\n      setCurrentPhase(null);\n    }\n  }\n\n  // React DevTools reads this flag.\n  workInProgress.effectTag |= PerformedWork;\n  if (current$$1 !== null && didCaptureError) {\n    // If we're recovering from an error, reconcile without reusing any of\n    // the existing children. Conceptually, the normal children and the children\n    // that are shown on error are two different sets, so we shouldn't reuse\n    // normal children even if their identities match.\n    forceUnmountCurrentAndReconcile(current$$1, workInProgress, nextChildren, renderExpirationTime);\n  } else {\n    reconcileChildren(current$$1, workInProgress, nextChildren, renderExpirationTime);\n  }\n\n  // Memoize state using the values we just used to render.\n  // TODO: Restructure so we never read values from the instance.\n  workInProgress.memoizedState = instance.state;\n\n  // The context might have changed so we need to recalculate it.\n  if (hasContext) {\n    invalidateContextProvider(workInProgress, Component, true);\n  }\n\n  return workInProgress.child;\n}\n\nfunction pushHostRootContext(workInProgress) {\n  var root = workInProgress.stateNode;\n  if (root.pendingContext) {\n    pushTopLevelContextObject(workInProgress, root.pendingContext, root.pendingContext !== root.context);\n  } else if (root.context) {\n    // Should always be set\n    pushTopLevelContextObject(workInProgress, root.context, false);\n  }\n  pushHostContainer(workInProgress, root.containerInfo);\n}\n\nfunction updateHostRoot(current$$1, workInProgress, renderExpirationTime) {\n  pushHostRootContext(workInProgress);\n  var updateQueue = workInProgress.updateQueue;\n  !(updateQueue !== null) ? invariant(false, 'If the root does not have an updateQueue, we should have already bailed out. This error is likely caused by a bug in React. Please file an issue.') : void 0;\n  var nextProps = workInProgress.pendingProps;\n  var prevState = workInProgress.memoizedState;\n  var prevChildren = prevState !== null ? prevState.element : null;\n  processUpdateQueue(workInProgress, updateQueue, nextProps, null, renderExpirationTime);\n  var nextState = workInProgress.memoizedState;\n  // Caution: React DevTools currently depends on this property\n  // being called \"element\".\n  var nextChildren = nextState.element;\n  if (nextChildren === prevChildren) {\n    // If the state is the same as before, that's a bailout because we had\n    // no work that expires at this time.\n    resetHydrationState();\n    return bailoutOnAlreadyFinishedWork(current$$1, workInProgress, renderExpirationTime);\n  }\n  var root = workInProgress.stateNode;\n  if ((current$$1 === null || current$$1.child === null) && root.hydrate && enterHydrationState(workInProgress)) {\n    // If we don't have any current children this might be the first pass.\n    // We always try to hydrate. If this isn't a hydration pass there won't\n    // be any children to hydrate which is effectively the same thing as\n    // not hydrating.\n\n    // This is a bit of a hack. We track the host root as a placement to\n    // know that we're currently in a mounting state. That way isMounted\n    // works as expected. We must reset this before committing.\n    // TODO: Delete this when we delete isMounted and findDOMNode.\n    workInProgress.effectTag |= Placement;\n\n    // Ensure that children mount into this root without tracking\n    // side-effects. This ensures that we don't store Placement effects on\n    // nodes that will be hydrated.\n    workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderExpirationTime);\n  } else {\n    // Otherwise reset hydration state in case we aborted and resumed another\n    // root.\n    reconcileChildren(current$$1, workInProgress, nextChildren, renderExpirationTime);\n    resetHydrationState();\n  }\n  return workInProgress.child;\n}\n\nfunction updateHostComponent(current$$1, workInProgress, renderExpirationTime) {\n  pushHostContext(workInProgress);\n\n  if (current$$1 === null) {\n    tryToClaimNextHydratableInstance(workInProgress);\n  }\n\n  var type = workInProgress.type;\n  var nextProps = workInProgress.pendingProps;\n  var prevProps = current$$1 !== null ? current$$1.memoizedProps : null;\n\n  var nextChildren = nextProps.children;\n  var isDirectTextChild = shouldSetTextContent(type, nextProps);\n\n  if (isDirectTextChild) {\n    // We special case a direct text child of a host node. This is a common\n    // case. We won't handle it as a reified child. We will instead handle\n    // this in the host environment that also have access to this prop. That\n    // avoids allocating another HostText fiber and traversing it.\n    nextChildren = null;\n  } else if (prevProps !== null && shouldSetTextContent(type, prevProps)) {\n    // If we're switching from a direct text child to a normal child, or to\n    // empty, we need to schedule the text content to be reset.\n    workInProgress.effectTag |= ContentReset;\n  }\n\n  markRef(current$$1, workInProgress);\n\n  // Check the host config to see if the children are offscreen/hidden.\n  if (renderExpirationTime !== Never && workInProgress.mode & ConcurrentMode && shouldDeprioritizeSubtree(type, nextProps)) {\n    // Schedule this fiber to re-render at offscreen priority. Then bailout.\n    workInProgress.expirationTime = workInProgress.childExpirationTime = Never;\n    return null;\n  }\n\n  reconcileChildren(current$$1, workInProgress, nextChildren, renderExpirationTime);\n  return workInProgress.child;\n}\n\nfunction updateHostText(current$$1, workInProgress) {\n  if (current$$1 === null) {\n    tryToClaimNextHydratableInstance(workInProgress);\n  }\n  // Nothing to do here. This is terminal. We'll do the completion step\n  // immediately after.\n  return null;\n}\n\nfunction mountLazyComponent(_current, workInProgress, elementType, updateExpirationTime, renderExpirationTime) {\n  if (_current !== null) {\n    // An lazy component only mounts if it suspended inside a non-\n    // concurrent tree, in an inconsistent state. We want to treat it like\n    // a new mount, even though an empty version of it already committed.\n    // Disconnect the alternate pointers.\n    _current.alternate = null;\n    workInProgress.alternate = null;\n    // Since this is conceptually a new fiber, schedule a Placement effect\n    workInProgress.effectTag |= Placement;\n  }\n\n  var props = workInProgress.pendingProps;\n  // We can't start a User Timing measurement with correct label yet.\n  // Cancel and resume right after we know the tag.\n  cancelWorkTimer(workInProgress);\n  var Component = readLazyComponentType(elementType);\n  // Store the unwrapped component in the type.\n  workInProgress.type = Component;\n  var resolvedTag = workInProgress.tag = resolveLazyComponentTag(Component);\n  startWorkTimer(workInProgress);\n  var resolvedProps = resolveDefaultProps(Component, props);\n  var child = void 0;\n  switch (resolvedTag) {\n    case FunctionComponent:\n      {\n        {\n          validateFunctionComponentInDev(workInProgress, Component);\n        }\n        child = updateFunctionComponent(null, workInProgress, Component, resolvedProps, renderExpirationTime);\n        break;\n      }\n    case ClassComponent:\n      {\n        child = updateClassComponent(null, workInProgress, Component, resolvedProps, renderExpirationTime);\n        break;\n      }\n    case ForwardRef:\n      {\n        child = updateForwardRef(null, workInProgress, Component, resolvedProps, renderExpirationTime);\n        break;\n      }\n    case MemoComponent:\n      {\n        {\n          if (workInProgress.type !== workInProgress.elementType) {\n            var outerPropTypes = Component.propTypes;\n            if (outerPropTypes) {\n              checkPropTypes_1(outerPropTypes, resolvedProps, // Resolved for outer only\n              'prop', getComponentName(Component), getCurrentFiberStackInDev);\n            }\n          }\n        }\n        child = updateMemoComponent(null, workInProgress, Component, resolveDefaultProps(Component.type, resolvedProps), // The inner type can have defaults too\n        updateExpirationTime, renderExpirationTime);\n        break;\n      }\n    default:\n      {\n        var hint = '';\n        {\n          if (Component !== null && typeof Component === 'object' && Component.$$typeof === REACT_LAZY_TYPE) {\n            hint = ' Did you wrap a component in React.lazy() more than once?';\n          }\n        }\n        // This message intentionally doesn't mention ForwardRef or MemoComponent\n        // because the fact that it's a separate type of work is an\n        // implementation detail.\n        invariant(false, 'Element type is invalid. Received a promise that resolves to: %s. Lazy element type must resolve to a class or function.%s', Component, hint);\n      }\n  }\n  return child;\n}\n\nfunction mountIncompleteClassComponent(_current, workInProgress, Component, nextProps, renderExpirationTime) {\n  if (_current !== null) {\n    // An incomplete component only mounts if it suspended inside a non-\n    // concurrent tree, in an inconsistent state. We want to treat it like\n    // a new mount, even though an empty version of it already committed.\n    // Disconnect the alternate pointers.\n    _current.alternate = null;\n    workInProgress.alternate = null;\n    // Since this is conceptually a new fiber, schedule a Placement effect\n    workInProgress.effectTag |= Placement;\n  }\n\n  // Promote the fiber to a class and try rendering again.\n  workInProgress.tag = ClassComponent;\n\n  // The rest of this function is a fork of `updateClassComponent`\n\n  // Push context providers early to prevent context stack mismatches.\n  // During mounting we don't know the child context yet as the instance doesn't exist.\n  // We will invalidate the child context in finishClassComponent() right after rendering.\n  var hasContext = void 0;\n  if (isContextProvider(Component)) {\n    hasContext = true;\n    pushContextProvider(workInProgress);\n  } else {\n    hasContext = false;\n  }\n  prepareToReadContext(workInProgress, renderExpirationTime);\n\n  constructClassInstance(workInProgress, Component, nextProps, renderExpirationTime);\n  mountClassInstance(workInProgress, Component, nextProps, renderExpirationTime);\n\n  return finishClassComponent(null, workInProgress, Component, true, hasContext, renderExpirationTime);\n}\n\nfunction mountIndeterminateComponent(_current, workInProgress, Component, renderExpirationTime) {\n  if (_current !== null) {\n    // An indeterminate component only mounts if it suspended inside a non-\n    // concurrent tree, in an inconsistent state. We want to treat it like\n    // a new mount, even though an empty version of it already committed.\n    // Disconnect the alternate pointers.\n    _current.alternate = null;\n    workInProgress.alternate = null;\n    // Since this is conceptually a new fiber, schedule a Placement effect\n    workInProgress.effectTag |= Placement;\n  }\n\n  var props = workInProgress.pendingProps;\n  var unmaskedContext = getUnmaskedContext(workInProgress, Component, false);\n  var context = getMaskedContext(workInProgress, unmaskedContext);\n\n  prepareToReadContext(workInProgress, renderExpirationTime);\n\n  var value = void 0;\n\n  {\n    if (Component.prototype && typeof Component.prototype.render === 'function') {\n      var componentName = getComponentName(Component) || 'Unknown';\n\n      if (!didWarnAboutBadClass[componentName]) {\n        warningWithoutStack$1(false, \"The <%s /> component appears to have a render method, but doesn't extend React.Component. \" + 'This is likely to cause errors. Change %s to extend React.Component instead.', componentName, componentName);\n        didWarnAboutBadClass[componentName] = true;\n      }\n    }\n\n    if (workInProgress.mode & StrictMode) {\n      ReactStrictModeWarnings.recordLegacyContextWarning(workInProgress, null);\n    }\n\n    ReactCurrentOwner$3.current = workInProgress;\n    value = renderWithHooks(null, workInProgress, Component, props, context, renderExpirationTime);\n  }\n  // React DevTools reads this flag.\n  workInProgress.effectTag |= PerformedWork;\n\n  if (typeof value === 'object' && value !== null && typeof value.render === 'function' && value.$$typeof === undefined) {\n    // Proceed under the assumption that this is a class instance\n    workInProgress.tag = ClassComponent;\n\n    // Throw out any hooks that were used.\n    resetHooks();\n\n    // Push context providers early to prevent context stack mismatches.\n    // During mounting we don't know the child context yet as the instance doesn't exist.\n    // We will invalidate the child context in finishClassComponent() right after rendering.\n    var hasContext = false;\n    if (isContextProvider(Component)) {\n      hasContext = true;\n      pushContextProvider(workInProgress);\n    } else {\n      hasContext = false;\n    }\n\n    workInProgress.memoizedState = value.state !== null && value.state !== undefined ? value.state : null;\n\n    var getDerivedStateFromProps = Component.getDerivedStateFromProps;\n    if (typeof getDerivedStateFromProps === 'function') {\n      applyDerivedStateFromProps(workInProgress, Component, getDerivedStateFromProps, props);\n    }\n\n    adoptClassInstance(workInProgress, value);\n    mountClassInstance(workInProgress, Component, props, renderExpirationTime);\n    return finishClassComponent(null, workInProgress, Component, true, hasContext, renderExpirationTime);\n  } else {\n    // Proceed under the assumption that this is a function component\n    workInProgress.tag = FunctionComponent;\n    {\n      if (debugRenderPhaseSideEffects || debugRenderPhaseSideEffectsForStrictMode && workInProgress.mode & StrictMode) {\n        // Only double-render components with Hooks\n        if (workInProgress.memoizedState !== null) {\n          value = renderWithHooks(null, workInProgress, Component, props, context, renderExpirationTime);\n        }\n      }\n    }\n    reconcileChildren(null, workInProgress, value, renderExpirationTime);\n    {\n      validateFunctionComponentInDev(workInProgress, Component);\n    }\n    return workInProgress.child;\n  }\n}\n\nfunction validateFunctionComponentInDev(workInProgress, Component) {\n  if (Component) {\n    !!Component.childContextTypes ? warningWithoutStack$1(false, '%s(...): childContextTypes cannot be defined on a function component.', Component.displayName || Component.name || 'Component') : void 0;\n  }\n  if (workInProgress.ref !== null) {\n    var info = '';\n    var ownerName = getCurrentFiberOwnerNameInDevOrNull();\n    if (ownerName) {\n      info += '\\n\\nCheck the render method of `' + ownerName + '`.';\n    }\n\n    var warningKey = ownerName || workInProgress._debugID || '';\n    var debugSource = workInProgress._debugSource;\n    if (debugSource) {\n      warningKey = debugSource.fileName + ':' + debugSource.lineNumber;\n    }\n    if (!didWarnAboutFunctionRefs[warningKey]) {\n      didWarnAboutFunctionRefs[warningKey] = true;\n      warning$1(false, 'Function components cannot be given refs. ' + 'Attempts to access this ref will fail. ' + 'Did you mean to use React.forwardRef()?%s', info);\n    }\n  }\n\n  if (typeof Component.getDerivedStateFromProps === 'function') {\n    var componentName = getComponentName(Component) || 'Unknown';\n\n    if (!didWarnAboutGetDerivedStateOnFunctionComponent[componentName]) {\n      warningWithoutStack$1(false, '%s: Function components do not support getDerivedStateFromProps.', componentName);\n      didWarnAboutGetDerivedStateOnFunctionComponent[componentName] = true;\n    }\n  }\n\n  if (typeof Component.contextType === 'object' && Component.contextType !== null) {\n    var _componentName = getComponentName(Component) || 'Unknown';\n\n    if (!didWarnAboutContextTypeOnFunctionComponent[_componentName]) {\n      warningWithoutStack$1(false, '%s: Function components do not support contextType.', _componentName);\n      didWarnAboutContextTypeOnFunctionComponent[_componentName] = true;\n    }\n  }\n}\n\nfunction updateSuspenseComponent(current$$1, workInProgress, renderExpirationTime) {\n  var mode = workInProgress.mode;\n  var nextProps = workInProgress.pendingProps;\n\n  // We should attempt to render the primary children unless this boundary\n  // already suspended during this render (`alreadyCaptured` is true).\n  var nextState = workInProgress.memoizedState;\n\n  var nextDidTimeout = void 0;\n  if ((workInProgress.effectTag & DidCapture) === NoEffect) {\n    // This is the first attempt.\n    nextState = null;\n    nextDidTimeout = false;\n  } else {\n    // Something in this boundary's subtree already suspended. Switch to\n    // rendering the fallback children.\n    nextState = {\n      timedOutAt: nextState !== null ? nextState.timedOutAt : NoWork\n    };\n    nextDidTimeout = true;\n    workInProgress.effectTag &= ~DidCapture;\n  }\n\n  // This next part is a bit confusing. If the children timeout, we switch to\n  // showing the fallback children in place of the \"primary\" children.\n  // However, we don't want to delete the primary children because then their\n  // state will be lost (both the React state and the host state, e.g.\n  // uncontrolled form inputs). Instead we keep them mounted and hide them.\n  // Both the fallback children AND the primary children are rendered at the\n  // same time. Once the primary children are un-suspended, we can delete\n  // the fallback children — don't need to preserve their state.\n  //\n  // The two sets of children are siblings in the host environment, but\n  // semantically, for purposes of reconciliation, they are two separate sets.\n  // So we store them using two fragment fibers.\n  //\n  // However, we want to avoid allocating extra fibers for every placeholder.\n  // They're only necessary when the children time out, because that's the\n  // only time when both sets are mounted.\n  //\n  // So, the extra fragment fibers are only used if the children time out.\n  // Otherwise, we render the primary children directly. This requires some\n  // custom reconciliation logic to preserve the state of the primary\n  // children. It's essentially a very basic form of re-parenting.\n\n  // `child` points to the child fiber. In the normal case, this is the first\n  // fiber of the primary children set. In the timed-out case, it's a\n  // a fragment fiber containing the primary children.\n  var child = void 0;\n  // `next` points to the next fiber React should render. In the normal case,\n  // it's the same as `child`: the first fiber of the primary children set.\n  // In the timed-out case, it's a fragment fiber containing the *fallback*\n  // children -- we skip over the primary children entirely.\n  var next = void 0;\n  if (current$$1 === null) {\n    if (enableSuspenseServerRenderer) {\n      // If we're currently hydrating, try to hydrate this boundary.\n      // But only if this has a fallback.\n      if (nextProps.fallback !== undefined) {\n        tryToClaimNextHydratableInstance(workInProgress);\n        // This could've changed the tag if this was a dehydrated suspense component.\n        if (workInProgress.tag === DehydratedSuspenseComponent) {\n          return updateDehydratedSuspenseComponent(null, workInProgress, renderExpirationTime);\n        }\n      }\n    }\n\n    // This is the initial mount. This branch is pretty simple because there's\n    // no previous state that needs to be preserved.\n    if (nextDidTimeout) {\n      // Mount separate fragments for primary and fallback children.\n      var nextFallbackChildren = nextProps.fallback;\n      var primaryChildFragment = createFiberFromFragment(null, mode, NoWork, null);\n\n      if ((workInProgress.mode & ConcurrentMode) === NoContext) {\n        // Outside of concurrent mode, we commit the effects from the\n        var progressedState = workInProgress.memoizedState;\n        var progressedPrimaryChild = progressedState !== null ? workInProgress.child.child : workInProgress.child;\n        primaryChildFragment.child = progressedPrimaryChild;\n      }\n\n      var fallbackChildFragment = createFiberFromFragment(nextFallbackChildren, mode, renderExpirationTime, null);\n      primaryChildFragment.sibling = fallbackChildFragment;\n      child = primaryChildFragment;\n      // Skip the primary children, and continue working on the\n      // fallback children.\n      next = fallbackChildFragment;\n      child.return = next.return = workInProgress;\n    } else {\n      // Mount the primary children without an intermediate fragment fiber.\n      var nextPrimaryChildren = nextProps.children;\n      child = next = mountChildFibers(workInProgress, null, nextPrimaryChildren, renderExpirationTime);\n    }\n  } else {\n    // This is an update. This branch is more complicated because we need to\n    // ensure the state of the primary children is preserved.\n    var prevState = current$$1.memoizedState;\n    var prevDidTimeout = prevState !== null;\n    if (prevDidTimeout) {\n      // The current tree already timed out. That means each child set is\n      var currentPrimaryChildFragment = current$$1.child;\n      var currentFallbackChildFragment = currentPrimaryChildFragment.sibling;\n      if (nextDidTimeout) {\n        // Still timed out. Reuse the current primary children by cloning\n        // its fragment. We're going to skip over these entirely.\n        var _nextFallbackChildren = nextProps.fallback;\n        var _primaryChildFragment = createWorkInProgress(currentPrimaryChildFragment, currentPrimaryChildFragment.pendingProps, NoWork);\n\n        if ((workInProgress.mode & ConcurrentMode) === NoContext) {\n          // Outside of concurrent mode, we commit the effects from the\n          var _progressedState = workInProgress.memoizedState;\n          var _progressedPrimaryChild = _progressedState !== null ? workInProgress.child.child : workInProgress.child;\n          if (_progressedPrimaryChild !== currentPrimaryChildFragment.child) {\n            _primaryChildFragment.child = _progressedPrimaryChild;\n          }\n        }\n\n        // Because primaryChildFragment is a new fiber that we're inserting as the\n        // parent of a new tree, we need to set its treeBaseDuration.\n        if (enableProfilerTimer && workInProgress.mode & ProfileMode) {\n          // treeBaseDuration is the sum of all the child tree base durations.\n          var treeBaseDuration = 0;\n          var hiddenChild = _primaryChildFragment.child;\n          while (hiddenChild !== null) {\n            treeBaseDuration += hiddenChild.treeBaseDuration;\n            hiddenChild = hiddenChild.sibling;\n          }\n          _primaryChildFragment.treeBaseDuration = treeBaseDuration;\n        }\n\n        // Clone the fallback child fragment, too. These we'll continue\n        // working on.\n        var _fallbackChildFragment = _primaryChildFragment.sibling = createWorkInProgress(currentFallbackChildFragment, _nextFallbackChildren, currentFallbackChildFragment.expirationTime);\n        child = _primaryChildFragment;\n        _primaryChildFragment.childExpirationTime = NoWork;\n        // Skip the primary children, and continue working on the\n        // fallback children.\n        next = _fallbackChildFragment;\n        child.return = next.return = workInProgress;\n      } else {\n        // No longer suspended. Switch back to showing the primary children,\n        // and remove the intermediate fragment fiber.\n        var _nextPrimaryChildren = nextProps.children;\n        var currentPrimaryChild = currentPrimaryChildFragment.child;\n        var primaryChild = reconcileChildFibers(workInProgress, currentPrimaryChild, _nextPrimaryChildren, renderExpirationTime);\n\n        // If this render doesn't suspend, we need to delete the fallback\n        // children. Wait until the complete phase, after we've confirmed the\n        // fallback is no longer needed.\n        // TODO: Would it be better to store the fallback fragment on\n        // the stateNode?\n\n        // Continue rendering the children, like we normally do.\n        child = next = primaryChild;\n      }\n    } else {\n      // The current tree has not already timed out. That means the primary\n      // children are not wrapped in a fragment fiber.\n      var _currentPrimaryChild = current$$1.child;\n      if (nextDidTimeout) {\n        // Timed out. Wrap the children in a fragment fiber to keep them\n        // separate from the fallback children.\n        var _nextFallbackChildren2 = nextProps.fallback;\n        var _primaryChildFragment2 = createFiberFromFragment(\n        // It shouldn't matter what the pending props are because we aren't\n        // going to render this fragment.\n        null, mode, NoWork, null);\n        _primaryChildFragment2.child = _currentPrimaryChild;\n\n        // Even though we're creating a new fiber, there are no new children,\n        // because we're reusing an already mounted tree. So we don't need to\n        // schedule a placement.\n        // primaryChildFragment.effectTag |= Placement;\n\n        if ((workInProgress.mode & ConcurrentMode) === NoContext) {\n          // Outside of concurrent mode, we commit the effects from the\n          var _progressedState2 = workInProgress.memoizedState;\n          var _progressedPrimaryChild2 = _progressedState2 !== null ? workInProgress.child.child : workInProgress.child;\n          _primaryChildFragment2.child = _progressedPrimaryChild2;\n        }\n\n        // Because primaryChildFragment is a new fiber that we're inserting as the\n        // parent of a new tree, we need to set its treeBaseDuration.\n        if (enableProfilerTimer && workInProgress.mode & ProfileMode) {\n          // treeBaseDuration is the sum of all the child tree base durations.\n          var _treeBaseDuration = 0;\n          var _hiddenChild = _primaryChildFragment2.child;\n          while (_hiddenChild !== null) {\n            _treeBaseDuration += _hiddenChild.treeBaseDuration;\n            _hiddenChild = _hiddenChild.sibling;\n          }\n          _primaryChildFragment2.treeBaseDuration = _treeBaseDuration;\n        }\n\n        // Create a fragment from the fallback children, too.\n        var _fallbackChildFragment2 = _primaryChildFragment2.sibling = createFiberFromFragment(_nextFallbackChildren2, mode, renderExpirationTime, null);\n        _fallbackChildFragment2.effectTag |= Placement;\n        child = _primaryChildFragment2;\n        _primaryChildFragment2.childExpirationTime = NoWork;\n        // Skip the primary children, and continue working on the\n        // fallback children.\n        next = _fallbackChildFragment2;\n        child.return = next.return = workInProgress;\n      } else {\n        // Still haven't timed out.  Continue rendering the children, like we\n        // normally do.\n        var _nextPrimaryChildren2 = nextProps.children;\n        next = child = reconcileChildFibers(workInProgress, _currentPrimaryChild, _nextPrimaryChildren2, renderExpirationTime);\n      }\n    }\n    workInProgress.stateNode = current$$1.stateNode;\n  }\n\n  workInProgress.memoizedState = nextState;\n  workInProgress.child = child;\n  return next;\n}\n\nfunction updateDehydratedSuspenseComponent(current$$1, workInProgress, renderExpirationTime) {\n  if (current$$1 === null) {\n    // During the first pass, we'll bail out and not drill into the children.\n    // Instead, we'll leave the content in place and try to hydrate it later.\n    workInProgress.expirationTime = Never;\n    return null;\n  }\n  // We use childExpirationTime to indicate that a child might depend on context, so if\n  // any context has changed, we need to treat is as if the input might have changed.\n  var hasContextChanged$$1 = current$$1.childExpirationTime >= renderExpirationTime;\n  if (didReceiveUpdate || hasContextChanged$$1) {\n    // This boundary has changed since the first render. This means that we are now unable to\n    // hydrate it. We might still be able to hydrate it using an earlier expiration time but\n    // during this render we can't. Instead, we're going to delete the whole subtree and\n    // instead inject a new real Suspense boundary to take its place, which may render content\n    // or fallback. The real Suspense boundary will suspend for a while so we have some time\n    // to ensure it can produce real content, but all state and pending events will be lost.\n\n    // Detach from the current dehydrated boundary.\n    current$$1.alternate = null;\n    workInProgress.alternate = null;\n\n    // Insert a deletion in the effect list.\n    var returnFiber = workInProgress.return;\n    !(returnFiber !== null) ? invariant(false, 'Suspense boundaries are never on the root. This is probably a bug in React.') : void 0;\n    var last = returnFiber.lastEffect;\n    if (last !== null) {\n      last.nextEffect = current$$1;\n      returnFiber.lastEffect = current$$1;\n    } else {\n      returnFiber.firstEffect = returnFiber.lastEffect = current$$1;\n    }\n    current$$1.nextEffect = null;\n    current$$1.effectTag = Deletion;\n\n    // Upgrade this work in progress to a real Suspense component.\n    workInProgress.tag = SuspenseComponent;\n    workInProgress.stateNode = null;\n    workInProgress.memoizedState = null;\n    // This is now an insertion.\n    workInProgress.effectTag |= Placement;\n    // Retry as a real Suspense component.\n    return updateSuspenseComponent(null, workInProgress, renderExpirationTime);\n  }\n  if ((workInProgress.effectTag & DidCapture) === NoEffect) {\n    // This is the first attempt.\n    reenterHydrationStateFromDehydratedSuspenseInstance(workInProgress);\n    var nextProps = workInProgress.pendingProps;\n    var nextChildren = nextProps.children;\n    workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderExpirationTime);\n    return workInProgress.child;\n  } else {\n    // Something suspended. Leave the existing children in place.\n    // TODO: In non-concurrent mode, should we commit the nodes we have hydrated so far?\n    workInProgress.child = null;\n    return null;\n  }\n}\n\nfunction updatePortalComponent(current$$1, workInProgress, renderExpirationTime) {\n  pushHostContainer(workInProgress, workInProgress.stateNode.containerInfo);\n  var nextChildren = workInProgress.pendingProps;\n  if (current$$1 === null) {\n    // Portals are special because we don't append the children during mount\n    // but at commit. Therefore we need to track insertions which the normal\n    // flow doesn't do during mount. This doesn't happen at the root because\n    // the root always starts with a \"current\" with a null child.\n    // TODO: Consider unifying this with how the root works.\n    workInProgress.child = reconcileChildFibers(workInProgress, null, nextChildren, renderExpirationTime);\n  } else {\n    reconcileChildren(current$$1, workInProgress, nextChildren, renderExpirationTime);\n  }\n  return workInProgress.child;\n}\n\nfunction updateContextProvider(current$$1, workInProgress, renderExpirationTime) {\n  var providerType = workInProgress.type;\n  var context = providerType._context;\n\n  var newProps = workInProgress.pendingProps;\n  var oldProps = workInProgress.memoizedProps;\n\n  var newValue = newProps.value;\n\n  {\n    var providerPropTypes = workInProgress.type.propTypes;\n\n    if (providerPropTypes) {\n      checkPropTypes_1(providerPropTypes, newProps, 'prop', 'Context.Provider', getCurrentFiberStackInDev);\n    }\n  }\n\n  pushProvider(workInProgress, newValue);\n\n  if (oldProps !== null) {\n    var oldValue = oldProps.value;\n    var changedBits = calculateChangedBits(context, newValue, oldValue);\n    if (changedBits === 0) {\n      // No change. Bailout early if children are the same.\n      if (oldProps.children === newProps.children && !hasContextChanged()) {\n        return bailoutOnAlreadyFinishedWork(current$$1, workInProgress, renderExpirationTime);\n      }\n    } else {\n      // The context value changed. Search for matching consumers and schedule\n      // them to update.\n      propagateContextChange(workInProgress, context, changedBits, renderExpirationTime);\n    }\n  }\n\n  var newChildren = newProps.children;\n  reconcileChildren(current$$1, workInProgress, newChildren, renderExpirationTime);\n  return workInProgress.child;\n}\n\nvar hasWarnedAboutUsingContextAsConsumer = false;\n\nfunction updateContextConsumer(current$$1, workInProgress, renderExpirationTime) {\n  var context = workInProgress.type;\n  // The logic below for Context differs depending on PROD or DEV mode. In\n  // DEV mode, we create a separate object for Context.Consumer that acts\n  // like a proxy to Context. This proxy object adds unnecessary code in PROD\n  // so we use the old behaviour (Context.Consumer references Context) to\n  // reduce size and overhead. The separate object references context via\n  // a property called \"_context\", which also gives us the ability to check\n  // in DEV mode if this property exists or not and warn if it does not.\n  {\n    if (context._context === undefined) {\n      // This may be because it's a Context (rather than a Consumer).\n      // Or it may be because it's older React where they're the same thing.\n      // We only want to warn if we're sure it's a new React.\n      if (context !== context.Consumer) {\n        if (!hasWarnedAboutUsingContextAsConsumer) {\n          hasWarnedAboutUsingContextAsConsumer = true;\n          warning$1(false, 'Rendering <Context> directly is not supported and will be removed in ' + 'a future major release. Did you mean to render <Context.Consumer> instead?');\n        }\n      }\n    } else {\n      context = context._context;\n    }\n  }\n  var newProps = workInProgress.pendingProps;\n  var render = newProps.children;\n\n  {\n    !(typeof render === 'function') ? warningWithoutStack$1(false, 'A context consumer was rendered with multiple children, or a child ' + \"that isn't a function. A context consumer expects a single child \" + 'that is a function. If you did pass a function, make sure there ' + 'is no trailing or leading whitespace around it.') : void 0;\n  }\n\n  prepareToReadContext(workInProgress, renderExpirationTime);\n  var newValue = readContext(context, newProps.unstable_observedBits);\n  var newChildren = void 0;\n  {\n    ReactCurrentOwner$3.current = workInProgress;\n    setCurrentPhase('render');\n    newChildren = render(newValue);\n    setCurrentPhase(null);\n  }\n\n  // React DevTools reads this flag.\n  workInProgress.effectTag |= PerformedWork;\n  reconcileChildren(current$$1, workInProgress, newChildren, renderExpirationTime);\n  return workInProgress.child;\n}\n\nfunction markWorkInProgressReceivedUpdate() {\n  didReceiveUpdate = true;\n}\n\nfunction bailoutOnAlreadyFinishedWork(current$$1, workInProgress, renderExpirationTime) {\n  cancelWorkTimer(workInProgress);\n\n  if (current$$1 !== null) {\n    // Reuse previous context list\n    workInProgress.contextDependencies = current$$1.contextDependencies;\n  }\n\n  if (enableProfilerTimer) {\n    // Don't update \"base\" render times for bailouts.\n    stopProfilerTimerIfRunning(workInProgress);\n  }\n\n  // Check if the children have any pending work.\n  var childExpirationTime = workInProgress.childExpirationTime;\n  if (childExpirationTime < renderExpirationTime) {\n    // The children don't have any work either. We can skip them.\n    // TODO: Once we add back resuming, we should check if the children are\n    // a work-in-progress set. If so, we need to transfer their effects.\n    return null;\n  } else {\n    // This fiber doesn't have work, but its subtree does. Clone the child\n    // fibers and continue.\n    cloneChildFibers(current$$1, workInProgress);\n    return workInProgress.child;\n  }\n}\n\nfunction beginWork(current$$1, workInProgress, renderExpirationTime) {\n  var updateExpirationTime = workInProgress.expirationTime;\n\n  if (current$$1 !== null) {\n    var oldProps = current$$1.memoizedProps;\n    var newProps = workInProgress.pendingProps;\n\n    if (oldProps !== newProps || hasContextChanged()) {\n      // If props or context changed, mark the fiber as having performed work.\n      // This may be unset if the props are determined to be equal later (memo).\n      didReceiveUpdate = true;\n    } else if (updateExpirationTime < renderExpirationTime) {\n      didReceiveUpdate = false;\n      // This fiber does not have any pending work. Bailout without entering\n      // the begin phase. There's still some bookkeeping we that needs to be done\n      // in this optimized path, mostly pushing stuff onto the stack.\n      switch (workInProgress.tag) {\n        case HostRoot:\n          pushHostRootContext(workInProgress);\n          resetHydrationState();\n          break;\n        case HostComponent:\n          pushHostContext(workInProgress);\n          break;\n        case ClassComponent:\n          {\n            var Component = workInProgress.type;\n            if (isContextProvider(Component)) {\n              pushContextProvider(workInProgress);\n            }\n            break;\n          }\n        case HostPortal:\n          pushHostContainer(workInProgress, workInProgress.stateNode.containerInfo);\n          break;\n        case ContextProvider:\n          {\n            var newValue = workInProgress.memoizedProps.value;\n            pushProvider(workInProgress, newValue);\n            break;\n          }\n        case Profiler:\n          if (enableProfilerTimer) {\n            workInProgress.effectTag |= Update;\n          }\n          break;\n        case SuspenseComponent:\n          {\n            var state = workInProgress.memoizedState;\n            var didTimeout = state !== null;\n            if (didTimeout) {\n              // If this boundary is currently timed out, we need to decide\n              // whether to retry the primary children, or to skip over it and\n              // go straight to the fallback. Check the priority of the primary\n              var primaryChildFragment = workInProgress.child;\n              var primaryChildExpirationTime = primaryChildFragment.childExpirationTime;\n              if (primaryChildExpirationTime !== NoWork && primaryChildExpirationTime >= renderExpirationTime) {\n                // The primary children have pending work. Use the normal path\n                // to attempt to render the primary children again.\n                return updateSuspenseComponent(current$$1, workInProgress, renderExpirationTime);\n              } else {\n                // The primary children do not have pending work with sufficient\n                // priority. Bailout.\n                var child = bailoutOnAlreadyFinishedWork(current$$1, workInProgress, renderExpirationTime);\n                if (child !== null) {\n                  // The fallback children have pending work. Skip over the\n                  // primary children and work on the fallback.\n                  return child.sibling;\n                } else {\n                  return null;\n                }\n              }\n            }\n            break;\n          }\n        case DehydratedSuspenseComponent:\n          {\n            if (enableSuspenseServerRenderer) {\n              // We know that this component will suspend again because if it has\n              // been unsuspended it has committed as a regular Suspense component.\n              // If it needs to be retried, it should have work scheduled on it.\n              workInProgress.effectTag |= DidCapture;\n              break;\n            }\n          }\n      }\n      return bailoutOnAlreadyFinishedWork(current$$1, workInProgress, renderExpirationTime);\n    }\n  } else {\n    didReceiveUpdate = false;\n  }\n\n  // Before entering the begin phase, clear the expiration time.\n  workInProgress.expirationTime = NoWork;\n\n  switch (workInProgress.tag) {\n    case IndeterminateComponent:\n      {\n        var elementType = workInProgress.elementType;\n        return mountIndeterminateComponent(current$$1, workInProgress, elementType, renderExpirationTime);\n      }\n    case LazyComponent:\n      {\n        var _elementType = workInProgress.elementType;\n        return mountLazyComponent(current$$1, workInProgress, _elementType, updateExpirationTime, renderExpirationTime);\n      }\n    case FunctionComponent:\n      {\n        var _Component = workInProgress.type;\n        var unresolvedProps = workInProgress.pendingProps;\n        var resolvedProps = workInProgress.elementType === _Component ? unresolvedProps : resolveDefaultProps(_Component, unresolvedProps);\n        return updateFunctionComponent(current$$1, workInProgress, _Component, resolvedProps, renderExpirationTime);\n      }\n    case ClassComponent:\n      {\n        var _Component2 = workInProgress.type;\n        var _unresolvedProps = workInProgress.pendingProps;\n        var _resolvedProps = workInProgress.elementType === _Component2 ? _unresolvedProps : resolveDefaultProps(_Component2, _unresolvedProps);\n        return updateClassComponent(current$$1, workInProgress, _Component2, _resolvedProps, renderExpirationTime);\n      }\n    case HostRoot:\n      return updateHostRoot(current$$1, workInProgress, renderExpirationTime);\n    case HostComponent:\n      return updateHostComponent(current$$1, workInProgress, renderExpirationTime);\n    case HostText:\n      return updateHostText(current$$1, workInProgress);\n    case SuspenseComponent:\n      return updateSuspenseComponent(current$$1, workInProgress, renderExpirationTime);\n    case HostPortal:\n      return updatePortalComponent(current$$1, workInProgress, renderExpirationTime);\n    case ForwardRef:\n      {\n        var type = workInProgress.type;\n        var _unresolvedProps2 = workInProgress.pendingProps;\n        var _resolvedProps2 = workInProgress.elementType === type ? _unresolvedProps2 : resolveDefaultProps(type, _unresolvedProps2);\n        return updateForwardRef(current$$1, workInProgress, type, _resolvedProps2, renderExpirationTime);\n      }\n    case Fragment:\n      return updateFragment(current$$1, workInProgress, renderExpirationTime);\n    case Mode:\n      return updateMode(current$$1, workInProgress, renderExpirationTime);\n    case Profiler:\n      return updateProfiler(current$$1, workInProgress, renderExpirationTime);\n    case ContextProvider:\n      return updateContextProvider(current$$1, workInProgress, renderExpirationTime);\n    case ContextConsumer:\n      return updateContextConsumer(current$$1, workInProgress, renderExpirationTime);\n    case MemoComponent:\n      {\n        var _type2 = workInProgress.type;\n        var _unresolvedProps3 = workInProgress.pendingProps;\n        // Resolve outer props first, then resolve inner props.\n        var _resolvedProps3 = resolveDefaultProps(_type2, _unresolvedProps3);\n        {\n          if (workInProgress.type !== workInProgress.elementType) {\n            var outerPropTypes = _type2.propTypes;\n            if (outerPropTypes) {\n              checkPropTypes_1(outerPropTypes, _resolvedProps3, // Resolved for outer only\n              'prop', getComponentName(_type2), getCurrentFiberStackInDev);\n            }\n          }\n        }\n        _resolvedProps3 = resolveDefaultProps(_type2.type, _resolvedProps3);\n        return updateMemoComponent(current$$1, workInProgress, _type2, _resolvedProps3, updateExpirationTime, renderExpirationTime);\n      }\n    case SimpleMemoComponent:\n      {\n        return updateSimpleMemoComponent(current$$1, workInProgress, workInProgress.type, workInProgress.pendingProps, updateExpirationTime, renderExpirationTime);\n      }\n    case IncompleteClassComponent:\n      {\n        var _Component3 = workInProgress.type;\n        var _unresolvedProps4 = workInProgress.pendingProps;\n        var _resolvedProps4 = workInProgress.elementType === _Component3 ? _unresolvedProps4 : resolveDefaultProps(_Component3, _unresolvedProps4);\n        return mountIncompleteClassComponent(current$$1, workInProgress, _Component3, _resolvedProps4, renderExpirationTime);\n      }\n    case DehydratedSuspenseComponent:\n      {\n        if (enableSuspenseServerRenderer) {\n          return updateDehydratedSuspenseComponent(current$$1, workInProgress, renderExpirationTime);\n        }\n        break;\n      }\n  }\n  invariant(false, 'Unknown unit of work tag. This error is likely caused by a bug in React. Please file an issue.');\n}\n\nvar valueCursor = createCursor(null);\n\nvar rendererSigil = void 0;\n{\n  // Use this to detect multiple renderers using the same context\n  rendererSigil = {};\n}\n\nvar currentlyRenderingFiber = null;\nvar lastContextDependency = null;\nvar lastContextWithAllBitsObserved = null;\n\nvar isDisallowedContextReadInDEV = false;\n\nfunction resetContextDependences() {\n  // This is called right before React yields execution, to ensure `readContext`\n  // cannot be called outside the render phase.\n  currentlyRenderingFiber = null;\n  lastContextDependency = null;\n  lastContextWithAllBitsObserved = null;\n  {\n    isDisallowedContextReadInDEV = false;\n  }\n}\n\nfunction enterDisallowedContextReadInDEV() {\n  {\n    isDisallowedContextReadInDEV = true;\n  }\n}\n\nfunction exitDisallowedContextReadInDEV() {\n  {\n    isDisallowedContextReadInDEV = false;\n  }\n}\n\nfunction pushProvider(providerFiber, nextValue) {\n  var context = providerFiber.type._context;\n\n  if (isPrimaryRenderer) {\n    push(valueCursor, context._currentValue, providerFiber);\n\n    context._currentValue = nextValue;\n    {\n      !(context._currentRenderer === undefined || context._currentRenderer === null || context._currentRenderer === rendererSigil) ? warningWithoutStack$1(false, 'Detected multiple renderers concurrently rendering the ' + 'same context provider. This is currently unsupported.') : void 0;\n      context._currentRenderer = rendererSigil;\n    }\n  } else {\n    push(valueCursor, context._currentValue2, providerFiber);\n\n    context._currentValue2 = nextValue;\n    {\n      !(context._currentRenderer2 === undefined || context._currentRenderer2 === null || context._currentRenderer2 === rendererSigil) ? warningWithoutStack$1(false, 'Detected multiple renderers concurrently rendering the ' + 'same context provider. This is currently unsupported.') : void 0;\n      context._currentRenderer2 = rendererSigil;\n    }\n  }\n}\n\nfunction popProvider(providerFiber) {\n  var currentValue = valueCursor.current;\n\n  pop(valueCursor, providerFiber);\n\n  var context = providerFiber.type._context;\n  if (isPrimaryRenderer) {\n    context._currentValue = currentValue;\n  } else {\n    context._currentValue2 = currentValue;\n  }\n}\n\nfunction calculateChangedBits(context, newValue, oldValue) {\n  if (is(oldValue, newValue)) {\n    // No change\n    return 0;\n  } else {\n    var changedBits = typeof context._calculateChangedBits === 'function' ? context._calculateChangedBits(oldValue, newValue) : maxSigned31BitInt;\n\n    {\n      !((changedBits & maxSigned31BitInt) === changedBits) ? warning$1(false, 'calculateChangedBits: Expected the return value to be a ' + '31-bit integer. Instead received: %s', changedBits) : void 0;\n    }\n    return changedBits | 0;\n  }\n}\n\nfunction scheduleWorkOnParentPath(parent, renderExpirationTime) {\n  // Update the child expiration time of all the ancestors, including\n  // the alternates.\n  var node = parent;\n  while (node !== null) {\n    var alternate = node.alternate;\n    if (node.childExpirationTime < renderExpirationTime) {\n      node.childExpirationTime = renderExpirationTime;\n      if (alternate !== null && alternate.childExpirationTime < renderExpirationTime) {\n        alternate.childExpirationTime = renderExpirationTime;\n      }\n    } else if (alternate !== null && alternate.childExpirationTime < renderExpirationTime) {\n      alternate.childExpirationTime = renderExpirationTime;\n    } else {\n      // Neither alternate was updated, which means the rest of the\n      // ancestor path already has sufficient priority.\n      break;\n    }\n    node = node.return;\n  }\n}\n\nfunction propagateContextChange(workInProgress, context, changedBits, renderExpirationTime) {\n  var fiber = workInProgress.child;\n  if (fiber !== null) {\n    // Set the return pointer of the child to the work-in-progress fiber.\n    fiber.return = workInProgress;\n  }\n  while (fiber !== null) {\n    var nextFiber = void 0;\n\n    // Visit this fiber.\n    var list = fiber.contextDependencies;\n    if (list !== null) {\n      nextFiber = fiber.child;\n\n      var dependency = list.first;\n      while (dependency !== null) {\n        // Check if the context matches.\n        if (dependency.context === context && (dependency.observedBits & changedBits) !== 0) {\n          // Match! Schedule an update on this fiber.\n\n          if (fiber.tag === ClassComponent) {\n            // Schedule a force update on the work-in-progress.\n            var update = createUpdate(renderExpirationTime);\n            update.tag = ForceUpdate;\n            // TODO: Because we don't have a work-in-progress, this will add the\n            // update to the current fiber, too, which means it will persist even if\n            // this render is thrown away. Since it's a race condition, not sure it's\n            // worth fixing.\n            enqueueUpdate(fiber, update);\n          }\n\n          if (fiber.expirationTime < renderExpirationTime) {\n            fiber.expirationTime = renderExpirationTime;\n          }\n          var alternate = fiber.alternate;\n          if (alternate !== null && alternate.expirationTime < renderExpirationTime) {\n            alternate.expirationTime = renderExpirationTime;\n          }\n\n          scheduleWorkOnParentPath(fiber.return, renderExpirationTime);\n\n          // Mark the expiration time on the list, too.\n          if (list.expirationTime < renderExpirationTime) {\n            list.expirationTime = renderExpirationTime;\n          }\n\n          // Since we already found a match, we can stop traversing the\n          // dependency list.\n          break;\n        }\n        dependency = dependency.next;\n      }\n    } else if (fiber.tag === ContextProvider) {\n      // Don't scan deeper if this is a matching provider\n      nextFiber = fiber.type === workInProgress.type ? null : fiber.child;\n    } else if (enableSuspenseServerRenderer && fiber.tag === DehydratedSuspenseComponent) {\n      // If a dehydrated suspense component is in this subtree, we don't know\n      // if it will have any context consumers in it. The best we can do is\n      // mark it as having updates on its children.\n      if (fiber.expirationTime < renderExpirationTime) {\n        fiber.expirationTime = renderExpirationTime;\n      }\n      var _alternate = fiber.alternate;\n      if (_alternate !== null && _alternate.expirationTime < renderExpirationTime) {\n        _alternate.expirationTime = renderExpirationTime;\n      }\n      // This is intentionally passing this fiber as the parent\n      // because we want to schedule this fiber as having work\n      // on its children. We'll use the childExpirationTime on\n      // this fiber to indicate that a context has changed.\n      scheduleWorkOnParentPath(fiber, renderExpirationTime);\n      nextFiber = fiber.sibling;\n    } else {\n      // Traverse down.\n      nextFiber = fiber.child;\n    }\n\n    if (nextFiber !== null) {\n      // Set the return pointer of the child to the work-in-progress fiber.\n      nextFiber.return = fiber;\n    } else {\n      // No child. Traverse to next sibling.\n      nextFiber = fiber;\n      while (nextFiber !== null) {\n        if (nextFiber === workInProgress) {\n          // We're back to the root of this subtree. Exit.\n          nextFiber = null;\n          break;\n        }\n        var sibling = nextFiber.sibling;\n        if (sibling !== null) {\n          // Set the return pointer of the sibling to the work-in-progress fiber.\n          sibling.return = nextFiber.return;\n          nextFiber = sibling;\n          break;\n        }\n        // No more siblings. Traverse up.\n        nextFiber = nextFiber.return;\n      }\n    }\n    fiber = nextFiber;\n  }\n}\n\nfunction prepareToReadContext(workInProgress, renderExpirationTime) {\n  currentlyRenderingFiber = workInProgress;\n  lastContextDependency = null;\n  lastContextWithAllBitsObserved = null;\n\n  var currentDependencies = workInProgress.contextDependencies;\n  if (currentDependencies !== null && currentDependencies.expirationTime >= renderExpirationTime) {\n    // Context list has a pending update. Mark that this fiber performed work.\n    markWorkInProgressReceivedUpdate();\n  }\n\n  // Reset the work-in-progress list\n  workInProgress.contextDependencies = null;\n}\n\nfunction readContext(context, observedBits) {\n  {\n    // This warning would fire if you read context inside a Hook like useMemo.\n    // Unlike the class check below, it's not enforced in production for perf.\n    !!isDisallowedContextReadInDEV ? warning$1(false, 'Context can only be read while React is rendering. ' + 'In classes, you can read it in the render method or getDerivedStateFromProps. ' + 'In function components, you can read it directly in the function body, but not ' + 'inside Hooks like useReducer() or useMemo().') : void 0;\n  }\n\n  if (lastContextWithAllBitsObserved === context) {\n    // Nothing to do. We already observe everything in this context.\n  } else if (observedBits === false || observedBits === 0) {\n    // Do not observe any updates.\n  } else {\n    var resolvedObservedBits = void 0; // Avoid deopting on observable arguments or heterogeneous types.\n    if (typeof observedBits !== 'number' || observedBits === maxSigned31BitInt) {\n      // Observe all updates.\n      lastContextWithAllBitsObserved = context;\n      resolvedObservedBits = maxSigned31BitInt;\n    } else {\n      resolvedObservedBits = observedBits;\n    }\n\n    var contextItem = {\n      context: context,\n      observedBits: resolvedObservedBits,\n      next: null\n    };\n\n    if (lastContextDependency === null) {\n      !(currentlyRenderingFiber !== null) ? invariant(false, 'Context can only be read while React is rendering. In classes, you can read it in the render method or getDerivedStateFromProps. In function components, you can read it directly in the function body, but not inside Hooks like useReducer() or useMemo().') : void 0;\n\n      // This is the first dependency for this component. Create a new list.\n      lastContextDependency = contextItem;\n      currentlyRenderingFiber.contextDependencies = {\n        first: contextItem,\n        expirationTime: NoWork\n      };\n    } else {\n      // Append a new context item.\n      lastContextDependency = lastContextDependency.next = contextItem;\n    }\n  }\n  return isPrimaryRenderer ? context._currentValue : context._currentValue2;\n}\n\n// UpdateQueue is a linked list of prioritized updates.\n//\n// Like fibers, update queues come in pairs: a current queue, which represents\n// the visible state of the screen, and a work-in-progress queue, which can be\n// mutated and processed asynchronously before it is committed — a form of\n// double buffering. If a work-in-progress render is discarded before finishing,\n// we create a new work-in-progress by cloning the current queue.\n//\n// Both queues share a persistent, singly-linked list structure. To schedule an\n// update, we append it to the end of both queues. Each queue maintains a\n// pointer to first update in the persistent list that hasn't been processed.\n// The work-in-progress pointer always has a position equal to or greater than\n// the current queue, since we always work on that one. The current queue's\n// pointer is only updated during the commit phase, when we swap in the\n// work-in-progress.\n//\n// For example:\n//\n//   Current pointer:           A - B - C - D - E - F\n//   Work-in-progress pointer:              D - E - F\n//                                          ^\n//                                          The work-in-progress queue has\n//                                          processed more updates than current.\n//\n// The reason we append to both queues is because otherwise we might drop\n// updates without ever processing them. For example, if we only add updates to\n// the work-in-progress queue, some updates could be lost whenever a work-in\n// -progress render restarts by cloning from current. Similarly, if we only add\n// updates to the current queue, the updates will be lost whenever an already\n// in-progress queue commits and swaps with the current queue. However, by\n// adding to both queues, we guarantee that the update will be part of the next\n// work-in-progress. (And because the work-in-progress queue becomes the\n// current queue once it commits, there's no danger of applying the same\n// update twice.)\n//\n// Prioritization\n// --------------\n//\n// Updates are not sorted by priority, but by insertion; new updates are always\n// appended to the end of the list.\n//\n// The priority is still important, though. When processing the update queue\n// during the render phase, only the updates with sufficient priority are\n// included in the result. If we skip an update because it has insufficient\n// priority, it remains in the queue to be processed later, during a lower\n// priority render. Crucially, all updates subsequent to a skipped update also\n// remain in the queue *regardless of their priority*. That means high priority\n// updates are sometimes processed twice, at two separate priorities. We also\n// keep track of a base state, that represents the state before the first\n// update in the queue is applied.\n//\n// For example:\n//\n//   Given a base state of '', and the following queue of updates\n//\n//     A1 - B2 - C1 - D2\n//\n//   where the number indicates the priority, and the update is applied to the\n//   previous state by appending a letter, React will process these updates as\n//   two separate renders, one per distinct priority level:\n//\n//   First render, at priority 1:\n//     Base state: ''\n//     Updates: [A1, C1]\n//     Result state: 'AC'\n//\n//   Second render, at priority 2:\n//     Base state: 'A'            <-  The base state does not include C1,\n//                                    because B2 was skipped.\n//     Updates: [B2, C1, D2]      <-  C1 was rebased on top of B2\n//     Result state: 'ABCD'\n//\n// Because we process updates in insertion order, and rebase high priority\n// updates when preceding updates are skipped, the final result is deterministic\n// regardless of priority. Intermediate state may vary according to system\n// resources, but the final state is always the same.\n\nvar UpdateState = 0;\nvar ReplaceState = 1;\nvar ForceUpdate = 2;\nvar CaptureUpdate = 3;\n\n// Global state that is reset at the beginning of calling `processUpdateQueue`.\n// It should only be read right after calling `processUpdateQueue`, via\n// `checkHasForceUpdateAfterProcessing`.\nvar hasForceUpdate = false;\n\nvar didWarnUpdateInsideUpdate = void 0;\nvar currentlyProcessingQueue = void 0;\nvar resetCurrentlyProcessingQueue = void 0;\n{\n  didWarnUpdateInsideUpdate = false;\n  currentlyProcessingQueue = null;\n  resetCurrentlyProcessingQueue = function () {\n    currentlyProcessingQueue = null;\n  };\n}\n\nfunction createUpdateQueue(baseState) {\n  var queue = {\n    baseState: baseState,\n    firstUpdate: null,\n    lastUpdate: null,\n    firstCapturedUpdate: null,\n    lastCapturedUpdate: null,\n    firstEffect: null,\n    lastEffect: null,\n    firstCapturedEffect: null,\n    lastCapturedEffect: null\n  };\n  return queue;\n}\n\nfunction cloneUpdateQueue(currentQueue) {\n  var queue = {\n    baseState: currentQueue.baseState,\n    firstUpdate: currentQueue.firstUpdate,\n    lastUpdate: currentQueue.lastUpdate,\n\n    // TODO: With resuming, if we bail out and resuse the child tree, we should\n    // keep these effects.\n    firstCapturedUpdate: null,\n    lastCapturedUpdate: null,\n\n    firstEffect: null,\n    lastEffect: null,\n\n    firstCapturedEffect: null,\n    lastCapturedEffect: null\n  };\n  return queue;\n}\n\nfunction createUpdate(expirationTime) {\n  return {\n    expirationTime: expirationTime,\n\n    tag: UpdateState,\n    payload: null,\n    callback: null,\n\n    next: null,\n    nextEffect: null\n  };\n}\n\nfunction appendUpdateToQueue(queue, update) {\n  // Append the update to the end of the list.\n  if (queue.lastUpdate === null) {\n    // Queue is empty\n    queue.firstUpdate = queue.lastUpdate = update;\n  } else {\n    queue.lastUpdate.next = update;\n    queue.lastUpdate = update;\n  }\n}\n\nfunction enqueueUpdate(fiber, update) {\n  // Update queues are created lazily.\n  var alternate = fiber.alternate;\n  var queue1 = void 0;\n  var queue2 = void 0;\n  if (alternate === null) {\n    // There's only one fiber.\n    queue1 = fiber.updateQueue;\n    queue2 = null;\n    if (queue1 === null) {\n      queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);\n    }\n  } else {\n    // There are two owners.\n    queue1 = fiber.updateQueue;\n    queue2 = alternate.updateQueue;\n    if (queue1 === null) {\n      if (queue2 === null) {\n        // Neither fiber has an update queue. Create new ones.\n        queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);\n        queue2 = alternate.updateQueue = createUpdateQueue(alternate.memoizedState);\n      } else {\n        // Only one fiber has an update queue. Clone to create a new one.\n        queue1 = fiber.updateQueue = cloneUpdateQueue(queue2);\n      }\n    } else {\n      if (queue2 === null) {\n        // Only one fiber has an update queue. Clone to create a new one.\n        queue2 = alternate.updateQueue = cloneUpdateQueue(queue1);\n      } else {\n        // Both owners have an update queue.\n      }\n    }\n  }\n  if (queue2 === null || queue1 === queue2) {\n    // There's only a single queue.\n    appendUpdateToQueue(queue1, update);\n  } else {\n    // There are two queues. We need to append the update to both queues,\n    // while accounting for the persistent structure of the list — we don't\n    // want the same update to be added multiple times.\n    if (queue1.lastUpdate === null || queue2.lastUpdate === null) {\n      // One of the queues is not empty. We must add the update to both queues.\n      appendUpdateToQueue(queue1, update);\n      appendUpdateToQueue(queue2, update);\n    } else {\n      // Both queues are non-empty. The last update is the same in both lists,\n      // because of structural sharing. So, only append to one of the lists.\n      appendUpdateToQueue(queue1, update);\n      // But we still need to update the `lastUpdate` pointer of queue2.\n      queue2.lastUpdate = update;\n    }\n  }\n\n  {\n    if (fiber.tag === ClassComponent && (currentlyProcessingQueue === queue1 || queue2 !== null && currentlyProcessingQueue === queue2) && !didWarnUpdateInsideUpdate) {\n      warningWithoutStack$1(false, 'An update (setState, replaceState, or forceUpdate) was scheduled ' + 'from inside an update function. Update functions should be pure, ' + 'with zero side-effects. Consider using componentDidUpdate or a ' + 'callback.');\n      didWarnUpdateInsideUpdate = true;\n    }\n  }\n}\n\nfunction enqueueCapturedUpdate(workInProgress, update) {\n  // Captured updates go into a separate list, and only on the work-in-\n  // progress queue.\n  var workInProgressQueue = workInProgress.updateQueue;\n  if (workInProgressQueue === null) {\n    workInProgressQueue = workInProgress.updateQueue = createUpdateQueue(workInProgress.memoizedState);\n  } else {\n    // TODO: I put this here rather than createWorkInProgress so that we don't\n    // clone the queue unnecessarily. There's probably a better way to\n    // structure this.\n    workInProgressQueue = ensureWorkInProgressQueueIsAClone(workInProgress, workInProgressQueue);\n  }\n\n  // Append the update to the end of the list.\n  if (workInProgressQueue.lastCapturedUpdate === null) {\n    // This is the first render phase update\n    workInProgressQueue.firstCapturedUpdate = workInProgressQueue.lastCapturedUpdate = update;\n  } else {\n    workInProgressQueue.lastCapturedUpdate.next = update;\n    workInProgressQueue.lastCapturedUpdate = update;\n  }\n}\n\nfunction ensureWorkInProgressQueueIsAClone(workInProgress, queue) {\n  var current = workInProgress.alternate;\n  if (current !== null) {\n    // If the work-in-progress queue is equal to the current queue,\n    // we need to clone it first.\n    if (queue === current.updateQueue) {\n      queue = workInProgress.updateQueue = cloneUpdateQueue(queue);\n    }\n  }\n  return queue;\n}\n\nfunction getStateFromUpdate(workInProgress, queue, update, prevState, nextProps, instance) {\n  switch (update.tag) {\n    case ReplaceState:\n      {\n        var _payload = update.payload;\n        if (typeof _payload === 'function') {\n          // Updater function\n          {\n            enterDisallowedContextReadInDEV();\n            if (debugRenderPhaseSideEffects || debugRenderPhaseSideEffectsForStrictMode && workInProgress.mode & StrictMode) {\n              _payload.call(instance, prevState, nextProps);\n            }\n          }\n          var nextState = _payload.call(instance, prevState, nextProps);\n          {\n            exitDisallowedContextReadInDEV();\n          }\n          return nextState;\n        }\n        // State object\n        return _payload;\n      }\n    case CaptureUpdate:\n      {\n        workInProgress.effectTag = workInProgress.effectTag & ~ShouldCapture | DidCapture;\n      }\n    // Intentional fallthrough\n    case UpdateState:\n      {\n        var _payload2 = update.payload;\n        var partialState = void 0;\n        if (typeof _payload2 === 'function') {\n          // Updater function\n          {\n            enterDisallowedContextReadInDEV();\n            if (debugRenderPhaseSideEffects || debugRenderPhaseSideEffectsForStrictMode && workInProgress.mode & StrictMode) {\n              _payload2.call(instance, prevState, nextProps);\n            }\n          }\n          partialState = _payload2.call(instance, prevState, nextProps);\n          {\n            exitDisallowedContextReadInDEV();\n          }\n        } else {\n          // Partial state object\n          partialState = _payload2;\n        }\n        if (partialState === null || partialState === undefined) {\n          // Null and undefined are treated as no-ops.\n          return prevState;\n        }\n        // Merge the partial state and the previous state.\n        return _assign({}, prevState, partialState);\n      }\n    case ForceUpdate:\n      {\n        hasForceUpdate = true;\n        return prevState;\n      }\n  }\n  return prevState;\n}\n\nfunction processUpdateQueue(workInProgress, queue, props, instance, renderExpirationTime) {\n  hasForceUpdate = false;\n\n  queue = ensureWorkInProgressQueueIsAClone(workInProgress, queue);\n\n  {\n    currentlyProcessingQueue = queue;\n  }\n\n  // These values may change as we process the queue.\n  var newBaseState = queue.baseState;\n  var newFirstUpdate = null;\n  var newExpirationTime = NoWork;\n\n  // Iterate through the list of updates to compute the result.\n  var update = queue.firstUpdate;\n  var resultState = newBaseState;\n  while (update !== null) {\n    var updateExpirationTime = update.expirationTime;\n    if (updateExpirationTime < renderExpirationTime) {\n      // This update does not have sufficient priority. Skip it.\n      if (newFirstUpdate === null) {\n        // This is the first skipped update. It will be the first update in\n        // the new list.\n        newFirstUpdate = update;\n        // Since this is the first update that was skipped, the current result\n        // is the new base state.\n        newBaseState = resultState;\n      }\n      // Since this update will remain in the list, update the remaining\n      // expiration time.\n      if (newExpirationTime < updateExpirationTime) {\n        newExpirationTime = updateExpirationTime;\n      }\n    } else {\n      // This update does have sufficient priority. Process it and compute\n      // a new result.\n      resultState = getStateFromUpdate(workInProgress, queue, update, resultState, props, instance);\n      var _callback = update.callback;\n      if (_callback !== null) {\n        workInProgress.effectTag |= Callback;\n        // Set this to null, in case it was mutated during an aborted render.\n        update.nextEffect = null;\n        if (queue.lastEffect === null) {\n          queue.firstEffect = queue.lastEffect = update;\n        } else {\n          queue.lastEffect.nextEffect = update;\n          queue.lastEffect = update;\n        }\n      }\n    }\n    // Continue to the next update.\n    update = update.next;\n  }\n\n  // Separately, iterate though the list of captured updates.\n  var newFirstCapturedUpdate = null;\n  update = queue.firstCapturedUpdate;\n  while (update !== null) {\n    var _updateExpirationTime = update.expirationTime;\n    if (_updateExpirationTime < renderExpirationTime) {\n      // This update does not have sufficient priority. Skip it.\n      if (newFirstCapturedUpdate === null) {\n        // This is the first skipped captured update. It will be the first\n        // update in the new list.\n        newFirstCapturedUpdate = update;\n        // If this is the first update that was skipped, the current result is\n        // the new base state.\n        if (newFirstUpdate === null) {\n          newBaseState = resultState;\n        }\n      }\n      // Since this update will remain in the list, update the remaining\n      // expiration time.\n      if (newExpirationTime < _updateExpirationTime) {\n        newExpirationTime = _updateExpirationTime;\n      }\n    } else {\n      // This update does have sufficient priority. Process it and compute\n      // a new result.\n      resultState = getStateFromUpdate(workInProgress, queue, update, resultState, props, instance);\n      var _callback2 = update.callback;\n      if (_callback2 !== null) {\n        workInProgress.effectTag |= Callback;\n        // Set this to null, in case it was mutated during an aborted render.\n        update.nextEffect = null;\n        if (queue.lastCapturedEffect === null) {\n          queue.firstCapturedEffect = queue.lastCapturedEffect = update;\n        } else {\n          queue.lastCapturedEffect.nextEffect = update;\n          queue.lastCapturedEffect = update;\n        }\n      }\n    }\n    update = update.next;\n  }\n\n  if (newFirstUpdate === null) {\n    queue.lastUpdate = null;\n  }\n  if (newFirstCapturedUpdate === null) {\n    queue.lastCapturedUpdate = null;\n  } else {\n    workInProgress.effectTag |= Callback;\n  }\n  if (newFirstUpdate === null && newFirstCapturedUpdate === null) {\n    // We processed every update, without skipping. That means the new base\n    // state is the same as the result state.\n    newBaseState = resultState;\n  }\n\n  queue.baseState = newBaseState;\n  queue.firstUpdate = newFirstUpdate;\n  queue.firstCapturedUpdate = newFirstCapturedUpdate;\n\n  // Set the remaining expiration time to be whatever is remaining in the queue.\n  // This should be fine because the only two other things that contribute to\n  // expiration time are props and context. We're already in the middle of the\n  // begin phase by the time we start processing the queue, so we've already\n  // dealt with the props. Context in components that specify\n  // shouldComponentUpdate is tricky; but we'll have to account for\n  // that regardless.\n  workInProgress.expirationTime = newExpirationTime;\n  workInProgress.memoizedState = resultState;\n\n  {\n    currentlyProcessingQueue = null;\n  }\n}\n\nfunction callCallback(callback, context) {\n  !(typeof callback === 'function') ? invariant(false, 'Invalid argument passed as callback. Expected a function. Instead received: %s', callback) : void 0;\n  callback.call(context);\n}\n\nfunction resetHasForceUpdateBeforeProcessing() {\n  hasForceUpdate = false;\n}\n\nfunction checkHasForceUpdateAfterProcessing() {\n  return hasForceUpdate;\n}\n\nfunction commitUpdateQueue(finishedWork, finishedQueue, instance, renderExpirationTime) {\n  // If the finished render included captured updates, and there are still\n  // lower priority updates left over, we need to keep the captured updates\n  // in the queue so that they are rebased and not dropped once we process the\n  // queue again at the lower priority.\n  if (finishedQueue.firstCapturedUpdate !== null) {\n    // Join the captured update list to the end of the normal list.\n    if (finishedQueue.lastUpdate !== null) {\n      finishedQueue.lastUpdate.next = finishedQueue.firstCapturedUpdate;\n      finishedQueue.lastUpdate = finishedQueue.lastCapturedUpdate;\n    }\n    // Clear the list of captured updates.\n    finishedQueue.firstCapturedUpdate = finishedQueue.lastCapturedUpdate = null;\n  }\n\n  // Commit the effects\n  commitUpdateEffects(finishedQueue.firstEffect, instance);\n  finishedQueue.firstEffect = finishedQueue.lastEffect = null;\n\n  commitUpdateEffects(finishedQueue.firstCapturedEffect, instance);\n  finishedQueue.firstCapturedEffect = finishedQueue.lastCapturedEffect = null;\n}\n\nfunction commitUpdateEffects(effect, instance) {\n  while (effect !== null) {\n    var _callback3 = effect.callback;\n    if (_callback3 !== null) {\n      effect.callback = null;\n      callCallback(_callback3, instance);\n    }\n    effect = effect.nextEffect;\n  }\n}\n\nfunction createCapturedValue(value, source) {\n  // If the value is an error, call this function immediately after it is thrown\n  // so the stack is accurate.\n  return {\n    value: value,\n    source: source,\n    stack: getStackByFiberInDevAndProd(source)\n  };\n}\n\nfunction markUpdate(workInProgress) {\n  // Tag the fiber with an update effect. This turns a Placement into\n  // a PlacementAndUpdate.\n  workInProgress.effectTag |= Update;\n}\n\nfunction markRef$1(workInProgress) {\n  workInProgress.effectTag |= Ref;\n}\n\nvar appendAllChildren = void 0;\nvar updateHostContainer = void 0;\nvar updateHostComponent$1 = void 0;\nvar updateHostText$1 = void 0;\nif (supportsMutation) {\n  // Mutation mode\n\n  appendAllChildren = function (parent, workInProgress, needsVisibilityToggle, isHidden) {\n    // We only have the top Fiber that was created but we need recurse down its\n    // children to find all the terminal nodes.\n    var node = workInProgress.child;\n    while (node !== null) {\n      if (node.tag === HostComponent || node.tag === HostText) {\n        appendInitialChild(parent, node.stateNode);\n      } else if (node.tag === HostPortal) {\n        // If we have a portal child, then we don't want to traverse\n        // down its children. Instead, we'll get insertions from each child in\n        // the portal directly.\n      } else if (node.child !== null) {\n        node.child.return = node;\n        node = node.child;\n        continue;\n      }\n      if (node === workInProgress) {\n        return;\n      }\n      while (node.sibling === null) {\n        if (node.return === null || node.return === workInProgress) {\n          return;\n        }\n        node = node.return;\n      }\n      node.sibling.return = node.return;\n      node = node.sibling;\n    }\n  };\n\n  updateHostContainer = function (workInProgress) {\n    // Noop\n  };\n  updateHostComponent$1 = function (current, workInProgress, type, newProps, rootContainerInstance) {\n    // If we have an alternate, that means this is an update and we need to\n    // schedule a side-effect to do the updates.\n    var oldProps = current.memoizedProps;\n    if (oldProps === newProps) {\n      // In mutation mode, this is sufficient for a bailout because\n      // we won't touch this node even if children changed.\n      return;\n    }\n\n    // If we get updated because one of our children updated, we don't\n    // have newProps so we'll have to reuse them.\n    // TODO: Split the update API as separate for the props vs. children.\n    // Even better would be if children weren't special cased at all tho.\n    var instance = workInProgress.stateNode;\n    var currentHostContext = getHostContext();\n    // TODO: Experiencing an error where oldProps is null. Suggests a host\n    // component is hitting the resume path. Figure out why. Possibly\n    // related to `hidden`.\n    var updatePayload = prepareUpdate(instance, type, oldProps, newProps, rootContainerInstance, currentHostContext);\n    // TODO: Type this specific to this type of component.\n    workInProgress.updateQueue = updatePayload;\n    // If the update payload indicates that there is a change or if there\n    // is a new ref we mark this as an update. All the work is done in commitWork.\n    if (updatePayload) {\n      markUpdate(workInProgress);\n    }\n  };\n  updateHostText$1 = function (current, workInProgress, oldText, newText) {\n    // If the text differs, mark it as an update. All the work in done in commitWork.\n    if (oldText !== newText) {\n      markUpdate(workInProgress);\n    }\n  };\n} else if (supportsPersistence) {\n  // Persistent host tree mode\n\n  appendAllChildren = function (parent, workInProgress, needsVisibilityToggle, isHidden) {\n    // We only have the top Fiber that was created but we need recurse down its\n    // children to find all the terminal nodes.\n    var node = workInProgress.child;\n    while (node !== null) {\n      // eslint-disable-next-line no-labels\n      branches: if (node.tag === HostComponent) {\n        var instance = node.stateNode;\n        if (needsVisibilityToggle) {\n          var props = node.memoizedProps;\n          var type = node.type;\n          if (isHidden) {\n            // This child is inside a timed out tree. Hide it.\n            instance = cloneHiddenInstance(instance, type, props, node);\n          } else {\n            // This child was previously inside a timed out tree. If it was not\n            // updated during this render, it may need to be unhidden. Clone\n            // again to be sure.\n            instance = cloneUnhiddenInstance(instance, type, props, node);\n          }\n          node.stateNode = instance;\n        }\n        appendInitialChild(parent, instance);\n      } else if (node.tag === HostText) {\n        var _instance = node.stateNode;\n        if (needsVisibilityToggle) {\n          var text = node.memoizedProps;\n          var rootContainerInstance = getRootHostContainer();\n          var currentHostContext = getHostContext();\n          if (isHidden) {\n            _instance = createHiddenTextInstance(text, rootContainerInstance, currentHostContext, workInProgress);\n          } else {\n            _instance = createTextInstance(text, rootContainerInstance, currentHostContext, workInProgress);\n          }\n          node.stateNode = _instance;\n        }\n        appendInitialChild(parent, _instance);\n      } else if (node.tag === HostPortal) {\n        // If we have a portal child, then we don't want to traverse\n        // down its children. Instead, we'll get insertions from each child in\n        // the portal directly.\n      } else if (node.tag === SuspenseComponent) {\n        var current = node.alternate;\n        if (current !== null) {\n          var oldState = current.memoizedState;\n          var newState = node.memoizedState;\n          var oldIsHidden = oldState !== null;\n          var newIsHidden = newState !== null;\n          if (oldIsHidden !== newIsHidden) {\n            // The placeholder either just timed out or switched back to the normal\n            // children after having previously timed out. Toggle the visibility of\n            // the direct host children.\n            var primaryChildParent = newIsHidden ? node.child : node;\n            if (primaryChildParent !== null) {\n              appendAllChildren(parent, primaryChildParent, true, newIsHidden);\n            }\n            // eslint-disable-next-line no-labels\n            break branches;\n          }\n        }\n        if (node.child !== null) {\n          // Continue traversing like normal\n          node.child.return = node;\n          node = node.child;\n          continue;\n        }\n      } else if (node.child !== null) {\n        node.child.return = node;\n        node = node.child;\n        continue;\n      }\n      // $FlowFixMe This is correct but Flow is confused by the labeled break.\n      node = node;\n      if (node === workInProgress) {\n        return;\n      }\n      while (node.sibling === null) {\n        if (node.return === null || node.return === workInProgress) {\n          return;\n        }\n        node = node.return;\n      }\n      node.sibling.return = node.return;\n      node = node.sibling;\n    }\n  };\n\n  // An unfortunate fork of appendAllChildren because we have two different parent types.\n  var appendAllChildrenToContainer = function (containerChildSet, workInProgress, needsVisibilityToggle, isHidden) {\n    // We only have the top Fiber that was created but we need recurse down its\n    // children to find all the terminal nodes.\n    var node = workInProgress.child;\n    while (node !== null) {\n      // eslint-disable-next-line no-labels\n      branches: if (node.tag === HostComponent) {\n        var instance = node.stateNode;\n        if (needsVisibilityToggle) {\n          var props = node.memoizedProps;\n          var type = node.type;\n          if (isHidden) {\n            // This child is inside a timed out tree. Hide it.\n            instance = cloneHiddenInstance(instance, type, props, node);\n          } else {\n            // This child was previously inside a timed out tree. If it was not\n            // updated during this render, it may need to be unhidden. Clone\n            // again to be sure.\n            instance = cloneUnhiddenInstance(instance, type, props, node);\n          }\n          node.stateNode = instance;\n        }\n        appendChildToContainerChildSet(containerChildSet, instance);\n      } else if (node.tag === HostText) {\n        var _instance2 = node.stateNode;\n        if (needsVisibilityToggle) {\n          var text = node.memoizedProps;\n          var rootContainerInstance = getRootHostContainer();\n          var currentHostContext = getHostContext();\n          if (isHidden) {\n            _instance2 = createHiddenTextInstance(text, rootContainerInstance, currentHostContext, workInProgress);\n          } else {\n            _instance2 = createTextInstance(text, rootContainerInstance, currentHostContext, workInProgress);\n          }\n          node.stateNode = _instance2;\n        }\n        appendChildToContainerChildSet(containerChildSet, _instance2);\n      } else if (node.tag === HostPortal) {\n        // If we have a portal child, then we don't want to traverse\n        // down its children. Instead, we'll get insertions from each child in\n        // the portal directly.\n      } else if (node.tag === SuspenseComponent) {\n        var current = node.alternate;\n        if (current !== null) {\n          var oldState = current.memoizedState;\n          var newState = node.memoizedState;\n          var oldIsHidden = oldState !== null;\n          var newIsHidden = newState !== null;\n          if (oldIsHidden !== newIsHidden) {\n            // The placeholder either just timed out or switched back to the normal\n            // children after having previously timed out. Toggle the visibility of\n            // the direct host children.\n            var primaryChildParent = newIsHidden ? node.child : node;\n            if (primaryChildParent !== null) {\n              appendAllChildrenToContainer(containerChildSet, primaryChildParent, true, newIsHidden);\n            }\n            // eslint-disable-next-line no-labels\n            break branches;\n          }\n        }\n        if (node.child !== null) {\n          // Continue traversing like normal\n          node.child.return = node;\n          node = node.child;\n          continue;\n        }\n      } else if (node.child !== null) {\n        node.child.return = node;\n        node = node.child;\n        continue;\n      }\n      // $FlowFixMe This is correct but Flow is confused by the labeled break.\n      node = node;\n      if (node === workInProgress) {\n        return;\n      }\n      while (node.sibling === null) {\n        if (node.return === null || node.return === workInProgress) {\n          return;\n        }\n        node = node.return;\n      }\n      node.sibling.return = node.return;\n      node = node.sibling;\n    }\n  };\n  updateHostContainer = function (workInProgress) {\n    var portalOrRoot = workInProgress.stateNode;\n    var childrenUnchanged = workInProgress.firstEffect === null;\n    if (childrenUnchanged) {\n      // No changes, just reuse the existing instance.\n    } else {\n      var container = portalOrRoot.containerInfo;\n      var newChildSet = createContainerChildSet(container);\n      // If children might have changed, we have to add them all to the set.\n      appendAllChildrenToContainer(newChildSet, workInProgress, false, false);\n      portalOrRoot.pendingChildren = newChildSet;\n      // Schedule an update on the container to swap out the container.\n      markUpdate(workInProgress);\n      finalizeContainerChildren(container, newChildSet);\n    }\n  };\n  updateHostComponent$1 = function (current, workInProgress, type, newProps, rootContainerInstance) {\n    var currentInstance = current.stateNode;\n    var oldProps = current.memoizedProps;\n    // If there are no effects associated with this node, then none of our children had any updates.\n    // This guarantees that we can reuse all of them.\n    var childrenUnchanged = workInProgress.firstEffect === null;\n    if (childrenUnchanged && oldProps === newProps) {\n      // No changes, just reuse the existing instance.\n      // Note that this might release a previous clone.\n      workInProgress.stateNode = currentInstance;\n      return;\n    }\n    var recyclableInstance = workInProgress.stateNode;\n    var currentHostContext = getHostContext();\n    var updatePayload = null;\n    if (oldProps !== newProps) {\n      updatePayload = prepareUpdate(recyclableInstance, type, oldProps, newProps, rootContainerInstance, currentHostContext);\n    }\n    if (childrenUnchanged && updatePayload === null) {\n      // No changes, just reuse the existing instance.\n      // Note that this might release a previous clone.\n      workInProgress.stateNode = currentInstance;\n      return;\n    }\n    var newInstance = cloneInstance(currentInstance, updatePayload, type, oldProps, newProps, workInProgress, childrenUnchanged, recyclableInstance);\n    if (finalizeInitialChildren(newInstance, type, newProps, rootContainerInstance, currentHostContext)) {\n      markUpdate(workInProgress);\n    }\n    workInProgress.stateNode = newInstance;\n    if (childrenUnchanged) {\n      // If there are no other effects in this tree, we need to flag this node as having one.\n      // Even though we're not going to use it for anything.\n      // Otherwise parents won't know that there are new children to propagate upwards.\n      markUpdate(workInProgress);\n    } else {\n      // If children might have changed, we have to add them all to the set.\n      appendAllChildren(newInstance, workInProgress, false, false);\n    }\n  };\n  updateHostText$1 = function (current, workInProgress, oldText, newText) {\n    if (oldText !== newText) {\n      // If the text content differs, we'll create a new text instance for it.\n      var rootContainerInstance = getRootHostContainer();\n      var currentHostContext = getHostContext();\n      workInProgress.stateNode = createTextInstance(newText, rootContainerInstance, currentHostContext, workInProgress);\n      // We'll have to mark it as having an effect, even though we won't use the effect for anything.\n      // This lets the parents know that at least one of their children has changed.\n      markUpdate(workInProgress);\n    }\n  };\n} else {\n  // No host operations\n  updateHostContainer = function (workInProgress) {\n    // Noop\n  };\n  updateHostComponent$1 = function (current, workInProgress, type, newProps, rootContainerInstance) {\n    // Noop\n  };\n  updateHostText$1 = function (current, workInProgress, oldText, newText) {\n    // Noop\n  };\n}\n\nfunction completeWork(current, workInProgress, renderExpirationTime) {\n  var newProps = workInProgress.pendingProps;\n\n  switch (workInProgress.tag) {\n    case IndeterminateComponent:\n      break;\n    case LazyComponent:\n      break;\n    case SimpleMemoComponent:\n    case FunctionComponent:\n      break;\n    case ClassComponent:\n      {\n        var Component = workInProgress.type;\n        if (isContextProvider(Component)) {\n          popContext(workInProgress);\n        }\n        break;\n      }\n    case HostRoot:\n      {\n        popHostContainer(workInProgress);\n        popTopLevelContextObject(workInProgress);\n        var fiberRoot = workInProgress.stateNode;\n        if (fiberRoot.pendingContext) {\n          fiberRoot.context = fiberRoot.pendingContext;\n          fiberRoot.pendingContext = null;\n        }\n        if (current === null || current.child === null) {\n          // If we hydrated, pop so that we can delete any remaining children\n          // that weren't hydrated.\n          popHydrationState(workInProgress);\n          // This resets the hacky state to fix isMounted before committing.\n          // TODO: Delete this when we delete isMounted and findDOMNode.\n          workInProgress.effectTag &= ~Placement;\n        }\n        updateHostContainer(workInProgress);\n        break;\n      }\n    case HostComponent:\n      {\n        popHostContext(workInProgress);\n        var rootContainerInstance = getRootHostContainer();\n        var type = workInProgress.type;\n        if (current !== null && workInProgress.stateNode != null) {\n          updateHostComponent$1(current, workInProgress, type, newProps, rootContainerInstance);\n\n          if (current.ref !== workInProgress.ref) {\n            markRef$1(workInProgress);\n          }\n        } else {\n          if (!newProps) {\n            !(workInProgress.stateNode !== null) ? invariant(false, 'We must have new props for new mounts. This error is likely caused by a bug in React. Please file an issue.') : void 0;\n            // This can happen when we abort work.\n            break;\n          }\n\n          var currentHostContext = getHostContext();\n          // TODO: Move createInstance to beginWork and keep it on a context\n          // \"stack\" as the parent. Then append children as we go in beginWork\n          // or completeWork depending on we want to add then top->down or\n          // bottom->up. Top->down is faster in IE11.\n          var wasHydrated = popHydrationState(workInProgress);\n          if (wasHydrated) {\n            // TODO: Move this and createInstance step into the beginPhase\n            // to consolidate.\n            if (prepareToHydrateHostInstance(workInProgress, rootContainerInstance, currentHostContext)) {\n              // If changes to the hydrated node needs to be applied at the\n              // commit-phase we mark this as such.\n              markUpdate(workInProgress);\n            }\n          } else {\n            var instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress);\n\n            appendAllChildren(instance, workInProgress, false, false);\n\n            // Certain renderers require commit-time effects for initial mount.\n            // (eg DOM renderer supports auto-focus for certain elements).\n            // Make sure such renderers get scheduled for later work.\n            if (finalizeInitialChildren(instance, type, newProps, rootContainerInstance, currentHostContext)) {\n              markUpdate(workInProgress);\n            }\n            workInProgress.stateNode = instance;\n          }\n\n          if (workInProgress.ref !== null) {\n            // If there is a ref on a host node we need to schedule a callback\n            markRef$1(workInProgress);\n          }\n        }\n        break;\n      }\n    case HostText:\n      {\n        var newText = newProps;\n        if (current && workInProgress.stateNode != null) {\n          var oldText = current.memoizedProps;\n          // If we have an alternate, that means this is an update and we need\n          // to schedule a side-effect to do the updates.\n          updateHostText$1(current, workInProgress, oldText, newText);\n        } else {\n          if (typeof newText !== 'string') {\n            !(workInProgress.stateNode !== null) ? invariant(false, 'We must have new props for new mounts. This error is likely caused by a bug in React. Please file an issue.') : void 0;\n            // This can happen when we abort work.\n          }\n          var _rootContainerInstance = getRootHostContainer();\n          var _currentHostContext = getHostContext();\n          var _wasHydrated = popHydrationState(workInProgress);\n          if (_wasHydrated) {\n            if (prepareToHydrateHostTextInstance(workInProgress)) {\n              markUpdate(workInProgress);\n            }\n          } else {\n            workInProgress.stateNode = createTextInstance(newText, _rootContainerInstance, _currentHostContext, workInProgress);\n          }\n        }\n        break;\n      }\n    case ForwardRef:\n      break;\n    case SuspenseComponent:\n      {\n        var nextState = workInProgress.memoizedState;\n        if ((workInProgress.effectTag & DidCapture) !== NoEffect) {\n          // Something suspended. Re-render with the fallback children.\n          workInProgress.expirationTime = renderExpirationTime;\n          // Do not reset the effect list.\n          return workInProgress;\n        }\n\n        var nextDidTimeout = nextState !== null;\n        var prevDidTimeout = current !== null && current.memoizedState !== null;\n\n        if (current !== null && !nextDidTimeout && prevDidTimeout) {\n          // We just switched from the fallback to the normal children. Delete\n          // the fallback.\n          // TODO: Would it be better to store the fallback fragment on\n          var currentFallbackChild = current.child.sibling;\n          if (currentFallbackChild !== null) {\n            // Deletions go at the beginning of the return fiber's effect list\n            var first = workInProgress.firstEffect;\n            if (first !== null) {\n              workInProgress.firstEffect = currentFallbackChild;\n              currentFallbackChild.nextEffect = first;\n            } else {\n              workInProgress.firstEffect = workInProgress.lastEffect = currentFallbackChild;\n              currentFallbackChild.nextEffect = null;\n            }\n            currentFallbackChild.effectTag = Deletion;\n          }\n        }\n\n        if (nextDidTimeout || prevDidTimeout) {\n          // If the children are hidden, or if they were previous hidden, schedule\n          // an effect to toggle their visibility. This is also used to attach a\n          // retry listener to the promise.\n          workInProgress.effectTag |= Update;\n        }\n        break;\n      }\n    case Fragment:\n      break;\n    case Mode:\n      break;\n    case Profiler:\n      break;\n    case HostPortal:\n      popHostContainer(workInProgress);\n      updateHostContainer(workInProgress);\n      break;\n    case ContextProvider:\n      // Pop provider fiber\n      popProvider(workInProgress);\n      break;\n    case ContextConsumer:\n      break;\n    case MemoComponent:\n      break;\n    case IncompleteClassComponent:\n      {\n        // Same as class component case. I put it down here so that the tags are\n        // sequential to ensure this switch is compiled to a jump table.\n        var _Component = workInProgress.type;\n        if (isContextProvider(_Component)) {\n          popContext(workInProgress);\n        }\n        break;\n      }\n    case DehydratedSuspenseComponent:\n      {\n        if (enableSuspenseServerRenderer) {\n          if (current === null) {\n            var _wasHydrated2 = popHydrationState(workInProgress);\n            !_wasHydrated2 ? invariant(false, 'A dehydrated suspense component was completed without a hydrated node. This is probably a bug in React.') : void 0;\n            skipPastDehydratedSuspenseInstance(workInProgress);\n          } else if ((workInProgress.effectTag & DidCapture) === NoEffect) {\n            // This boundary did not suspend so it's now hydrated.\n            // To handle any future suspense cases, we're going to now upgrade it\n            // to a Suspense component. We detach it from the existing current fiber.\n            current.alternate = null;\n            workInProgress.alternate = null;\n            workInProgress.tag = SuspenseComponent;\n            workInProgress.memoizedState = null;\n            workInProgress.stateNode = null;\n          }\n        }\n        break;\n      }\n    default:\n      invariant(false, 'Unknown unit of work tag. This error is likely caused by a bug in React. Please file an issue.');\n  }\n\n  return null;\n}\n\nfunction shouldCaptureSuspense(workInProgress) {\n  // In order to capture, the Suspense component must have a fallback prop.\n  if (workInProgress.memoizedProps.fallback === undefined) {\n    return false;\n  }\n  // If it was the primary children that just suspended, capture and render the\n  // fallback. Otherwise, don't capture and bubble to the next boundary.\n  var nextState = workInProgress.memoizedState;\n  return nextState === null;\n}\n\n// This module is forked in different environments.\n// By default, return `true` to log errors to the console.\n// Forks can return `false` if this isn't desirable.\nfunction showErrorDialog(capturedError) {\n  return true;\n}\n\nfunction logCapturedError(capturedError) {\n  var logError = showErrorDialog(capturedError);\n\n  // Allow injected showErrorDialog() to prevent default console.error logging.\n  // This enables renderers like ReactNative to better manage redbox behavior.\n  if (logError === false) {\n    return;\n  }\n\n  var error = capturedError.error;\n  {\n    var componentName = capturedError.componentName,\n        componentStack = capturedError.componentStack,\n        errorBoundaryName = capturedError.errorBoundaryName,\n        errorBoundaryFound = capturedError.errorBoundaryFound,\n        willRetry = capturedError.willRetry;\n\n    // Browsers support silencing uncaught errors by calling\n    // `preventDefault()` in window `error` handler.\n    // We record this information as an expando on the error.\n\n    if (error != null && error._suppressLogging) {\n      if (errorBoundaryFound && willRetry) {\n        // The error is recoverable and was silenced.\n        // Ignore it and don't print the stack addendum.\n        // This is handy for testing error boundaries without noise.\n        return;\n      }\n      // The error is fatal. Since the silencing might have\n      // been accidental, we'll surface it anyway.\n      // However, the browser would have silenced the original error\n      // so we'll print it first, and then print the stack addendum.\n      console.error(error);\n      // For a more detailed description of this block, see:\n      // https://github.com/facebook/react/pull/13384\n    }\n\n    var componentNameMessage = componentName ? 'The above error occurred in the <' + componentName + '> component:' : 'The above error occurred in one of your React components:';\n\n    var errorBoundaryMessage = void 0;\n    // errorBoundaryFound check is sufficient; errorBoundaryName check is to satisfy Flow.\n    if (errorBoundaryFound && errorBoundaryName) {\n      if (willRetry) {\n        errorBoundaryMessage = 'React will try to recreate this component tree from scratch ' + ('using the error boundary you provided, ' + errorBoundaryName + '.');\n      } else {\n        errorBoundaryMessage = 'This error was initially handled by the error boundary ' + errorBoundaryName + '.\\n' + 'Recreating the tree from scratch failed so React will unmount the tree.';\n      }\n    } else {\n      errorBoundaryMessage = 'Consider adding an error boundary to your tree to customize error handling behavior.\\n' + 'Visit https://fb.me/react-error-boundaries to learn more about error boundaries.';\n    }\n    var combinedMessage = '' + componentNameMessage + componentStack + '\\n\\n' + ('' + errorBoundaryMessage);\n\n    // In development, we provide our own message with just the component stack.\n    // We don't include the original error message and JS stack because the browser\n    // has already printed it. Even if the application swallows the error, it is still\n    // displayed by the browser thanks to the DEV-only fake event trick in ReactErrorUtils.\n    console.error(combinedMessage);\n  }\n}\n\nvar didWarnAboutUndefinedSnapshotBeforeUpdate = null;\n{\n  didWarnAboutUndefinedSnapshotBeforeUpdate = new Set();\n}\n\nvar PossiblyWeakSet$1 = typeof WeakSet === 'function' ? WeakSet : Set;\n\nfunction logError(boundary, errorInfo) {\n  var source = errorInfo.source;\n  var stack = errorInfo.stack;\n  if (stack === null && source !== null) {\n    stack = getStackByFiberInDevAndProd(source);\n  }\n\n  var capturedError = {\n    componentName: source !== null ? getComponentName(source.type) : null,\n    componentStack: stack !== null ? stack : '',\n    error: errorInfo.value,\n    errorBoundary: null,\n    errorBoundaryName: null,\n    errorBoundaryFound: false,\n    willRetry: false\n  };\n\n  if (boundary !== null && boundary.tag === ClassComponent) {\n    capturedError.errorBoundary = boundary.stateNode;\n    capturedError.errorBoundaryName = getComponentName(boundary.type);\n    capturedError.errorBoundaryFound = true;\n    capturedError.willRetry = true;\n  }\n\n  try {\n    logCapturedError(capturedError);\n  } catch (e) {\n    // This method must not throw, or React internal state will get messed up.\n    // If console.error is overridden, or logCapturedError() shows a dialog that throws,\n    // we want to report this error outside of the normal stack as a last resort.\n    // https://github.com/facebook/react/issues/13188\n    setTimeout(function () {\n      throw e;\n    });\n  }\n}\n\nvar callComponentWillUnmountWithTimer = function (current$$1, instance) {\n  startPhaseTimer(current$$1, 'componentWillUnmount');\n  instance.props = current$$1.memoizedProps;\n  instance.state = current$$1.memoizedState;\n  instance.componentWillUnmount();\n  stopPhaseTimer();\n};\n\n// Capture errors so they don't interrupt unmounting.\nfunction safelyCallComponentWillUnmount(current$$1, instance) {\n  {\n    invokeGuardedCallback(null, callComponentWillUnmountWithTimer, null, current$$1, instance);\n    if (hasCaughtError()) {\n      var unmountError = clearCaughtError();\n      captureCommitPhaseError(current$$1, unmountError);\n    }\n  }\n}\n\nfunction safelyDetachRef(current$$1) {\n  var ref = current$$1.ref;\n  if (ref !== null) {\n    if (typeof ref === 'function') {\n      {\n        invokeGuardedCallback(null, ref, null, null);\n        if (hasCaughtError()) {\n          var refError = clearCaughtError();\n          captureCommitPhaseError(current$$1, refError);\n        }\n      }\n    } else {\n      ref.current = null;\n    }\n  }\n}\n\nfunction safelyCallDestroy(current$$1, destroy) {\n  {\n    invokeGuardedCallback(null, destroy, null);\n    if (hasCaughtError()) {\n      var error = clearCaughtError();\n      captureCommitPhaseError(current$$1, error);\n    }\n  }\n}\n\nfunction commitBeforeMutationLifeCycles(current$$1, finishedWork) {\n  switch (finishedWork.tag) {\n    case FunctionComponent:\n    case ForwardRef:\n    case SimpleMemoComponent:\n      {\n        commitHookEffectList(UnmountSnapshot, NoEffect$1, finishedWork);\n        return;\n      }\n    case ClassComponent:\n      {\n        if (finishedWork.effectTag & Snapshot) {\n          if (current$$1 !== null) {\n            var prevProps = current$$1.memoizedProps;\n            var prevState = current$$1.memoizedState;\n            startPhaseTimer(finishedWork, 'getSnapshotBeforeUpdate');\n            var instance = finishedWork.stateNode;\n            // We could update instance props and state here,\n            // but instead we rely on them being set during last render.\n            // TODO: revisit this when we implement resuming.\n            {\n              if (finishedWork.type === finishedWork.elementType && !didWarnAboutReassigningProps) {\n                !(instance.props === finishedWork.memoizedProps) ? warning$1(false, 'Expected %s props to match memoized props before ' + 'getSnapshotBeforeUpdate. ' + 'This might either be because of a bug in React, or because ' + 'a component reassigns its own `this.props`. ' + 'Please file an issue.', getComponentName(finishedWork.type) || 'instance') : void 0;\n                !(instance.state === finishedWork.memoizedState) ? warning$1(false, 'Expected %s state to match memoized state before ' + 'getSnapshotBeforeUpdate. ' + 'This might either be because of a bug in React, or because ' + 'a component reassigns its own `this.props`. ' + 'Please file an issue.', getComponentName(finishedWork.type) || 'instance') : void 0;\n              }\n            }\n            var snapshot = instance.getSnapshotBeforeUpdate(finishedWork.elementType === finishedWork.type ? prevProps : resolveDefaultProps(finishedWork.type, prevProps), prevState);\n            {\n              var didWarnSet = didWarnAboutUndefinedSnapshotBeforeUpdate;\n              if (snapshot === undefined && !didWarnSet.has(finishedWork.type)) {\n                didWarnSet.add(finishedWork.type);\n                warningWithoutStack$1(false, '%s.getSnapshotBeforeUpdate(): A snapshot value (or null) ' + 'must be returned. You have returned undefined.', getComponentName(finishedWork.type));\n              }\n            }\n            instance.__reactInternalSnapshotBeforeUpdate = snapshot;\n            stopPhaseTimer();\n          }\n        }\n        return;\n      }\n    case HostRoot:\n    case HostComponent:\n    case HostText:\n    case HostPortal:\n    case IncompleteClassComponent:\n      // Nothing to do for these component types\n      return;\n    default:\n      {\n        invariant(false, 'This unit of work tag should not have side-effects. This error is likely caused by a bug in React. Please file an issue.');\n      }\n  }\n}\n\nfunction commitHookEffectList(unmountTag, mountTag, finishedWork) {\n  var updateQueue = finishedWork.updateQueue;\n  var lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;\n  if (lastEffect !== null) {\n    var firstEffect = lastEffect.next;\n    var effect = firstEffect;\n    do {\n      if ((effect.tag & unmountTag) !== NoEffect$1) {\n        // Unmount\n        var destroy = effect.destroy;\n        effect.destroy = undefined;\n        if (destroy !== undefined) {\n          destroy();\n        }\n      }\n      if ((effect.tag & mountTag) !== NoEffect$1) {\n        // Mount\n        var create = effect.create;\n        effect.destroy = create();\n\n        {\n          var _destroy = effect.destroy;\n          if (_destroy !== undefined && typeof _destroy !== 'function') {\n            var addendum = void 0;\n            if (_destroy === null) {\n              addendum = ' You returned null. If your effect does not require clean ' + 'up, return undefined (or nothing).';\n            } else if (typeof _destroy.then === 'function') {\n              addendum = '\\n\\nIt looks like you wrote useEffect(async () => ...) or returned a Promise. ' + 'Instead, write the async function inside your effect ' + 'and call it immediately:\\n\\n' + 'useEffect(() => {\\n' + '  async function fetchData() {\\n' + '    // You can await here\\n' + '    const response = await MyAPI.getData(someId);\\n' + '    // ...\\n' + '  }\\n' + '  fetchData();\\n' + '}, [someId]); // Or [] if effect doesn\\'t need props or state\\n\\n' + 'Learn more about data fetching with Hooks: https://fb.me/react-hooks-data-fetching';\n            } else {\n              addendum = ' You returned: ' + _destroy;\n            }\n            warningWithoutStack$1(false, 'An effect function must not return anything besides a function, ' + 'which is used for clean-up.%s%s', addendum, getStackByFiberInDevAndProd(finishedWork));\n          }\n        }\n      }\n      effect = effect.next;\n    } while (effect !== firstEffect);\n  }\n}\n\nfunction commitPassiveHookEffects(finishedWork) {\n  commitHookEffectList(UnmountPassive, NoEffect$1, finishedWork);\n  commitHookEffectList(NoEffect$1, MountPassive, finishedWork);\n}\n\nfunction commitLifeCycles(finishedRoot, current$$1, finishedWork, committedExpirationTime) {\n  switch (finishedWork.tag) {\n    case FunctionComponent:\n    case ForwardRef:\n    case SimpleMemoComponent:\n      {\n        commitHookEffectList(UnmountLayout, MountLayout, finishedWork);\n        break;\n      }\n    case ClassComponent:\n      {\n        var instance = finishedWork.stateNode;\n        if (finishedWork.effectTag & Update) {\n          if (current$$1 === null) {\n            startPhaseTimer(finishedWork, 'componentDidMount');\n            // We could update instance props and state here,\n            // but instead we rely on them being set during last render.\n            // TODO: revisit this when we implement resuming.\n            {\n              if (finishedWork.type === finishedWork.elementType && !didWarnAboutReassigningProps) {\n                !(instance.props === finishedWork.memoizedProps) ? warning$1(false, 'Expected %s props to match memoized props before ' + 'componentDidMount. ' + 'This might either be because of a bug in React, or because ' + 'a component reassigns its own `this.props`. ' + 'Please file an issue.', getComponentName(finishedWork.type) || 'instance') : void 0;\n                !(instance.state === finishedWork.memoizedState) ? warning$1(false, 'Expected %s state to match memoized state before ' + 'componentDidMount. ' + 'This might either be because of a bug in React, or because ' + 'a component reassigns its own `this.props`. ' + 'Please file an issue.', getComponentName(finishedWork.type) || 'instance') : void 0;\n              }\n            }\n            instance.componentDidMount();\n            stopPhaseTimer();\n          } else {\n            var prevProps = finishedWork.elementType === finishedWork.type ? current$$1.memoizedProps : resolveDefaultProps(finishedWork.type, current$$1.memoizedProps);\n            var prevState = current$$1.memoizedState;\n            startPhaseTimer(finishedWork, 'componentDidUpdate');\n            // We could update instance props and state here,\n            // but instead we rely on them being set during last render.\n            // TODO: revisit this when we implement resuming.\n            {\n              if (finishedWork.type === finishedWork.elementType && !didWarnAboutReassigningProps) {\n                !(instance.props === finishedWork.memoizedProps) ? warning$1(false, 'Expected %s props to match memoized props before ' + 'componentDidUpdate. ' + 'This might either be because of a bug in React, or because ' + 'a component reassigns its own `this.props`. ' + 'Please file an issue.', getComponentName(finishedWork.type) || 'instance') : void 0;\n                !(instance.state === finishedWork.memoizedState) ? warning$1(false, 'Expected %s state to match memoized state before ' + 'componentDidUpdate. ' + 'This might either be because of a bug in React, or because ' + 'a component reassigns its own `this.props`. ' + 'Please file an issue.', getComponentName(finishedWork.type) || 'instance') : void 0;\n              }\n            }\n            instance.componentDidUpdate(prevProps, prevState, instance.__reactInternalSnapshotBeforeUpdate);\n            stopPhaseTimer();\n          }\n        }\n        var updateQueue = finishedWork.updateQueue;\n        if (updateQueue !== null) {\n          {\n            if (finishedWork.type === finishedWork.elementType && !didWarnAboutReassigningProps) {\n              !(instance.props === finishedWork.memoizedProps) ? warning$1(false, 'Expected %s props to match memoized props before ' + 'processing the update queue. ' + 'This might either be because of a bug in React, or because ' + 'a component reassigns its own `this.props`. ' + 'Please file an issue.', getComponentName(finishedWork.type) || 'instance') : void 0;\n              !(instance.state === finishedWork.memoizedState) ? warning$1(false, 'Expected %s state to match memoized state before ' + 'processing the update queue. ' + 'This might either be because of a bug in React, or because ' + 'a component reassigns its own `this.props`. ' + 'Please file an issue.', getComponentName(finishedWork.type) || 'instance') : void 0;\n            }\n          }\n          // We could update instance props and state here,\n          // but instead we rely on them being set during last render.\n          // TODO: revisit this when we implement resuming.\n          commitUpdateQueue(finishedWork, updateQueue, instance, committedExpirationTime);\n        }\n        return;\n      }\n    case HostRoot:\n      {\n        var _updateQueue = finishedWork.updateQueue;\n        if (_updateQueue !== null) {\n          var _instance = null;\n          if (finishedWork.child !== null) {\n            switch (finishedWork.child.tag) {\n              case HostComponent:\n                _instance = getPublicInstance(finishedWork.child.stateNode);\n                break;\n              case ClassComponent:\n                _instance = finishedWork.child.stateNode;\n                break;\n            }\n          }\n          commitUpdateQueue(finishedWork, _updateQueue, _instance, committedExpirationTime);\n        }\n        return;\n      }\n    case HostComponent:\n      {\n        var _instance2 = finishedWork.stateNode;\n\n        // Renderers may schedule work to be done after host components are mounted\n        // (eg DOM renderer may schedule auto-focus for inputs and form controls).\n        // These effects should only be committed when components are first mounted,\n        // aka when there is no current/alternate.\n        if (current$$1 === null && finishedWork.effectTag & Update) {\n          var type = finishedWork.type;\n          var props = finishedWork.memoizedProps;\n          commitMount(_instance2, type, props, finishedWork);\n        }\n\n        return;\n      }\n    case HostText:\n      {\n        // We have no life-cycles associated with text.\n        return;\n      }\n    case HostPortal:\n      {\n        // We have no life-cycles associated with portals.\n        return;\n      }\n    case Profiler:\n      {\n        if (enableProfilerTimer) {\n          var onRender = finishedWork.memoizedProps.onRender;\n\n          if (enableSchedulerTracing) {\n            onRender(finishedWork.memoizedProps.id, current$$1 === null ? 'mount' : 'update', finishedWork.actualDuration, finishedWork.treeBaseDuration, finishedWork.actualStartTime, getCommitTime(), finishedRoot.memoizedInteractions);\n          } else {\n            onRender(finishedWork.memoizedProps.id, current$$1 === null ? 'mount' : 'update', finishedWork.actualDuration, finishedWork.treeBaseDuration, finishedWork.actualStartTime, getCommitTime());\n          }\n        }\n        return;\n      }\n    case SuspenseComponent:\n      break;\n    case IncompleteClassComponent:\n      break;\n    default:\n      {\n        invariant(false, 'This unit of work tag should not have side-effects. This error is likely caused by a bug in React. Please file an issue.');\n      }\n  }\n}\n\nfunction hideOrUnhideAllChildren(finishedWork, isHidden) {\n  if (supportsMutation) {\n    // We only have the top Fiber that was inserted but we need to recurse down its\n    var node = finishedWork;\n    while (true) {\n      if (node.tag === HostComponent) {\n        var instance = node.stateNode;\n        if (isHidden) {\n          hideInstance(instance);\n        } else {\n          unhideInstance(node.stateNode, node.memoizedProps);\n        }\n      } else if (node.tag === HostText) {\n        var _instance3 = node.stateNode;\n        if (isHidden) {\n          hideTextInstance(_instance3);\n        } else {\n          unhideTextInstance(_instance3, node.memoizedProps);\n        }\n      } else if (node.tag === SuspenseComponent && node.memoizedState !== null) {\n        // Found a nested Suspense component that timed out. Skip over the\n        var fallbackChildFragment = node.child.sibling;\n        fallbackChildFragment.return = node;\n        node = fallbackChildFragment;\n        continue;\n      } else if (node.child !== null) {\n        node.child.return = node;\n        node = node.child;\n        continue;\n      }\n      if (node === finishedWork) {\n        return;\n      }\n      while (node.sibling === null) {\n        if (node.return === null || node.return === finishedWork) {\n          return;\n        }\n        node = node.return;\n      }\n      node.sibling.return = node.return;\n      node = node.sibling;\n    }\n  }\n}\n\nfunction commitAttachRef(finishedWork) {\n  var ref = finishedWork.ref;\n  if (ref !== null) {\n    var instance = finishedWork.stateNode;\n    var instanceToUse = void 0;\n    switch (finishedWork.tag) {\n      case HostComponent:\n        instanceToUse = getPublicInstance(instance);\n        break;\n      default:\n        instanceToUse = instance;\n    }\n    if (typeof ref === 'function') {\n      ref(instanceToUse);\n    } else {\n      {\n        if (!ref.hasOwnProperty('current')) {\n          warningWithoutStack$1(false, 'Unexpected ref object provided for %s. ' + 'Use either a ref-setter function or React.createRef().%s', getComponentName(finishedWork.type), getStackByFiberInDevAndProd(finishedWork));\n        }\n      }\n\n      ref.current = instanceToUse;\n    }\n  }\n}\n\nfunction commitDetachRef(current$$1) {\n  var currentRef = current$$1.ref;\n  if (currentRef !== null) {\n    if (typeof currentRef === 'function') {\n      currentRef(null);\n    } else {\n      currentRef.current = null;\n    }\n  }\n}\n\n// User-originating errors (lifecycles and refs) should not interrupt\n// deletion, so don't let them throw. Host-originating errors should\n// interrupt deletion, so it's okay\nfunction commitUnmount(current$$1) {\n  onCommitUnmount(current$$1);\n\n  switch (current$$1.tag) {\n    case FunctionComponent:\n    case ForwardRef:\n    case MemoComponent:\n    case SimpleMemoComponent:\n      {\n        var updateQueue = current$$1.updateQueue;\n        if (updateQueue !== null) {\n          var lastEffect = updateQueue.lastEffect;\n          if (lastEffect !== null) {\n            var firstEffect = lastEffect.next;\n            var effect = firstEffect;\n            do {\n              var destroy = effect.destroy;\n              if (destroy !== undefined) {\n                safelyCallDestroy(current$$1, destroy);\n              }\n              effect = effect.next;\n            } while (effect !== firstEffect);\n          }\n        }\n        break;\n      }\n    case ClassComponent:\n      {\n        safelyDetachRef(current$$1);\n        var instance = current$$1.stateNode;\n        if (typeof instance.componentWillUnmount === 'function') {\n          safelyCallComponentWillUnmount(current$$1, instance);\n        }\n        return;\n      }\n    case HostComponent:\n      {\n        safelyDetachRef(current$$1);\n        return;\n      }\n    case HostPortal:\n      {\n        // TODO: this is recursive.\n        // We are also not using this parent because\n        // the portal will get pushed immediately.\n        if (supportsMutation) {\n          unmountHostComponents(current$$1);\n        } else if (supportsPersistence) {\n          emptyPortalContainer(current$$1);\n        }\n        return;\n      }\n  }\n}\n\nfunction commitNestedUnmounts(root) {\n  // While we're inside a removed host node we don't want to call\n  // removeChild on the inner nodes because they're removed by the top\n  // call anyway. We also want to call componentWillUnmount on all\n  // composites before this host node is removed from the tree. Therefore\n  var node = root;\n  while (true) {\n    commitUnmount(node);\n    // Visit children because they may contain more composite or host nodes.\n    // Skip portals because commitUnmount() currently visits them recursively.\n    if (node.child !== null && (\n    // If we use mutation we drill down into portals using commitUnmount above.\n    // If we don't use mutation we drill down into portals here instead.\n    !supportsMutation || node.tag !== HostPortal)) {\n      node.child.return = node;\n      node = node.child;\n      continue;\n    }\n    if (node === root) {\n      return;\n    }\n    while (node.sibling === null) {\n      if (node.return === null || node.return === root) {\n        return;\n      }\n      node = node.return;\n    }\n    node.sibling.return = node.return;\n    node = node.sibling;\n  }\n}\n\nfunction detachFiber(current$$1) {\n  // Cut off the return pointers to disconnect it from the tree. Ideally, we\n  // should clear the child pointer of the parent alternate to let this\n  // get GC:ed but we don't know which for sure which parent is the current\n  // one so we'll settle for GC:ing the subtree of this child. This child\n  // itself will be GC:ed when the parent updates the next time.\n  current$$1.return = null;\n  current$$1.child = null;\n  current$$1.memoizedState = null;\n  current$$1.updateQueue = null;\n  var alternate = current$$1.alternate;\n  if (alternate !== null) {\n    alternate.return = null;\n    alternate.child = null;\n    alternate.memoizedState = null;\n    alternate.updateQueue = null;\n  }\n}\n\nfunction emptyPortalContainer(current$$1) {\n  if (!supportsPersistence) {\n    return;\n  }\n\n  var portal = current$$1.stateNode;\n  var containerInfo = portal.containerInfo;\n\n  var emptyChildSet = createContainerChildSet(containerInfo);\n  replaceContainerChildren(containerInfo, emptyChildSet);\n}\n\nfunction commitContainer(finishedWork) {\n  if (!supportsPersistence) {\n    return;\n  }\n\n  switch (finishedWork.tag) {\n    case ClassComponent:\n      {\n        return;\n      }\n    case HostComponent:\n      {\n        return;\n      }\n    case HostText:\n      {\n        return;\n      }\n    case HostRoot:\n    case HostPortal:\n      {\n        var portalOrRoot = finishedWork.stateNode;\n        var containerInfo = portalOrRoot.containerInfo,\n            _pendingChildren = portalOrRoot.pendingChildren;\n\n        replaceContainerChildren(containerInfo, _pendingChildren);\n        return;\n      }\n    default:\n      {\n        invariant(false, 'This unit of work tag should not have side-effects. This error is likely caused by a bug in React. Please file an issue.');\n      }\n  }\n}\n\nfunction getHostParentFiber(fiber) {\n  var parent = fiber.return;\n  while (parent !== null) {\n    if (isHostParent(parent)) {\n      return parent;\n    }\n    parent = parent.return;\n  }\n  invariant(false, 'Expected to find a host parent. This error is likely caused by a bug in React. Please file an issue.');\n}\n\nfunction isHostParent(fiber) {\n  return fiber.tag === HostComponent || fiber.tag === HostRoot || fiber.tag === HostPortal;\n}\n\nfunction getHostSibling(fiber) {\n  // We're going to search forward into the tree until we find a sibling host\n  // node. Unfortunately, if multiple insertions are done in a row we have to\n  // search past them. This leads to exponential search for the next sibling.\n  var node = fiber;\n  siblings: while (true) {\n    // If we didn't find anything, let's try the next sibling.\n    while (node.sibling === null) {\n      if (node.return === null || isHostParent(node.return)) {\n        // If we pop out of the root or hit the parent the fiber we are the\n        // last sibling.\n        return null;\n      }\n      node = node.return;\n    }\n    node.sibling.return = node.return;\n    node = node.sibling;\n    while (node.tag !== HostComponent && node.tag !== HostText && node.tag !== DehydratedSuspenseComponent) {\n      // If it is not host node and, we might have a host node inside it.\n      // Try to search down until we find one.\n      if (node.effectTag & Placement) {\n        // If we don't have a child, try the siblings instead.\n        continue siblings;\n      }\n      // If we don't have a child, try the siblings instead.\n      // We also skip portals because they are not part of this host tree.\n      if (node.child === null || node.tag === HostPortal) {\n        continue siblings;\n      } else {\n        node.child.return = node;\n        node = node.child;\n      }\n    }\n    // Check if this host node is stable or about to be placed.\n    if (!(node.effectTag & Placement)) {\n      // Found it!\n      return node.stateNode;\n    }\n  }\n}\n\nfunction commitPlacement(finishedWork) {\n  if (!supportsMutation) {\n    return;\n  }\n\n  // Recursively insert all host nodes into the parent.\n  var parentFiber = getHostParentFiber(finishedWork);\n\n  // Note: these two variables *must* always be updated together.\n  var parent = void 0;\n  var isContainer = void 0;\n\n  switch (parentFiber.tag) {\n    case HostComponent:\n      parent = parentFiber.stateNode;\n      isContainer = false;\n      break;\n    case HostRoot:\n      parent = parentFiber.stateNode.containerInfo;\n      isContainer = true;\n      break;\n    case HostPortal:\n      parent = parentFiber.stateNode.containerInfo;\n      isContainer = true;\n      break;\n    default:\n      invariant(false, 'Invalid host parent fiber. This error is likely caused by a bug in React. Please file an issue.');\n  }\n  if (parentFiber.effectTag & ContentReset) {\n    // Reset the text content of the parent before doing any insertions\n    resetTextContent(parent);\n    // Clear ContentReset from the effect tag\n    parentFiber.effectTag &= ~ContentReset;\n  }\n\n  var before = getHostSibling(finishedWork);\n  // We only have the top Fiber that was inserted but we need to recurse down its\n  // children to find all the terminal nodes.\n  var node = finishedWork;\n  while (true) {\n    if (node.tag === HostComponent || node.tag === HostText) {\n      if (before) {\n        if (isContainer) {\n          insertInContainerBefore(parent, node.stateNode, before);\n        } else {\n          insertBefore(parent, node.stateNode, before);\n        }\n      } else {\n        if (isContainer) {\n          appendChildToContainer(parent, node.stateNode);\n        } else {\n          appendChild(parent, node.stateNode);\n        }\n      }\n    } else if (node.tag === HostPortal) {\n      // If the insertion itself is a portal, then we don't want to traverse\n      // down its children. Instead, we'll get insertions from each child in\n      // the portal directly.\n    } else if (node.child !== null) {\n      node.child.return = node;\n      node = node.child;\n      continue;\n    }\n    if (node === finishedWork) {\n      return;\n    }\n    while (node.sibling === null) {\n      if (node.return === null || node.return === finishedWork) {\n        return;\n      }\n      node = node.return;\n    }\n    node.sibling.return = node.return;\n    node = node.sibling;\n  }\n}\n\nfunction unmountHostComponents(current$$1) {\n  // We only have the top Fiber that was deleted but we need to recurse down its\n  var node = current$$1;\n\n  // Each iteration, currentParent is populated with node's host parent if not\n  // currentParentIsValid.\n  var currentParentIsValid = false;\n\n  // Note: these two variables *must* always be updated together.\n  var currentParent = void 0;\n  var currentParentIsContainer = void 0;\n\n  while (true) {\n    if (!currentParentIsValid) {\n      var parent = node.return;\n      findParent: while (true) {\n        !(parent !== null) ? invariant(false, 'Expected to find a host parent. This error is likely caused by a bug in React. Please file an issue.') : void 0;\n        switch (parent.tag) {\n          case HostComponent:\n            currentParent = parent.stateNode;\n            currentParentIsContainer = false;\n            break findParent;\n          case HostRoot:\n            currentParent = parent.stateNode.containerInfo;\n            currentParentIsContainer = true;\n            break findParent;\n          case HostPortal:\n            currentParent = parent.stateNode.containerInfo;\n            currentParentIsContainer = true;\n            break findParent;\n        }\n        parent = parent.return;\n      }\n      currentParentIsValid = true;\n    }\n\n    if (node.tag === HostComponent || node.tag === HostText) {\n      commitNestedUnmounts(node);\n      // After all the children have unmounted, it is now safe to remove the\n      // node from the tree.\n      if (currentParentIsContainer) {\n        removeChildFromContainer(currentParent, node.stateNode);\n      } else {\n        removeChild(currentParent, node.stateNode);\n      }\n      // Don't visit children because we already visited them.\n    } else if (enableSuspenseServerRenderer && node.tag === DehydratedSuspenseComponent) {\n      // Delete the dehydrated suspense boundary and all of its content.\n      if (currentParentIsContainer) {\n        clearSuspenseBoundaryFromContainer(currentParent, node.stateNode);\n      } else {\n        clearSuspenseBoundary(currentParent, node.stateNode);\n      }\n    } else if (node.tag === HostPortal) {\n      if (node.child !== null) {\n        // When we go into a portal, it becomes the parent to remove from.\n        // We will reassign it back when we pop the portal on the way up.\n        currentParent = node.stateNode.containerInfo;\n        currentParentIsContainer = true;\n        // Visit children because portals might contain host components.\n        node.child.return = node;\n        node = node.child;\n        continue;\n      }\n    } else {\n      commitUnmount(node);\n      // Visit children because we may find more host components below.\n      if (node.child !== null) {\n        node.child.return = node;\n        node = node.child;\n        continue;\n      }\n    }\n    if (node === current$$1) {\n      return;\n    }\n    while (node.sibling === null) {\n      if (node.return === null || node.return === current$$1) {\n        return;\n      }\n      node = node.return;\n      if (node.tag === HostPortal) {\n        // When we go out of the portal, we need to restore the parent.\n        // Since we don't keep a stack of them, we will search for it.\n        currentParentIsValid = false;\n      }\n    }\n    node.sibling.return = node.return;\n    node = node.sibling;\n  }\n}\n\nfunction commitDeletion(current$$1) {\n  if (supportsMutation) {\n    // Recursively delete all host nodes from the parent.\n    // Detach refs and call componentWillUnmount() on the whole subtree.\n    unmountHostComponents(current$$1);\n  } else {\n    // Detach refs and call componentWillUnmount() on the whole subtree.\n    commitNestedUnmounts(current$$1);\n  }\n  detachFiber(current$$1);\n}\n\nfunction commitWork(current$$1, finishedWork) {\n  if (!supportsMutation) {\n    switch (finishedWork.tag) {\n      case FunctionComponent:\n      case ForwardRef:\n      case MemoComponent:\n      case SimpleMemoComponent:\n        {\n          // Note: We currently never use MountMutation, but useLayout uses\n          // UnmountMutation.\n          commitHookEffectList(UnmountMutation, MountMutation, finishedWork);\n          return;\n        }\n    }\n\n    commitContainer(finishedWork);\n    return;\n  }\n\n  switch (finishedWork.tag) {\n    case FunctionComponent:\n    case ForwardRef:\n    case MemoComponent:\n    case SimpleMemoComponent:\n      {\n        // Note: We currently never use MountMutation, but useLayout uses\n        // UnmountMutation.\n        commitHookEffectList(UnmountMutation, MountMutation, finishedWork);\n        return;\n      }\n    case ClassComponent:\n      {\n        return;\n      }\n    case HostComponent:\n      {\n        var instance = finishedWork.stateNode;\n        if (instance != null) {\n          // Commit the work prepared earlier.\n          var newProps = finishedWork.memoizedProps;\n          // For hydration we reuse the update path but we treat the oldProps\n          // as the newProps. The updatePayload will contain the real change in\n          // this case.\n          var oldProps = current$$1 !== null ? current$$1.memoizedProps : newProps;\n          var type = finishedWork.type;\n          // TODO: Type the updateQueue to be specific to host components.\n          var updatePayload = finishedWork.updateQueue;\n          finishedWork.updateQueue = null;\n          if (updatePayload !== null) {\n            commitUpdate(instance, updatePayload, type, oldProps, newProps, finishedWork);\n          }\n        }\n        return;\n      }\n    case HostText:\n      {\n        !(finishedWork.stateNode !== null) ? invariant(false, 'This should have a text node initialized. This error is likely caused by a bug in React. Please file an issue.') : void 0;\n        var textInstance = finishedWork.stateNode;\n        var newText = finishedWork.memoizedProps;\n        // For hydration we reuse the update path but we treat the oldProps\n        // as the newProps. The updatePayload will contain the real change in\n        // this case.\n        var oldText = current$$1 !== null ? current$$1.memoizedProps : newText;\n        commitTextUpdate(textInstance, oldText, newText);\n        return;\n      }\n    case HostRoot:\n      {\n        return;\n      }\n    case Profiler:\n      {\n        return;\n      }\n    case SuspenseComponent:\n      {\n        var newState = finishedWork.memoizedState;\n\n        var newDidTimeout = void 0;\n        var primaryChildParent = finishedWork;\n        if (newState === null) {\n          newDidTimeout = false;\n        } else {\n          newDidTimeout = true;\n          primaryChildParent = finishedWork.child;\n          if (newState.timedOutAt === NoWork) {\n            // If the children had not already timed out, record the time.\n            // This is used to compute the elapsed time during subsequent\n            // attempts to render the children.\n            newState.timedOutAt = requestCurrentTime();\n          }\n        }\n\n        if (primaryChildParent !== null) {\n          hideOrUnhideAllChildren(primaryChildParent, newDidTimeout);\n        }\n\n        // If this boundary just timed out, then it will have a set of thenables.\n        // For each thenable, attach a listener so that when it resolves, React\n        // attempts to re-render the boundary in the primary (pre-timeout) state.\n        var thenables = finishedWork.updateQueue;\n        if (thenables !== null) {\n          finishedWork.updateQueue = null;\n          var retryCache = finishedWork.stateNode;\n          if (retryCache === null) {\n            retryCache = finishedWork.stateNode = new PossiblyWeakSet$1();\n          }\n          thenables.forEach(function (thenable) {\n            // Memoize using the boundary fiber to prevent redundant listeners.\n            var retry = retryTimedOutBoundary.bind(null, finishedWork, thenable);\n            if (enableSchedulerTracing) {\n              retry = unstable_wrap(retry);\n            }\n            if (!retryCache.has(thenable)) {\n              retryCache.add(thenable);\n              thenable.then(retry, retry);\n            }\n          });\n        }\n\n        return;\n      }\n    case IncompleteClassComponent:\n      {\n        return;\n      }\n    default:\n      {\n        invariant(false, 'This unit of work tag should not have side-effects. This error is likely caused by a bug in React. Please file an issue.');\n      }\n  }\n}\n\nfunction commitResetTextContent(current$$1) {\n  if (!supportsMutation) {\n    return;\n  }\n  resetTextContent(current$$1.stateNode);\n}\n\nvar PossiblyWeakSet = typeof WeakSet === 'function' ? WeakSet : Set;\nvar PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;\n\nfunction createRootErrorUpdate(fiber, errorInfo, expirationTime) {\n  var update = createUpdate(expirationTime);\n  // Unmount the root by rendering null.\n  update.tag = CaptureUpdate;\n  // Caution: React DevTools currently depends on this property\n  // being called \"element\".\n  update.payload = { element: null };\n  var error = errorInfo.value;\n  update.callback = function () {\n    onUncaughtError(error);\n    logError(fiber, errorInfo);\n  };\n  return update;\n}\n\nfunction createClassErrorUpdate(fiber, errorInfo, expirationTime) {\n  var update = createUpdate(expirationTime);\n  update.tag = CaptureUpdate;\n  var getDerivedStateFromError = fiber.type.getDerivedStateFromError;\n  if (typeof getDerivedStateFromError === 'function') {\n    var error = errorInfo.value;\n    update.payload = function () {\n      return getDerivedStateFromError(error);\n    };\n  }\n\n  var inst = fiber.stateNode;\n  if (inst !== null && typeof inst.componentDidCatch === 'function') {\n    update.callback = function callback() {\n      if (typeof getDerivedStateFromError !== 'function') {\n        // To preserve the preexisting retry behavior of error boundaries,\n        // we keep track of which ones already failed during this batch.\n        // This gets reset before we yield back to the browser.\n        // TODO: Warn in strict mode if getDerivedStateFromError is\n        // not defined.\n        markLegacyErrorBoundaryAsFailed(this);\n      }\n      var error = errorInfo.value;\n      var stack = errorInfo.stack;\n      logError(fiber, errorInfo);\n      this.componentDidCatch(error, {\n        componentStack: stack !== null ? stack : ''\n      });\n      {\n        if (typeof getDerivedStateFromError !== 'function') {\n          // If componentDidCatch is the only error boundary method defined,\n          // then it needs to call setState to recover from errors.\n          // If no state update is scheduled then the boundary will swallow the error.\n          !(fiber.expirationTime === Sync) ? warningWithoutStack$1(false, '%s: Error boundaries should implement getDerivedStateFromError(). ' + 'In that method, return a state update to display an error message or fallback UI.', getComponentName(fiber.type) || 'Unknown') : void 0;\n        }\n      }\n    };\n  }\n  return update;\n}\n\nfunction attachPingListener(root, renderExpirationTime, thenable) {\n  // Attach a listener to the promise to \"ping\" the root and retry. But\n  // only if one does not already exist for the current render expiration\n  // time (which acts like a \"thread ID\" here).\n  var pingCache = root.pingCache;\n  var threadIDs = void 0;\n  if (pingCache === null) {\n    pingCache = root.pingCache = new PossiblyWeakMap();\n    threadIDs = new Set();\n    pingCache.set(thenable, threadIDs);\n  } else {\n    threadIDs = pingCache.get(thenable);\n    if (threadIDs === undefined) {\n      threadIDs = new Set();\n      pingCache.set(thenable, threadIDs);\n    }\n  }\n  if (!threadIDs.has(renderExpirationTime)) {\n    // Memoize using the thread ID to prevent redundant listeners.\n    threadIDs.add(renderExpirationTime);\n    var ping = pingSuspendedRoot.bind(null, root, thenable, renderExpirationTime);\n    if (enableSchedulerTracing) {\n      ping = unstable_wrap(ping);\n    }\n    thenable.then(ping, ping);\n  }\n}\n\nfunction throwException(root, returnFiber, sourceFiber, value, renderExpirationTime) {\n  // The source fiber did not complete.\n  sourceFiber.effectTag |= Incomplete;\n  // Its effect list is no longer valid.\n  sourceFiber.firstEffect = sourceFiber.lastEffect = null;\n\n  if (value !== null && typeof value === 'object' && typeof value.then === 'function') {\n    // This is a thenable.\n    var thenable = value;\n\n    // Find the earliest timeout threshold of all the placeholders in the\n    // ancestor path. We could avoid this traversal by storing the thresholds on\n    // the stack, but we choose not to because we only hit this path if we're\n    // IO-bound (i.e. if something suspends). Whereas the stack is used even in\n    // the non-IO- bound case.\n    var _workInProgress = returnFiber;\n    var earliestTimeoutMs = -1;\n    var startTimeMs = -1;\n    do {\n      if (_workInProgress.tag === SuspenseComponent) {\n        var current$$1 = _workInProgress.alternate;\n        if (current$$1 !== null) {\n          var currentState = current$$1.memoizedState;\n          if (currentState !== null) {\n            // Reached a boundary that already timed out. Do not search\n            // any further.\n            var timedOutAt = currentState.timedOutAt;\n            startTimeMs = expirationTimeToMs(timedOutAt);\n            // Do not search any further.\n            break;\n          }\n        }\n        var timeoutPropMs = _workInProgress.pendingProps.maxDuration;\n        if (typeof timeoutPropMs === 'number') {\n          if (timeoutPropMs <= 0) {\n            earliestTimeoutMs = 0;\n          } else if (earliestTimeoutMs === -1 || timeoutPropMs < earliestTimeoutMs) {\n            earliestTimeoutMs = timeoutPropMs;\n          }\n        }\n      }\n      // If there is a DehydratedSuspenseComponent we don't have to do anything because\n      // if something suspends inside it, we will simply leave that as dehydrated. It\n      // will never timeout.\n      _workInProgress = _workInProgress.return;\n    } while (_workInProgress !== null);\n\n    // Schedule the nearest Suspense to re-render the timed out view.\n    _workInProgress = returnFiber;\n    do {\n      if (_workInProgress.tag === SuspenseComponent && shouldCaptureSuspense(_workInProgress)) {\n        // Found the nearest boundary.\n\n        // Stash the promise on the boundary fiber. If the boundary times out, we'll\n        var thenables = _workInProgress.updateQueue;\n        if (thenables === null) {\n          var updateQueue = new Set();\n          updateQueue.add(thenable);\n          _workInProgress.updateQueue = updateQueue;\n        } else {\n          thenables.add(thenable);\n        }\n\n        // If the boundary is outside of concurrent mode, we should *not*\n        // suspend the commit. Pretend as if the suspended component rendered\n        // null and keep rendering. In the commit phase, we'll schedule a\n        // subsequent synchronous update to re-render the Suspense.\n        //\n        // Note: It doesn't matter whether the component that suspended was\n        // inside a concurrent mode tree. If the Suspense is outside of it, we\n        // should *not* suspend the commit.\n        if ((_workInProgress.mode & ConcurrentMode) === NoEffect) {\n          _workInProgress.effectTag |= DidCapture;\n\n          // We're going to commit this fiber even though it didn't complete.\n          // But we shouldn't call any lifecycle methods or callbacks. Remove\n          // all lifecycle effect tags.\n          sourceFiber.effectTag &= ~(LifecycleEffectMask | Incomplete);\n\n          if (sourceFiber.tag === ClassComponent) {\n            var currentSourceFiber = sourceFiber.alternate;\n            if (currentSourceFiber === null) {\n              // This is a new mount. Change the tag so it's not mistaken for a\n              // completed class component. For example, we should not call\n              // componentWillUnmount if it is deleted.\n              sourceFiber.tag = IncompleteClassComponent;\n            } else {\n              // When we try rendering again, we should not reuse the current fiber,\n              // since it's known to be in an inconsistent state. Use a force updte to\n              // prevent a bail out.\n              var update = createUpdate(Sync);\n              update.tag = ForceUpdate;\n              enqueueUpdate(sourceFiber, update);\n            }\n          }\n\n          // The source fiber did not complete. Mark it with Sync priority to\n          // indicate that it still has pending work.\n          sourceFiber.expirationTime = Sync;\n\n          // Exit without suspending.\n          return;\n        }\n\n        // Confirmed that the boundary is in a concurrent mode tree. Continue\n        // with the normal suspend path.\n\n        attachPingListener(root, renderExpirationTime, thenable);\n\n        var absoluteTimeoutMs = void 0;\n        if (earliestTimeoutMs === -1) {\n          // If no explicit threshold is given, default to an arbitrarily large\n          // value. The actual size doesn't matter because the threshold for the\n          // whole tree will be clamped to the expiration time.\n          absoluteTimeoutMs = maxSigned31BitInt;\n        } else {\n          if (startTimeMs === -1) {\n            // This suspend happened outside of any already timed-out\n            // placeholders. We don't know exactly when the update was\n            // scheduled, but we can infer an approximate start time from the\n            // expiration time. First, find the earliest uncommitted expiration\n            // time in the tree, including work that is suspended. Then subtract\n            // the offset used to compute an async update's expiration time.\n            // This will cause high priority (interactive) work to expire\n            // earlier than necessary, but we can account for this by adjusting\n            // for the Just Noticeable Difference.\n            var earliestExpirationTime = findEarliestOutstandingPriorityLevel(root, renderExpirationTime);\n            var earliestExpirationTimeMs = expirationTimeToMs(earliestExpirationTime);\n            startTimeMs = earliestExpirationTimeMs - LOW_PRIORITY_EXPIRATION;\n          }\n          absoluteTimeoutMs = startTimeMs + earliestTimeoutMs;\n        }\n\n        // Mark the earliest timeout in the suspended fiber's ancestor path.\n        // After completing the root, we'll take the largest of all the\n        // suspended fiber's timeouts and use it to compute a timeout for the\n        // whole tree.\n        renderDidSuspend(root, absoluteTimeoutMs, renderExpirationTime);\n\n        _workInProgress.effectTag |= ShouldCapture;\n        _workInProgress.expirationTime = renderExpirationTime;\n        return;\n      } else if (enableSuspenseServerRenderer && _workInProgress.tag === DehydratedSuspenseComponent) {\n        attachPingListener(root, renderExpirationTime, thenable);\n\n        // Since we already have a current fiber, we can eagerly add a retry listener.\n        var retryCache = _workInProgress.memoizedState;\n        if (retryCache === null) {\n          retryCache = _workInProgress.memoizedState = new PossiblyWeakSet();\n          var _current = _workInProgress.alternate;\n          !_current ? invariant(false, 'A dehydrated suspense boundary must commit before trying to render. This is probably a bug in React.') : void 0;\n          _current.memoizedState = retryCache;\n        }\n        // Memoize using the boundary fiber to prevent redundant listeners.\n        if (!retryCache.has(thenable)) {\n          retryCache.add(thenable);\n          var retry = retryTimedOutBoundary.bind(null, _workInProgress, thenable);\n          if (enableSchedulerTracing) {\n            retry = unstable_wrap(retry);\n          }\n          thenable.then(retry, retry);\n        }\n        _workInProgress.effectTag |= ShouldCapture;\n        _workInProgress.expirationTime = renderExpirationTime;\n        return;\n      }\n      // This boundary already captured during this render. Continue to the next\n      // boundary.\n      _workInProgress = _workInProgress.return;\n    } while (_workInProgress !== null);\n    // No boundary was found. Fallthrough to error mode.\n    // TODO: Use invariant so the message is stripped in prod?\n    value = new Error((getComponentName(sourceFiber.type) || 'A React component') + ' suspended while rendering, but no fallback UI was specified.\\n' + '\\n' + 'Add a <Suspense fallback=...> component higher in the tree to ' + 'provide a loading indicator or placeholder to display.' + getStackByFiberInDevAndProd(sourceFiber));\n  }\n\n  // We didn't find a boundary that could handle this type of exception. Start\n  // over and traverse parent path again, this time treating the exception\n  // as an error.\n  renderDidError();\n  value = createCapturedValue(value, sourceFiber);\n  var workInProgress = returnFiber;\n  do {\n    switch (workInProgress.tag) {\n      case HostRoot:\n        {\n          var _errorInfo = value;\n          workInProgress.effectTag |= ShouldCapture;\n          workInProgress.expirationTime = renderExpirationTime;\n          var _update = createRootErrorUpdate(workInProgress, _errorInfo, renderExpirationTime);\n          enqueueCapturedUpdate(workInProgress, _update);\n          return;\n        }\n      case ClassComponent:\n        // Capture and retry\n        var errorInfo = value;\n        var ctor = workInProgress.type;\n        var instance = workInProgress.stateNode;\n        if ((workInProgress.effectTag & DidCapture) === NoEffect && (typeof ctor.getDerivedStateFromError === 'function' || instance !== null && typeof instance.componentDidCatch === 'function' && !isAlreadyFailedLegacyErrorBoundary(instance))) {\n          workInProgress.effectTag |= ShouldCapture;\n          workInProgress.expirationTime = renderExpirationTime;\n          // Schedule the error boundary to re-render using updated state\n          var _update2 = createClassErrorUpdate(workInProgress, errorInfo, renderExpirationTime);\n          enqueueCapturedUpdate(workInProgress, _update2);\n          return;\n        }\n        break;\n      default:\n        break;\n    }\n    workInProgress = workInProgress.return;\n  } while (workInProgress !== null);\n}\n\nfunction unwindWork(workInProgress, renderExpirationTime) {\n  switch (workInProgress.tag) {\n    case ClassComponent:\n      {\n        var Component = workInProgress.type;\n        if (isContextProvider(Component)) {\n          popContext(workInProgress);\n        }\n        var effectTag = workInProgress.effectTag;\n        if (effectTag & ShouldCapture) {\n          workInProgress.effectTag = effectTag & ~ShouldCapture | DidCapture;\n          return workInProgress;\n        }\n        return null;\n      }\n    case HostRoot:\n      {\n        popHostContainer(workInProgress);\n        popTopLevelContextObject(workInProgress);\n        var _effectTag = workInProgress.effectTag;\n        !((_effectTag & DidCapture) === NoEffect) ? invariant(false, 'The root failed to unmount after an error. This is likely a bug in React. Please file an issue.') : void 0;\n        workInProgress.effectTag = _effectTag & ~ShouldCapture | DidCapture;\n        return workInProgress;\n      }\n    case HostComponent:\n      {\n        // TODO: popHydrationState\n        popHostContext(workInProgress);\n        return null;\n      }\n    case SuspenseComponent:\n      {\n        var _effectTag2 = workInProgress.effectTag;\n        if (_effectTag2 & ShouldCapture) {\n          workInProgress.effectTag = _effectTag2 & ~ShouldCapture | DidCapture;\n          // Captured a suspense effect. Re-render the boundary.\n          return workInProgress;\n        }\n        return null;\n      }\n    case DehydratedSuspenseComponent:\n      {\n        if (enableSuspenseServerRenderer) {\n          // TODO: popHydrationState\n          var _effectTag3 = workInProgress.effectTag;\n          if (_effectTag3 & ShouldCapture) {\n            workInProgress.effectTag = _effectTag3 & ~ShouldCapture | DidCapture;\n            // Captured a suspense effect. Re-render the boundary.\n            return workInProgress;\n          }\n        }\n        return null;\n      }\n    case HostPortal:\n      popHostContainer(workInProgress);\n      return null;\n    case ContextProvider:\n      popProvider(workInProgress);\n      return null;\n    default:\n      return null;\n  }\n}\n\nfunction unwindInterruptedWork(interruptedWork) {\n  switch (interruptedWork.tag) {\n    case ClassComponent:\n      {\n        var childContextTypes = interruptedWork.type.childContextTypes;\n        if (childContextTypes !== null && childContextTypes !== undefined) {\n          popContext(interruptedWork);\n        }\n        break;\n      }\n    case HostRoot:\n      {\n        popHostContainer(interruptedWork);\n        popTopLevelContextObject(interruptedWork);\n        break;\n      }\n    case HostComponent:\n      {\n        popHostContext(interruptedWork);\n        break;\n      }\n    case HostPortal:\n      popHostContainer(interruptedWork);\n      break;\n    case ContextProvider:\n      popProvider(interruptedWork);\n      break;\n    default:\n      break;\n  }\n}\n\nvar ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher;\nvar ReactCurrentOwner$2 = ReactSharedInternals.ReactCurrentOwner;\n\n\nvar didWarnAboutStateTransition = void 0;\nvar didWarnSetStateChildContext = void 0;\nvar warnAboutUpdateOnUnmounted = void 0;\nvar warnAboutInvalidUpdates = void 0;\n\nif (enableSchedulerTracing) {\n  // Provide explicit error message when production+profiling bundle of e.g. react-dom\n  // is used with production (non-profiling) bundle of scheduler/tracing\n  !(__interactionsRef != null && __interactionsRef.current != null) ? invariant(false, 'It is not supported to run the profiling version of a renderer (for example, `react-dom/profiling`) without also replacing the `scheduler/tracing` module with `scheduler/tracing-profiling`. Your bundler might have a setting for aliasing both modules. Learn more at http://fb.me/react-profiling') : void 0;\n}\n\n{\n  didWarnAboutStateTransition = false;\n  didWarnSetStateChildContext = false;\n  var didWarnStateUpdateForUnmountedComponent = {};\n\n  warnAboutUpdateOnUnmounted = function (fiber, isClass) {\n    // We show the whole stack but dedupe on the top component's name because\n    // the problematic code almost always lies inside that component.\n    var componentName = getComponentName(fiber.type) || 'ReactComponent';\n    if (didWarnStateUpdateForUnmountedComponent[componentName]) {\n      return;\n    }\n    warningWithoutStack$1(false, \"Can't perform a React state update on an unmounted component. This \" + 'is a no-op, but it indicates a memory leak in your application. To ' + 'fix, cancel all subscriptions and asynchronous tasks in %s.%s', isClass ? 'the componentWillUnmount method' : 'a useEffect cleanup function', getStackByFiberInDevAndProd(fiber));\n    didWarnStateUpdateForUnmountedComponent[componentName] = true;\n  };\n\n  warnAboutInvalidUpdates = function (instance) {\n    switch (phase) {\n      case 'getChildContext':\n        if (didWarnSetStateChildContext) {\n          return;\n        }\n        warningWithoutStack$1(false, 'setState(...): Cannot call setState() inside getChildContext()');\n        didWarnSetStateChildContext = true;\n        break;\n      case 'render':\n        if (didWarnAboutStateTransition) {\n          return;\n        }\n        warningWithoutStack$1(false, 'Cannot update during an existing state transition (such as within ' + '`render`). Render methods should be a pure function of props and state.');\n        didWarnAboutStateTransition = true;\n        break;\n    }\n  };\n}\n\n// Used to ensure computeUniqueAsyncExpiration is monotonically decreasing.\nvar lastUniqueAsyncExpiration = Sync - 1;\n\nvar isWorking = false;\n\n// The next work in progress fiber that we're currently working on.\nvar nextUnitOfWork = null;\nvar nextRoot = null;\n// The time at which we're currently rendering work.\nvar nextRenderExpirationTime = NoWork;\nvar nextLatestAbsoluteTimeoutMs = -1;\nvar nextRenderDidError = false;\n\n// The next fiber with an effect that we're currently committing.\nvar nextEffect = null;\n\nvar isCommitting$1 = false;\nvar rootWithPendingPassiveEffects = null;\nvar passiveEffectCallbackHandle = null;\nvar passiveEffectCallback = null;\n\nvar legacyErrorBoundariesThatAlreadyFailed = null;\n\n// Used for performance tracking.\nvar interruptedBy = null;\n\nvar stashedWorkInProgressProperties = void 0;\nvar replayUnitOfWork = void 0;\nvar mayReplayFailedUnitOfWork = void 0;\nvar isReplayingFailedUnitOfWork = void 0;\nvar originalReplayError = void 0;\nvar rethrowOriginalError = void 0;\nif (true && replayFailedUnitOfWorkWithInvokeGuardedCallback) {\n  stashedWorkInProgressProperties = null;\n  mayReplayFailedUnitOfWork = true;\n  isReplayingFailedUnitOfWork = false;\n  originalReplayError = null;\n  replayUnitOfWork = function (failedUnitOfWork, thrownValue, isYieldy) {\n    if (thrownValue !== null && typeof thrownValue === 'object' && typeof thrownValue.then === 'function') {\n      // Don't replay promises. Treat everything else like an error.\n      // TODO: Need to figure out a different strategy if/when we add\n      // support for catching other types.\n      return;\n    }\n\n    // Restore the original state of the work-in-progress\n    if (stashedWorkInProgressProperties === null) {\n      // This should never happen. Don't throw because this code is DEV-only.\n      warningWithoutStack$1(false, 'Could not replay rendering after an error. This is likely a bug in React. ' + 'Please file an issue.');\n      return;\n    }\n    assignFiberPropertiesInDEV(failedUnitOfWork, stashedWorkInProgressProperties);\n\n    switch (failedUnitOfWork.tag) {\n      case HostRoot:\n        popHostContainer(failedUnitOfWork);\n        popTopLevelContextObject(failedUnitOfWork);\n        break;\n      case HostComponent:\n        popHostContext(failedUnitOfWork);\n        break;\n      case ClassComponent:\n        {\n          var Component = failedUnitOfWork.type;\n          if (isContextProvider(Component)) {\n            popContext(failedUnitOfWork);\n          }\n          break;\n        }\n      case HostPortal:\n        popHostContainer(failedUnitOfWork);\n        break;\n      case ContextProvider:\n        popProvider(failedUnitOfWork);\n        break;\n    }\n    // Replay the begin phase.\n    isReplayingFailedUnitOfWork = true;\n    originalReplayError = thrownValue;\n    invokeGuardedCallback(null, workLoop, null, isYieldy);\n    isReplayingFailedUnitOfWork = false;\n    originalReplayError = null;\n    if (hasCaughtError()) {\n      var replayError = clearCaughtError();\n      if (replayError != null && thrownValue != null) {\n        try {\n          // Reading the expando property is intentionally\n          // inside `try` because it might be a getter or Proxy.\n          if (replayError._suppressLogging) {\n            // Also suppress logging for the original error.\n            thrownValue._suppressLogging = true;\n          }\n        } catch (inner) {\n          // Ignore.\n        }\n      }\n    } else {\n      // If the begin phase did not fail the second time, set this pointer\n      // back to the original value.\n      nextUnitOfWork = failedUnitOfWork;\n    }\n  };\n  rethrowOriginalError = function () {\n    throw originalReplayError;\n  };\n}\n\nfunction resetStack() {\n  if (nextUnitOfWork !== null) {\n    var interruptedWork = nextUnitOfWork.return;\n    while (interruptedWork !== null) {\n      unwindInterruptedWork(interruptedWork);\n      interruptedWork = interruptedWork.return;\n    }\n  }\n\n  {\n    ReactStrictModeWarnings.discardPendingWarnings();\n    checkThatStackIsEmpty();\n  }\n\n  nextRoot = null;\n  nextRenderExpirationTime = NoWork;\n  nextLatestAbsoluteTimeoutMs = -1;\n  nextRenderDidError = false;\n  nextUnitOfWork = null;\n}\n\nfunction commitAllHostEffects() {\n  while (nextEffect !== null) {\n    {\n      setCurrentFiber(nextEffect);\n    }\n    recordEffect();\n\n    var effectTag = nextEffect.effectTag;\n\n    if (effectTag & ContentReset) {\n      commitResetTextContent(nextEffect);\n    }\n\n    if (effectTag & Ref) {\n      var current$$1 = nextEffect.alternate;\n      if (current$$1 !== null) {\n        commitDetachRef(current$$1);\n      }\n    }\n\n    // The following switch statement is only concerned about placement,\n    // updates, and deletions. To avoid needing to add a case for every\n    // possible bitmap value, we remove the secondary effects from the\n    // effect tag and switch on that value.\n    var primaryEffectTag = effectTag & (Placement | Update | Deletion);\n    switch (primaryEffectTag) {\n      case Placement:\n        {\n          commitPlacement(nextEffect);\n          // Clear the \"placement\" from effect tag so that we know that this is inserted, before\n          // any life-cycles like componentDidMount gets called.\n          // TODO: findDOMNode doesn't rely on this any more but isMounted\n          // does and isMounted is deprecated anyway so we should be able\n          // to kill this.\n          nextEffect.effectTag &= ~Placement;\n          break;\n        }\n      case PlacementAndUpdate:\n        {\n          // Placement\n          commitPlacement(nextEffect);\n          // Clear the \"placement\" from effect tag so that we know that this is inserted, before\n          // any life-cycles like componentDidMount gets called.\n          nextEffect.effectTag &= ~Placement;\n\n          // Update\n          var _current = nextEffect.alternate;\n          commitWork(_current, nextEffect);\n          break;\n        }\n      case Update:\n        {\n          var _current2 = nextEffect.alternate;\n          commitWork(_current2, nextEffect);\n          break;\n        }\n      case Deletion:\n        {\n          commitDeletion(nextEffect);\n          break;\n        }\n    }\n    nextEffect = nextEffect.nextEffect;\n  }\n\n  {\n    resetCurrentFiber();\n  }\n}\n\nfunction commitBeforeMutationLifecycles() {\n  while (nextEffect !== null) {\n    {\n      setCurrentFiber(nextEffect);\n    }\n\n    var effectTag = nextEffect.effectTag;\n    if (effectTag & Snapshot) {\n      recordEffect();\n      var current$$1 = nextEffect.alternate;\n      commitBeforeMutationLifeCycles(current$$1, nextEffect);\n    }\n\n    nextEffect = nextEffect.nextEffect;\n  }\n\n  {\n    resetCurrentFiber();\n  }\n}\n\nfunction commitAllLifeCycles(finishedRoot, committedExpirationTime) {\n  {\n    ReactStrictModeWarnings.flushPendingUnsafeLifecycleWarnings();\n    ReactStrictModeWarnings.flushLegacyContextWarning();\n\n    if (warnAboutDeprecatedLifecycles) {\n      ReactStrictModeWarnings.flushPendingDeprecationWarnings();\n    }\n  }\n  while (nextEffect !== null) {\n    {\n      setCurrentFiber(nextEffect);\n    }\n    var effectTag = nextEffect.effectTag;\n\n    if (effectTag & (Update | Callback)) {\n      recordEffect();\n      var current$$1 = nextEffect.alternate;\n      commitLifeCycles(finishedRoot, current$$1, nextEffect, committedExpirationTime);\n    }\n\n    if (effectTag & Ref) {\n      recordEffect();\n      commitAttachRef(nextEffect);\n    }\n\n    if (effectTag & Passive) {\n      rootWithPendingPassiveEffects = finishedRoot;\n    }\n\n    nextEffect = nextEffect.nextEffect;\n  }\n  {\n    resetCurrentFiber();\n  }\n}\n\nfunction commitPassiveEffects(root, firstEffect) {\n  rootWithPendingPassiveEffects = null;\n  passiveEffectCallbackHandle = null;\n  passiveEffectCallback = null;\n\n  // Set this to true to prevent re-entrancy\n  var previousIsRendering = isRendering;\n  isRendering = true;\n\n  var effect = firstEffect;\n  do {\n    {\n      setCurrentFiber(effect);\n    }\n\n    if (effect.effectTag & Passive) {\n      var didError = false;\n      var error = void 0;\n      {\n        invokeGuardedCallback(null, commitPassiveHookEffects, null, effect);\n        if (hasCaughtError()) {\n          didError = true;\n          error = clearCaughtError();\n        }\n      }\n      if (didError) {\n        captureCommitPhaseError(effect, error);\n      }\n    }\n    effect = effect.nextEffect;\n  } while (effect !== null);\n  {\n    resetCurrentFiber();\n  }\n\n  isRendering = previousIsRendering;\n\n  // Check if work was scheduled by one of the effects\n  var rootExpirationTime = root.expirationTime;\n  if (rootExpirationTime !== NoWork) {\n    requestWork(root, rootExpirationTime);\n  }\n  // Flush any sync work that was scheduled by effects\n  if (!isBatchingUpdates && !isRendering) {\n    performSyncWork();\n  }\n}\n\nfunction isAlreadyFailedLegacyErrorBoundary(instance) {\n  return legacyErrorBoundariesThatAlreadyFailed !== null && legacyErrorBoundariesThatAlreadyFailed.has(instance);\n}\n\nfunction markLegacyErrorBoundaryAsFailed(instance) {\n  if (legacyErrorBoundariesThatAlreadyFailed === null) {\n    legacyErrorBoundariesThatAlreadyFailed = new Set([instance]);\n  } else {\n    legacyErrorBoundariesThatAlreadyFailed.add(instance);\n  }\n}\n\nfunction flushPassiveEffects() {\n  if (passiveEffectCallbackHandle !== null) {\n    cancelPassiveEffects(passiveEffectCallbackHandle);\n  }\n  if (passiveEffectCallback !== null) {\n    // We call the scheduled callback instead of commitPassiveEffects directly\n    // to ensure tracing works correctly.\n    passiveEffectCallback();\n  }\n}\n\nfunction commitRoot(root, finishedWork) {\n  isWorking = true;\n  isCommitting$1 = true;\n  startCommitTimer();\n\n  !(root.current !== finishedWork) ? invariant(false, 'Cannot commit the same tree as before. This is probably a bug related to the return field. This error is likely caused by a bug in React. Please file an issue.') : void 0;\n  var committedExpirationTime = root.pendingCommitExpirationTime;\n  !(committedExpirationTime !== NoWork) ? invariant(false, 'Cannot commit an incomplete root. This error is likely caused by a bug in React. Please file an issue.') : void 0;\n  root.pendingCommitExpirationTime = NoWork;\n\n  // Update the pending priority levels to account for the work that we are\n  // about to commit. This needs to happen before calling the lifecycles, since\n  // they may schedule additional updates.\n  var updateExpirationTimeBeforeCommit = finishedWork.expirationTime;\n  var childExpirationTimeBeforeCommit = finishedWork.childExpirationTime;\n  var earliestRemainingTimeBeforeCommit = childExpirationTimeBeforeCommit > updateExpirationTimeBeforeCommit ? childExpirationTimeBeforeCommit : updateExpirationTimeBeforeCommit;\n  markCommittedPriorityLevels(root, earliestRemainingTimeBeforeCommit);\n\n  var prevInteractions = null;\n  if (enableSchedulerTracing) {\n    // Restore any pending interactions at this point,\n    // So that cascading work triggered during the render phase will be accounted for.\n    prevInteractions = __interactionsRef.current;\n    __interactionsRef.current = root.memoizedInteractions;\n  }\n\n  // Reset this to null before calling lifecycles\n  ReactCurrentOwner$2.current = null;\n\n  var firstEffect = void 0;\n  if (finishedWork.effectTag > PerformedWork) {\n    // A fiber's effect list consists only of its children, not itself. So if\n    // the root has an effect, we need to add it to the end of the list. The\n    // resulting list is the set that would belong to the root's parent, if\n    // it had one; that is, all the effects in the tree including the root.\n    if (finishedWork.lastEffect !== null) {\n      finishedWork.lastEffect.nextEffect = finishedWork;\n      firstEffect = finishedWork.firstEffect;\n    } else {\n      firstEffect = finishedWork;\n    }\n  } else {\n    // There is no effect on the root.\n    firstEffect = finishedWork.firstEffect;\n  }\n\n  prepareForCommit(root.containerInfo);\n\n  // Invoke instances of getSnapshotBeforeUpdate before mutation.\n  nextEffect = firstEffect;\n  startCommitSnapshotEffectsTimer();\n  while (nextEffect !== null) {\n    var didError = false;\n    var error = void 0;\n    {\n      invokeGuardedCallback(null, commitBeforeMutationLifecycles, null);\n      if (hasCaughtError()) {\n        didError = true;\n        error = clearCaughtError();\n      }\n    }\n    if (didError) {\n      !(nextEffect !== null) ? invariant(false, 'Should have next effect. This error is likely caused by a bug in React. Please file an issue.') : void 0;\n      captureCommitPhaseError(nextEffect, error);\n      // Clean-up\n      if (nextEffect !== null) {\n        nextEffect = nextEffect.nextEffect;\n      }\n    }\n  }\n  stopCommitSnapshotEffectsTimer();\n\n  if (enableProfilerTimer) {\n    // Mark the current commit time to be shared by all Profilers in this batch.\n    // This enables them to be grouped later.\n    recordCommitTime();\n  }\n\n  // Commit all the side-effects within a tree. We'll do this in two passes.\n  // The first pass performs all the host insertions, updates, deletions and\n  // ref unmounts.\n  nextEffect = firstEffect;\n  startCommitHostEffectsTimer();\n  while (nextEffect !== null) {\n    var _didError = false;\n    var _error = void 0;\n    {\n      invokeGuardedCallback(null, commitAllHostEffects, null);\n      if (hasCaughtError()) {\n        _didError = true;\n        _error = clearCaughtError();\n      }\n    }\n    if (_didError) {\n      !(nextEffect !== null) ? invariant(false, 'Should have next effect. This error is likely caused by a bug in React. Please file an issue.') : void 0;\n      captureCommitPhaseError(nextEffect, _error);\n      // Clean-up\n      if (nextEffect !== null) {\n        nextEffect = nextEffect.nextEffect;\n      }\n    }\n  }\n  stopCommitHostEffectsTimer();\n\n  resetAfterCommit(root.containerInfo);\n\n  // The work-in-progress tree is now the current tree. This must come after\n  // the first pass of the commit phase, so that the previous tree is still\n  // current during componentWillUnmount, but before the second pass, so that\n  // the finished work is current during componentDidMount/Update.\n  root.current = finishedWork;\n\n  // In the second pass we'll perform all life-cycles and ref callbacks.\n  // Life-cycles happen as a separate pass so that all placements, updates,\n  // and deletions in the entire tree have already been invoked.\n  // This pass also triggers any renderer-specific initial effects.\n  nextEffect = firstEffect;\n  startCommitLifeCyclesTimer();\n  while (nextEffect !== null) {\n    var _didError2 = false;\n    var _error2 = void 0;\n    {\n      invokeGuardedCallback(null, commitAllLifeCycles, null, root, committedExpirationTime);\n      if (hasCaughtError()) {\n        _didError2 = true;\n        _error2 = clearCaughtError();\n      }\n    }\n    if (_didError2) {\n      !(nextEffect !== null) ? invariant(false, 'Should have next effect. This error is likely caused by a bug in React. Please file an issue.') : void 0;\n      captureCommitPhaseError(nextEffect, _error2);\n      if (nextEffect !== null) {\n        nextEffect = nextEffect.nextEffect;\n      }\n    }\n  }\n\n  if (firstEffect !== null && rootWithPendingPassiveEffects !== null) {\n    // This commit included a passive effect. These do not need to fire until\n    // after the next paint. Schedule an callback to fire them in an async\n    // event. To ensure serial execution, the callback will be flushed early if\n    // we enter rootWithPendingPassiveEffects commit phase before then.\n    var callback = commitPassiveEffects.bind(null, root, firstEffect);\n    if (enableSchedulerTracing) {\n      // TODO: Avoid this extra callback by mutating the tracing ref directly,\n      // like we do at the beginning of commitRoot. I've opted not to do that\n      // here because that code is still in flux.\n      callback = unstable_wrap(callback);\n    }\n    passiveEffectCallbackHandle = unstable_runWithPriority(unstable_NormalPriority, function () {\n      return schedulePassiveEffects(callback);\n    });\n    passiveEffectCallback = callback;\n  }\n\n  isCommitting$1 = false;\n  isWorking = false;\n  stopCommitLifeCyclesTimer();\n  stopCommitTimer();\n  onCommitRoot(finishedWork.stateNode);\n  if (true && ReactFiberInstrumentation_1.debugTool) {\n    ReactFiberInstrumentation_1.debugTool.onCommitWork(finishedWork);\n  }\n\n  var updateExpirationTimeAfterCommit = finishedWork.expirationTime;\n  var childExpirationTimeAfterCommit = finishedWork.childExpirationTime;\n  var earliestRemainingTimeAfterCommit = childExpirationTimeAfterCommit > updateExpirationTimeAfterCommit ? childExpirationTimeAfterCommit : updateExpirationTimeAfterCommit;\n  if (earliestRemainingTimeAfterCommit === NoWork) {\n    // If there's no remaining work, we can clear the set of already failed\n    // error boundaries.\n    legacyErrorBoundariesThatAlreadyFailed = null;\n  }\n  onCommit(root, earliestRemainingTimeAfterCommit);\n\n  if (enableSchedulerTracing) {\n    __interactionsRef.current = prevInteractions;\n\n    var subscriber = void 0;\n\n    try {\n      subscriber = __subscriberRef.current;\n      if (subscriber !== null && root.memoizedInteractions.size > 0) {\n        var threadID = computeThreadID(committedExpirationTime, root.interactionThreadID);\n        subscriber.onWorkStopped(root.memoizedInteractions, threadID);\n      }\n    } catch (error) {\n      // It's not safe for commitRoot() to throw.\n      // Store the error for now and we'll re-throw in finishRendering().\n      if (!hasUnhandledError) {\n        hasUnhandledError = true;\n        unhandledError = error;\n      }\n    } finally {\n      // Clear completed interactions from the pending Map.\n      // Unless the render was suspended or cascading work was scheduled,\n      // In which case– leave pending interactions until the subsequent render.\n      var pendingInteractionMap = root.pendingInteractionMap;\n      pendingInteractionMap.forEach(function (scheduledInteractions, scheduledExpirationTime) {\n        // Only decrement the pending interaction count if we're done.\n        // If there's still work at the current priority,\n        // That indicates that we are waiting for suspense data.\n        if (scheduledExpirationTime > earliestRemainingTimeAfterCommit) {\n          pendingInteractionMap.delete(scheduledExpirationTime);\n\n          scheduledInteractions.forEach(function (interaction) {\n            interaction.__count--;\n\n            if (subscriber !== null && interaction.__count === 0) {\n              try {\n                subscriber.onInteractionScheduledWorkCompleted(interaction);\n              } catch (error) {\n                // It's not safe for commitRoot() to throw.\n                // Store the error for now and we'll re-throw in finishRendering().\n                if (!hasUnhandledError) {\n                  hasUnhandledError = true;\n                  unhandledError = error;\n                }\n              }\n            }\n          });\n        }\n      });\n    }\n  }\n}\n\nfunction resetChildExpirationTime(workInProgress, renderTime) {\n  if (renderTime !== Never && workInProgress.childExpirationTime === Never) {\n    // The children of this component are hidden. Don't bubble their\n    // expiration times.\n    return;\n  }\n\n  var newChildExpirationTime = NoWork;\n\n  // Bubble up the earliest expiration time.\n  if (enableProfilerTimer && workInProgress.mode & ProfileMode) {\n    // We're in profiling mode.\n    // Let's use this same traversal to update the render durations.\n    var actualDuration = workInProgress.actualDuration;\n    var treeBaseDuration = workInProgress.selfBaseDuration;\n\n    // When a fiber is cloned, its actualDuration is reset to 0.\n    // This value will only be updated if work is done on the fiber (i.e. it doesn't bailout).\n    // When work is done, it should bubble to the parent's actualDuration.\n    // If the fiber has not been cloned though, (meaning no work was done),\n    // Then this value will reflect the amount of time spent working on a previous render.\n    // In that case it should not bubble.\n    // We determine whether it was cloned by comparing the child pointer.\n    var shouldBubbleActualDurations = workInProgress.alternate === null || workInProgress.child !== workInProgress.alternate.child;\n\n    var child = workInProgress.child;\n    while (child !== null) {\n      var childUpdateExpirationTime = child.expirationTime;\n      var childChildExpirationTime = child.childExpirationTime;\n      if (childUpdateExpirationTime > newChildExpirationTime) {\n        newChildExpirationTime = childUpdateExpirationTime;\n      }\n      if (childChildExpirationTime > newChildExpirationTime) {\n        newChildExpirationTime = childChildExpirationTime;\n      }\n      if (shouldBubbleActualDurations) {\n        actualDuration += child.actualDuration;\n      }\n      treeBaseDuration += child.treeBaseDuration;\n      child = child.sibling;\n    }\n    workInProgress.actualDuration = actualDuration;\n    workInProgress.treeBaseDuration = treeBaseDuration;\n  } else {\n    var _child = workInProgress.child;\n    while (_child !== null) {\n      var _childUpdateExpirationTime = _child.expirationTime;\n      var _childChildExpirationTime = _child.childExpirationTime;\n      if (_childUpdateExpirationTime > newChildExpirationTime) {\n        newChildExpirationTime = _childUpdateExpirationTime;\n      }\n      if (_childChildExpirationTime > newChildExpirationTime) {\n        newChildExpirationTime = _childChildExpirationTime;\n      }\n      _child = _child.sibling;\n    }\n  }\n\n  workInProgress.childExpirationTime = newChildExpirationTime;\n}\n\nfunction completeUnitOfWork(workInProgress) {\n  // Attempt to complete the current unit of work, then move to the\n  // next sibling. If there are no more siblings, return to the\n  // parent fiber.\n  while (true) {\n    // The current, flushed, state of this fiber is the alternate.\n    // Ideally nothing should rely on this, but relying on it here\n    // means that we don't need an additional field on the work in\n    // progress.\n    var current$$1 = workInProgress.alternate;\n    {\n      setCurrentFiber(workInProgress);\n    }\n\n    var returnFiber = workInProgress.return;\n    var siblingFiber = workInProgress.sibling;\n\n    if ((workInProgress.effectTag & Incomplete) === NoEffect) {\n      if (true && replayFailedUnitOfWorkWithInvokeGuardedCallback) {\n        // Don't replay if it fails during completion phase.\n        mayReplayFailedUnitOfWork = false;\n      }\n      // This fiber completed.\n      // Remember we're completing this unit so we can find a boundary if it fails.\n      nextUnitOfWork = workInProgress;\n      if (enableProfilerTimer) {\n        if (workInProgress.mode & ProfileMode) {\n          startProfilerTimer(workInProgress);\n        }\n        nextUnitOfWork = completeWork(current$$1, workInProgress, nextRenderExpirationTime);\n        if (workInProgress.mode & ProfileMode) {\n          // Update render duration assuming we didn't error.\n          stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false);\n        }\n      } else {\n        nextUnitOfWork = completeWork(current$$1, workInProgress, nextRenderExpirationTime);\n      }\n      if (true && replayFailedUnitOfWorkWithInvokeGuardedCallback) {\n        // We're out of completion phase so replaying is fine now.\n        mayReplayFailedUnitOfWork = true;\n      }\n      stopWorkTimer(workInProgress);\n      resetChildExpirationTime(workInProgress, nextRenderExpirationTime);\n      {\n        resetCurrentFiber();\n      }\n\n      if (nextUnitOfWork !== null) {\n        // Completing this fiber spawned new work. Work on that next.\n        return nextUnitOfWork;\n      }\n\n      if (returnFiber !== null &&\n      // Do not append effects to parents if a sibling failed to complete\n      (returnFiber.effectTag & Incomplete) === NoEffect) {\n        // Append all the effects of the subtree and this fiber onto the effect\n        // list of the parent. The completion order of the children affects the\n        // side-effect order.\n        if (returnFiber.firstEffect === null) {\n          returnFiber.firstEffect = workInProgress.firstEffect;\n        }\n        if (workInProgress.lastEffect !== null) {\n          if (returnFiber.lastEffect !== null) {\n            returnFiber.lastEffect.nextEffect = workInProgress.firstEffect;\n          }\n          returnFiber.lastEffect = workInProgress.lastEffect;\n        }\n\n        // If this fiber had side-effects, we append it AFTER the children's\n        // side-effects. We can perform certain side-effects earlier if\n        // needed, by doing multiple passes over the effect list. We don't want\n        // to schedule our own side-effect on our own list because if end up\n        // reusing children we'll schedule this effect onto itself since we're\n        // at the end.\n        var effectTag = workInProgress.effectTag;\n        // Skip both NoWork and PerformedWork tags when creating the effect list.\n        // PerformedWork effect is read by React DevTools but shouldn't be committed.\n        if (effectTag > PerformedWork) {\n          if (returnFiber.lastEffect !== null) {\n            returnFiber.lastEffect.nextEffect = workInProgress;\n          } else {\n            returnFiber.firstEffect = workInProgress;\n          }\n          returnFiber.lastEffect = workInProgress;\n        }\n      }\n\n      if (true && ReactFiberInstrumentation_1.debugTool) {\n        ReactFiberInstrumentation_1.debugTool.onCompleteWork(workInProgress);\n      }\n\n      if (siblingFiber !== null) {\n        // If there is more work to do in this returnFiber, do that next.\n        return siblingFiber;\n      } else if (returnFiber !== null) {\n        // If there's no more work in this returnFiber. Complete the returnFiber.\n        workInProgress = returnFiber;\n        continue;\n      } else {\n        // We've reached the root.\n        return null;\n      }\n    } else {\n      if (enableProfilerTimer && workInProgress.mode & ProfileMode) {\n        // Record the render duration for the fiber that errored.\n        stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false);\n\n        // Include the time spent working on failed children before continuing.\n        var actualDuration = workInProgress.actualDuration;\n        var child = workInProgress.child;\n        while (child !== null) {\n          actualDuration += child.actualDuration;\n          child = child.sibling;\n        }\n        workInProgress.actualDuration = actualDuration;\n      }\n\n      // This fiber did not complete because something threw. Pop values off\n      // the stack without entering the complete phase. If this is a boundary,\n      // capture values if possible.\n      var next = unwindWork(workInProgress, nextRenderExpirationTime);\n      // Because this fiber did not complete, don't reset its expiration time.\n      if (workInProgress.effectTag & DidCapture) {\n        // Restarting an error boundary\n        stopFailedWorkTimer(workInProgress);\n      } else {\n        stopWorkTimer(workInProgress);\n      }\n\n      {\n        resetCurrentFiber();\n      }\n\n      if (next !== null) {\n        stopWorkTimer(workInProgress);\n        if (true && ReactFiberInstrumentation_1.debugTool) {\n          ReactFiberInstrumentation_1.debugTool.onCompleteWork(workInProgress);\n        }\n\n        // If completing this work spawned new work, do that next. We'll come\n        // back here again.\n        // Since we're restarting, remove anything that is not a host effect\n        // from the effect tag.\n        next.effectTag &= HostEffectMask;\n        return next;\n      }\n\n      if (returnFiber !== null) {\n        // Mark the parent fiber as incomplete and clear its effect list.\n        returnFiber.firstEffect = returnFiber.lastEffect = null;\n        returnFiber.effectTag |= Incomplete;\n      }\n\n      if (true && ReactFiberInstrumentation_1.debugTool) {\n        ReactFiberInstrumentation_1.debugTool.onCompleteWork(workInProgress);\n      }\n\n      if (siblingFiber !== null) {\n        // If there is more work to do in this returnFiber, do that next.\n        return siblingFiber;\n      } else if (returnFiber !== null) {\n        // If there's no more work in this returnFiber. Complete the returnFiber.\n        workInProgress = returnFiber;\n        continue;\n      } else {\n        return null;\n      }\n    }\n  }\n\n  // Without this explicit null return Flow complains of invalid return type\n  // TODO Remove the above while(true) loop\n  // eslint-disable-next-line no-unreachable\n  return null;\n}\n\nfunction performUnitOfWork(workInProgress) {\n  // The current, flushed, state of this fiber is the alternate.\n  // Ideally nothing should rely on this, but relying on it here\n  // means that we don't need an additional field on the work in\n  // progress.\n  var current$$1 = workInProgress.alternate;\n\n  // See if beginning this work spawns more work.\n  startWorkTimer(workInProgress);\n  {\n    setCurrentFiber(workInProgress);\n  }\n\n  if (true && replayFailedUnitOfWorkWithInvokeGuardedCallback) {\n    stashedWorkInProgressProperties = assignFiberPropertiesInDEV(stashedWorkInProgressProperties, workInProgress);\n  }\n\n  var next = void 0;\n  if (enableProfilerTimer) {\n    if (workInProgress.mode & ProfileMode) {\n      startProfilerTimer(workInProgress);\n    }\n\n    next = beginWork(current$$1, workInProgress, nextRenderExpirationTime);\n    workInProgress.memoizedProps = workInProgress.pendingProps;\n\n    if (workInProgress.mode & ProfileMode) {\n      // Record the render duration assuming we didn't bailout (or error).\n      stopProfilerTimerIfRunningAndRecordDelta(workInProgress, true);\n    }\n  } else {\n    next = beginWork(current$$1, workInProgress, nextRenderExpirationTime);\n    workInProgress.memoizedProps = workInProgress.pendingProps;\n  }\n\n  {\n    resetCurrentFiber();\n    if (isReplayingFailedUnitOfWork) {\n      // Currently replaying a failed unit of work. This should be unreachable,\n      // because the render phase is meant to be idempotent, and it should\n      // have thrown again. Since it didn't, rethrow the original error, so\n      // React's internal stack is not misaligned.\n      rethrowOriginalError();\n    }\n  }\n  if (true && ReactFiberInstrumentation_1.debugTool) {\n    ReactFiberInstrumentation_1.debugTool.onBeginWork(workInProgress);\n  }\n\n  if (next === null) {\n    // If this doesn't spawn new work, complete the current work.\n    next = completeUnitOfWork(workInProgress);\n  }\n\n  ReactCurrentOwner$2.current = null;\n\n  return next;\n}\n\nfunction workLoop(isYieldy) {\n  if (!isYieldy) {\n    // Flush work without yielding\n    while (nextUnitOfWork !== null) {\n      nextUnitOfWork = performUnitOfWork(nextUnitOfWork);\n    }\n  } else {\n    // Flush asynchronous work until there's a higher priority event\n    while (nextUnitOfWork !== null && !shouldYieldToRenderer()) {\n      nextUnitOfWork = performUnitOfWork(nextUnitOfWork);\n    }\n  }\n}\n\nfunction renderRoot(root, isYieldy) {\n  !!isWorking ? invariant(false, 'renderRoot was called recursively. This error is likely caused by a bug in React. Please file an issue.') : void 0;\n\n  flushPassiveEffects();\n\n  isWorking = true;\n  var previousDispatcher = ReactCurrentDispatcher.current;\n  ReactCurrentDispatcher.current = ContextOnlyDispatcher;\n\n  var expirationTime = root.nextExpirationTimeToWorkOn;\n\n  // Check if we're starting from a fresh stack, or if we're resuming from\n  // previously yielded work.\n  if (expirationTime !== nextRenderExpirationTime || root !== nextRoot || nextUnitOfWork === null) {\n    // Reset the stack and start working from the root.\n    resetStack();\n    nextRoot = root;\n    nextRenderExpirationTime = expirationTime;\n    nextUnitOfWork = createWorkInProgress(nextRoot.current, null, nextRenderExpirationTime);\n    root.pendingCommitExpirationTime = NoWork;\n\n    if (enableSchedulerTracing) {\n      // Determine which interactions this batch of work currently includes,\n      // So that we can accurately attribute time spent working on it,\n      var interactions = new Set();\n      root.pendingInteractionMap.forEach(function (scheduledInteractions, scheduledExpirationTime) {\n        if (scheduledExpirationTime >= expirationTime) {\n          scheduledInteractions.forEach(function (interaction) {\n            return interactions.add(interaction);\n          });\n        }\n      });\n\n      // Store the current set of interactions on the FiberRoot for a few reasons:\n      // We can re-use it in hot functions like renderRoot() without having to recalculate it.\n      // We will also use it in commitWork() to pass to any Profiler onRender() hooks.\n      // This also provides DevTools with a way to access it when the onCommitRoot() hook is called.\n      root.memoizedInteractions = interactions;\n\n      if (interactions.size > 0) {\n        var subscriber = __subscriberRef.current;\n        if (subscriber !== null) {\n          var threadID = computeThreadID(expirationTime, root.interactionThreadID);\n          try {\n            subscriber.onWorkStarted(interactions, threadID);\n          } catch (error) {\n            // Work thrown by an interaction tracing subscriber should be rethrown,\n            // But only once it's safe (to avoid leaving the scheduler in an invalid state).\n            // Store the error for now and we'll re-throw in finishRendering().\n            if (!hasUnhandledError) {\n              hasUnhandledError = true;\n              unhandledError = error;\n            }\n          }\n        }\n      }\n    }\n  }\n\n  var prevInteractions = null;\n  if (enableSchedulerTracing) {\n    // We're about to start new traced work.\n    // Restore pending interactions so cascading work triggered during the render phase will be accounted for.\n    prevInteractions = __interactionsRef.current;\n    __interactionsRef.current = root.memoizedInteractions;\n  }\n\n  var didFatal = false;\n\n  startWorkLoopTimer(nextUnitOfWork);\n\n  do {\n    try {\n      workLoop(isYieldy);\n    } catch (thrownValue) {\n      resetContextDependences();\n      resetHooks();\n\n      // Reset in case completion throws.\n      // This is only used in DEV and when replaying is on.\n      var mayReplay = void 0;\n      if (true && replayFailedUnitOfWorkWithInvokeGuardedCallback) {\n        mayReplay = mayReplayFailedUnitOfWork;\n        mayReplayFailedUnitOfWork = true;\n      }\n\n      if (nextUnitOfWork === null) {\n        // This is a fatal error.\n        didFatal = true;\n        onUncaughtError(thrownValue);\n      } else {\n        if (enableProfilerTimer && nextUnitOfWork.mode & ProfileMode) {\n          // Record the time spent rendering before an error was thrown.\n          // This avoids inaccurate Profiler durations in the case of a suspended render.\n          stopProfilerTimerIfRunningAndRecordDelta(nextUnitOfWork, true);\n        }\n\n        {\n          // Reset global debug state\n          // We assume this is defined in DEV\n          resetCurrentlyProcessingQueue();\n        }\n\n        if (true && replayFailedUnitOfWorkWithInvokeGuardedCallback) {\n          if (mayReplay) {\n            var failedUnitOfWork = nextUnitOfWork;\n            replayUnitOfWork(failedUnitOfWork, thrownValue, isYieldy);\n          }\n        }\n\n        // TODO: we already know this isn't true in some cases.\n        // At least this shows a nicer error message until we figure out the cause.\n        // https://github.com/facebook/react/issues/12449#issuecomment-386727431\n        !(nextUnitOfWork !== null) ? invariant(false, 'Failed to replay rendering after an error. This is likely caused by a bug in React. Please file an issue with a reproducing case to help us find it.') : void 0;\n\n        var sourceFiber = nextUnitOfWork;\n        var returnFiber = sourceFiber.return;\n        if (returnFiber === null) {\n          // This is the root. The root could capture its own errors. However,\n          // we don't know if it errors before or after we pushed the host\n          // context. This information is needed to avoid a stack mismatch.\n          // Because we're not sure, treat this as a fatal error. We could track\n          // which phase it fails in, but doesn't seem worth it. At least\n          // for now.\n          didFatal = true;\n          onUncaughtError(thrownValue);\n        } else {\n          throwException(root, returnFiber, sourceFiber, thrownValue, nextRenderExpirationTime);\n          nextUnitOfWork = completeUnitOfWork(sourceFiber);\n          continue;\n        }\n      }\n    }\n    break;\n  } while (true);\n\n  if (enableSchedulerTracing) {\n    // Traced work is done for now; restore the previous interactions.\n    __interactionsRef.current = prevInteractions;\n  }\n\n  // We're done performing work. Time to clean up.\n  isWorking = false;\n  ReactCurrentDispatcher.current = previousDispatcher;\n  resetContextDependences();\n  resetHooks();\n\n  // Yield back to main thread.\n  if (didFatal) {\n    var _didCompleteRoot = false;\n    stopWorkLoopTimer(interruptedBy, _didCompleteRoot);\n    interruptedBy = null;\n    // There was a fatal error.\n    {\n      resetStackAfterFatalErrorInDev();\n    }\n    // `nextRoot` points to the in-progress root. A non-null value indicates\n    // that we're in the middle of an async render. Set it to null to indicate\n    // there's no more work to be done in the current batch.\n    nextRoot = null;\n    onFatal(root);\n    return;\n  }\n\n  if (nextUnitOfWork !== null) {\n    // There's still remaining async work in this tree, but we ran out of time\n    // in the current frame. Yield back to the renderer. Unless we're\n    // interrupted by a higher priority update, we'll continue later from where\n    // we left off.\n    var _didCompleteRoot2 = false;\n    stopWorkLoopTimer(interruptedBy, _didCompleteRoot2);\n    interruptedBy = null;\n    onYield(root);\n    return;\n  }\n\n  // We completed the whole tree.\n  var didCompleteRoot = true;\n  stopWorkLoopTimer(interruptedBy, didCompleteRoot);\n  var rootWorkInProgress = root.current.alternate;\n  !(rootWorkInProgress !== null) ? invariant(false, 'Finished root should have a work-in-progress. This error is likely caused by a bug in React. Please file an issue.') : void 0;\n\n  // `nextRoot` points to the in-progress root. A non-null value indicates\n  // that we're in the middle of an async render. Set it to null to indicate\n  // there's no more work to be done in the current batch.\n  nextRoot = null;\n  interruptedBy = null;\n\n  if (nextRenderDidError) {\n    // There was an error\n    if (hasLowerPriorityWork(root, expirationTime)) {\n      // There's lower priority work. If so, it may have the effect of fixing\n      // the exception that was just thrown. Exit without committing. This is\n      // similar to a suspend, but without a timeout because we're not waiting\n      // for a promise to resolve. React will restart at the lower\n      // priority level.\n      markSuspendedPriorityLevel(root, expirationTime);\n      var suspendedExpirationTime = expirationTime;\n      var rootExpirationTime = root.expirationTime;\n      onSuspend(root, rootWorkInProgress, suspendedExpirationTime, rootExpirationTime, -1 // Indicates no timeout\n      );\n      return;\n    } else if (\n    // There's no lower priority work, but we're rendering asynchronously.\n    // Synchronously attempt to render the same level one more time. This is\n    // similar to a suspend, but without a timeout because we're not waiting\n    // for a promise to resolve.\n    !root.didError && isYieldy) {\n      root.didError = true;\n      var _suspendedExpirationTime = root.nextExpirationTimeToWorkOn = expirationTime;\n      var _rootExpirationTime = root.expirationTime = Sync;\n      onSuspend(root, rootWorkInProgress, _suspendedExpirationTime, _rootExpirationTime, -1 // Indicates no timeout\n      );\n      return;\n    }\n  }\n\n  if (isYieldy && nextLatestAbsoluteTimeoutMs !== -1) {\n    // The tree was suspended.\n    var _suspendedExpirationTime2 = expirationTime;\n    markSuspendedPriorityLevel(root, _suspendedExpirationTime2);\n\n    // Find the earliest uncommitted expiration time in the tree, including\n    // work that is suspended. The timeout threshold cannot be longer than\n    // the overall expiration.\n    var earliestExpirationTime = findEarliestOutstandingPriorityLevel(root, expirationTime);\n    var earliestExpirationTimeMs = expirationTimeToMs(earliestExpirationTime);\n    if (earliestExpirationTimeMs < nextLatestAbsoluteTimeoutMs) {\n      nextLatestAbsoluteTimeoutMs = earliestExpirationTimeMs;\n    }\n\n    // Subtract the current time from the absolute timeout to get the number\n    // of milliseconds until the timeout. In other words, convert an absolute\n    // timestamp to a relative time. This is the value that is passed\n    // to `setTimeout`.\n    var currentTimeMs = expirationTimeToMs(requestCurrentTime());\n    var msUntilTimeout = nextLatestAbsoluteTimeoutMs - currentTimeMs;\n    msUntilTimeout = msUntilTimeout < 0 ? 0 : msUntilTimeout;\n\n    // TODO: Account for the Just Noticeable Difference\n\n    var _rootExpirationTime2 = root.expirationTime;\n    onSuspend(root, rootWorkInProgress, _suspendedExpirationTime2, _rootExpirationTime2, msUntilTimeout);\n    return;\n  }\n\n  // Ready to commit.\n  onComplete(root, rootWorkInProgress, expirationTime);\n}\n\nfunction captureCommitPhaseError(sourceFiber, value) {\n  var expirationTime = Sync;\n  var fiber = sourceFiber.return;\n  while (fiber !== null) {\n    switch (fiber.tag) {\n      case ClassComponent:\n        var ctor = fiber.type;\n        var instance = fiber.stateNode;\n        if (typeof ctor.getDerivedStateFromError === 'function' || typeof instance.componentDidCatch === 'function' && !isAlreadyFailedLegacyErrorBoundary(instance)) {\n          var errorInfo = createCapturedValue(value, sourceFiber);\n          var update = createClassErrorUpdate(fiber, errorInfo, expirationTime);\n          enqueueUpdate(fiber, update);\n          scheduleWork(fiber, expirationTime);\n          return;\n        }\n        break;\n      case HostRoot:\n        {\n          var _errorInfo = createCapturedValue(value, sourceFiber);\n          var _update = createRootErrorUpdate(fiber, _errorInfo, expirationTime);\n          enqueueUpdate(fiber, _update);\n          scheduleWork(fiber, expirationTime);\n          return;\n        }\n    }\n    fiber = fiber.return;\n  }\n\n  if (sourceFiber.tag === HostRoot) {\n    // Error was thrown at the root. There is no parent, so the root\n    // itself should capture it.\n    var rootFiber = sourceFiber;\n    var _errorInfo2 = createCapturedValue(value, rootFiber);\n    var _update2 = createRootErrorUpdate(rootFiber, _errorInfo2, expirationTime);\n    enqueueUpdate(rootFiber, _update2);\n    scheduleWork(rootFiber, expirationTime);\n  }\n}\n\nfunction computeThreadID(expirationTime, interactionThreadID) {\n  // Interaction threads are unique per root and expiration time.\n  return expirationTime * 1000 + interactionThreadID;\n}\n\n// Creates a unique async expiration time.\nfunction computeUniqueAsyncExpiration() {\n  var currentTime = requestCurrentTime();\n  var result = computeAsyncExpiration(currentTime);\n  if (result >= lastUniqueAsyncExpiration) {\n    // Since we assume the current time monotonically increases, we only hit\n    // this branch when computeUniqueAsyncExpiration is fired multiple times\n    // within a 200ms window (or whatever the async bucket size is).\n    result = lastUniqueAsyncExpiration - 1;\n  }\n  lastUniqueAsyncExpiration = result;\n  return lastUniqueAsyncExpiration;\n}\n\nfunction computeExpirationForFiber(currentTime, fiber) {\n  var priorityLevel = unstable_getCurrentPriorityLevel();\n\n  var expirationTime = void 0;\n  if ((fiber.mode & ConcurrentMode) === NoContext) {\n    // Outside of concurrent mode, updates are always synchronous.\n    expirationTime = Sync;\n  } else if (isWorking && !isCommitting$1) {\n    // During render phase, updates expire during as the current render.\n    expirationTime = nextRenderExpirationTime;\n  } else {\n    switch (priorityLevel) {\n      case unstable_ImmediatePriority:\n        expirationTime = Sync;\n        break;\n      case unstable_UserBlockingPriority:\n        expirationTime = computeInteractiveExpiration(currentTime);\n        break;\n      case unstable_NormalPriority:\n        // This is a normal, concurrent update\n        expirationTime = computeAsyncExpiration(currentTime);\n        break;\n      case unstable_LowPriority:\n      case unstable_IdlePriority:\n        expirationTime = Never;\n        break;\n      default:\n        invariant(false, 'Unknown priority level. This error is likely caused by a bug in React. Please file an issue.');\n    }\n\n    // If we're in the middle of rendering a tree, do not update at the same\n    // expiration time that is already rendering.\n    if (nextRoot !== null && expirationTime === nextRenderExpirationTime) {\n      expirationTime -= 1;\n    }\n  }\n\n  // Keep track of the lowest pending interactive expiration time. This\n  // allows us to synchronously flush all interactive updates\n  // when needed.\n  // TODO: Move this to renderer?\n  if (priorityLevel === unstable_UserBlockingPriority && (lowestPriorityPendingInteractiveExpirationTime === NoWork || expirationTime < lowestPriorityPendingInteractiveExpirationTime)) {\n    lowestPriorityPendingInteractiveExpirationTime = expirationTime;\n  }\n\n  return expirationTime;\n}\n\nfunction renderDidSuspend(root, absoluteTimeoutMs, suspendedTime) {\n  // Schedule the timeout.\n  if (absoluteTimeoutMs >= 0 && nextLatestAbsoluteTimeoutMs < absoluteTimeoutMs) {\n    nextLatestAbsoluteTimeoutMs = absoluteTimeoutMs;\n  }\n}\n\nfunction renderDidError() {\n  nextRenderDidError = true;\n}\n\nfunction pingSuspendedRoot(root, thenable, pingTime) {\n  // A promise that previously suspended React from committing has resolved.\n  // If React is still suspended, try again at the previous level (pingTime).\n\n  var pingCache = root.pingCache;\n  if (pingCache !== null) {\n    // The thenable resolved, so we no longer need to memoize, because it will\n    // never be thrown again.\n    pingCache.delete(thenable);\n  }\n\n  if (nextRoot !== null && nextRenderExpirationTime === pingTime) {\n    // Received a ping at the same priority level at which we're currently\n    // rendering. Restart from the root.\n    nextRoot = null;\n  } else {\n    // Confirm that the root is still suspended at this level. Otherwise exit.\n    if (isPriorityLevelSuspended(root, pingTime)) {\n      // Ping at the original level\n      markPingedPriorityLevel(root, pingTime);\n      var rootExpirationTime = root.expirationTime;\n      if (rootExpirationTime !== NoWork) {\n        requestWork(root, rootExpirationTime);\n      }\n    }\n  }\n}\n\nfunction retryTimedOutBoundary(boundaryFiber, thenable) {\n  // The boundary fiber (a Suspense component) previously timed out and was\n  // rendered in its fallback state. One of the promises that suspended it has\n  // resolved, which means at least part of the tree was likely unblocked. Try\n  var retryCache = void 0;\n  if (enableSuspenseServerRenderer) {\n    switch (boundaryFiber.tag) {\n      case SuspenseComponent:\n        retryCache = boundaryFiber.stateNode;\n        break;\n      case DehydratedSuspenseComponent:\n        retryCache = boundaryFiber.memoizedState;\n        break;\n      default:\n        invariant(false, 'Pinged unknown suspense boundary type. This is probably a bug in React.');\n    }\n  } else {\n    retryCache = boundaryFiber.stateNode;\n  }\n  if (retryCache !== null) {\n    // The thenable resolved, so we no longer need to memoize, because it will\n    // never be thrown again.\n    retryCache.delete(thenable);\n  }\n\n  var currentTime = requestCurrentTime();\n  var retryTime = computeExpirationForFiber(currentTime, boundaryFiber);\n  var root = scheduleWorkToRoot(boundaryFiber, retryTime);\n  if (root !== null) {\n    markPendingPriorityLevel(root, retryTime);\n    var rootExpirationTime = root.expirationTime;\n    if (rootExpirationTime !== NoWork) {\n      requestWork(root, rootExpirationTime);\n    }\n  }\n}\n\nfunction scheduleWorkToRoot(fiber, expirationTime) {\n  recordScheduleUpdate();\n\n  {\n    if (fiber.tag === ClassComponent) {\n      var instance = fiber.stateNode;\n      warnAboutInvalidUpdates(instance);\n    }\n  }\n\n  // Update the source fiber's expiration time\n  if (fiber.expirationTime < expirationTime) {\n    fiber.expirationTime = expirationTime;\n  }\n  var alternate = fiber.alternate;\n  if (alternate !== null && alternate.expirationTime < expirationTime) {\n    alternate.expirationTime = expirationTime;\n  }\n  // Walk the parent path to the root and update the child expiration time.\n  var node = fiber.return;\n  var root = null;\n  if (node === null && fiber.tag === HostRoot) {\n    root = fiber.stateNode;\n  } else {\n    while (node !== null) {\n      alternate = node.alternate;\n      if (node.childExpirationTime < expirationTime) {\n        node.childExpirationTime = expirationTime;\n        if (alternate !== null && alternate.childExpirationTime < expirationTime) {\n          alternate.childExpirationTime = expirationTime;\n        }\n      } else if (alternate !== null && alternate.childExpirationTime < expirationTime) {\n        alternate.childExpirationTime = expirationTime;\n      }\n      if (node.return === null && node.tag === HostRoot) {\n        root = node.stateNode;\n        break;\n      }\n      node = node.return;\n    }\n  }\n\n  if (enableSchedulerTracing) {\n    if (root !== null) {\n      var interactions = __interactionsRef.current;\n      if (interactions.size > 0) {\n        var pendingInteractionMap = root.pendingInteractionMap;\n        var pendingInteractions = pendingInteractionMap.get(expirationTime);\n        if (pendingInteractions != null) {\n          interactions.forEach(function (interaction) {\n            if (!pendingInteractions.has(interaction)) {\n              // Update the pending async work count for previously unscheduled interaction.\n              interaction.__count++;\n            }\n\n            pendingInteractions.add(interaction);\n          });\n        } else {\n          pendingInteractionMap.set(expirationTime, new Set(interactions));\n\n          // Update the pending async work count for the current interactions.\n          interactions.forEach(function (interaction) {\n            interaction.__count++;\n          });\n        }\n\n        var subscriber = __subscriberRef.current;\n        if (subscriber !== null) {\n          var threadID = computeThreadID(expirationTime, root.interactionThreadID);\n          subscriber.onWorkScheduled(interactions, threadID);\n        }\n      }\n    }\n  }\n  return root;\n}\n\nfunction warnIfNotCurrentlyBatchingInDev(fiber) {\n  {\n    if (isRendering === false && isBatchingUpdates === false) {\n      warningWithoutStack$1(false, 'An update to %s inside a test was not wrapped in act(...).\\n\\n' + 'When testing, code that causes React state updates should be wrapped into act(...):\\n\\n' + 'act(() => {\\n' + '  /* fire events that update state */\\n' + '});\\n' + '/* assert on the output */\\n\\n' + \"This ensures that you're testing the behavior the user would see in the browser.\" + ' Learn more at https://fb.me/react-wrap-tests-with-act' + '%s', getComponentName(fiber.type), getStackByFiberInDevAndProd(fiber));\n    }\n  }\n}\n\nfunction scheduleWork(fiber, expirationTime) {\n  var root = scheduleWorkToRoot(fiber, expirationTime);\n  if (root === null) {\n    {\n      switch (fiber.tag) {\n        case ClassComponent:\n          warnAboutUpdateOnUnmounted(fiber, true);\n          break;\n        case FunctionComponent:\n        case ForwardRef:\n        case MemoComponent:\n        case SimpleMemoComponent:\n          warnAboutUpdateOnUnmounted(fiber, false);\n          break;\n      }\n    }\n    return;\n  }\n\n  if (!isWorking && nextRenderExpirationTime !== NoWork && expirationTime > nextRenderExpirationTime) {\n    // This is an interruption. (Used for performance tracking.)\n    interruptedBy = fiber;\n    resetStack();\n  }\n  markPendingPriorityLevel(root, expirationTime);\n  if (\n  // If we're in the render phase, we don't need to schedule this root\n  // for an update, because we'll do it before we exit...\n  !isWorking || isCommitting$1 ||\n  // ...unless this is a different root than the one we're rendering.\n  nextRoot !== root) {\n    var rootExpirationTime = root.expirationTime;\n    requestWork(root, rootExpirationTime);\n  }\n  if (nestedUpdateCount > NESTED_UPDATE_LIMIT) {\n    // Reset this back to zero so subsequent updates don't throw.\n    nestedUpdateCount = 0;\n    invariant(false, 'Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.');\n  }\n}\n\nfunction syncUpdates(fn, a, b, c, d) {\n  return unstable_runWithPriority(unstable_ImmediatePriority, function () {\n    return fn(a, b, c, d);\n  });\n}\n\n// TODO: Everything below this is written as if it has been lifted to the\n// renderers. I'll do this in a follow-up.\n\n// Linked-list of roots\nvar firstScheduledRoot = null;\nvar lastScheduledRoot = null;\n\nvar callbackExpirationTime = NoWork;\nvar callbackID = void 0;\nvar isRendering = false;\nvar nextFlushedRoot = null;\nvar nextFlushedExpirationTime = NoWork;\nvar lowestPriorityPendingInteractiveExpirationTime = NoWork;\nvar hasUnhandledError = false;\nvar unhandledError = null;\n\nvar isBatchingUpdates = false;\nvar isUnbatchingUpdates = false;\n\nvar completedBatches = null;\n\nvar originalStartTimeMs = unstable_now();\nvar currentRendererTime = msToExpirationTime(originalStartTimeMs);\nvar currentSchedulerTime = currentRendererTime;\n\n// Use these to prevent an infinite loop of nested updates\nvar NESTED_UPDATE_LIMIT = 50;\nvar nestedUpdateCount = 0;\nvar lastCommittedRootDuringThisBatch = null;\n\nfunction recomputeCurrentRendererTime() {\n  var currentTimeMs = unstable_now() - originalStartTimeMs;\n  currentRendererTime = msToExpirationTime(currentTimeMs);\n}\n\nfunction scheduleCallbackWithExpirationTime(root, expirationTime) {\n  if (callbackExpirationTime !== NoWork) {\n    // A callback is already scheduled. Check its expiration time (timeout).\n    if (expirationTime < callbackExpirationTime) {\n      // Existing callback has sufficient timeout. Exit.\n      return;\n    } else {\n      if (callbackID !== null) {\n        // Existing callback has insufficient timeout. Cancel and schedule a\n        // new one.\n        unstable_cancelCallback(callbackID);\n      }\n    }\n    // The request callback timer is already running. Don't start a new one.\n  } else {\n    startRequestCallbackTimer();\n  }\n\n  callbackExpirationTime = expirationTime;\n  var currentMs = unstable_now() - originalStartTimeMs;\n  var expirationTimeMs = expirationTimeToMs(expirationTime);\n  var timeout = expirationTimeMs - currentMs;\n  callbackID = unstable_scheduleCallback(performAsyncWork, { timeout: timeout });\n}\n\n// For every call to renderRoot, one of onFatal, onComplete, onSuspend, and\n// onYield is called upon exiting. We use these in lieu of returning a tuple.\n// I've also chosen not to inline them into renderRoot because these will\n// eventually be lifted into the renderer.\nfunction onFatal(root) {\n  root.finishedWork = null;\n}\n\nfunction onComplete(root, finishedWork, expirationTime) {\n  root.pendingCommitExpirationTime = expirationTime;\n  root.finishedWork = finishedWork;\n}\n\nfunction onSuspend(root, finishedWork, suspendedExpirationTime, rootExpirationTime, msUntilTimeout) {\n  root.expirationTime = rootExpirationTime;\n  if (msUntilTimeout === 0 && !shouldYieldToRenderer()) {\n    // Don't wait an additional tick. Commit the tree immediately.\n    root.pendingCommitExpirationTime = suspendedExpirationTime;\n    root.finishedWork = finishedWork;\n  } else if (msUntilTimeout > 0) {\n    // Wait `msUntilTimeout` milliseconds before committing.\n    root.timeoutHandle = scheduleTimeout(onTimeout.bind(null, root, finishedWork, suspendedExpirationTime), msUntilTimeout);\n  }\n}\n\nfunction onYield(root) {\n  root.finishedWork = null;\n}\n\nfunction onTimeout(root, finishedWork, suspendedExpirationTime) {\n  // The root timed out. Commit it.\n  root.pendingCommitExpirationTime = suspendedExpirationTime;\n  root.finishedWork = finishedWork;\n  // Read the current time before entering the commit phase. We can be\n  // certain this won't cause tearing related to batching of event updates\n  // because we're at the top of a timer event.\n  recomputeCurrentRendererTime();\n  currentSchedulerTime = currentRendererTime;\n  flushRoot(root, suspendedExpirationTime);\n}\n\nfunction onCommit(root, expirationTime) {\n  root.expirationTime = expirationTime;\n  root.finishedWork = null;\n}\n\nfunction requestCurrentTime() {\n  // requestCurrentTime is called by the scheduler to compute an expiration\n  // time.\n  //\n  // Expiration times are computed by adding to the current time (the start\n  // time). However, if two updates are scheduled within the same event, we\n  // should treat their start times as simultaneous, even if the actual clock\n  // time has advanced between the first and second call.\n\n  // In other words, because expiration times determine how updates are batched,\n  // we want all updates of like priority that occur within the same event to\n  // receive the same expiration time. Otherwise we get tearing.\n  //\n  // We keep track of two separate times: the current \"renderer\" time and the\n  // current \"scheduler\" time. The renderer time can be updated whenever; it\n  // only exists to minimize the calls performance.now.\n  //\n  // But the scheduler time can only be updated if there's no pending work, or\n  // if we know for certain that we're not in the middle of an event.\n\n  if (isRendering) {\n    // We're already rendering. Return the most recently read time.\n    return currentSchedulerTime;\n  }\n  // Check if there's pending work.\n  findHighestPriorityRoot();\n  if (nextFlushedExpirationTime === NoWork || nextFlushedExpirationTime === Never) {\n    // If there's no pending work, or if the pending work is offscreen, we can\n    // read the current time without risk of tearing.\n    recomputeCurrentRendererTime();\n    currentSchedulerTime = currentRendererTime;\n    return currentSchedulerTime;\n  }\n  // There's already pending work. We might be in the middle of a browser\n  // event. If we were to read the current time, it could cause multiple updates\n  // within the same event to receive different expiration times, leading to\n  // tearing. Return the last read time. During the next idle callback, the\n  // time will be updated.\n  return currentSchedulerTime;\n}\n\n// requestWork is called by the scheduler whenever a root receives an update.\n// It's up to the renderer to call renderRoot at some point in the future.\nfunction requestWork(root, expirationTime) {\n  addRootToSchedule(root, expirationTime);\n  if (isRendering) {\n    // Prevent reentrancy. Remaining work will be scheduled at the end of\n    // the currently rendering batch.\n    return;\n  }\n\n  if (isBatchingUpdates) {\n    // Flush work at the end of the batch.\n    if (isUnbatchingUpdates) {\n      // ...unless we're inside unbatchedUpdates, in which case we should\n      // flush it now.\n      nextFlushedRoot = root;\n      nextFlushedExpirationTime = Sync;\n      performWorkOnRoot(root, Sync, false);\n    }\n    return;\n  }\n\n  // TODO: Get rid of Sync and use current time?\n  if (expirationTime === Sync) {\n    performSyncWork();\n  } else {\n    scheduleCallbackWithExpirationTime(root, expirationTime);\n  }\n}\n\nfunction addRootToSchedule(root, expirationTime) {\n  // Add the root to the schedule.\n  // Check if this root is already part of the schedule.\n  if (root.nextScheduledRoot === null) {\n    // This root is not already scheduled. Add it.\n    root.expirationTime = expirationTime;\n    if (lastScheduledRoot === null) {\n      firstScheduledRoot = lastScheduledRoot = root;\n      root.nextScheduledRoot = root;\n    } else {\n      lastScheduledRoot.nextScheduledRoot = root;\n      lastScheduledRoot = root;\n      lastScheduledRoot.nextScheduledRoot = firstScheduledRoot;\n    }\n  } else {\n    // This root is already scheduled, but its priority may have increased.\n    var remainingExpirationTime = root.expirationTime;\n    if (expirationTime > remainingExpirationTime) {\n      // Update the priority.\n      root.expirationTime = expirationTime;\n    }\n  }\n}\n\nfunction findHighestPriorityRoot() {\n  var highestPriorityWork = NoWork;\n  var highestPriorityRoot = null;\n  if (lastScheduledRoot !== null) {\n    var previousScheduledRoot = lastScheduledRoot;\n    var root = firstScheduledRoot;\n    while (root !== null) {\n      var remainingExpirationTime = root.expirationTime;\n      if (remainingExpirationTime === NoWork) {\n        // This root no longer has work. Remove it from the scheduler.\n\n        // TODO: This check is redudant, but Flow is confused by the branch\n        // below where we set lastScheduledRoot to null, even though we break\n        // from the loop right after.\n        !(previousScheduledRoot !== null && lastScheduledRoot !== null) ? invariant(false, 'Should have a previous and last root. This error is likely caused by a bug in React. Please file an issue.') : void 0;\n        if (root === root.nextScheduledRoot) {\n          // This is the only root in the list.\n          root.nextScheduledRoot = null;\n          firstScheduledRoot = lastScheduledRoot = null;\n          break;\n        } else if (root === firstScheduledRoot) {\n          // This is the first root in the list.\n          var next = root.nextScheduledRoot;\n          firstScheduledRoot = next;\n          lastScheduledRoot.nextScheduledRoot = next;\n          root.nextScheduledRoot = null;\n        } else if (root === lastScheduledRoot) {\n          // This is the last root in the list.\n          lastScheduledRoot = previousScheduledRoot;\n          lastScheduledRoot.nextScheduledRoot = firstScheduledRoot;\n          root.nextScheduledRoot = null;\n          break;\n        } else {\n          previousScheduledRoot.nextScheduledRoot = root.nextScheduledRoot;\n          root.nextScheduledRoot = null;\n        }\n        root = previousScheduledRoot.nextScheduledRoot;\n      } else {\n        if (remainingExpirationTime > highestPriorityWork) {\n          // Update the priority, if it's higher\n          highestPriorityWork = remainingExpirationTime;\n          highestPriorityRoot = root;\n        }\n        if (root === lastScheduledRoot) {\n          break;\n        }\n        if (highestPriorityWork === Sync) {\n          // Sync is highest priority by definition so\n          // we can stop searching.\n          break;\n        }\n        previousScheduledRoot = root;\n        root = root.nextScheduledRoot;\n      }\n    }\n  }\n\n  nextFlushedRoot = highestPriorityRoot;\n  nextFlushedExpirationTime = highestPriorityWork;\n}\n\n// TODO: This wrapper exists because many of the older tests (the ones that use\n// flushDeferredPri) rely on the number of times `shouldYield` is called. We\n// should get rid of it.\nvar didYield = false;\nfunction shouldYieldToRenderer() {\n  if (didYield) {\n    return true;\n  }\n  if (unstable_shouldYield()) {\n    didYield = true;\n    return true;\n  }\n  return false;\n}\n\nfunction performAsyncWork() {\n  try {\n    if (!shouldYieldToRenderer()) {\n      // The callback timed out. That means at least one update has expired.\n      // Iterate through the root schedule. If they contain expired work, set\n      // the next render expiration time to the current time. This has the effect\n      // of flushing all expired work in a single batch, instead of flushing each\n      // level one at a time.\n      if (firstScheduledRoot !== null) {\n        recomputeCurrentRendererTime();\n        var root = firstScheduledRoot;\n        do {\n          didExpireAtExpirationTime(root, currentRendererTime);\n          // The root schedule is circular, so this is never null.\n          root = root.nextScheduledRoot;\n        } while (root !== firstScheduledRoot);\n      }\n    }\n    performWork(NoWork, true);\n  } finally {\n    didYield = false;\n  }\n}\n\nfunction performSyncWork() {\n  performWork(Sync, false);\n}\n\nfunction performWork(minExpirationTime, isYieldy) {\n  // Keep working on roots until there's no more work, or until there's a higher\n  // priority event.\n  findHighestPriorityRoot();\n\n  if (isYieldy) {\n    recomputeCurrentRendererTime();\n    currentSchedulerTime = currentRendererTime;\n\n    if (enableUserTimingAPI) {\n      var didExpire = nextFlushedExpirationTime > currentRendererTime;\n      var timeout = expirationTimeToMs(nextFlushedExpirationTime);\n      stopRequestCallbackTimer(didExpire, timeout);\n    }\n\n    while (nextFlushedRoot !== null && nextFlushedExpirationTime !== NoWork && minExpirationTime <= nextFlushedExpirationTime && !(didYield && currentRendererTime > nextFlushedExpirationTime)) {\n      performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime, currentRendererTime > nextFlushedExpirationTime);\n      findHighestPriorityRoot();\n      recomputeCurrentRendererTime();\n      currentSchedulerTime = currentRendererTime;\n    }\n  } else {\n    while (nextFlushedRoot !== null && nextFlushedExpirationTime !== NoWork && minExpirationTime <= nextFlushedExpirationTime) {\n      performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime, false);\n      findHighestPriorityRoot();\n    }\n  }\n\n  // We're done flushing work. Either we ran out of time in this callback,\n  // or there's no more work left with sufficient priority.\n\n  // If we're inside a callback, set this to false since we just completed it.\n  if (isYieldy) {\n    callbackExpirationTime = NoWork;\n    callbackID = null;\n  }\n  // If there's work left over, schedule a new callback.\n  if (nextFlushedExpirationTime !== NoWork) {\n    scheduleCallbackWithExpirationTime(nextFlushedRoot, nextFlushedExpirationTime);\n  }\n\n  // Clean-up.\n  finishRendering();\n}\n\nfunction flushRoot(root, expirationTime) {\n  !!isRendering ? invariant(false, 'work.commit(): Cannot commit while already rendering. This likely means you attempted to commit from inside a lifecycle method.') : void 0;\n  // Perform work on root as if the given expiration time is the current time.\n  // This has the effect of synchronously flushing all work up to and\n  // including the given time.\n  nextFlushedRoot = root;\n  nextFlushedExpirationTime = expirationTime;\n  performWorkOnRoot(root, expirationTime, false);\n  // Flush any sync work that was scheduled by lifecycles\n  performSyncWork();\n}\n\nfunction finishRendering() {\n  nestedUpdateCount = 0;\n  lastCommittedRootDuringThisBatch = null;\n\n  if (completedBatches !== null) {\n    var batches = completedBatches;\n    completedBatches = null;\n    for (var i = 0; i < batches.length; i++) {\n      var batch = batches[i];\n      try {\n        batch._onComplete();\n      } catch (error) {\n        if (!hasUnhandledError) {\n          hasUnhandledError = true;\n          unhandledError = error;\n        }\n      }\n    }\n  }\n\n  if (hasUnhandledError) {\n    var error = unhandledError;\n    unhandledError = null;\n    hasUnhandledError = false;\n    throw error;\n  }\n}\n\nfunction performWorkOnRoot(root, expirationTime, isYieldy) {\n  !!isRendering ? invariant(false, 'performWorkOnRoot was called recursively. This error is likely caused by a bug in React. Please file an issue.') : void 0;\n\n  isRendering = true;\n\n  // Check if this is async work or sync/expired work.\n  if (!isYieldy) {\n    // Flush work without yielding.\n    // TODO: Non-yieldy work does not necessarily imply expired work. A renderer\n    // may want to perform some work without yielding, but also without\n    // requiring the root to complete (by triggering placeholders).\n\n    var finishedWork = root.finishedWork;\n    if (finishedWork !== null) {\n      // This root is already complete. We can commit it.\n      completeRoot(root, finishedWork, expirationTime);\n    } else {\n      root.finishedWork = null;\n      // If this root previously suspended, clear its existing timeout, since\n      // we're about to try rendering again.\n      var timeoutHandle = root.timeoutHandle;\n      if (timeoutHandle !== noTimeout) {\n        root.timeoutHandle = noTimeout;\n        // $FlowFixMe Complains noTimeout is not a TimeoutID, despite the check above\n        cancelTimeout(timeoutHandle);\n      }\n      renderRoot(root, isYieldy);\n      finishedWork = root.finishedWork;\n      if (finishedWork !== null) {\n        // We've completed the root. Commit it.\n        completeRoot(root, finishedWork, expirationTime);\n      }\n    }\n  } else {\n    // Flush async work.\n    var _finishedWork = root.finishedWork;\n    if (_finishedWork !== null) {\n      // This root is already complete. We can commit it.\n      completeRoot(root, _finishedWork, expirationTime);\n    } else {\n      root.finishedWork = null;\n      // If this root previously suspended, clear its existing timeout, since\n      // we're about to try rendering again.\n      var _timeoutHandle = root.timeoutHandle;\n      if (_timeoutHandle !== noTimeout) {\n        root.timeoutHandle = noTimeout;\n        // $FlowFixMe Complains noTimeout is not a TimeoutID, despite the check above\n        cancelTimeout(_timeoutHandle);\n      }\n      renderRoot(root, isYieldy);\n      _finishedWork = root.finishedWork;\n      if (_finishedWork !== null) {\n        // We've completed the root. Check the if we should yield one more time\n        // before committing.\n        if (!shouldYieldToRenderer()) {\n          // Still time left. Commit the root.\n          completeRoot(root, _finishedWork, expirationTime);\n        } else {\n          // There's no time left. Mark this root as complete. We'll come\n          // back and commit it later.\n          root.finishedWork = _finishedWork;\n        }\n      }\n    }\n  }\n\n  isRendering = false;\n}\n\nfunction completeRoot(root, finishedWork, expirationTime) {\n  // Check if there's a batch that matches this expiration time.\n  var firstBatch = root.firstBatch;\n  if (firstBatch !== null && firstBatch._expirationTime >= expirationTime) {\n    if (completedBatches === null) {\n      completedBatches = [firstBatch];\n    } else {\n      completedBatches.push(firstBatch);\n    }\n    if (firstBatch._defer) {\n      // This root is blocked from committing by a batch. Unschedule it until\n      // we receive another update.\n      root.finishedWork = finishedWork;\n      root.expirationTime = NoWork;\n      return;\n    }\n  }\n\n  // Commit the root.\n  root.finishedWork = null;\n\n  // Check if this is a nested update (a sync update scheduled during the\n  // commit phase).\n  if (root === lastCommittedRootDuringThisBatch) {\n    // If the next root is the same as the previous root, this is a nested\n    // update. To prevent an infinite loop, increment the nested update count.\n    nestedUpdateCount++;\n  } else {\n    // Reset whenever we switch roots.\n    lastCommittedRootDuringThisBatch = root;\n    nestedUpdateCount = 0;\n  }\n  unstable_runWithPriority(unstable_ImmediatePriority, function () {\n    commitRoot(root, finishedWork);\n  });\n}\n\nfunction onUncaughtError(error) {\n  !(nextFlushedRoot !== null) ? invariant(false, 'Should be working on a root. This error is likely caused by a bug in React. Please file an issue.') : void 0;\n  // Unschedule this root so we don't work on it again until there's\n  // another update.\n  nextFlushedRoot.expirationTime = NoWork;\n  if (!hasUnhandledError) {\n    hasUnhandledError = true;\n    unhandledError = error;\n  }\n}\n\n// TODO: Batching should be implemented at the renderer level, not inside\n// the reconciler.\nfunction batchedUpdates$1(fn, a) {\n  var previousIsBatchingUpdates = isBatchingUpdates;\n  isBatchingUpdates = true;\n  try {\n    return fn(a);\n  } finally {\n    isBatchingUpdates = previousIsBatchingUpdates;\n    if (!isBatchingUpdates && !isRendering) {\n      performSyncWork();\n    }\n  }\n}\n\n// TODO: Batching should be implemented at the renderer level, not inside\n// the reconciler.\nfunction unbatchedUpdates(fn, a) {\n  if (isBatchingUpdates && !isUnbatchingUpdates) {\n    isUnbatchingUpdates = true;\n    try {\n      return fn(a);\n    } finally {\n      isUnbatchingUpdates = false;\n    }\n  }\n  return fn(a);\n}\n\n// TODO: Batching should be implemented at the renderer level, not within\n// the reconciler.\nfunction flushSync(fn, a) {\n  !!isRendering ? invariant(false, 'flushSync was called from inside a lifecycle method. It cannot be called when React is already rendering.') : void 0;\n  var previousIsBatchingUpdates = isBatchingUpdates;\n  isBatchingUpdates = true;\n  try {\n    return syncUpdates(fn, a);\n  } finally {\n    isBatchingUpdates = previousIsBatchingUpdates;\n    performSyncWork();\n  }\n}\n\nfunction interactiveUpdates$1(fn, a, b) {\n  // If there are any pending interactive updates, synchronously flush them.\n  // This needs to happen before we read any handlers, because the effect of\n  // the previous event may influence which handlers are called during\n  // this event.\n  if (!isBatchingUpdates && !isRendering && lowestPriorityPendingInteractiveExpirationTime !== NoWork) {\n    // Synchronously flush pending interactive updates.\n    performWork(lowestPriorityPendingInteractiveExpirationTime, false);\n    lowestPriorityPendingInteractiveExpirationTime = NoWork;\n  }\n  var previousIsBatchingUpdates = isBatchingUpdates;\n  isBatchingUpdates = true;\n  try {\n    return unstable_runWithPriority(unstable_UserBlockingPriority, function () {\n      return fn(a, b);\n    });\n  } finally {\n    isBatchingUpdates = previousIsBatchingUpdates;\n    if (!isBatchingUpdates && !isRendering) {\n      performSyncWork();\n    }\n  }\n}\n\nfunction flushInteractiveUpdates$1() {\n  if (!isRendering && lowestPriorityPendingInteractiveExpirationTime !== NoWork) {\n    // Synchronously flush pending interactive updates.\n    performWork(lowestPriorityPendingInteractiveExpirationTime, false);\n    lowestPriorityPendingInteractiveExpirationTime = NoWork;\n  }\n}\n\nfunction flushControlled(fn) {\n  var previousIsBatchingUpdates = isBatchingUpdates;\n  isBatchingUpdates = true;\n  try {\n    syncUpdates(fn);\n  } finally {\n    isBatchingUpdates = previousIsBatchingUpdates;\n    if (!isBatchingUpdates && !isRendering) {\n      performSyncWork();\n    }\n  }\n}\n\n// 0 is PROD, 1 is DEV.\n// Might add PROFILE later.\n\n\nvar didWarnAboutNestedUpdates = void 0;\nvar didWarnAboutFindNodeInStrictMode = void 0;\n\n{\n  didWarnAboutNestedUpdates = false;\n  didWarnAboutFindNodeInStrictMode = {};\n}\n\nfunction getContextForSubtree(parentComponent) {\n  if (!parentComponent) {\n    return emptyContextObject;\n  }\n\n  var fiber = get(parentComponent);\n  var parentContext = findCurrentUnmaskedContext(fiber);\n\n  if (fiber.tag === ClassComponent) {\n    var Component = fiber.type;\n    if (isContextProvider(Component)) {\n      return processChildContext(fiber, Component, parentContext);\n    }\n  }\n\n  return parentContext;\n}\n\nfunction scheduleRootUpdate(current$$1, element, expirationTime, callback) {\n  {\n    if (phase === 'render' && current !== null && !didWarnAboutNestedUpdates) {\n      didWarnAboutNestedUpdates = true;\n      warningWithoutStack$1(false, 'Render methods should be a pure function of props and state; ' + 'triggering nested component updates from render is not allowed. ' + 'If necessary, trigger nested updates in componentDidUpdate.\\n\\n' + 'Check the render method of %s.', getComponentName(current.type) || 'Unknown');\n    }\n  }\n\n  var update = createUpdate(expirationTime);\n  // Caution: React DevTools currently depends on this property\n  // being called \"element\".\n  update.payload = { element: element };\n\n  callback = callback === undefined ? null : callback;\n  if (callback !== null) {\n    !(typeof callback === 'function') ? warningWithoutStack$1(false, 'render(...): Expected the last optional `callback` argument to be a ' + 'function. Instead received: %s.', callback) : void 0;\n    update.callback = callback;\n  }\n\n  flushPassiveEffects();\n  enqueueUpdate(current$$1, update);\n  scheduleWork(current$$1, expirationTime);\n\n  return expirationTime;\n}\n\nfunction updateContainerAtExpirationTime(element, container, parentComponent, expirationTime, callback) {\n  // TODO: If this is a nested container, this won't be the root.\n  var current$$1 = container.current;\n\n  {\n    if (ReactFiberInstrumentation_1.debugTool) {\n      if (current$$1.alternate === null) {\n        ReactFiberInstrumentation_1.debugTool.onMountContainer(container);\n      } else if (element === null) {\n        ReactFiberInstrumentation_1.debugTool.onUnmountContainer(container);\n      } else {\n        ReactFiberInstrumentation_1.debugTool.onUpdateContainer(container);\n      }\n    }\n  }\n\n  var context = getContextForSubtree(parentComponent);\n  if (container.context === null) {\n    container.context = context;\n  } else {\n    container.pendingContext = context;\n  }\n\n  return scheduleRootUpdate(current$$1, element, expirationTime, callback);\n}\n\nfunction findHostInstance(component) {\n  var fiber = get(component);\n  if (fiber === undefined) {\n    if (typeof component.render === 'function') {\n      invariant(false, 'Unable to find node on an unmounted component.');\n    } else {\n      invariant(false, 'Argument appears to not be a ReactComponent. Keys: %s', Object.keys(component));\n    }\n  }\n  var hostFiber = findCurrentHostFiber(fiber);\n  if (hostFiber === null) {\n    return null;\n  }\n  return hostFiber.stateNode;\n}\n\nfunction findHostInstanceWithWarning(component, methodName) {\n  {\n    var fiber = get(component);\n    if (fiber === undefined) {\n      if (typeof component.render === 'function') {\n        invariant(false, 'Unable to find node on an unmounted component.');\n      } else {\n        invariant(false, 'Argument appears to not be a ReactComponent. Keys: %s', Object.keys(component));\n      }\n    }\n    var hostFiber = findCurrentHostFiber(fiber);\n    if (hostFiber === null) {\n      return null;\n    }\n    if (hostFiber.mode & StrictMode) {\n      var componentName = getComponentName(fiber.type) || 'Component';\n      if (!didWarnAboutFindNodeInStrictMode[componentName]) {\n        didWarnAboutFindNodeInStrictMode[componentName] = true;\n        if (fiber.mode & StrictMode) {\n          warningWithoutStack$1(false, '%s is deprecated in StrictMode. ' + '%s was passed an instance of %s which is inside StrictMode. ' + 'Instead, add a ref directly to the element you want to reference.' + '\\n%s' + '\\n\\nLearn more about using refs safely here:' + '\\nhttps://fb.me/react-strict-mode-find-node', methodName, methodName, componentName, getStackByFiberInDevAndProd(hostFiber));\n        } else {\n          warningWithoutStack$1(false, '%s is deprecated in StrictMode. ' + '%s was passed an instance of %s which renders StrictMode children. ' + 'Instead, add a ref directly to the element you want to reference.' + '\\n%s' + '\\n\\nLearn more about using refs safely here:' + '\\nhttps://fb.me/react-strict-mode-find-node', methodName, methodName, componentName, getStackByFiberInDevAndProd(hostFiber));\n        }\n      }\n    }\n    return hostFiber.stateNode;\n  }\n  return findHostInstance(component);\n}\n\nfunction createContainer(containerInfo, isConcurrent, hydrate) {\n  return createFiberRoot(containerInfo, isConcurrent, hydrate);\n}\n\nfunction updateContainer(element, container, parentComponent, callback) {\n  var current$$1 = container.current;\n  var currentTime = requestCurrentTime();\n  var expirationTime = computeExpirationForFiber(currentTime, current$$1);\n  return updateContainerAtExpirationTime(element, container, parentComponent, expirationTime, callback);\n}\n\nfunction getPublicRootInstance(container) {\n  var containerFiber = container.current;\n  if (!containerFiber.child) {\n    return null;\n  }\n  switch (containerFiber.child.tag) {\n    case HostComponent:\n      return getPublicInstance(containerFiber.child.stateNode);\n    default:\n      return containerFiber.child.stateNode;\n  }\n}\n\nfunction findHostInstanceWithNoPortals(fiber) {\n  var hostFiber = findCurrentHostFiberWithNoPortals(fiber);\n  if (hostFiber === null) {\n    return null;\n  }\n  return hostFiber.stateNode;\n}\n\nvar overrideProps = null;\n\n{\n  var copyWithSetImpl = function (obj, path, idx, value) {\n    if (idx >= path.length) {\n      return value;\n    }\n    var key = path[idx];\n    var updated = Array.isArray(obj) ? obj.slice() : _assign({}, obj);\n    // $FlowFixMe number or string is fine here\n    updated[key] = copyWithSetImpl(obj[key], path, idx + 1, value);\n    return updated;\n  };\n\n  var copyWithSet = function (obj, path, value) {\n    return copyWithSetImpl(obj, path, 0, value);\n  };\n\n  // Support DevTools props for function components, forwardRef, memo, host components, etc.\n  overrideProps = function (fiber, path, value) {\n    flushPassiveEffects();\n    fiber.pendingProps = copyWithSet(fiber.memoizedProps, path, value);\n    if (fiber.alternate) {\n      fiber.alternate.pendingProps = fiber.pendingProps;\n    }\n    scheduleWork(fiber, Sync);\n  };\n}\n\nfunction injectIntoDevTools(devToolsConfig) {\n  var findFiberByHostInstance = devToolsConfig.findFiberByHostInstance;\n  var ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher;\n\n\n  return injectInternals(_assign({}, devToolsConfig, {\n    overrideProps: overrideProps,\n    currentDispatcherRef: ReactCurrentDispatcher,\n    findHostInstanceByFiber: function (fiber) {\n      var hostFiber = findCurrentHostFiber(fiber);\n      if (hostFiber === null) {\n        return null;\n      }\n      return hostFiber.stateNode;\n    },\n    findFiberByHostInstance: function (instance) {\n      if (!findFiberByHostInstance) {\n        // Might not be implemented by the renderer.\n        return null;\n      }\n      return findFiberByHostInstance(instance);\n    }\n  }));\n}\n\n// This file intentionally does *not* have the Flow annotation.\n// Don't add it. See `./inline-typed.js` for an explanation.\n\nfunction createPortal$1(children, containerInfo,\n// TODO: figure out the API for cross-renderer implementation.\nimplementation) {\n  var key = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null;\n\n  return {\n    // This tag allow us to uniquely identify this as a React Portal\n    $$typeof: REACT_PORTAL_TYPE,\n    key: key == null ? null : '' + key,\n    children: children,\n    containerInfo: containerInfo,\n    implementation: implementation\n  };\n}\n\n// TODO: this is special because it gets imported during build.\n\nvar ReactVersion = '16.8.6';\n\n// TODO: This type is shared between the reconciler and ReactDOM, but will\n// eventually be lifted out to the renderer.\n\nvar ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;\n\nvar topLevelUpdateWarnings = void 0;\nvar warnOnInvalidCallback = void 0;\nvar didWarnAboutUnstableCreatePortal = false;\n\n{\n  if (typeof Map !== 'function' ||\n  // $FlowIssue Flow incorrectly thinks Map has no prototype\n  Map.prototype == null || typeof Map.prototype.forEach !== 'function' || typeof Set !== 'function' ||\n  // $FlowIssue Flow incorrectly thinks Set has no prototype\n  Set.prototype == null || typeof Set.prototype.clear !== 'function' || typeof Set.prototype.forEach !== 'function') {\n    warningWithoutStack$1(false, 'React depends on Map and Set built-in types. Make sure that you load a ' + 'polyfill in older browsers. https://fb.me/react-polyfills');\n  }\n\n  topLevelUpdateWarnings = function (container) {\n    if (container._reactRootContainer && container.nodeType !== COMMENT_NODE) {\n      var hostInstance = findHostInstanceWithNoPortals(container._reactRootContainer._internalRoot.current);\n      if (hostInstance) {\n        !(hostInstance.parentNode === container) ? warningWithoutStack$1(false, 'render(...): It looks like the React-rendered content of this ' + 'container was removed without using React. This is not ' + 'supported and will cause errors. Instead, call ' + 'ReactDOM.unmountComponentAtNode to empty a container.') : void 0;\n      }\n    }\n\n    var isRootRenderedBySomeReact = !!container._reactRootContainer;\n    var rootEl = getReactRootElementInContainer(container);\n    var hasNonRootReactChild = !!(rootEl && getInstanceFromNode$1(rootEl));\n\n    !(!hasNonRootReactChild || isRootRenderedBySomeReact) ? warningWithoutStack$1(false, 'render(...): Replacing React-rendered children with a new root ' + 'component. If you intended to update the children of this node, ' + 'you should instead have the existing children update their state ' + 'and render the new components instead of calling ReactDOM.render.') : void 0;\n\n    !(container.nodeType !== ELEMENT_NODE || !container.tagName || container.tagName.toUpperCase() !== 'BODY') ? warningWithoutStack$1(false, 'render(): Rendering components directly into document.body is ' + 'discouraged, since its children are often manipulated by third-party ' + 'scripts and browser extensions. This may lead to subtle ' + 'reconciliation issues. Try rendering into a container element created ' + 'for your app.') : void 0;\n  };\n\n  warnOnInvalidCallback = function (callback, callerName) {\n    !(callback === null || typeof callback === 'function') ? warningWithoutStack$1(false, '%s(...): Expected the last optional `callback` argument to be a ' + 'function. Instead received: %s.', callerName, callback) : void 0;\n  };\n}\n\nsetRestoreImplementation(restoreControlledState$1);\n\nfunction ReactBatch(root) {\n  var expirationTime = computeUniqueAsyncExpiration();\n  this._expirationTime = expirationTime;\n  this._root = root;\n  this._next = null;\n  this._callbacks = null;\n  this._didComplete = false;\n  this._hasChildren = false;\n  this._children = null;\n  this._defer = true;\n}\nReactBatch.prototype.render = function (children) {\n  !this._defer ? invariant(false, 'batch.render: Cannot render a batch that already committed.') : void 0;\n  this._hasChildren = true;\n  this._children = children;\n  var internalRoot = this._root._internalRoot;\n  var expirationTime = this._expirationTime;\n  var work = new ReactWork();\n  updateContainerAtExpirationTime(children, internalRoot, null, expirationTime, work._onCommit);\n  return work;\n};\nReactBatch.prototype.then = function (onComplete) {\n  if (this._didComplete) {\n    onComplete();\n    return;\n  }\n  var callbacks = this._callbacks;\n  if (callbacks === null) {\n    callbacks = this._callbacks = [];\n  }\n  callbacks.push(onComplete);\n};\nReactBatch.prototype.commit = function () {\n  var internalRoot = this._root._internalRoot;\n  var firstBatch = internalRoot.firstBatch;\n  !(this._defer && firstBatch !== null) ? invariant(false, 'batch.commit: Cannot commit a batch multiple times.') : void 0;\n\n  if (!this._hasChildren) {\n    // This batch is empty. Return.\n    this._next = null;\n    this._defer = false;\n    return;\n  }\n\n  var expirationTime = this._expirationTime;\n\n  // Ensure this is the first batch in the list.\n  if (firstBatch !== this) {\n    // This batch is not the earliest batch. We need to move it to the front.\n    // Update its expiration time to be the expiration time of the earliest\n    // batch, so that we can flush it without flushing the other batches.\n    if (this._hasChildren) {\n      expirationTime = this._expirationTime = firstBatch._expirationTime;\n      // Rendering this batch again ensures its children will be the final state\n      // when we flush (updates are processed in insertion order: last\n      // update wins).\n      // TODO: This forces a restart. Should we print a warning?\n      this.render(this._children);\n    }\n\n    // Remove the batch from the list.\n    var previous = null;\n    var batch = firstBatch;\n    while (batch !== this) {\n      previous = batch;\n      batch = batch._next;\n    }\n    !(previous !== null) ? invariant(false, 'batch.commit: Cannot commit a batch multiple times.') : void 0;\n    previous._next = batch._next;\n\n    // Add it to the front.\n    this._next = firstBatch;\n    firstBatch = internalRoot.firstBatch = this;\n  }\n\n  // Synchronously flush all the work up to this batch's expiration time.\n  this._defer = false;\n  flushRoot(internalRoot, expirationTime);\n\n  // Pop the batch from the list.\n  var next = this._next;\n  this._next = null;\n  firstBatch = internalRoot.firstBatch = next;\n\n  // Append the next earliest batch's children to the update queue.\n  if (firstBatch !== null && firstBatch._hasChildren) {\n    firstBatch.render(firstBatch._children);\n  }\n};\nReactBatch.prototype._onComplete = function () {\n  if (this._didComplete) {\n    return;\n  }\n  this._didComplete = true;\n  var callbacks = this._callbacks;\n  if (callbacks === null) {\n    return;\n  }\n  // TODO: Error handling.\n  for (var i = 0; i < callbacks.length; i++) {\n    var _callback = callbacks[i];\n    _callback();\n  }\n};\n\nfunction ReactWork() {\n  this._callbacks = null;\n  this._didCommit = false;\n  // TODO: Avoid need to bind by replacing callbacks in the update queue with\n  // list of Work objects.\n  this._onCommit = this._onCommit.bind(this);\n}\nReactWork.prototype.then = function (onCommit) {\n  if (this._didCommit) {\n    onCommit();\n    return;\n  }\n  var callbacks = this._callbacks;\n  if (callbacks === null) {\n    callbacks = this._callbacks = [];\n  }\n  callbacks.push(onCommit);\n};\nReactWork.prototype._onCommit = function () {\n  if (this._didCommit) {\n    return;\n  }\n  this._didCommit = true;\n  var callbacks = this._callbacks;\n  if (callbacks === null) {\n    return;\n  }\n  // TODO: Error handling.\n  for (var i = 0; i < callbacks.length; i++) {\n    var _callback2 = callbacks[i];\n    !(typeof _callback2 === 'function') ? invariant(false, 'Invalid argument passed as callback. Expected a function. Instead received: %s', _callback2) : void 0;\n    _callback2();\n  }\n};\n\nfunction ReactRoot(container, isConcurrent, hydrate) {\n  var root = createContainer(container, isConcurrent, hydrate);\n  this._internalRoot = root;\n}\nReactRoot.prototype.render = function (children, callback) {\n  var root = this._internalRoot;\n  var work = new ReactWork();\n  callback = callback === undefined ? null : callback;\n  {\n    warnOnInvalidCallback(callback, 'render');\n  }\n  if (callback !== null) {\n    work.then(callback);\n  }\n  updateContainer(children, root, null, work._onCommit);\n  return work;\n};\nReactRoot.prototype.unmount = function (callback) {\n  var root = this._internalRoot;\n  var work = new ReactWork();\n  callback = callback === undefined ? null : callback;\n  {\n    warnOnInvalidCallback(callback, 'render');\n  }\n  if (callback !== null) {\n    work.then(callback);\n  }\n  updateContainer(null, root, null, work._onCommit);\n  return work;\n};\nReactRoot.prototype.legacy_renderSubtreeIntoContainer = function (parentComponent, children, callback) {\n  var root = this._internalRoot;\n  var work = new ReactWork();\n  callback = callback === undefined ? null : callback;\n  {\n    warnOnInvalidCallback(callback, 'render');\n  }\n  if (callback !== null) {\n    work.then(callback);\n  }\n  updateContainer(children, root, parentComponent, work._onCommit);\n  return work;\n};\nReactRoot.prototype.createBatch = function () {\n  var batch = new ReactBatch(this);\n  var expirationTime = batch._expirationTime;\n\n  var internalRoot = this._internalRoot;\n  var firstBatch = internalRoot.firstBatch;\n  if (firstBatch === null) {\n    internalRoot.firstBatch = batch;\n    batch._next = null;\n  } else {\n    // Insert sorted by expiration time then insertion order\n    var insertAfter = null;\n    var insertBefore = firstBatch;\n    while (insertBefore !== null && insertBefore._expirationTime >= expirationTime) {\n      insertAfter = insertBefore;\n      insertBefore = insertBefore._next;\n    }\n    batch._next = insertBefore;\n    if (insertAfter !== null) {\n      insertAfter._next = batch;\n    }\n  }\n\n  return batch;\n};\n\n/**\n * True if the supplied DOM node is a valid node element.\n *\n * @param {?DOMElement} node The candidate DOM node.\n * @return {boolean} True if the DOM is a valid DOM node.\n * @internal\n */\nfunction isValidContainer(node) {\n  return !!(node && (node.nodeType === ELEMENT_NODE || node.nodeType === DOCUMENT_NODE || node.nodeType === DOCUMENT_FRAGMENT_NODE || node.nodeType === COMMENT_NODE && node.nodeValue === ' react-mount-point-unstable '));\n}\n\nfunction getReactRootElementInContainer(container) {\n  if (!container) {\n    return null;\n  }\n\n  if (container.nodeType === DOCUMENT_NODE) {\n    return container.documentElement;\n  } else {\n    return container.firstChild;\n  }\n}\n\nfunction shouldHydrateDueToLegacyHeuristic(container) {\n  var rootElement = getReactRootElementInContainer(container);\n  return !!(rootElement && rootElement.nodeType === ELEMENT_NODE && rootElement.hasAttribute(ROOT_ATTRIBUTE_NAME));\n}\n\nsetBatchingImplementation(batchedUpdates$1, interactiveUpdates$1, flushInteractiveUpdates$1);\n\nvar warnedAboutHydrateAPI = false;\n\nfunction legacyCreateRootFromDOMContainer(container, forceHydrate) {\n  var shouldHydrate = forceHydrate || shouldHydrateDueToLegacyHeuristic(container);\n  // First clear any existing content.\n  if (!shouldHydrate) {\n    var warned = false;\n    var rootSibling = void 0;\n    while (rootSibling = container.lastChild) {\n      {\n        if (!warned && rootSibling.nodeType === ELEMENT_NODE && rootSibling.hasAttribute(ROOT_ATTRIBUTE_NAME)) {\n          warned = true;\n          warningWithoutStack$1(false, 'render(): Target node has markup rendered by React, but there ' + 'are unrelated nodes as well. This is most commonly caused by ' + 'white-space inserted around server-rendered markup.');\n        }\n      }\n      container.removeChild(rootSibling);\n    }\n  }\n  {\n    if (shouldHydrate && !forceHydrate && !warnedAboutHydrateAPI) {\n      warnedAboutHydrateAPI = true;\n      lowPriorityWarning$1(false, 'render(): Calling ReactDOM.render() to hydrate server-rendered markup ' + 'will stop working in React v17. Replace the ReactDOM.render() call ' + 'with ReactDOM.hydrate() if you want React to attach to the server HTML.');\n    }\n  }\n  // Legacy roots are not async by default.\n  var isConcurrent = false;\n  return new ReactRoot(container, isConcurrent, shouldHydrate);\n}\n\nfunction legacyRenderSubtreeIntoContainer(parentComponent, children, container, forceHydrate, callback) {\n  {\n    topLevelUpdateWarnings(container);\n  }\n\n  // TODO: Without `any` type, Flow says \"Property cannot be accessed on any\n  // member of intersection type.\" Whyyyyyy.\n  var root = container._reactRootContainer;\n  if (!root) {\n    // Initial mount\n    root = container._reactRootContainer = legacyCreateRootFromDOMContainer(container, forceHydrate);\n    if (typeof callback === 'function') {\n      var originalCallback = callback;\n      callback = function () {\n        var instance = getPublicRootInstance(root._internalRoot);\n        originalCallback.call(instance);\n      };\n    }\n    // Initial mount should not be batched.\n    unbatchedUpdates(function () {\n      if (parentComponent != null) {\n        root.legacy_renderSubtreeIntoContainer(parentComponent, children, callback);\n      } else {\n        root.render(children, callback);\n      }\n    });\n  } else {\n    if (typeof callback === 'function') {\n      var _originalCallback = callback;\n      callback = function () {\n        var instance = getPublicRootInstance(root._internalRoot);\n        _originalCallback.call(instance);\n      };\n    }\n    // Update\n    if (parentComponent != null) {\n      root.legacy_renderSubtreeIntoContainer(parentComponent, children, callback);\n    } else {\n      root.render(children, callback);\n    }\n  }\n  return getPublicRootInstance(root._internalRoot);\n}\n\nfunction createPortal$$1(children, container) {\n  var key = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;\n\n  !isValidContainer(container) ? invariant(false, 'Target container is not a DOM element.') : void 0;\n  // TODO: pass ReactDOM portal implementation as third argument\n  return createPortal$1(children, container, null, key);\n}\n\nvar ReactDOM = {\n  createPortal: createPortal$$1,\n\n  findDOMNode: function (componentOrElement) {\n    {\n      var owner = ReactCurrentOwner.current;\n      if (owner !== null && owner.stateNode !== null) {\n        var warnedAboutRefsInRender = owner.stateNode._warnedAboutRefsInRender;\n        !warnedAboutRefsInRender ? warningWithoutStack$1(false, '%s is accessing findDOMNode inside its render(). ' + 'render() should be a pure function of props and state. It should ' + 'never access something that requires stale data from the previous ' + 'render, such as refs. Move this logic to componentDidMount and ' + 'componentDidUpdate instead.', getComponentName(owner.type) || 'A component') : void 0;\n        owner.stateNode._warnedAboutRefsInRender = true;\n      }\n    }\n    if (componentOrElement == null) {\n      return null;\n    }\n    if (componentOrElement.nodeType === ELEMENT_NODE) {\n      return componentOrElement;\n    }\n    {\n      return findHostInstanceWithWarning(componentOrElement, 'findDOMNode');\n    }\n    return findHostInstance(componentOrElement);\n  },\n  hydrate: function (element, container, callback) {\n    !isValidContainer(container) ? invariant(false, 'Target container is not a DOM element.') : void 0;\n    {\n      !!container._reactHasBeenPassedToCreateRootDEV ? warningWithoutStack$1(false, 'You are calling ReactDOM.hydrate() on a container that was previously ' + 'passed to ReactDOM.%s(). This is not supported. ' + 'Did you mean to call createRoot(container, {hydrate: true}).render(element)?', enableStableConcurrentModeAPIs ? 'createRoot' : 'unstable_createRoot') : void 0;\n    }\n    // TODO: throw or warn if we couldn't hydrate?\n    return legacyRenderSubtreeIntoContainer(null, element, container, true, callback);\n  },\n  render: function (element, container, callback) {\n    !isValidContainer(container) ? invariant(false, 'Target container is not a DOM element.') : void 0;\n    {\n      !!container._reactHasBeenPassedToCreateRootDEV ? warningWithoutStack$1(false, 'You are calling ReactDOM.render() on a container that was previously ' + 'passed to ReactDOM.%s(). This is not supported. ' + 'Did you mean to call root.render(element)?', enableStableConcurrentModeAPIs ? 'createRoot' : 'unstable_createRoot') : void 0;\n    }\n    return legacyRenderSubtreeIntoContainer(null, element, container, false, callback);\n  },\n  unstable_renderSubtreeIntoContainer: function (parentComponent, element, containerNode, callback) {\n    !isValidContainer(containerNode) ? invariant(false, 'Target container is not a DOM element.') : void 0;\n    !(parentComponent != null && has(parentComponent)) ? invariant(false, 'parentComponent must be a valid React Component') : void 0;\n    return legacyRenderSubtreeIntoContainer(parentComponent, element, containerNode, false, callback);\n  },\n  unmountComponentAtNode: function (container) {\n    !isValidContainer(container) ? invariant(false, 'unmountComponentAtNode(...): Target container is not a DOM element.') : void 0;\n\n    {\n      !!container._reactHasBeenPassedToCreateRootDEV ? warningWithoutStack$1(false, 'You are calling ReactDOM.unmountComponentAtNode() on a container that was previously ' + 'passed to ReactDOM.%s(). This is not supported. Did you mean to call root.unmount()?', enableStableConcurrentModeAPIs ? 'createRoot' : 'unstable_createRoot') : void 0;\n    }\n\n    if (container._reactRootContainer) {\n      {\n        var rootEl = getReactRootElementInContainer(container);\n        var renderedByDifferentReact = rootEl && !getInstanceFromNode$1(rootEl);\n        !!renderedByDifferentReact ? warningWithoutStack$1(false, \"unmountComponentAtNode(): The node you're attempting to unmount \" + 'was rendered by another copy of React.') : void 0;\n      }\n\n      // Unmount should not be batched.\n      unbatchedUpdates(function () {\n        legacyRenderSubtreeIntoContainer(null, null, container, false, function () {\n          container._reactRootContainer = null;\n        });\n      });\n      // If you call unmountComponentAtNode twice in quick succession, you'll\n      // get `true` twice. That's probably fine?\n      return true;\n    } else {\n      {\n        var _rootEl = getReactRootElementInContainer(container);\n        var hasNonRootReactChild = !!(_rootEl && getInstanceFromNode$1(_rootEl));\n\n        // Check if the container itself is a React root node.\n        var isContainerReactRoot = container.nodeType === ELEMENT_NODE && isValidContainer(container.parentNode) && !!container.parentNode._reactRootContainer;\n\n        !!hasNonRootReactChild ? warningWithoutStack$1(false, \"unmountComponentAtNode(): The node you're attempting to unmount \" + 'was rendered by React and is not a top-level container. %s', isContainerReactRoot ? 'You may have accidentally passed in a React root node instead ' + 'of its container.' : 'Instead, have the parent component update its state and ' + 'rerender in order to remove this component.') : void 0;\n      }\n\n      return false;\n    }\n  },\n\n\n  // Temporary alias since we already shipped React 16 RC with it.\n  // TODO: remove in React 17.\n  unstable_createPortal: function () {\n    if (!didWarnAboutUnstableCreatePortal) {\n      didWarnAboutUnstableCreatePortal = true;\n      lowPriorityWarning$1(false, 'The ReactDOM.unstable_createPortal() alias has been deprecated, ' + 'and will be removed in React 17+. Update your code to use ' + 'ReactDOM.createPortal() instead. It has the exact same API, ' + 'but without the \"unstable_\" prefix.');\n    }\n    return createPortal$$1.apply(undefined, arguments);\n  },\n\n\n  unstable_batchedUpdates: batchedUpdates$1,\n\n  unstable_interactiveUpdates: interactiveUpdates$1,\n\n  flushSync: flushSync,\n\n  unstable_createRoot: createRoot,\n  unstable_flushControlled: flushControlled,\n\n  __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: {\n    // Keep in sync with ReactDOMUnstableNativeDependencies.js\n    // and ReactTestUtils.js. This is an array for better minification.\n    Events: [getInstanceFromNode$1, getNodeFromInstance$1, getFiberCurrentPropsFromNode$1, injection.injectEventPluginsByName, eventNameDispatchConfigs, accumulateTwoPhaseDispatches, accumulateDirectDispatches, enqueueStateRestore, restoreStateIfNeeded, dispatchEvent, runEventsInBatch]\n  }\n};\n\nfunction createRoot(container, options) {\n  var functionName = enableStableConcurrentModeAPIs ? 'createRoot' : 'unstable_createRoot';\n  !isValidContainer(container) ? invariant(false, '%s(...): Target container is not a DOM element.', functionName) : void 0;\n  {\n    !!container._reactRootContainer ? warningWithoutStack$1(false, 'You are calling ReactDOM.%s() on a container that was previously ' + 'passed to ReactDOM.render(). This is not supported.', enableStableConcurrentModeAPIs ? 'createRoot' : 'unstable_createRoot') : void 0;\n    container._reactHasBeenPassedToCreateRootDEV = true;\n  }\n  var hydrate = options != null && options.hydrate === true;\n  return new ReactRoot(container, true, hydrate);\n}\n\nif (enableStableConcurrentModeAPIs) {\n  ReactDOM.createRoot = createRoot;\n  ReactDOM.unstable_createRoot = undefined;\n}\n\nvar foundDevTools = injectIntoDevTools({\n  findFiberByHostInstance: getClosestInstanceFromNode,\n  bundleType: 1,\n  version: ReactVersion,\n  rendererPackageName: 'react-dom'\n});\n\n{\n  if (!foundDevTools && canUseDOM && window.top === window.self) {\n    // If we're in Chrome or Firefox, provide a download link if not installed.\n    if (navigator.userAgent.indexOf('Chrome') > -1 && navigator.userAgent.indexOf('Edge') === -1 || navigator.userAgent.indexOf('Firefox') > -1) {\n      var protocol = window.location.protocol;\n      // Don't warn in exotic cases like chrome-extension://.\n      if (/^(https?|file):$/.test(protocol)) {\n        console.info('%cDownload the React DevTools ' + 'for a better development experience: ' + 'https://fb.me/react-devtools' + (protocol === 'file:' ? '\\nYou might need to use a local HTTP server (instead of file://): ' + 'https://fb.me/react-devtools-faq' : ''), 'font-weight:bold');\n      }\n    }\n  }\n}\n\n\n\nvar ReactDOM$2 = Object.freeze({\n\tdefault: ReactDOM\n});\n\nvar ReactDOM$3 = ( ReactDOM$2 && ReactDOM ) || ReactDOM$2;\n\n// TODO: decide on the top-level export form.\n// This is hacky but makes it work with both Rollup and Jest.\nvar reactDom = ReactDOM$3.default || ReactDOM$3;\n\nreturn reactDom;\n\n})));\n"
  },
  {
    "path": "vendor/react-dom.js",
    "content": "/** @license React v16.8.6\n * react-dom.production.min.js\n *\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n/*\n Modernizr 3.0.0pre (Custom Build) | MIT\n*/\n'use strict';(function(da,pb){\"object\"===typeof exports&&\"undefined\"!==typeof module?module.exports=pb(require(\"react\")):\"function\"===typeof define&&define.amd?define([\"react\"],pb):da.ReactDOM=pb(da.React)})(this,function(da){function pb(a,b,c,d,e,f,g,h){if(!a){a=void 0;if(void 0===b)a=Error(\"Minified exception occurred; use the non-minified dev environment for the full error message and additional helpful warnings.\");else{var l=[c,d,e,f,g,h],k=0;a=Error(b.replace(/%s/g,function(){return l[k++]}));\na.name=\"Invariant Violation\"}a.framesToPop=1;throw a;}}function n(a){for(var b=arguments.length-1,c=\"https://reactjs.org/docs/error-decoder.html?invariant=\"+a,d=0;d<b;d++)c+=\"&args[]=\"+encodeURIComponent(arguments[d+1]);pb(!1,\"Minified React error #\"+a+\"; visit %s for the full message or use the non-minified dev environment for full errors and additional helpful warnings. \",c)}function rh(a,b,c,d,e,f,g,h,l){qb=!1;$b=null;sh.apply(th,arguments)}function uh(a,b,c,d,e,f,g,h,l){rh.apply(this,arguments);\nif(qb){if(qb){var k=$b;qb=!1;$b=null}else n(\"198\"),k=void 0;ac||(ac=!0,Yc=k)}}function Ee(){if(bc)for(var a in Na){var b=Na[a],c=bc.indexOf(a);-1<c?void 0:n(\"96\",a);if(!cc[c]){b.extractEvents?void 0:n(\"97\",a);cc[c]=b;c=b.eventTypes;for(var d in c){var e=void 0;var f=c[d],g=b,h=d;Zc.hasOwnProperty(h)?n(\"99\",h):void 0;Zc[h]=f;var l=f.phasedRegistrationNames;if(l){for(e in l)l.hasOwnProperty(e)&&Fe(l[e],g,h);e=!0}else f.registrationName?(Fe(f.registrationName,g,h),e=!0):e=!1;e?void 0:n(\"98\",d,a)}}}}\nfunction Fe(a,b,c){Oa[a]?n(\"100\",a):void 0;Oa[a]=b;$c[a]=b.eventTypes[c].dependencies}function Ge(a,b,c){var d=a.type||\"unknown-event\";a.currentTarget=He(c);uh(d,b,void 0,a);a.currentTarget=null}function Pa(a,b){null==b?n(\"30\"):void 0;if(null==a)return b;if(Array.isArray(a)){if(Array.isArray(b))return a.push.apply(a,b),a;a.push(b);return a}return Array.isArray(b)?[a].concat(b):[a,b]}function ad(a,b,c){Array.isArray(a)?a.forEach(b,c):a&&b.call(c,a)}function Ie(a,b){var c=a.stateNode;if(!c)return null;\nvar d=bd(c);if(!d)return null;c=d[b];a:switch(b){case \"onClick\":case \"onClickCapture\":case \"onDoubleClick\":case \"onDoubleClickCapture\":case \"onMouseDown\":case \"onMouseDownCapture\":case \"onMouseMove\":case \"onMouseMoveCapture\":case \"onMouseUp\":case \"onMouseUpCapture\":(d=!d.disabled)||(a=a.type,d=!(\"button\"===a||\"input\"===a||\"select\"===a||\"textarea\"===a));a=!d;break a;default:a=!1}if(a)return null;c&&\"function\"!==typeof c?n(\"231\",b,typeof c):void 0;return c}function cd(a){null!==a&&(rb=Pa(rb,a));a=rb;\nrb=null;if(a&&(ad(a,vh),rb?n(\"95\"):void 0,ac))throw a=Yc,ac=!1,Yc=null,a;}function dc(a){if(a[ea])return a[ea];for(;!a[ea];)if(a.parentNode)a=a.parentNode;else return null;a=a[ea];return 5===a.tag||6===a.tag?a:null}function Je(a){a=a[ea];return!a||5!==a.tag&&6!==a.tag?null:a}function Da(a){if(5===a.tag||6===a.tag)return a.stateNode;n(\"33\")}function dd(a){return a[ec]||null}function fa(a){do a=a.return;while(a&&5!==a.tag);return a?a:null}function Ke(a,b,c){if(b=Ie(a,c.dispatchConfig.phasedRegistrationNames[b]))c._dispatchListeners=\nPa(c._dispatchListeners,b),c._dispatchInstances=Pa(c._dispatchInstances,a)}function wh(a){if(a&&a.dispatchConfig.phasedRegistrationNames){for(var b=a._targetInst,c=[];b;)c.push(b),b=fa(b);for(b=c.length;0<b--;)Ke(c[b],\"captured\",a);for(b=0;b<c.length;b++)Ke(c[b],\"bubbled\",a)}}function ed(a,b,c){a&&c&&c.dispatchConfig.registrationName&&(b=Ie(a,c.dispatchConfig.registrationName))&&(c._dispatchListeners=Pa(c._dispatchListeners,b),c._dispatchInstances=Pa(c._dispatchInstances,a))}function xh(a){a&&a.dispatchConfig.registrationName&&\ned(a._targetInst,null,a)}function Qa(a){ad(a,wh)}function fc(a,b){var c={};c[a.toLowerCase()]=b.toLowerCase();c[\"Webkit\"+a]=\"webkit\"+b;c[\"Moz\"+a]=\"moz\"+b;return c}function gc(a){if(fd[a])return fd[a];if(!Ra[a])return a;var b=Ra[a],c;for(c in b)if(b.hasOwnProperty(c)&&c in Le)return fd[a]=b[c];return a}function Me(){if(hc)return hc;var a,b=gd,c=b.length,d,e=\"value\"in qa?qa.value:qa.textContent,f=e.length;for(a=0;a<c&&b[a]===e[a];a++);var g=c-a;for(d=1;d<=g&&b[c-d]===e[f-d];d++);return hc=e.slice(a,\n1<d?1-d:void 0)}function ic(){return!0}function jc(){return!1}function J(a,b,c,d){this.dispatchConfig=a;this._targetInst=b;this.nativeEvent=c;a=this.constructor.Interface;for(var e in a)a.hasOwnProperty(e)&&((b=a[e])?this[e]=b(c):\"target\"===e?this.target=d:this[e]=c[e]);this.isDefaultPrevented=(null!=c.defaultPrevented?c.defaultPrevented:!1===c.returnValue)?ic:jc;this.isPropagationStopped=jc;return this}function yh(a,b,c,d){if(this.eventPool.length){var e=this.eventPool.pop();this.call(e,a,b,c,d);\nreturn e}return new this(a,b,c,d)}function zh(a){a instanceof this?void 0:n(\"279\");a.destructor();10>this.eventPool.length&&this.eventPool.push(a)}function Ne(a){a.eventPool=[];a.getPooled=yh;a.release=zh}function Oe(a,b){switch(a){case \"keyup\":return-1!==Ah.indexOf(b.keyCode);case \"keydown\":return 229!==b.keyCode;case \"keypress\":case \"mousedown\":case \"blur\":return!0;default:return!1}}function Pe(a){a=a.detail;return\"object\"===typeof a&&\"data\"in a?a.data:null}function Bh(a,b){switch(a){case \"compositionend\":return Pe(b);\ncase \"keypress\":if(32!==b.which)return null;Qe=!0;return Re;case \"textInput\":return a=b.data,a===Re&&Qe?null:a;default:return null}}function Ch(a,b){if(Sa)return\"compositionend\"===a||!hd&&Oe(a,b)?(a=Me(),hc=gd=qa=null,Sa=!1,a):null;switch(a){case \"paste\":return null;case \"keypress\":if(!(b.ctrlKey||b.altKey||b.metaKey)||b.ctrlKey&&b.altKey){if(b.char&&1<b.char.length)return b.char;if(b.which)return String.fromCharCode(b.which)}return null;case \"compositionend\":return Se&&\"ko\"!==b.locale?null:b.data;\ndefault:return null}}function Te(a){if(a=Ue(a)){\"function\"!==typeof id?n(\"280\"):void 0;var b=bd(a.stateNode);id(a.stateNode,a.type,b)}}function Ve(a){Ta?Ua?Ua.push(a):Ua=[a]:Ta=a}function We(){if(Ta){var a=Ta,b=Ua;Ua=Ta=null;Te(a);if(b)for(a=0;a<b.length;a++)Te(b[a])}}function Xe(a,b){if(jd)return a(b);jd=!0;try{return Ye(a,b)}finally{if(jd=!1,null!==Ta||null!==Ua)Ze(),We()}}function $e(a){var b=a&&a.nodeName&&a.nodeName.toLowerCase();return\"input\"===b?!!Dh[a.type]:\"textarea\"===b?!0:!1}function kd(a){a=\na.target||a.srcElement||window;a.correspondingUseElement&&(a=a.correspondingUseElement);return 3===a.nodeType?a.parentNode:a}function af(a){if(!ra)return!1;a=\"on\"+a;var b=a in document;b||(b=document.createElement(\"div\"),b.setAttribute(a,\"return;\"),b=\"function\"===typeof b[a]);return b}function bf(a){var b=a.type;return(a=a.nodeName)&&\"input\"===a.toLowerCase()&&(\"checkbox\"===b||\"radio\"===b)}function Eh(a){var b=bf(a)?\"checked\":\"value\",c=Object.getOwnPropertyDescriptor(a.constructor.prototype,b),d=\n\"\"+a[b];if(!a.hasOwnProperty(b)&&\"undefined\"!==typeof c&&\"function\"===typeof c.get&&\"function\"===typeof c.set){var e=c.get,f=c.set;Object.defineProperty(a,b,{configurable:!0,get:function(){return e.call(this)},set:function(a){d=\"\"+a;f.call(this,a)}});Object.defineProperty(a,b,{enumerable:c.enumerable});return{getValue:function(){return d},setValue:function(a){d=\"\"+a},stopTracking:function(){a._valueTracker=null;delete a[b]}}}}function kc(a){a._valueTracker||(a._valueTracker=Eh(a))}function cf(a){if(!a)return!1;\nvar b=a._valueTracker;if(!b)return!0;var c=b.getValue();var d=\"\";a&&(d=bf(a)?a.checked?\"true\":\"false\":a.value);a=d;return a!==c?(b.setValue(a),!0):!1}function sb(a){if(null===a||\"object\"!==typeof a)return null;a=df&&a[df]||a[\"@@iterator\"];return\"function\"===typeof a?a:null}function sa(a){if(null==a)return null;if(\"function\"===typeof a)return a.displayName||a.name||null;if(\"string\"===typeof a)return a;switch(a){case ld:return\"ConcurrentMode\";case ta:return\"Fragment\";case Va:return\"Portal\";case lc:return\"Profiler\";\ncase md:return\"StrictMode\";case nd:return\"Suspense\"}if(\"object\"===typeof a)switch(a.$$typeof){case ef:return\"Context.Consumer\";case ff:return\"Context.Provider\";case od:var b=a.render;b=b.displayName||b.name||\"\";return a.displayName||(\"\"!==b?\"ForwardRef(\"+b+\")\":\"ForwardRef\");case pd:return sa(a.type);case gf:if(a=1===a._status?a._result:null)return sa(a)}return null}function qd(a){var b=\"\";do{a:switch(a.tag){case 3:case 4:case 6:case 7:case 10:case 9:var c=\"\";break a;default:var d=a._debugOwner,e=\na._debugSource,f=sa(a.type);c=null;d&&(c=sa(d.type));d=f;f=\"\";e?f=\" (at \"+e.fileName.replace(Fh,\"\")+\":\"+e.lineNumber+\")\":c&&(f=\" (created by \"+c+\")\");c=\"\\n    in \"+(d||\"Unknown\")+f}b+=c;a=a.return}while(a);return b}function Gh(a){if(hf.call(jf,a))return!0;if(hf.call(kf,a))return!1;if(Hh.test(a))return jf[a]=!0;kf[a]=!0;return!1}function Ih(a,b,c,d){if(null!==c&&0===c.type)return!1;switch(typeof b){case \"function\":case \"symbol\":return!0;case \"boolean\":if(d)return!1;if(null!==c)return!c.acceptsBooleans;\na=a.toLowerCase().slice(0,5);return\"data-\"!==a&&\"aria-\"!==a;default:return!1}}function Jh(a,b,c,d){if(null===b||\"undefined\"===typeof b||Ih(a,b,c,d))return!0;if(d)return!1;if(null!==c)switch(c.type){case 3:return!b;case 4:return!1===b;case 5:return isNaN(b);case 6:return isNaN(b)||1>b}return!1}function K(a,b,c,d,e){this.acceptsBooleans=2===b||3===b||4===b;this.attributeName=d;this.attributeNamespace=e;this.mustUseProperty=c;this.propertyName=a;this.type=b}function rd(a,b,c,d){var e=A.hasOwnProperty(b)?\nA[b]:null;var f=null!==e?0===e.type:d?!1:!(2<b.length)||\"o\"!==b[0]&&\"O\"!==b[0]||\"n\"!==b[1]&&\"N\"!==b[1]?!1:!0;f||(Jh(b,c,e,d)&&(c=null),d||null===e?Gh(b)&&(null===c?a.removeAttribute(b):a.setAttribute(b,\"\"+c)):e.mustUseProperty?a[e.propertyName]=null===c?3===e.type?!1:\"\":c:(b=e.attributeName,d=e.attributeNamespace,null===c?a.removeAttribute(b):(e=e.type,c=3===e||4===e&&!0===c?\"\":\"\"+c,d?a.setAttributeNS(d,b,c):a.setAttribute(b,c))))}function ua(a){switch(typeof a){case \"boolean\":case \"number\":case \"object\":case \"string\":case \"undefined\":return a;\ndefault:return\"\"}}function sd(a,b){var c=b.checked;return B({},b,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:null!=c?c:a._wrapperState.initialChecked})}function lf(a,b){var c=null==b.defaultValue?\"\":b.defaultValue,d=null!=b.checked?b.checked:b.defaultChecked;c=ua(null!=b.value?b.value:c);a._wrapperState={initialChecked:d,initialValue:c,controlled:\"checkbox\"===b.type||\"radio\"===b.type?null!=b.checked:null!=b.value}}function mf(a,b){b=b.checked;null!=b&&rd(a,\"checked\",b,!1)}function td(a,\nb){mf(a,b);var c=ua(b.value),d=b.type;if(null!=c)if(\"number\"===d){if(0===c&&\"\"===a.value||a.value!=c)a.value=\"\"+c}else a.value!==\"\"+c&&(a.value=\"\"+c);else if(\"submit\"===d||\"reset\"===d){a.removeAttribute(\"value\");return}b.hasOwnProperty(\"value\")?ud(a,b.type,c):b.hasOwnProperty(\"defaultValue\")&&ud(a,b.type,ua(b.defaultValue));null==b.checked&&null!=b.defaultChecked&&(a.defaultChecked=!!b.defaultChecked)}function nf(a,b,c){if(b.hasOwnProperty(\"value\")||b.hasOwnProperty(\"defaultValue\")){var d=b.type;\nif(!(\"submit\"!==d&&\"reset\"!==d||void 0!==b.value&&null!==b.value))return;b=\"\"+a._wrapperState.initialValue;c||b===a.value||(a.value=b);a.defaultValue=b}c=a.name;\"\"!==c&&(a.name=\"\");a.defaultChecked=!a.defaultChecked;a.defaultChecked=!!a._wrapperState.initialChecked;\"\"!==c&&(a.name=c)}function ud(a,b,c){if(\"number\"!==b||a.ownerDocument.activeElement!==a)null==c?a.defaultValue=\"\"+a._wrapperState.initialValue:a.defaultValue!==\"\"+c&&(a.defaultValue=\"\"+c)}function of(a,b,c){a=J.getPooled(pf.change,a,b,\nc);a.type=\"change\";Ve(c);Qa(a);return a}function Kh(a){cd(a)}function mc(a){var b=Da(a);if(cf(b))return a}function Lh(a,b){if(\"change\"===a)return b}function qf(){tb&&(tb.detachEvent(\"onpropertychange\",rf),ub=tb=null)}function rf(a){\"value\"===a.propertyName&&mc(ub)&&(a=of(ub,a,kd(a)),Xe(Kh,a))}function Mh(a,b,c){\"focus\"===a?(qf(),tb=b,ub=c,tb.attachEvent(\"onpropertychange\",rf)):\"blur\"===a&&qf()}function Nh(a,b){if(\"selectionchange\"===a||\"keyup\"===a||\"keydown\"===a)return mc(ub)}function Oh(a,b){if(\"click\"===\na)return mc(b)}function Ph(a,b){if(\"input\"===a||\"change\"===a)return mc(b)}function Qh(a){var b=this.nativeEvent;return b.getModifierState?b.getModifierState(a):(a=Rh[a])?!!b[a]:!1}function vd(a){return Qh}function Ea(a,b){return a===b&&(0!==a||1/a===1/b)||a!==a&&b!==b}function vb(a,b){if(Ea(a,b))return!0;if(\"object\"!==typeof a||null===a||\"object\"!==typeof b||null===b)return!1;var c=Object.keys(a),d=Object.keys(b);if(c.length!==d.length)return!1;for(d=0;d<c.length;d++)if(!Sh.call(b,c[d])||!Ea(a[c[d]],\nb[c[d]]))return!1;return!0}function wb(a){var b=a;if(a.alternate)for(;b.return;)b=b.return;else{if(0!==(b.effectTag&2))return 1;for(;b.return;)if(b=b.return,0!==(b.effectTag&2))return 1}return 3===b.tag?2:3}function sf(a){2!==wb(a)?n(\"188\"):void 0}function Th(a){var b=a.alternate;if(!b)return b=wb(a),3===b?n(\"188\"):void 0,1===b?null:a;for(var c=a,d=b;;){var e=c.return,f=e?e.alternate:null;if(!e||!f)break;if(e.child===f.child){for(var g=e.child;g;){if(g===c)return sf(e),a;if(g===d)return sf(e),b;g=\ng.sibling}n(\"188\")}if(c.return!==d.return)c=e,d=f;else{g=!1;for(var h=e.child;h;){if(h===c){g=!0;c=e;d=f;break}if(h===d){g=!0;d=e;c=f;break}h=h.sibling}if(!g){for(h=f.child;h;){if(h===c){g=!0;c=f;d=e;break}if(h===d){g=!0;d=f;c=e;break}h=h.sibling}g?void 0:n(\"189\")}}c.alternate!==d?n(\"190\"):void 0}3!==c.tag?n(\"188\"):void 0;return c.stateNode.current===c?a:b}function tf(a){a=Th(a);if(!a)return null;for(var b=a;;){if(5===b.tag||6===b.tag)return b;if(b.child)b.child.return=b,b=b.child;else{if(b===a)break;\nfor(;!b.sibling;){if(!b.return||b.return===a)return null;b=b.return}b.sibling.return=b.return;b=b.sibling}}return null}function nc(a){var b=a.keyCode;\"charCode\"in a?(a=a.charCode,0===a&&13===b&&(a=13)):a=b;10===a&&(a=13);return 32<=a||13===a?a:0}function uf(a,b){var c=a[0];a=a[1];var d=\"on\"+(a[0].toUpperCase()+a.slice(1));b={phasedRegistrationNames:{bubbled:d,captured:d+\"Capture\"},dependencies:[c],isInteractive:b};vf[a]=b;wd[c]=b}function Uh(a){var b=a.targetInst,c=b;do{if(!c){a.ancestors.push(c);\nbreak}var d;for(d=c;d.return;)d=d.return;d=3!==d.tag?null:d.stateNode.containerInfo;if(!d)break;a.ancestors.push(c);c=dc(d)}while(c);for(c=0;c<a.ancestors.length;c++){b=a.ancestors[c];var e=kd(a.nativeEvent);d=a.topLevelType;for(var f=a.nativeEvent,g=null,h=0;h<cc.length;h++){var l=cc[h];l&&(l=l.extractEvents(d,b,f,e))&&(g=Pa(g,l))}cd(g)}}function r(a,b){if(!b)return null;var c=(wf(a)?xf:oc).bind(null,a);b.addEventListener(a,c,!1)}function pc(a,b){if(!b)return null;var c=(wf(a)?xf:oc).bind(null,a);\nb.addEventListener(a,c,!0)}function xf(a,b){yf(oc,a,b)}function oc(a,b){if(qc){var c=kd(b);c=dc(c);null===c||\"number\"!==typeof c.tag||2===wb(c)||(c=null);if(rc.length){var d=rc.pop();d.topLevelType=a;d.nativeEvent=b;d.targetInst=c;a=d}else a={topLevelType:a,nativeEvent:b,targetInst:c,ancestors:[]};try{Xe(Uh,a)}finally{a.topLevelType=null,a.nativeEvent=null,a.targetInst=null,a.ancestors.length=0,10>rc.length&&rc.push(a)}}}function zf(a){Object.prototype.hasOwnProperty.call(a,sc)||(a[sc]=Vh++,Af[a[sc]]=\n{});return Af[a[sc]]}function xd(a){a=a||(\"undefined\"!==typeof document?document:void 0);if(\"undefined\"===typeof a)return null;try{return a.activeElement||a.body}catch(b){return a.body}}function Bf(a){for(;a&&a.firstChild;)a=a.firstChild;return a}function Cf(a,b){var c=Bf(a);a=0;for(var d;c;){if(3===c.nodeType){d=a+c.textContent.length;if(a<=b&&d>=b)return{node:c,offset:b-a};a=d}a:{for(;c;){if(c.nextSibling){c=c.nextSibling;break a}c=c.parentNode}c=void 0}c=Bf(c)}}function Df(a,b){return a&&b?a===\nb?!0:a&&3===a.nodeType?!1:b&&3===b.nodeType?Df(a,b.parentNode):\"contains\"in a?a.contains(b):a.compareDocumentPosition?!!(a.compareDocumentPosition(b)&16):!1:!1}function Ef(){for(var a=window,b=xd();b instanceof a.HTMLIFrameElement;){try{var c=\"string\"===typeof b.contentWindow.location.href}catch(d){c=!1}if(c)a=b.contentWindow;else break;b=xd(a.document)}return b}function yd(a){var b=a&&a.nodeName&&a.nodeName.toLowerCase();return b&&(\"input\"===b&&(\"text\"===a.type||\"search\"===a.type||\"tel\"===a.type||\n\"url\"===a.type||\"password\"===a.type)||\"textarea\"===b||\"true\"===a.contentEditable)}function Wh(){var a=Ef();if(yd(a)){if(\"selectionStart\"in a)var b={start:a.selectionStart,end:a.selectionEnd};else a:{b=(b=a.ownerDocument)&&b.defaultView||window;var c=b.getSelection&&b.getSelection();if(c&&0!==c.rangeCount){b=c.anchorNode;var d=c.anchorOffset,e=c.focusNode;c=c.focusOffset;try{b.nodeType,e.nodeType}catch(cj){b=null;break a}var f=0,g=-1,h=-1,l=0,k=0,m=a,n=null;b:for(;;){for(var p;;){m!==b||0!==d&&3!==\nm.nodeType||(g=f+d);m!==e||0!==c&&3!==m.nodeType||(h=f+c);3===m.nodeType&&(f+=m.nodeValue.length);if(null===(p=m.firstChild))break;n=m;m=p}for(;;){if(m===a)break b;n===b&&++l===d&&(g=f);n===e&&++k===c&&(h=f);if(null!==(p=m.nextSibling))break;m=n;n=m.parentNode}m=p}b=-1===g||-1===h?null:{start:g,end:h}}else b=null}b=b||{start:0,end:0}}else b=null;return{focusedElem:a,selectionRange:b}}function Xh(a){var b=Ef(),c=a.focusedElem,d=a.selectionRange;if(b!==c&&c&&c.ownerDocument&&Df(c.ownerDocument.documentElement,\nc)){if(null!==d&&yd(c))if(b=d.start,a=d.end,void 0===a&&(a=b),\"selectionStart\"in c)c.selectionStart=b,c.selectionEnd=Math.min(a,c.value.length);else if(a=(b=c.ownerDocument||document)&&b.defaultView||window,a.getSelection){a=a.getSelection();var e=c.textContent.length,f=Math.min(d.start,e);d=void 0===d.end?f:Math.min(d.end,e);!a.extend&&f>d&&(e=d,d=f,f=e);e=Cf(c,f);var g=Cf(c,d);e&&g&&(1!==a.rangeCount||a.anchorNode!==e.node||a.anchorOffset!==e.offset||a.focusNode!==g.node||a.focusOffset!==g.offset)&&\n(b=b.createRange(),b.setStart(e.node,e.offset),a.removeAllRanges(),f>d?(a.addRange(b),a.extend(g.node,g.offset)):(b.setEnd(g.node,g.offset),a.addRange(b)))}b=[];for(a=c;a=a.parentNode;)1===a.nodeType&&b.push({element:a,left:a.scrollLeft,top:a.scrollTop});\"function\"===typeof c.focus&&c.focus();for(c=0;c<b.length;c++)a=b[c],a.element.scrollLeft=a.left,a.element.scrollTop=a.top}}function Gf(a,b){var c=b.window===b?b.document:9===b.nodeType?b:b.ownerDocument;if(zd||null==Wa||Wa!==xd(c))return null;c=\nWa;\"selectionStart\"in c&&yd(c)?c={start:c.selectionStart,end:c.selectionEnd}:(c=(c.ownerDocument&&c.ownerDocument.defaultView||window).getSelection(),c={anchorNode:c.anchorNode,anchorOffset:c.anchorOffset,focusNode:c.focusNode,focusOffset:c.focusOffset});return xb&&vb(xb,c)?null:(xb=c,a=J.getPooled(Hf.select,Ad,a,b),a.type=\"select\",a.target=Wa,Qa(a),a)}function Yh(a){var b=\"\";da.Children.forEach(a,function(a){null!=a&&(b+=a)});return b}function Bd(a,b){a=B({children:void 0},b);if(b=Yh(b.children))a.children=\nb;return a}function Xa(a,b,c,d){a=a.options;if(b){b={};for(var e=0;e<c.length;e++)b[\"$\"+c[e]]=!0;for(c=0;c<a.length;c++)e=b.hasOwnProperty(\"$\"+a[c].value),a[c].selected!==e&&(a[c].selected=e),e&&d&&(a[c].defaultSelected=!0)}else{c=\"\"+ua(c);b=null;for(e=0;e<a.length;e++){if(a[e].value===c){a[e].selected=!0;d&&(a[e].defaultSelected=!0);return}null!==b||a[e].disabled||(b=a[e])}null!==b&&(b.selected=!0)}}function Cd(a,b){null!=b.dangerouslySetInnerHTML?n(\"91\"):void 0;return B({},b,{value:void 0,defaultValue:void 0,\nchildren:\"\"+a._wrapperState.initialValue})}function If(a,b){var c=b.value;null==c&&(c=b.defaultValue,b=b.children,null!=b&&(null!=c?n(\"92\"):void 0,Array.isArray(b)&&(1>=b.length?void 0:n(\"93\"),b=b[0]),c=b),null==c&&(c=\"\"));a._wrapperState={initialValue:ua(c)}}function Jf(a,b){var c=ua(b.value),d=ua(b.defaultValue);null!=c&&(c=\"\"+c,c!==a.value&&(a.value=c),null==b.defaultValue&&a.defaultValue!==c&&(a.defaultValue=c));null!=d&&(a.defaultValue=\"\"+d)}function Kf(a){switch(a){case \"svg\":return\"http://www.w3.org/2000/svg\";\ncase \"math\":return\"http://www.w3.org/1998/Math/MathML\";default:return\"http://www.w3.org/1999/xhtml\"}}function Dd(a,b){return null==a||\"http://www.w3.org/1999/xhtml\"===a?Kf(b):\"http://www.w3.org/2000/svg\"===a&&\"foreignObject\"===b?\"http://www.w3.org/1999/xhtml\":a}function Lf(a,b,c){return null==b||\"boolean\"===typeof b||\"\"===b?\"\":c||\"number\"!==typeof b||0===b||yb.hasOwnProperty(a)&&yb[a]?(\"\"+b).trim():b+\"px\"}function Mf(a,b){a=a.style;for(var c in b)if(b.hasOwnProperty(c)){var d=0===c.indexOf(\"--\"),\ne=Lf(c,b[c],d);\"float\"===c&&(c=\"cssFloat\");d?a.setProperty(c,e):a[c]=e}}function Ed(a,b){b&&(Zh[a]&&(null!=b.children||null!=b.dangerouslySetInnerHTML?n(\"137\",a,\"\"):void 0),null!=b.dangerouslySetInnerHTML&&(null!=b.children?n(\"60\"):void 0,\"object\"===typeof b.dangerouslySetInnerHTML&&\"__html\"in b.dangerouslySetInnerHTML?void 0:n(\"61\")),null!=b.style&&\"object\"!==typeof b.style?n(\"62\",\"\"):void 0)}function Fd(a,b){if(-1===a.indexOf(\"-\"))return\"string\"===typeof b.is;switch(a){case \"annotation-xml\":case \"color-profile\":case \"font-face\":case \"font-face-src\":case \"font-face-uri\":case \"font-face-format\":case \"font-face-name\":case \"missing-glyph\":return!1;\ndefault:return!0}}function ha(a,b){a=9===a.nodeType||11===a.nodeType?a:a.ownerDocument;var c=zf(a);b=$c[b];for(var d=0;d<b.length;d++){var e=b[d];if(!c.hasOwnProperty(e)||!c[e]){switch(e){case \"scroll\":pc(\"scroll\",a);break;case \"focus\":case \"blur\":pc(\"focus\",a);pc(\"blur\",a);c.blur=!0;c.focus=!0;break;case \"cancel\":case \"close\":af(e)&&pc(e,a);break;case \"invalid\":case \"submit\":case \"reset\":break;default:-1===zb.indexOf(e)&&r(e,a)}c[e]=!0}}}function tc(){}function Nf(a,b){switch(a){case \"button\":case \"input\":case \"select\":case \"textarea\":return!!b.autoFocus}return!1}\nfunction Gd(a,b){return\"textarea\"===a||\"option\"===a||\"noscript\"===a||\"string\"===typeof b.children||\"number\"===typeof b.children||\"object\"===typeof b.dangerouslySetInnerHTML&&null!==b.dangerouslySetInnerHTML&&null!=b.dangerouslySetInnerHTML.__html}function $h(a,b,c,d,e,f){a[ec]=e;\"input\"===c&&\"radio\"===e.type&&null!=e.name&&mf(a,e);Fd(c,d);d=Fd(c,e);for(f=0;f<b.length;f+=2){var g=b[f],h=b[f+1];\"style\"===g?Mf(a,h):\"dangerouslySetInnerHTML\"===g?Of(a,h):\"children\"===g?Ab(a,h):rd(a,g,h,d)}switch(c){case \"input\":td(a,\ne);break;case \"textarea\":Jf(a,e);break;case \"select\":b=a._wrapperState.wasMultiple,a._wrapperState.wasMultiple=!!e.multiple,c=e.value,null!=c?Xa(a,!!e.multiple,c,!1):b!==!!e.multiple&&(null!=e.defaultValue?Xa(a,!!e.multiple,e.defaultValue,!0):Xa(a,!!e.multiple,e.multiple?[]:\"\",!1))}}function Hd(a){for(a=a.nextSibling;a&&1!==a.nodeType&&3!==a.nodeType;)a=a.nextSibling;return a}function Pf(a){for(a=a.firstChild;a&&1!==a.nodeType&&3!==a.nodeType;)a=a.nextSibling;return a}function D(a,b){0>Ya||(a.current=\nId[Ya],Id[Ya]=null,Ya--)}function L(a,b,c){Ya++;Id[Ya]=a.current;a.current=b}function Za(a,b){var c=a.type.contextTypes;if(!c)return va;var d=a.stateNode;if(d&&d.__reactInternalMemoizedUnmaskedChildContext===b)return d.__reactInternalMemoizedMaskedChildContext;var e={},f;for(f in c)e[f]=b[f];d&&(a=a.stateNode,a.__reactInternalMemoizedUnmaskedChildContext=b,a.__reactInternalMemoizedMaskedChildContext=e);return e}function E(a){a=a.childContextTypes;return null!==a&&void 0!==a}function uc(a){D(M,a);\nD(F,a)}function Jd(a){D(M,a);D(F,a)}function Qf(a,b,c){F.current!==va?n(\"168\"):void 0;L(F,b,a);L(M,c,a)}function Rf(a,b,c){var d=a.stateNode;a=b.childContextTypes;if(\"function\"!==typeof d.getChildContext)return c;d=d.getChildContext();for(var e in d)e in a?void 0:n(\"108\",sa(b)||\"Unknown\",e);return B({},c,d)}function vc(a){var b=a.stateNode;b=b&&b.__reactInternalMemoizedMergedChildContext||va;Fa=F.current;L(F,b,a);L(M,M.current,a);return!0}function Sf(a,b,c){var d=a.stateNode;d?void 0:n(\"169\");c?(b=\nRf(a,b,Fa),d.__reactInternalMemoizedMergedChildContext=b,D(M,a),D(F,a),L(F,b,a)):D(M,a);L(M,c,a)}function Tf(a){return function(b){try{return a(b)}catch(c){}}}function ai(a){if(\"undefined\"===typeof __REACT_DEVTOOLS_GLOBAL_HOOK__)return!1;var b=__REACT_DEVTOOLS_GLOBAL_HOOK__;if(b.isDisabled||!b.supportsFiber)return!0;try{var c=b.inject(a);Kd=Tf(function(a){return b.onCommitFiberRoot(c,a)});Ld=Tf(function(a){return b.onCommitFiberUnmount(c,a)})}catch(d){}return!0}function bi(a,b,c,d){this.tag=a;this.key=\nc;this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null;this.index=0;this.ref=null;this.pendingProps=b;this.contextDependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null;this.mode=d;this.effectTag=0;this.lastEffect=this.firstEffect=this.nextEffect=null;this.childExpirationTime=this.expirationTime=0;this.alternate=null}function Md(a){a=a.prototype;return!(!a||!a.isReactComponent)}function ci(a){if(\"function\"===typeof a)return Md(a)?1:0;if(void 0!==a&&\nnull!==a){a=a.$$typeof;if(a===od)return 11;if(a===pd)return 14}return 2}function Ga(a,b,c){c=a.alternate;null===c?(c=S(a.tag,b,a.key,a.mode),c.elementType=a.elementType,c.type=a.type,c.stateNode=a.stateNode,c.alternate=a,a.alternate=c):(c.pendingProps=b,c.effectTag=0,c.nextEffect=null,c.firstEffect=null,c.lastEffect=null);c.childExpirationTime=a.childExpirationTime;c.expirationTime=a.expirationTime;c.child=a.child;c.memoizedProps=a.memoizedProps;c.memoizedState=a.memoizedState;c.updateQueue=a.updateQueue;\nc.contextDependencies=a.contextDependencies;c.sibling=a.sibling;c.index=a.index;c.ref=a.ref;return c}function wc(a,b,c,d,e,f){var g=2;d=a;if(\"function\"===typeof a)Md(a)&&(g=1);else if(\"string\"===typeof a)g=5;else a:switch(a){case ta:return wa(c.children,e,f,b);case ld:return Uf(c,e|3,f,b);case md:return Uf(c,e|2,f,b);case lc:return a=S(12,c,b,e|4),a.elementType=lc,a.type=lc,a.expirationTime=f,a;case nd:return a=S(13,c,b,e),b=nd,a.elementType=b,a.type=b,a.expirationTime=f,a;default:if(\"object\"===typeof a&&\nnull!==a)switch(a.$$typeof){case ff:g=10;break a;case ef:g=9;break a;case od:g=11;break a;case pd:g=14;break a;case gf:g=16;d=null;break a}n(\"130\",null==a?a:typeof a,\"\")}b=S(g,c,b,e);b.elementType=a;b.type=d;b.expirationTime=f;return b}function wa(a,b,c,d){a=S(7,a,d,b);a.expirationTime=c;return a}function Uf(a,b,c,d){a=S(8,a,d,b);b=0===(b&1)?md:ld;a.elementType=b;a.type=b;a.expirationTime=c;return a}function Nd(a,b,c){a=S(6,a,null,b);a.expirationTime=c;return a}function Od(a,b,c){b=S(4,null!==a.children?\na.children:[],a.key,b);b.expirationTime=c;b.stateNode={containerInfo:a.containerInfo,pendingChildren:null,implementation:a.implementation};return b}function Bb(a,b){a.didError=!1;var c=a.earliestPendingTime;0===c?a.earliestPendingTime=a.latestPendingTime=b:c<b?a.earliestPendingTime=b:a.latestPendingTime>b&&(a.latestPendingTime=b);xc(b,a)}function di(a,b){a.didError=!1;if(0===b)a.earliestPendingTime=0,a.latestPendingTime=0,a.earliestSuspendedTime=0,a.latestSuspendedTime=0,a.latestPingedTime=0;else{b<\na.latestPingedTime&&(a.latestPingedTime=0);var c=a.latestPendingTime;0!==c&&(c>b?a.earliestPendingTime=a.latestPendingTime=0:a.earliestPendingTime>b&&(a.earliestPendingTime=a.latestPendingTime));c=a.earliestSuspendedTime;0===c?Bb(a,b):b<a.latestSuspendedTime?(a.earliestSuspendedTime=0,a.latestSuspendedTime=0,a.latestPingedTime=0,Bb(a,b)):b>c&&Bb(a,b)}xc(0,a)}function Vf(a,b){a.didError=!1;a.latestPingedTime>=b&&(a.latestPingedTime=0);var c=a.earliestPendingTime,d=a.latestPendingTime;c===b?a.earliestPendingTime=\nd===b?a.latestPendingTime=0:d:d===b&&(a.latestPendingTime=c);c=a.earliestSuspendedTime;d=a.latestSuspendedTime;0===c?a.earliestSuspendedTime=a.latestSuspendedTime=b:c<b?a.earliestSuspendedTime=b:d>b&&(a.latestSuspendedTime=b);xc(b,a)}function Wf(a,b){var c=a.earliestPendingTime;a=a.earliestSuspendedTime;c>b&&(b=c);a>b&&(b=a);return b}function xc(a,b){var c=b.earliestSuspendedTime,d=b.latestSuspendedTime,e=b.earliestPendingTime,f=b.latestPingedTime;e=0!==e?e:f;0===e&&(0===a||d<a)&&(e=d);a=e;0!==a&&\nc>a&&(a=c);b.nextExpirationTimeToWorkOn=e;b.expirationTime=a}function P(a,b){if(a&&a.defaultProps){b=B({},b);a=a.defaultProps;for(var c in a)void 0===b[c]&&(b[c]=a[c])}return b}function ei(a){var b=a._result;switch(a._status){case 1:return b;case 2:throw b;case 0:throw b;default:a._status=0;b=a._ctor;b=b();b.then(function(b){0===a._status&&(b=b.default,a._status=1,a._result=b)},function(b){0===a._status&&(a._status=2,a._result=b)});switch(a._status){case 1:return a._result;case 2:throw a._result;\n}a._result=b;throw b;}}function yc(a,b,c,d){b=a.memoizedState;c=c(d,b);c=null===c||void 0===c?b:B({},b,c);a.memoizedState=c;d=a.updateQueue;null!==d&&0===a.expirationTime&&(d.baseState=c)}function Xf(a,b,c,d,e,f,g){a=a.stateNode;return\"function\"===typeof a.shouldComponentUpdate?a.shouldComponentUpdate(d,f,g):b.prototype&&b.prototype.isPureReactComponent?!vb(c,d)||!vb(e,f):!0}function Yf(a,b,c,d){var e=!1;d=va;var f=b.contextType;\"object\"===typeof f&&null!==f?f=T(f):(d=E(b)?Fa:F.current,e=b.contextTypes,\nf=(e=null!==e&&void 0!==e)?Za(a,d):va);b=new b(c,f);a.memoizedState=null!==b.state&&void 0!==b.state?b.state:null;b.updater=zc;a.stateNode=b;b._reactInternalFiber=a;e&&(a=a.stateNode,a.__reactInternalMemoizedUnmaskedChildContext=d,a.__reactInternalMemoizedMaskedChildContext=f);return b}function Zf(a,b,c,d){a=b.state;\"function\"===typeof b.componentWillReceiveProps&&b.componentWillReceiveProps(c,d);\"function\"===typeof b.UNSAFE_componentWillReceiveProps&&b.UNSAFE_componentWillReceiveProps(c,d);b.state!==\na&&zc.enqueueReplaceState(b,b.state,null)}function Pd(a,b,c,d){var e=a.stateNode;e.props=c;e.state=a.memoizedState;e.refs=$f;var f=b.contextType;\"object\"===typeof f&&null!==f?e.context=T(f):(f=E(b)?Fa:F.current,e.context=Za(a,f));f=a.updateQueue;null!==f&&(Cb(a,f,c,e,d),e.state=a.memoizedState);f=b.getDerivedStateFromProps;\"function\"===typeof f&&(yc(a,b,f,c),e.state=a.memoizedState);\"function\"===typeof b.getDerivedStateFromProps||\"function\"===typeof e.getSnapshotBeforeUpdate||\"function\"!==typeof e.UNSAFE_componentWillMount&&\n\"function\"!==typeof e.componentWillMount||(b=e.state,\"function\"===typeof e.componentWillMount&&e.componentWillMount(),\"function\"===typeof e.UNSAFE_componentWillMount&&e.UNSAFE_componentWillMount(),b!==e.state&&zc.enqueueReplaceState(e,e.state,null),f=a.updateQueue,null!==f&&(Cb(a,f,c,e,d),e.state=a.memoizedState));\"function\"===typeof e.componentDidMount&&(a.effectTag|=4)}function Db(a,b,c){a=c.ref;if(null!==a&&\"function\"!==typeof a&&\"object\"!==typeof a){if(c._owner){c=c._owner;var d=void 0;c&&(1!==\nc.tag?n(\"309\"):void 0,d=c.stateNode);d?void 0:n(\"147\",a);var e=\"\"+a;if(null!==b&&null!==b.ref&&\"function\"===typeof b.ref&&b.ref._stringRef===e)return b.ref;b=function(a){var b=d.refs;b===$f&&(b=d.refs={});null===a?delete b[e]:b[e]=a};b._stringRef=e;return b}\"string\"!==typeof a?n(\"284\"):void 0;c._owner?void 0:n(\"290\",a)}return a}function Ac(a,b){\"textarea\"!==a.type&&n(\"31\",\"[object Object]\"===Object.prototype.toString.call(b)?\"object with keys {\"+Object.keys(b).join(\", \")+\"}\":b,\"\")}function ag(a){function b(b,\nc){if(a){var d=b.lastEffect;null!==d?(d.nextEffect=c,b.lastEffect=c):b.firstEffect=b.lastEffect=c;c.nextEffect=null;c.effectTag=8}}function c(c,d){if(!a)return null;for(;null!==d;)b(c,d),d=d.sibling;return null}function d(a,b){for(a=new Map;null!==b;)null!==b.key?a.set(b.key,b):a.set(b.index,b),b=b.sibling;return a}function e(a,b,c){a=Ga(a,b,c);a.index=0;a.sibling=null;return a}function f(b,c,d){b.index=d;if(!a)return c;d=b.alternate;if(null!==d)return d=d.index,d<c?(b.effectTag=2,c):d;b.effectTag=\n2;return c}function g(b){a&&null===b.alternate&&(b.effectTag=2);return b}function h(a,b,c,d){if(null===b||6!==b.tag)return b=Nd(c,a.mode,d),b.return=a,b;b=e(b,c,d);b.return=a;return b}function l(a,b,c,d){if(null!==b&&b.elementType===c.type)return d=e(b,c.props,d),d.ref=Db(a,b,c),d.return=a,d;d=wc(c.type,c.key,c.props,null,a.mode,d);d.ref=Db(a,b,c);d.return=a;return d}function k(a,b,c,d){if(null===b||4!==b.tag||b.stateNode.containerInfo!==c.containerInfo||b.stateNode.implementation!==c.implementation)return b=\nOd(c,a.mode,d),b.return=a,b;b=e(b,c.children||[],d);b.return=a;return b}function m(a,b,c,d,f){if(null===b||7!==b.tag)return b=wa(c,a.mode,d,f),b.return=a,b;b=e(b,c,d);b.return=a;return b}function Ff(a,b,c){if(\"string\"===typeof b||\"number\"===typeof b)return b=Nd(\"\"+b,a.mode,c),b.return=a,b;if(\"object\"===typeof b&&null!==b){switch(b.$$typeof){case Bc:return c=wc(b.type,b.key,b.props,null,a.mode,c),c.ref=Db(a,null,b),c.return=a,c;case Va:return b=Od(b,a.mode,c),b.return=a,b}if(Cc(b)||sb(b))return b=\nwa(b,a.mode,c,null),b.return=a,b;Ac(a,b)}return null}function p(a,b,c,d){var e=null!==b?b.key:null;if(\"string\"===typeof c||\"number\"===typeof c)return null!==e?null:h(a,b,\"\"+c,d);if(\"object\"===typeof c&&null!==c){switch(c.$$typeof){case Bc:return c.key===e?c.type===ta?m(a,b,c.props.children,d,e):l(a,b,c,d):null;case Va:return c.key===e?k(a,b,c,d):null}if(Cc(c)||sb(c))return null!==e?null:m(a,b,c,d,null);Ac(a,c)}return null}function r(a,b,c,d,e){if(\"string\"===typeof d||\"number\"===typeof d)return a=\na.get(c)||null,h(b,a,\"\"+d,e);if(\"object\"===typeof d&&null!==d){switch(d.$$typeof){case Bc:return a=a.get(null===d.key?c:d.key)||null,d.type===ta?m(b,a,d.props.children,e,d.key):l(b,a,d,e);case Va:return a=a.get(null===d.key?c:d.key)||null,k(b,a,d,e)}if(Cc(d)||sb(d))return a=a.get(c)||null,m(b,a,d,e,null);Ac(b,d)}return null}function u(e,g,h,k){for(var l=null,m=null,n=g,q=g=0,v=null;null!==n&&q<h.length;q++){n.index>q?(v=n,n=null):v=n.sibling;var Q=p(e,n,h[q],k);if(null===Q){null===n&&(n=v);break}a&&\nn&&null===Q.alternate&&b(e,n);g=f(Q,g,q);null===m?l=Q:m.sibling=Q;m=Q;n=v}if(q===h.length)return c(e,n),l;if(null===n){for(;q<h.length;q++)if(n=Ff(e,h[q],k))g=f(n,g,q),null===m?l=n:m.sibling=n,m=n;return l}for(n=d(e,n);q<h.length;q++)if(v=r(n,e,q,h[q],k))a&&null!==v.alternate&&n.delete(null===v.key?q:v.key),g=f(v,g,q),null===m?l=v:m.sibling=v,m=v;a&&n.forEach(function(a){return b(e,a)});return l}function x(e,g,h,k){var l=sb(h);\"function\"!==typeof l?n(\"150\"):void 0;h=l.call(h);null==h?n(\"151\"):void 0;\nfor(var m=l=null,q=g,v=g=0,Q=null,t=h.next();null!==q&&!t.done;v++,t=h.next()){q.index>v?(Q=q,q=null):Q=q.sibling;var u=p(e,q,t.value,k);if(null===u){q||(q=Q);break}a&&q&&null===u.alternate&&b(e,q);g=f(u,g,v);null===m?l=u:m.sibling=u;m=u;q=Q}if(t.done)return c(e,q),l;if(null===q){for(;!t.done;v++,t=h.next())t=Ff(e,t.value,k),null!==t&&(g=f(t,g,v),null===m?l=t:m.sibling=t,m=t);return l}for(q=d(e,q);!t.done;v++,t=h.next())t=r(q,e,v,t.value,k),null!==t&&(a&&null!==t.alternate&&q.delete(null===t.key?\nv:t.key),g=f(t,g,v),null===m?l=t:m.sibling=t,m=t);a&&q.forEach(function(a){return b(e,a)});return l}return function(a,d,f,h){var k=\"object\"===typeof f&&null!==f&&f.type===ta&&null===f.key;k&&(f=f.props.children);var l=\"object\"===typeof f&&null!==f;if(l)switch(f.$$typeof){case Bc:a:{l=f.key;for(k=d;null!==k;){if(k.key===l)if(7===k.tag?f.type===ta:k.elementType===f.type){c(a,k.sibling);d=e(k,f.type===ta?f.props.children:f.props,h);d.ref=Db(a,k,f);d.return=a;a=d;break a}else{c(a,k);break}else b(a,k);\nk=k.sibling}f.type===ta?(d=wa(f.props.children,a.mode,h,f.key),d.return=a,a=d):(h=wc(f.type,f.key,f.props,null,a.mode,h),h.ref=Db(a,d,f),h.return=a,a=h)}return g(a);case Va:a:{for(k=f.key;null!==d;){if(d.key===k)if(4===d.tag&&d.stateNode.containerInfo===f.containerInfo&&d.stateNode.implementation===f.implementation){c(a,d.sibling);d=e(d,f.children||[],h);d.return=a;a=d;break a}else{c(a,d);break}else b(a,d);d=d.sibling}d=Od(f,a.mode,h);d.return=a;a=d}return g(a)}if(\"string\"===typeof f||\"number\"===\ntypeof f)return f=\"\"+f,null!==d&&6===d.tag?(c(a,d.sibling),d=e(d,f,h),d.return=a,a=d):(c(a,d),d=Nd(f,a.mode,h),d.return=a,a=d),g(a);if(Cc(f))return u(a,d,f,h);if(sb(f))return x(a,d,f,h);l&&Ac(a,f);if(\"undefined\"===typeof f&&!k)switch(a.tag){case 1:case 0:h=a.type,n(\"152\",h.displayName||h.name||\"Component\")}return c(a,d)}}function Ha(a){a===Eb?n(\"174\"):void 0;return a}function Qd(a,b){L(Fb,b,a);L(Gb,a,a);L(U,Eb,a);var c=b.nodeType;switch(c){case 9:case 11:b=(b=b.documentElement)?b.namespaceURI:Dd(null,\n\"\");break;default:c=8===c?b.parentNode:b,b=c.namespaceURI||null,c=c.tagName,b=Dd(b,c)}D(U,a);L(U,b,a)}function $a(a){D(U,a);D(Gb,a);D(Fb,a)}function bg(a){Ha(Fb.current);var b=Ha(U.current);var c=Dd(b,a.type);b!==c&&(L(Gb,a,a),L(U,c,a))}function Rd(a){Gb.current===a&&(D(U,a),D(Gb,a))}function V(){n(\"321\")}function Sd(a,b){if(null===b)return!1;for(var c=0;c<b.length&&c<a.length;c++)if(!Ea(a[c],b[c]))return!1;return!0}function Td(a,b,c,d,e,f){Hb=f;xa=b;W=null!==a?a.memoizedState:null;Dc.current=null===\nW?fi:cg;b=c(d,e);if(Ib){do Ib=!1,Jb+=1,W=null!==a?a.memoizedState:null,ab=bb,X=G=y=null,Dc.current=cg,b=c(d,e);while(Ib);ia=null;Jb=0}Dc.current=Ud;a=xa;a.memoizedState=bb;a.expirationTime=Kb;a.updateQueue=X;a.effectTag|=Lb;a=null!==y&&null!==y.next;Hb=0;ab=G=bb=W=y=xa=null;Kb=0;X=null;Lb=0;a?n(\"300\"):void 0;return b}function Vd(){Dc.current=Ud;Hb=0;ab=G=bb=W=y=xa=null;Kb=0;X=null;Lb=0;Ib=!1;ia=null;Jb=0}function cb(){var a={memoizedState:null,baseState:null,queue:null,baseUpdate:null,next:null};\nnull===G?bb=G=a:G=G.next=a;return G}function Mb(){if(null!==ab)G=ab,ab=G.next,y=W,W=null!==y?y.next:null;else{null===W?n(\"310\"):void 0;y=W;var a={memoizedState:y.memoizedState,baseState:y.baseState,queue:y.queue,baseUpdate:y.baseUpdate,next:null};G=null===G?bb=a:G.next=a;W=y.next}return G}function dg(a,b){return\"function\"===typeof b?b(a):b}function eg(a,b,c){b=Mb();c=b.queue;null===c?n(\"311\"):void 0;c.lastRenderedReducer=a;if(0<Jb){var d=c.dispatch;if(null!==ia){var e=ia.get(c);if(void 0!==e){ia.delete(c);\nvar f=b.memoizedState;do f=a(f,e.action),e=e.next;while(null!==e);Ea(f,b.memoizedState)||(ja=!0);b.memoizedState=f;b.baseUpdate===c.last&&(b.baseState=f);c.lastRenderedState=f;return[f,d]}}return[b.memoizedState,d]}d=c.last;var g=b.baseUpdate;f=b.baseState;null!==g?(null!==d&&(d.next=null),d=g.next):d=null!==d?d.next:null;if(null!==d){var h=e=null,l=d,k=!1;do{var m=l.expirationTime;m<Hb?(k||(k=!0,h=g,e=f),m>Kb&&(Kb=m)):f=l.eagerReducer===a?l.eagerState:a(f,l.action);g=l;l=l.next}while(null!==l&&l!==\nd);k||(h=g,e=f);Ea(f,b.memoizedState)||(ja=!0);b.memoizedState=f;b.baseUpdate=h;b.baseState=e;c.lastRenderedState=f}return[b.memoizedState,c.dispatch]}function Wd(a,b,c,d){a={tag:a,create:b,destroy:c,deps:d,next:null};null===X?(X={lastEffect:null},X.lastEffect=a.next=a):(b=X.lastEffect,null===b?X.lastEffect=a.next=a:(c=b.next,b.next=a,a.next=c,X.lastEffect=a));return a}function Xd(a,b,c,d){var e=cb();Lb|=a;e.memoizedState=Wd(b,c,void 0,void 0===d?null:d)}function Yd(a,b,c,d){var e=Mb();d=void 0===\nd?null:d;var f=void 0;if(null!==y){var g=y.memoizedState;f=g.destroy;if(null!==d&&Sd(d,g.deps)){Wd(db,c,f,d);return}}Lb|=a;e.memoizedState=Wd(b,c,f,d)}function fg(a,b){if(\"function\"===typeof b)return a=a(),b(a),function(){b(null)};if(null!==b&&void 0!==b)return a=a(),b.current=a,function(){b.current=null}}function gg(a,b){}function hg(a,b,c){25>Jb?void 0:n(\"301\");var d=a.alternate;if(a===xa||null!==d&&d===xa)if(Ib=!0,a={expirationTime:Hb,action:c,eagerReducer:null,eagerState:null,next:null},null===\nia&&(ia=new Map),c=ia.get(b),void 0===c)ia.set(b,a);else{for(b=c;null!==b.next;)b=b.next;b.next=a}else{eb();var e=ka();e=fb(e,a);var f={expirationTime:e,action:c,eagerReducer:null,eagerState:null,next:null},g=b.last;if(null===g)f.next=f;else{var h=g.next;null!==h&&(f.next=h);g.next=f}b.last=f;if(0===a.expirationTime&&(null===d||0===d.expirationTime)&&(d=b.lastRenderedReducer,null!==d))try{var l=b.lastRenderedState,k=d(l,c);f.eagerReducer=d;f.eagerState=k;if(Ea(k,l))return}catch(m){}finally{}ya(a,\ne)}}function ig(a,b){var c=S(5,null,null,0);c.elementType=\"DELETED\";c.type=\"DELETED\";c.stateNode=b;c.return=a;c.effectTag=8;null!==a.lastEffect?(a.lastEffect.nextEffect=c,a.lastEffect=c):a.firstEffect=a.lastEffect=c}function jg(a,b){switch(a.tag){case 5:var c=a.type;b=1!==b.nodeType||c.toLowerCase()!==b.nodeName.toLowerCase()?null:b;return null!==b?(a.stateNode=b,!0):!1;case 6:return b=\"\"===a.pendingProps||3!==b.nodeType?null:b,null!==b?(a.stateNode=b,!0):!1;case 13:return!1;default:return!1}}function kg(a){if(Ia){var b=\ngb;if(b){var c=b;if(!jg(a,b)){b=Hd(c);if(!b||!jg(a,b)){a.effectTag|=2;Ia=!1;la=a;return}ig(la,c)}la=a;gb=Pf(b)}else a.effectTag|=2,Ia=!1,la=a}}function lg(a){for(a=a.return;null!==a&&5!==a.tag&&3!==a.tag&&18!==a.tag;)a=a.return;la=a}function Zd(a){if(a!==la)return!1;if(!Ia)return lg(a),Ia=!0,!1;var b=a.type;if(5!==a.tag||\"head\"!==b&&\"body\"!==b&&!Gd(b,a.memoizedProps))for(b=gb;b;)ig(a,b),b=Hd(b);lg(a);gb=la?Hd(a.stateNode):null;return!0}function $d(){gb=la=null;Ia=!1}function N(a,b,c,d){b.child=null===\na?ae(b,null,c,d):hb(b,a.child,c,d)}function mg(a,b,c,d,e){c=c.render;var f=b.ref;ib(b,e);d=Td(a,b,c,d,f,e);if(null!==a&&!ja)return b.updateQueue=a.updateQueue,b.effectTag&=-517,a.expirationTime<=e&&(a.expirationTime=0),ma(a,b,e);b.effectTag|=1;N(a,b,d,e);return b.child}function ng(a,b,c,d,e,f){if(null===a){var g=c.type;if(\"function\"===typeof g&&!Md(g)&&void 0===g.defaultProps&&null===c.compare&&void 0===c.defaultProps)return b.tag=15,b.type=g,og(a,b,g,d,e,f);a=wc(c.type,null,d,null,b.mode,f);a.ref=\nb.ref;a.return=b;return b.child=a}g=a.child;if(e<f&&(e=g.memoizedProps,c=c.compare,c=null!==c?c:vb,c(e,d)&&a.ref===b.ref))return ma(a,b,f);b.effectTag|=1;a=Ga(g,d,f);a.ref=b.ref;a.return=b;return b.child=a}function og(a,b,c,d,e,f){return null!==a&&vb(a.memoizedProps,d)&&a.ref===b.ref&&(ja=!1,e<f)?ma(a,b,f):be(a,b,c,d,f)}function pg(a,b){var c=b.ref;if(null===a&&null!==c||null!==a&&a.ref!==c)b.effectTag|=128}function be(a,b,c,d,e){var f=E(c)?Fa:F.current;f=Za(b,f);ib(b,e);c=Td(a,b,c,d,f,e);if(null!==\na&&!ja)return b.updateQueue=a.updateQueue,b.effectTag&=-517,a.expirationTime<=e&&(a.expirationTime=0),ma(a,b,e);b.effectTag|=1;N(a,b,c,e);return b.child}function qg(a,b,c,d,e){if(E(c)){var f=!0;vc(b)}else f=!1;ib(b,e);if(null===b.stateNode)null!==a&&(a.alternate=null,b.alternate=null,b.effectTag|=2),Yf(b,c,d,e),Pd(b,c,d,e),d=!0;else if(null===a){var g=b.stateNode,h=b.memoizedProps;g.props=h;var l=g.context,k=c.contextType;\"object\"===typeof k&&null!==k?k=T(k):(k=E(c)?Fa:F.current,k=Za(b,k));var m=\nc.getDerivedStateFromProps,n=\"function\"===typeof m||\"function\"===typeof g.getSnapshotBeforeUpdate;n||\"function\"!==typeof g.UNSAFE_componentWillReceiveProps&&\"function\"!==typeof g.componentWillReceiveProps||(h!==d||l!==k)&&Zf(b,g,d,k);za=!1;var p=b.memoizedState;l=g.state=p;var r=b.updateQueue;null!==r&&(Cb(b,r,d,g,e),l=b.memoizedState);h!==d||p!==l||M.current||za?(\"function\"===typeof m&&(yc(b,c,m,d),l=b.memoizedState),(h=za||Xf(b,c,h,d,p,l,k))?(n||\"function\"!==typeof g.UNSAFE_componentWillMount&&\n\"function\"!==typeof g.componentWillMount||(\"function\"===typeof g.componentWillMount&&g.componentWillMount(),\"function\"===typeof g.UNSAFE_componentWillMount&&g.UNSAFE_componentWillMount()),\"function\"===typeof g.componentDidMount&&(b.effectTag|=4)):(\"function\"===typeof g.componentDidMount&&(b.effectTag|=4),b.memoizedProps=d,b.memoizedState=l),g.props=d,g.state=l,g.context=k,d=h):(\"function\"===typeof g.componentDidMount&&(b.effectTag|=4),d=!1)}else g=b.stateNode,h=b.memoizedProps,g.props=b.type===b.elementType?\nh:P(b.type,h),l=g.context,k=c.contextType,\"object\"===typeof k&&null!==k?k=T(k):(k=E(c)?Fa:F.current,k=Za(b,k)),m=c.getDerivedStateFromProps,(n=\"function\"===typeof m||\"function\"===typeof g.getSnapshotBeforeUpdate)||\"function\"!==typeof g.UNSAFE_componentWillReceiveProps&&\"function\"!==typeof g.componentWillReceiveProps||(h!==d||l!==k)&&Zf(b,g,d,k),za=!1,l=b.memoizedState,p=g.state=l,r=b.updateQueue,null!==r&&(Cb(b,r,d,g,e),p=b.memoizedState),h!==d||l!==p||M.current||za?(\"function\"===typeof m&&(yc(b,\nc,m,d),p=b.memoizedState),(m=za||Xf(b,c,h,d,l,p,k))?(n||\"function\"!==typeof g.UNSAFE_componentWillUpdate&&\"function\"!==typeof g.componentWillUpdate||(\"function\"===typeof g.componentWillUpdate&&g.componentWillUpdate(d,p,k),\"function\"===typeof g.UNSAFE_componentWillUpdate&&g.UNSAFE_componentWillUpdate(d,p,k)),\"function\"===typeof g.componentDidUpdate&&(b.effectTag|=4),\"function\"===typeof g.getSnapshotBeforeUpdate&&(b.effectTag|=256)):(\"function\"!==typeof g.componentDidUpdate||h===a.memoizedProps&&l===\na.memoizedState||(b.effectTag|=4),\"function\"!==typeof g.getSnapshotBeforeUpdate||h===a.memoizedProps&&l===a.memoizedState||(b.effectTag|=256),b.memoizedProps=d,b.memoizedState=p),g.props=d,g.state=p,g.context=k,d=m):(\"function\"!==typeof g.componentDidUpdate||h===a.memoizedProps&&l===a.memoizedState||(b.effectTag|=4),\"function\"!==typeof g.getSnapshotBeforeUpdate||h===a.memoizedProps&&l===a.memoizedState||(b.effectTag|=256),d=!1);return ce(a,b,c,d,f,e)}function ce(a,b,c,d,e,f){pg(a,b);var g=0!==(b.effectTag&\n64);if(!d&&!g)return e&&Sf(b,c,!1),ma(a,b,f);d=b.stateNode;gi.current=b;var h=g&&\"function\"!==typeof c.getDerivedStateFromError?null:d.render();b.effectTag|=1;null!==a&&g?(b.child=hb(b,a.child,null,f),b.child=hb(b,null,h,f)):N(a,b,h,f);b.memoizedState=d.state;e&&Sf(b,c,!0);return b.child}function rg(a){var b=a.stateNode;b.pendingContext?Qf(a,b.pendingContext,b.pendingContext!==b.context):b.context&&Qf(a,b.context,!1);Qd(a,b.containerInfo)}function sg(a,b,c){var d=b.mode,e=b.pendingProps,f=b.memoizedState;\nif(0===(b.effectTag&64)){f=null;var g=!1}else f={timedOutAt:null!==f?f.timedOutAt:0},g=!0,b.effectTag&=-65;if(null===a)if(g){var h=e.fallback;a=wa(null,d,0,null);0===(b.mode&1)&&(a.child=null!==b.memoizedState?b.child.child:b.child);d=wa(h,d,c,null);a.sibling=d;c=a;c.return=d.return=b}else c=d=ae(b,null,e.children,c);else null!==a.memoizedState?(d=a.child,h=d.sibling,g?(c=e.fallback,e=Ga(d,d.pendingProps,0),0===(b.mode&1)&&(g=null!==b.memoizedState?b.child.child:b.child,g!==d.child&&(e.child=g)),\nd=e.sibling=Ga(h,c,h.expirationTime),c=e,e.childExpirationTime=0,c.return=d.return=b):c=d=hb(b,d.child,e.children,c)):(h=a.child,g?(g=e.fallback,e=wa(null,d,0,null),e.child=h,0===(b.mode&1)&&(e.child=null!==b.memoizedState?b.child.child:b.child),d=e.sibling=wa(g,d,c,null),d.effectTag|=2,c=e,e.childExpirationTime=0,c.return=d.return=b):d=c=hb(b,h,e.children,c)),b.stateNode=a.stateNode;b.memoizedState=f;b.child=c;return d}function ma(a,b,c){null!==a&&(b.contextDependencies=a.contextDependencies);if(b.childExpirationTime<\nc)return null;null!==a&&b.child!==a.child?n(\"153\"):void 0;if(null!==b.child){a=b.child;c=Ga(a,a.pendingProps,a.expirationTime);b.child=c;for(c.return=b;null!==a.sibling;)a=a.sibling,c=c.sibling=Ga(a,a.pendingProps,a.expirationTime),c.return=b;c.sibling=null}return b.child}function hi(a,b,c){var d=b.expirationTime;if(null!==a)if(a.memoizedProps!==b.pendingProps||M.current)ja=!0;else{if(d<c){ja=!1;switch(b.tag){case 3:rg(b);$d();break;case 5:bg(b);break;case 1:E(b.type)&&vc(b);break;case 4:Qd(b,b.stateNode.containerInfo);\nbreak;case 10:tg(b,b.memoizedProps.value);break;case 13:if(null!==b.memoizedState){d=b.child.childExpirationTime;if(0!==d&&d>=c)return sg(a,b,c);b=ma(a,b,c);return null!==b?b.sibling:null}}return ma(a,b,c)}}else ja=!1;b.expirationTime=0;switch(b.tag){case 2:d=b.elementType;null!==a&&(a.alternate=null,b.alternate=null,b.effectTag|=2);a=b.pendingProps;var e=Za(b,F.current);ib(b,c);e=Td(null,b,d,a,e,c);b.effectTag|=1;if(\"object\"===typeof e&&null!==e&&\"function\"===typeof e.render&&void 0===e.$$typeof){b.tag=\n1;Vd();if(E(d)){var f=!0;vc(b)}else f=!1;b.memoizedState=null!==e.state&&void 0!==e.state?e.state:null;var g=d.getDerivedStateFromProps;\"function\"===typeof g&&yc(b,d,g,a);e.updater=zc;b.stateNode=e;e._reactInternalFiber=b;Pd(b,d,a,c);b=ce(null,b,d,!0,f,c)}else b.tag=0,N(null,b,e,c),b=b.child;return b;case 16:e=b.elementType;null!==a&&(a.alternate=null,b.alternate=null,b.effectTag|=2);f=b.pendingProps;a=ei(e);b.type=a;e=b.tag=ci(a);f=P(a,f);g=void 0;switch(e){case 0:g=be(null,b,a,f,c);break;case 1:g=\nqg(null,b,a,f,c);break;case 11:g=mg(null,b,a,f,c);break;case 14:g=ng(null,b,a,P(a.type,f),d,c);break;default:n(\"306\",a,\"\")}return g;case 0:return d=b.type,e=b.pendingProps,e=b.elementType===d?e:P(d,e),be(a,b,d,e,c);case 1:return d=b.type,e=b.pendingProps,e=b.elementType===d?e:P(d,e),qg(a,b,d,e,c);case 3:rg(b);d=b.updateQueue;null===d?n(\"282\"):void 0;e=b.memoizedState;e=null!==e?e.element:null;Cb(b,d,b.pendingProps,null,c);d=b.memoizedState.element;if(d===e)$d(),b=ma(a,b,c);else{e=b.stateNode;if(e=\n(null===a||null===a.child)&&e.hydrate)gb=Pf(b.stateNode.containerInfo),la=b,e=Ia=!0;e?(b.effectTag|=2,b.child=ae(b,null,d,c)):(N(a,b,d,c),$d());b=b.child}return b;case 5:return bg(b),null===a&&kg(b),d=b.type,e=b.pendingProps,f=null!==a?a.memoizedProps:null,g=e.children,Gd(d,e)?g=null:null!==f&&Gd(d,f)&&(b.effectTag|=16),pg(a,b),1!==c&&b.mode&1&&e.hidden?(b.expirationTime=b.childExpirationTime=1,b=null):(N(a,b,g,c),b=b.child),b;case 6:return null===a&&kg(b),null;case 13:return sg(a,b,c);case 4:return Qd(b,\nb.stateNode.containerInfo),d=b.pendingProps,null===a?b.child=hb(b,null,d,c):N(a,b,d,c),b.child;case 11:return d=b.type,e=b.pendingProps,e=b.elementType===d?e:P(d,e),mg(a,b,d,e,c);case 7:return N(a,b,b.pendingProps,c),b.child;case 8:return N(a,b,b.pendingProps.children,c),b.child;case 12:return N(a,b,b.pendingProps.children,c),b.child;case 10:a:{d=b.type._context;e=b.pendingProps;g=b.memoizedProps;f=e.value;tg(b,f);if(null!==g){var h=g.value;f=Ea(h,f)?0:(\"function\"===typeof d._calculateChangedBits?\nd._calculateChangedBits(h,f):1073741823)|0;if(0===f){if(g.children===e.children&&!M.current){b=ma(a,b,c);break a}}else for(h=b.child,null!==h&&(h.return=b);null!==h;){var l=h.contextDependencies;if(null!==l){g=h.child;for(var k=l.first;null!==k;){if(k.context===d&&0!==(k.observedBits&f)){1===h.tag&&(k=Aa(c),k.tag=Ec,na(h,k));h.expirationTime<c&&(h.expirationTime=c);k=h.alternate;null!==k&&k.expirationTime<c&&(k.expirationTime=c);k=c;for(var m=h.return;null!==m;){var p=m.alternate;if(m.childExpirationTime<\nk)m.childExpirationTime=k,null!==p&&p.childExpirationTime<k&&(p.childExpirationTime=k);else if(null!==p&&p.childExpirationTime<k)p.childExpirationTime=k;else break;m=m.return}l.expirationTime<c&&(l.expirationTime=c);break}k=k.next}}else g=10===h.tag?h.type===b.type?null:h.child:h.child;if(null!==g)g.return=h;else for(g=h;null!==g;){if(g===b){g=null;break}h=g.sibling;if(null!==h){h.return=g.return;g=h;break}g=g.return}h=g}}N(a,b,e.children,c);b=b.child}return b;case 9:return e=b.type,f=b.pendingProps,\nd=f.children,ib(b,c),e=T(e,f.unstable_observedBits),d=d(e),b.effectTag|=1,N(a,b,d,c),b.child;case 14:return e=b.type,f=P(e,b.pendingProps),f=P(e.type,f),ng(a,b,e,f,d,c);case 15:return og(a,b,b.type,b.pendingProps,d,c);case 17:return d=b.type,e=b.pendingProps,e=b.elementType===d?e:P(d,e),null!==a&&(a.alternate=null,b.alternate=null,b.effectTag|=2),b.tag=1,E(d)?(a=!0,vc(b)):a=!1,ib(b,c),Yf(b,d,e,c),Pd(b,d,e,c),ce(null,b,d,!0,a,c)}n(\"156\")}function tg(a,b){var c=a.type._context;L(de,c._currentValue,\na);c._currentValue=b}function ee(a){var b=de.current;D(de,a);a.type._context._currentValue=b}function ib(a,b){Nb=a;Ob=Ja=null;var c=a.contextDependencies;null!==c&&c.expirationTime>=b&&(ja=!0);a.contextDependencies=null}function T(a,b){if(Ob!==a&&!1!==b&&0!==b){if(\"number\"!==typeof b||1073741823===b)Ob=a,b=1073741823;b={context:a,observedBits:b,next:null};null===Ja?(null===Nb?n(\"308\"):void 0,Ja=b,Nb.contextDependencies={first:b,expirationTime:0}):Ja=Ja.next=b}return a._currentValue}function Fc(a){return{baseState:a,\nfirstUpdate:null,lastUpdate:null,firstCapturedUpdate:null,lastCapturedUpdate:null,firstEffect:null,lastEffect:null,firstCapturedEffect:null,lastCapturedEffect:null}}function fe(a){return{baseState:a.baseState,firstUpdate:a.firstUpdate,lastUpdate:a.lastUpdate,firstCapturedUpdate:null,lastCapturedUpdate:null,firstEffect:null,lastEffect:null,firstCapturedEffect:null,lastCapturedEffect:null}}function Aa(a){return{expirationTime:a,tag:ug,payload:null,callback:null,next:null,nextEffect:null}}function Gc(a,\nb){null===a.lastUpdate?a.firstUpdate=a.lastUpdate=b:(a.lastUpdate.next=b,a.lastUpdate=b)}function na(a,b){var c=a.alternate;if(null===c){var d=a.updateQueue;var e=null;null===d&&(d=a.updateQueue=Fc(a.memoizedState))}else d=a.updateQueue,e=c.updateQueue,null===d?null===e?(d=a.updateQueue=Fc(a.memoizedState),e=c.updateQueue=Fc(c.memoizedState)):d=a.updateQueue=fe(e):null===e&&(e=c.updateQueue=fe(d));null===e||d===e?Gc(d,b):null===d.lastUpdate||null===e.lastUpdate?(Gc(d,b),Gc(e,b)):(Gc(d,b),e.lastUpdate=\nb)}function vg(a,b){var c=a.updateQueue;c=null===c?a.updateQueue=Fc(a.memoizedState):wg(a,c);null===c.lastCapturedUpdate?c.firstCapturedUpdate=c.lastCapturedUpdate=b:(c.lastCapturedUpdate.next=b,c.lastCapturedUpdate=b)}function wg(a,b){var c=a.alternate;null!==c&&b===c.updateQueue&&(b=a.updateQueue=fe(b));return b}function xg(a,b,c,d,e,f){switch(c.tag){case yg:return a=c.payload,\"function\"===typeof a?a.call(f,d,e):a;case ge:a.effectTag=a.effectTag&-2049|64;case ug:a=c.payload;e=\"function\"===typeof a?\na.call(f,d,e):a;if(null===e||void 0===e)break;return B({},d,e);case Ec:za=!0}return d}function Cb(a,b,c,d,e){za=!1;b=wg(a,b);for(var f=b.baseState,g=null,h=0,l=b.firstUpdate,k=f;null!==l;){var m=l.expirationTime;m<e?(null===g&&(g=l,f=k),h<m&&(h=m)):(k=xg(a,b,l,k,c,d),null!==l.callback&&(a.effectTag|=32,l.nextEffect=null,null===b.lastEffect?b.firstEffect=b.lastEffect=l:(b.lastEffect.nextEffect=l,b.lastEffect=l)));l=l.next}m=null;for(l=b.firstCapturedUpdate;null!==l;){var n=l.expirationTime;n<e?(null===\nm&&(m=l,null===g&&(f=k)),h<n&&(h=n)):(k=xg(a,b,l,k,c,d),null!==l.callback&&(a.effectTag|=32,l.nextEffect=null,null===b.lastCapturedEffect?b.firstCapturedEffect=b.lastCapturedEffect=l:(b.lastCapturedEffect.nextEffect=l,b.lastCapturedEffect=l)));l=l.next}null===g&&(b.lastUpdate=null);null===m?b.lastCapturedUpdate=null:a.effectTag|=32;null===g&&null===m&&(f=k);b.baseState=f;b.firstUpdate=g;b.firstCapturedUpdate=m;a.expirationTime=h;a.memoizedState=k}function zg(a,b,c,d){null!==b.firstCapturedUpdate&&\n(null!==b.lastUpdate&&(b.lastUpdate.next=b.firstCapturedUpdate,b.lastUpdate=b.lastCapturedUpdate),b.firstCapturedUpdate=b.lastCapturedUpdate=null);Ag(b.firstEffect,c);b.firstEffect=b.lastEffect=null;Ag(b.firstCapturedEffect,c);b.firstCapturedEffect=b.lastCapturedEffect=null}function Ag(a,b){for(;null!==a;){var c=a.callback;if(null!==c){a.callback=null;var d=b;\"function\"!==typeof c?n(\"191\",c):void 0;c.call(d)}a=a.nextEffect}}function Hc(a,b){return{value:a,source:b,stack:qd(b)}}function Pb(a){a.effectTag|=\n4}function Bg(a,b){var c=b.source,d=b.stack;null===d&&null!==c&&(d=qd(c));null!==c&&sa(c.type);b=b.value;null!==a&&1===a.tag&&sa(a.type);try{console.error(b)}catch(e){setTimeout(function(){throw e;})}}function Cg(a){var b=a.ref;if(null!==b)if(\"function\"===typeof b)try{b(null)}catch(c){Ka(a,c)}else b.current=null}function Qb(a,b,c){c=c.updateQueue;c=null!==c?c.lastEffect:null;if(null!==c){var d=c=c.next;do{if((d.tag&a)!==db){var e=d.destroy;d.destroy=void 0;void 0!==e&&e()}(d.tag&b)!==db&&(e=d.create,\nd.destroy=e());d=d.next}while(d!==c)}}function ii(a,b){for(var c=a;;){if(5===c.tag){var d=c.stateNode;if(b)d.style.display=\"none\";else{d=c.stateNode;var e=c.memoizedProps.style;e=void 0!==e&&null!==e&&e.hasOwnProperty(\"display\")?e.display:null;d.style.display=Lf(\"display\",e)}}else if(6===c.tag)c.stateNode.nodeValue=b?\"\":c.memoizedProps;else if(13===c.tag&&null!==c.memoizedState){d=c.child.sibling;d.return=c;c=d;continue}else if(null!==c.child){c.child.return=c;c=c.child;continue}if(c===a)break;for(;null===\nc.sibling;){if(null===c.return||c.return===a)return;c=c.return}c.sibling.return=c.return;c=c.sibling}}function Dg(a){\"function\"===typeof Ld&&Ld(a);switch(a.tag){case 0:case 11:case 14:case 15:var b=a.updateQueue;if(null!==b&&(b=b.lastEffect,null!==b)){var c=b=b.next;do{var d=c.destroy;if(void 0!==d){var e=a;try{d()}catch(f){Ka(e,f)}}c=c.next}while(c!==b)}break;case 1:Cg(a);b=a.stateNode;if(\"function\"===typeof b.componentWillUnmount)try{b.props=a.memoizedProps,b.state=a.memoizedState,b.componentWillUnmount()}catch(f){Ka(a,\nf)}break;case 5:Cg(a);break;case 4:Eg(a)}}function Fg(a){return 5===a.tag||3===a.tag||4===a.tag}function Gg(a){a:{for(var b=a.return;null!==b;){if(Fg(b)){var c=b;break a}b=b.return}n(\"160\");c=void 0}var d=b=void 0;switch(c.tag){case 5:b=c.stateNode;d=!1;break;case 3:b=c.stateNode.containerInfo;d=!0;break;case 4:b=c.stateNode.containerInfo;d=!0;break;default:n(\"161\")}c.effectTag&16&&(Ab(b,\"\"),c.effectTag&=-17);a:b:for(c=a;;){for(;null===c.sibling;){if(null===c.return||Fg(c.return)){c=null;break a}c=\nc.return}c.sibling.return=c.return;for(c=c.sibling;5!==c.tag&&6!==c.tag&&18!==c.tag;){if(c.effectTag&2)continue b;if(null===c.child||4===c.tag)continue b;else c.child.return=c,c=c.child}if(!(c.effectTag&2)){c=c.stateNode;break a}}for(var e=a;;){if(5===e.tag||6===e.tag)if(c)if(d){var f=b,g=e.stateNode,h=c;8===f.nodeType?f.parentNode.insertBefore(g,h):f.insertBefore(g,h)}else b.insertBefore(e.stateNode,c);else d?(g=b,h=e.stateNode,8===g.nodeType?(f=g.parentNode,f.insertBefore(h,g)):(f=g,f.appendChild(h)),\ng=g._reactRootContainer,null!==g&&void 0!==g||null!==f.onclick||(f.onclick=tc)):b.appendChild(e.stateNode);else if(4!==e.tag&&null!==e.child){e.child.return=e;e=e.child;continue}if(e===a)break;for(;null===e.sibling;){if(null===e.return||e.return===a)return;e=e.return}e.sibling.return=e.return;e=e.sibling}}function Eg(a){for(var b=a,c=!1,d=void 0,e=void 0;;){if(!c){c=b.return;a:for(;;){null===c?n(\"160\"):void 0;switch(c.tag){case 5:d=c.stateNode;e=!1;break a;case 3:d=c.stateNode.containerInfo;e=!0;\nbreak a;case 4:d=c.stateNode.containerInfo;e=!0;break a}c=c.return}c=!0}if(5===b.tag||6===b.tag){a:for(var f=b,g=f;;)if(Dg(g),null!==g.child&&4!==g.tag)g.child.return=g,g=g.child;else{if(g===f)break;for(;null===g.sibling;){if(null===g.return||g.return===f)break a;g=g.return}g.sibling.return=g.return;g=g.sibling}e?(f=d,g=b.stateNode,8===f.nodeType?f.parentNode.removeChild(g):f.removeChild(g)):d.removeChild(b.stateNode)}else if(4===b.tag){if(null!==b.child){d=b.stateNode.containerInfo;e=!0;b.child.return=\nb;b=b.child;continue}}else if(Dg(b),null!==b.child){b.child.return=b;b=b.child;continue}if(b===a)break;for(;null===b.sibling;){if(null===b.return||b.return===a)return;b=b.return;4===b.tag&&(c=!1)}b.sibling.return=b.return;b=b.sibling}}function Hg(a,b){switch(b.tag){case 0:case 11:case 14:case 15:Qb(Rb,ji,b);break;case 1:break;case 5:var c=b.stateNode;if(null!=c){var d=b.memoizedProps;a=null!==a?a.memoizedProps:d;var e=b.type,f=b.updateQueue;b.updateQueue=null;null!==f&&$h(c,f,e,a,d,b)}break;case 6:null===\nb.stateNode?n(\"162\"):void 0;b.stateNode.nodeValue=b.memoizedProps;break;case 3:break;case 12:break;case 13:c=b.memoizedState;d=void 0;a=b;null===c?d=!1:(d=!0,a=b.child,0===c.timedOutAt&&(c.timedOutAt=ka()));null!==a&&ii(a,d);c=b.updateQueue;if(null!==c){b.updateQueue=null;var g=b.stateNode;null===g&&(g=b.stateNode=new ki);c.forEach(function(a){var c=li.bind(null,b,a);g.has(a)||(g.add(a),a.then(c,c))})}break;case 17:break;default:n(\"163\")}}function he(a,b,c){c=Aa(c);c.tag=ge;c.payload={element:null};\nvar d=b.value;c.callback=function(){ie(d);Bg(a,b)};return c}function Ig(a,b,c){c=Aa(c);c.tag=ge;var d=a.type.getDerivedStateFromError;if(\"function\"===typeof d){var e=b.value;c.payload=function(){return d(e)}}var f=a.stateNode;null!==f&&\"function\"===typeof f.componentDidCatch&&(c.callback=function(){\"function\"!==typeof d&&(null===Ba?Ba=new Set([this]):Ba.add(this));var c=b.value,e=b.stack;Bg(a,b);this.componentDidCatch(c,{componentStack:null!==e?e:\"\"})});return c}function mi(a,b){switch(a.tag){case 1:return E(a.type)&&\nuc(a),b=a.effectTag,b&2048?(a.effectTag=b&-2049|64,a):null;case 3:return $a(a),Jd(a),b=a.effectTag,0!==(b&64)?n(\"285\"):void 0,a.effectTag=b&-2049|64,a;case 5:return Rd(a),null;case 13:return b=a.effectTag,b&2048?(a.effectTag=b&-2049|64,a):null;case 18:return null;case 4:return $a(a),null;case 10:return ee(a),null;default:return null}}function Jg(){if(null!==x)for(var a=x.return;null!==a;){var b=a;switch(b.tag){case 1:var c=b.type.childContextTypes;null!==c&&void 0!==c&&uc(b);break;case 3:$a(b);Jd(b);\nbreak;case 5:Rd(b);break;case 4:$a(b);break;case 10:ee(b)}a=a.return}Y=null;H=0;La=-1;je=!1;x=null}function ni(){for(;null!==p;){var a=p.effectTag;a&16&&Ab(p.stateNode,\"\");if(a&128){var b=p.alternate;null!==b&&(b=b.ref,null!==b&&(\"function\"===typeof b?b(null):b.current=null))}switch(a&14){case 2:Gg(p);p.effectTag&=-3;break;case 6:Gg(p);p.effectTag&=-3;Hg(p.alternate,p);break;case 4:Hg(p.alternate,p);break;case 8:a=p,Eg(a),a.return=null,a.child=null,a.memoizedState=null,a.updateQueue=null,a=a.alternate,\nnull!==a&&(a.return=null,a.child=null,a.memoizedState=null,a.updateQueue=null)}p=p.nextEffect}}function oi(){for(;null!==p;){if(p.effectTag&256)a:{var a=p.alternate,b=p;switch(b.tag){case 0:case 11:case 15:Qb(pi,db,b);break a;case 1:if(b.effectTag&256&&null!==a){var c=a.memoizedProps,d=a.memoizedState;a=b.stateNode;b=a.getSnapshotBeforeUpdate(b.elementType===b.type?c:P(b.type,c),d);a.__reactInternalSnapshotBeforeUpdate=b}break a;case 3:case 5:case 6:case 4:case 17:break a;default:n(\"163\")}}p=p.nextEffect}}\nfunction qi(a,b){for(;null!==p;){var c=p.effectTag;if(c&36){var d=p.alternate,e=p,f=b;switch(e.tag){case 0:case 11:case 15:Qb(ri,Sb,e);break;case 1:var g=e.stateNode;if(e.effectTag&4)if(null===d)g.componentDidMount();else{var h=e.elementType===e.type?d.memoizedProps:P(e.type,d.memoizedProps);g.componentDidUpdate(h,d.memoizedState,g.__reactInternalSnapshotBeforeUpdate)}d=e.updateQueue;null!==d&&zg(e,d,g,f);break;case 3:d=e.updateQueue;if(null!==d){g=null;if(null!==e.child)switch(e.child.tag){case 5:g=\ne.child.stateNode;break;case 1:g=e.child.stateNode}zg(e,d,g,f)}break;case 5:f=e.stateNode;null===d&&e.effectTag&4&&Nf(e.type,e.memoizedProps)&&f.focus();break;case 6:break;case 4:break;case 12:break;case 13:break;case 17:break;default:n(\"163\")}}c&128&&(e=p.ref,null!==e&&(f=p.stateNode,\"function\"===typeof e?e(f):e.current=f));c&512&&(ke=a);p=p.nextEffect}}function si(a,b){Ic=Jc=ke=null;var c=w;w=!0;do{if(b.effectTag&512){var d=!1,e=void 0;try{var f=b;Qb(le,db,f);Qb(db,me,f)}catch(g){d=!0,e=g}d&&Ka(b,\ne)}b=b.nextEffect}while(null!==b);w=c;c=a.expirationTime;0!==c&&Kc(a,c);z||w||Z(1073741823,!1)}function eb(){null!==Jc&&ti(Jc);null!==Ic&&Ic()}function ui(a,b){Lc=Ca=!0;a.current===b?n(\"177\"):void 0;var c=a.pendingCommitExpirationTime;0===c?n(\"261\"):void 0;a.pendingCommitExpirationTime=0;var d=b.expirationTime,e=b.childExpirationTime;di(a,e>d?e:d);Kg.current=null;d=void 0;1<b.effectTag?null!==b.lastEffect?(b.lastEffect.nextEffect=b,d=b.firstEffect):d=b:d=b.firstEffect;ne=qc;oe=Wh();qc=!1;for(p=d;null!==\np;){e=!1;var f=void 0;try{oi()}catch(h){e=!0,f=h}e&&(null===p?n(\"178\"):void 0,Ka(p,f),null!==p&&(p=p.nextEffect))}for(p=d;null!==p;){e=!1;f=void 0;try{ni()}catch(h){e=!0,f=h}e&&(null===p?n(\"178\"):void 0,Ka(p,f),null!==p&&(p=p.nextEffect))}Xh(oe);oe=null;qc=!!ne;ne=null;a.current=b;for(p=d;null!==p;){e=!1;f=void 0;try{qi(a,c)}catch(h){e=!0,f=h}e&&(null===p?n(\"178\"):void 0,Ka(p,f),null!==p&&(p=p.nextEffect))}if(null!==d&&null!==ke){var g=si.bind(null,a,d);Jc=Mc(Lg,function(){return vi(g)});Ic=g}Ca=\nLc=!1;\"function\"===typeof Kd&&Kd(b.stateNode);c=b.expirationTime;b=b.childExpirationTime;b=b>c?b:c;0===b&&(Ba=null);wi(a,b)}function Mg(a){for(;;){var b=a.alternate,c=a.return,d=a.sibling;if(0===(a.effectTag&1024)){x=a;a:{var e=b;b=a;var f=H;var g=b.pendingProps;switch(b.tag){case 2:break;case 16:break;case 15:case 0:break;case 1:E(b.type)&&uc(b);break;case 3:$a(b);Jd(b);g=b.stateNode;g.pendingContext&&(g.context=g.pendingContext,g.pendingContext=null);if(null===e||null===e.child)Zd(b),b.effectTag&=\n-3;pe(b);break;case 5:Rd(b);var h=Ha(Fb.current);f=b.type;if(null!==e&&null!=b.stateNode)Ng(e,b,f,g,h),e.ref!==b.ref&&(b.effectTag|=128);else if(g){var l=Ha(U.current);if(Zd(b)){g=b;e=g.stateNode;var k=g.type,m=g.memoizedProps,p=h;e[ea]=g;e[ec]=m;f=void 0;h=k;switch(h){case \"iframe\":case \"object\":r(\"load\",e);break;case \"video\":case \"audio\":for(k=0;k<zb.length;k++)r(zb[k],e);break;case \"source\":r(\"error\",e);break;case \"img\":case \"image\":case \"link\":r(\"error\",e);r(\"load\",e);break;case \"form\":r(\"reset\",\ne);r(\"submit\",e);break;case \"details\":r(\"toggle\",e);break;case \"input\":lf(e,m);r(\"invalid\",e);ha(p,\"onChange\");break;case \"select\":e._wrapperState={wasMultiple:!!m.multiple};r(\"invalid\",e);ha(p,\"onChange\");break;case \"textarea\":If(e,m),r(\"invalid\",e),ha(p,\"onChange\")}Ed(h,m);k=null;for(f in m)m.hasOwnProperty(f)&&(l=m[f],\"children\"===f?\"string\"===typeof l?e.textContent!==l&&(k=[\"children\",l]):\"number\"===typeof l&&e.textContent!==\"\"+l&&(k=[\"children\",\"\"+l]):Oa.hasOwnProperty(f)&&null!=l&&ha(p,f));\nswitch(h){case \"input\":kc(e);nf(e,m,!0);break;case \"textarea\":kc(e);f=e.textContent;f===e._wrapperState.initialValue&&(e.value=f);break;case \"select\":case \"option\":break;default:\"function\"===typeof m.onClick&&(e.onclick=tc)}f=k;g.updateQueue=f;g=null!==f?!0:!1;g&&Pb(b)}else{m=b;p=f;e=g;k=9===h.nodeType?h:h.ownerDocument;\"http://www.w3.org/1999/xhtml\"===l&&(l=Kf(p));\"http://www.w3.org/1999/xhtml\"===l?\"script\"===p?(e=k.createElement(\"div\"),e.innerHTML=\"<script>\\x3c/script>\",k=e.removeChild(e.firstChild)):\n\"string\"===typeof e.is?k=k.createElement(p,{is:e.is}):(k=k.createElement(p),\"select\"===p&&(p=k,e.multiple?p.multiple=!0:e.size&&(p.size=e.size))):k=k.createElementNS(l,p);e=k;e[ea]=m;e[ec]=g;Og(e,b,!1,!1);m=e;k=f;p=g;var t=h,y=Fd(k,p);switch(k){case \"iframe\":case \"object\":r(\"load\",m);h=p;break;case \"video\":case \"audio\":for(h=0;h<zb.length;h++)r(zb[h],m);h=p;break;case \"source\":r(\"error\",m);h=p;break;case \"img\":case \"image\":case \"link\":r(\"error\",m);r(\"load\",m);h=p;break;case \"form\":r(\"reset\",m);r(\"submit\",\nm);h=p;break;case \"details\":r(\"toggle\",m);h=p;break;case \"input\":lf(m,p);h=sd(m,p);r(\"invalid\",m);ha(t,\"onChange\");break;case \"option\":h=Bd(m,p);break;case \"select\":m._wrapperState={wasMultiple:!!p.multiple};h=B({},p,{value:void 0});r(\"invalid\",m);ha(t,\"onChange\");break;case \"textarea\":If(m,p);h=Cd(m,p);r(\"invalid\",m);ha(t,\"onChange\");break;default:h=p}Ed(k,h);l=void 0;var u=k,w=m,v=h;for(l in v)if(v.hasOwnProperty(l)){var q=v[l];\"style\"===l?Mf(w,q):\"dangerouslySetInnerHTML\"===l?(q=q?q.__html:void 0,\nnull!=q&&Of(w,q)):\"children\"===l?\"string\"===typeof q?(\"textarea\"!==u||\"\"!==q)&&Ab(w,q):\"number\"===typeof q&&Ab(w,\"\"+q):\"suppressContentEditableWarning\"!==l&&\"suppressHydrationWarning\"!==l&&\"autoFocus\"!==l&&(Oa.hasOwnProperty(l)?null!=q&&ha(t,l):null!=q&&rd(w,l,q,y))}switch(k){case \"input\":kc(m);nf(m,p,!1);break;case \"textarea\":kc(m);h=m.textContent;h===m._wrapperState.initialValue&&(m.value=h);break;case \"option\":null!=p.value&&m.setAttribute(\"value\",\"\"+ua(p.value));break;case \"select\":h=m;m=p;h.multiple=\n!!m.multiple;p=m.value;null!=p?Xa(h,!!m.multiple,p,!1):null!=m.defaultValue&&Xa(h,!!m.multiple,m.defaultValue,!0);break;default:\"function\"===typeof h.onClick&&(m.onclick=tc)}(g=Nf(f,g))&&Pb(b);b.stateNode=e}null!==b.ref&&(b.effectTag|=128)}else null===b.stateNode?n(\"166\"):void 0;break;case 6:e&&null!=b.stateNode?Pg(e,b,e.memoizedProps,g):(\"string\"!==typeof g&&(null===b.stateNode?n(\"166\"):void 0),e=Ha(Fb.current),Ha(U.current),Zd(b)?(g=b,f=g.stateNode,e=g.memoizedProps,f[ea]=g,(g=f.nodeValue!==e)&&\nPb(b)):(f=b,g=(9===e.nodeType?e:e.ownerDocument).createTextNode(g),g[ea]=b,f.stateNode=g));break;case 11:break;case 13:g=b.memoizedState;if(0!==(b.effectTag&64)){b.expirationTime=f;x=b;break a}g=null!==g;f=null!==e&&null!==e.memoizedState;null!==e&&!g&&f&&(e=e.child.sibling,null!==e&&(h=b.firstEffect,null!==h?(b.firstEffect=e,e.nextEffect=h):(b.firstEffect=b.lastEffect=e,e.nextEffect=null),e.effectTag=8));if(g||f)b.effectTag|=4;break;case 7:break;case 8:break;case 12:break;case 4:$a(b);pe(b);break;\ncase 10:ee(b);break;case 9:break;case 14:break;case 17:E(b.type)&&uc(b);break;case 18:break;default:n(\"156\")}x=null}b=a;if(1===H||1!==b.childExpirationTime){g=0;for(f=b.child;null!==f;)e=f.expirationTime,h=f.childExpirationTime,e>g&&(g=e),h>g&&(g=h),f=f.sibling;b.childExpirationTime=g}if(null!==x)return x;null!==c&&0===(c.effectTag&1024)&&(null===c.firstEffect&&(c.firstEffect=a.firstEffect),null!==a.lastEffect&&(null!==c.lastEffect&&(c.lastEffect.nextEffect=a.firstEffect),c.lastEffect=a.lastEffect),\n1<a.effectTag&&(null!==c.lastEffect?c.lastEffect.nextEffect=a:c.firstEffect=a,c.lastEffect=a))}else{a=mi(a,H);if(null!==a)return a.effectTag&=1023,a;null!==c&&(c.firstEffect=c.lastEffect=null,c.effectTag|=1024)}if(null!==d)return d;if(null!==c)a=c;else break}return null}function Qg(a){var b=hi(a.alternate,a,H);a.memoizedProps=a.pendingProps;null===b&&(b=Mg(a));Kg.current=null;return b}function Rg(a,b){Ca?n(\"243\"):void 0;eb();Ca=!0;var c=qe.current;qe.current=Ud;var d=a.nextExpirationTimeToWorkOn;\nif(d!==H||a!==Y||null===x)Jg(),Y=a,H=d,x=Ga(Y.current,null,H),a.pendingCommitExpirationTime=0;var e=!1;do{try{if(b)for(;null!==x&&!Nc();)x=Qg(x);else for(;null!==x;)x=Qg(x)}catch(v){if(Ob=Ja=Nb=null,Vd(),null===x)e=!0,ie(v);else{null===x?n(\"271\"):void 0;var f=x,g=f.return;if(null===g)e=!0,ie(v);else{a:{var h=a,l=g,k=f,m=v;g=H;k.effectTag|=1024;k.firstEffect=k.lastEffect=null;if(null!==m&&\"object\"===typeof m&&\"function\"===typeof m.then){var p=m;m=l;var t=-1,r=-1;do{if(13===m.tag){var u=m.alternate;\nif(null!==u&&(u=u.memoizedState,null!==u)){r=10*(1073741822-u.timedOutAt);break}u=m.pendingProps.maxDuration;if(\"number\"===typeof u)if(0>=u)t=0;else if(-1===t||u<t)t=u}m=m.return}while(null!==m);m=l;do{if(u=13===m.tag)u=void 0===m.memoizedProps.fallback?!1:null===m.memoizedState;if(u){l=m.updateQueue;null===l?(l=new Set,l.add(p),m.updateQueue=l):l.add(p);if(0===(m.mode&1)){m.effectTag|=64;k.effectTag&=-1957;1===k.tag&&(null===k.alternate?k.tag=17:(g=Aa(1073741823),g.tag=Ec,na(k,g)));k.expirationTime=\n1073741823;break a}k=h;l=g;var w=k.pingCache;null===w?(w=k.pingCache=new xi,u=new Set,w.set(p,u)):(u=w.get(p),void 0===u&&(u=new Set,w.set(p,u)));u.has(l)||(u.add(l),k=yi.bind(null,k,p,l),p.then(k,k));-1===t?h=1073741823:(-1===r&&(r=10*(1073741822-Wf(h,g))-5E3),h=r+t);0<=h&&La<h&&(La=h);m.effectTag|=2048;m.expirationTime=g;break a}m=m.return}while(null!==m);m=Error((sa(k.type)||\"A React component\")+\" suspended while rendering, but no fallback UI was specified.\\n\\nAdd a <Suspense fallback=...> component higher in the tree to provide a loading indicator or placeholder to display.\"+\nqd(k))}je=!0;m=Hc(m,k);h=l;do{switch(h.tag){case 3:h.effectTag|=2048;h.expirationTime=g;g=he(h,m,g);vg(h,g);break a;case 1:if(t=m,r=h.type,k=h.stateNode,0===(h.effectTag&64)&&(\"function\"===typeof r.getDerivedStateFromError||null!==k&&\"function\"===typeof k.componentDidCatch&&(null===Ba||!Ba.has(k)))){h.effectTag|=2048;h.expirationTime=g;g=Ig(h,t,g);vg(h,g);break a}}h=h.return}while(null!==h)}x=Mg(f);continue}}}break}while(1);Ca=!1;qe.current=c;Ob=Ja=Nb=null;Vd();if(e)Y=null,a.finishedWork=null;else if(null!==\nx)a.finishedWork=null;else{c=a.current.alternate;null===c?n(\"281\"):void 0;Y=null;if(je){e=a.latestPendingTime;f=a.latestSuspendedTime;g=a.latestPingedTime;if(0!==e&&e<d||0!==f&&f<d||0!==g&&g<d){Vf(a,d);re(a,c,d,a.expirationTime,-1);return}if(!a.didError&&b){a.didError=!0;d=a.nextExpirationTimeToWorkOn=d;b=a.expirationTime=1073741823;re(a,c,d,b,-1);return}}b&&-1!==La?(Vf(a,d),b=10*(1073741822-Wf(a,d)),b<La&&(La=b),b=10*(1073741822-ka()),b=La-b,re(a,c,d,a.expirationTime,0>b?0:b)):(a.pendingCommitExpirationTime=\nd,a.finishedWork=c)}}function Ka(a,b){for(var c=a.return;null!==c;){switch(c.tag){case 1:var d=c.stateNode;if(\"function\"===typeof c.type.getDerivedStateFromError||\"function\"===typeof d.componentDidCatch&&(null===Ba||!Ba.has(d))){a=Hc(b,a);a=Ig(c,a,1073741823);na(c,a);ya(c,1073741823);return}break;case 3:a=Hc(b,a);a=he(c,a,1073741823);na(c,a);ya(c,1073741823);return}c=c.return}3===a.tag&&(c=Hc(b,a),c=he(a,c,1073741823),na(a,c),ya(a,1073741823))}function fb(a,b){var c=zi(),d=void 0;if(0===(b.mode&1))d=\n1073741823;else if(Ca&&!Lc)d=H;else{switch(c){case se:d=1073741823;break;case te:d=1073741822-10*(((1073741822-a+15)/10|0)+1);break;case Lg:d=1073741822-25*(((1073741822-a+500)/25|0)+1);break;case Ai:case Bi:d=1;break;default:n(\"313\")}null!==Y&&d===H&&--d}c===te&&(0===oa||d<oa)&&(oa=d);return d}function yi(a,b,c){var d=a.pingCache;null!==d&&d.delete(b);if(null!==Y&&H===c)Y=null;else if(b=a.earliestSuspendedTime,d=a.latestSuspendedTime,0!==b&&c<=b&&c>=d){a.didError=!1;b=a.latestPingedTime;if(0===b||\nb>c)a.latestPingedTime=c;xc(c,a);c=a.expirationTime;0!==c&&Kc(a,c)}}function li(a,b){var c=a.stateNode;null!==c&&c.delete(b);b=ka();b=fb(b,a);a=Sg(a,b);null!==a&&(Bb(a,b),b=a.expirationTime,0!==b&&Kc(a,b))}function Sg(a,b){a.expirationTime<b&&(a.expirationTime=b);var c=a.alternate;null!==c&&c.expirationTime<b&&(c.expirationTime=b);var d=a.return,e=null;if(null===d&&3===a.tag)e=a.stateNode;else for(;null!==d;){c=d.alternate;d.childExpirationTime<b&&(d.childExpirationTime=b);null!==c&&c.childExpirationTime<\nb&&(c.childExpirationTime=b);if(null===d.return&&3===d.tag){e=d.stateNode;break}d=d.return}return e}function ya(a,b){a=Sg(a,b);null!==a&&(!Ca&&0!==H&&b>H&&Jg(),Bb(a,b),Ca&&!Lc&&Y===a||Kc(a,a.expirationTime),Tb>Ci&&(Tb=0,n(\"185\")))}function Tg(a,b,c,d,e){return Mc(se,function(){return a(b,c,d,e)})}function Ub(){aa=1073741822-((ue()-ve)/10|0)}function Ug(a,b){if(0!==Oc){if(b<Oc)return;null!==Pc&&Vg(Pc)}Oc=b;a=ue()-ve;Pc=Wg(Di,{timeout:10*(1073741822-b)-a})}function re(a,b,c,d,e){a.expirationTime=d;\n0!==e||Nc()?0<e&&(a.timeoutHandle=Ei(Fi.bind(null,a,b,c),e)):(a.pendingCommitExpirationTime=c,a.finishedWork=b)}function Fi(a,b,c){a.pendingCommitExpirationTime=c;a.finishedWork=b;Ub();jb=aa;Xg(a,c)}function wi(a,b){a.expirationTime=b;a.finishedWork=null}function ka(){if(w)return jb;Qc();if(0===C||1===C)Ub(),jb=aa;return jb}function Kc(a,b){null===a.nextScheduledRoot?(a.expirationTime=b,null===I?(ba=I=a,a.nextScheduledRoot=a):(I=I.nextScheduledRoot=a,I.nextScheduledRoot=ba)):b>a.expirationTime&&(a.expirationTime=\nb);w||(z?Rc&&(ca=a,C=1073741823,Sc(a,1073741823,!1)):1073741823===b?Z(1073741823,!1):Ug(a,b))}function Qc(){var a=0,b=null;if(null!==I)for(var c=I,d=ba;null!==d;){var e=d.expirationTime;if(0===e){null===c||null===I?n(\"244\"):void 0;if(d===d.nextScheduledRoot){ba=I=d.nextScheduledRoot=null;break}else if(d===ba)ba=e=d.nextScheduledRoot,I.nextScheduledRoot=e,d.nextScheduledRoot=null;else if(d===I){I=c;I.nextScheduledRoot=ba;d.nextScheduledRoot=null;break}else c.nextScheduledRoot=d.nextScheduledRoot,d.nextScheduledRoot=\nnull;d=c.nextScheduledRoot}else{e>a&&(a=e,b=d);if(d===I)break;if(1073741823===a)break;c=d;d=d.nextScheduledRoot}}ca=b;C=a}function Nc(){return Tc?!0:Gi()?Tc=!0:!1}function Di(){try{if(!Nc()&&null!==ba){Ub();var a=ba;do{var b=a.expirationTime;0!==b&&aa<=b&&(a.nextExpirationTimeToWorkOn=aa);a=a.nextScheduledRoot}while(a!==ba)}Z(0,!0)}finally{Tc=!1}}function Z(a,b){Qc();if(b)for(Ub(),jb=aa;null!==ca&&0!==C&&a<=C&&!(Tc&&aa>C);)Sc(ca,C,aa>C),Qc(),Ub(),jb=aa;else for(;null!==ca&&0!==C&&a<=C;)Sc(ca,C,!1),\nQc();b&&(Oc=0,Pc=null);0!==C&&Ug(ca,C);Tb=0;we=null;if(null!==kb)for(a=kb,kb=null,b=0;b<a.length;b++){var c=a[b];try{c._onComplete()}catch(d){lb||(lb=!0,Uc=d)}}if(lb)throw a=Uc,Uc=null,lb=!1,a;}function Xg(a,b){w?n(\"253\"):void 0;ca=a;C=b;Sc(a,b,!1);Z(1073741823,!1)}function Sc(a,b,c){w?n(\"245\"):void 0;w=!0;if(c){var d=a.finishedWork;null!==d?Vc(a,d,b):(a.finishedWork=null,d=a.timeoutHandle,-1!==d&&(a.timeoutHandle=-1,Yg(d)),Rg(a,c),d=a.finishedWork,null!==d&&(Nc()?a.finishedWork=d:Vc(a,d,b)))}else d=\na.finishedWork,null!==d?Vc(a,d,b):(a.finishedWork=null,d=a.timeoutHandle,-1!==d&&(a.timeoutHandle=-1,Yg(d)),Rg(a,c),d=a.finishedWork,null!==d&&Vc(a,d,b));w=!1}function Vc(a,b,c){var d=a.firstBatch;if(null!==d&&d._expirationTime>=c&&(null===kb?kb=[d]:kb.push(d),d._defer)){a.finishedWork=b;a.expirationTime=0;return}a.finishedWork=null;a===we?Tb++:(we=a,Tb=0);Mc(se,function(){ui(a,b)})}function ie(a){null===ca?n(\"246\"):void 0;ca.expirationTime=0;lb||(lb=!0,Uc=a)}function Zg(a,b){var c=z;z=!0;try{return a(b)}finally{(z=\nc)||w||Z(1073741823,!1)}}function $g(a,b){if(z&&!Rc){Rc=!0;try{return a(b)}finally{Rc=!1}}return a(b)}function ah(a,b,c){z||w||0===oa||(Z(oa,!1),oa=0);var d=z;z=!0;try{return Mc(te,function(){return a(b,c)})}finally{(z=d)||w||Z(1073741823,!1)}}function bh(a,b,c,d,e){var f=b.current;a:if(c){c=c._reactInternalFiber;b:{2===wb(c)&&1===c.tag?void 0:n(\"170\");var g=c;do{switch(g.tag){case 3:g=g.stateNode.context;break b;case 1:if(E(g.type)){g=g.stateNode.__reactInternalMemoizedMergedChildContext;break b}}g=\ng.return}while(null!==g);n(\"171\");g=void 0}if(1===c.tag){var h=c.type;if(E(h)){c=Rf(c,h,g);break a}}c=g}else c=va;null===b.context?b.context=c:b.pendingContext=c;b=e;e=Aa(d);e.payload={element:a};b=void 0===b?null:b;null!==b&&(e.callback=b);eb();na(f,e);ya(f,d);return d}function xe(a,b,c,d){var e=b.current,f=ka();e=fb(f,e);return bh(a,b,c,e,d)}function ye(a){a=a.current;if(!a.child)return null;switch(a.child.tag){case 5:return a.child.stateNode;default:return a.child.stateNode}}function Hi(a,b,c){var d=\n3<arguments.length&&void 0!==arguments[3]?arguments[3]:null;return{$$typeof:Va,key:null==d?null:\"\"+d,children:a,containerInfo:b,implementation:c}}function Vb(a){var b=1073741822-25*(((1073741822-ka()+500)/25|0)+1);b>=ze&&(b=ze-1);this._expirationTime=ze=b;this._root=a;this._callbacks=this._next=null;this._hasChildren=this._didComplete=!1;this._children=null;this._defer=!0}function mb(){this._callbacks=null;this._didCommit=!1;this._onCommit=this._onCommit.bind(this)}function nb(a,b,c){b=S(3,null,null,\nb?3:0);a={current:b,containerInfo:a,pendingChildren:null,pingCache:null,earliestPendingTime:0,latestPendingTime:0,earliestSuspendedTime:0,latestSuspendedTime:0,latestPingedTime:0,didError:!1,pendingCommitExpirationTime:0,finishedWork:null,timeoutHandle:-1,context:null,pendingContext:null,hydrate:c,nextExpirationTimeToWorkOn:0,expirationTime:0,firstBatch:null,nextScheduledRoot:null};this._internalRoot=b.stateNode=a}function ob(a){return!(!a||1!==a.nodeType&&9!==a.nodeType&&11!==a.nodeType&&(8!==a.nodeType||\n\" react-mount-point-unstable \"!==a.nodeValue))}function Ii(a,b){b||(b=a?9===a.nodeType?a.documentElement:a.firstChild:null,b=!(!b||1!==b.nodeType||!b.hasAttribute(\"data-reactroot\")));if(!b)for(var c;c=a.lastChild;)a.removeChild(c);return new nb(a,!1,b)}function Wc(a,b,c,d,e){var f=c._reactRootContainer;if(f){if(\"function\"===typeof e){var g=e;e=function(){var a=ye(f._internalRoot);g.call(a)}}null!=a?f.legacy_renderSubtreeIntoContainer(a,b,e):f.render(b,e)}else{f=c._reactRootContainer=Ii(c,d);if(\"function\"===\ntypeof e){var h=e;e=function(){var a=ye(f._internalRoot);h.call(a)}}$g(function(){null!=a?f.legacy_renderSubtreeIntoContainer(a,b,e):f.render(b,e)})}return ye(f._internalRoot)}function ch(a,b){var c=2<arguments.length&&void 0!==arguments[2]?arguments[2]:null;ob(b)?void 0:n(\"200\");return Hi(a,b,null,c)}da?void 0:n(\"227\");var sh=function(a,b,c,d,e,f,g,h,l){var k=Array.prototype.slice.call(arguments,3);try{b.apply(c,k)}catch(m){this.onError(m)}},qb=!1,$b=null,ac=!1,Yc=null,th={onError:function(a){qb=\n!0;$b=a}},bc=null,Na={},cc=[],Zc={},Oa={},$c={},bd=null,Ue=null,He=null,rb=null,vh=function(a){if(a){var b=a._dispatchListeners,c=a._dispatchInstances;if(Array.isArray(b))for(var d=0;d<b.length&&!a.isPropagationStopped();d++)Ge(a,b[d],c[d]);else b&&Ge(a,b,c);a._dispatchListeners=null;a._dispatchInstances=null;a.isPersistent()||a.constructor.release(a)}},Ae={injectEventPluginOrder:function(a){bc?n(\"101\"):void 0;bc=Array.prototype.slice.call(a);Ee()},injectEventPluginsByName:function(a){var b=!1,c;\nfor(c in a)if(a.hasOwnProperty(c)){var d=a[c];Na.hasOwnProperty(c)&&Na[c]===d||(Na[c]?n(\"102\",c):void 0,Na[c]=d,b=!0)}b&&Ee()}},dh=Math.random().toString(36).slice(2),ea=\"__reactInternalInstance$\"+dh,ec=\"__reactEventHandlers$\"+dh,ra=!(\"undefined\"===typeof window||!window.document||!window.document.createElement),Ra={animationend:fc(\"Animation\",\"AnimationEnd\"),animationiteration:fc(\"Animation\",\"AnimationIteration\"),animationstart:fc(\"Animation\",\"AnimationStart\"),transitionend:fc(\"Transition\",\"TransitionEnd\")},\nfd={},Le={};ra&&(Le=document.createElement(\"div\").style,\"AnimationEvent\"in window||(delete Ra.animationend.animation,delete Ra.animationiteration.animation,delete Ra.animationstart.animation),\"TransitionEvent\"in window||delete Ra.transitionend.transition);var eh=gc(\"animationend\"),fh=gc(\"animationiteration\"),gh=gc(\"animationstart\"),hh=gc(\"transitionend\"),zb=\"abort canplay canplaythrough durationchange emptied encrypted ended error loadeddata loadedmetadata loadstart pause play playing progress ratechange seeked seeking stalled suspend timeupdate volumechange waiting\".split(\" \"),\nqa=null,gd=null,hc=null,B=da.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.assign;B(J.prototype,{preventDefault:function(){this.defaultPrevented=!0;var a=this.nativeEvent;a&&(a.preventDefault?a.preventDefault():\"unknown\"!==typeof a.returnValue&&(a.returnValue=!1),this.isDefaultPrevented=ic)},stopPropagation:function(){var a=this.nativeEvent;a&&(a.stopPropagation?a.stopPropagation():\"unknown\"!==typeof a.cancelBubble&&(a.cancelBubble=!0),this.isPropagationStopped=ic)},persist:function(){this.isPersistent=\nic},isPersistent:jc,destructor:function(){var a=this.constructor.Interface,b;for(b in a)this[b]=null;this.nativeEvent=this._targetInst=this.dispatchConfig=null;this.isPropagationStopped=this.isDefaultPrevented=jc;this._dispatchInstances=this._dispatchListeners=null}});J.Interface={type:null,target:null,currentTarget:function(){return null},eventPhase:null,bubbles:null,cancelable:null,timeStamp:function(a){return a.timeStamp||Date.now()},defaultPrevented:null,isTrusted:null};J.extend=function(a){function b(){return c.apply(this,\narguments)}var c=this,d=function(){};d.prototype=c.prototype;d=new d;B(d,b.prototype);b.prototype=d;b.prototype.constructor=b;b.Interface=B({},c.Interface,a);b.extend=c.extend;Ne(b);return b};Ne(J);var Ji=J.extend({data:null}),Ki=J.extend({data:null}),Ah=[9,13,27,32],hd=ra&&\"CompositionEvent\"in window,Wb=null;ra&&\"documentMode\"in document&&(Wb=document.documentMode);var Li=ra&&\"TextEvent\"in window&&!Wb,Se=ra&&(!hd||Wb&&8<Wb&&11>=Wb),Re=String.fromCharCode(32),pa={beforeInput:{phasedRegistrationNames:{bubbled:\"onBeforeInput\",\ncaptured:\"onBeforeInputCapture\"},dependencies:[\"compositionend\",\"keypress\",\"textInput\",\"paste\"]},compositionEnd:{phasedRegistrationNames:{bubbled:\"onCompositionEnd\",captured:\"onCompositionEndCapture\"},dependencies:\"blur compositionend keydown keypress keyup mousedown\".split(\" \")},compositionStart:{phasedRegistrationNames:{bubbled:\"onCompositionStart\",captured:\"onCompositionStartCapture\"},dependencies:\"blur compositionstart keydown keypress keyup mousedown\".split(\" \")},compositionUpdate:{phasedRegistrationNames:{bubbled:\"onCompositionUpdate\",\ncaptured:\"onCompositionUpdateCapture\"},dependencies:\"blur compositionupdate keydown keypress keyup mousedown\".split(\" \")}},Qe=!1,Sa=!1,Mi={eventTypes:pa,extractEvents:function(a,b,c,d){var e=void 0;var f=void 0;if(hd)b:{switch(a){case \"compositionstart\":e=pa.compositionStart;break b;case \"compositionend\":e=pa.compositionEnd;break b;case \"compositionupdate\":e=pa.compositionUpdate;break b}e=void 0}else Sa?Oe(a,c)&&(e=pa.compositionEnd):\"keydown\"===a&&229===c.keyCode&&(e=pa.compositionStart);e?(Se&&\n\"ko\"!==c.locale&&(Sa||e!==pa.compositionStart?e===pa.compositionEnd&&Sa&&(f=Me()):(qa=d,gd=\"value\"in qa?qa.value:qa.textContent,Sa=!0)),e=Ji.getPooled(e,b,c,d),f?e.data=f:(f=Pe(c),null!==f&&(e.data=f)),Qa(e),f=e):f=null;(a=Li?Bh(a,c):Ch(a,c))?(b=Ki.getPooled(pa.beforeInput,b,c,d),b.data=a,Qa(b)):b=null;return null===f?b:null===b?f:[f,b]}},id=null,Ta=null,Ua=null,Ye=function(a,b){return a(b)},yf=function(a,b,c){return a(b,c)},Ze=function(){},jd=!1,Dh={color:!0,date:!0,datetime:!0,\"datetime-local\":!0,\nemail:!0,month:!0,number:!0,password:!0,range:!0,search:!0,tel:!0,text:!0,time:!0,url:!0,week:!0},Ma=da.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;Ma.hasOwnProperty(\"ReactCurrentDispatcher\")||(Ma.ReactCurrentDispatcher={current:null});var Fh=/^(.*)[\\\\\\/]/,O=\"function\"===typeof Symbol&&Symbol.for,Bc=O?Symbol.for(\"react.element\"):60103,Va=O?Symbol.for(\"react.portal\"):60106,ta=O?Symbol.for(\"react.fragment\"):60107,md=O?Symbol.for(\"react.strict_mode\"):60108,lc=O?Symbol.for(\"react.profiler\"):60114,\nff=O?Symbol.for(\"react.provider\"):60109,ef=O?Symbol.for(\"react.context\"):60110,ld=O?Symbol.for(\"react.concurrent_mode\"):60111,od=O?Symbol.for(\"react.forward_ref\"):60112,nd=O?Symbol.for(\"react.suspense\"):60113,pd=O?Symbol.for(\"react.memo\"):60115,gf=O?Symbol.for(\"react.lazy\"):60116,df=\"function\"===typeof Symbol&&Symbol.iterator,Hh=/^[:A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD][:A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD\\-.0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040]*$/,\nhf=Object.prototype.hasOwnProperty,kf={},jf={},A={};\"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style\".split(\" \").forEach(function(a){A[a]=new K(a,0,!1,a,null)});[[\"acceptCharset\",\"accept-charset\"],[\"className\",\"class\"],[\"htmlFor\",\"for\"],[\"httpEquiv\",\"http-equiv\"]].forEach(function(a){var b=a[0];A[b]=new K(b,1,!1,a[1],null)});[\"contentEditable\",\"draggable\",\"spellCheck\",\"value\"].forEach(function(a){A[a]=new K(a,2,!1,\na.toLowerCase(),null)});[\"autoReverse\",\"externalResourcesRequired\",\"focusable\",\"preserveAlpha\"].forEach(function(a){A[a]=new K(a,2,!1,a,null)});\"allowFullScreen async autoFocus autoPlay controls default defer disabled formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope\".split(\" \").forEach(function(a){A[a]=new K(a,3,!1,a.toLowerCase(),null)});[\"checked\",\"multiple\",\"muted\",\"selected\"].forEach(function(a){A[a]=new K(a,3,!0,a,null)});[\"capture\",\n\"download\"].forEach(function(a){A[a]=new K(a,4,!1,a,null)});[\"cols\",\"rows\",\"size\",\"span\"].forEach(function(a){A[a]=new K(a,6,!1,a,null)});[\"rowSpan\",\"start\"].forEach(function(a){A[a]=new K(a,5,!1,a.toLowerCase(),null)});var Be=/[\\-:]([a-z])/g,Ce=function(a){return a[1].toUpperCase()};\"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height\".split(\" \").forEach(function(a){var b=\na.replace(Be,Ce);A[b]=new K(b,1,!1,a,null)});\"xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type\".split(\" \").forEach(function(a){var b=a.replace(Be,Ce);A[b]=new K(b,1,!1,a,\"http://www.w3.org/1999/xlink\")});[\"xml:base\",\"xml:lang\",\"xml:space\"].forEach(function(a){var b=a.replace(Be,Ce);A[b]=new K(b,1,!1,a,\"http://www.w3.org/XML/1998/namespace\")});[\"tabIndex\",\"crossOrigin\"].forEach(function(a){A[a]=new K(a,1,!1,a.toLowerCase(),null)});var pf={change:{phasedRegistrationNames:{bubbled:\"onChange\",\ncaptured:\"onChangeCapture\"},dependencies:\"blur change click focus input keydown keyup selectionchange\".split(\" \")}},tb=null,ub=null,De=!1;ra&&(De=af(\"input\")&&(!document.documentMode||9<document.documentMode));var Ni={eventTypes:pf,_isInputEventSupported:De,extractEvents:function(a,b,c,d){var e=b?Da(b):window,f=void 0,g=void 0,h=e.nodeName&&e.nodeName.toLowerCase();\"select\"===h||\"input\"===h&&\"file\"===e.type?f=Lh:$e(e)?De?f=Ph:(f=Nh,g=Mh):(h=e.nodeName)&&\"input\"===h.toLowerCase()&&(\"checkbox\"===e.type||\n\"radio\"===e.type)&&(f=Oh);if(f&&(f=f(a,b)))return of(f,c,d);g&&g(a,e,b);\"blur\"===a&&(a=e._wrapperState)&&a.controlled&&\"number\"===e.type&&ud(e,\"number\",e.value)}},Xb=J.extend({view:null,detail:null}),Rh={Alt:\"altKey\",Control:\"ctrlKey\",Meta:\"metaKey\",Shift:\"shiftKey\"},ih=0,jh=0,kh=!1,lh=!1,Yb=Xb.extend({screenX:null,screenY:null,clientX:null,clientY:null,pageX:null,pageY:null,ctrlKey:null,shiftKey:null,altKey:null,metaKey:null,getModifierState:vd,button:null,buttons:null,relatedTarget:function(a){return a.relatedTarget||\n(a.fromElement===a.srcElement?a.toElement:a.fromElement)},movementX:function(a){if(\"movementX\"in a)return a.movementX;var b=ih;ih=a.screenX;return kh?\"mousemove\"===a.type?a.screenX-b:0:(kh=!0,0)},movementY:function(a){if(\"movementY\"in a)return a.movementY;var b=jh;jh=a.screenY;return lh?\"mousemove\"===a.type?a.screenY-b:0:(lh=!0,0)}}),mh=Yb.extend({pointerId:null,width:null,height:null,pressure:null,tangentialPressure:null,tiltX:null,tiltY:null,twist:null,pointerType:null,isPrimary:null}),Zb={mouseEnter:{registrationName:\"onMouseEnter\",\ndependencies:[\"mouseout\",\"mouseover\"]},mouseLeave:{registrationName:\"onMouseLeave\",dependencies:[\"mouseout\",\"mouseover\"]},pointerEnter:{registrationName:\"onPointerEnter\",dependencies:[\"pointerout\",\"pointerover\"]},pointerLeave:{registrationName:\"onPointerLeave\",dependencies:[\"pointerout\",\"pointerover\"]}},Oi={eventTypes:Zb,extractEvents:function(a,b,c,d){var e=\"mouseover\"===a||\"pointerover\"===a,f=\"mouseout\"===a||\"pointerout\"===a;if(e&&(c.relatedTarget||c.fromElement)||!f&&!e)return null;e=d.window===\nd?d:(e=d.ownerDocument)?e.defaultView||e.parentWindow:window;f?(f=b,b=(b=c.relatedTarget||c.toElement)?dc(b):null):f=null;if(f===b)return null;var g=void 0,h=void 0,l=void 0,k=void 0;if(\"mouseout\"===a||\"mouseover\"===a)g=Yb,h=Zb.mouseLeave,l=Zb.mouseEnter,k=\"mouse\";else if(\"pointerout\"===a||\"pointerover\"===a)g=mh,h=Zb.pointerLeave,l=Zb.pointerEnter,k=\"pointer\";var m=null==f?e:Da(f);e=null==b?e:Da(b);a=g.getPooled(h,f,c,d);a.type=k+\"leave\";a.target=m;a.relatedTarget=e;c=g.getPooled(l,b,c,d);c.type=\nk+\"enter\";c.target=e;c.relatedTarget=m;d=b;if(f&&d)a:{b=f;e=d;k=0;for(g=b;g;g=fa(g))k++;g=0;for(l=e;l;l=fa(l))g++;for(;0<k-g;)b=fa(b),k--;for(;0<g-k;)e=fa(e),g--;for(;k--;){if(b===e||b===e.alternate)break a;b=fa(b);e=fa(e)}b=null}else b=null;e=b;for(b=[];f&&f!==e;){k=f.alternate;if(null!==k&&k===e)break;b.push(f);f=fa(f)}for(f=[];d&&d!==e;){k=d.alternate;if(null!==k&&k===e)break;f.push(d);d=fa(d)}for(d=0;d<b.length;d++)ed(b[d],\"bubbled\",a);for(d=f.length;0<d--;)ed(f[d],\"captured\",c);return[a,c]}},\nSh=Object.prototype.hasOwnProperty,Pi=J.extend({animationName:null,elapsedTime:null,pseudoElement:null}),Qi=J.extend({clipboardData:function(a){return\"clipboardData\"in a?a.clipboardData:window.clipboardData}}),Ri=Xb.extend({relatedTarget:null}),Si={Esc:\"Escape\",Spacebar:\" \",Left:\"ArrowLeft\",Up:\"ArrowUp\",Right:\"ArrowRight\",Down:\"ArrowDown\",Del:\"Delete\",Win:\"OS\",Menu:\"ContextMenu\",Apps:\"ContextMenu\",Scroll:\"ScrollLock\",MozPrintableKey:\"Unidentified\"},Ti={8:\"Backspace\",9:\"Tab\",12:\"Clear\",13:\"Enter\",\n16:\"Shift\",17:\"Control\",18:\"Alt\",19:\"Pause\",20:\"CapsLock\",27:\"Escape\",32:\" \",33:\"PageUp\",34:\"PageDown\",35:\"End\",36:\"Home\",37:\"ArrowLeft\",38:\"ArrowUp\",39:\"ArrowRight\",40:\"ArrowDown\",45:\"Insert\",46:\"Delete\",112:\"F1\",113:\"F2\",114:\"F3\",115:\"F4\",116:\"F5\",117:\"F6\",118:\"F7\",119:\"F8\",120:\"F9\",121:\"F10\",122:\"F11\",123:\"F12\",144:\"NumLock\",145:\"ScrollLock\",224:\"Meta\"},Ui=Xb.extend({key:function(a){if(a.key){var b=Si[a.key]||a.key;if(\"Unidentified\"!==b)return b}return\"keypress\"===a.type?(a=nc(a),13===a?\"Enter\":\nString.fromCharCode(a)):\"keydown\"===a.type||\"keyup\"===a.type?Ti[a.keyCode]||\"Unidentified\":\"\"},location:null,ctrlKey:null,shiftKey:null,altKey:null,metaKey:null,repeat:null,locale:null,getModifierState:vd,charCode:function(a){return\"keypress\"===a.type?nc(a):0},keyCode:function(a){return\"keydown\"===a.type||\"keyup\"===a.type?a.keyCode:0},which:function(a){return\"keypress\"===a.type?nc(a):\"keydown\"===a.type||\"keyup\"===a.type?a.keyCode:0}}),Vi=Yb.extend({dataTransfer:null}),Wi=Xb.extend({touches:null,targetTouches:null,\nchangedTouches:null,altKey:null,metaKey:null,ctrlKey:null,shiftKey:null,getModifierState:vd}),Xi=J.extend({propertyName:null,elapsedTime:null,pseudoElement:null}),Yi=Yb.extend({deltaX:function(a){return\"deltaX\"in a?a.deltaX:\"wheelDeltaX\"in a?-a.wheelDeltaX:0},deltaY:function(a){return\"deltaY\"in a?a.deltaY:\"wheelDeltaY\"in a?-a.wheelDeltaY:\"wheelDelta\"in a?-a.wheelDelta:0},deltaZ:null,deltaMode:null}),Zi=[[\"abort\",\"abort\"],[eh,\"animationEnd\"],[fh,\"animationIteration\"],[gh,\"animationStart\"],[\"canplay\",\n\"canPlay\"],[\"canplaythrough\",\"canPlayThrough\"],[\"drag\",\"drag\"],[\"dragenter\",\"dragEnter\"],[\"dragexit\",\"dragExit\"],[\"dragleave\",\"dragLeave\"],[\"dragover\",\"dragOver\"],[\"durationchange\",\"durationChange\"],[\"emptied\",\"emptied\"],[\"encrypted\",\"encrypted\"],[\"ended\",\"ended\"],[\"error\",\"error\"],[\"gotpointercapture\",\"gotPointerCapture\"],[\"load\",\"load\"],[\"loadeddata\",\"loadedData\"],[\"loadedmetadata\",\"loadedMetadata\"],[\"loadstart\",\"loadStart\"],[\"lostpointercapture\",\"lostPointerCapture\"],[\"mousemove\",\"mouseMove\"],\n[\"mouseout\",\"mouseOut\"],[\"mouseover\",\"mouseOver\"],[\"playing\",\"playing\"],[\"pointermove\",\"pointerMove\"],[\"pointerout\",\"pointerOut\"],[\"pointerover\",\"pointerOver\"],[\"progress\",\"progress\"],[\"scroll\",\"scroll\"],[\"seeking\",\"seeking\"],[\"stalled\",\"stalled\"],[\"suspend\",\"suspend\"],[\"timeupdate\",\"timeUpdate\"],[\"toggle\",\"toggle\"],[\"touchmove\",\"touchMove\"],[hh,\"transitionEnd\"],[\"waiting\",\"waiting\"],[\"wheel\",\"wheel\"]],vf={},wd={};[[\"blur\",\"blur\"],[\"cancel\",\"cancel\"],[\"click\",\"click\"],[\"close\",\"close\"],[\"contextmenu\",\n\"contextMenu\"],[\"copy\",\"copy\"],[\"cut\",\"cut\"],[\"auxclick\",\"auxClick\"],[\"dblclick\",\"doubleClick\"],[\"dragend\",\"dragEnd\"],[\"dragstart\",\"dragStart\"],[\"drop\",\"drop\"],[\"focus\",\"focus\"],[\"input\",\"input\"],[\"invalid\",\"invalid\"],[\"keydown\",\"keyDown\"],[\"keypress\",\"keyPress\"],[\"keyup\",\"keyUp\"],[\"mousedown\",\"mouseDown\"],[\"mouseup\",\"mouseUp\"],[\"paste\",\"paste\"],[\"pause\",\"pause\"],[\"play\",\"play\"],[\"pointercancel\",\"pointerCancel\"],[\"pointerdown\",\"pointerDown\"],[\"pointerup\",\"pointerUp\"],[\"ratechange\",\"rateChange\"],[\"reset\",\n\"reset\"],[\"seeked\",\"seeked\"],[\"submit\",\"submit\"],[\"touchcancel\",\"touchCancel\"],[\"touchend\",\"touchEnd\"],[\"touchstart\",\"touchStart\"],[\"volumechange\",\"volumeChange\"]].forEach(function(a){uf(a,!0)});Zi.forEach(function(a){uf(a,!1)});var nh={eventTypes:vf,isInteractiveTopLevelEventType:function(a){a=wd[a];return void 0!==a&&!0===a.isInteractive},extractEvents:function(a,b,c,d){var e=wd[a];if(!e)return null;switch(a){case \"keypress\":if(0===nc(c))return null;case \"keydown\":case \"keyup\":a=Ui;break;case \"blur\":case \"focus\":a=\nRi;break;case \"click\":if(2===c.button)return null;case \"auxclick\":case \"dblclick\":case \"mousedown\":case \"mousemove\":case \"mouseup\":case \"mouseout\":case \"mouseover\":case \"contextmenu\":a=Yb;break;case \"drag\":case \"dragend\":case \"dragenter\":case \"dragexit\":case \"dragleave\":case \"dragover\":case \"dragstart\":case \"drop\":a=Vi;break;case \"touchcancel\":case \"touchend\":case \"touchmove\":case \"touchstart\":a=Wi;break;case eh:case fh:case gh:a=Pi;break;case hh:a=Xi;break;case \"scroll\":a=Xb;break;case \"wheel\":a=\nYi;break;case \"copy\":case \"cut\":case \"paste\":a=Qi;break;case \"gotpointercapture\":case \"lostpointercapture\":case \"pointercancel\":case \"pointerdown\":case \"pointermove\":case \"pointerout\":case \"pointerover\":case \"pointerup\":a=mh;break;default:a=J}b=a.getPooled(e,b,c,d);Qa(b);return b}},wf=nh.isInteractiveTopLevelEventType,rc=[],qc=!0,Af={},Vh=0,sc=\"_reactListenersID\"+(\"\"+Math.random()).slice(2),$i=ra&&\"documentMode\"in document&&11>=document.documentMode,Hf={select:{phasedRegistrationNames:{bubbled:\"onSelect\",\ncaptured:\"onSelectCapture\"},dependencies:\"blur contextmenu dragend focus keydown keyup mousedown mouseup selectionchange\".split(\" \")}},Wa=null,Ad=null,xb=null,zd=!1,aj={eventTypes:Hf,extractEvents:function(a,b,c,d){var e=d.window===d?d.document:9===d.nodeType?d:d.ownerDocument,f;if(!(f=!e)){a:{e=zf(e);f=$c.onSelect;for(var g=0;g<f.length;g++){var h=f[g];if(!e.hasOwnProperty(h)||!e[h]){e=!1;break a}}e=!0}f=!e}if(f)return null;e=b?Da(b):window;switch(a){case \"focus\":if($e(e)||\"true\"===e.contentEditable)Wa=\ne,Ad=b,xb=null;break;case \"blur\":xb=Ad=Wa=null;break;case \"mousedown\":zd=!0;break;case \"contextmenu\":case \"mouseup\":case \"dragend\":return zd=!1,Gf(c,d);case \"selectionchange\":if($i)break;case \"keydown\":case \"keyup\":return Gf(c,d)}return null}};Ae.injectEventPluginOrder(\"ResponderEventPlugin SimpleEventPlugin EnterLeaveEventPlugin ChangeEventPlugin SelectEventPlugin BeforeInputEventPlugin\".split(\" \"));(function(a,b,c){bd=a;Ue=b;He=c})(dd,Je,Da);Ae.injectEventPluginsByName({SimpleEventPlugin:nh,EnterLeaveEventPlugin:Oi,\nChangeEventPlugin:Ni,SelectEventPlugin:aj,BeforeInputEventPlugin:Mi});var Xc=void 0,Of=function(a){return\"undefined\"!==typeof MSApp&&MSApp.execUnsafeLocalFunction?function(b,c,d,e){MSApp.execUnsafeLocalFunction(function(){return a(b,c,d,e)})}:a}(function(a,b){if(\"http://www.w3.org/2000/svg\"!==a.namespaceURI||\"innerHTML\"in a)a.innerHTML=b;else{Xc=Xc||document.createElement(\"div\");Xc.innerHTML=\"<svg>\"+b+\"</svg>\";for(b=Xc.firstChild;a.firstChild;)a.removeChild(a.firstChild);for(;b.firstChild;)a.appendChild(b.firstChild)}}),\nAb=function(a,b){if(b){var c=a.firstChild;if(c&&c===a.lastChild&&3===c.nodeType){c.nodeValue=b;return}}a.textContent=b},yb={animationIterationCount:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,\nlineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},bj=[\"Webkit\",\"ms\",\"Moz\",\"O\"];Object.keys(yb).forEach(function(a){bj.forEach(function(b){b=b+a.charAt(0).toUpperCase()+a.substring(1);yb[b]=yb[a]})});var Zh=B({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,\nsource:!0,track:!0,wbr:!0}),R=da.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler,Vg=R.unstable_cancelCallback,ue=R.unstable_now,Wg=R.unstable_scheduleCallback,Gi=R.unstable_shouldYield,Mc=R.unstable_runWithPriority,zi=R.unstable_getCurrentPriorityLevel,se=R.unstable_ImmediatePriority,te=R.unstable_UserBlockingPriority,Lg=R.unstable_NormalPriority,Ai=R.unstable_LowPriority,Bi=R.unstable_IdlePriority,ne=null,oe=null,Ei=\"function\"===typeof setTimeout?setTimeout:void 0,Yg=\"function\"===typeof clearTimeout?\nclearTimeout:void 0,vi=Wg,ti=Vg;new Set;var Id=[],Ya=-1,va={},F={current:va},M={current:!1},Fa=va,Kd=null,Ld=null,S=function(a,b,c,d){return new bi(a,b,c,d)},$f=(new da.Component).refs,zc={isMounted:function(a){return(a=a._reactInternalFiber)?2===wb(a):!1},enqueueSetState:function(a,b,c){a=a._reactInternalFiber;var d=ka();d=fb(d,a);var e=Aa(d);e.payload=b;void 0!==c&&null!==c&&(e.callback=c);eb();na(a,e);ya(a,d)},enqueueReplaceState:function(a,b,c){a=a._reactInternalFiber;var d=ka();d=fb(d,a);var e=\nAa(d);e.tag=yg;e.payload=b;void 0!==c&&null!==c&&(e.callback=c);eb();na(a,e);ya(a,d)},enqueueForceUpdate:function(a,b){a=a._reactInternalFiber;var c=ka();c=fb(c,a);var d=Aa(c);d.tag=Ec;void 0!==b&&null!==b&&(d.callback=b);eb();na(a,d);ya(a,c)}},Cc=Array.isArray,hb=ag(!0),ae=ag(!1),Eb={},U={current:Eb},Gb={current:Eb},Fb={current:Eb},db=0,pi=2,Rb=4,ji=8,ri=16,Sb=32,me=64,le=128,Dc=Ma.ReactCurrentDispatcher,Hb=0,xa=null,y=null,W=null,bb=null,G=null,ab=null,Kb=0,X=null,Lb=0,Ib=!1,ia=null,Jb=0,Ud={readContext:T,\nuseCallback:V,useContext:V,useEffect:V,useImperativeHandle:V,useLayoutEffect:V,useMemo:V,useReducer:V,useRef:V,useState:V,useDebugValue:V},fi={readContext:T,useCallback:function(a,b){cb().memoizedState=[a,void 0===b?null:b];return a},useContext:T,useEffect:function(a,b){return Xd(516,le|me,a,b)},useImperativeHandle:function(a,b,c){c=null!==c&&void 0!==c?c.concat([a]):null;return Xd(4,Rb|Sb,fg.bind(null,b,a),c)},useLayoutEffect:function(a,b){return Xd(4,Rb|Sb,a,b)},useMemo:function(a,b){var c=cb();\nb=void 0===b?null:b;a=a();c.memoizedState=[a,b];return a},useReducer:function(a,b,c){var d=cb();b=void 0!==c?c(b):b;d.memoizedState=d.baseState=b;a=d.queue={last:null,dispatch:null,lastRenderedReducer:a,lastRenderedState:b};a=a.dispatch=hg.bind(null,xa,a);return[d.memoizedState,a]},useRef:function(a){var b=cb();a={current:a};return b.memoizedState=a},useState:function(a){var b=cb();\"function\"===typeof a&&(a=a());b.memoizedState=b.baseState=a;a=b.queue={last:null,dispatch:null,lastRenderedReducer:dg,\nlastRenderedState:a};a=a.dispatch=hg.bind(null,xa,a);return[b.memoizedState,a]},useDebugValue:gg},cg={readContext:T,useCallback:function(a,b){var c=Mb();b=void 0===b?null:b;var d=c.memoizedState;if(null!==d&&null!==b&&Sd(b,d[1]))return d[0];c.memoizedState=[a,b];return a},useContext:T,useEffect:function(a,b){return Yd(516,le|me,a,b)},useImperativeHandle:function(a,b,c){c=null!==c&&void 0!==c?c.concat([a]):null;return Yd(4,Rb|Sb,fg.bind(null,b,a),c)},useLayoutEffect:function(a,b){return Yd(4,Rb|Sb,\na,b)},useMemo:function(a,b){var c=Mb();b=void 0===b?null:b;var d=c.memoizedState;if(null!==d&&null!==b&&Sd(b,d[1]))return d[0];a=a();c.memoizedState=[a,b];return a},useReducer:eg,useRef:function(a){return Mb().memoizedState},useState:function(a){return eg(dg,a)},useDebugValue:gg},la=null,gb=null,Ia=!1,gi=Ma.ReactCurrentOwner,ja=!1,de={current:null},Nb=null,Ja=null,Ob=null,ug=0,yg=1,Ec=2,ge=3,za=!1,Og=void 0,pe=void 0,Ng=void 0,Pg=void 0;Og=function(a,b,c,d){for(c=b.child;null!==c;){if(5===c.tag||\n6===c.tag)a.appendChild(c.stateNode);else if(4!==c.tag&&null!==c.child){c.child.return=c;c=c.child;continue}if(c===b)break;for(;null===c.sibling;){if(null===c.return||c.return===b)return;c=c.return}c.sibling.return=c.return;c=c.sibling}};pe=function(a){};Ng=function(a,b,c,d,e){var f=a.memoizedProps;if(f!==d){var g=b.stateNode;Ha(U.current);a=null;switch(c){case \"input\":f=sd(g,f);d=sd(g,d);a=[];break;case \"option\":f=Bd(g,f);d=Bd(g,d);a=[];break;case \"select\":f=B({},f,{value:void 0});d=B({},d,{value:void 0});\na=[];break;case \"textarea\":f=Cd(g,f);d=Cd(g,d);a=[];break;default:\"function\"!==typeof f.onClick&&\"function\"===typeof d.onClick&&(g.onclick=tc)}Ed(c,d);g=c=void 0;var h=null;for(c in f)if(!d.hasOwnProperty(c)&&f.hasOwnProperty(c)&&null!=f[c])if(\"style\"===c){var l=f[c];for(g in l)l.hasOwnProperty(g)&&(h||(h={}),h[g]=\"\")}else\"dangerouslySetInnerHTML\"!==c&&\"children\"!==c&&\"suppressContentEditableWarning\"!==c&&\"suppressHydrationWarning\"!==c&&\"autoFocus\"!==c&&(Oa.hasOwnProperty(c)?a||(a=[]):(a=a||[]).push(c,\nnull));for(c in d){var k=d[c];l=null!=f?f[c]:void 0;if(d.hasOwnProperty(c)&&k!==l&&(null!=k||null!=l))if(\"style\"===c)if(l){for(g in l)!l.hasOwnProperty(g)||k&&k.hasOwnProperty(g)||(h||(h={}),h[g]=\"\");for(g in k)k.hasOwnProperty(g)&&l[g]!==k[g]&&(h||(h={}),h[g]=k[g])}else h||(a||(a=[]),a.push(c,h)),h=k;else\"dangerouslySetInnerHTML\"===c?(k=k?k.__html:void 0,l=l?l.__html:void 0,null!=k&&l!==k&&(a=a||[]).push(c,\"\"+k)):\"children\"===c?l===k||\"string\"!==typeof k&&\"number\"!==typeof k||(a=a||[]).push(c,\"\"+\nk):\"suppressContentEditableWarning\"!==c&&\"suppressHydrationWarning\"!==c&&(Oa.hasOwnProperty(c)?(null!=k&&ha(e,c),a||l===k||(a=[])):(a=a||[]).push(c,k))}h&&(a=a||[]).push(\"style\",h);e=a;(b.updateQueue=e)&&Pb(b)}};Pg=function(a,b,c,d){c!==d&&Pb(b)};var ki=\"function\"===typeof WeakSet?WeakSet:Set,xi=\"function\"===typeof WeakMap?WeakMap:Map,qe=Ma.ReactCurrentDispatcher,Kg=Ma.ReactCurrentOwner,ze=1073741822,Ca=!1,x=null,Y=null,H=0,La=-1,je=!1,p=null,Lc=!1,ke=null,Jc=null,Ic=null,Ba=null,ba=null,I=null,Oc=\n0,Pc=void 0,w=!1,ca=null,C=0,oa=0,lb=!1,Uc=null,z=!1,Rc=!1,kb=null,ve=ue(),aa=1073741822-(ve/10|0),jb=aa,Ci=50,Tb=0,we=null,Tc=!1;id=function(a,b,c){switch(b){case \"input\":td(a,c);b=c.name;if(\"radio\"===c.type&&null!=b){for(c=a;c.parentNode;)c=c.parentNode;c=c.querySelectorAll(\"input[name=\"+JSON.stringify(\"\"+b)+'][type=\"radio\"]');for(b=0;b<c.length;b++){var d=c[b];if(d!==a&&d.form===a.form){var e=dd(d);e?void 0:n(\"90\");cf(d);td(d,e)}}}break;case \"textarea\":Jf(a,c);break;case \"select\":b=c.value,null!=\nb&&Xa(a,!!c.multiple,b,!1)}};Vb.prototype.render=function(a){this._defer?void 0:n(\"250\");this._hasChildren=!0;this._children=a;var b=this._root._internalRoot,c=this._expirationTime,d=new mb;bh(a,b,null,c,d._onCommit);return d};Vb.prototype.then=function(a){if(this._didComplete)a();else{var b=this._callbacks;null===b&&(b=this._callbacks=[]);b.push(a)}};Vb.prototype.commit=function(){var a=this._root._internalRoot,b=a.firstBatch;this._defer&&null!==b?void 0:n(\"251\");if(this._hasChildren){var c=this._expirationTime;\nif(b!==this){this._hasChildren&&(c=this._expirationTime=b._expirationTime,this.render(this._children));for(var d=null,e=b;e!==this;)d=e,e=e._next;null===d?n(\"251\"):void 0;d._next=e._next;this._next=b;a.firstBatch=this}this._defer=!1;Xg(a,c);b=this._next;this._next=null;b=a.firstBatch=b;null!==b&&b._hasChildren&&b.render(b._children)}else this._next=null,this._defer=!1};Vb.prototype._onComplete=function(){if(!this._didComplete){this._didComplete=!0;var a=this._callbacks;if(null!==a)for(var b=0;b<a.length;b++)(0,a[b])()}};\nmb.prototype.then=function(a){if(this._didCommit)a();else{var b=this._callbacks;null===b&&(b=this._callbacks=[]);b.push(a)}};mb.prototype._onCommit=function(){if(!this._didCommit){this._didCommit=!0;var a=this._callbacks;if(null!==a)for(var b=0;b<a.length;b++){var c=a[b];\"function\"!==typeof c?n(\"191\",c):void 0;c()}}};nb.prototype.render=function(a,b){var c=this._internalRoot,d=new mb;b=void 0===b?null:b;null!==b&&d.then(b);xe(a,c,null,d._onCommit);return d};nb.prototype.unmount=function(a){var b=\nthis._internalRoot,c=new mb;a=void 0===a?null:a;null!==a&&c.then(a);xe(null,b,null,c._onCommit);return c};nb.prototype.legacy_renderSubtreeIntoContainer=function(a,b,c){var d=this._internalRoot,e=new mb;c=void 0===c?null:c;null!==c&&e.then(c);xe(b,d,a,e._onCommit);return e};nb.prototype.createBatch=function(){var a=new Vb(this),b=a._expirationTime,c=this._internalRoot,d=c.firstBatch;if(null===d)c.firstBatch=a,a._next=null;else{for(c=null;null!==d&&d._expirationTime>=b;)c=d,d=d._next;a._next=d;null!==\nc&&(c._next=a)}return a};(function(a,b,c){Ye=a;yf=b;Ze=c})(Zg,ah,function(){w||0===oa||(Z(oa,!1),oa=0)});var oh={createPortal:ch,findDOMNode:function(a){if(null==a)return null;if(1===a.nodeType)return a;var b=a._reactInternalFiber;void 0===b&&(\"function\"===typeof a.render?n(\"188\"):n(\"268\",Object.keys(a)));a=tf(b);a=null===a?null:a.stateNode;return a},hydrate:function(a,b,c){ob(b)?void 0:n(\"200\");return Wc(null,a,b,!0,c)},render:function(a,b,c){ob(b)?void 0:n(\"200\");return Wc(null,a,b,!1,c)},unstable_renderSubtreeIntoContainer:function(a,\nb,c,d){ob(c)?void 0:n(\"200\");null==a||void 0===a._reactInternalFiber?n(\"38\"):void 0;return Wc(a,b,c,!1,d)},unmountComponentAtNode:function(a){ob(a)?void 0:n(\"40\");return a._reactRootContainer?($g(function(){Wc(null,null,a,!1,function(){a._reactRootContainer=null})}),!0):!1},unstable_createPortal:function(){return ch.apply(void 0,arguments)},unstable_batchedUpdates:Zg,unstable_interactiveUpdates:ah,flushSync:function(a,b){w?n(\"187\"):void 0;var c=z;z=!0;try{return Tg(a,b)}finally{z=c,Z(1073741823,!1)}},\nunstable_createRoot:function(a,b){ob(a)?void 0:n(\"299\",\"unstable_createRoot\");return new nb(a,!0,null!=b&&!0===b.hydrate)},unstable_flushControlled:function(a){var b=z;z=!0;try{Tg(a)}finally{(z=b)||w||Z(1073741823,!1)}},__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED:{Events:[Je,Da,dd,Ae.injectEventPluginsByName,Zc,Qa,function(a){ad(a,xh)},Ve,We,oc,cd]}};(function(a){var b=a.findFiberByHostInstance;return ai(B({},a,{overrideProps:null,currentDispatcherRef:Ma.ReactCurrentDispatcher,findHostInstanceByFiber:function(a){a=\ntf(a);return null===a?null:a.stateNode},findFiberByHostInstance:function(a){return b?b(a):null}}))})({findFiberByHostInstance:dc,bundleType:0,version:\"16.8.6\",rendererPackageName:\"react-dom\"});var ph={default:oh},qh=ph&&oh||ph;return qh.default||qh});\n"
  },
  {
    "path": "vendor/react-redux.js",
    "content": "!function(e,t){\"object\"==typeof exports&&\"undefined\"!=typeof module?t(exports,require(\"react\"),require(\"redux\"),require(\"react-dom\")):\"function\"==typeof define&&define.amd?define([\"exports\",\"react\",\"redux\",\"react-dom\"],t):t((e=e||self).ReactRedux={},e.React,e.Redux,e.ReactDOM)}(this,function(e,t,r,n){\"use strict\";var o=\"default\"in t?t.default:t;function i(e,t){return e(t={exports:{}},t.exports),t.exports}var s=\"SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED\";function u(){}function a(){}a.resetWarningCache=u;var c=i(function(e){e.exports=function(){function e(e,t,r,n,o,i){if(i!==s){var u=Error(\"Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types\");throw u.name=\"Invariant Violation\",u}}function t(){return e}e.isRequired=e;var r={array:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:t,element:e,elementType:e,instanceOf:t,node:e,objectOf:t,oneOf:t,oneOfType:t,shape:t,exact:t,checkPropTypes:a,resetWarningCache:u};return r.PropTypes=r,r}()}),p=o.createContext(null);var f=function(e){e()},d=function(){return f},l=null,y={notify:function(){}};var h=function(){function e(e,t){this.store=e,this.parentSub=t,this.unsubscribe=null,this.listeners=y,this.handleChangeWrapper=this.handleChangeWrapper.bind(this)}var t=e.prototype;return t.addNestedSub=function(e){return this.trySubscribe(),this.listeners.subscribe(e)},t.notifyNestedSubs=function(){this.listeners.notify()},t.handleChangeWrapper=function(){this.onStateChange&&this.onStateChange()},t.isSubscribed=function(){return!!this.unsubscribe},t.trySubscribe=function(){var e,t,r;this.unsubscribe||(this.unsubscribe=this.parentSub?this.parentSub.addNestedSub(this.handleChangeWrapper):this.store.subscribe(this.handleChangeWrapper),this.listeners=(e=d(),t=[],r=[],{clear:function(){r=l,t=l},notify:function(){var n=t=r;e(function(){for(var e=0;n.length>e;e++)n[e]()})},get:function(){return r},subscribe:function(e){var n=!0;return r===t&&(r=t.slice()),r.push(e),function(){n&&t!==l&&(n=!1,r===t&&(r=t.slice()),r.splice(r.indexOf(e),1))}}}))},t.tryUnsubscribe=function(){this.unsubscribe&&(this.unsubscribe(),this.unsubscribe=null,this.listeners.clear(),this.listeners=y)},e}(),b=function(e){var t,r;function n(t){var r;r=e.call(this,t)||this;var n=t.store;r.notifySubscribers=r.notifySubscribers.bind(function(e){if(void 0===e)throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\");return e}(r));var o=new h(n);return o.onStateChange=r.notifySubscribers,r.state={store:n,subscription:o},r.previousState=n.getState(),r}(t=n).prototype=Object.create((r=e).prototype),t.prototype.constructor=t,t.__proto__=r;var i=n.prototype;return i.componentDidMount=function(){this._isMounted=!0,this.state.subscription.trySubscribe(),this.previousState!==this.props.store.getState()&&this.state.subscription.notifyNestedSubs()},i.componentWillUnmount=function(){this.unsubscribe&&this.unsubscribe(),this.state.subscription.tryUnsubscribe(),this._isMounted=!1},i.componentDidUpdate=function(e){if(this.props.store!==e.store){this.state.subscription.tryUnsubscribe();var t=new h(this.props.store);t.onStateChange=this.notifySubscribers,this.setState({store:this.props.store,subscription:t})}},i.notifySubscribers=function(){this.state.subscription.notifyNestedSubs()},i.render=function(){return o.createElement((this.props.context||p).Provider,{value:this.state},this.props.children)},n}(t.Component);function m(){return(m=Object.assign||function(e){for(var t=1;arguments.length>t;t++){var r=arguments[t];for(var n in r)Object.prototype.hasOwnProperty.call(r,n)&&(e[n]=r[n])}return e}).apply(this,arguments)}function v(e,t){if(null==e)return{};var r,n,o={},i=Object.keys(e);for(n=0;i.length>n;n++)0>t.indexOf(r=i[n])&&(o[r]=e[r]);return o}b.propTypes={store:c.shape({subscribe:c.func.isRequired,dispatch:c.func.isRequired,getState:c.func.isRequired}),context:c.object,children:c.any};var P,S=i(function(e,t){Object.defineProperty(t,\"__esModule\",{value:!0});var r=\"function\"==typeof Symbol&&Symbol.for,n=r?Symbol.for(\"react.element\"):60103,o=r?Symbol.for(\"react.portal\"):60106,i=r?Symbol.for(\"react.fragment\"):60107,s=r?Symbol.for(\"react.strict_mode\"):60108,u=r?Symbol.for(\"react.profiler\"):60114,a=r?Symbol.for(\"react.provider\"):60109,c=r?Symbol.for(\"react.context\"):60110,p=r?Symbol.for(\"react.async_mode\"):60111,f=r?Symbol.for(\"react.concurrent_mode\"):60111,d=r?Symbol.for(\"react.forward_ref\"):60112,l=r?Symbol.for(\"react.suspense\"):60113,y=r?Symbol.for(\"react.memo\"):60115,h=r?Symbol.for(\"react.lazy\"):60116;function b(e){if(\"object\"==typeof e&&null!==e){var t=e.$$typeof;switch(t){case n:switch(e=e.type){case p:case f:case i:case u:case s:case l:return e;default:switch(e=e&&e.$$typeof){case c:case d:case a:return e;default:return t}}case h:case y:case o:return t}}}function m(e){return b(e)===f}t.typeOf=b,t.AsyncMode=p,t.ConcurrentMode=f,t.ContextConsumer=c,t.ContextProvider=a,t.Element=n,t.ForwardRef=d,t.Fragment=i,t.Lazy=h,t.Memo=y,t.Portal=o,t.Profiler=u,t.StrictMode=s,t.Suspense=l,t.isValidElementType=function(e){return\"string\"==typeof e||\"function\"==typeof e||e===i||e===f||e===u||e===s||e===l||\"object\"==typeof e&&null!==e&&(e.$$typeof===h||e.$$typeof===y||e.$$typeof===a||e.$$typeof===c||e.$$typeof===d)},t.isAsyncMode=function(e){return m(e)||b(e)===p},t.isConcurrentMode=m,t.isContextConsumer=function(e){return b(e)===c},t.isContextProvider=function(e){return b(e)===a},t.isElement=function(e){return\"object\"==typeof e&&null!==e&&e.$$typeof===n},t.isForwardRef=function(e){return b(e)===d},t.isFragment=function(e){return b(e)===i},t.isLazy=function(e){return b(e)===h},t.isMemo=function(e){return b(e)===y},t.isPortal=function(e){return b(e)===o},t.isProfiler=function(e){return b(e)===u},t.isStrictMode=function(e){return b(e)===s},t.isSuspense=function(e){return b(e)===l}});(P=S)&&P.__esModule&&Object.prototype.hasOwnProperty.call(P,\"default\");var g=i(function(e){e.exports=S}),O=g.isContextConsumer,C={childContextTypes:!0,contextType:!0,contextTypes:!0,defaultProps:!0,displayName:!0,getDefaultProps:!0,getDerivedStateFromError:!0,getDerivedStateFromProps:!0,mixins:!0,propTypes:!0,type:!0},w={name:!0,length:!0,prototype:!0,caller:!0,callee:!0,arguments:!0,arity:!0},x={$$typeof:!0,compare:!0,defaultProps:!0,displayName:!0,propTypes:!0,type:!0},R={};function M(e){return g.isMemo(e)?x:R[e.$$typeof]||C}R[g.ForwardRef]={$$typeof:!0,render:!0,defaultProps:!0,displayName:!0,propTypes:!0};var E=Object.defineProperty,T=Object.getOwnPropertyNames,j=Object.getOwnPropertySymbols,_=Object.getOwnPropertyDescriptor,N=Object.getPrototypeOf,q=Object.prototype;var $=function e(t,r,n){if(\"string\"!=typeof r){if(q){var o=N(r);o&&o!==q&&e(t,o,n)}var i=T(r);j&&(i=i.concat(j(r)));for(var s=M(t),u=M(r),a=0;i.length>a;++a){var c=i[a];if(!(w[c]||n&&n[c]||u&&u[c]||s&&s[c])){var p=_(r,c);try{E(t,c,p)}catch(e){}}}return t}return t},D=function(e,t,r,n,o,i,s,u){if(!e){var a;if(void 0===t)a=Error(\"Minified exception occurred; use the non-minified dev environment for the full error message and additional helpful warnings.\");else{var c=[r,n,o,i,s,u],p=0;(a=Error(t.replace(/%s/g,function(){return c[p++]}))).name=\"Invariant Violation\"}throw a.framesToPop=1,a}},F=[],W=[null,null];function U(e,t){return[t.payload,e[1]+1]}var k=function(){return[null,0]},A=\"undefined\"!=typeof window?t.useLayoutEffect:t.useEffect;function H(e,r){void 0===r&&(r={});var n=r.getDisplayName,i=void 0===n?function(e){return\"ConnectAdvanced(\"+e+\")\"}:n,s=r.methodName,u=void 0===s?\"connectAdvanced\":s,a=r.renderCountProp,c=void 0===a?void 0:a,f=r.shouldHandleStateChanges,d=void 0===f||f,l=r.storeKey,y=void 0===l?\"store\":l,b=r.withRef,P=void 0!==b&&b,S=r.forwardRef,g=void 0!==S&&S,C=r.context,w=void 0===C?p:C,x=v(r,[\"getDisplayName\",\"methodName\",\"renderCountProp\",\"shouldHandleStateChanges\",\"storeKey\",\"withRef\",\"forwardRef\",\"context\"]);D(void 0===c,\"renderCountProp is removed. render counting is built into the latest React Dev Tools profiling extension\"),D(!P,\"withRef is removed. To access the wrapped instance, use a ref on the connected component\");D(\"store\"===y,\"storeKey has been removed and does not do anything. To use a custom Redux store for specific components, create a custom React context with React.createContext(), and pass the context object to React Redux's Provider and specific components like: <Provider context={MyContext}><ConnectedComponent context={MyContext} /></Provider>. You may also pass a {context : MyContext} option to connect\");var R=w;return function(r){var n=r.displayName||r.name||\"Component\",s=i(n),a=m({},x,{getDisplayName:i,methodName:u,renderCountProp:c,shouldHandleStateChanges:d,storeKey:y,displayName:s,wrappedComponentName:n,WrappedComponent:r}),p=x.pure;var f=p?t.useMemo:function(e){return e()};function l(n){var i=t.useMemo(function(){return[n.context,n.forwardedRef,v(n,[\"context\",\"forwardedRef\"])]},[n]),u=i[0],c=i[1],p=i[2],l=t.useMemo(function(){return u&&u.Consumer&&O(o.createElement(u.Consumer,null))?u:R},[u,R]),y=t.useContext(l),b=!!n.store;D(b||!!y&&!!y.store,'Could not find \"store\" in the context of \"'+s+'\". Either wrap the root component in a <Provider>, or pass a custom React context provider to <Provider> and the corresponding React context consumer to '+s+\" in connect options.\");var P=n.store||y.store,S=t.useMemo(function(){return function(t){return e(t.dispatch,a)}(P)},[P]),g=t.useMemo(function(){if(!d)return W;var e=new h(P,b?null:y.subscription),t=e.notifyNestedSubs.bind(e);return[e,t]},[P,b,y]),C=g[0],w=g[1],x=t.useMemo(function(){return b?y:m({},y,{subscription:C})},[b,y,C]),M=t.useReducer(U,F,k),E=M[0][0],T=M[1];if(E&&E.error)throw E.error;var j=t.useRef(),_=t.useRef(p),N=t.useRef(),q=t.useRef(!1),$=f(function(){return N.current&&p===_.current?N.current:S(P.getState(),p)},[P,E,p]);A(function(){_.current=p,j.current=$,q.current=!1,N.current&&(N.current=null,w())}),A(function(){if(d){var e=!1,t=null,r=function(){if(!e){var r,n,o=P.getState();try{r=S(o,_.current)}catch(e){n=e,t=e}n||(t=null),r===j.current?q.current||w():(j.current=r,N.current=r,q.current=!0,T({type:\"STORE_UPDATED\",payload:{latestStoreState:o,error:n}}))}};C.onStateChange=r,C.trySubscribe(),r();return function(){if(e=!0,C.tryUnsubscribe(),t)throw t}}},[P,C,S]);var H=t.useMemo(function(){return o.createElement(r,m({},$,{ref:c}))},[c,r,$]);return t.useMemo(function(){return d?o.createElement(l.Provider,{value:x},H):H},[l,H,x])}var b=p?o.memo(l):l;if(b.WrappedComponent=r,b.displayName=s,g){var P=o.forwardRef(function(e,t){return o.createElement(b,m({},e,{forwardedRef:t}))});return P.displayName=s,P.WrappedComponent=r,$(P,r)}return $(b,r)}}var I=Object.prototype.hasOwnProperty;function L(e,t){return e===t?0!==e||0!==t||1/e==1/t:e!=e&&t!=t}function K(e,t){if(L(e,t))return!0;if(\"object\"!=typeof e||null===e||\"object\"!=typeof t||null===t)return!1;var r=Object.keys(e);if(r.length!==Object.keys(t).length)return!1;for(var n=0;r.length>n;n++)if(!I.call(t,r[n])||!L(e[r[n]],t[r[n]]))return!1;return!0}function z(e){return function(t,r){var n=e(t,r);function o(){return n}return o.dependsOnOwnProps=!1,o}}function V(e){return null!=e.dependsOnOwnProps?!!e.dependsOnOwnProps:1!==e.length}function Y(e,t){return function(t,r){var n=function(e,t){return n.dependsOnOwnProps?n.mapToProps(e,t):n.mapToProps(e)};return n.dependsOnOwnProps=!0,n.mapToProps=function(t,r){n.mapToProps=e,n.dependsOnOwnProps=V(e);var o=n(t,r);return\"function\"==typeof o&&(n.mapToProps=o,n.dependsOnOwnProps=V(o),o=n(t,r)),o},n}}function B(e,t,r){return m({},r,e,t)}var G=[function(e){return\"function\"==typeof e?function(e){return function(t,r){var n,o=r.pure,i=r.areMergedPropsEqual,s=!1;return function(t,r,u){var a=e(t,r,u);return s?o&&i(a,n)||(n=a):(s=!0,n=a),n}}}(e):void 0},function(e){return e?void 0:function(){return B}}];function J(e,t,r,n){return function(o,i){return r(e(o,i),t(n,i),i)}}function Q(e,t,r,n,o){var i,s,u,a,c,p=o.areStatesEqual,f=o.areOwnPropsEqual,d=o.areStatePropsEqual,l=!1;function y(o,l){var y,h,b=!f(l,s),m=!p(o,i);return i=o,s=l,b&&m?(u=e(i,s),t.dependsOnOwnProps&&(a=t(n,s)),c=r(u,a,s)):b?(e.dependsOnOwnProps&&(u=e(i,s)),t.dependsOnOwnProps&&(a=t(n,s)),c=r(u,a,s)):m?(y=e(i,s),h=!d(y,u),u=y,h&&(c=r(u,a,s)),c):c}return function(o,p){return l?y(o,p):(u=e(i=o,s=p),a=t(n,s),c=r(u,a,s),l=!0,c)}}function X(e,t){var r=t.initMapStateToProps,n=t.initMapDispatchToProps,o=t.initMergeProps,i=v(t,[\"initMapStateToProps\",\"initMapDispatchToProps\",\"initMergeProps\"]),s=r(e,i),u=n(e,i),a=o(e,i);return(i.pure?Q:J)(s,u,a,e,i)}function Z(e,t,r){for(var n=t.length-1;n>=0;n--){var o=t[n](e);if(o)return o}return function(t,n){throw Error(\"Invalid value of type \"+typeof e+\" for \"+r+\" argument when connecting component \"+n.wrappedComponentName+\".\")}}function ee(e,t){return e===t}var te,re,ne,oe,ie,se,ue,ae,ce,pe,fe,de,le=(oe=void 0===(ne=(re=void 0===te?{}:te).connectHOC)?H:ne,se=void 0===(ie=re.mapStateToPropsFactories)?[function(e){return\"function\"==typeof e?Y(e):void 0},function(e){return e?void 0:z(function(){return{}})}]:ie,ae=void 0===(ue=re.mapDispatchToPropsFactories)?[function(e){return\"function\"==typeof e?Y(e):void 0},function(e){return e?void 0:z(function(e){return{dispatch:e}})},function(e){return e&&\"object\"==typeof e?z(function(t){return r.bindActionCreators(e,t)}):void 0}]:ue,pe=void 0===(ce=re.mergePropsFactories)?G:ce,de=void 0===(fe=re.selectorFactory)?X:fe,function(e,t,r,n){void 0===n&&(n={});var o=n.pure,i=void 0===o||o,s=n.areStatesEqual,u=void 0===s?ee:s,a=n.areOwnPropsEqual,c=void 0===a?K:a,p=n.areStatePropsEqual,f=void 0===p?K:p,d=n.areMergedPropsEqual,l=void 0===d?K:d,y=v(n,[\"pure\",\"areStatesEqual\",\"areOwnPropsEqual\",\"areStatePropsEqual\",\"areMergedPropsEqual\"]),h=Z(e,se,\"mapStateToProps\"),b=Z(t,ae,\"mapDispatchToProps\"),P=Z(r,pe,\"mergeProps\");return oe(de,m({methodName:\"connect\",getDisplayName:function(e){return\"Connect(\"+e+\")\"},shouldHandleStateChanges:!!e,initMapStateToProps:h,initMapDispatchToProps:b,initMergeProps:P,pure:i,areStatesEqual:u,areOwnPropsEqual:c,areStatePropsEqual:f,areMergedPropsEqual:l},y))});f=n.unstable_batchedUpdates,Object.defineProperty(e,\"batch\",{enumerable:!0,get:function(){return n.unstable_batchedUpdates}}),e.Provider=b,e.ReactReduxContext=p,e.connect=le,e.connectAdvanced=H,Object.defineProperty(e,\"__esModule\",{value:!0})});\n"
  },
  {
    "path": "vendor/react-transition-group.js",
    "content": "!function(e,t){\"object\"==typeof exports&&\"undefined\"!=typeof module?t(exports,require(\"react\"),require(\"react-dom\")):\"function\"==typeof define&&define.amd?define([\"exports\",\"react\",\"react-dom\"],t):t((e=e||self).ReactTransitionGroup={},e.React,e.ReactDOM)}(this,function(e,t,n){\"use strict\";var r=\"default\"in t?t.default:t,i=\"default\"in n?n.default:n;function o(){return(o=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e}).apply(this,arguments)}function a(e,t){if(null==e)return{};var n,r,i={},o=Object.keys(e);for(r=0;r<o.length;r++)n=o[r],t.indexOf(n)>=0||(i[n]=e[n]);return i}function s(e,t){e.prototype=Object.create(t.prototype),e.prototype.constructor=e,e.__proto__=t}function l(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,\"default\")?e.default:e}function c(e,t){return e(t={exports:{}},t.exports),t.exports}var u=Object.getOwnPropertySymbols,p=Object.prototype.hasOwnProperty,d=Object.prototype.propertyIsEnumerable;(function(){try{if(!Object.assign)return!1;var e=new String(\"abc\");if(e[5]=\"de\",\"5\"===Object.getOwnPropertyNames(e)[0])return!1;for(var t={},n=0;n<10;n++)t[\"_\"+String.fromCharCode(n)]=n;if(\"0123456789\"!==Object.getOwnPropertyNames(t).map(function(e){return t[e]}).join(\"\"))return!1;var r={};return\"abcdefghijklmnopqrst\".split(\"\").forEach(function(e){r[e]=e}),\"abcdefghijklmnopqrst\"===Object.keys(Object.assign({},r)).join(\"\")}catch(e){return!1}})()&&Object.assign;var f=\"SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED\";function h(){}var E=c(function(e){e.exports=function(){function e(e,t,n,r,i,o){if(o!==f){var a=new Error(\"Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types\");throw a.name=\"Invariant Violation\",a}}function t(){return e}e.isRequired=e;var n={array:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:t,element:e,instanceOf:t,node:e,objectOf:t,oneOf:t,oneOfType:t,shape:t,exact:t};return n.checkPropTypes=h,n.PropTypes=n,n}()}),m=(E.object,E.oneOfType,E.element,E.bool,E.func,c(function(e){e.exports=function(e){return e&&e.__esModule?e:{default:e}}}));l(m);var x=c(function(e,t){t.__esModule=!0,t.default=function(e,t){return e.classList?!!t&&e.classList.contains(t):-1!==(\" \"+(e.className.baseVal||e.className)+\" \").indexOf(\" \"+t+\" \")},e.exports=t.default});l(x);var v=l(c(function(e,t){t.__esModule=!0,t.default=function(e,t){e.classList?e.classList.add(t):(0,n.default)(e,t)||(\"string\"==typeof e.className?e.className=e.className+\" \"+t:e.setAttribute(\"class\",(e.className&&e.className.baseVal||\"\")+\" \"+t))};var n=m(x);e.exports=t.default}));function y(e,t){return e.replace(new RegExp(\"(^|\\\\s)\"+t+\"(?:\\\\s|$)\",\"g\"),\"$1\").replace(/\\s+/g,\" \").replace(/^\\s*|\\s*$/g,\"\")}var g={disabled:!1},b=r.createContext(null),C=\"unmounted\",O=\"exited\",S=\"entering\",N=\"entered\",k=function(e){function t(t,n){var r;r=e.call(this,t,n)||this;var i,o=n&&!n.isMounting?t.enter:t.appear;return r.appearStatus=null,t.in?o?(i=O,r.appearStatus=S):i=N:i=t.unmountOnExit||t.mountOnEnter?C:O,r.state={status:i},r.nextCallback=null,r}s(t,e),t.getDerivedStateFromProps=function(e,t){return e.in&&t.status===C?{status:O}:null};var n=t.prototype;return n.componentDidMount=function(){this.updateStatus(!0,this.appearStatus)},n.componentDidUpdate=function(e){var t=null;if(e!==this.props){var n=this.state.status;this.props.in?n!==S&&n!==N&&(t=S):n!==S&&n!==N||(t=\"exiting\")}this.updateStatus(!1,t)},n.componentWillUnmount=function(){this.cancelNextCallback()},n.getTimeouts=function(){var e,t,n,r=this.props.timeout;return e=t=n=r,null!=r&&\"number\"!=typeof r&&(e=r.exit,t=r.enter,n=void 0!==r.appear?r.appear:t),{exit:e,enter:t,appear:n}},n.updateStatus=function(e,t){if(void 0===e&&(e=!1),null!==t){this.cancelNextCallback();var n=i.findDOMNode(this);t===S?this.performEnter(n,e):this.performExit(n)}else this.props.unmountOnExit&&this.state.status===O&&this.setState({status:C})},n.performEnter=function(e,t){var n=this,r=this.props.enter,i=this.context?this.context.isMounting:t,o=this.getTimeouts(),a=i?o.appear:o.enter;!t&&!r||g.disabled?this.safeSetState({status:N},function(){n.props.onEntered(e)}):(this.props.onEnter(e,i),this.safeSetState({status:S},function(){n.props.onEntering(e,i),n.onTransitionEnd(e,a,function(){n.safeSetState({status:N},function(){n.props.onEntered(e,i)})})}))},n.performExit=function(e){var t=this,n=this.props.exit,r=this.getTimeouts();n&&!g.disabled?(this.props.onExit(e),this.safeSetState({status:\"exiting\"},function(){t.props.onExiting(e),t.onTransitionEnd(e,r.exit,function(){t.safeSetState({status:O},function(){t.props.onExited(e)})})})):this.safeSetState({status:O},function(){t.props.onExited(e)})},n.cancelNextCallback=function(){null!==this.nextCallback&&(this.nextCallback.cancel(),this.nextCallback=null)},n.safeSetState=function(e,t){t=this.setNextCallback(t),this.setState(e,t)},n.setNextCallback=function(e){var t=this,n=!0;return this.nextCallback=function(r){n&&(n=!1,t.nextCallback=null,e(r))},this.nextCallback.cancel=function(){n=!1},this.nextCallback},n.onTransitionEnd=function(e,t,n){this.setNextCallback(n);var r=null==t&&!this.props.addEndListener;e&&!r?(this.props.addEndListener&&this.props.addEndListener(e,this.nextCallback),null!=t&&setTimeout(this.nextCallback,t)):setTimeout(this.nextCallback,0)},n.render=function(){var e=this.state.status;if(e===C)return null;var t=this.props,n=t.children,i=a(t,[\"children\"]);if(delete i.in,delete i.mountOnEnter,delete i.unmountOnExit,delete i.appear,delete i.enter,delete i.exit,delete i.timeout,delete i.addEndListener,delete i.onEnter,delete i.onEntering,delete i.onEntered,delete i.onExit,delete i.onExiting,delete i.onExited,\"function\"==typeof n)return r.createElement(b.Provider,{value:null},n(e,i));var o=r.Children.only(n);return r.createElement(b.Provider,{value:null},r.cloneElement(o,i))},t}(r.Component);function T(){}k.contextType=b,k.propTypes={},k.defaultProps={in:!1,mountOnEnter:!1,unmountOnExit:!1,appear:!1,enter:!0,exit:!0,onEnter:T,onEntering:T,onEntered:T,onExit:T,onExiting:T,onExited:T},k.UNMOUNTED=0,k.EXITED=1,k.ENTERING=2,k.ENTERED=3,k.EXITING=4;var j=function(e,t){return e&&t&&t.split(\" \").forEach(function(t){return r=t,void((n=e).classList?n.classList.remove(r):\"string\"==typeof n.className?n.className=y(n.className,r):n.setAttribute(\"class\",y(n.className&&n.className.baseVal||\"\",r)));var n,r})},P=function(e){function t(){for(var t,n=arguments.length,r=new Array(n),i=0;i<n;i++)r[i]=arguments[i];return(t=e.call.apply(e,[this].concat(r))||this).appliedClasses={appear:{},enter:{},exit:{}},t.onEnter=function(e,n){t.removeClasses(e,\"exit\"),t.addClass(e,n?\"appear\":\"enter\",\"base\"),t.props.onEnter&&t.props.onEnter(e,n)},t.onEntering=function(e,n){var r=n?\"appear\":\"enter\";t.addClass(e,r,\"active\"),t.props.onEntering&&t.props.onEntering(e,n)},t.onEntered=function(e,n){var r=n?\"appear\":\"enter\";t.removeClasses(e,r),t.addClass(e,r,\"done\"),t.props.onEntered&&t.props.onEntered(e,n)},t.onExit=function(e){t.removeClasses(e,\"appear\"),t.removeClasses(e,\"enter\"),t.addClass(e,\"exit\",\"base\"),t.props.onExit&&t.props.onExit(e)},t.onExiting=function(e){t.addClass(e,\"exit\",\"active\"),t.props.onExiting&&t.props.onExiting(e)},t.onExited=function(e){t.removeClasses(e,\"exit\"),t.addClass(e,\"exit\",\"done\"),t.props.onExited&&t.props.onExited(e)},t.getClassNames=function(e){var n=t.props.classNames,r=\"string\"==typeof n,i=r?\"\"+(r&&n?n+\"-\":\"\")+e:n[e];return{baseClassName:i,activeClassName:r?i+\"-active\":n[e+\"Active\"],doneClassName:r?i+\"-done\":n[e+\"Done\"]}},t}s(t,e);var n=t.prototype;return n.addClass=function(e,t,n){var r=this.getClassNames(t)[n+\"ClassName\"];\"appear\"===t&&\"done\"===n&&(r+=\" \"+this.getClassNames(\"enter\").doneClassName),\"active\"===n&&e&&e.scrollTop,this.appliedClasses[t][n]=r,function(e,t){e&&t&&t.split(\" \").forEach(function(t){return v(e,t)})}(e,r)},n.removeClasses=function(e,t){var n=this.appliedClasses[t],r=n.base,i=n.active,o=n.done;this.appliedClasses[t]={},r&&j(e,r),i&&j(e,i),o&&j(e,o)},n.render=function(){var e=this.props,t=(e.classNames,a(e,[\"classNames\"]));return r.createElement(k,o({},t,{onEnter:this.onEnter,onEntered:this.onEntered,onEntering:this.onEntering,onExit:this.onExit,onExiting:this.onExiting,onExited:this.onExited}))},t}(r.Component);function _(e){if(void 0===e)throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\");return e}function w(e,n){var r=Object.create(null);return e&&t.Children.map(e,function(e){return e}).forEach(function(e){r[e.key]=function(e){return n&&t.isValidElement(e)?n(e):e}(e)}),r}function L(e,t,n){return null!=n[t]?n[t]:e.props[t]}function M(e,n,r){var i=w(e.children),o=function(e,t){function n(n){return n in t?t[n]:e[n]}e=e||{},t=t||{};var r,i=Object.create(null),o=[];for(var a in e)a in t?o.length&&(i[a]=o,o=[]):o.push(a);var s={};for(var l in t){if(i[l])for(r=0;r<i[l].length;r++){var c=i[l][r];s[i[l][r]]=n(c)}s[l]=n(l)}for(r=0;r<o.length;r++)s[o[r]]=n(o[r]);return s}(n,i);return Object.keys(o).forEach(function(a){var s=o[a];if(t.isValidElement(s)){var l=a in n,c=a in i,u=n[a],p=t.isValidElement(u)&&!u.props.in;!c||l&&!p?c||!l||p?c&&l&&t.isValidElement(u)&&(o[a]=t.cloneElement(s,{onExited:r.bind(null,s),in:u.props.in,exit:L(s,\"exit\",e),enter:L(s,\"enter\",e)})):o[a]=t.cloneElement(s,{in:!1}):o[a]=t.cloneElement(s,{onExited:r.bind(null,s),in:!0,exit:L(s,\"exit\",e),enter:L(s,\"enter\",e)})}}),o}P.defaultProps={classNames:\"\"},P.propTypes={};var D=Object.values||function(e){return Object.keys(e).map(function(t){return e[t]})},R=function(e){function n(t,n){var r,i=(r=e.call(this,t,n)||this).handleExited.bind(_(_(r)));return r.state={contextValue:{isMounting:!0},handleExited:i,firstRender:!0},r}s(n,e);var i=n.prototype;return i.componentDidMount=function(){this.mounted=!0,this.setState({contextValue:{isMounting:!1}})},i.componentWillUnmount=function(){this.mounted=!1},n.getDerivedStateFromProps=function(e,n){var r,i,o=n.children,a=n.handleExited;return{children:n.firstRender?(r=e,i=a,w(r.children,function(e){return t.cloneElement(e,{onExited:i.bind(null,e),in:!0,appear:L(e,\"appear\",r),enter:L(e,\"enter\",r),exit:L(e,\"exit\",r)})})):M(e,o,a),firstRender:!1}},i.handleExited=function(e,t){var n=w(this.props.children);e.key in n||(e.props.onExited&&e.props.onExited(t),this.mounted&&this.setState(function(t){var n=o({},t.children);return delete n[e.key],{children:n}}))},i.render=function(){var e=this.props,t=e.component,n=e.childFactory,i=a(e,[\"component\",\"childFactory\"]),o=this.state.contextValue,s=D(this.state.children).map(n);return delete i.appear,delete i.enter,delete i.exit,null===t?r.createElement(b.Provider,{value:o},s):r.createElement(b.Provider,{value:o},r.createElement(t,i,s))},n}(r.Component);R.propTypes={},R.defaultProps={component:\"div\",childFactory:function(e){return e}};var A,V,I=function(e){function t(){for(var t,n=arguments.length,r=new Array(n),i=0;i<n;i++)r[i]=arguments[i];return(t=e.call.apply(e,[this].concat(r))||this).handleEnter=function(){for(var e=arguments.length,n=new Array(e),r=0;r<e;r++)n[r]=arguments[r];return t.handleLifecycle(\"onEnter\",0,n)},t.handleEntering=function(){for(var e=arguments.length,n=new Array(e),r=0;r<e;r++)n[r]=arguments[r];return t.handleLifecycle(\"onEntering\",0,n)},t.handleEntered=function(){for(var e=arguments.length,n=new Array(e),r=0;r<e;r++)n[r]=arguments[r];return t.handleLifecycle(\"onEntered\",0,n)},t.handleExit=function(){for(var e=arguments.length,n=new Array(e),r=0;r<e;r++)n[r]=arguments[r];return t.handleLifecycle(\"onExit\",1,n)},t.handleExiting=function(){for(var e=arguments.length,n=new Array(e),r=0;r<e;r++)n[r]=arguments[r];return t.handleLifecycle(\"onExiting\",1,n)},t.handleExited=function(){for(var e=arguments.length,n=new Array(e),r=0;r<e;r++)n[r]=arguments[r];return t.handleLifecycle(\"onExited\",1,n)},t}s(t,e);var i=t.prototype;return i.handleLifecycle=function(e,t,i){var o,a=this.props.children,s=r.Children.toArray(a)[t];s.props[e]&&(o=s.props)[e].apply(o,i),this.props[e]&&this.props[e](n.findDOMNode(this))},i.render=function(){var e=this.props,t=e.children,n=e.in,i=a(e,[\"children\",\"in\"]),o=r.Children.toArray(t),s=o[0],l=o[1];return delete i.onEnter,delete i.onEntering,delete i.onEntered,delete i.onExit,delete i.onExiting,delete i.onExited,r.createElement(R,i,n?r.cloneElement(s,{key:\"first\",onEnter:this.handleEnter,onEntering:this.handleEntering,onEntered:this.handleEntered}):r.cloneElement(l,{key:\"second\",onEnter:this.handleExit,onEntering:this.handleExiting,onEntered:this.handleExited}))},t}(r.Component);I.propTypes={};var F=\"out-in\",U=\"in-out\",q=function(e,t,n){return function(){var r;e.props[t]&&(r=e.props)[t].apply(r,arguments),n()}},G=((A={})[F]=function(e){var t=e.current,n=e.changeState;return r.cloneElement(t,{in:!1,onExited:q(t,\"onExited\",function(){n(S,null)})})},A[U]=function(e){var t=e.current,n=e.changeState,i=e.children;return[t,r.cloneElement(i,{in:!0,onEntered:q(i,\"onEntered\",function(){n(S)})})]},A),W=((V={})[F]=function(e){var t=e.children,n=e.changeState;return r.cloneElement(t,{in:!0,onEntered:q(t,\"onEntered\",function(){n(N,r.cloneElement(t,{in:!0}))})})},V[U]=function(e){var t=e.current,n=e.children,i=e.changeState;return[r.cloneElement(t,{in:!1,onExited:q(t,\"onExited\",function(){i(N,r.cloneElement(n,{in:!0}))})}),r.cloneElement(n,{in:!0})]},V),$=function(e){function t(){for(var t,n=arguments.length,r=new Array(n),i=0;i<n;i++)r[i]=arguments[i];return(t=e.call.apply(e,[this].concat(r))||this).state={status:N,current:null},t.appeared=!1,t.changeState=function(e,n){void 0===n&&(n=t.state.current),t.setState({status:e,current:n})},t}s(t,e);var n=t.prototype;return n.componentDidMount=function(){this.appeared=!0},t.getDerivedStateFromProps=function(e,t){return null==e.children?{current:null}:t.status===S&&e.mode===U?{status:S}:!t.current||(n=t.current,i=e.children,n===i||r.isValidElement(n)&&r.isValidElement(i)&&null!=n.key&&n.key===i.key)?{current:r.cloneElement(e.children,{in:!0})}:{status:\"exiting\"};var n,i},n.render=function(){var e,t=this.props,n=t.children,i=t.mode,o=this.state,a=o.status,s=o.current,l={children:n,current:s,changeState:this.changeState,status:a};switch(a){case S:e=W[i](l);break;case\"exiting\":e=G[i](l);break;case N:e=s}return r.createElement(b.Provider,{value:{isMounting:!this.appeared}},e)},t}(r.Component);$.propTypes={},$.defaultProps={mode:F},e.CSSTransition=P,e.ReplaceTransition=I,e.SwitchTransition=$,e.Transition=k,e.TransitionGroup=R,e.config=g,Object.defineProperty(e,\"__esModule\",{value:!0})});\n"
  },
  {
    "path": "vendor/react.js",
    "content": "/** @license React v16.8.6\n * react.production.min.js\n *\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n'use strict';(function(N,q){\"object\"===typeof exports&&\"undefined\"!==typeof module?module.exports=q():\"function\"===typeof define&&define.amd?define(q):N.React=q()})(this,function(){function N(a,b,d,g,p,c,e,h){if(!a){a=void 0;if(void 0===b)a=Error(\"Minified exception occurred; use the non-minified dev environment for the full error message and additional helpful warnings.\");else{var n=[d,g,p,c,e,h],f=0;a=Error(b.replace(/%s/g,function(){return n[f++]}));a.name=\"Invariant Violation\"}a.framesToPop=1;\nthrow a;}}function q(a){for(var b=arguments.length-1,d=\"https://reactjs.org/docs/error-decoder.html?invariant=\"+a,g=0;g<b;g++)d+=\"&args[]=\"+encodeURIComponent(arguments[g+1]);N(!1,\"Minified React error #\"+a+\"; visit %s for the full message or use the non-minified dev environment for full errors and additional helpful warnings. \",d)}function t(a,b,d){this.props=a;this.context=b;this.refs=ba;this.updater=d||ca}function da(){}function O(a,b,d){this.props=a;this.context=b;this.refs=ba;this.updater=d||\nca}function u(){if(!x){var a=c.expirationTime;C?P():C=!0;D(ta,a)}}function Q(){var a=c,b=c.next;if(c===b)c=null;else{var d=c.previous;c=d.next=b;b.previous=d}a.next=a.previous=null;d=a.callback;b=a.expirationTime;a=a.priorityLevel;var g=f,p=E;f=a;E=b;try{var n=d()}finally{f=g,E=p}if(\"function\"===typeof n)if(n={callback:n,priorityLevel:a,expirationTime:b,next:null,previous:null},null===c)c=n.next=n.previous=n;else{d=null;a=c;do{if(a.expirationTime>=b){d=a;break}a=a.next}while(a!==c);null===d?d=c:d===\nc&&(c=n,u());b=d.previous;b.next=d.previous=n;n.next=d;n.previous=b}}function F(){if(-1===k&&null!==c&&1===c.priorityLevel){x=!0;try{do Q();while(null!==c&&1===c.priorityLevel)}finally{x=!1,null!==c?u():C=!1}}}function ta(a){x=!0;var b=G;G=a;try{if(a)for(;null!==c;){var d=l();if(c.expirationTime<=d){do Q();while(null!==c&&c.expirationTime<=d)}else break}else if(null!==c){do Q();while(null!==c&&!H())}}finally{x=!1,G=b,null!==c?u():C=!1,F()}}function ea(a,b,d){var g=void 0,p={},c=null,e=null;if(null!=\nb)for(g in void 0!==b.ref&&(e=b.ref),void 0!==b.key&&(c=\"\"+b.key),b)fa.call(b,g)&&!ha.hasOwnProperty(g)&&(p[g]=b[g]);var h=arguments.length-2;if(1===h)p.children=d;else if(1<h){for(var f=Array(h),k=0;k<h;k++)f[k]=arguments[k+2];p.children=f}if(a&&a.defaultProps)for(g in h=a.defaultProps,h)void 0===p[g]&&(p[g]=h[g]);return{$$typeof:y,type:a,key:c,ref:e,props:p,_owner:R.current}}function ua(a,b){return{$$typeof:y,type:a.type,key:b,ref:a.ref,props:a.props,_owner:a._owner}}function S(a){return\"object\"===\ntypeof a&&null!==a&&a.$$typeof===y}function va(a){var b={\"=\":\"=0\",\":\":\"=2\"};return\"$\"+(\"\"+a).replace(/[=:]/g,function(a){return b[a]})}function ia(a,b,d,g){if(I.length){var c=I.pop();c.result=a;c.keyPrefix=b;c.func=d;c.context=g;c.count=0;return c}return{result:a,keyPrefix:b,func:d,context:g,count:0}}function ja(a){a.result=null;a.keyPrefix=null;a.func=null;a.context=null;a.count=0;10>I.length&&I.push(a)}function T(a,b,d,g){var c=typeof a;if(\"undefined\"===c||\"boolean\"===c)a=null;var e=!1;if(null===\na)e=!0;else switch(c){case \"string\":case \"number\":e=!0;break;case \"object\":switch(a.$$typeof){case y:case wa:e=!0}}if(e)return d(g,a,\"\"===b?\".\"+U(a,0):b),1;e=0;b=\"\"===b?\".\":b+\":\";if(Array.isArray(a))for(var f=0;f<a.length;f++){c=a[f];var h=b+U(c,f);e+=T(c,h,d,g)}else if(null===a||\"object\"!==typeof a?h=null:(h=ka&&a[ka]||a[\"@@iterator\"],h=\"function\"===typeof h?h:null),\"function\"===typeof h)for(a=h.call(a),f=0;!(c=a.next()).done;)c=c.value,h=b+U(c,f++),e+=T(c,h,d,g);else\"object\"===c&&(d=\"\"+a,q(\"31\",\n\"[object Object]\"===d?\"object with keys {\"+Object.keys(a).join(\", \")+\"}\":d,\"\"));return e}function V(a,b,d){return null==a?0:T(a,\"\",b,d)}function U(a,b){return\"object\"===typeof a&&null!==a&&null!=a.key?va(a.key):b.toString(36)}function xa(a,b,d){a.func.call(a.context,b,a.count++)}function ya(a,b,d){var g=a.result,c=a.keyPrefix;a=a.func.call(a.context,b,a.count++);Array.isArray(a)?W(a,g,d,function(a){return a}):null!=a&&(S(a)&&(a=ua(a,c+(!a.key||b&&b.key===a.key?\"\":(\"\"+a.key).replace(la,\"$&/\")+\"/\")+\nd)),g.push(a))}function W(a,b,d,g,c){var e=\"\";null!=d&&(e=(\"\"+d).replace(la,\"$&/\")+\"/\");b=ia(b,e,g,c);V(a,ya,b);ja(b)}function m(){var a=ma.current;null===a?q(\"321\"):void 0;return a}var e=\"function\"===typeof Symbol&&Symbol.for,y=e?Symbol.for(\"react.element\"):60103,wa=e?Symbol.for(\"react.portal\"):60106,r=e?Symbol.for(\"react.fragment\"):60107,X=e?Symbol.for(\"react.strict_mode\"):60108,za=e?Symbol.for(\"react.profiler\"):60114,Aa=e?Symbol.for(\"react.provider\"):60109,Ba=e?Symbol.for(\"react.context\"):60110,\nCa=e?Symbol.for(\"react.concurrent_mode\"):60111,Da=e?Symbol.for(\"react.forward_ref\"):60112,Ea=e?Symbol.for(\"react.suspense\"):60113,Fa=e?Symbol.for(\"react.memo\"):60115,Ga=e?Symbol.for(\"react.lazy\"):60116,ka=\"function\"===typeof Symbol&&Symbol.iterator,na=Object.getOwnPropertySymbols,Ha=Object.prototype.hasOwnProperty,Ia=Object.prototype.propertyIsEnumerable,J=function(){try{if(!Object.assign)return!1;var a=new String(\"abc\");a[5]=\"de\";if(\"5\"===Object.getOwnPropertyNames(a)[0])return!1;var b={};for(a=\n0;10>a;a++)b[\"_\"+String.fromCharCode(a)]=a;if(\"0123456789\"!==Object.getOwnPropertyNames(b).map(function(a){return b[a]}).join(\"\"))return!1;var d={};\"abcdefghijklmnopqrst\".split(\"\").forEach(function(a){d[a]=a});return\"abcdefghijklmnopqrst\"!==Object.keys(Object.assign({},d)).join(\"\")?!1:!0}catch(g){return!1}}()?Object.assign:function(a,b){if(null===a||void 0===a)throw new TypeError(\"Object.assign cannot be called with null or undefined\");var d=Object(a);for(var c,e=1;e<arguments.length;e++){var f=Object(arguments[e]);\nfor(var k in f)Ha.call(f,k)&&(d[k]=f[k]);if(na){c=na(f);for(var h=0;h<c.length;h++)Ia.call(f,c[h])&&(d[c[h]]=f[c[h]])}}return d},ca={isMounted:function(a){return!1},enqueueForceUpdate:function(a,b,d){},enqueueReplaceState:function(a,b,d,c){},enqueueSetState:function(a,b,d,c){}},ba={};t.prototype.isReactComponent={};t.prototype.setState=function(a,b){\"object\"!==typeof a&&\"function\"!==typeof a&&null!=a?q(\"85\"):void 0;this.updater.enqueueSetState(this,a,b,\"setState\")};t.prototype.forceUpdate=function(a){this.updater.enqueueForceUpdate(this,\na,\"forceUpdate\")};da.prototype=t.prototype;e=O.prototype=new da;e.constructor=O;J(e,t.prototype);e.isPureReactComponent=!0;var c=null,G=!1,f=3,k=-1,E=-1,x=!1,C=!1,Ja=Date,Ka=\"function\"===typeof setTimeout?setTimeout:void 0,La=\"function\"===typeof clearTimeout?clearTimeout:void 0,oa=\"function\"===typeof requestAnimationFrame?requestAnimationFrame:void 0,pa=\"function\"===typeof cancelAnimationFrame?cancelAnimationFrame:void 0,qa,ra,Y=function(a){qa=oa(function(b){La(ra);a(b)});ra=Ka(function(){pa(qa);\na(l())},100)};if(\"object\"===typeof performance&&\"function\"===typeof performance.now){var Ma=performance;var l=function(){return Ma.now()}}else l=function(){return Ja.now()};e=null;\"undefined\"!==typeof window?e=window:\"undefined\"!==typeof global&&(e=global);if(e&&e._schedMock){e=e._schedMock;var D=e[0];var P=e[1];var H=e[2];l=e[3]}else if(\"undefined\"===typeof window||\"function\"!==typeof MessageChannel){var v=null,Na=function(a){if(null!==v)try{v(a)}finally{v=null}};D=function(a,b){null!==v?setTimeout(D,\n0,a):(v=a,setTimeout(Na,0,!1))};P=function(){v=null};H=function(){return!1}}else{\"undefined\"!==typeof console&&(\"function\"!==typeof oa&&console.error(\"This browser doesn't support requestAnimationFrame. Make sure that you load a polyfill in older browsers. https://fb.me/react-polyfills\"),\"function\"!==typeof pa&&console.error(\"This browser doesn't support cancelAnimationFrame. Make sure that you load a polyfill in older browsers. https://fb.me/react-polyfills\"));var w=null,K=!1,z=-1,A=!1,Z=!1,L=0,\nM=33,B=33;H=function(){return L<=l()};e=new MessageChannel;var sa=e.port2;e.port1.onmessage=function(a){K=!1;a=w;var b=z;w=null;z=-1;var d=l(),c=!1;if(0>=L-d)if(-1!==b&&b<=d)c=!0;else{A||(A=!0,Y(aa));w=a;z=b;return}if(null!==a){Z=!0;try{a(c)}finally{Z=!1}}};var aa=function(a){if(null!==w){Y(aa);var b=a-L+B;b<B&&M<B?(8>b&&(b=8),B=b<M?M:b):M=b;L=a+B;K||(K=!0,sa.postMessage(void 0))}else A=!1};D=function(a,b){w=a;z=b;Z||0>b?sa.postMessage(void 0):A||(A=!0,Y(aa))};P=function(){w=null;K=!1;z=-1}}var Oa=\n0,ma={current:null},R={current:null};e={ReactCurrentDispatcher:ma,ReactCurrentOwner:R,assign:J};J(e,{Scheduler:{unstable_cancelCallback:function(a){var b=a.next;if(null!==b){if(b===a)c=null;else{a===c&&(c=b);var d=a.previous;d.next=b;b.previous=d}a.next=a.previous=null}},unstable_shouldYield:function(){return!G&&(null!==c&&c.expirationTime<E||H())},unstable_now:l,unstable_scheduleCallback:function(a,b){var d=-1!==k?k:l();if(\"object\"===typeof b&&null!==b&&\"number\"===typeof b.timeout)b=d+b.timeout;\nelse switch(f){case 1:b=d+-1;break;case 2:b=d+250;break;case 5:b=d+1073741823;break;case 4:b=d+1E4;break;default:b=d+5E3}a={callback:a,priorityLevel:f,expirationTime:b,next:null,previous:null};if(null===c)c=a.next=a.previous=a,u();else{d=null;var g=c;do{if(g.expirationTime>b){d=g;break}g=g.next}while(g!==c);null===d?d=c:d===c&&(c=a,u());b=d.previous;b.next=d.previous=a;a.next=d;a.previous=b}return a},unstable_runWithPriority:function(a,b){switch(a){case 1:case 2:case 3:case 4:case 5:break;default:a=\n3}var d=f,c=k;f=a;k=l();try{return b()}finally{f=d,k=c,F()}},unstable_next:function(a){switch(f){case 1:case 2:case 3:var b=3;break;default:b=f}var d=f,c=k;f=b;k=l();try{return a()}finally{f=d,k=c,F()}},unstable_wrapCallback:function(a){var b=f;return function(){var d=f,c=k;f=b;k=l();try{return a.apply(this,arguments)}finally{f=d,k=c,F()}}},unstable_getFirstCallbackNode:function(){return c},unstable_pauseExecution:function(){},unstable_continueExecution:function(){null!==c&&u()},unstable_getCurrentPriorityLevel:function(){return f},\nunstable_IdlePriority:5,unstable_ImmediatePriority:1,unstable_LowPriority:4,unstable_NormalPriority:3,unstable_UserBlockingPriority:2},SchedulerTracing:{__interactionsRef:null,__subscriberRef:null,unstable_clear:function(a){return a()},unstable_getCurrent:function(){return null},unstable_getThreadID:function(){return++Oa},unstable_subscribe:function(a){},unstable_trace:function(a,b,d){return d()},unstable_unsubscribe:function(a){},unstable_wrap:function(a){return a}}});var fa=Object.prototype.hasOwnProperty,\nha={key:!0,ref:!0,__self:!0,__source:!0},la=/\\/+/g,I=[];r={Children:{map:function(a,b,d){if(null==a)return a;var c=[];W(a,c,null,b,d);return c},forEach:function(a,b,d){if(null==a)return a;b=ia(null,null,b,d);V(a,xa,b);ja(b)},count:function(a){return V(a,function(){return null},null)},toArray:function(a){var b=[];W(a,b,null,function(a){return a});return b},only:function(a){S(a)?void 0:q(\"143\");return a}},createRef:function(){return{current:null}},Component:t,PureComponent:O,createContext:function(a,\nb){void 0===b&&(b=null);a={$$typeof:Ba,_calculateChangedBits:b,_currentValue:a,_currentValue2:a,_threadCount:0,Provider:null,Consumer:null};a.Provider={$$typeof:Aa,_context:a};return a.Consumer=a},forwardRef:function(a){return{$$typeof:Da,render:a}},lazy:function(a){return{$$typeof:Ga,_ctor:a,_status:-1,_result:null}},memo:function(a,b){return{$$typeof:Fa,type:a,compare:void 0===b?null:b}},useCallback:function(a,b){return m().useCallback(a,b)},useContext:function(a,b){return m().useContext(a,b)},\nuseEffect:function(a,b){return m().useEffect(a,b)},useImperativeHandle:function(a,b,d){return m().useImperativeHandle(a,b,d)},useDebugValue:function(a,b){},useLayoutEffect:function(a,b){return m().useLayoutEffect(a,b)},useMemo:function(a,b){return m().useMemo(a,b)},useReducer:function(a,b,d){return m().useReducer(a,b,d)},useRef:function(a){return m().useRef(a)},useState:function(a){return m().useState(a)},Fragment:r,StrictMode:X,Suspense:Ea,createElement:ea,cloneElement:function(a,b,d){null===a||\nvoid 0===a?q(\"267\",a):void 0;var c=void 0,e=J({},a.props),f=a.key,k=a.ref,h=a._owner;if(null!=b){void 0!==b.ref&&(k=b.ref,h=R.current);void 0!==b.key&&(f=\"\"+b.key);var l=void 0;a.type&&a.type.defaultProps&&(l=a.type.defaultProps);for(c in b)fa.call(b,c)&&!ha.hasOwnProperty(c)&&(e[c]=void 0===b[c]&&void 0!==l?l[c]:b[c])}c=arguments.length-2;if(1===c)e.children=d;else if(1<c){l=Array(c);for(var m=0;m<c;m++)l[m]=arguments[m+2];e.children=l}return{$$typeof:y,type:a.type,key:f,ref:k,props:e,_owner:h}},\ncreateFactory:function(a){var b=ea.bind(null,a);b.type=a;return b},isValidElement:S,version:\"16.8.6\",unstable_ConcurrentMode:Ca,unstable_Profiler:za,__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED:e};r=(X={default:r},r)||X;return r.default||r});\n"
  },
  {
    "path": "vendor/redux.js",
    "content": "/**\n * Redux v.3.6.0\n */\n(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"Redux\"] = factory();\n\telse\n\t\troot[\"Redux\"] = factory();\n})(this, function() {\nreturn /******/ (function(modules) { // webpackBootstrap\n/******/ \t// The module cache\n/******/ \tvar installedModules = {};\n\n/******/ \t// The require function\n/******/ \tfunction __webpack_require__(moduleId) {\n\n/******/ \t\t// Check if module is in cache\n/******/ \t\tif(installedModules[moduleId])\n/******/ \t\t\treturn installedModules[moduleId].exports;\n\n/******/ \t\t// Create a new module (and put it into the cache)\n/******/ \t\tvar module = installedModules[moduleId] = {\n/******/ \t\t\texports: {},\n/******/ \t\t\tid: moduleId,\n/******/ \t\t\tloaded: false\n/******/ \t\t};\n\n/******/ \t\t// Execute the module function\n/******/ \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n/******/ \t\t// Flag the module as loaded\n/******/ \t\tmodule.loaded = true;\n\n/******/ \t\t// Return the exports of the module\n/******/ \t\treturn module.exports;\n/******/ \t}\n\n\n/******/ \t// expose the modules object (__webpack_modules__)\n/******/ \t__webpack_require__.m = modules;\n\n/******/ \t// expose the module cache\n/******/ \t__webpack_require__.c = installedModules;\n\n/******/ \t// __webpack_public_path__\n/******/ \t__webpack_require__.p = \"\";\n\n/******/ \t// Load entry module and return exports\n/******/ \treturn __webpack_require__(0);\n/******/ })\n/************************************************************************/\n/******/ ([\n/* 0 */\n/***/ function(module, exports, __webpack_require__) {\n\n\t'use strict';\n\n\texports.__esModule = true;\n\texports.compose = exports.applyMiddleware = exports.bindActionCreators = exports.combineReducers = exports.createStore = undefined;\n\n\tvar _createStore = __webpack_require__(2);\n\n\tvar _createStore2 = _interopRequireDefault(_createStore);\n\n\tvar _combineReducers = __webpack_require__(7);\n\n\tvar _combineReducers2 = _interopRequireDefault(_combineReducers);\n\n\tvar _bindActionCreators = __webpack_require__(6);\n\n\tvar _bindActionCreators2 = _interopRequireDefault(_bindActionCreators);\n\n\tvar _applyMiddleware = __webpack_require__(5);\n\n\tvar _applyMiddleware2 = _interopRequireDefault(_applyMiddleware);\n\n\tvar _compose = __webpack_require__(1);\n\n\tvar _compose2 = _interopRequireDefault(_compose);\n\n\tvar _warning = __webpack_require__(3);\n\n\tvar _warning2 = _interopRequireDefault(_warning);\n\n\tfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }\n\n\t/*\n\t* This is a dummy function to check if the function name has been altered by minification.\n\t* If the function has been minified and NODE_ENV !== 'production', warn the user.\n\t*/\n\tfunction isCrushed() {}\n\n\tif ((\"development\") !== 'production' && typeof isCrushed.name === 'string' && isCrushed.name !== 'isCrushed') {\n\t  (0, _warning2['default'])('You are currently using minified code outside of NODE_ENV === \\'production\\'. ' + 'This means that you are running a slower development build of Redux. ' + 'You can use loose-envify (https://github.com/zertosh/loose-envify) for browserify ' + 'or DefinePlugin for webpack (http://stackoverflow.com/questions/30030031) ' + 'to ensure you have the correct code for your production build.');\n\t}\n\n\texports.createStore = _createStore2['default'];\n\texports.combineReducers = _combineReducers2['default'];\n\texports.bindActionCreators = _bindActionCreators2['default'];\n\texports.applyMiddleware = _applyMiddleware2['default'];\n\texports.compose = _compose2['default'];\n\n/***/ },\n/* 1 */\n/***/ function(module, exports) {\n\n\t\"use strict\";\n\n\texports.__esModule = true;\n\texports[\"default\"] = compose;\n\t/**\n\t * Composes single-argument functions from right to left. The rightmost\n\t * function can take multiple arguments as it provides the signature for\n\t * the resulting composite function.\n\t *\n\t * @param {...Function} funcs The functions to compose.\n\t * @returns {Function} A function obtained by composing the argument functions\n\t * from right to left. For example, compose(f, g, h) is identical to doing\n\t * (...args) => f(g(h(...args))).\n\t */\n\n\tfunction compose() {\n\t  for (var _len = arguments.length, funcs = Array(_len), _key = 0; _key < _len; _key++) {\n\t    funcs[_key] = arguments[_key];\n\t  }\n\n\t  if (funcs.length === 0) {\n\t    return function (arg) {\n\t      return arg;\n\t    };\n\t  }\n\n\t  if (funcs.length === 1) {\n\t    return funcs[0];\n\t  }\n\n\t  var last = funcs[funcs.length - 1];\n\t  var rest = funcs.slice(0, -1);\n\t  return function () {\n\t    return rest.reduceRight(function (composed, f) {\n\t      return f(composed);\n\t    }, last.apply(undefined, arguments));\n\t  };\n\t}\n\n/***/ },\n/* 2 */\n/***/ function(module, exports, __webpack_require__) {\n\n\t'use strict';\n\n\texports.__esModule = true;\n\texports.ActionTypes = undefined;\n\texports['default'] = createStore;\n\n\tvar _isPlainObject = __webpack_require__(4);\n\n\tvar _isPlainObject2 = _interopRequireDefault(_isPlainObject);\n\n\tvar _symbolObservable = __webpack_require__(12);\n\n\tvar _symbolObservable2 = _interopRequireDefault(_symbolObservable);\n\n\tfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }\n\n\t/**\n\t * These are private action types reserved by Redux.\n\t * For any unknown actions, you must return the current state.\n\t * If the current state is undefined, you must return the initial state.\n\t * Do not reference these action types directly in your code.\n\t */\n\tvar ActionTypes = exports.ActionTypes = {\n\t  INIT: '@@redux/INIT'\n\t};\n\n\t/**\n\t * Creates a Redux store that holds the state tree.\n\t * The only way to change the data in the store is to call `dispatch()` on it.\n\t *\n\t * There should only be a single store in your app. To specify how different\n\t * parts of the state tree respond to actions, you may combine several reducers\n\t * into a single reducer function by using `combineReducers`.\n\t *\n\t * @param {Function} reducer A function that returns the next state tree, given\n\t * the current state tree and the action to handle.\n\t *\n\t * @param {any} [preloadedState] The initial state. You may optionally specify it\n\t * to hydrate the state from the server in universal apps, or to restore a\n\t * previously serialized user session.\n\t * If you use `combineReducers` to produce the root reducer function, this must be\n\t * an object with the same shape as `combineReducers` keys.\n\t *\n\t * @param {Function} enhancer The store enhancer. You may optionally specify it\n\t * to enhance the store with third-party capabilities such as middleware,\n\t * time travel, persistence, etc. The only store enhancer that ships with Redux\n\t * is `applyMiddleware()`.\n\t *\n\t * @returns {Store} A Redux store that lets you read the state, dispatch actions\n\t * and subscribe to changes.\n\t */\n\tfunction createStore(reducer, preloadedState, enhancer) {\n\t  var _ref2;\n\n\t  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {\n\t    enhancer = preloadedState;\n\t    preloadedState = undefined;\n\t  }\n\n\t  if (typeof enhancer !== 'undefined') {\n\t    if (typeof enhancer !== 'function') {\n\t      throw new Error('Expected the enhancer to be a function.');\n\t    }\n\n\t    return enhancer(createStore)(reducer, preloadedState);\n\t  }\n\n\t  if (typeof reducer !== 'function') {\n\t    throw new Error('Expected the reducer to be a function.');\n\t  }\n\n\t  var currentReducer = reducer;\n\t  var currentState = preloadedState;\n\t  var currentListeners = [];\n\t  var nextListeners = currentListeners;\n\t  var isDispatching = false;\n\n\t  function ensureCanMutateNextListeners() {\n\t    if (nextListeners === currentListeners) {\n\t      nextListeners = currentListeners.slice();\n\t    }\n\t  }\n\n\t  /**\n\t   * Reads the state tree managed by the store.\n\t   *\n\t   * @returns {any} The current state tree of your application.\n\t   */\n\t  function getState() {\n\t    return currentState;\n\t  }\n\n\t  /**\n\t   * Adds a change listener. It will be called any time an action is dispatched,\n\t   * and some part of the state tree may potentially have changed. You may then\n\t   * call `getState()` to read the current state tree inside the callback.\n\t   *\n\t   * You may call `dispatch()` from a change listener, with the following\n\t   * caveats:\n\t   *\n\t   * 1. The subscriptions are snapshotted just before every `dispatch()` call.\n\t   * If you subscribe or unsubscribe while the listeners are being invoked, this\n\t   * will not have any effect on the `dispatch()` that is currently in progress.\n\t   * However, the next `dispatch()` call, whether nested or not, will use a more\n\t   * recent snapshot of the subscription list.\n\t   *\n\t   * 2. The listener should not expect to see all state changes, as the state\n\t   * might have been updated multiple times during a nested `dispatch()` before\n\t   * the listener is called. It is, however, guaranteed that all subscribers\n\t   * registered before the `dispatch()` started will be called with the latest\n\t   * state by the time it exits.\n\t   *\n\t   * @param {Function} listener A callback to be invoked on every dispatch.\n\t   * @returns {Function} A function to remove this change listener.\n\t   */\n\t  function subscribe(listener) {\n\t    if (typeof listener !== 'function') {\n\t      throw new Error('Expected listener to be a function.');\n\t    }\n\n\t    var isSubscribed = true;\n\n\t    ensureCanMutateNextListeners();\n\t    nextListeners.push(listener);\n\n\t    return function unsubscribe() {\n\t      if (!isSubscribed) {\n\t        return;\n\t      }\n\n\t      isSubscribed = false;\n\n\t      ensureCanMutateNextListeners();\n\t      var index = nextListeners.indexOf(listener);\n\t      nextListeners.splice(index, 1);\n\t    };\n\t  }\n\n\t  /**\n\t   * Dispatches an action. It is the only way to trigger a state change.\n\t   *\n\t   * The `reducer` function, used to create the store, will be called with the\n\t   * current state tree and the given `action`. Its return value will\n\t   * be considered the **next** state of the tree, and the change listeners\n\t   * will be notified.\n\t   *\n\t   * The base implementation only supports plain object actions. If you want to\n\t   * dispatch a Promise, an Observable, a thunk, or something else, you need to\n\t   * wrap your store creating function into the corresponding middleware. For\n\t   * example, see the documentation for the `redux-thunk` package. Even the\n\t   * middleware will eventually dispatch plain object actions using this method.\n\t   *\n\t   * @param {Object} action A plain object representing â€œwhat changedâ€. It is\n\t   * a good idea to keep actions serializable so you can record and replay user\n\t   * sessions, or use the time travelling `redux-devtools`. An action must have\n\t   * a `type` property which may not be `undefined`. It is a good idea to use\n\t   * string constants for action types.\n\t   *\n\t   * @returns {Object} For convenience, the same action object you dispatched.\n\t   *\n\t   * Note that, if you use a custom middleware, it may wrap `dispatch()` to\n\t   * return something else (for example, a Promise you can await).\n\t   */\n\t  function dispatch(action) {\n\t    if (!(0, _isPlainObject2['default'])(action)) {\n\t      throw new Error('Actions must be plain objects. ' + 'Use custom middleware for async actions.');\n\t    }\n\n\t    if (typeof action.type === 'undefined') {\n\t      throw new Error('Actions may not have an undefined \"type\" property. ' + 'Have you misspelled a constant?');\n\t    }\n\n\t    if (isDispatching) {\n\t      throw new Error('Reducers may not dispatch actions.');\n\t    }\n\n\t    try {\n\t      isDispatching = true;\n\t      currentState = currentReducer(currentState, action);\n\t    } finally {\n\t      isDispatching = false;\n\t    }\n\n\t    var listeners = currentListeners = nextListeners;\n\t    for (var i = 0; i < listeners.length; i++) {\n\t      listeners[i]();\n\t    }\n\n\t    return action;\n\t  }\n\n\t  /**\n\t   * Replaces the reducer currently used by the store to calculate the state.\n\t   *\n\t   * You might need this if your app implements code splitting and you want to\n\t   * load some of the reducers dynamically. You might also need this if you\n\t   * implement a hot reloading mechanism for Redux.\n\t   *\n\t   * @param {Function} nextReducer The reducer for the store to use instead.\n\t   * @returns {void}\n\t   */\n\t  function replaceReducer(nextReducer) {\n\t    if (typeof nextReducer !== 'function') {\n\t      throw new Error('Expected the nextReducer to be a function.');\n\t    }\n\n\t    currentReducer = nextReducer;\n\t    dispatch({ type: ActionTypes.INIT });\n\t  }\n\n\t  /**\n\t   * Interoperability point for observable/reactive libraries.\n\t   * @returns {observable} A minimal observable of state changes.\n\t   * For more information, see the observable proposal:\n\t   * https://github.com/zenparsing/es-observable\n\t   */\n\t  function observable() {\n\t    var _ref;\n\n\t    var outerSubscribe = subscribe;\n\t    return _ref = {\n\t      /**\n\t       * The minimal observable subscription method.\n\t       * @param {Object} observer Any object that can be used as an observer.\n\t       * The observer object should have a `next` method.\n\t       * @returns {subscription} An object with an `unsubscribe` method that can\n\t       * be used to unsubscribe the observable from the store, and prevent further\n\t       * emission of values from the observable.\n\t       */\n\t      subscribe: function subscribe(observer) {\n\t        if (typeof observer !== 'object') {\n\t          throw new TypeError('Expected the observer to be an object.');\n\t        }\n\n\t        function observeState() {\n\t          if (observer.next) {\n\t            observer.next(getState());\n\t          }\n\t        }\n\n\t        observeState();\n\t        var unsubscribe = outerSubscribe(observeState);\n\t        return { unsubscribe: unsubscribe };\n\t      }\n\t    }, _ref[_symbolObservable2['default']] = function () {\n\t      return this;\n\t    }, _ref;\n\t  }\n\n\t  // When a store is created, an \"INIT\" action is dispatched so that every\n\t  // reducer returns their initial state. This effectively populates\n\t  // the initial state tree.\n\t  dispatch({ type: ActionTypes.INIT });\n\n\t  return _ref2 = {\n\t    dispatch: dispatch,\n\t    subscribe: subscribe,\n\t    getState: getState,\n\t    replaceReducer: replaceReducer\n\t  }, _ref2[_symbolObservable2['default']] = observable, _ref2;\n\t}\n\n/***/ },\n/* 3 */\n/***/ function(module, exports) {\n\n\t'use strict';\n\n\texports.__esModule = true;\n\texports['default'] = warning;\n\t/**\n\t * Prints a warning in the console if it exists.\n\t *\n\t * @param {String} message The warning message.\n\t * @returns {void}\n\t */\n\tfunction warning(message) {\n\t  /* eslint-disable no-console */\n\t  if (typeof console !== 'undefined' && typeof console.error === 'function') {\n\t    console.error(message);\n\t  }\n\t  /* eslint-enable no-console */\n\t  try {\n\t    // This error was thrown as a convenience so that if you enable\n\t    // \"break on all exceptions\" in your console,\n\t    // it would pause the execution at this line.\n\t    throw new Error(message);\n\t    /* eslint-disable no-empty */\n\t  } catch (e) {}\n\t  /* eslint-enable no-empty */\n\t}\n\n/***/ },\n/* 4 */\n/***/ function(module, exports, __webpack_require__) {\n\n\tvar getPrototype = __webpack_require__(8),\n\t    isHostObject = __webpack_require__(9),\n\t    isObjectLike = __webpack_require__(11);\n\n\t/** `Object#toString` result references. */\n\tvar objectTag = '[object Object]';\n\n\t/** Used for built-in method references. */\n\tvar funcProto = Function.prototype,\n\t    objectProto = Object.prototype;\n\n\t/** Used to resolve the decompiled source of functions. */\n\tvar funcToString = funcProto.toString;\n\n\t/** Used to check objects for own properties. */\n\tvar hasOwnProperty = objectProto.hasOwnProperty;\n\n\t/** Used to infer the `Object` constructor. */\n\tvar objectCtorString = funcToString.call(Object);\n\n\t/**\n\t * Used to resolve the\n\t * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)\n\t * of values.\n\t */\n\tvar objectToString = objectProto.toString;\n\n\t/**\n\t * Checks if `value` is a plain object, that is, an object created by the\n\t * `Object` constructor or one with a `[[Prototype]]` of `null`.\n\t *\n\t * @static\n\t * @memberOf _\n\t * @since 0.8.0\n\t * @category Lang\n\t * @param {*} value The value to check.\n\t * @returns {boolean} Returns `true` if `value` is a plain object, else `false`.\n\t * @example\n\t *\n\t * function Foo() {\n\t *   this.a = 1;\n\t * }\n\t *\n\t * _.isPlainObject(new Foo);\n\t * // => false\n\t *\n\t * _.isPlainObject([1, 2, 3]);\n\t * // => false\n\t *\n\t * _.isPlainObject({ 'x': 0, 'y': 0 });\n\t * // => true\n\t *\n\t * _.isPlainObject(Object.create(null));\n\t * // => true\n\t */\n\tfunction isPlainObject(value) {\n\t  if (!isObjectLike(value) ||\n\t      objectToString.call(value) != objectTag || isHostObject(value)) {\n\t    return false;\n\t  }\n\t  var proto = getPrototype(value);\n\t  if (proto === null) {\n\t    return true;\n\t  }\n\t  var Ctor = hasOwnProperty.call(proto, 'constructor') && proto.constructor;\n\t  return (typeof Ctor == 'function' &&\n\t    Ctor instanceof Ctor && funcToString.call(Ctor) == objectCtorString);\n\t}\n\n\tmodule.exports = isPlainObject;\n\n\n/***/ },\n/* 5 */\n/***/ function(module, exports, __webpack_require__) {\n\n\t'use strict';\n\n\texports.__esModule = true;\n\n\tvar _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };\n\n\texports['default'] = applyMiddleware;\n\n\tvar _compose = __webpack_require__(1);\n\n\tvar _compose2 = _interopRequireDefault(_compose);\n\n\tfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }\n\n\t/**\n\t * Creates a store enhancer that applies middleware to the dispatch method\n\t * of the Redux store. This is handy for a variety of tasks, such as expressing\n\t * asynchronous actions in a concise manner, or logging every action payload.\n\t *\n\t * See `redux-thunk` package as an example of the Redux middleware.\n\t *\n\t * Because middleware is potentially asynchronous, this should be the first\n\t * store enhancer in the composition chain.\n\t *\n\t * Note that each middleware will be given the `dispatch` and `getState` functions\n\t * as named arguments.\n\t *\n\t * @param {...Function} middlewares The middleware chain to be applied.\n\t * @returns {Function} A store enhancer applying the middleware.\n\t */\n\tfunction applyMiddleware() {\n\t  for (var _len = arguments.length, middlewares = Array(_len), _key = 0; _key < _len; _key++) {\n\t    middlewares[_key] = arguments[_key];\n\t  }\n\n\t  return function (createStore) {\n\t    return function (reducer, preloadedState, enhancer) {\n\t      var store = createStore(reducer, preloadedState, enhancer);\n\t      var _dispatch = store.dispatch;\n\t      var chain = [];\n\n\t      var middlewareAPI = {\n\t        getState: store.getState,\n\t        dispatch: function dispatch(action) {\n\t          return _dispatch(action);\n\t        }\n\t      };\n\t      chain = middlewares.map(function (middleware) {\n\t        return middleware(middlewareAPI);\n\t      });\n\t      _dispatch = _compose2['default'].apply(undefined, chain)(store.dispatch);\n\n\t      return _extends({}, store, {\n\t        dispatch: _dispatch\n\t      });\n\t    };\n\t  };\n\t}\n\n/***/ },\n/* 6 */\n/***/ function(module, exports) {\n\n\t'use strict';\n\n\texports.__esModule = true;\n\texports['default'] = bindActionCreators;\n\tfunction bindActionCreator(actionCreator, dispatch) {\n\t  return function () {\n\t    return dispatch(actionCreator.apply(undefined, arguments));\n\t  };\n\t}\n\n\t/**\n\t * Turns an object whose values are action creators, into an object with the\n\t * same keys, but with every function wrapped into a `dispatch` call so they\n\t * may be invoked directly. This is just a convenience method, as you can call\n\t * `store.dispatch(MyActionCreators.doSomething())` yourself just fine.\n\t *\n\t * For convenience, you can also pass a single function as the first argument,\n\t * and get a function in return.\n\t *\n\t * @param {Function|Object} actionCreators An object whose values are action\n\t * creator functions. One handy way to obtain it is to use ES6 `import * as`\n\t * syntax. You may also pass a single function.\n\t *\n\t * @param {Function} dispatch The `dispatch` function available on your Redux\n\t * store.\n\t *\n\t * @returns {Function|Object} The object mimicking the original object, but with\n\t * every action creator wrapped into the `dispatch` call. If you passed a\n\t * function as `actionCreators`, the return value will also be a single\n\t * function.\n\t */\n\tfunction bindActionCreators(actionCreators, dispatch) {\n\t  if (typeof actionCreators === 'function') {\n\t    return bindActionCreator(actionCreators, dispatch);\n\t  }\n\n\t  if (typeof actionCreators !== 'object' || actionCreators === null) {\n\t    throw new Error('bindActionCreators expected an object or a function, instead received ' + (actionCreators === null ? 'null' : typeof actionCreators) + '. ' + 'Did you write \"import ActionCreators from\" instead of \"import * as ActionCreators from\"?');\n\t  }\n\n\t  var keys = Object.keys(actionCreators);\n\t  var boundActionCreators = {};\n\t  for (var i = 0; i < keys.length; i++) {\n\t    var key = keys[i];\n\t    var actionCreator = actionCreators[key];\n\t    if (typeof actionCreator === 'function') {\n\t      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch);\n\t    }\n\t  }\n\t  return boundActionCreators;\n\t}\n\n/***/ },\n/* 7 */\n/***/ function(module, exports, __webpack_require__) {\n\n\t'use strict';\n\n\texports.__esModule = true;\n\texports['default'] = combineReducers;\n\n\tvar _createStore = __webpack_require__(2);\n\n\tvar _isPlainObject = __webpack_require__(4);\n\n\tvar _isPlainObject2 = _interopRequireDefault(_isPlainObject);\n\n\tvar _warning = __webpack_require__(3);\n\n\tvar _warning2 = _interopRequireDefault(_warning);\n\n\tfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }\n\n\tfunction getUndefinedStateErrorMessage(key, action) {\n\t  var actionType = action && action.type;\n\t  var actionName = actionType && '\"' + actionType.toString() + '\"' || 'an action';\n\n\t  return 'Given action ' + actionName + ', reducer \"' + key + '\" returned undefined. ' + 'To ignore an action, you must explicitly return the previous state.';\n\t}\n\n\tfunction getUnexpectedStateShapeWarningMessage(inputState, reducers, action, unexpectedKeyCache) {\n\t  var reducerKeys = Object.keys(reducers);\n\t  var argumentName = action && action.type === _createStore.ActionTypes.INIT ? 'preloadedState argument passed to createStore' : 'previous state received by the reducer';\n\n\t  if (reducerKeys.length === 0) {\n\t    return 'Store does not have a valid reducer. Make sure the argument passed ' + 'to combineReducers is an object whose values are reducers.';\n\t  }\n\n\t  if (!(0, _isPlainObject2['default'])(inputState)) {\n\t    return 'The ' + argumentName + ' has unexpected type of \"' + {}.toString.call(inputState).match(/\\s([a-z|A-Z]+)/)[1] + '\". Expected argument to be an object with the following ' + ('keys: \"' + reducerKeys.join('\", \"') + '\"');\n\t  }\n\n\t  var unexpectedKeys = Object.keys(inputState).filter(function (key) {\n\t    return !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key];\n\t  });\n\n\t  unexpectedKeys.forEach(function (key) {\n\t    unexpectedKeyCache[key] = true;\n\t  });\n\n\t  if (unexpectedKeys.length > 0) {\n\t    return 'Unexpected ' + (unexpectedKeys.length > 1 ? 'keys' : 'key') + ' ' + ('\"' + unexpectedKeys.join('\", \"') + '\" found in ' + argumentName + '. ') + 'Expected to find one of the known reducer keys instead: ' + ('\"' + reducerKeys.join('\", \"') + '\". Unexpected keys will be ignored.');\n\t  }\n\t}\n\n\tfunction assertReducerSanity(reducers) {\n\t  Object.keys(reducers).forEach(function (key) {\n\t    var reducer = reducers[key];\n\t    var initialState = reducer(undefined, { type: _createStore.ActionTypes.INIT });\n\n\t    if (typeof initialState === 'undefined') {\n\t      throw new Error('Reducer \"' + key + '\" returned undefined during initialization. ' + 'If the state passed to the reducer is undefined, you must ' + 'explicitly return the initial state. The initial state may ' + 'not be undefined.');\n\t    }\n\n\t    var type = '@@redux/PROBE_UNKNOWN_ACTION_' + Math.random().toString(36).substring(7).split('').join('.');\n\t    if (typeof reducer(undefined, { type: type }) === 'undefined') {\n\t      throw new Error('Reducer \"' + key + '\" returned undefined when probed with a random type. ' + ('Don\\'t try to handle ' + _createStore.ActionTypes.INIT + ' or other actions in \"redux/*\" ') + 'namespace. They are considered private. Instead, you must return the ' + 'current state for any unknown actions, unless it is undefined, ' + 'in which case you must return the initial state, regardless of the ' + 'action type. The initial state may not be undefined.');\n\t    }\n\t  });\n\t}\n\n\t/**\n\t * Turns an object whose values are different reducer functions, into a single\n\t * reducer function. It will call every child reducer, and gather their results\n\t * into a single state object, whose keys correspond to the keys of the passed\n\t * reducer functions.\n\t *\n\t * @param {Object} reducers An object whose values correspond to different\n\t * reducer functions that need to be combined into one. One handy way to obtain\n\t * it is to use ES6 `import * as reducers` syntax. The reducers may never return\n\t * undefined for any action. Instead, they should return their initial state\n\t * if the state passed to them was undefined, and the current state for any\n\t * unrecognized action.\n\t *\n\t * @returns {Function} A reducer function that invokes every reducer inside the\n\t * passed object, and builds a state object with the same shape.\n\t */\n\tfunction combineReducers(reducers) {\n\t  var reducerKeys = Object.keys(reducers);\n\t  var finalReducers = {};\n\t  for (var i = 0; i < reducerKeys.length; i++) {\n\t    var key = reducerKeys[i];\n\n\t    if (true) {\n\t      if (typeof reducers[key] === 'undefined') {\n\t        (0, _warning2['default'])('No reducer provided for key \"' + key + '\"');\n\t      }\n\t    }\n\n\t    if (typeof reducers[key] === 'function') {\n\t      finalReducers[key] = reducers[key];\n\t    }\n\t  }\n\t  var finalReducerKeys = Object.keys(finalReducers);\n\n\t  if (true) {\n\t    var unexpectedKeyCache = {};\n\t  }\n\n\t  var sanityError;\n\t  try {\n\t    assertReducerSanity(finalReducers);\n\t  } catch (e) {\n\t    sanityError = e;\n\t  }\n\n\t  return function combination() {\n\t    var state = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];\n\t    var action = arguments[1];\n\n\t    if (sanityError) {\n\t      throw sanityError;\n\t    }\n\n\t    if (true) {\n\t      var warningMessage = getUnexpectedStateShapeWarningMessage(state, finalReducers, action, unexpectedKeyCache);\n\t      if (warningMessage) {\n\t        (0, _warning2['default'])(warningMessage);\n\t      }\n\t    }\n\n\t    var hasChanged = false;\n\t    var nextState = {};\n\t    for (var i = 0; i < finalReducerKeys.length; i++) {\n\t      var key = finalReducerKeys[i];\n\t      var reducer = finalReducers[key];\n\t      var previousStateForKey = state[key];\n\t      var nextStateForKey = reducer(previousStateForKey, action);\n\t      if (typeof nextStateForKey === 'undefined') {\n\t        var errorMessage = getUndefinedStateErrorMessage(key, action);\n\t        throw new Error(errorMessage);\n\t      }\n\t      nextState[key] = nextStateForKey;\n\t      hasChanged = hasChanged || nextStateForKey !== previousStateForKey;\n\t    }\n\t    return hasChanged ? nextState : state;\n\t  };\n\t}\n\n/***/ },\n/* 8 */\n/***/ function(module, exports, __webpack_require__) {\n\n\tvar overArg = __webpack_require__(10);\n\n\t/** Built-in value references. */\n\tvar getPrototype = overArg(Object.getPrototypeOf, Object);\n\n\tmodule.exports = getPrototype;\n\n\n/***/ },\n/* 9 */\n/***/ function(module, exports) {\n\n\t/**\n\t * Checks if `value` is a host object in IE < 9.\n\t *\n\t * @private\n\t * @param {*} value The value to check.\n\t * @returns {boolean} Returns `true` if `value` is a host object, else `false`.\n\t */\n\tfunction isHostObject(value) {\n\t  // Many host objects are `Object` objects that can coerce to strings\n\t  // despite having improperly defined `toString` methods.\n\t  var result = false;\n\t  if (value != null && typeof value.toString != 'function') {\n\t    try {\n\t      result = !!(value + '');\n\t    } catch (e) {}\n\t  }\n\t  return result;\n\t}\n\n\tmodule.exports = isHostObject;\n\n\n/***/ },\n/* 10 */\n/***/ function(module, exports) {\n\n\t/**\n\t * Creates a unary function that invokes `func` with its argument transformed.\n\t *\n\t * @private\n\t * @param {Function} func The function to wrap.\n\t * @param {Function} transform The argument transform.\n\t * @returns {Function} Returns the new function.\n\t */\n\tfunction overArg(func, transform) {\n\t  return function(arg) {\n\t    return func(transform(arg));\n\t  };\n\t}\n\n\tmodule.exports = overArg;\n\n\n/***/ },\n/* 11 */\n/***/ function(module, exports) {\n\n\t/**\n\t * Checks if `value` is object-like. A value is object-like if it's not `null`\n\t * and has a `typeof` result of \"object\".\n\t *\n\t * @static\n\t * @memberOf _\n\t * @since 4.0.0\n\t * @category Lang\n\t * @param {*} value The value to check.\n\t * @returns {boolean} Returns `true` if `value` is object-like, else `false`.\n\t * @example\n\t *\n\t * _.isObjectLike({});\n\t * // => true\n\t *\n\t * _.isObjectLike([1, 2, 3]);\n\t * // => true\n\t *\n\t * _.isObjectLike(_.noop);\n\t * // => false\n\t *\n\t * _.isObjectLike(null);\n\t * // => false\n\t */\n\tfunction isObjectLike(value) {\n\t  return !!value && typeof value == 'object';\n\t}\n\n\tmodule.exports = isObjectLike;\n\n\n/***/ },\n/* 12 */\n/***/ function(module, exports, __webpack_require__) {\n\n\tmodule.exports = __webpack_require__(13);\n\n\n/***/ },\n/* 13 */\n/***/ function(module, exports, __webpack_require__) {\n\n\t/* WEBPACK VAR INJECTION */(function(global) {'use strict';\n\n\tObject.defineProperty(exports, \"__esModule\", {\n\t\tvalue: true\n\t});\n\n\tvar _ponyfill = __webpack_require__(14);\n\n\tvar _ponyfill2 = _interopRequireDefault(_ponyfill);\n\n\tfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }\n\n\tvar root = undefined; /* global window */\n\n\tif (typeof global !== 'undefined') {\n\t\troot = global;\n\t} else if (typeof window !== 'undefined') {\n\t\troot = window;\n\t}\n\n\tvar result = (0, _ponyfill2['default'])(root);\n\texports['default'] = result;\n\t/* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }())))\n\n/***/ },\n/* 14 */\n/***/ function(module, exports) {\n\n\t'use strict';\n\n\tObject.defineProperty(exports, \"__esModule\", {\n\t\tvalue: true\n\t});\n\texports['default'] = symbolObservablePonyfill;\n\tfunction symbolObservablePonyfill(root) {\n\t\tvar result;\n\t\tvar _Symbol = root.Symbol;\n\n\t\tif (typeof _Symbol === 'function') {\n\t\t\tif (_Symbol.observable) {\n\t\t\t\tresult = _Symbol.observable;\n\t\t\t} else {\n\t\t\t\tresult = _Symbol('observable');\n\t\t\t\t_Symbol.observable = result;\n\t\t\t}\n\t\t} else {\n\t\t\tresult = '@@observable';\n\t\t}\n\n\t\treturn result;\n\t};\n\n/***/ }\n/******/ ])\n});\n;\n"
  },
  {
    "path": "webpack.aboutlibrary.config.js",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nconst path = require(\"path\");\nconst config = require(\"./webpack.system-addon.config.js\");\nconst absolute = relPath => path.join(__dirname, relPath);\nmodule.exports = Object.assign({}, config(), {\n  entry: absolute(\"content-src/aboutlibrary/aboutlibrary.jsx\"),\n  output: {\n    path: absolute(\"aboutlibrary/content\"),\n    filename: \"aboutlibrary.bundle.js\",\n  },\n});\n"
  },
  {
    "path": "webpack.system-addon.config.js",
    "content": "/* This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this file,\n * You can obtain one at http://mozilla.org/MPL/2.0/. */\n\nconst path = require(\"path\");\nconst webpack = require(\"webpack\");\n\nconst absolute = relPath => path.join(__dirname, relPath);\n\nconst resourcePathRegEx = /^resource:\\/\\/activity-stream\\//;\n\nmodule.exports = (env = {}) => ({\n  mode: \"none\",\n  entry: absolute(\"content-src/activity-stream.jsx\"),\n  output: {\n    path: absolute(\"data/content\"),\n    filename: \"activity-stream.bundle.js\",\n  },\n  // TODO: switch to eval-source-map for faster builds. Requires CSP changes\n  devtool: env.development ? \"inline-source-map\" : false,\n  plugins: [\n    new webpack.BannerPlugin(\n      `THIS FILE IS AUTO-GENERATED: ${path.basename(__filename)}`\n    ),\n    new webpack.optimize.ModuleConcatenationPlugin(),\n  ],\n  module: {\n    rules: [\n      {\n        test: /\\.jsx?$/,\n        exclude: /node_modules\\/(?!(fluent|fluent-react)\\/).*/,\n        loader: \"babel-loader\",\n        options: {\n          presets: [\"@babel/preset-react\"],\n          plugins: [[\"@babel/plugin-proposal-async-generator-functions\"]],\n        },\n      },\n      {\n        test: /\\.jsm$/,\n        exclude: /node_modules/,\n        loader: \"babel-loader\",\n        // Converts .jsm files into common-js modules\n        options: {\n          plugins: [\n            [\n              \"jsm-to-esmodules\",\n              {\n                basePath: resourcePathRegEx,\n                removeOtherImports: true,\n                replace: true,\n              },\n            ],\n          ],\n        },\n      },\n    ],\n  },\n  // This resolve config allows us to import with paths relative to the root directory, e.g. \"lib/ActivityStream.jsm\"\n  resolve: {\n    extensions: [\".js\", \".jsx\"],\n    modules: [\"node_modules\", \".\"],\n  },\n  externals: {\n    \"prop-types\": \"PropTypes\",\n    react: \"React\",\n    \"react-dom\": \"ReactDOM\",\n    redux: \"Redux\",\n    \"react-redux\": \"ReactRedux\",\n    \"react-transition-group\": \"ReactTransitionGroup\",\n  },\n});\n"
  },
  {
    "path": "yamscripts.yml",
    "content": "# This file compiles to package.json scripts.\n# When you add or modify anything, you *MUST* run:\n#      npm run yamscripts\n# to compile your changes.\n\nscripts:\n  # Run the activity-stream mochitests\n  mochitest: (cd $npm_package_config_mc_dir && ./mach mochitest browser/components/newtab/test/browser --headless)\n\n  # Run the activity-stream mochitests with the browser toolbox debugger.\n  # Often handy in combination with adding a \"debugger\" statement in your\n  # mochitest somewhere.\n  mochitest-debug: (cd $npm_package_config_mc_dir && ./mach mochitest --jsdebugger browser/components/newtab/test/browser)\n\n# bundle: Build all assets for activity stream\n  bundle:\n    webpack: webpack --config webpack.system-addon.config.js\n    css: node-sass content-src/styles -o css\n    html: rimraf prerendered && node ./bin/render-activity-stream-html.js\n\n# buildmc: Export code to mozilla central\n  buildmc:\n    pre: rimraf $npm_package_config_mc_dir/browser/components/newtab/\n    bundle: => bundle\n    copy: rsync --exclude-from .mcignore -a . $npm_package_config_mc_dir/browser/components/newtab/\n    copyPingCentre: cpx \"ping-centre/PingCentre.jsm\" $npm_package_config_mc_dir/browser/modules\n\n# builduplift: Build and export to mozilla central for uplifts without exporting strings to browser/locales\n  builduplift:\n    pre: =>prebuildmc\n    bundle: => bundle\n    copy: =>buildmc:copy\n\n# buildlibrary: Export about:library code to mozilla-central - intentionally not included in buildmc for now\n  buildlibrary:\n    webpack: webpack --config webpack.aboutlibrary.config.js\n    css: node-sass --source-map true --source-map-contents content-src/aboutlibrary -o aboutlibrary/content\n    copy: cpx \"aboutlibrary/**/{,.}*\" $npm_package_config_mc_dir/browser/components/library\n\n# startmc: Automatically rebuild/export to mozilla central when files are changed. NOTE: Includes sourcemaps, do not use for profiling/perf testing.\n  startmc:\n    _parallel: true\n    pre: =>buildmc\n    # This copies only the system addon sub-folder; changing anything outside of it will need a full rebuild.\n    copy: cpx \"{{,.}*,!(node_modules)/**/{,.}*}\" $npm_package_config_mc_dir/browser/components/newtab/ -w\n    copyPingCentre: =>buildmc:copyPingCentre -- -w\n    watch: =>watchmc\n\n# watchmc: same as startmc, without the copy behavior which is not needed when working directly from mozilla-central\n  watchmc:\n    _parallel: true\n    webpack: =>bundle:webpack -- --env.development -w\n    css: =>bundle:css && =>bundle:css -- --source-map-embed --source-map-contents -w\n\n  # importmc: Import changes from mc to github repo\n  importmc:\n    src: rsync --exclude-from .mcignore -a $npm_package_config_mc_dir/browser/components/newtab/ .\n\n  testmc:\n    lint: =>lint\n    build: =>bundle:webpack\n    unit: karma start karma.mc.config.js\n\n  tddmc: karma start karma.mc.config.js --tdd\n\n  debugcoverage: open logs/coverage/index.html\n\n# lint: Run eslint and sass-lint\n  lint:\n    eslint-check: eslint --cache --print-config AboutNewTabService.jsm | eslint-config-prettier-check\n    eslint: eslint --cache --ext=.js,.jsm,.jsx .\n    sasslint: sass-lint -v -q\n\n# test: Run all tests once\n  test: =>testmc\n\n# tdd: Run content tests continuously\n  tdd: =>tddmc\n\n  # Utility scripts for use when vendoring in Node packages\n  vendor:\n    react: node ./bin/vendor-react.js\n\n  fix:\n    # Note that since we're currently running eslint-plugin-prettier,\n    # running fix:eslint will also reformat changed JS files using prettier.\n    eslint: =>lint:eslint -- --fix\n"
  }
]