mui-places-autocomplete
Version:
Material-UI React component that provides suggestions/autocompletes places using the Google Places API
362 lines (289 loc) • 16.1 kB
JSX
// Disable 'prefer-arrow-callback' as Mocha recommends against passing arrow functions to Mocha. See
// https://mochajs.org/#arrow-functions for more info.
// Disable 'func-names' so we can use anonymous functions as a convenience to us as test writers.
/* eslint prefer-arrow-callback: 0, func-names: 0 */
/* eslint no-unused-expressions: 0, chai-friendly/no-unused-expressions: 2 */
// 3rd-party supporting test libs
import chai, { expect } from 'chai'
import chaiJestSnapshot from 'chai-jest-snapshot'
import Enzyme, { mount } from 'enzyme'
import Adapter from 'enzyme-adapter-react-16'
import toJson from 'enzyme-to-json'
import React from 'react'
// Code under test
import MUIPlacesAutocomplete from './../src'
// Supporting test code
import { getACServiceClassDef } from './testHelper'
// Configure Chai to work with Jest
chai.use(chaiJestSnapshot)
describe('React component test: <MUIPlacesAutocomplete>', function () {
// Common search value that can be used throughout the tests
const searchInputValue = 'Bellingham'
// Wrapper providing access to DOM APIs/full DOM rendering for the <MUIPlacesAutocomplete>
// component that will be under test
let mpaWrapper = null
before('Configure Enzyme', function () {
Enzyme.configure({ adapter: new Adapter() })
})
before('Reset Jest snapshot registry for Chai usage', function () {
chaiJestSnapshot.resetSnapshotRegistry()
})
beforeEach('Configure chai-jest-snapshot for "Mocha configuration mode"', function () {
chaiJestSnapshot.configureUsingMochaContext(this)
})
before('"Load" the Google Maps JavaScript API on \'window\'', function () {
// These tests depend on a DOM to be setup for more indepth tests that either do full DOM
// rendering or for leveraging DOM APIs. At this point we presume that the DOM has been setup
// and we can add additional properties to the global 'window' object.
expect(global.window).to.exist
// The <MUIPlacesAutocomplete> component expects the Google Maps JavaScript API to be loaded
// in the 'window' object. Since we aren't loading it we mock out the API and add it to the
// 'window' object.
global.window.google = {
maps: {
places: {
// We disable the ESLint rule 'class-methods-use-this' on the next line as our component
// depends on the method being available to an instance of the AutocompleteService
// class (i.e. we can't convert it into a static).
// eslint-disable-next-line class-methods-use-this
AutocompleteService: class AutocompleteService { getPlacePredictions() { } },
// Don't forget to mock out the status' that are used by the <MUIPlacesAutocomplete>
// component
PlacesServiceStatus: {
OK: 'OK',
},
},
},
}
})
beforeEach('Setup Enzyme wrapper', function () {
mpaWrapper = mount(
<MUIPlacesAutocomplete onSuggestionSelected={() => {}} renderTarget={() => (<div />)} />,
)
expect(mpaWrapper).to.not.be.null
expect(mpaWrapper.length).to.equal(1)
})
describe('Renders correctly for given application state:', function () {
// Helper function to get the JSON of the <MenuList> component in our <MUIPlacesAutocomplete>
// component. Useful for snapshot testing only the components that we are in charge of
// rendering (i.e. the suggestions).
const getMenuListJSON = () => {
expect(mpaWrapper).to.not.be.null
const mlWrapper = mpaWrapper.find('MenuList')
// Make sure we have a wrapper around a <MenuList> and only a single <MenuList>
expect(mlWrapper).to.not.be.null
expect(mlWrapper.length).to.equal(1)
return toJson(mlWrapper)
}
it('Initial state', function () {
expect(mpaWrapper.state().suggestions).to.exist
expect(mpaWrapper.state().suggestions).to.be.empty
// Protect us from forgetting to update this test if we add an additional key(s) to the
// components state. We currently only expect the 'suggestions' key...
expect(Object.keys(mpaWrapper.state()).length).to.equal(1)
// We don't snapshot test our component since 1) the <Downshift>/<Popper> components that our
// <MUIPlacesAutocomplete> component composes is massive and takes long to diff the
// serializations and 2) our container ought not be open anyway. We can verify that the
// suggestions container isn't open by searching for the <MenuList> component.
expect(mpaWrapper.find('MenuList').length).to.equal(0)
})
it('Suggestions rendered from \'suggestions\' state', function () {
// To get suggestions to be rendered first simulate an input onChange event which will cause
// <Downshift> to believe that our autocomplete/dropdown is open...
mpaWrapper.find('input').simulate('change', { target: { value: searchInputValue } })
// Second set the state of our component to provide suggestions as if they were returned from
// the Google AutocompleteService...
const expectedSuggestion = { description: 'Bellingham, WA, United States' }
const expectedSuggestionCount = 1
mpaWrapper.setState({ suggestions: [expectedSuggestion] })
// Now check that our suggestions are rendered...
expect(mpaWrapper.find('MenuItem').length).to.equal(expectedSuggestionCount)
expect(mpaWrapper.find('MenuItem').text()).to.equal(expectedSuggestion.description)
// Snapshot test only the <MenuList> of our suggestions as the <Downshift>/<Popper> components
// that our <MUIPlacesAutocomplete> component composes is massive and takes to long to diff
// the serializations.
expect(getMenuListJSON()).to.matchSnapshot()
})
it('Empty input renders no suggestions after previous ones rendered', function () {
// To get suggestions to be rendered first simulate an input onChange event which will cause
// <Downshift> to believe that our autocomplete/dropdown is open...
mpaWrapper.find('input').simulate('change', { target: { value: searchInputValue } })
// Second set the start of our component to provide suggestions as if they were returned from
// the Google AutocompleteService...
const expectedSuggestion = { description: 'Bellingham, WA, United States' }
let expectedSuggestionCount = 1
mpaWrapper.setState({ suggestions: [expectedSuggestion] })
// Now check that our suggestions are rendered...
expect(mpaWrapper.find('MenuItem').length).to.equal(expectedSuggestionCount)
expect(mpaWrapper.find('MenuItem').text()).to.equal(expectedSuggestion.description)
// Now clear the input and check that no suggestions are rendered...
mpaWrapper.find('input').simulate('change', { target: { value: '' } })
expectedSuggestionCount = 0
expect(mpaWrapper.find('MenuItem').length).to.equal(expectedSuggestionCount)
// We don't snapshot test since our suggestions container ought not be open. We can verify it
// isn't by searching for the <MenuList> component
expect(mpaWrapper.find('MenuList').length).to.equal(0)
})
it('Duplicate suggestions aren\'t rendered', function () {
// To get suggestions to be rendered first simulate an input onChange event which will cause
// <Downshift> to believe that our autocomplete/dropdown is open...
mpaWrapper.find('input').simulate('change', { target: { value: searchInputValue } })
// Second set the state of our component to provide duplicate suggestions as if they were
// returned from the Google AutocompleteService...
const expectedSuggestion = { description: 'Bellingham, WA, United States' }
const expectedSuggestionCount = 1
mpaWrapper.setState({ suggestions: [expectedSuggestion, expectedSuggestion] })
// Now check that our suggestions are uniquely rendered...
expect(mpaWrapper.find('MenuItem').length).to.equal(expectedSuggestionCount)
expect(mpaWrapper.find('MenuItem').text()).to.equal(expectedSuggestion.description)
// Snapshot test only the <MenuList> of our suggestions as the <Downshift>/<Popper> components
// that our <MUIPlacesAutocomplete> component composes is massive and takes to long to diff
// the serializations.
expect(getMenuListJSON()).to.matchSnapshot()
})
it('Google logo is present in a populated list of suggestions', function () {
// To get suggestions to be rendered first simulate an input onChange event which will cause
// <Downshift> to believe that our autocomplete/dropdown is open...
mpaWrapper.find('input').simulate('change', { target: { value: searchInputValue } })
// Second set the start of our component to provide suggestions as if they were returned from
// the Google AutocompleteService...
mpaWrapper.setState({ suggestions: [{ description: 'Bellingham, WA, United States' }] })
// Now check that our image is rendered...
expect(mpaWrapper.find('img').length).to.equal(1)
// Snapshot test only the <MenuList> of our suggestions as the <Downshift>/<Popper> components
// that our <MUIPlacesAutocomplete> component composes is massive and takes to long to diff
// the serializations.
expect(getMenuListJSON()).to.matchSnapshot()
})
})
describe('Provides expected UX:', function () {
it('\'textFieldProps.value\' prop can be used to control <input>', function () {
const controlValue = 'LOL Bananas'
mpaWrapper.setProps({ textFieldProps: { value: controlValue } })
expect(mpaWrapper.find(`input[value="${controlValue}"]`).length).to.equal(1)
})
it('\'textFieldProps.id\' prop can be used to set \'id\' on the <input> element', function () {
const idValue = 'lol-bananas'
// When testing if the 'id' value gets set properly on the resulting <input> element you must
// set the 'id' input prop when first mounting <MUIPlacesAutocomplete>. That is because the
// first time the <Downshift> element is mounted it sets and maintains the 'id' value
// throughout re-renderings as seen here:
// https://github.com/paypal/downshift/blob/118a87234a9331e716142acfb95eb411cc4f8015/src/downshift.js#L623
//
// This fact is important as the 'id' value is the last value set on the props returned from
// the 'getInputProps()' function as seen here:
// https://github.com/paypal/downshift/blob/118a87234a9331e716142acfb95eb411cc4f8015/src/downshift.js#L661
//
// In other words setting the props on an already mounted <MUIPlacesAutocomplete> won't change
// the value of 'id' and will result in the test always failing.
mpaWrapper = mount(
<MUIPlacesAutocomplete
onSuggestionSelected={() => {}}
renderTarget={() => (<div />)}
textFieldProps={{ id: idValue }}
/>,
)
expect(mpaWrapper).to.not.be.null
expect(mpaWrapper.length).to.equal(1)
expect(mpaWrapper.find(`input[id="${idValue}"]`).length).to.equal(1)
})
it('\'textFieldProps.onChange\' prop invoked when input changed', function (done) {
// Setup our wrapper to signal that our test has completed successfully
const testSuccessCB = (inputValue) => {
try {
expect(inputValue).to.be.an('object')
expect(inputValue.target).to.exist
expect(inputValue.target.value).to.exist
expect(inputValue.target.value).to.equal(searchInputValue)
} catch (e) {
done(e)
return
}
done()
}
mpaWrapper.setProps({ textFieldProps: { onChange: testSuccessCB } })
// Signal to <MUIPlacesAutocomplete> we would like to be called back when the input changes
mpaWrapper.find('input').simulate('change', { target: { value: searchInputValue } })
})
it('\'onSuggestionSelected\' invoked when suggestion selected', function (done) {
// Setup our wrapper to signal that our test has completed successfully
const testSuccessCB = (suggestion) => {
try {
expect(suggestion).to.exist
} catch (e) {
done(e)
}
done()
}
mpaWrapper.setProps({ onSuggestionSelected: testSuccessCB })
// To get suggestions to be rendered first simulate an input onChange event which will cause
// <Downshift> to believe that our autocomplete/dropdown is open...
mpaWrapper.find('input').simulate('change', { target: { value: searchInputValue } })
// Second set the start of our component to provide suggestions as if they were returned from
// the Google AutocompleteService...
mpaWrapper.setState({ suggestions: [{ description: 'Bellingham, WA, United States' }] })
// Now simulate a click on a rendered suggestion which ought to signal our success callback
const miWrapper = mpaWrapper.find('MenuItem')
expect(miWrapper.length).to.equal(1)
miWrapper.simulate('click')
})
it('Popper has default z-index of 1', function () {
// To get suggestions to be rendered first simulate an input onChange event which will cause
// <Downshift> to believe that our autocomplete/dropdown is open...
mpaWrapper.find('input').simulate('change', { target: { value: searchInputValue } })
// Second set the start of our component to provide suggestions as if they were returned from
// the Google AutocompleteService...
mpaWrapper.setState({ suggestions: [{ description: 'Bellingham, WA, United States' }] })
const pWrapper = mpaWrapper.find('Popper')
expect(pWrapper.exists()).to.be.true
const styleProps = pWrapper.prop('style')
expect(styleProps).to.exist
expect(styleProps.zIndex).to.exist
expect(styleProps.zIndex).to.be.equal(1)
})
})
describe('Consumes Google Maps JavaScript API correctly:', function () {
it('AutocompleteService.getPlacePredictions() returns predictions for given input', function (done) {
// Find the 'input' element so we can simulate an event for changing the input which will
// cause our component to get place predictions with the Google Maps API and ultimately update
// its state with place suggestions. Before doing so setup our test callback so we can assert
// that suggestions have been populated.
const testCallback = () => {
try {
expect(mpaWrapper.state().suggestions).to.not.be.empty
} catch (e) {
done(e)
return
}
done()
}
// This test depends on a DOM to be setup. At this point we presume that the DOM has been
// setup and we can add additional properties to the global 'window' object.
expect(global.window).to.exist
// The <MUIPlacesAutocomplete> component expects the Google Maps JavaScript API to be loaded
// in the 'window' object. Use the mock API test helper that actually wraps the Google Maps
// JavaScript API and add it to the 'window' object. We setup our DOM here rather than in a
// 'before' Mocha hook as we want to use the mock API with our specific test callback.
global.window.google = {
maps: {
places: {
AutocompleteService: getACServiceClassDef(testCallback),
// Don't forget to mock out the status' that are used by the <MUIPlacesAutocomplete>
// component
PlacesServiceStatus: {
OK: 'OK',
},
},
},
}
// Now re-mount our component to make use of our mock API when we send a 'change' event on our
// 'input' element
mpaWrapper = mount(
<MUIPlacesAutocomplete onSuggestionSelected={() => {}} renderTarget={() => (<div />)} />,
)
const inputWrapper = mpaWrapper.find('input')
inputWrapper.simulate('focus')
inputWrapper.simulate('change', { target: { value: searchInputValue } })
})
})
})