Back to Repositories

Testing Swipe Gesture Implementation in Bootstrap

This test suite validates the Swipe utility functionality in Bootstrap, covering touch and pointer event handling for swipe gestures. It ensures proper swipe detection, callback execution, and event management across different browser environments.

Test Coverage Overview

The test suite provides comprehensive coverage of the Swipe utility’s core functionality.

Key areas tested include:
  • Touch and pointer event handling
  • Left and right swipe gesture detection
  • Callback execution for swipe directions
  • Device compatibility and support detection
  • Edge cases like multi-touch prevention

Implementation Analysis

The testing approach utilizes Jest’s describe/it blocks with async promise-based tests for gesture handling.

Notable patterns include:
  • Mock gesture simulation for touch/pointer events
  • Environment condition testing with browser feature detection
  • Spy implementation for event handler verification
  • Cleanup and fixture management between tests

Technical Details

Testing infrastructure includes:
  • Jest test framework
  • Custom Simulator utility for gesture mocking
  • EventHandler for DOM event management
  • Fixture helpers for DOM manipulation
  • Jasmine spy functionality for method tracking

Best Practices Demonstrated

The test suite exemplifies high-quality testing practices through thorough setup and teardown procedures.

Notable practices include:
  • Isolated test environments with fixture cleanup
  • Comprehensive feature detection handling
  • Cross-browser compatibility testing
  • Proper async test management
  • Clear test organization and grouping

twbs/bootstrap

js/tests/unit/util/swipe.spec.js

            
import EventHandler from '../../../src/dom/event-handler.js'
import { noop } from '../../../src/util/index.js'
import Swipe from '../../../src/util/swipe.js'
import { clearFixture, getFixture } from '../../helpers/fixture.js'

describe('Swipe', () => {
  const { Simulator, PointerEvent } = window
  const originWinPointerEvent = PointerEvent
  const supportPointerEvent = Boolean(PointerEvent)

  let fixtureEl
  let swipeEl
  const clearPointerEvents = () => {
    window.PointerEvent = null
  }

  const restorePointerEvents = () => {
    window.PointerEvent = originWinPointerEvent
  }

  // The headless browser does not support touch events, so we need to fake it
  // in order to test that touch events are added properly
  const defineDocumentElementOntouchstart = () => {
    document.documentElement.ontouchstart = noop
  }

  const deleteDocumentElementOntouchstart = () => {
    delete document.documentElement.ontouchstart
  }

  const mockSwipeGesture = (element, options = {}, type = 'touch') => {
    Simulator.setType(type)
    const _options = { deltaX: 0, deltaY: 0, ...options }

    Simulator.gestures.swipe(element, _options)
  }

  beforeEach(() => {
    fixtureEl = getFixture()
    const cssStyle = [
      '<style>',
      '  #fixture .pointer-event {',
      '    touch-action: pan-y;',
      '  }',
      '  #fixture div {',
      '    width: 300px;',
      '    height: 300px;',
      '  }',
      '</style>'
    ].join('')

    fixtureEl.innerHTML = `<div id="swipeEl"></div>${cssStyle}`
    swipeEl = fixtureEl.querySelector('div')
  })

  afterEach(() => {
    clearFixture()
    deleteDocumentElementOntouchstart()
  })

  describe('constructor', () => {
    it('should add touch event listeners by default', () => {
      defineDocumentElementOntouchstart()

      spyOn(Swipe.prototype, '_initEvents').and.callThrough()
      const swipe = new Swipe(swipeEl)
      expect(swipe._initEvents).toHaveBeenCalled()
    })

    it('should not add touch event listeners if touch is not supported', () => {
      spyOn(Swipe, 'isSupported').and.returnValue(false)

      spyOn(Swipe.prototype, '_initEvents').and.callThrough()
      const swipe = new Swipe(swipeEl)

      expect(swipe._initEvents).not.toHaveBeenCalled()
    })
  })

  describe('Config', () => {
    it('Test leftCallback', () => {
      return new Promise(resolve => {
        const spyRight = jasmine.createSpy('spy')
        clearPointerEvents()
        defineDocumentElementOntouchstart()
        // eslint-disable-next-line no-new
        new Swipe(swipeEl, {
          leftCallback() {
            expect(spyRight).not.toHaveBeenCalled()
            restorePointerEvents()
            resolve()
          },
          rightCallback: spyRight
        })

        mockSwipeGesture(swipeEl, {
          pos: [300, 10],
          deltaX: -300
        })
      })
    })

    it('Test rightCallback', () => {
      return new Promise(resolve => {
        const spyLeft = jasmine.createSpy('spy')
        clearPointerEvents()
        defineDocumentElementOntouchstart()
        // eslint-disable-next-line no-new
        new Swipe(swipeEl, {
          rightCallback() {
            expect(spyLeft).not.toHaveBeenCalled()
            restorePointerEvents()
            resolve()
          },
          leftCallback: spyLeft
        })

        mockSwipeGesture(swipeEl, {
          pos: [10, 10],
          deltaX: 300
        })
      })
    })

    it('Test endCallback', () => {
      return new Promise(resolve => {
        clearPointerEvents()
        defineDocumentElementOntouchstart()
        let isFirstTime = true

        const callback = () => {
          if (isFirstTime) {
            isFirstTime = false
            return
          }

          expect().nothing()
          restorePointerEvents()
          resolve()
        }

        // eslint-disable-next-line no-new
        new Swipe(swipeEl, {
          endCallback: callback
        })
        mockSwipeGesture(swipeEl, {
          pos: [10, 10],
          deltaX: 300
        })

        mockSwipeGesture(swipeEl, {
          pos: [300, 10],
          deltaX: -300
        })
      })
    })
  })

  describe('Functionality on PointerEvents', () => {
    it('should not allow pinch with touch events', () => {
      Simulator.setType('touch')
      clearPointerEvents()
      deleteDocumentElementOntouchstart()

      const swipe = new Swipe(swipeEl)
      const spy = spyOn(swipe, '_handleSwipe')

      mockSwipeGesture(swipeEl, {
        pos: [300, 10],
        deltaX: -300,
        deltaY: 0,
        touches: 2
      })

      restorePointerEvents()
      expect(spy).not.toHaveBeenCalled()
    })

    it('should allow swipeRight and call "rightCallback" with pointer events', () => {
      return new Promise(resolve => {
        if (!supportPointerEvent) {
          expect().nothing()
          resolve()
          return
        }

        const style = '#fixture .pointer-event { touch-action: none !important; }'
        fixtureEl.innerHTML += style

        defineDocumentElementOntouchstart()
        // eslint-disable-next-line no-new
        new Swipe(swipeEl, {
          rightCallback() {
            deleteDocumentElementOntouchstart()
            expect().nothing()
            resolve()
          }
        })

        mockSwipeGesture(swipeEl, { deltaX: 300 }, 'pointer')
      })
    })

    it('should allow swipeLeft and call "leftCallback" with pointer events', () => {
      return new Promise(resolve => {
        if (!supportPointerEvent) {
          expect().nothing()
          resolve()
          return
        }

        const style = '#fixture .pointer-event { touch-action: none !important; }'
        fixtureEl.innerHTML += style

        defineDocumentElementOntouchstart()
        // eslint-disable-next-line no-new
        new Swipe(swipeEl, {
          leftCallback() {
            expect().nothing()
            deleteDocumentElementOntouchstart()
            resolve()
          }
        })

        mockSwipeGesture(swipeEl, {
          pos: [300, 10],
          deltaX: -300
        }, 'pointer')
      })
    })
  })

  describe('Dispose', () => {
    it('should call EventHandler.off', () => {
      defineDocumentElementOntouchstart()
      spyOn(EventHandler, 'off').and.callThrough()
      const swipe = new Swipe(swipeEl)

      swipe.dispose()
      expect(EventHandler.off).toHaveBeenCalledWith(swipeEl, '.bs.swipe')
    })

    it('should destroy', () => {
      const addEventSpy = spyOn(fixtureEl, 'addEventListener').and.callThrough()
      const removeEventSpy = spyOn(EventHandler, 'off').and.callThrough()
      defineDocumentElementOntouchstart()

      const swipe = new Swipe(fixtureEl)

      const expectedArgs =
        swipe._supportPointerEvents ?
          [
            ['pointerdown', jasmine.any(Function), jasmine.any(Boolean)],
            ['pointerup', jasmine.any(Function), jasmine.any(Boolean)]
          ] :
          [
            ['touchstart', jasmine.any(Function), jasmine.any(Boolean)],
            ['touchmove', jasmine.any(Function), jasmine.any(Boolean)],
            ['touchend', jasmine.any(Function), jasmine.any(Boolean)]
          ]

      expect(addEventSpy.calls.allArgs()).toEqual(expectedArgs)

      swipe.dispose()

      expect(removeEventSpy).toHaveBeenCalledWith(fixtureEl, '.bs.swipe')
      deleteDocumentElementOntouchstart()
    })
  })

  describe('"isSupported" static', () => {
    it('should return "true" if "touchstart" exists in document element)', () => {
      Object.defineProperty(window.navigator, 'maxTouchPoints', () => 0)
      defineDocumentElementOntouchstart()

      expect(Swipe.isSupported()).toBeTrue()
    })

    it('should return "false" if "touchstart" not exists in document element and "navigator.maxTouchPoints" are zero (0)', () => {
      Object.defineProperty(window.navigator, 'maxTouchPoints', () => 0)
      deleteDocumentElementOntouchstart()

      if ('ontouchstart' in document.documentElement) {
        expect().nothing()
        return
      }

      expect(Swipe.isSupported()).toBeFalse()
    })
  })
})