text: /**
   * @license
   * Copyright 2016 Google Inc.
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *     http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  
  /**
   * Add a set of http plugin tests, for the given scheme plugin.
   *
   * @param {boolean} usingFetch True if this should use fetch, false otherwise.
   */
  function httpPluginTests(usingFetch) {
    // Neither plugin uses the request type, so this is arbitrary.
    const requestType = shaka.net.NetworkingEngine.RequestType.MANIFEST;
  
    /** @type {shaka.extern.RetryParameters} */
    let retryParameters;
  
    /** @type {shaka.extern.SchemePlugin} */
    let plugin;
  
    beforeAll(function() {
      plugin = usingFetch ? shaka.net.HttpFetchPlugin : shaka.net.HttpXHRPlugin;
      PromiseMock.install();
  
      if (usingFetch) {
        // Install the mock only briefly in the global namespace, to get a handle
        // to the mocked fetch implementation.
        jasmine.Fetch.install();
        let MockFetch = window.fetch;
        const MockAbortController = window.AbortController;
        const MockHeaders = window.Headers;
        jasmine.Fetch.uninstall();
        // Now plug this mock into HttpRequest directly, so it does not interfere
        // with other requests, such as those made by karma frameworks like
        // source-map-support.
        shaka.net.HttpFetchPlugin['fetch_'] = MockFetch;
        shaka.net.HttpFetchPlugin['AbortController_'] = MockAbortController;
        shaka.net.HttpFetchPlugin['Headers_'] = MockHeaders;
      } else {
        // Install the mock only briefly in the global namespace, to get a handle
        // to the mocked XHR implementation.
        jasmine.Ajax.install();
        const jasmineXHRMock = window.XMLHttpRequest;
        jasmine.Ajax.uninstall();
  
        // Wrap event handlers to catch errors
        const MockXHR = function() {
          const instance = new jasmineXHRMock();
  
          ['abort', 'load', 'error', 'timeout'].forEach(function(eventName) {
            const eventHandlerName = 'on' + eventName;
            let eventHandler = null;
  
            Object.defineProperty(instance, eventHandlerName, {
              set: function(callback) {
                eventHandler = function(event) {
                  // If an event handler throws, the test should fail, since
                  // errors should be passed as reasons to `reject()`. Otherwise
                  // we would leave the Promise in a pending state.
                  try {
                    callback(event);
                  } catch (error) {
                    fail('Uncaught error in XMLHttpRequest#' + eventHandlerName);
                  }
                };
              },
              get: function() {
                return eventHandler;
              }
            });
          });
  
          return instance;
        };
  
  
        // Now plug this mock into HttpRequest directly, so it does not interfere
        // with other requests, such as those made by karma frameworks like
        // source-map-support.
        shaka.net.HttpXHRPlugin['xhr_'] = MockXHR;
      }
  
      jasmine.clock().install();
  
      stubRequest('https://foo.bar/').andReturn({
        'response': new ArrayBuffer(10),
        'status': 200,
        'responseHeaders': {'FOO': 'BAR'}
      });
      stubRequest('https://foo.bar/202').andReturn({
        'response': new ArrayBuffer(0),
        'status': 202
      });
      stubRequest('https://foo.bar/204').andReturn({
        'response': new ArrayBuffer(10),
        'status': 204,
        'responseHeaders': {'FOO': 'BAR'}
      });
      stubRequest('https://foo.bar/withemptyline').andReturn({
        'response': new ArrayBuffer(0),
        'status': 200,
        'responseHeaders': {'\nFOO': 'BAR'}
      });
      stubRequest('https://foo.bar/302').andReturn({
        'response': new ArrayBuffer(10),
        'status': 200,
        'responseHeaders': {'FOO': 'BAR'},
        'responseURL': 'https://foo.bar/after/302'
      });
      stubRequest('https://foo.bar/401').andReturn({
        'response': new ArrayBuffer(0),
        'status': 401
      });
      stubRequest('https://foo.bar/403').andReturn({
        'response': new ArrayBuffer(0),
        'status': 403
      });
      stubRequest('https://foo.bar/404').andReturn({
        'response': new Uint8Array([65, 66, 67]).buffer, // "ABC"
        'status': 404,
        'responseHeaders': {'FOO': 'BAR'}
      });
      stubRequest('https://foo.bar/cache').andReturn({
        'response': new ArrayBuffer(0),
        'status': 200,
        'responseHeaders': {'X-Shaka-From-Cache': 'true'}
      });
  
      stubRequest('https://foo.bar/timeout').andTimeout();
      stubRequest('https://foo.bar/error').andError();
  
      retryParameters = shaka.net.NetworkingEngine.defaultRetryParameters();
      retryParameters.timeout = 4000;
    });
  
    afterAll(function() {
      if (usingFetch) {
        shaka.net.HttpFetchPlugin['fetch_'] = window.fetch;
        shaka.net.HttpFetchPlugin['AbortController_'] = window.AbortController;
        shaka.net.HttpFetchPlugin['Headers_'] = window.Headers;
      } else {
        shaka.net.HttpXHRPlugin['xhr_'] = window.XMLHttpRequest;
      }
      jasmine.clock().uninstall();
      PromiseMock.uninstall();
    });
  
    it('sets the correct fields', function(done) {
      let request = shaka.net.NetworkingEngine.makeRequest(
          ['https://foo.bar/'], retryParameters);
      request.allowCrossSiteCredentials = true;
      request.method = 'POST';
      request.headers['BAZ'] = '123';
  
      plugin(request.uris[0], request, requestType).promise
          .then(function() {
            let actual = mostRecentRequest();
            expect(actual).toBeTruthy();
            expect(actual.url).toBe(request.uris[0]);
            expect(actual.method).toBe(request.method);
            expect(actual.withCredentials).toBe(true);
            // Headers are normalized into lowercase, so 'BAZ' becomes 'baz'.
            expect(actual.requestHeaders['baz']).toBe('123');
          })
          .catch(fail)
          .then(done);
      PromiseMock.flush();
    });
  
    if (usingFetch) {
      // Regression test for an issue with Edge, where Fetch fails if the body
      // is set to null but succeeds on undefined.
      it('sets a request\'s null body to undefined', function(done) {
        let request = shaka.net.NetworkingEngine.makeRequest(
            ['https://foo.bar/'], retryParameters);
        request.body = null;
        request.method = 'GET';
  
        plugin(request.uris[0], request, requestType).promise
            .then(function() {
              let actual = jasmine.Fetch.requests.mostRecent();
              expect(actual).toBeTruthy();
              expect(actual.body).toBeUndefined();
            })
            .catch(fail)
            .then(done);
        PromiseMock.flush();
      });
    }
  
    it('fails with 202 status', function(done) {
      testFails('https://foo.bar/202', done);
      PromiseMock.flush();
    });
  
    it('succeeds with 204 status', function(done) {
      testSucceeds('https://foo.bar/204', done);
      PromiseMock.flush();
    });
  
    it('succeeds with empty line in response', function(done) {
      testSucceedsWithEmptyLine('https://foo.bar/withemptyline', done);
      PromiseMock.flush();
    });
  
    it('gets redirect URLs with 302 status', function(done) {
      testSucceeds('https://foo.bar/302', done,
                   'https://foo.bar/after/302');
      PromiseMock.flush();
    });
  
    it('fails with CRITICAL for 401 status', function(done) {
      testFails('https://foo.bar/401', done, shaka.util.Error.Severity.CRITICAL);
      PromiseMock.flush();
    });
  
    it('fails with CRITICAL for 403 status', function(done) {
      testFails('https://foo.bar/403', done, shaka.util.Error.Severity.CRITICAL);
      PromiseMock.flush();
    });
  
    it('fails if non-2xx status', function(done) {
      const uri = 'https://foo.bar/404';
      testFails(uri, done, undefined, shaka.util.Error.Code.BAD_HTTP_STATUS,
          [uri, 404, 'ABC', {'foo': 'BAR'}, requestType]);
      PromiseMock.flush();
    });
  
    it('fails on timeout', function(done) {
      const uri = 'https://foo.bar/timeout';
      testFails(uri, done, shaka.util.Error.Severity.RECOVERABLE,
          shaka.util.Error.Code.TIMEOUT, [uri, requestType]);
  
      // When using fetch, timeout is handled manually by the plugin, instead of
      // being done by the mocking framework, so we need to actually wait.
      if (usingFetch) {
        jasmine.clock().tick(5000);
      }
      PromiseMock.flush();
    });
  
    it('fails on error', function(done) {
      const uri = 'https://foo.bar/error';
      testFails(uri, done, shaka.util.Error.Severity.RECOVERABLE,
          shaka.util.Error.Code.HTTP_ERROR,
          [uri, jasmine.any(Object), requestType]);
      PromiseMock.flush();
    });
  
    it('detects cache headers', function(done) {
      let request = shaka.net.NetworkingEngine.makeRequest(
          ['https://foo.bar/cache'], retryParameters);
      plugin(request.uris[0], request, requestType).promise
          .catch(fail)
          .then(function(response) {
            expect(response).toBeTruthy();
            expect(response.fromCache).toBe(true);
          })
          .then(done);
      PromiseMock.flush();
    });
  
    it('aborts the request when the operation is aborted', function(done) {
      let abortPromise;
      let requestPromise;
      let oldXHRMock = shaka.net.HttpXHRPlugin['xhr_'];
      if (usingFetch) {
        let request = shaka.net.NetworkingEngine.makeRequest(
            ['https://foo.bar/timeout'], retryParameters);
        let operation = plugin(request.uris[0], request, requestType);
  
        /** @type {jasmine.Fetch.RequestStub} */
        let actual = jasmine.Fetch.requests.mostRecent();
  
        requestPromise = operation.promise;
  
        expect(actual.aborted).toBe(false);
        abortPromise = operation.abort();
        jasmine.clock().tick(400);
        PromiseMock.flush();
        expect(actual.aborted).toBe(true);
      } else {
        /** @type {shaka.extern.IAbortableOperation.<shaka.extern.Response>} */
        let operation;
  
        // Jasmine-ajax stubbed requests are purely synchronous, so we can't
        // actually insert a call to abort in the middle.
        // Instead, install a very elementary mock.
        /** @constructor */
        let newXHRMock = function() {
          this.abort = shaka.test.Util.spyFunc(jasmine.createSpy('abort'));
  
          this.open = shaka.test.Util.spyFunc(jasmine.createSpy('open'));
  
          /** @type {function()} */
          this.onabort;
  
          this.send = function() {
            // Delay the effects of send until after operation is defined.
            Promise.resolve().then(function() {
              expect(this.abort).not.toHaveBeenCalled();
              operation.abort();
              expect(this.abort).toHaveBeenCalled();
              this.onabort();
            }.bind(this));
          };
        };
        shaka.net.HttpXHRPlugin['xhr_'] = newXHRMock;
  
        let request = shaka.net.NetworkingEngine.makeRequest(
            ['https://foo.bar/'], retryParameters);
        operation = plugin(request.uris[0], request, requestType);
        requestPromise = operation.promise;
      }
  
      requestPromise = requestPromise.then(fail).catch((error) => {
        expect(error.code).toBe(shaka.util.Error.Code.OPERATION_ABORTED);
      });
  
      Promise.all([abortPromise, requestPromise]).catch(fail).then(done);
      PromiseMock.flush();
      shaka.net.HttpXHRPlugin['xhr_'] = oldXHRMock;
    });
  
    /**
     * @param {string} uri
     * @return {jasmine.Ajax.Stub|jasmine.Fetch.RequestStub}
     */
    function stubRequest(uri) {
      if (usingFetch) {
        return jasmine.Fetch.stubRequest(uri);
      } else {
        return jasmine.Ajax.stubRequest(uri);
      }
    }
  
    /**
     * @param {string} uri
     * @param {function()} done
     * @param {string=} overrideUri
     */
    function testSucceeds(uri, done, overrideUri) {
      let request = shaka.net.NetworkingEngine.makeRequest(
          [uri], retryParameters);
      plugin(uri, request, requestType).promise
          .catch(fail)
          .then(function(response) {
            expect(mostRecentRequest().url).toBe(uri);
            expect(response).toBeTruthy();
            expect(response.uri).toBe(overrideUri || uri);
            expect(response.data).toBeTruthy();
            expect(response.data.byteLength).toBe(10);
            expect(response.fromCache).toBe(false);
            expect(response.headers).toBeTruthy();
            // Returned header names are in lowercase.
            expect(response.headers['foo']).toBe('BAR');
          })
          .then(done);
    }
  
    /**
     * @param {string} uri
     * @param {function()} done
     * @param {shaka.util.Error.Severity=} severity
     * @param {shaka.util.Error.Code=} code
     * @param {Array<*>=} errorData
     */
    function testFails(uri, done, severity, code, errorData) {
      let request = shaka.net.NetworkingEngine.makeRequest(
          [uri], retryParameters);
      plugin(uri, request, requestType).promise
          .then(fail)
          .catch(function(error) {
            expect(error).toBeTruthy();
            expect(error.severity)
                .toBe(severity || shaka.util.Error.Severity.RECOVERABLE);
            if (code) {
              expect(error.code).toBe(code);
            }
            expect(error.category).toBe(shaka.util.Error.Category.NETWORK);
            if (errorData) {
              expect(error.data).toEqual(errorData);
            }
  
            expect(mostRecentRequest().url).toBe(uri);
          })
          .then(done);
    }
  
    /**
     * Since IE/Edge incorrectly return the header with a leading new line
     * character ('\n'), we need to trim the response header.
     * @param {string} uri
     * @param {function()} done
     * @param {string=} overrideUri
     */
    function testSucceedsWithEmptyLine(uri, done, overrideUri) {
      let request = shaka.net.NetworkingEngine.makeRequest(
          [uri], retryParameters);
      plugin(uri, request, requestType).promise
          .catch(fail)
          .then(function(response) {
            expect(mostRecentRequest().url).toBe(uri);
            expect(response).toBeTruthy();
            expect(response.uri).toBe(overrideUri || uri);
            expect(response.data).toBeTruthy();
            expect(response.fromCache).toBe(false);
            expect(response.headers).toBeTruthy();
            // Returned header names do not contain empty lines.
            expect(response.headers['foo']).toBe('BAR');
          })
          .then(done);
    }
  
    /**
     * @return {jasmine.Ajax.RequestStub}
     */
    function mostRecentRequest() {
      if (usingFetch) {
        let mostRecent = jasmine.Fetch.requests.mostRecent();
        if (mostRecent) {
          // Convert from jasmine.Fetch.RequestStub to jasmine.Ajax.RequestStub
          return /** @type {jasmine.Ajax.RequestStub} */({
            url: mostRecent.url,
            query: mostRecent.query,
            data: mostRecent.data,
            method: mostRecent.method,
            requestHeaders: mostRecent.requestHeaders,
            withCredentials: mostRecent.withCredentials
          });
        }
      }
      return jasmine.Ajax.requests.mostRecent();
    }
  }
  
  describe('HttpXHRPlugin', httpPluginTests.bind(null, false));
  describe('HttpFetchPlugin', httpPluginTests.bind(null, true));
  
  