UNPKG

tus-js-client-stall-detection

Version:

A pure JavaScript client for the tus resumable upload protocol (fork with stall detection)

3,901 lines 126 kB
(function (tusJsClient) {
  'use strict';

  /**
   * Helper function to create a Blob from a string.
   */
  function getBlob(str) {
    return new Blob(str.split(''))
  }

  /**
   * Create a promise and obtain the resolve/reject functions
   * outside of the Promise callback.
   */
  function flatPromise() {
    let resolveFn;
    let rejectFn;
    const p = new Promise((resolve, reject) => {
      resolveFn = resolve;
      rejectFn = reject;
    });

    return [p, resolveFn, rejectFn]
  }

  /**
   * Create a spy-able function which resolves a Promise
   * once it is called.
   */
  function waitableFunction(name = 'func') {
    const [promise, resolve] = flatPromise();
    const fn = jasmine.createSpy(name, resolve).and.callThrough();

    fn.toBeCalled = () => promise;
    return fn
  }

  /**
   * Create a Promise that resolves after the specified duration.
   */
  function wait(delay) {
    return new Promise((resolve) => {
      setTimeout(resolve, delay, 'timed out');
    })
  }

  /**
   * TestHttpStack implements the HTTP stack interface for tus-js-client
   * and can be used to assert outgoing requests and respond with mock data.
   */
  class TestHttpStack {
    constructor() {
      this._pendingRequests = [];
      this._pendingWaits = [];
    }

    createRequest(method, url) {
      return new TestRequest(method, url, (req) => {
        if (this._pendingWaits.length >= 1) {
          const handler = this._pendingWaits.shift();
          handler(req);
          return
        }

        this._pendingRequests.push(req);
      })
    }

    nextRequest() {
      if (this._pendingRequests.length >= 1) {
        return Promise.resolve(this._pendingRequests.shift())
      }

      return new Promise((resolve) => {
        this._pendingWaits.push(resolve);
      })
    }

    supportsProgressEvents() {
      // Test HTTP stack supports progress events for testing purposes
      return true
    }
  }

  class TestRequest {
    constructor(method, url, onRequestSend) {
      this.method = method;
      this.url = url;
      this.requestHeaders = {};
      this.body = null;
      this.bodySize = null;

      this._onRequestSend = onRequestSend;
      this._onProgress = () => {}
      ;[this._requestPromise, this._resolveRequest, this._rejectRequest] = flatPromise();
    }

    getMethod() {
      return this.method
    }

    getURL() {
      return this.url
    }

    setHeader(header, value) {
      this.requestHeaders[header] = value;
    }

    getHeader(header) {
      return this.requestHeaders[header] || null
    }

    setProgressHandler(progressHandler) {
      this._onProgress = progressHandler;
    }

    async send(body = null) {
      this.body = body;

      if (body) {
        this.bodySize = await getBodySize$1(body);

        this._onProgress(0);
        this._onProgress(this.bodySize);
      }

      this._onRequestSend(this);
      return this._requestPromise
    }

    abort() {
      this._rejectRequest(new Error('request aborted'));
    }

    getUnderlyingObject() {
      throw new Error('not implemented')
    }

    respondWith(resData) {
      resData.responseHeaders = resData.responseHeaders || {};

      const res = new TestResponse(resData);
      this._resolveRequest(res);
    }

    responseError(err) {
      this._rejectRequest(err);
    }
  }

  async function getBodySize$1(body) {
    if (body == null) {
      return null
    }

    if (
      body instanceof ArrayBuffer ||
      (typeof SharedArrayBuffer !== 'undefined' && body instanceof SharedArrayBuffer) ||
      ArrayBuffer.isView(body)
    ) {
      return body.byteLength
    }

    if (body instanceof Blob) {
      return body.size
    }

    if (body.length != null) {
      return body.length
    }

    return new Promise((resolve) => {
      body.on('readable', () => {
        while (true) {
          const chunk = body.read();
          if (chunk == null) break

          resolve(chunk.length);
        }
      });
    })
  }

  class TestResponse {
    constructor(res) {
      this._response = res;
    }

    getStatus() {
      return this._response.status
    }

    getHeader(header) {
      return this._response.responseHeaders[header]
    }

    getBody() {
      return this._response.responseText
    }

    getUnderlyingObject() {
      throw new Error('not implemented')
    }
  }

  // Uncomment to enable debug log from tus-js-client
  // tus.enableDebugLog();

  describe('tus', () => {
    describe('#isSupported', () => {
      it('should be true', () => {
        expect(tusJsClient.isSupported).toBe(true);
      });
    });

    describe('#Upload', () => {
      it('should throw if no error handler is available', () => {
        const upload = new tusJsClient.Upload(null);
        expect(upload.start.bind(upload)).toThrowError('tus: no file or stream to upload provided');
      });

      it('should throw if no endpoint and upload URL is provided', () => {
        const file = getBlob('hello world');
        const upload = new tusJsClient.Upload(file);
        expect(upload.start.bind(upload)).toThrowError(
          'tus: neither an endpoint or an upload URL is provided',
        );
      });

      it('should upload a file', async () => {
        const testStack = new TestHttpStack();
        const file = getBlob('hello world');
        const options = {
          httpStack: testStack,
          endpoint: 'https://tus.io/uploads',
          headers: {
            Custom: 'blargh',
          },
          metadata: {
            foo: 'hello',
            bar: 'world',
            nonlatin: 'słońce',
            number: 100,
          },
          onProgress() {},
          onUploadUrlAvailable: waitableFunction('onUploadUrlAvailable'),
          onSuccess: waitableFunction('onSuccess'),
        };
        spyOn(options, 'onProgress');

        const upload = new tusJsClient.Upload(file, options);
        upload.start();

        let req = await testStack.nextRequest();

        expect(req.url).toBe('https://tus.io/uploads');
        expect(req.method).toBe('POST');
        expect(req.requestHeaders.Custom).toBe('blargh');
        expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0');
        expect(req.requestHeaders['Upload-Length']).toBe('11');
        expect(req.requestHeaders['Upload-Metadata']).toBe(
          'foo aGVsbG8=,bar d29ybGQ=,nonlatin c8WCb8WEY2U=,number MTAw',
        );

        req.respondWith({
          status: 201,
          responseHeaders: {
            Location: 'https://tus.io/uploads/blargh',
          },
        });

        req = await testStack.nextRequest();

        expect(options.onUploadUrlAvailable).toHaveBeenCalled();

        expect(req.url).toBe('https://tus.io/uploads/blargh');
        expect(req.method).toBe('PATCH');
        expect(req.requestHeaders.Custom).toBe('blargh');
        expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0');
        expect(req.requestHeaders['Upload-Offset']).toBe('0');
        expect(req.requestHeaders['Content-Type']).toBe('application/offset+octet-stream');
        expect(req.bodySize).toBe(11);

        req.respondWith({
          status: 204,
          responseHeaders: {
            'Upload-Offset': '11',
          },
        });

        await options.onSuccess.toBeCalled();

        expect(upload.url).toBe('https://tus.io/uploads/blargh');
        expect(options.onProgress).toHaveBeenCalledWith(11, 11);
      });

      it('should create an upload if resuming fails', async () => {
        const testStack = new TestHttpStack();
        const file = getBlob('hello world');
        const options = {
          httpStack: testStack,
          endpoint: 'http://tus.io/uploads',
          uploadUrl: 'http://tus.io/uploads/resuming',
        };

        const upload = new tusJsClient.Upload(file, options);
        upload.start();

        let req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/uploads/resuming');
        expect(req.method).toBe('HEAD');
        expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0');

        req.respondWith({
          status: 404,
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/uploads');
        expect(req.method).toBe('POST');
        expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0');
        expect(req.requestHeaders['Upload-Length']).toBe('11');

        // The upload URL should be cleared when tus-js.client tries to create a new upload.
        expect(upload.url).toBe(null);
      });

      it('should create an upload using the creation-with-data extension', async () => {
        const testStack = new TestHttpStack();
        const file = getBlob('hello world');
        const options = {
          httpStack: testStack,
          endpoint: 'http://tus.io/uploads',
          uploadDataDuringCreation: true,
          onProgress() {},
          onChunkComplete() {},
          onSuccess: waitableFunction('onSuccess'),
        };

        spyOn(options, 'onProgress');
        spyOn(options, 'onChunkComplete');

        const upload = new tusJsClient.Upload(file, options);
        upload.start();

        const req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/uploads');
        expect(req.method).toBe('POST');
        expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0');
        expect(req.requestHeaders['Upload-Length']).toBe('11');
        expect(req.requestHeaders['Content-Type']).toBe('application/offset+octet-stream');
        expect(req.bodySize).toBe(11);

        req.respondWith({
          status: 201,
          responseHeaders: {
            Location: 'http://tus.io/uploads/blargh',
            'Upload-Offset': '11',
          },
        });

        await options.onSuccess.toBeCalled();

        expect(options.onProgress).toHaveBeenCalledWith(11, 11);
        expect(options.onChunkComplete).toHaveBeenCalledWith(11, 11, 11);
        expect(options.onSuccess).toHaveBeenCalled();

        expect(upload.url).toBe('http://tus.io/uploads/blargh');
      });

      it('should create an upload with partial data and continue', async () => {
        const testStack = new TestHttpStack();
        const file = getBlob('hello world');
        const options = {
          httpStack: testStack,
          endpoint: 'http://tus.io/uploads',
          uploadDataDuringCreation: true,
          chunkSize: 6,
          onProgress() {},
          onChunkComplete() {},
          onSuccess: waitableFunction('onSuccess'),
        };

        spyOn(options, 'onProgress');
        spyOn(options, 'onChunkComplete');

        const upload = new tusJsClient.Upload(file, options);
        upload.start();

        let req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/uploads');
        expect(req.method).toBe('POST');
        expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0');
        expect(req.requestHeaders['Upload-Length']).toBe('11');
        expect(req.requestHeaders['Content-Type']).toBe('application/offset+octet-stream');
        expect(req.bodySize).toBe(6);

        req.respondWith({
          status: 201,
          responseHeaders: {
            Location: 'http://tus.io/uploads/blargh',
            'Upload-Offset': '6',
          },
        });

        req = await testStack.nextRequest();

        // Once the second request has been sent, the progress handler must have been invoked.
        expect(options.onProgress).toHaveBeenCalledWith(6, 11);
        expect(options.onChunkComplete).toHaveBeenCalledWith(6, 6, 11);
        expect(options.onSuccess).not.toHaveBeenCalled();
        expect(upload.url).toBe('http://tus.io/uploads/blargh');

        expect(req.url).toBe('http://tus.io/uploads/blargh');
        expect(req.method).toBe('PATCH');
        expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0');
        expect(req.requestHeaders['Upload-Offset']).toBe('6');
        expect(req.requestHeaders['Content-Type']).toBe('application/offset+octet-stream');
        expect(req.bodySize).toBe(5);

        req.respondWith({
          status: 201,
          responseHeaders: {
            Location: 'http://tus.io/uploads/blargh',
            'Upload-Offset': '11',
          },
        });

        await options.onSuccess.toBeCalled();

        expect(options.onProgress).toHaveBeenCalledWith(11, 11);
        expect(options.onChunkComplete).toHaveBeenCalledWith(5, 11, 11);
        expect(options.onSuccess).toHaveBeenCalled();
      });

      it("should add the request's body and ID to errors", async () => {
        const testStack = new TestHttpStack();
        const file = getBlob('hello world');
        const options = {
          httpStack: testStack,
          endpoint: 'http://tus.io/uploads',
          addRequestId: true,
          retryDelays: null,
          onError: waitableFunction('onError'),
        };

        const upload = new tusJsClient.Upload(file, options);
        upload.start();

        const req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/uploads');
        expect(req.method).toBe('POST');

        const reqId = req.requestHeaders['X-Request-ID'];
        expect(typeof reqId).toBe('string');
        expect(reqId.length).toBe(36);

        req.respondWith({
          status: 500,
          responseText: 'server_error',
        });

        const err = await options.onError.toBeCalled();

        expect(err.message).toBe(
          `tus: unexpected response while creating upload, originated from request (method: POST, url: http://tus.io/uploads, response code: 500, response text: server_error, request id: ${reqId})`,
        );
        expect(err.originalRequest).toBeDefined();
        expect(err.originalResponse).toBeDefined();
      });

      it('should invoke the request and response callbacks', async () => {
        const testStack = new TestHttpStack();
        const file = getBlob('hello world');
        const options = {
          httpStack: testStack,
          uploadUrl: 'http://tus.io/uploads/foo',
          onBeforeRequest(req) {
            expect(req.getURL()).toBe('http://tus.io/uploads/foo');
            expect(req.getMethod()).toBe('HEAD');
          },
          onAfterResponse(req, res) {
            expect(req.getURL()).toBe('http://tus.io/uploads/foo');
            expect(req.getMethod()).toBe('HEAD');
            expect(res.getStatus()).toBe(204);
            expect(res.getHeader('Upload-Offset')).toBe(11);
          },
          onSuccess: waitableFunction('onSuccess'),
        };
        spyOn(options, 'onBeforeRequest');
        spyOn(options, 'onAfterResponse');

        const upload = new tusJsClient.Upload(file, options);
        upload.start();

        const req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/uploads/foo');
        expect(req.method).toBe('HEAD');

        req.respondWith({
          status: 204,
          responseHeaders: {
            'Upload-Offset': '11',
            'Upload-Length': '11',
          },
        });

        await options.onSuccess.toBeCalled();
        expect(options.onBeforeRequest).toHaveBeenCalled();
        expect(options.onAfterResponse).toHaveBeenCalled();
      });

      it('should invoke the onSuccess callback with event payload', async () => {
        const testStack = new TestHttpStack();
        const file = getBlob('hello world');
        const options = {
          httpStack: testStack,
          uploadUrl: 'http://tus.io/uploads/foo',
          onSuccess: waitableFunction('onSuccess'),
        };

        const upload = new tusJsClient.Upload(file, options);
        upload.start();

        const req = await testStack.nextRequest();
        req.respondWith({
          status: 204,
          responseHeaders: {
            'Upload-Offset': '11',
            'Upload-Length': '11',
            'Custom-Header': 'hello',
          },
        });

        const { lastResponse } = await options.onSuccess.toBeCalled();
        expect(lastResponse).toBeInstanceOf(TestResponse);
        expect(lastResponse.getHeader('Custom-Header')).toBe('hello');
      });

      it('should throw an error if resuming fails and no endpoint is provided', async () => {
        const testStack = new TestHttpStack();
        const file = getBlob('hello world');
        const options = {
          httpStack: testStack,
          uploadUrl: 'http://tus.io/uploads/resuming',
          onError: waitableFunction('onError'),
        };

        const upload = new tusJsClient.Upload(file, options);
        upload.start();

        const req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/uploads/resuming');
        expect(req.method).toBe('HEAD');
        expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0');

        req.respondWith({
          status: 404,
        });

        const err = await options.onError.toBeCalled();
        expect(err.message).toBe(
          'tus: unable to resume upload (new upload cannot be created without an endpoint), originated from request (method: HEAD, url: http://tus.io/uploads/resuming, response code: 404, response text: , request id: n/a)',
        );
      });

      it('should resolve relative URLs', async () => {
        const testStack = new TestHttpStack();
        const file = getBlob('hello world');
        const options = {
          httpStack: testStack,
          endpoint: 'http://tus.io:1080/files/',
        };

        const upload = new tusJsClient.Upload(file, options);
        upload.start();

        let req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io:1080/files/');
        expect(req.method).toBe('POST');

        req.respondWith({
          status: 201,
          responseHeaders: {
            Location: '//localhost/uploads/foo',
          },
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('http://localhost/uploads/foo');
        expect(req.method).toBe('PATCH');

        req.respondWith({
          status: 204,
          responseHeaders: {
            'Upload-Offset': '11',
          },
        });

        expect(upload.url).toBe('http://localhost/uploads/foo');
      });

      it('should upload a file in chunks', async () => {
        const testStack = new TestHttpStack();
        const file = getBlob('hello world');
        const options = {
          httpStack: testStack,
          endpoint: 'http://tus.io/uploads',
          chunkSize: 7,
          onSuccess: waitableFunction('onSuccess'),
          onProgress() {},
          onChunkComplete() {},
        };
        spyOn(options, 'onProgress');
        spyOn(options, 'onChunkComplete');

        const upload = new tusJsClient.Upload(file, options);
        upload.start();

        let req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/uploads');
        expect(req.method).toBe('POST');
        expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0');
        expect(req.requestHeaders['Upload-Length']).toBe('11');

        req.respondWith({
          status: 201,
          responseHeaders: {
            Location: '/uploads/blargh',
          },
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/uploads/blargh');
        expect(req.method).toBe('PATCH');
        expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0');
        expect(req.requestHeaders['Upload-Offset']).toBe('0');
        expect(req.requestHeaders['Content-Type']).toBe('application/offset+octet-stream');
        expect(req.bodySize).toBe(7);

        req.respondWith({
          status: 204,
          responseHeaders: {
            'Upload-Offset': '7',
          },
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/uploads/blargh');
        expect(req.method).toBe('PATCH');
        expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0');
        expect(req.requestHeaders['Upload-Offset']).toBe('7');
        expect(req.requestHeaders['Content-Type']).toBe('application/offset+octet-stream');
        expect(req.bodySize).toBe(4);

        req.respondWith({
          status: 204,
          responseHeaders: {
            'Upload-Offset': '11',
          },
        });

        await options.onSuccess.toBeCalled();

        expect(upload.url).toBe('http://tus.io/uploads/blargh');
        expect(options.onProgress).toHaveBeenCalledWith(11, 11);
        expect(options.onChunkComplete).toHaveBeenCalledWith(7, 7, 11);
        expect(options.onChunkComplete).toHaveBeenCalledWith(4, 11, 11);
      });

      it('should add the original request to errors', async () => {
        const testStack = new TestHttpStack();
        const file = getBlob('hello world');
        const options = {
          httpStack: testStack,
          endpoint: 'http://tus.io/uploads',
          retryDelays: null,
          onError: waitableFunction('onError'),
        };

        const upload = new tusJsClient.Upload(file, options);
        upload.start();

        const req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/uploads');
        expect(req.method).toBe('POST');

        req.respondWith({
          status: 500,
          responseHeaders: {
            Custom: 'blargh',
          },
        });

        const err = await options.onError.toBeCalled();

        expect(upload.url).toBe(null);
        expect(err.message).toBe(
          'tus: unexpected response while creating upload, originated from request (method: POST, url: http://tus.io/uploads, response code: 500, response text: , request id: n/a)',
        );
        expect(err.originalRequest).toBeDefined();
        expect(err.originalResponse).toBeDefined();
        expect(err.originalResponse.getHeader('Custom')).toBe('blargh');
      });

      it('should only create an upload for empty files', async () => {
        const testStack = new TestHttpStack();
        const file = getBlob('');
        const options = {
          httpStack: testStack,
          endpoint: 'http://tus.io/uploads',
          onSuccess: waitableFunction('onSuccess'),
        };

        const upload = new tusJsClient.Upload(file, options);
        upload.start();

        const req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/uploads');
        expect(req.method).toBe('POST');
        expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0');
        expect(req.requestHeaders['Upload-Length']).toBe('0');

        req.respondWith({
          status: 201,
          responseHeaders: {
            Location: 'http://tus.io/uploads/empty',
          },
        });

        await options.onSuccess.toBeCalled();
        expect(options.onSuccess).toHaveBeenCalled();
      });

      it('should not resume a finished upload', async () => {
        const testStack = new TestHttpStack();
        const file = getBlob('hello world');
        const options = {
          httpStack: testStack,
          endpoint: 'http://tus.io/uploads',
          onProgress() {},
          onSuccess: waitableFunction('onSuccess'),
          uploadUrl: 'http://tus.io/uploads/resuming',
        };
        spyOn(options, 'onProgress');

        const upload = new tusJsClient.Upload(file, options);
        upload.start();

        const req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/uploads/resuming');
        expect(req.method).toBe('HEAD');
        expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0');

        req.respondWith({
          status: 204,
          responseHeaders: {
            'Upload-Length': '11',
            'Upload-Offset': '11',
          },
        });

        await options.onSuccess.toBeCalled();

        expect(options.onProgress).toHaveBeenCalledWith(11, 11);
        expect(options.onSuccess).toHaveBeenCalled();
      });

      it('should resume an upload from a specified url', async () => {
        const testStack = new TestHttpStack();
        const file = getBlob('hello world');
        const options = {
          httpStack: testStack,
          endpoint: 'http://tus.io/uploads',
          uploadUrl: 'http://tus.io/files/upload',
          onProgress() {},
          onUploadUrlAvailable: waitableFunction('onUploadUrlAvailable'),
          onSuccess: waitableFunction('onSuccess'),
          fingerprint() {},
        };
        spyOn(options, 'fingerprint').and.resolveTo('fingerprinted');
        spyOn(options, 'onProgress');

        const upload = new tusJsClient.Upload(file, options);
        upload.start();

        expect(options.fingerprint).toHaveBeenCalled();

        let req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/files/upload');
        expect(req.method).toBe('HEAD');
        expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0');

        req.respondWith({
          status: 204,
          responseHeaders: {
            'Upload-Length': '11',
            'Upload-Offset': '3',
          },
        });

        req = await testStack.nextRequest();

        expect(options.onUploadUrlAvailable).toHaveBeenCalled();

        expect(req.url).toBe('http://tus.io/files/upload');
        expect(req.method).toBe('PATCH');
        expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0');
        expect(req.requestHeaders['Upload-Offset']).toBe('3');
        expect(req.requestHeaders['Content-Type']).toBe('application/offset+octet-stream');
        expect(req.bodySize).toBe(11 - 3);

        req.respondWith({
          status: 204,
          responseHeaders: {
            'Upload-Offset': '11',
          },
        });

        await options.onSuccess.toBeCalled();
        expect(options.onProgress).toHaveBeenCalledWith(11, 11);
        expect(upload.url).toBe('http://tus.io/files/upload');
      });

      it('should resume a previously started upload', async () => {
        const testStack = new TestHttpStack();
        const file = getBlob('hello world');
        const options = {
          httpStack: testStack,
          endpoint: 'http://tus.io/uploads',
          onSuccess: waitableFunction('onSuccess'),
          onError() {},
        };

        const upload = new tusJsClient.Upload(file, options);
        upload.start();

        let req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/uploads');
        expect(req.method).toBe('POST');

        req.respondWith({
          status: 201,
          responseHeaders: {
            Location: 'http://tus.io/uploads/blargh',
          },
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/uploads/blargh');
        expect(req.method).toBe('PATCH');

        upload.abort();

        req.respondWith({
          status: 204,
          responseHeaders: {
            'Upload-Offset': '5',
          },
        });

        upload.start();

        req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/uploads/blargh');
        expect(req.method).toBe('HEAD');

        req.respondWith({
          status: 204,
          responseHeaders: {
            'Upload-Offset': '5',
            'Upload-Length': '11',
          },
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/uploads/blargh');
        expect(req.method).toBe('PATCH');

        req.respondWith({
          status: 204,
          responseHeaders: {
            'Upload-Offset': '11',
          },
        });

        await options.onSuccess.toBeCalled();
        expect(options.onSuccess).toHaveBeenCalled();
      });

      it('should override the PATCH method', async () => {
        const testStack = new TestHttpStack();
        const file = getBlob('hello world');
        const options = {
          httpStack: testStack,
          endpoint: 'http://tus.io/uploads',
          uploadUrl: 'http://tus.io/files/upload',
          overridePatchMethod: true,
        };

        const upload = new tusJsClient.Upload(file, options);
        upload.start();

        let req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/files/upload');
        expect(req.method).toBe('HEAD');
        expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0');

        req.respondWith({
          status: 204,
          responseHeaders: {
            'Upload-Length': '11',
            'Upload-Offset': '3',
          },
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/files/upload');
        expect(req.method).toBe('POST');
        expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0');
        expect(req.requestHeaders['Upload-Offset']).toBe('3');
        expect(req.requestHeaders['X-HTTP-Method-Override']).toBe('PATCH');

        req.respondWith({
          status: 204,
          responseHeaders: {
            'Upload-Offset': '11',
          },
        });
      });

      it('should emit an error if an upload is locked', async () => {
        const testStack = new TestHttpStack();
        const file = getBlob('hello world');
        const options = {
          httpStack: testStack,
          endpoint: 'http://tus.io/uploads',
          uploadUrl: 'http://tus.io/files/upload',
          onError: waitableFunction('onError'),
          retryDelays: null,
        };

        const upload = new tusJsClient.Upload(file, options);
        upload.start();

        const req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/files/upload');
        expect(req.method).toBe('HEAD');

        req.respondWith({
          status: 423, // Locked
        });

        await options.onError.toBeCalled();
        expect(options.onError).toHaveBeenCalledWith(
          new Error(
            'tus: upload is currently locked; retry later, originated from request (method: HEAD, url: http://tus.io/files/upload, response code: 423, response text: , request id: n/a)',
          ),
        );
      });

      it('should emit an error if no Location header is presented', async () => {
        const testStack = new TestHttpStack();
        const file = getBlob('hello world');
        const options = {
          httpStack: testStack,
          endpoint: 'http://tus.io/uploads',
          onError: waitableFunction('onError'),
          retryDelays: null,
        };

        const upload = new tusJsClient.Upload(file, options);
        upload.start();

        const req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/uploads');
        expect(req.method).toBe('POST');

        // The Location header is omitted on purpose here
        req.respondWith({
          status: 201,
        });

        await options.onError.toBeCalled();

        expect(options.onError).toHaveBeenCalledWith(
          new Error(
            'tus: invalid or missing Location header, originated from request (method: POST, url: http://tus.io/uploads, response code: 201, response text: , request id: n/a)',
          ),
        );
      });

      it('should throw an error if the source provides less data than uploadSize', async () => {
        const testStack = new TestHttpStack();
        const file = getBlob('hello world');
        const options = {
          httpStack: testStack,
          uploadSize: 100,
          endpoint: 'http://tus.io/uploads',
          retryDelays: [],
          onError: waitableFunction('onError'),
        };

        const upload = new tusJsClient.Upload(file, options);
        upload.start();

        const req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/uploads');
        expect(req.method).toBe('POST');
        expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0');

        req.respondWith({
          status: 204,
          responseHeaders: {
            Location: 'http://tus.io/uploads/foo',
          },
        });

        const err = await options.onError.toBeCalled();
        expect(err.message).toBe(
          'tus: failed to upload chunk at offset 0, caused by Error: upload was configured with a size of 100 bytes, but the source is done after 11 bytes, originated from request (method: PATCH, url: http://tus.io/uploads/foo, response code: n/a, response text: n/a, request id: n/a)',
        );
      });

      it('should throw if retryDelays is not an array', () => {
        const file = getBlob('hello world');
        const upload = new tusJsClient.Upload(file, {
          endpoint: 'http://endpoint/',
          retryDelays: 44,
        });
        expect(upload.start.bind(upload)).toThrowError(
          'tus: the `retryDelays` option must either be an array or null',
        );
      });

      // This tests ensures that tus-js-client correctly retries if the
      // response has the code 500 Internal Error, 423 Locked or 409 Conflict.
      it('should retry the upload', async () => {
        const testStack = new TestHttpStack();
        const file = getBlob('hello world');
        const options = {
          httpStack: testStack,
          endpoint: 'http://tus.io/files/',
          retryDelays: [10, 10, 10],
          onSuccess: waitableFunction('onSuccess'),
        };

        const upload = new tusJsClient.Upload(file, options);
        upload.start();

        let req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/files/');
        expect(req.method).toBe('POST');

        req.respondWith({
          status: 500,
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/files/');
        expect(req.method).toBe('POST');

        req.respondWith({
          status: 201,
          responseHeaders: {
            Location: '/files/foo',
          },
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/files/foo');
        expect(req.method).toBe('PATCH');

        req.respondWith({
          status: 423,
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/files/foo');
        expect(req.method).toBe('HEAD');

        req.respondWith({
          status: 201,
          responseHeaders: {
            'Upload-Offset': '0',
            'Upload-Length': '11',
          },
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/files/foo');
        expect(req.method).toBe('PATCH');

        req.respondWith({
          status: 409,
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/files/foo');
        expect(req.method).toBe('HEAD');

        req.respondWith({
          status: 201,
          responseHeaders: {
            'Upload-Offset': '0',
            'Upload-Length': '11',
          },
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/files/foo');
        expect(req.method).toBe('PATCH');

        req.respondWith({
          status: 204,
          responseHeaders: {
            'Upload-Offset': '11',
          },
        });

        await options.onSuccess.toBeCalled();
        expect(options.onSuccess).toHaveBeenCalled();
      });

      // This tests ensures that tus-js-client correctly retries if the
      // return value of onShouldRetry is true.
      it('should retry the upload when onShouldRetry specified and returns true', async () => {
        const testStack = new TestHttpStack();
        const file = getBlob('hello world');
        const options = {
          httpStack: testStack,
          endpoint: 'http://tus.io/files/',
          retryDelays: [10, 10, 10],
          onSuccess: waitableFunction('onSuccess'),
          onShouldRetry: () => true,
        };

        spyOn(options, 'onShouldRetry').and.callThrough();

        const upload = new tusJsClient.Upload(file, options);
        upload.start();

        let req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/files/');
        expect(req.method).toBe('POST');

        req.respondWith({
          status: 500,
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/files/');
        expect(req.method).toBe('POST');

        req.respondWith({
          status: 201,
          responseHeaders: {
            Location: '/files/foo',
          },
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/files/foo');
        expect(req.method).toBe('PATCH');

        req.respondWith({
          status: 423,
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/files/foo');
        expect(req.method).toBe('HEAD');

        req.respondWith({
          status: 201,
          responseHeaders: {
            'Upload-Offset': '0',
            'Upload-Length': '11',
          },
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/files/foo');
        expect(req.method).toBe('PATCH');

        req.respondWith({
          status: 409,
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/files/foo');
        expect(req.method).toBe('HEAD');

        req.respondWith({
          status: 201,
          responseHeaders: {
            'Upload-Offset': '0',
            'Upload-Length': '11',
          },
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/files/foo');
        expect(req.method).toBe('PATCH');

        req.respondWith({
          status: 204,
          responseHeaders: {
            'Upload-Offset': '11',
          },
        });

        await options.onSuccess.toBeCalled();
        expect(options.onSuccess).toHaveBeenCalled();

        expect(options.onShouldRetry).toHaveBeenCalled();
        const args1 = options.onShouldRetry.calls.argsFor(0);
        expect(args1[0].message).toEqual(
          'tus: unexpected response while creating upload, originated from request (method: POST, url: http://tus.io/files/, response code: 500, response text: , request id: n/a)',
        );
        expect(args1[1]).toEqual(0);
        expect(args1[2]).toEqual(upload.options);

        const args2 = options.onShouldRetry.calls.argsFor(1);
        expect(args2[0].message).toEqual(
          'tus: unexpected response while uploading chunk, originated from request (method: PATCH, url: http://tus.io/files/foo, response code: 423, response text: , request id: n/a)',
        );
        expect(args2[1]).toEqual(1);
        expect(args2[2]).toEqual(upload.options);
      });

      // This tests ensures that tus-js-client correctly aborts if the
      // return value of onShouldRetry is false.
      it('should not retry the upload when callback specified and returns false', async () => {
        const testStack = new TestHttpStack();
        const file = getBlob('hello world');
        const options = {
          httpStack: testStack,
          endpoint: 'http://tus.io/files/',
          retryDelays: [10, 10, 10],
          onSuccess: waitableFunction('onSuccess'),
          onError: waitableFunction('onError'),
          onShouldRetry: () => false,
        };

        const upload = new tusJsClient.Upload(file, options);
        upload.start();

        const req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/files/');
        expect(req.method).toBe('POST');

        // The error callback should not be invoked for the first error response.
        expect(options.onError).not.toHaveBeenCalled();

        req.respondWith({
          status: 500,
        });

        await options.onError.toBeCalled();

        expect(options.onSuccess).not.toHaveBeenCalled();
        expect(options.onError).toHaveBeenCalledTimes(1);
      });

      it('should not retry if the error has not been caused by a request', async () => {
        const file = getBlob('hello world');
        const options = {
          httpStack: new TestHttpStack(),
          endpoint: 'http://tus.io/files/',
          retryDelays: [10, 10, 10],
          onSuccess() {},
          onError() {},
        };

        spyOn(options, 'onSuccess');
        spyOn(options, 'onError');

        const upload = new tusJsClient.Upload(file, options);
        spyOn(upload, '_createUpload');
        upload.start();

        await wait(200);

        const error = new Error('custom error');
        upload._emitError(error);

        expect(upload._createUpload).toHaveBeenCalledTimes(1);
        expect(options.onError).toHaveBeenCalledWith(error);
        expect(options.onSuccess).not.toHaveBeenCalled();
      });

      it('should stop retrying after all delays have been used', async () => {
        const testStack = new TestHttpStack();
        const file = getBlob('hello world');
        const options = {
          httpStack: testStack,
          endpoint: 'http://tus.io/files/',
          retryDelays: [10],
          onSuccess() {},
          onError: waitableFunction('onError'),
        };
        spyOn(options, 'onSuccess');

        const upload = new tusJsClient.Upload(file, options);
        upload.start();

        let req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/files/');
        expect(req.method).toBe('POST');

        req.respondWith({
          status: 500,
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/files/');
        expect(req.method).toBe('POST');

        // The error callback should not be invoked for the first error response.
        expect(options.onError).not.toHaveBeenCalled();

        req.respondWith({
          status: 500,
        });

        await options.onError.toBeCalled();

        expect(options.onSuccess).not.toHaveBeenCalled();
        expect(options.onError).toHaveBeenCalledTimes(1);
      });

      it('should stop retrying when the abort function is called', async () => {
        const testStack = new TestHttpStack();
        const file = getBlob('hello world');
        const options = {
          httpStack: testStack,
          endpoint: 'http://tus.io/files/',
          retryDelays: [10],
          onError() {},
        };

        spyOn(options, 'onError');

        const upload = new tusJsClient.Upload(file, options);
        upload.start();

        const req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/files/');
        expect(req.method).toBe('POST');

        spyOn(upload, 'start').and.callThrough();

        upload.abort();

        req.respondWith({
          status: 500,
        });

        const result = await Promise.race([testStack.nextRequest(), wait(100)]);

        expect(result).toBe('timed out');
      });

      it('should stop upload when the abort function is called during a callback', async () => {
        const testStack = new TestHttpStack();
        const file = getBlob('hello world');
        const options = {
          httpStack: testStack,
          endpoint: 'http://tus.io/files/',
          chunkSize: 5,
          onChunkComplete() {
            upload.abort();
          },
        };

        spyOn(options, 'onChunkComplete').and.callThrough();

        const upload = new tusJsClient.Upload(file, options);
        upload.start();

        let req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/files/');
        expect(req.method).toBe('POST');

        req.respondWith({
          status: 201,
          responseHeaders: {
            Location: '/files/foo',
          },
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/files/foo');
        expect(req.method).toBe('PATCH');

        req.respondWith({
          status: 204,
          responseHeaders: {
            'Upload-Offset': '5',
          },
        });

        const result = await Promise.race([testStack.nextRequest(), wait(200)]);

        expect(options.onChunkComplete).toHaveBeenCalled();
        expect(result).toBe('timed out');
      });

      it('should stop upload when the abort function is called during the POST request', async () => {
        const testStack = new TestHttpStack();
        const file = getBlob('hello world');
        const options = {
          httpStack: testStack,
          endpoint: 'http://tus.io/files/',
          onError() {},
        };

        spyOn(options, 'onError').and.callThrough();

        const upload = new tusJsClient.Upload(file, options);
        upload.start();

        const req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/files/');
        expect(req.method).toBe('POST');

        upload.abort();

        req.respondWith({
          status: 201,
          responseHeaders: {
            Location: '/files/foo',
          },
        });

        const result = await Promise.race([testStack.nextRequest(), wait(200)]);

        expect(options.onError).not.toHaveBeenCalled();
        expect(result).toBe('timed out');
      });

      it('should reset the attempt counter if an upload proceeds', async () => {
        const testStack = new TestHttpStack();
        const file = getBlob('hello world');
        const options = {
          httpStack: testStack,
          endpoint: 'http://tus.io/files/',
          retryDelays: [10],
          onError() {},
          onSuccess: waitableFunction('onSuccess'),
        };
        spyOn(options, 'onError');

        const upload = new tusJsClient.Upload(file, options);
        upload.start();

        let req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/files/');
        expect(req.method).toBe('POST');

        req.respondWith({
          status: 201,
          responseHeaders: {
            Location: '/files/foo',
          },
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/files/foo');
        expect(req.method).toBe('PATCH');

        req.respondWith({
          status: 500,
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/files/foo');
        expect(req.method).toBe('HEAD');

        req.respondWith({
          status: 204,
          responseHeaders: {
            'Upload-Offset': '0',
            'Upload-Length': '11',
          },
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/files/foo');
        expect(req.method).toBe('PATCH');

        req.respondWith({
          status: 204,
          responseHeaders: {
            'Upload-Offset': '5',
          },
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/files/foo');
        expect(req.method).toBe('PATCH');

        req.respondWith({
          status: 500,
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/files/foo');
        expect(req.method).toBe('HEAD');

        req.respondWith({
          status: 204,
          responseHeaders: {
            'Upload-Offset': '5',
            'Upload-Length': '11',
          },
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/files/foo');
        expect(req.method).toBe('PATCH');

        req.respondWith({
          status: 204,
          responseHeaders: {
            'Upload-Offset': '11',
          },
        });

        await options.onSuccess.toBeCalled();
        expect(options.onError).not.toHaveBeenCalled();
        expect(options.onSuccess).toHaveBeenCalled();
      });

      it('should send upload length on the last request when length is deferred and we know the total size', async () => {
        const testStack = new TestHttpStack();
        const file = getBlob('hello world');
        const options = {
          httpStack: testStack,
          endpoint: 'http://tus.io/uploads',
          uploadUrl: 'http://tus.io/uploads/resuming',
          chunkSize: 4,
          // No `uploadLengthDeferred: true` here, but the client learns
          // about the deferred length from the HEAD response.
        };

        const upload = new tusJsClient.Upload(file, options);
        upload.start();

        let req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/uploads/resuming');
        expect(req.method).toBe('HEAD');

        req.respondWith({
          status: 204,
          responseHeaders: {
            'Upload-Defer-Length': '1',
            'Upload-Offset': '5',
          },
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/uploads/resuming');
        expect(req.method).toBe('PATCH');
        expect(req.requestHeaders['Upload-Offset']).toBe('5');
        expect(req.requestHeaders['Upload-Length']).toBe(undefined);
        expect(req.body.size).toBe(4);
        expect(await req.body.text()).toBe(' wor');

        req.respondWith({
          status: 204,
          responseHeaders: {
            'Upload-Offset': '9',
          },
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/uploads/resuming');
        expect(req.method).toBe('PATCH');
        expect(req.requestHeaders['Upload-Offset']).toBe('9');
        expect(req.requestHeaders['Upload-Length']).toBe('11');
        expect(req.body.size).toBe(2);
        expect(await req.body.text()).toBe('ld');

        req.respondWith({
          status: 204,
          responseHeaders: {
            'Upload-Offset': '11',
          },
        });
      });
    });
  });

  async function assertUrlStorage(urlStorage) {
    // In the beginning of the test, the storage should be empty.
    let result = await urlStorage.findAllUploads();
    expect(result).toEqual([]);

    // Add a few uploads into the storage
    const key1 = await urlStorage.addUpload('fingerprintA', { id: 1 });
    const key2 = await urlStorage.addUpload('fingerprintA', { id: 2 });
    const key3 = await urlStorage.addUpload('fingerprintB', { id: 3 });

    expect(/^tus::fingerprintA::/.test(key1)).toBe(true);
    expect(/^tus::fingerprintA::/.test(key2)).toBe(true);
    expect(/^tus::fingerprintB::/.test(key3)).toBe(true);

    // Query the just stored uploads individually
    result = await urlStorage.findUploadsByFingerprint('fingerprintA');
    sort(result);
    expect(result).toEqual([
      { id: 1, urlStorageKey: key1 },
      { id: 2, urlStorageKey: key2 },
    ]);

    result = await urlStorage.findUploadsByFingerprint('fingerprintB');
    sort(result);
    expect(result).toEqual([{ id: 3, urlStorageKey: key3 }]);

    // Check that we can retrieve all stored uploads
    result = await urlStorage.findAllUploads();
    sort(result);
    expect(result).toEqual([
      { id: 1, urlStorageKey: key1 },
      { id: 2, urlStorageKey: key2 },
      { id: 3, urlStorageKey: key3 },
    ]);

    // Check that it can remove an upload and will not return it back
    await urlStorage.removeUpload(key2);
    await urlStorage.removeUpload(key3);

    result = await urlStorage.findUploadsByFingerprint('fingerprintA');
    expect(result).toEqual([{ id: 1, urlStorageKey: key1 }]);

    result = await urlStorage.findUploadsByFingerprint('fingerprintB');
    expect(result).toEqual([]);
  }

  // Sort the results from the URL storage since the order in not deterministic.
  function sort(result) {
    result.sort((a, b) => a.id - b.id);
  }

  describe('tus', () => {
    beforeEach(() => {
      localStorage.clear();
    });

    describe('#Upload', () => {
      it('should resume an upload from a stored url', async () => {
        localStorage.setItem(
          'tus::fingerprinted::1337',
          JSON.stringify({
            uploadUrl: 'http://tus.io/uploads/resuming',
          }),
        );

        const testStack = new TestHttpStack();
        const file = new Blob('hello world'.split(''));
        const options = {
          httpStack: testStack,
          endpoint: 'http://tus.io/uploads',
          onProgress() {},
          fingerprint() {},
        };
        spyOn(options, 'fingerprint').and.resolveTo('fingerprinted');
        spyOn(options, 'onProgress');

        const upload = new tusJsClient.Upload(file, options);

        const previousUploads = await upload.findPreviousUploads();
        expect(previousUploads).toEqual([
          {
            uploadUrl: 'http://tus.io/uploads/resuming',
            urlStorageKey: 'tus::fingerprinted::1337',
          },
        ]);
        upload.resumeFromPreviousUpload(previousUploads[0]);

        upload.start();

        expect(options.fingerprint).toHaveBeenCalledWith(file, upload.options);

        let req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/uploads/resuming');
        expect(req.method).toBe('HEAD');
        expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0');

        req.respondWith({
          status: 204,
          responseHeaders: {
            'Upload-Length': '11',
            'Upload-Offset': '3',
          },
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/uploads/resuming');
        expect(req.method).toBe('PATCH');
        expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0');
        expect(req.requestHeaders['Upload-Offset']).toBe('3');
        expect(req.requestHeaders['Content-Type']).toBe('application/offset+octet-stream');
        expect(req.bodySize).toBe(11 - 3);

        req.respondWith({
          status: 204,
          responseHeaders: {
            'Upload-Offset': '11',
          },
        });

        expect(upload.url).toBe('http://tus.io/uploads/resuming');
        expect(options.onProgress).toHaveBeenCalledWith(11, 11);
      });

      describe('storing of upload urls', () => {
        const testStack = new TestHttpStack();
        const options = {
          httpStack: testStack,
          endpoint: 'http://tus.io/uploads',
          fingerprint() {},
        };

        async function startUpload() {
          const file = new Blob('hello world'.split(''));
          spyOn(options, 'fingerprint').and.resolveTo('fingerprinted');
          options.onSuccess = waitableFunction('onSuccess');

          const upload = new tusJsClient.Upload(file, options);
          upload.start();

          expect(options.fingerprint).toHaveBeenCalled();

          const req = await testStack.nextRequest();
          expect(req.url).toBe('http://tus.io/uploads');
          expect(req.method).toBe('POST');

          req.respondWith({
            status: 201,
            responseHeaders: {
              Location: '/uploads/blargh',
            },
          });

          // Wait a short delay to allow the Promises to settle
          await wait(10);
        }

        async function finishUpload() {
          const req = await testStack.nextRequest();
          expect(req.url).toBe('http://tus.io/uploads/blargh');
          expect(req.method).toBe('PATCH');

          req.respondWith({
            status: 204,
            responseHeaders: {
              'Upload-Offset': '11',
            },
          });

          await options.onSuccess.toBeCalled();
        }

        it('should store and retain with default options', async () => {
          options.removeFingerprintOnSuccess = false;
          await startUpload();

          const key = localStorage.key(0);
          expect(key.indexOf('tus::fingerprinted::')).toBe(0);

          const storedUpload = JSON.parse(localStorage.getItem(key));
          expect(storedUpload.uploadUrl).toBe('http://tus.io/uploads/blargh');
          expect(storedUpload.size).toBe(11);

          await finishUpload();

          expect(localStorage.getItem(key)).toBe(JSON.stringify(storedUpload));
        });

        it('should store and remove with option removeFingerprintOnSuccess set', async () => {
          options.removeFingerprintOnSuccess = true;
          await startUpload();

          const key = localStorage.key(0);
          expect(key.indexOf('tus::fingerprinted::')).toBe(0);

          const storedUpload = JSON.parse(localStorage.getItem(key));
          expect(storedUpload.uploadUrl).toBe('http://tus.io/uploads/blargh');
          expect(storedUpload.size).toBe(11);

          await finishUpload();
          expect(localStorage.getItem(key)).toBe(null);
        });

        it('should store URLs passed in using the uploadUrl option', async () => {
          const file = new Blob('hello world'.split(''));
          const options2 = {
            httpStack: testStack,
            uploadUrl: 'http://tus.io/uploads/storedUrl',
            fingerprint() {},
            onSuccess: waitableFunction('onSuccess'),
            removeFingerprintOnSuccess: true,
          };
          spyOn(options2, 'fingerprint').and.resolveTo('fingerprinted');

          const upload = new tusJsClient.Upload(file, options2);
          upload.start();

          expect(options2.fingerprint).toHaveBeenCalled();

          let req = await testStack.nextRequest();
          expect(req.url).toBe('http://tus.io/uploads/storedUrl');
          expect(req.method).toBe('HEAD');
          expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0');

          req.respondWith({
            status: 204,
            responseHeaders: {
              'Upload-Length': '11',
              'Upload-Offset': '3',
            },
          });

          // Wait a short delay to allow the Promises to settle
          await wait(10);

          const key = localStorage.key(0);
          expect(key.indexOf('tus::fingerprinted::')).toBe(0);

          const storedUpload = JSON.parse(localStorage.getItem(key));
          expect(storedUpload.uploadUrl).toBe('http://tus.io/uploads/storedUrl');
          expect(storedUpload.size).toBe(11);

          req = await testStack.nextRequest();
          expect(req.url).toBe('http://tus.io/uploads/storedUrl');
          expect(req.method).toBe('PATCH');
          expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0');
          expect(req.requestHeaders['Upload-Offset']).toBe('3');
          expect(req.requestHeaders['Content-Type']).toBe('application/offset+octet-stream');
          expect(req.bodySize).toBe(11 - 3);

          req.respondWith({
            status: 204,
            responseHeaders: {
              'Upload-Offset': '11',
            },
          });

          await options2.onSuccess.toBeCalled();

          // Entry in localStorage should be removed after successful upload
          expect(localStorage.getItem(key)).toBe(null);
        });
      });

      it('should delete upload urls on a 4XX', async () => {
        const testStack = new TestHttpStack();
        const file = new Blob('hello world'.split(''));
        const options = {
          httpStack: testStack,
          endpoint: 'http://tus.io/uploads',
          fingerprint() {},
        };
        spyOn(options, 'fingerprint').and.resolveTo('fingerprinted');

        const upload = new tusJsClient.Upload(file, options);

        upload.resumeFromPreviousUpload({
          uploadUrl: 'http://tus.io/uploads/resuming',
          urlStorageKey: 'tus::fingerprinted::1337',
        });

        upload.start();

        const req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/uploads/resuming');
        expect(req.method).toBe('HEAD');

        req.respondWith({
          status: 400,
        });

        await wait(10);

        expect(localStorage.getItem('tus::fingerprinted::1337')).toBe(null);
      });

      describe('resolving of URIs', () => {
        // Disable these tests for IE 10 and 11 because it's not possible to overwrite
        // the navigator.product property.
        const isIE = navigator.userAgent.indexOf('Trident/') > 0;
        if (isIE) {
          console.log('Skipping tests for React Native in Internet Explorer');
          return
        }

        const originalProduct = navigator.product;

        beforeEach(() => {
          jasmine.Ajax.install();
          // Simulate React Native environment to enable URIs as input objects.
          Object.defineProperty(navigator, 'product', {
            value: 'ReactNative',
            configurable: true,
          });
        });

        afterEach(() => {
          jasmine.Ajax.uninstall();
          Object.defineProperty(navigator, 'product', {
            value: originalProduct,
            configurable: true,
          });
        });

        it('should upload a file from an URI', async () => {
          const file = {
            uri: 'file:///my/file.dat',
          };
          const testStack = new TestHttpStack();
          const options = {
            httpStack: testStack,
            endpoint: 'http://tus.io/uploads',
            onSuccess: waitableFunction('onSuccess'),
          };

          const upload = new tusJsClient.Upload(file, options);
          upload.start();

          // Wait a short interval to make sure that the XHR has been sent.
          await wait(0);

          let req = jasmine.Ajax.requests.mostRecent();
          expect(req.url).toBe('file:///my/file.dat');
          expect(req.method).toBe('GET');
          expect(req.responseType).toBe('blob');

          req.respondWith({
            status: 200,
            responseHeaders: {
              'Upload-Length': '11',
              'Upload-Offset': '3',
            },
            response: new Blob('hello world'.split('')),
          });

          req = await testStack.nextRequest();
          expect(req.url).toBe('http://tus.io/uploads');
          expect(req.method).toBe('POST');
          expect(req.requestHeaders['Upload-Length']).toBe('11');

          req.respondWith({
            status: 201,
            responseHeaders: {
              Location: '/uploads/blargh',
            },
          });

          req = await testStack.nextRequest();
          expect(req.url).toBe('http://tus.io/uploads/blargh');
          expect(req.method).toBe('PATCH');
          expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0');
          expect(req.requestHeaders['Upload-Offset']).toBe('0');
          expect(req.requestHeaders['Content-Type']).toBe('application/offset+octet-stream');
          expect(req.bodySize).toBe(11);

          req.respondWith({
            status: 204,
            responseHeaders: {
              'Upload-Offset': '11',
            },
          });

          await options.onSuccess.toBeCalled();
          expect(upload.url).toBe('http://tus.io/uploads/blargh');
        });

        it("should emit an error if it can't resolve the URI", async () => {
          const file = {
            uri: 'file:///my/file.dat',
          };
          const options = {
            endpoint: 'http://tus.io/uploads',
            onError: waitableFunction('onError'),
          };

          const upload = new tusJsClient.Upload(file, options);
          upload.start();

          // Wait a short interval to make sure that the XHR has been sent.
          await wait(0);

          const req = jasmine.Ajax.requests.mostRecent();
          expect(req.url).toBe('file:///my/file.dat');
          expect(req.method).toBe('GET');
          expect(req.responseType).toBe('blob');

          req.responseError();

          await options.onError.toBeCalled();
          expect(options.onError).toHaveBeenCalledWith(
            new Error(
              'tus: cannot fetch `file.uri` as Blob, make sure the uri is correct and accessible. [object Object]',
            ),
          );
        });
      });
    });

    describe('#LocalStorageUrlStorage', () => {
      it('should allow storing and retrieving uploads', async () => {
        await assertUrlStorage(tusJsClient.defaultOptions.urlStorage);
      });
    });
  });

  describe('tus', () => {
    describe('parallel uploading', () => {
      it('should throw if incompatible options are used', () => {
        const file = getBlob('hello world');
        const upload = new tusJsClient.Upload(file, {
          endpoint: 'https://tus.io/uploads',
          parallelUploads: 2,
          uploadUrl: 'foo',
        });
        expect(upload.start.bind(upload)).toThrowError(
          'tus: cannot use the `uploadUrl` option when parallelUploads is enabled',
        );
      });

      it('should throw if `parallelUploadBoundaries` is passed without `parallelUploads`', () => {
        const file = getBlob('hello world');
        const upload = new tusJsClient.Upload(file, {
          endpoint: 'https://tus.io/uploads',
          parallelUploadBoundaries: [{ start: 0, end: 2 }],
        });
        expect(upload.start.bind(upload)).toThrowError(
          'tus: cannot use the `parallelUploadBoundaries` option when `parallelUploads` is disabled',
        );
      });

      it('should throw if `parallelUploadBoundaries` is not the same length as the value of `parallelUploads`', () => {
        const file = getBlob('hello world');
        const upload = new tusJsClient.Upload(file, {
          endpoint: 'https://tus.io/uploads',
          parallelUploads: 3,
          parallelUploadBoundaries: [{ start: 0, end: 2 }],
        });
        expect(upload.start.bind(upload)).toThrowError(
          'tus: the `parallelUploadBoundaries` must have the same length as the value of `parallelUploads`',
        );
      });

      it('should split a file into multiple parts and create an upload for each', async () => {
        const testStack = new TestHttpStack();

        const testUrlStorage = {
          addUpload: (fingerprint, upload) => {
            expect(fingerprint).toBe('fingerprinted');
            expect(upload.uploadUrl).toBeUndefined();
            expect(upload.size).toBe(11);
            expect(upload.parallelUploadUrls).toEqual([
              'https://tus.io/uploads/upload1',
              'https://tus.io/uploads/upload2',
            ]);

            return Promise.resolve('tus::fingerprinted::1337')
          },
          removeUpload: (urlStorageKey) => {
            expect(urlStorageKey).toBe('tus::fingerprinted::1337');
            return Promise.resolve()
          },
        };
        spyOn(testUrlStorage, 'removeUpload').and.callThrough();
        spyOn(testUrlStorage, 'addUpload').and.callThrough();

        const file = getBlob('hello world');
        const options = {
          httpStack: testStack,
          urlStorage: testUrlStorage,
          storeFingerprintForResuming: true,
          removeFingerprintOnSuccess: true,
          parallelUploads: 2,
          retryDelays: [10],
          endpoint: 'https://tus.io/uploads',
          headers: {
            Custom: 'blargh',
          },
          metadata: {
            foo: 'hello',
          },
          metadataForPartialUploads: {
            test: 'world',
          },
          onProgress() {},
          onSuccess: waitableFunction(),
          fingerprint: () => Promise.resolve('fingerprinted'),
        };
        spyOn(options, 'onProgress');

        const upload = new tusJsClient.Upload(file, options);
        upload.start();

        let req = await testStack.nextRequest();
        expect(req.url).toBe('https://tus.io/uploads');
        expect(req.method).toBe('POST');
        expect(req.requestHeaders.Custom).toBe('blargh');
        expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0');
        expect(req.requestHeaders['Upload-Length']).toBe('5');
        expect(req.requestHeaders['Upload-Concat']).toBe('partial');
        expect(req.requestHeaders['Upload-Metadata']).toBe('test d29ybGQ='); // world

        req.respondWith({
          status: 201,
          responseHeaders: {
            Location: 'https://tus.io/uploads/upload1',
          },
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('https://tus.io/uploads');
        expect(req.method).toBe('POST');
        expect(req.requestHeaders.Custom).toBe('blargh');
        expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0');
        expect(req.requestHeaders['Upload-Length']).toBe('6');
        expect(req.requestHeaders['Upload-Concat']).toBe('partial');
        expect(req.requestHeaders['Upload-Metadata']).toBe('test d29ybGQ='); // world

        req.respondWith({
          status: 201,
          responseHeaders: {
            Location: 'https://tus.io/uploads/upload2',
          },
        });

        req = await testStack.nextRequest();

        // Assert that the URLs have been stored.
        expect(testUrlStorage.addUpload).toHaveBeenCalled();

        expect(req.url).toBe('https://tus.io/uploads/upload1');
        expect(req.method).toBe('PATCH');
        expect(req.requestHeaders.Custom).toBe('blargh');
        expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0');
        expect(req.requestHeaders['Upload-Offset']).toBe('0');
        expect(req.requestHeaders['Content-Type']).toBe('application/offset+octet-stream');
        expect(req.bodySize).toBe(5);

        req.respondWith({
          status: 204,
          responseHeaders: {
            'Upload-Offset': '5',
          },
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('https://tus.io/uploads/upload2');
        expect(req.method).toBe('PATCH');
        expect(req.requestHeaders.Custom).toBe('blargh');
        expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0');
        expect(req.requestHeaders['Upload-Offset']).toBe('0');
        expect(req.requestHeaders['Content-Type']).toBe('application/offset+octet-stream');
        expect(req.bodySize).toBe(6);

        // Return an error to ensure that the individual partial upload is properly retried.
        req.respondWith({
          status: 500,
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('https://tus.io/uploads/upload2');
        expect(req.method).toBe('HEAD');

        req.respondWith({
          status: 204,
          responseHeaders: {
            'Upload-Length': '11',
            'Upload-Offset': '0',
          },
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('https://tus.io/uploads/upload2');
        expect(req.method).toBe('PATCH');
        expect(req.requestHeaders.Custom).toBe('blargh');
        expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0');
        expect(req.requestHeaders['Upload-Offset']).toBe('0');
        expect(req.requestHeaders['Content-Type']).toBe('application/offset+octet-stream');
        expect(req.bodySize).toBe(6);

        req.respondWith({
          status: 204,
          responseHeaders: {
            'Upload-Offset': '6',
          },
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('https://tus.io/uploads');
        expect(req.method).toBe('POST');
        expect(req.requestHeaders.Custom).toBe('blargh');
        expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0');
        expect(req.requestHeaders['Upload-Length']).toBeUndefined();
        expect(req.requestHeaders['Upload-Concat']).toBe(
          'final;https://tus.io/uploads/upload1 https://tus.io/uploads/upload2',
        );
        expect(req.requestHeaders['Upload-Metadata']).toBe('foo aGVsbG8='); // hello

        req.respondWith({
          status: 201,
          responseHeaders: {
            Location: 'https://tus.io/uploads/upload3',
          },
        });

        await options.onSuccess.toBeCalled();

        expect(upload.url).toBe('https://tus.io/uploads/upload3');
        expect(options.onProgress).toHaveBeenCalledWith(5, 11);
        expect(options.onProgress).toHaveBeenCalledWith(11, 11);
        expect(testUrlStorage.removeUpload).toHaveBeenCalled();
      });

      it('should split a file into multiple parts based on custom `parallelUploadBoundaries`', async () => {
        const testStack = new TestHttpStack();

        const parallelUploadBoundaries = [
          { start: 0, end: 1 },
          { start: 1, end: 11 },
        ];
        const file = getBlob('hello world');
        const options = {
          httpStack: testStack,
          parallelUploads: 2,
          parallelUploadBoundaries,
          endpoint: 'https://tus.io/uploads',
          onSuccess: waitableFunction(),
        };

        const upload = new tusJsClient.Upload(file, options);
        upload.start();

        let req = await testStack.nextRequest();
        expect(req.url).toBe('https://tus.io/uploads');
        expect(req.method).toBe('POST');
        expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0');
        expect(req.requestHeaders['Upload-Length']).toBe('1');
        expect(req.requestHeaders['Upload-Concat']).toBe('partial');

        req.respondWith({
          status: 201,
          responseHeaders: {
            Location: 'https://tus.io/uploads/upload1',
          },
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('https://tus.io/uploads');
        expect(req.method).toBe('POST');
        expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0');
        expect(req.requestHeaders['Upload-Length']).toBe('10');
        expect(req.requestHeaders['Upload-Concat']).toBe('partial');

        req.respondWith({
          status: 201,
          responseHeaders: {
            Location: 'https://tus.io/uploads/upload2',
          },
        });

        req = await testStack.nextRequest();

        expect(req.url).toBe('https://tus.io/uploads/upload1');
        expect(req.method).toBe('PATCH');
        expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0');
        expect(req.requestHeaders['Upload-Offset']).toBe('0');
        expect(req.requestHeaders['Content-Type']).toBe('application/offset+octet-stream');
        expect(req.bodySize).toBe(1);

        req.respondWith({
          status: 204,
          responseHeaders: {
            'Upload-Offset': '1',
          },
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('https://tus.io/uploads/upload2');
        expect(req.method).toBe('PATCH');
        expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0');
        expect(req.requestHeaders['Upload-Offset']).toBe('0');
        expect(req.requestHeaders['Content-Type']).toBe('application/offset+octet-stream');
        expect(req.bodySize).toBe(10);

        req.respondWith({
          status: 204,
          responseHeaders: {
            'Upload-Length': '11',
            'Upload-Offset': '0',
          },
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('https://tus.io/uploads/upload2');
        expect(req.method).toBe('PATCH');
        expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0');
        expect(req.requestHeaders['Upload-Offset']).toBe('0');
        expect(req.requestHeaders['Content-Type']).toBe('application/offset+octet-stream');
        expect(req.bodySize).toBe(10);

        req.respondWith({
          status: 204,
          responseHeaders: {
            'Upload-Offset': '10',
          },
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('https://tus.io/uploads');
        expect(req.method).toBe('POST');
        expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0');
        expect(req.requestHeaders['Upload-Length']).toBeUndefined();
        expect(req.requestHeaders['Upload-Concat']).toBe(
          'final;https://tus.io/uploads/upload1 https://tus.io/uploads/upload2',
        );

        req.respondWith({
          status: 201,
          responseHeaders: {
            Location: 'https://tus.io/uploads/upload3',
          },
        });

        await options.onSuccess.toBeCalled();
        expect(upload.url).toBe('https://tus.io/uploads/upload3');
      });

      it('should emit error from a partial upload', async () => {
        const testStack = new TestHttpStack();
        const file = getBlob('hello world');
        const options = {
          httpStack: testStack,
          parallelUploads: 2,
          retryDelays: null,
          endpoint: 'https://tus.io/uploads',
          onError: waitableFunction('onError'),
        };

        const upload = new tusJsClient.Upload(file, options);
        upload.start();

        const req = await testStack.nextRequest();
        expect(req.url).toBe('https://tus.io/uploads');
        expect(req.method).toBe('POST');
        expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0');
        expect(req.requestHeaders['Upload-Length']).toBe('5');

        req.respondWith({
          status: 500,
        });

        const err = await options.onError.toBeCalled();
        expect(err.message).toBe(
          'tus: unexpected response while creating upload, originated from request (method: POST, url: https://tus.io/uploads, response code: 500, response text: , request id: n/a)',
        );
        expect(err.originalRequest).toBe(req);
      });

      it('should resume the partial uploads', async () => {
        const testStack = new TestHttpStack();
        const file = getBlob('hello world');
        const options = {
          httpStack: testStack,
          // The client should resume the parallel uploads, even if it is not
          // configured for new uploads.
          parallelUploads: 1,
          endpoint: 'https://tus.io/uploads',
          onProgress() {},
          onSuccess: waitableFunction(),
        };
        spyOn(options, 'onProgress');

        const upload = new tusJsClient.Upload(file, options);

        upload.resumeFromPreviousUpload({
          urlStorageKey: 'tus::fingerprint::1337',
          parallelUploadUrls: ['https://tus.io/uploads/upload1', 'https://tus.io/uploads/upload2'],
        });

        upload.start();

        let req = await testStack.nextRequest();
        expect(req.url).toBe('https://tus.io/uploads/upload1');
        expect(req.method).toBe('HEAD');

        req.respondWith({
          status: 204,
          responseHeaders: {
            'Upload-Length': '5',
            'Upload-Offset': '2',
          },
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('https://tus.io/uploads/upload2');
        expect(req.method).toBe('HEAD');

        req.respondWith({
          status: 204,
          responseHeaders: {
            'Upload-Length': '6',
            'Upload-Offset': '0',
          },
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('https://tus.io/uploads/upload1');
        expect(req.method).toBe('PATCH');
        expect(req.bodySize).toBe(3);

        req.respondWith({
          status: 204,
          responseHeaders: {
            'Upload-Offset': '5',
          },
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('https://tus.io/uploads/upload2');
        expect(req.method).toBe('PATCH');
        expect(req.bodySize).toBe(6);

        req.respondWith({
          status: 204,
          responseHeaders: {
            'Upload-Offset': '6',
          },
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('https://tus.io/uploads');
        expect(req.method).toBe('POST');
        expect(req.requestHeaders['Upload-Concat']).toBe(
          'final;https://tus.io/uploads/upload1 https://tus.io/uploads/upload2',
        );

        req.respondWith({
          status: 201,
          responseHeaders: {
            Location: 'https://tus.io/uploads/upload3',
          },
        });

        await options.onSuccess.toBeCalled();

        expect(upload.url).toBe('https://tus.io/uploads/upload3');
        expect(options.onProgress).toHaveBeenCalledWith(5, 11);
        expect(options.onProgress).toHaveBeenCalledWith(11, 11);
      });

      it('should abort all partial uploads and resume from them', async () => {
        const testStack = new TestHttpStack();
        const file = getBlob('hello world');
        const options = {
          httpStack: testStack,
          parallelUploads: 2,
          endpoint: 'https://tus.io/uploads',
          onProgress() {},
          onSuccess: waitableFunction(),
          fingerprint: () => Promise.resolve('fingerprinted'),
        };
        spyOn(options, 'onProgress');

        const upload = new tusJsClient.Upload(file, options);
        upload.start();

        let req = await testStack.nextRequest();
        expect(req.url).toBe('https://tus.io/uploads');
        expect(req.method).toBe('POST');
        expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0');
        expect(req.requestHeaders['Upload-Length']).toBe('5');
        expect(req.requestHeaders['Upload-Concat']).toBe('partial');
        expect(req.requestHeaders['Upload-Metadata']).toBeUndefined();

        req.respondWith({
          status: 201,
          responseHeaders: {
            Location: 'https://tus.io/uploads/upload1',
          },
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('https://tus.io/uploads');
        expect(req.method).toBe('POST');
        expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0');
        expect(req.requestHeaders['Upload-Length']).toBe('6');
        expect(req.requestHeaders['Upload-Concat']).toBe('partial');
        expect(req.requestHeaders['Upload-Metadata']).toBeUndefined();

        req.respondWith({
          status: 201,
          responseHeaders: {
            Location: 'https://tus.io/uploads/upload2',
          },
        });

        const req1 = await testStack.nextRequest();
        expect(req1.url).toBe('https://tus.io/uploads/upload1');
        expect(req1.method).toBe('PATCH');
        expect(req1.requestHeaders['Tus-Resumable']).toBe('1.0.0');
        expect(req1.requestHeaders['Upload-Offset']).toBe('0');
        expect(req1.requestHeaders['Content-Type']).toBe('application/offset+octet-stream');
        expect(req1.bodySize).toBe(5);

        const req2 = await testStack.nextRequest();
        expect(req2.url).toBe('https://tus.io/uploads/upload2');
        expect(req2.method).toBe('PATCH');
        expect(req2.requestHeaders['Tus-Resumable']).toBe('1.0.0');
        expect(req2.requestHeaders['Upload-Offset']).toBe('0');
        expect(req2.requestHeaders['Content-Type']).toBe('application/offset+octet-stream');
        expect(req2.bodySize).toBe(6);

        upload.abort();

        req1.respondWith({
          status: 204,
          responseHeaders: {
            'Upload-Offset': '5',
          },
        });

        req2.respondWith({
          status: 204,
          responseHeaders: {
            'Upload-Offset': '6',
          },
        });

        // No further requests should be sent.
        const reqPromise = testStack.nextRequest();
        const result = await Promise.race([reqPromise, wait(100)]);
        expect(result).toBe('timed out');

        // Restart the upload
        upload.start();

        // Reuse the promise from before as it is not cancelled.
        req = await reqPromise;
        expect(req.url).toBe('https://tus.io/uploads/upload1');
        expect(req.method).toBe('HEAD');

        req.respondWith({
          status: 204,
          responseHeaders: {
            'Upload-Length': '5',
            'Upload-Offset': '5',
          },
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('https://tus.io/uploads/upload2');
        expect(req.method).toBe('HEAD');

        req.respondWith({
          status: 204,
          responseHeaders: {
            'Upload-Length': '6',
            'Upload-Offset': '6',
          },
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('https://tus.io/uploads');
        expect(req.method).toBe('POST');
        expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0');
        expect(req.requestHeaders['Upload-Length']).toBeUndefined();
        expect(req.requestHeaders['Upload-Concat']).toBe(
          'final;https://tus.io/uploads/upload1 https://tus.io/uploads/upload2',
        );

        req.respondWith({
          status: 201,
          responseHeaders: {
            Location: 'https://tus.io/uploads/upload3',
          },
        });

        await options.onSuccess.toBeCalled();

        expect(upload.url).toBe('https://tus.io/uploads/upload3');
        expect(options.onProgress).toHaveBeenCalledWith(5, 11);
        expect(options.onProgress).toHaveBeenCalledWith(11, 11);
      });
    });
  });

  describe('tus', () => {
    describe('terminate upload', () => {
      it('should terminate upload when abort is called with true', async () => {
        let abortPromise;
        const testStack = new TestHttpStack();
        const file = getBlob('hello world');
        const options = {
          httpStack: testStack,
          endpoint: 'http://tus.io/files/',
          chunkSize: 5,
          onChunkComplete() {
            abortPromise = upload.abort(true);
          },
        };

        spyOn(options, 'onChunkComplete').and.callThrough();

        const upload = new tusJsClient.Upload(file, options);
        upload.start();

        let req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/files/');
        expect(req.method).toBe('POST');

        req.respondWith({
          status: 201,
          responseHeaders: {
            Location: '/files/foo',
          },
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/files/foo');
        expect(req.method).toBe('PATCH');

        req.respondWith({
          status: 204,
          responseHeaders: {
            'Upload-Offset': '5',
          },
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/files/foo');
        expect(req.method).toBe('DELETE');

        req.respondWith({
          status: 204,
        });

        expect(options.onChunkComplete).toHaveBeenCalled();
        await abortPromise;
      });

      it('should retry terminate when an error is returned on first try', async () => {
        let abortPromise;
        const testStack = new TestHttpStack();
        const file = getBlob('hello world');
        const options = {
          httpStack: testStack,
          endpoint: 'http://tus.io/files/',
          chunkSize: 5,
          retryDelays: [10, 10, 10],
          onChunkComplete() {
            abortPromise = upload.abort(true);
          },
        };

        spyOn(options, 'onChunkComplete').and.callThrough();

        const upload = new tusJsClient.Upload(file, options);
        upload.start();

        let req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/files/');
        expect(req.method).toBe('POST');

        req.respondWith({
          status: 201,
          responseHeaders: {
            Location: '/files/foo',
          },
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/files/foo');
        expect(req.method).toBe('PATCH');

        req.respondWith({
          status: 204,
          responseHeaders: {
            'Upload-Offset': '5',
          },
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/files/foo');
        expect(req.method).toBe('DELETE');

        req.respondWith({
          status: 423,
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/files/foo');
        expect(req.method).toBe('DELETE');

        req.respondWith({
          status: 204,
        });

        await abortPromise;
        expect(options.onChunkComplete).toHaveBeenCalled();
      });

      it('should stop retrying when all delays are used up', async () => {
        const testStack = new TestHttpStack();
        const options = {
          httpStack: testStack,
          retryDelays: [10, 10],
        };

        const terminatePromise = tusJsClient.Upload.terminate('http://tus.io/files/foo', options);

        let req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/files/foo');
        expect(req.method).toBe('DELETE');

        req.respondWith({
          status: 500,
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/files/foo');
        expect(req.method).toBe('DELETE');

        req.respondWith({
          status: 500,
        });

        req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/files/foo');
        expect(req.method).toBe('DELETE');

        req.respondWith({
          status: 500,
        });

        await expectAsync(terminatePromise).toBeRejectedWithError(
          /tus: unexpected response while terminating upload/,
        );
      });

      it('should invoke the request and response Promises', async () => {
        const testStack = new TestHttpStack();
        const options = {
          httpStack: testStack,
          onBeforeRequest(req) {
            return new Promise((resolve) => {
              expect(req.getURL()).toBe('http://tus.io/uploads/foo');
              expect(req.getMethod()).toBe('DELETE');
              resolve();
            })
          },
          onAfterResponse(req, res) {
            return new Promise((resolve) => {
              expect(req.getURL()).toBe('http://tus.io/uploads/foo');
              expect(req.getMethod()).toBe('DELETE');
              expect(res.getStatus()).toBe(204);
              resolve();
            })
          },
        };
        spyOn(options, 'onBeforeRequest');
        spyOn(options, 'onAfterResponse');

        const terminatePromise = tusJsClient.Upload.terminate('http://tus.io/uploads/foo', options);

        const req = await testStack.nextRequest();
        expect(req.url).toBe('http://tus.io/uploads/foo');
        expect(req.method).toBe('DELETE');

        req.respondWith({
          status: 204,
        });

        await expectAsync(terminatePromise).toBeResolved();
        expect(options.onBeforeRequest).toHaveBeenCalled();
        expect(options.onAfterResponse).toHaveBeenCalled();
      });
    });
  });

  describe('tus', () => {
    describe('#Upload', () => {
      describe('uploading data from a Web Stream ReadableStream', () => {
        function makeReader(content, readSize = content.length) {
          let remainingData = new TextEncoder().encode(content);
          return new ReadableStream({
            pull(controller) {
              if (remainingData.length > 0) {
                const chunk = remainingData.subarray(0, readSize);
                remainingData = remainingData.subarray(readSize);
                controller.enqueue(chunk);
              } else {
                controller.close();
              }
            },
          })
        }

        async function assertReaderUpload({ readSize, chunkSize }) {
          const reader = makeReader('hello world', readSize);

          const testStack = new TestHttpStack();
          const options = {
            httpStack: testStack,
            endpoint: 'http://tus.io/uploads',
            chunkSize,
            onProgress: waitableFunction('onProgress'),
            onSuccess: waitableFunction('onSuccess'),
            fingerprint() {},
            uploadLengthDeferred: true,
          };
          spyOn(options, 'fingerprint').and.resolveTo('fingerprinted');

          const upload = new tusJsClient.Upload(reader, options);
          upload.start();

          expect(options.fingerprint).toHaveBeenCalledWith(reader, upload.options);

          let req = await testStack.nextRequest();
          expect(req.url).toBe('http://tus.io/uploads');
          expect(req.method).toBe('POST');
          expect(req.requestHeaders['Upload-Length']).toBe(undefined);
          expect(req.requestHeaders['Upload-Defer-Length']).toBe('1');

          req.respondWith({
            status: 201,
            responseHeaders: {
              Location: 'http://tus.io/uploads/blargh',
            },
          });

          req = await testStack.nextRequest();
          expect(req.url).toBe('http://tus.io/uploads/blargh');
          expect(req.method).toBe('PATCH');
          expect(req.requestHeaders['Upload-Offset']).toBe('0');
          expect(req.requestHeaders['Upload-Length']).toBe('11');
          expect(req.requestHeaders['Content-Type']).toBe('application/offset+octet-stream');
          expect(req.bodySize).toBe(11);

          req.respondWith({
            status: 204,
            responseHeaders: {
              'Upload-Offset': '11',
            },
          });

          await options.onProgress.toBeCalled();
          expect(options.onProgress).toHaveBeenCalledWith(0, 11);

          await options.onSuccess.toBeCalled();
          expect(upload.url).toBe('http://tus.io/uploads/blargh');
          expect(options.onProgress).toHaveBeenCalledWith(11, 11);
        }

        it('should upload data', async () => {
          await assertReaderUpload({ chunkSize: 100, readSize: 100 });
        });

        it('should read multiple times from the reader', async () => {
          await assertReaderUpload({ chunkSize: 100, readSize: 6 });
        });

        it('should use multiple PATCH requests', async () => {
          const reader = makeReader('hello world', 1);

          const testStack = new TestHttpStack();
          const options = {
            httpStack: testStack,
            endpoint: 'http://tus.io/uploads',
            chunkSize: 6,
            onProgress: waitableFunction('onProgress'),
            onSuccess: waitableFunction('onSuccess'),
            fingerprint() {},
            uploadLengthDeferred: true,
          };
          spyOn(options, 'fingerprint').and.resolveTo('fingerprinted');

          const upload = new tusJsClient.Upload(reader, options);
          upload.start();

          expect(options.fingerprint).toHaveBeenCalledWith(reader, upload.options);

          let req = await testStack.nextRequest();
          expect(req.url).toBe('http://tus.io/uploads');
          expect(req.method).toBe('POST');
          expect(req.requestHeaders['Upload-Length']).toBe(undefined);
          expect(req.requestHeaders['Upload-Defer-Length']).toBe('1');

          req.respondWith({
            status: 201,
            responseHeaders: {
              Location: 'http://tus.io/uploads/blargh',
            },
          });

          req = await testStack.nextRequest();
          expect(req.url).toBe('http://tus.io/uploads/blargh');
          expect(req.method).toBe('PATCH');
          expect(req.requestHeaders['Upload-Offset']).toBe('0');
          expect(req.requestHeaders['Content-Type']).toBe('application/offset+octet-stream');
          expect(req.bodySize).toBe(6);

          req.respondWith({
            status: 204,
            responseHeaders: {
              'Upload-Offset': '6',
            },
          });

          await options.onProgress.toBeCalled();
          expect(options.onProgress).toHaveBeenCalledWith(6, null);

          req = await testStack.nextRequest();
          expect(req.url).toBe('http://tus.io/uploads/blargh');
          expect(req.method).toBe('PATCH');
          expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0');
          expect(req.requestHeaders['Upload-Offset']).toBe('6');
          expect(req.requestHeaders['Upload-Length']).toBe('11');
          expect(req.requestHeaders['Content-Type']).toBe('application/offset+octet-stream');
          expect(req.bodySize).toBe(5);

          req.respondWith({
            status: 204,
            responseHeaders: {
              'Upload-Offset': '11',
            },
          });

          await options.onSuccess.toBeCalled();
          expect(upload.url).toBe('http://tus.io/uploads/blargh');
          expect(options.onProgress).toHaveBeenCalledWith(11, 11);
        });

        it('should retry the POST request', async () => {
          const reader = makeReader('hello world', 1);

          const testStack = new TestHttpStack();
          const options = {
            httpStack: testStack,
            endpoint: 'http://tus.io/files/',
            chunkSize: 11,
            retryDelays: [10, 10, 10],
            onSuccess: waitableFunction('onSuccess'),
            uploadLengthDeferred: true,
          };

          const upload = new tusJsClient.Upload(reader, options);
          upload.start();

          let req = await testStack.nextRequest();
          expect(req.url).toBe('http://tus.io/files/');
          expect(req.method).toBe('POST');

          req.respondWith({
            status: 500,
          });

          req = await testStack.nextRequest();
          expect(req.url).toBe('http://tus.io/files/');
          expect(req.method).toBe('POST');

          req.respondWith({
            status: 201,
            responseHeaders: {
              Location: '/files/foo',
            },
          });

          req = await testStack.nextRequest();
          expect(req.url).toBe('http://tus.io/files/foo');
          expect(req.method).toBe('PATCH');

          req.respondWith({
            status: 204,
            responseHeaders: {
              'Upload-Offset': '11',
            },
          });

          req = await testStack.nextRequest();
          expect(req.url).toBe('http://tus.io/files/foo');
          expect(req.method).toBe('PATCH');
          expect(req.requestHeaders['Upload-Length']).toBe('11');

          req.respondWith({
            status: 204,
            responseHeaders: {
              'Upload-Offset': '11',
            },
          });

          await options.onSuccess.toBeCalled();
        });

        it('should retry the first PATCH request', async () => {
          const reader = makeReader('hello world', 1);

          const testStack = new TestHttpStack();
          const options = {
            httpStack: testStack,
            endpoint: 'http://tus.io/files/',
            chunkSize: 11,
            retryDelays: [10, 10, 10],
            onSuccess: waitableFunction('onSuccess'),
            uploadLengthDeferred: true,
          };

          const upload = new tusJsClient.Upload(reader, options);
          upload.start();

          let req = await testStack.nextRequest();
          expect(req.url).toBe('http://tus.io/files/');
          expect(req.method).toBe('POST');
          expect(req.requestHeaders['Upload-Defer-Length']).toBe('1');

          req.respondWith({
            status: 201,
            responseHeaders: {
              Location: '/files/foo',
            },
          });

          req = await testStack.nextRequest();
          expect(req.url).toBe('http://tus.io/files/foo');
          expect(req.method).toBe('PATCH');

          req.respondWith({
            status: 500,
          });

          req = await testStack.nextRequest();
          expect(req.url).toBe('http://tus.io/files/foo');
          expect(req.method).toBe('HEAD');

          req.respondWith({
            status: 204,
            responseHeaders: {
              'Upload-Offset': '0',
              'Upload-Defer-Length': '1',
            },
          });

          req = await testStack.nextRequest();
          expect(req.url).toBe('http://tus.io/files/foo');
          expect(req.method).toBe('PATCH');

          req.respondWith({
            status: 204,
            responseHeaders: {
              'Upload-Offset': '11',
            },
          });

          req = await testStack.nextRequest();
          expect(req.url).toBe('http://tus.io/files/foo');
          expect(req.method).toBe('PATCH');
          expect(req.requestHeaders['Upload-Length']).toBe('11');

          req.respondWith({
            status: 204,
            responseHeaders: {
              'Upload-Offset': '11',
            },
          });

          await options.onSuccess.toBeCalled();
        });

        it('should retry following PATCH requests', async () => {
          const reader = makeReader('hello world there!');

          const testStack = new TestHttpStack();
          const options = {
            httpStack: testStack,
            endpoint: 'http://tus.io/files/',
            chunkSize: 6,
            retryDelays: [10, 10, 10],
            onSuccess: waitableFunction('onSuccess'),
            uploadLengthDeferred: true,
          };

          const upload = new tusJsClient.Upload(reader, options);
          upload.start();

          let req = await testStack.nextRequest();
          expect(req.url).toBe('http://tus.io/files/');
          expect(req.method).toBe('POST');
          expect(req.requestHeaders['Upload-Defer-Length']).toBe('1');

          req.respondWith({
            status: 201,
            responseHeaders: {
              Location: '/files/foo',
            },
          });

          req = await testStack.nextRequest();
          expect(req.url).toBe('http://tus.io/files/foo');
          expect(req.method).toBe('PATCH');

          req.respondWith({
            status: 204,
            responseHeaders: {
              'Upload-Offset': '6',
            },
          });

          req = await testStack.nextRequest();
          expect(req.url).toBe('http://tus.io/files/foo');
          expect(req.method).toBe('PATCH');

          req.respondWith({
            status: 500,
          });

          req = await testStack.nextRequest();
          expect(req.url).toBe('http://tus.io/files/foo');
          expect(req.method).toBe('HEAD');

          req.respondWith({
            status: 204,
            responseHeaders: {
              'Upload-Offset': '6',
              'Upload-Defer-Length': '1',
            },
          });

          req = await testStack.nextRequest();
          expect(req.url).toBe('http://tus.io/files/foo');
          expect(req.method).toBe('PATCH');

          req.respondWith({
            status: 204,
            responseHeaders: {
              'Upload-Offset': '12',
            },
          });

          req = await testStack.nextRequest();
          expect(req.url).toBe('http://tus.io/files/foo');
          expect(req.method).toBe('PATCH');

          req.respondWith({
            status: 204,
            responseHeaders: {
              'Upload-Offset': '18',
            },
          });

          req = await testStack.nextRequest();
          expect(req.url).toBe('http://tus.io/files/foo');
          expect(req.method).toBe('PATCH');
          expect(req.requestHeaders['Upload-Length']).toBe('18');

          req.respondWith({
            status: 204,
            responseHeaders: {
              'Upload-Offset': '18',
            },
          });

          await options.onSuccess.toBeCalled();
        });

        it('should throw an error if the source provides less data than uploadSize', async () => {
          const reader = makeReader('hello world');

          const testStack = new TestHttpStack();
          const options = {
            httpStack: testStack,
            uploadSize: 100,
            chunkSize: 100,
            endpoint: 'http://tus.io/uploads',
            retryDelays: [],
            onError: waitableFunction('onError'),
          };

          const upload = new tusJsClient.Upload(reader, options);
          upload.start();
          const req = await testStack.nextRequest();
          expect(req.url).toBe('http://tus.io/uploads');
          expect(req.method).toBe('POST');
          expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0');

          req.respondWith({
            status: 204,
            responseHeaders: {
              Location: 'http://tus.io/uploads/foo',
            },
          });

          const err = await options.onError.toBeCalled();
          expect(err.message).toBe(
            'tus: failed to upload chunk at offset 0, caused by Error: upload was configured with a size of 100 bytes, but the source is done after 11 bytes, originated from request (method: PATCH, url: http://tus.io/uploads/foo, response code: n/a, response text: n/a, request id: n/a)',
          );
        });

        it('should upload data correctly when using a non zero starting offset', async () => {
          const reader = makeReader('hello world', 1);

          const testStack = new TestHttpStack();
          const options = {
            httpStack: testStack,
            uploadUrl: 'http://tus.io/uploads/fileid',
            retryDelays: [],
            chunkSize: 6,
            uploadLengthDeferred: true,
            onSuccess: waitableFunction('onSuccess'),
          };

          const upload = new tusJsClient.Upload(reader, options);
          upload.start();

          let req = await testStack.nextRequest();
          expect(req.url).toBe('http://tus.io/uploads/fileid');
          expect(req.method).toBe('HEAD');
          expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0');

          // Respond with a non zero offset to test that the stream that is created
          // for the reader returns the correct data and ignores the data in the stream
          // before the offset.
          req.respondWith({
            status: 200,
            responseHeaders: {
              'Upload-Offset': '6',
              'Upload-Defer-Length': '1',
            },
          });

          req = await testStack.nextRequest();
          expect(req.url).toBe('http://tus.io/uploads/fileid');
          expect(req.method).toBe('PATCH');
          expect(req.requestHeaders['Upload-Length']).toBe('11');
          expect(req.requestHeaders['Upload-Offset']).toBe('6');
          expect(req.requestHeaders['Content-Type']).toBe('application/offset+octet-stream');
          expect(req.body.length).toBe(5);
          const bodyText = new TextDecoder().decode(req.body);
          expect(bodyText).toBe('world');

          req.respondWith({
            status: 204,
            responseHeaders: {
              'Upload-Offset': '11',
            },
          });

          await options.onSuccess.toBeCalled();
        });
      });
    });
  });

  describe('tus', () => {
    describe('#Upload', () => {
      const cases = [
        {
          name: 'ArrayBuffer',
          get: () => new TextEncoder().encode('Hello, world!').buffer,
          check: (val) => val instanceof ArrayBuffer,
        },
        {
          name: 'Uint8Array',
          get: () => new TextEncoder().encode('Hello, world!'),
          check: (val) => val instanceof Uint8Array,
        },
        {
          name: 'DataView',
          // DataView does not cover the whole buffer and tus-js-client should respect that.
          get: () => new DataView(new TextEncoder().encode('XXXHello, world!XXX').buffer, 3, 13),
          check: (val) => ArrayBuffer.isView(val),
        },
        {
          name: 'Blob',
          get: () => new Blob(['Hello, world!'], { type: 'text/plain' }),
          check: (val) => val instanceof Blob,
        },
      ];

      for (const { name, get, check } of cases) {
        it(`should upload from a(n) ${name}`, async () => {
          const value = get();
          if (!check(value)) {
            throw new Error(`Value is not a(n) ${name}, but ${value} instead`)
          }

          const testStack = new TestHttpStack();
          const options = {
            httpStack: testStack,
            endpoint: 'http://tus.io/uploads',
            chunkSize: 7,
            onSuccess: waitableFunction('onSuccess'),
            onProgress() {},
            onChunkComplete() {},
          };
          spyOn(options, 'onProgress');
          spyOn(options, 'onChunkComplete');

          const upload = new tusJsClient.Upload(value, options);
          upload.start();

          let req = await testStack.nextRequest();
          expect(req.url).toBe('http://tus.io/uploads');
          expect(req.method).toBe('POST');
          expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0');
          expect(req.requestHeaders['Upload-Length']).toBe('13');

          req.respondWith({
            status: 201,
            responseHeaders: {
              Location: '/uploads/blargh',
            },
          });

          req = await testStack.nextRequest();
          expect(req.url).toBe('http://tus.io/uploads/blargh');
          expect(req.method).toBe('PATCH');
          expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0');
          expect(req.requestHeaders['Upload-Offset']).toBe('0');
          expect(req.requestHeaders['Content-Type']).toBe('application/offset+octet-stream');
          expect(req.bodySize).toBe(7);

          req.respondWith({
            status: 204,
            responseHeaders: {
              'Upload-Offset': '7',
            },
          });

          req = await testStack.nextRequest();
          expect(req.url).toBe('http://tus.io/uploads/blargh');
          expect(req.method).toBe('PATCH');
          expect(req.requestHeaders['Tus-Resumable']).toBe('1.0.0');
          expect(req.requestHeaders['Upload-Offset']).toBe('7');
          expect(req.requestHeaders['Content-Type']).toBe('application/offset+octet-stream');
          expect(req.bodySize).toBe(6);

          req.respondWith({
            status: 204,
            responseHeaders: {
              'Upload-Offset': '13',
            },
          });

          await options.onSuccess.toBeCalled();

          expect(upload.url).toBe('http://tus.io/uploads/blargh');
          expect(options.onProgress).toHaveBeenCalledWith(13, 13);
          expect(options.onChunkComplete).toHaveBeenCalledWith(7, 7, 13);
          expect(options.onChunkComplete).toHaveBeenCalledWith(6, 13, 13);
        });
      }
    });
  });

  // Test timeout for end-to-end tests when uploading to real server.
  const END_TO_END_TIMEOUT = 20 * 1000;

  describe('tus', () => {
    describe('end-to-end', () => {
      it(
        'should upload to a real tus server',
        async () => {
          return new Promise((resolve, reject) => {
            const file = getBlob('hello world');
            const options = {
              endpoint: 'https://tusd.tusdemo.net/files/',
              metadata: {
                nonlatin: 'słońce',
                number: 100,
                filename: 'hello.txt',
                filetype: 'text/plain',
              },
              onSuccess() {
                expect(upload.url).toMatch(/^https:\/\/tusd\.tusdemo\.net\/files\//);
                console.log('Upload URL:', upload.url);

                resolve(upload);
              },
              onError(err) {
                reject(err);
              },
            };

            const upload = new tusJsClient.Upload(file, options);
            upload.start();
          })
            .then(validateUploadContent)
            .then((upload) => {
              return upload.abort(true).then(() => upload)
            })
            .then(validateUploadDeletion)
        },
        END_TO_END_TIMEOUT,
      );

      it(
        'should upload to a real tus server with creation-with-upload',
        async () => {
          return new Promise((resolve, reject) => {
            const file = getBlob('hello world');
            const options = {
              endpoint: 'https://tusd.tusdemo.net/files/',
              metadata: {
                nonlatin: 'słońce',
                number: 100,
                filename: 'hello.txt',
                filetype: 'text/plain',
              },
              onSuccess() {
                expect(upload.url).toMatch(/^https:\/\/tusd\.tusdemo\.net\/files\//);
                console.log('Upload URL:', upload.url);

                resolve(upload);
              },
              onError(err) {
                reject(err);
              },
            };

            const upload = new tusJsClient.Upload(file, options);
            upload.start();
          }).then(validateUploadContent)
        },
        END_TO_END_TIMEOUT,
      );
    });
  });

  function validateUploadContent(upload) {
    return fetch(upload.url)
      .then((res) => {
        expect(res.status).toBe(200);
        return res.text()
      })
      .then((data) => {
        expect(data).toBe('hello world');

        return validateUploadMetadata(upload)
      })
  }

  function validateUploadMetadata(upload) {
    return fetch(upload.url, {
      method: 'HEAD',
      headers: {
        'Tus-Resumable': '1.0.0',
      },
    })
      .then((res) => {
        expect(res.status).toBe(200);
        expect(res.headers.get('tus-resumable')).toBe('1.0.0');
        expect(res.headers.get('upload-offset')).toBe('11');
        expect(res.headers.get('upload-length')).toBe('11');

        // The values in the Upload-Metadata header may not be in the same
        // order as we submitted them (the specification does not require
        // that). Therefore, we split the values and verify that each one
        // is present.
        const metadataStr = res.headers.get('upload-metadata');
        expect(metadataStr).toBeTruthy();
        const metadata = metadataStr.split(',');
        expect(metadata).toContain('filename aGVsbG8udHh0');
        expect(metadata).toContain('filetype dGV4dC9wbGFpbg==');
        expect(metadata).toContain('nonlatin c8WCb8WEY2U=');
        expect(metadata).toContain('number MTAw');
        expect(metadata.length).toBe(4);

        return res.text()
      })
      .then((data) => {
        expect(data).toBe('');

        return upload
      })
  }

  function validateUploadDeletion(upload) {
    return fetch(upload.url).then((res) => {
      expect(res.status).toBe(404);

      return upload
    })
  }

  /**
   * Helper to get body size for various input types
   */
  function getBodySize(body) {
    if (body == null) return null
    if (body instanceof Blob) return body.size
    if (body.length != null) return body.length
    return 0
  }

  /**
   * Enhanced HTTP stack for testing stall detection scenarios
   * Supports both complete stalls and custom progress sequences
   */
  class StallTestHttpStack extends TestHttpStack {
    constructor() {
      super();
      this.stallOnNextPatch = false;
      this.progressSequences = new Map();
      this.progressPromises = new Map();
      this.nextProgressSequence = null;
    }

    /**
     * Configure the stack to stall on the next PATCH request
     */
    simulateStallOnNextPatch() {
      this.stallOnNextPatch = true;
    }

    /**
     * Set a custom progress sequence for the next PATCH request
     * @param {Array} sequence - Array of {bytes: number, delay: number} objects
     */
    setNextProgressSequence(sequence) {
      this.nextProgressSequence = sequence;
    }

    supportsProgressEvents() {
      return true
    }

    createRequest(method, url) {
      const req = super.createRequest(method, url);

      if (method === 'PATCH') {
        this._setupPatchRequest(req);
      }

      return req
    }

    _setupPatchRequest(req) {
      const self = this;

      // Handle complete stalls
      if (this.stallOnNextPatch) {
        this.stallOnNextPatch = false;
        req.send = async function (body) {
          this.body = body;
          if (body) {
            this.bodySize = await getBodySize(body);
            // Don't call progress handler to simulate a complete stall
          }
          this._onRequestSend(this);
          return this._requestPromise
        };
        return
      }

      // Handle progress sequences
      if (this.nextProgressSequence) {
        this.progressSequences.set(req, this.nextProgressSequence);
        this.nextProgressSequence = null;
      }

      // Override respondWith to wait for progress events
      const originalRespondWith = req.respondWith.bind(req);
      req.respondWith = async (resData) => {
        const progressPromise = self.progressPromises.get(req);
        if (progressPromise) {
          await progressPromise;
          self.progressPromises.delete(req);
        }
        originalRespondWith(resData);
      };

      // Override send to handle progress sequences
      req.send = async function (body) {
        this.body = body;
        if (body) {
          this.bodySize = await getBodySize(body);
        }

        const progressSequence = self.progressSequences.get(req);
        if (progressSequence && this._onProgress) {
          self._scheduleProgressSequence(req, progressSequence, this._onProgress);
        } else if (this._onProgress) {
          self._scheduleDefaultProgress(req, this._onProgress, this.bodySize);
        }

        this._onRequestSend(this);
        return this._requestPromise
      };
    }

    _scheduleProgressSequence(req, sequence, progressHandler) {
      const progressPromise = new Promise((resolve) => {
        setTimeout(async () => {
          for (const event of sequence) {
            await new Promise((resolve) => setTimeout(resolve, event.delay || 0));
            progressHandler(event.bytes);
          }
          resolve();
        }, 10); // Small delay to ensure stall detector is started
      });
      this.progressPromises.set(req, progressPromise);
    }

    _scheduleDefaultProgress(req, progressHandler, bodySize) {
      const progressPromise = new Promise((resolve) => {
        setTimeout(() => {
          progressHandler(0);
          progressHandler(bodySize);
          resolve();
        }, 10); // Small delay to ensure stall detector is started
      });
      this.progressPromises.set(req, progressPromise);
    }
  }

  /**
   * Common test setup helper
   */
  function createTestUpload(options = {}) {
    const defaultOptions = {
      httpStack: new StallTestHttpStack(),
      endpoint: 'https://tus.io/uploads',
      onError: waitableFunction('onError'),
      onSuccess: waitableFunction('onSuccess'),
      onProgress: waitableFunction('onProgress'),
    };

    const file = options.file || getBlob('hello world');
    const uploadOptions = { ...defaultOptions, ...options };
    const upload = new tusJsClient.Upload(file, uploadOptions);

    return { upload, options: uploadOptions, testStack: uploadOptions.httpStack }
  }

  /**
   * Helper to handle standard upload creation flow
   */
  async function handleUploadCreation(testStack, location = '/uploads/12345') {
    const req = await testStack.nextRequest();
    expect(req.method).toBe('POST');
    req.respondWith({
      status: 201,
      responseHeaders: {
        Location: location,
      },
    });
    return req
  }

  describe('tus-stall-detection', () => {
    describe('integration tests', () => {
      it("should not enable stall detection if HTTP stack doesn't support progress events", async () => {
        const { enableDebugLog } = await import('tus-js-client');
        enableDebugLog();

        const testStack = new TestHttpStack();
        testStack.supportsProgressEvents = () => false;

        const { upload } = createTestUpload({
          httpStack: testStack,
          stallDetection: { enabled: true },
        });

        // Capture console output
        const originalLog = console.log;
        let loggedMessage = '';
        console.log = (message) => {
          loggedMessage += message;
        };

        upload.start();

        const req = await testStack.nextRequest();
        expect(req.url).toBe('https://tus.io/uploads');
        expect(req.method).toBe('POST');
        req.respondWith({
          status: 201,
          responseHeaders: { Location: '/uploads/12345' },
        });

        await wait(50);
        console.log = originalLog;

        expect(loggedMessage).toContain(
          'tus: stall detection is enabled but the HTTP stack does not support progress events',
        );

        upload.abort();
      });

      it('should upload a file with stall detection enabled', async () => {
        const { upload, options, testStack } = createTestUpload({
          stallDetection: {
            enabled: true,
            checkInterval: 1000,
            stallTimeout: 2000,
          },
        });

        upload.start();

        await handleUploadCreation(testStack);

        const patchReq = await testStack.nextRequest();
        expect(patchReq.url).toBe('https://tus.io/uploads/12345');
        expect(patchReq.method).toBe('PATCH');

        patchReq.respondWith({
          status: 204,
          responseHeaders: { 'Upload-Offset': '11' },
        });

        await options.onSuccess.toBeCalled();
        expect(options.onError.calls.count()).toBe(0);
      });

      it('should detect stalls and emit error when no retries configured', async () => {
        const { upload, options, testStack } = createTestUpload({
          stallDetection: {
            enabled: true,
            checkInterval: 100,
            stallTimeout: 200,
          },
          retryDelays: null,
        });

        testStack.simulateStallOnNextPatch();
        upload.start();

        await handleUploadCreation(testStack);

        const error = await options.onError.toBeCalled();
        expect(error.message).toContain('stalled:');
      });

      it('should retry when stall is detected', async () => {
        const { upload, options, testStack } = createTestUpload({
          stallDetection: {
            enabled: true,
            checkInterval: 100,
            stallTimeout: 200,
          },
          retryDelays: [100],
        });

        testStack.simulateStallOnNextPatch();
        upload.start();

        let requestCount = 0;
        while (true) {
          const req = await testStack.nextRequest();
          requestCount++;

          if (req.method === 'POST') {
            req.respondWith({
              status: 201,
              responseHeaders: { Location: '/uploads/12345' },
            });
          } else if (req.method === 'HEAD') {
            req.respondWith({
              status: 200,
              responseHeaders: {
                'Upload-Offset': '0',
                'Upload-Length': '11',
              },
            });
          } else if (req.method === 'PATCH') {
            req.respondWith({
              status: 204,
              responseHeaders: { 'Upload-Offset': '11' },
            });
            break
          }

          if (requestCount > 10) {
            throw new Error('Too many requests')
          }
        }

        await options.onSuccess.toBeCalled();
        expect(options.onError.calls.count()).toBe(0);
        expect(requestCount).toBeGreaterThan(1);
      });

      it('should not incorrectly detect stalls during onBeforeRequest delays', async () => {
        const { upload, options, testStack } = createTestUpload({
          stallDetection: {
            enabled: true,
            checkInterval: 100,
            stallTimeout: 200,
          },
          onBeforeRequest: async (_req) => {
            await wait(300); // Longer than stall timeout
          },
        });

        upload.start();

        await handleUploadCreation(testStack);

        const patchReq = await testStack.nextRequest();
        expect(patchReq.url).toBe('https://tus.io/uploads/12345');
        expect(patchReq.method).toBe('PATCH');

        patchReq.respondWith({
          status: 204,
          responseHeaders: { 'Upload-Offset': '11' },
        });

        await options.onSuccess.toBeCalled();
        expect(options.onError.calls.count()).toBe(0);
      });

      it('should detect stalls when progress events stop mid-upload', async () => {
        const file = getBlob('hello world'.repeat(100));
        const { upload, options, testStack } = createTestUpload({
          file,
          stallDetection: {
            enabled: true,
            checkInterval: 100,
            stallTimeout: 200,
          },
          retryDelays: null,
        });

        // Create a progress sequence that stops at 30% of the file
        const fileSize = file.size;
        const progressSequence = [
          { bytes: 0, delay: 10 },
          { bytes: Math.floor(fileSize * 0.1), delay: 50 },
          { bytes: Math.floor(fileSize * 0.2), delay: 50 },
          { bytes: Math.floor(fileSize * 0.3), delay: 50 },
          // No more progress events after 30%
        ];

        testStack.setNextProgressSequence(progressSequence);
        upload.start();
        await handleUploadCreation(testStack);

        const error = await options.onError.toBeCalled();
        expect(error.message).toContain('stalled:');
        expect(options.onProgress.calls.count()).toBeGreaterThan(0);
      });

      it('should detect stalls when progress value does not change', async () => {
        const { upload, options, testStack } = createTestUpload({
          stallDetection: {
            enabled: true,
            checkInterval: 50,
            stallTimeout: 500,
          },
          retryDelays: null,
        });

        // Create a progress sequence that gets stuck at 300 bytes
        const progressSequence = [
          { bytes: 0, delay: 10 },
          { bytes: 100, delay: 10 },
          { bytes: 200, delay: 10 },
          { bytes: 300, delay: 10 },
          // Repeat the same value to trigger value-based stall detection
          ...Array(12).fill({ bytes: 300, delay: 30 }),
        ];

        testStack.setNextProgressSequence(progressSequence);
        upload.start();

        await handleUploadCreation(testStack);

        const patchReq = await testStack.nextRequest();
        expect(patchReq.method).toBe('PATCH');

        const error = await options.onError.toBeCalled();
        expect(error.message).toContain('stalled: no progress');
        expect(options.onProgress.calls.count()).toBeGreaterThan(0);
      });
    });
  });

  beforeEach(() => {
    // Clear localStorage before every test to prevent stored URLs to
    // interfere with our setup.
    localStorage.clear();
  });

})(tusJsClient);
//# sourceMappingURL=browser-test-bundle.js.map