axios-mock-adapter icon indicating copy to clipboard operation
axios-mock-adapter copied to clipboard

Mock failed with 404 Error

Open kirankbs opened this issue 7 years ago • 64 comments

i am using axios-mock-adapter to mock tests in react app. got below error while running single test but it is passing in case of all tests.

/home/dev/code/client/node_modules/react-scripts/scripts/test.js:22 throw err; ^ Error: Request failed with status code 404 at createErrorResponse (/home/dev/code/client/node_modules/axios-mock-adapter/src/utils.js:122:15) at Object.settle (/home/dev/code/client/node_modules/axios-mock-adapter/src/utils.js:102:16) at handleRequest (/home/dev/code/client/node_modules/axios-mock-adapter/src/handle_request.js:69:11) at /home/dev/code/client/node_modules/axios-mock-adapter/src/index.js:16:9 at MockAdapter. (/home/dev/code/client/node_modules/axios-mock-adapter/src/index.js:15:14) at dispatchRequest (/home/dev/code/client/node_modules/axios/lib/core/dispatchRequest.js:52:10)

kirankbs avatar Feb 14 '18 12:02 kirankbs

I can't do much with the little information that you're giving, but likely the code that adds the mock handlers does not get run when running a single test.

ctimmerm avatar Feb 14 '18 12:02 ctimmerm

I got a similar error to @kirankbs.

My code seems to not evaluate the axios.get bit with mock

Here is a sample of my code

export const getLocationInfo = (lat, lng) => {
  return ((dispatch) => {
    return axios.get(process.env.API_LOCATION_URL+`/json?latlng=${lat},${lng}&key=${process.env.GOOGLE_KEY}`)
      .then((response) => {
        dispatch({ type: "FETCHED_LOCATION_INFO", payload: response.data.results });
      }).catch((error) => {
        dispatch({ type: 'FAILED_FETCHING_LOCATION_INFO', payload: error });
      });
  });
};

The test code below


it('should dispatch FETCHED_LOCATION_INFO when getLocationInfo completes successfully', (done) => {

    let middlewares = [thunk];
    let mockStore = configureMockStore(middlewares);
    let store = mockStore({});
    mock = new MockAdapter(axios);
    mock.onGet('http://maps.google.com/').reply(200, {
      data: {
        results: [
          {
            formatted_address: 'Abc Rd, City, Country'
          }
        ]
      }
    });

    store.dispatch(getLocationInfo(-5.2177265, 12.9889)).then(() => {
       const actualActions = store.getActions();
       expect(expectedActions[0].type).toEqual(actualActions[0].type);
       done();
    });
  });

This is my error

[ { type: 'FAILED_FETCHING_LOCATION_INFO',
    payload: { Error: Request failed with status code 404
    at createErrorResponse (/Users/brianhawi/Documents/learning-projects/weather-app/node_modules/axios-mock-adapter/src/utils.js:117:15)
    at Object.settle (/Users/brianhawi/Documents/learning-projects/weather-app/node_modules/axios-mock-adapter/src/utils.js:97:16)
    at handleRequest (/Users/brianhawi/Documents/learning-projects/weather-app/node_modules/axios-mock-adapter/src/handle_request.js:69:11)
    at /Users/brianhawi/Documents/learning-projects/weather-app/node_modules/axios-mock-adapter/src/index.js:18:9
    at new Promise (<anonymous>)
    at MockAdapter.<anonymous> (/Users/brianhawi/Documents/learning-projects/weather-app/node_modules/axios-mock-adapter/src/index.js:17:14)
    at dispatchRequest (/Users/brianhawi/Documents/learning-projects/weather-app/node_modules/axios/lib/core/dispatchRequest.js:52:10)
    at <anonymous> config: [Object], response: [Object] } } ]

HawiCaesar avatar Feb 20 '18 07:02 HawiCaesar

Getting the same error as mentioned above

        mock = new MockAdapter(requestInstance)
        
        mock.onPost(`${SERVER_URL}/api/cart`).reply(200, 'cart')

        mock.onGet(`${SERVER_URL}/api/quoteDetails/product/`).reply(200, { data: {x: 2} })

djalmaaraujo avatar Feb 20 '18 23:02 djalmaaraujo

@djalmaaraujo What does your http request look like for POST and GET?

davidlewallen avatar Feb 20 '18 23:02 davidlewallen

@davidlewallen Not sure if I understood the question.

djalmaaraujo avatar Feb 20 '18 23:02 djalmaaraujo

@djalmaaraujo can you show me the code that is making the request to ${SERVER_URL}/api/car

davidlewallen avatar Feb 20 '18 23:02 davidlewallen

@davidlewallen

/**
 * Axios Request Instance (Move to config later)
 */
export const requestInstance = axios.create({
  baseURL: SERVER_URL,
  timeout: 20000,
  headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }
})
/**
 * Fetch QuoteDetails using ISBN
 */
export const buybackFetchQuoteDetails = (isbn) => {
  return (dispatch) => {
    if (!isbn) {
      return dispatch({
        type: 'BUYBACK/quoteDetailsFail',
        payload: 'Invalid ISBN, please try another ISBN.'
      })
    }

    dispatch({
      type: 'BUYBACK/loading'
    })

    return createOrReturnCart().then((cart) => {
      dispatch({
        type: 'BUYBACK/createOrReturnCart',
        payload: cart
      })

      return requestInstance.get(`api/quoteDetails/product/${isbn}`)
        .then((response) => {
          dispatch({
            type: 'BUYBACK/quoteDetailsSuccess',
            payload: response.data
          })
        })
        .catch((error) => {
          console.log('I ALWAYS GET CATCH HERE')
          dispatch({
            type: 'BUYBACK/quoteDetailsFail',
            payload: (error.response.data && error.response.data.error) ? error.response.data.error : 'Invalid ISBN, please try another ISBN.'
          })

          return error
        })
    })
  }
}
export const createOrReturnCart = () => {
  return requestInstance.post('api/cart')
    .then((response) => response.data)
    .catch(err => err)
}

Spec:

describe('async quote details', () => {
      let mock;
      let dispatchMockSpy;

      beforeEach(() => {
        mock = new MockAdapter(requestInstance)
        dispatchMockSpy = sinon.spy()

        mock
          .onPost(`${SERVER_URL}/api/cart`).reply(200, 'cart')
          .onGet(`${SERVER_URL}/api/quoteDetails/product/123456`).reply(200, 'quoteDetailsData')
      })

      afterEach(() => {
        mock.reset()
      })

      it("should dispatch data", () => {
        const buybackFetchQuoteDetailsMock = buybackFetchQuoteDetails('123456')
        buybackFetchQuoteDetailsMock(dispatchMockSpy).then((x) => {
          expect(dispatchMockSpy.calledWith({
            type: 'BUYBACK/loading'
          })).toBeTruthy()

          expect(dispatchMockSpy.calledWith({
            type: 'BUYBACK/createOrReturnCart',
            payload: 'cart'
          })).toBeTruthy()
        }).catch((x) => console.log(x))
      })
    })

djalmaaraujo avatar Feb 20 '18 23:02 djalmaaraujo

@djalmaaraujo the code you link has no GET request to ${SERVER_URL}/api/cart or a POST request to ${SERVER_URL}/api/quoteDetails/product/

davidlewallen avatar Feb 20 '18 23:02 davidlewallen

@davidlewallen I updated the code above, please take a look. Just added what is requestInstance. In the code above, the /api/cart works, but the get, does not.

djalmaaraujo avatar Feb 21 '18 00:02 djalmaaraujo

@djalmaaraujo I think its how you are using the base url. You can console.log(mock.handlers.get) to see if the right url is saved in the array.

I just mocked this up and it works just fine isolated from Redux.

  fit('it should work', () => {
    const SERVER_URL = '/test';
    const requestInstance = axios.create({
      baseURL: SERVER_URL,
      timeout: 2000,
      headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
    });

    const newMock = new MockAdapter(requestInstance);
    newMock.onGet('/api/quoteDetails/product/123456').reply(200, 'quoteDetailsData');

    requestInstance.get('api/quoteDetails/product/123456')
      .then(response => console.log('response', response))
      .catch(error => console.log('error', error));
  });

Response console.log

 response { status: 200,
      data: 'quoteDetailsData',
      headers: undefined,
      config:
       { adapter: null,
         transformRequest: { '0': [Function: transformRequest] },
         transformResponse: { '0': [Function: transformResponse] },
         timeout: 2000,
         xsrfCookieName: 'XSRF-TOKEN',
         xsrfHeaderName: 'X-XSRF-TOKEN',
         maxContentLength: -1,
         validateStatus: [Function: validateStatus],
         headers:
          { Accept: 'application/json',
            'Content-Type': 'application/json' },
         baseURL: '/test',
         method: 'get',
         url: '/api/quoteDetails/product/123456',
         data: undefined } }

davidlewallen avatar Feb 21 '18 00:02 davidlewallen

@davidlewallen But why the /api/cart works and the other don't?

djalmaaraujo avatar Feb 21 '18 00:02 djalmaaraujo

I just console.log requestInstance

● Console

    console.log src/Actions/BuyBackActions.test.js:49
      { [Function: wrap]
        request: [Function: wrap],
        delete: [Function: wrap],
        get: [Function: wrap],
        head: [Function: wrap],
        options: [Function: wrap],
        post: [Function: wrap],
        put: [Function: wrap],
        patch: [Function: wrap],
        defaults:
         { adapter: [Function: xhrAdapter],
           transformRequest: [ [Function: transformRequest] ],
           transformResponse: [ [Function: transformResponse] ],
           timeout: 20000,
           xsrfCookieName: 'XSRF-TOKEN',
           xsrfHeaderName: 'X-XSRF-TOKEN',
           maxContentLength: -1,
           validateStatus: [Function: validateStatus],
           headers:
            { common: [Object],
              delete: {},
              get: {},
              head: {},
              post: [Object],
              put: [Object],
              patch: [Object],
              Accept: 'application/json',
              'Content-Type': 'application/json' },
           baseURL: 'http://localhost:8080/buyback' },
        interceptors:
         { request: InterceptorManager { handlers: [] },
           response: InterceptorManager { handlers: [] } } }
    console.log src/Actions/BuyBackActions.js:46

djalmaaraujo avatar Feb 21 '18 00:02 djalmaaraujo

@djalmaaraujo add console.log(JSON.stringify(mock.handlers, null, 2)) inside of your beforeEach but after the mock calls and post that up

davidlewallen avatar Feb 21 '18 00:02 davidlewallen

@davidlewallen

  ● Console

    console.log src/Actions/BuyBackActions.test.js:59
      {
        "get": [
          [
            "http://localhost:8080/buyback/api/quoteDetails/product/123456",
            null,
            200,
            "quoteDetailsData",
            null
          ]
        ],
        "post": [
          [
            "http://localhost:8080/buyback/api/cart",
            null,
            200,
            "cart",
            null
          ]
        ],
        "head": [],
        "delete": [],
        "patch": [],
        "put": []
      }

I was using onAny, just fixed. see now

djalmaaraujo avatar Feb 21 '18 00:02 djalmaaraujo

@davidlewallen The error returned in the catch (in the src code), is this:

{ Error: Request failed with status code 404
          at createErrorResponse (/Users/cooper/dev/valore/valore-buyback-webapp/client/node_modules/axios-mock-adapter/src/utils.js:110:15)
          at Object.settle (/Users/cooper/dev/valore/valore-buyback-webapp/client/node_modules/axios-mock-adapter/src/utils.js:90:16)
          at handleRequest (/Users/cooper/dev/valore/valore-buyback-webapp/client/node_modules/axios-mock-adapter/src/handle_request.js:55:11)
          at /Users/cooper/dev/valore/valore-buyback-webapp/client/node_modules/axios-mock-adapter/src/index.js:16:9
          at new Promise (<anonymous>)
          at MockAdapter.<anonymous> (/Users/cooper/dev/valore/valore-buyback-webapp/client/node_modules/axios-mock-adapter/src/index.js:15:14)
          at dispatchRequest (/Users/cooper/dev/valore/valore-buyback-webapp/client/node_modules/axios/lib/core/dispatchRequest.js:59:10)
          at <anonymous>
        config:
         { adapter: null,
           transformRequest: { '0': [Function: transformRequest] },
           transformResponse: { '0': [Function: transformResponse] },
           timeout: 20000,
           xsrfCookieName: 'XSRF-TOKEN',
           xsrfHeaderName: 'X-XSRF-TOKEN',
           maxContentLength: -1,
           validateStatus: [Function: validateStatus],
           headers:
            { Accept: 'application/json',
              'Content-Type': 'application/json' },
           baseURL: 'http://localhost:8080/buyback',
           method: 'get',
           url: '/api/quoteDetails/product/123456',
           data: undefined },
        response:
         { status: 404,
           config:
            { adapter: null,
              transformRequest: [Object],
              transformResponse: [Object],
              timeout: 20000,
              xsrfCookieName: 'XSRF-TOKEN',
              xsrfHeaderName: 'X-XSRF-TOKEN',
              maxContentLength: -1,
              validateStatus: [Function: validateStatus],
              headers: [Object],
              baseURL: 'http://localhost:8080/buyback',
              method: 'get',
              url: '/api/quoteDetails/product/123456',
              data: undefined },
           data: undefined } }

djalmaaraujo avatar Feb 21 '18 00:02 djalmaaraujo

@djalmaaraujo try removing the ${SERVER_URL} and see if that helps?

davidlewallen avatar Feb 21 '18 00:02 davidlewallen

@davidlewallen Same thing. Still have the /api/cart working, but not the other request. The difference is that this request is inside another promise. maybe that's the reason?

djalmaaraujo avatar Feb 21 '18 00:02 djalmaaraujo

@djalmaaraujo hmm I am seeing something a little different on my end. Try adding this to createOrReturnCart function:

const createOrReturnCart = () => requestInstance.post('/api/cart')
      .then(response => response)
      .catch((error) => {
        console.log('error', error)
        return error;
      });

And see if you get an error there?

davidlewallen avatar Feb 21 '18 00:02 davidlewallen

@davidlewallen I changed a little bit the code to show you the test results:

/**
 * Fetch QuoteDetails using ISBN
 */
export const buybackFetchQuoteDetails = (isbn) => {
  return (dispatch) => {
    if (!isbn) {
      return dispatch({
        type: 'BUYBACK/quoteDetailsFail',
        payload: 'Invalid ISBN, please try another ISBN.'
      })
    }

    dispatch({
      type: 'BUYBACK/loading'
    })

    return createOrReturnCart().then((cart) => {
      console.log('CART PASSED')
      dispatch({
        type: 'BUYBACK/createOrReturnCart',
        payload: cart
      })

      return requestInstance.get(`api/quoteDetails/product/${isbn}`)
        .then((response) => {
          console.log('QUOTE DETAILS RESOLVED')
          dispatch({
            type: 'BUYBACK/quoteDetailsSuccess',
            payload: response.data
          })
        })
        .catch((error) => {
          console.log('ERROR IN QUOTE DETAILS')
          dispatch({
            type: 'BUYBACK/quoteDetailsFail',
            payload: (error.response.data && error.response.data.error) ? error.response.data.error : 'Invalid ISBN, please try another ISBN.'
          })

          return error
        })
    })
  }
}

const createOrReturnCart = () => requestInstance.post('/api/cart')
  .then(response => response)
  .catch((error) => {
    console.log('error', error)
    return error;
  })

SPEC:

 PASS  src/Containers/BuyBackContainer/BuyBackContainer.test.js
 PASS  src/Actions/BuyBackActions.test.js
  ● Console

    console.log src/Actions/BuyBackActions.js:32
      CART PASSED
    console.log src/Actions/BuyBackActions.js:47
      ERROR IN QUOTE DETAILS
    console.log src/Actions/BuyBackActions.test.js:81
      { Error: expect(received).toBeTruthy()

      Expected value to be truthy, instead received
        false
          at buybackFetchQuoteDetailsMock.then.x (/Users/cooper/dev/valore/valore-buyback-webapp/client/src/Actions/BuyBackActions.test.js:75:11)
          at <anonymous> matcherResult: { message: [Function], pass: false } }


Test Suites: 2 passed, 2 total
Tests:       6 passed, 6 total
Snapshots:   0 total
Time:        0.938s, estimated 1s
Ran all test suites related to changed files.

Watch Usage: Press w to show more.

djalmaaraujo avatar Feb 21 '18 00:02 djalmaaraujo

@djalmaaraujo One last thing before I give up. Make these changes

mock
  .onPost('/api/cart').reply(200, 'cart')
  .onGet(/api\/quoteDetails\/product\/.+/).reply(200, 'quoteDetailsData')

See if that give any better results

davidlewallen avatar Feb 21 '18 01:02 davidlewallen

@davidlewallen Sorry the delay, but still failing. I will give up, check for another alternative. Thanks for your time, honestly.

djalmaaraujo avatar Feb 21 '18 02:02 djalmaaraujo

@davidlewallen I was able to make it work. I split the method into 2 methods and tested separately. I can't tell you exactly what is the issue, but I believe it's a bug from this mock-adapter, when you have a nested promise level, it's like the mock is not aware of that request.

djalmaaraujo avatar Feb 21 '18 18:02 djalmaaraujo

@davidlewallen After another time facing the same issue, I think the solution is to pass (done) and wait for the correct ajax to finish.

Check this example, and it works:

it('should return cartItem data and dispatch BUYBACK/cartAddItem', (done) => {
      mock
        .onPost('/api/cart').reply(200, { id: 5 })
        .onPost('/api/cart/addItem', {cartId: 5, isbn: 123456}).reply(200, expectedResponse)

      buybackAddItemToCart(5, {productCode: '123456'}, dispatchMockSpy)
        .then(() => {
          expect(dispatchMockSpy.calledWith({
            type: 'BUYBACK/cartLoading'
          })).toBeTruthy()

          expect(dispatchMockSpy.args[1][0].payload).toEqual({
            cartItem: expectedResponse,
            quoteDetails: { productCode: '123456' }
          })

          done()
        })
        .catch(err => {
          done.fail('Should not call catch')
          console.log(err)
        })
    })

In this case, I am always getting 404 for the /api/cart... but with the done, now it works.

djalmaaraujo avatar Mar 13 '18 19:03 djalmaaraujo

Hi, Sorry for reviving the thread but because its still open it could make sense to reuse it as I'm facing the same issues described by @kirankbs, @HawiCaesar and @djalmaaraujo.

I'm not able to get it to work even with @djalmaaraujo's solution using the done callback.

What could be missing here?

Dependencies: [email protected] [email protected]

The method under test:

export const createResource = field => {
  return axios
    .post(
      "http://app.io/api/resource",
      JSON.stringify({
        field
      })
    );
};

The test:

it("should create a resource", () => {
  const fieldValue = "field value", 
    resourceName = "resource name";

  new MockAdapter(axios)
    .onPost("http://app.io/api/resource", { field: fieldValue })
    .reply(200, { field: fieldValue, name: resourceName});

  return createResource(field).then(({ field, name }) => {
    expect(field).toBe(fieldvalue);
    expect(name).toBe(resourceName);
  }).catch(error => {
    console.error(error);
    return error;
  });
});

The error:

{ Error: Request failed with status code 404
  at createErrorResponse (/home/pmgmendes/repo/node_modules/axios-mock-adapter/src/utils.js:117:15)
  at Object.settle (/home/pmgmendes/repo/node_modules/axios-mock-adapter/src/utils.js:97:16)
  at handleRequest (/home/pmgmendes/repo/node_modules/axios-mock-adapter/src/handle_request.js:73:11)
  at /home/pmgmendes/repo/node_modules/axios-mock-adapter/src/index.js:18:9
  at new Promise (<anonymous>)
  at MockAdapter.<anonymous> (/home/pmgmendes/repo/node_modules/axios-mock-adapter/src/index.js:17:14)
  at dispatchRequest (/home/pmgmendes/repo/node_modules/axios/lib/core/dispatchRequest.js:52:10)
  at <anonymous>
  at process._tickCallback (internal/process/next_tick.js:118:7)
config: 
  { transformRequest: { '0': [Function: transformRequest] },
    transformResponse: { '0': [Function: transformResponse] },
    timeout: 0,
    xsrfCookieName: 'XSRF-TOKEN',
    xsrfHeaderName: 'X-XSRF-TOKEN',
    maxContentLength: -1,
    validateStatus: [Function: validateStatus],
    headers: 
    { Accept: 'application/json, text/plain, */*',
      'Content-Type': 'application/x-www-form-urlencoded' },
    method: 'post',
    url: 'http://app.io/api/resource',
    data: '{"field":"field value"}' 
  },
response: 
  { status: 404,
    config: 
    { transformRequest: [Object],
      transformResponse: [Object],
      timeout: 0,
      xsrfCookieName: 'XSRF-TOKEN',
      xsrfHeaderName: 'X-XSRF-TOKEN',
      maxContentLength: -1,
      validateStatus: [Function: validateStatus],
      headers: [Object],
      method: 'post',
      url: 'http://app.io/api/resource',
      data: '{"field":"field value"}' 
    },
    data: undefined 
  } 
}

pmgmendes avatar May 04 '18 12:05 pmgmendes

+1

I'm using axios in a node service layer that utilizes axios.create to put a common baseUrl in all axios requests. The mock-adapter seems to failing to work in my case as well.

william-bratches avatar May 08 '18 16:05 william-bratches

Getting exactly the same issue with a nested promise.

mdiflorio avatar May 21 '18 08:05 mdiflorio

After debugging session focused on utils.js#findHandler() I came to conclusion that the generated POST body didn't match the expected payload provided to the axios mock thus causing the 404.

The functions used within findHandler method would benefit immensely by having some verbose logging. It would be much easier to understand what's the real reason behind a possible 404.

pmgmendes avatar May 21 '18 22:05 pmgmendes

What version of node are people using? I am also facing the same issue and I believe I have chased it down to Utils.findHandler.

My environment:

> process.versions
{ ...
  node: '8.9.4',
  v8: '6.1.534.50',
  ... }

To demonstrate the issue, go into your node_modules and update your utils.js to the following:

function find(array, predicate) {
  var length = array.length;
  for (var i = 0; i < length; i++) {
    var value = array[i];
    const pred = predicate(value);
    if (pred) return value;
    else console.log(`Failed to match value: ${pred}`);
  }
}

// ...

function isBodyOrParametersMatching(method, body, parameters, required) {
  console.log('isBodyOrParametersMatching');
// ...

What you'll see is that the predicate fails to match with a log statement of: Failed to match value: undefined Further, you will not see isBodyOrParametersMatching printed in the logs either.

My theory is that there is a bug in the way V8 handles [arbitrarily] complex bitwise statements. I've seen this behavior (where it returns undefined instead of a boolean) in another of our internal projects (completely unrelated to axios-mock-adapter).

Once you confirmed the behavior above, try replacing the findHandler method with the following:

function findHandler(handlers, method, url, body, parameters, headers, baseURL) {
  console.log(arguments);
  console.log(handlers);
  return find(handlers[method.toLowerCase()], function(handler) {
    let urlMatch = false;
    if (typeof handler[0] === 'string') {
      urlMatch = isUrlMatching(url, handler[0]) || isUrlMatching(combineUrls(baseURL, url), handler[0]);
    } else if (handler[0] instanceof RegExp) {
      urlMatch = handler[0].test(url) || handler[0].test(combineUrls(baseURL, url));
    }
    const bodyParamMatch = urlMatch && isBodyOrParametersMatching(method, body, parameters, handler[1]);
    const headersMatch = bodyParamMatch && isRequestHeadersMatching(headers, handler[2]);
    return headersMatch
  });
}

You'll notice that isBodyOrParametersMatching is printed in the logs as expected and the predicate matches successfully.

My theory is that there is a bug in the way the V8 runtime parse boolean statements. Specifically, when the V8 runtime parses a bitwise boolean statement where there is a parentheses group followed by a method invocation. For whatever reason, I believe V8 falls on its face and returns undefined where that value should otherwise be unattainable.

As I said earlier, I've seen this in an internal ReactJS app (which was not using axios) running in the latest Chrome browser. I'll see if I can put together a sample app to test my theory.

I'll also put together a PR to solve this issue (with the updated findHandler method). Though it will have to wait until I get home tonight as we are unable to push anything to github from our office 😢

PS.- If someone else wants to take the code above and create a PR before I do, please feel free. (Just make sure to reference this issue)

jd-carroll avatar Jul 17 '18 14:07 jd-carroll

Note: An issue has been opened within V8 https://bugs.chromium.org/p/v8/issues/detail?id=7949

jd-carroll avatar Jul 17 '18 14:07 jd-carroll

I get the 404 randomly! If I chain three onGetmethods it works, next time I get 404 then unchain the methods and it works. Repat to infinite.

I'm lost. It is as it were working when HMR but not working on refresh (I am working with storybook btw)

See this video http://recordit.co/Hj45OY8yjB

beliolfa avatar Jul 19 '18 06:07 beliolfa