diff --git a/jest.config.js b/jest.config.js index 3443155c..86d3666d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -5,6 +5,9 @@ module.exports = { coverageDirectory: 'coverage', restoreMocks: true, moduleFileExtensions: ['js', 'jsx', 'json', 'vue'], + setupFiles: [ + '/tests/setup.js' + ], transform: { '^.+\\.vue$': 'vue-jest', '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': @@ -12,6 +15,7 @@ module.exports = { '^.+\\.jsx?$': 'babel-jest' }, moduleNameMapper: { + '^@/tests/(.*)$': '/tests/$1', '^@/(.*)$': '/src/$1' }, snapshotSerializers: ['jest-serializer-vue'], diff --git a/package.json b/package.json index 7b0fd726..dc7d9b53 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "@babel/plugin-transform-runtime": "^7.3.4", "@babel/preset-env": "^7.3.1", "@babel/runtime": "^7.3.4", - "@vue/test-utils": "^1.0.0-beta.20", + "@vue/test-utils": "^1.0.0-beta.29", "babel-eslint": "^8.2.6", "babel-jest": "^24.1.0", "coveralls": "^3.0.3", diff --git a/src/components/LCircle.vue b/src/components/LCircle.vue index 35cd7fcd..6178ebe4 100644 --- a/src/components/LCircle.vue +++ b/src/components/LCircle.vue @@ -15,7 +15,7 @@ export default { props: { latLng: { type: [Object, Array], - default: () => [] + default: () => [0, 0] } }, data () { diff --git a/src/components/LCircleMarker.vue b/src/components/LCircleMarker.vue index 1a68084b..a3f34c89 100644 --- a/src/components/LCircleMarker.vue +++ b/src/components/LCircleMarker.vue @@ -15,7 +15,7 @@ export default { props: { latLng: { type: [Object, Array], - default: () => [] + default: () => [0, 0] }, pane: { type: String, diff --git a/src/utils/utils.js b/src/utils/utils.js index 157e54f6..673aa5af 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -16,6 +16,7 @@ export const debounce = (fn, time) => { }; export const capitalizeFirstLetter = (string) => { + if (!string || typeof string.charAt !== 'function') { return string; } return string.charAt(0).toUpperCase() + string.slice(1); }; @@ -78,7 +79,7 @@ export const optionsMerger = (props, instance) => { export const findRealParent = (firstVueParent) => { let found = false; - while (!found) { + while (firstVueParent && !found) { if (firstVueParent.mapObject === undefined) { firstVueParent = firstVueParent.$parent; } else { diff --git a/tests/mixin-tests/circle-tests.js b/tests/mixin-tests/circle-tests.js new file mode 100644 index 00000000..87403b06 --- /dev/null +++ b/tests/mixin-tests/circle-tests.js @@ -0,0 +1,32 @@ +import { getWrapperWithMap } from '@/tests/test-helpers'; +import { testPathFunctionality } from './path-tests'; + +export const testCircleFunctionality = (component, componentName = 'it') => { + // The Circle mixin includes the Path mixin. + testPathFunctionality(component, componentName); + + // Radius + + test(`${componentName} has a getRadius method`, () => { + const { wrapper } = getWrapperWithMap(component); + + expect(typeof wrapper.vm.mapObject.getRadius).toEqual('function'); + }); + + test(`${componentName} accepts and uses radius prop`, () => { + const radius = 100; + const { wrapper } = getWrapperWithMap(component, { radius }); + + expect(wrapper.vm.mapObject.getRadius()).toEqual(radius); + }); + + test(`${componentName} updates radius when prop changes`, async () => { + const { wrapper } = getWrapperWithMap(component, { radius: 100 }); + + const newRadius = 2000; + wrapper.setProps({ radius: newRadius }); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.getRadius()).toEqual(newRadius); + }); +}; diff --git a/tests/mixin-tests/path-tests.js b/tests/mixin-tests/path-tests.js new file mode 100644 index 00000000..1ab5b7f0 --- /dev/null +++ b/tests/mixin-tests/path-tests.js @@ -0,0 +1,443 @@ +import { getWrapperWithMap } from '@/tests/test-helpers'; + +export const testPathFunctionality = (component, componentName = 'it') => { + // + // Stroke + + test(`${componentName} accepts and uses stroke prop when true`, () => { + const { wrapper } = getWrapperWithMap(component, { stroke: true }); + + expect(wrapper.vm.mapObject.options.stroke).toBeTruthy(); + }); + + test(`${componentName} accepts and uses stroke prop when false`, () => { + const { wrapper } = getWrapperWithMap(component, { stroke: false }); + + expect(wrapper.vm.mapObject.options.stroke).toBeFalsy(); + }); + + test(`${componentName} updates stroke when prop changes to false`, async () => { + const { wrapper } = getWrapperWithMap(component, { stroke: true }); + + wrapper.setProps({ stroke: false }); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.options.stroke).toBeFalsy(); + }); + + test(`${componentName} updates stroke when prop changes to true`, async () => { + const { wrapper } = getWrapperWithMap(component, { stroke: false }); + + wrapper.setProps({ stroke: true }); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.options.stroke).toBeTruthy(); + }); + + test(`${componentName} updates stroke to false using setStroke`, async () => { + const { wrapper } = getWrapperWithMap(component, { stroke: true }); + + wrapper.vm.setStroke(false); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.options.stroke).toBeFalsy(); + }); + + test(`${componentName} updates stroke to true using setStroke`, async () => { + const { wrapper } = getWrapperWithMap(component, { stroke: false }); + + wrapper.vm.setStroke(true); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.options.stroke).toBeTruthy(); + }); + + // Color + + test(`${componentName} accepts and uses color prop`, () => { + const color = '#314159'; + const { wrapper } = getWrapperWithMap(component, { color }); + + expect(wrapper.vm.mapObject.options.color).toEqual(color); + }); + + test(`${componentName} updates color when prop changes`, async () => { + const { wrapper } = getWrapperWithMap(component, { color: '#314159' }); + + const newColor = '#112233'; + wrapper.setProps({ color: newColor }); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.options.color).toEqual(newColor); + }); + + test(`${componentName} updates color using setColor`, async () => { + const { wrapper } = getWrapperWithMap(component, { color: '#314159' }); + + const newColor = '#112233'; + wrapper.vm.setColor(newColor); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.options.color).toEqual(newColor); + }); + + // Weight + + test(`${componentName} accepts and uses weight prop`, () => { + const weight = 3; + const { wrapper } = getWrapperWithMap(component, { weight }); + + expect(wrapper.vm.mapObject.options.weight).toEqual(weight); + }); + + test(`${componentName} updates weight when prop changes`, async () => { + const { wrapper } = getWrapperWithMap(component, { weight: 1 }); + + const newWeight = 5; + wrapper.setProps({ weight: newWeight }); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.options.weight).toEqual(newWeight); + }); + + test(`${componentName} updates weight using setWeight`, async () => { + const { wrapper } = getWrapperWithMap(component, { weight: 2 }); + + const newWeight = 4; + wrapper.vm.setWeight(newWeight); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.options.weight).toEqual(newWeight); + }); + + // Opacity + + test(`${componentName} accepts and uses opacity prop`, () => { + const opacity = 0.3; + const { wrapper } = getWrapperWithMap(component, { opacity }); + + expect(wrapper.vm.mapObject.options.opacity).toEqual(opacity); + }); + + test(`${componentName} updates opacity when prop changes`, async () => { + const { wrapper } = getWrapperWithMap(component, { opacity: 0.1 }); + + const newOpacity = 0.5; + wrapper.setProps({ opacity: newOpacity }); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.options.opacity).toEqual(newOpacity); + }); + + test(`${componentName} updates opacity using setOpacity`, async () => { + const { wrapper } = getWrapperWithMap(component, { opacity: 0.2 }); + + const newOpacity = 0.4; + wrapper.vm.setOpacity(newOpacity); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.options.opacity).toEqual(newOpacity); + }); + + test(`${componentName} can set opacity to zero`, async () => { + const { wrapper } = getWrapperWithMap(component, { opacity: 0.2 }); + + const newOpacity = 0; + wrapper.vm.setOpacity(newOpacity); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.options.opacity).toEqual(newOpacity); + }); + + // LineCap + + test(`${componentName} accepts and uses lineCap prop`, () => { + const lineCap = 'round'; + const { wrapper } = getWrapperWithMap(component, { lineCap }); + + expect(wrapper.vm.mapObject.options.lineCap).toEqual(lineCap); + }); + + test(`${componentName} updates lineCap when prop changes`, async () => { + const { wrapper } = getWrapperWithMap(component, { lineCap: 'round' }); + + const newLineCap = 'square'; + wrapper.setProps({ lineCap: newLineCap }); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.options.lineCap).toEqual(newLineCap); + }); + + test(`${componentName} updates lineCap using setLineCap`, async () => { + const { wrapper } = getWrapperWithMap(component, { lineCap: 'square' }); + + const newLineCap = 'round'; + wrapper.vm.setLineCap(newLineCap); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.options.lineCap).toEqual(newLineCap); + }); + + // LineJoin + + test(`${componentName} accepts and uses lineJoin prop`, () => { + const lineJoin = 'miter'; + const { wrapper } = getWrapperWithMap(component, { lineJoin }); + + expect(wrapper.vm.mapObject.options.lineJoin).toEqual(lineJoin); + }); + + test(`${componentName} updates lineJoin when prop changes`, async () => { + const { wrapper } = getWrapperWithMap(component, { lineJoin: 'miter' }); + + const newLineJoin = 'bevel'; + wrapper.setProps({ lineJoin: newLineJoin }); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.options.lineJoin).toEqual(newLineJoin); + }); + + test(`${componentName} updates lineJoin using setLineJoin`, async () => { + const { wrapper } = getWrapperWithMap(component, { lineJoin: 'bevel' }); + + const newLineJoin = 'miter'; + wrapper.vm.setLineJoin(newLineJoin); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.options.lineJoin).toEqual(newLineJoin); + }); + + // DashArray + + test(`${componentName} accepts and uses dashArray prop`, () => { + const dashArray = '4 1'; + const { wrapper } = getWrapperWithMap(component, { dashArray }); + + expect(wrapper.vm.mapObject.options.dashArray).toEqual(dashArray); + }); + + test(`${componentName} updates dashArray when prop changes`, async () => { + const { wrapper } = getWrapperWithMap(component, { dashArray: '4 1' }); + + const newDashArray = '3 2 3'; + wrapper.setProps({ dashArray: newDashArray }); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.options.dashArray).toEqual(newDashArray); + }); + + test(`${componentName} updates dashArray using setDashArray`, async () => { + const { wrapper } = getWrapperWithMap(component, { dashArray: '3 2 3' }); + + const newDashArray = '4 1'; + wrapper.vm.setDashArray(newDashArray); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.options.dashArray).toEqual(newDashArray); + }); + + // DashOffset + + test(`${componentName} accepts and uses dashOffset prop`, () => { + const dashOffset = '1'; + const { wrapper } = getWrapperWithMap(component, { dashOffset }); + + expect(wrapper.vm.mapObject.options.dashOffset).toEqual(dashOffset); + }); + + test(`${componentName} updates dashOffset when prop changes`, async () => { + const { wrapper } = getWrapperWithMap(component, { dashOffset: '1' }); + + const newDashOffset = '3'; + wrapper.setProps({ dashOffset: newDashOffset }); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.options.dashOffset).toEqual(newDashOffset); + }); + + test(`${componentName} updates dashOffset using setDashOffset`, async () => { + const { wrapper } = getWrapperWithMap(component, { dashOffset: '3' }); + + const newDashOffset = '1'; + wrapper.vm.setDashOffset(newDashOffset); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.options.dashOffset).toEqual(newDashOffset); + }); + + // Fill + + test(`${componentName} accepts and uses fill prop when true`, () => { + const { wrapper } = getWrapperWithMap(component, { fill: true }); + + expect(wrapper.vm.mapObject.options.fill).toBeTruthy(); + }); + + test(`${componentName} accepts and uses fill prop when false`, () => { + const { wrapper } = getWrapperWithMap(component, { fill: false }); + + expect(wrapper.vm.mapObject.options.fill).toBeFalsy(); + }); + + test(`${componentName} updates fill when prop changes to false`, async () => { + const { wrapper } = getWrapperWithMap(component, { fill: true }); + + wrapper.setProps({ fill: false }); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.options.fill).toBeFalsy(); + }); + + test(`${componentName} updates fill when prop changes to true`, async () => { + const { wrapper } = getWrapperWithMap(component, { fill: false }); + + wrapper.setProps({ fill: true }); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.options.fill).toBeTruthy(); + }); + + test(`${componentName} updates fill to false using setFill`, async () => { + const { wrapper } = getWrapperWithMap(component, { fill: true }); + + wrapper.vm.setFill(false); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.options.fill).toBeFalsy(); + }); + + test(`${componentName} updates fill to true using setFill`, async () => { + const { wrapper } = getWrapperWithMap(component, { fill: false }); + + wrapper.vm.setFill(true); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.options.fill).toBeTruthy(); + }); + + // FillColor + + test(`${componentName} accepts and uses fillColor prop`, () => { + const fillColor = '#abcdef'; + const { wrapper } = getWrapperWithMap(component, { fillColor }); + + expect(wrapper.vm.mapObject.options.fillColor).toEqual(fillColor); + }); + + test(`${componentName} updates fillColor when prop changes`, async () => { + const { wrapper } = getWrapperWithMap(component, { fillColor: '#abcdef' }); + + const newFillColor = '#987654'; + wrapper.setProps({ fillColor: newFillColor }); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.options.fillColor).toEqual(newFillColor); + }); + + test(`${componentName} updates fillColor using setFillColor`, async () => { + const { wrapper } = getWrapperWithMap(component, { fillColor: '#987654' }); + + const newFillColor = '#abcdef'; + wrapper.vm.setFillColor(newFillColor); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.options.fillColor).toEqual(newFillColor); + }); + + // FillOpacity + + test(`${componentName} accepts and uses fillOpacity prop`, () => { + const fillOpacity = 0.3; + const { wrapper } = getWrapperWithMap(component, { fillOpacity }); + + expect(wrapper.vm.mapObject.options.fillOpacity).toEqual(fillOpacity); + }); + + test(`${componentName} updates fillOpacity when prop changes`, async () => { + const { wrapper } = getWrapperWithMap(component, { fillOpacity: 0.1 }); + + const newFillOpacity = 0.5; + wrapper.setProps({ fillOpacity: newFillOpacity }); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.options.fillOpacity).toEqual(newFillOpacity); + }); + + test(`${componentName} updates fillOpacity using setFillOpacity`, async () => { + const { wrapper } = getWrapperWithMap(component, { fillOpacity: 0.2 }); + + const newFillOpacity = 0.4; + wrapper.vm.setFillOpacity(newFillOpacity); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.options.fillOpacity).toEqual(newFillOpacity); + }); + + test(`${componentName} can set fillOpacity to zero`, async () => { + const { wrapper } = getWrapperWithMap(component, { fillOpacity: 0.2 }); + + const newFillOpacity = 0; + wrapper.vm.setFillOpacity(newFillOpacity); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.options.fillOpacity).toEqual(newFillOpacity); + }); + + // FillRule + + test(`${componentName} accepts and uses fillRule prop`, () => { + const fillRule = 'nonzero'; + const { wrapper } = getWrapperWithMap(component, { fillRule }); + + expect(wrapper.vm.mapObject.options.fillRule).toEqual(fillRule); + }); + + test(`${componentName} updates fillRule when prop changes`, async () => { + const { wrapper } = getWrapperWithMap(component, { fillRule: 'nonzero' }); + + const newFillRule = 'evenodd'; + wrapper.setProps({ fillRule: newFillRule }); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.options.fillRule).toEqual(newFillRule); + }); + + test(`${componentName} updates fillRule using setFillRule`, async () => { + const { wrapper } = getWrapperWithMap(component, { fillRule: 'evenodd' }); + + const newFillRule = 'nonzero'; + wrapper.vm.setFillRule(newFillRule); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.options.fillRule).toEqual(newFillRule); + }); + + // ClassName + + test(`${componentName} accepts and uses className prop`, () => { + const className = 'good'; + const { wrapper } = getWrapperWithMap(component, { className }); + + expect(wrapper.vm.mapObject.options.className).toEqual(className); + }); + + test(`${componentName} updates className when prop changes`, async () => { + const { wrapper } = getWrapperWithMap(component, { className: 'good' }); + + const newClassName = 'evil'; + wrapper.setProps({ className: newClassName }); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.options.className).toEqual(newClassName); + }); + + test(`${componentName} updates className using setClassName`, async () => { + const { wrapper } = getWrapperWithMap(component, { className: 'evil' }); + + const newClassName = 'good'; + wrapper.vm.setClassName(newClassName); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.options.className).toEqual(newClassName); + }); +}; diff --git a/tests/mixin-tests/polyline-tests.js b/tests/mixin-tests/polyline-tests.js new file mode 100644 index 00000000..923401df --- /dev/null +++ b/tests/mixin-tests/polyline-tests.js @@ -0,0 +1,113 @@ +import { getWrapperWithMap } from '@/tests/test-helpers'; +import { testPathFunctionality } from './path-tests'; +import L from 'leaflet'; + +export const toLeafletLL = (latLngs) => latLngs.map(ll => L.latLng(ll)); + +export const testPolylineFunctionality = (component, componentName = 'it') => { + // The Polyline mixin includes the Path mixin. + testPathFunctionality(component, componentName); + + // SmoothFactor + + test(`${componentName} accepts and uses smoothFactor prop`, () => { + const smoothFactor = 1.5; + const { wrapper } = getWrapperWithMap(component, { smoothFactor }); + + expect(wrapper.vm.mapObject.options.smoothFactor).toEqual(smoothFactor); + }); + + test(`${componentName} updates smoothFactor when prop changes`, async () => { + const { wrapper } = getWrapperWithMap(component, { smoothFactor: 1.5 }); + + const newSmoothFactor = 2.2; + wrapper.setProps({ smoothFactor: newSmoothFactor }); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.options.smoothFactor).toEqual(newSmoothFactor); + }); + + test(`${componentName} updates smoothFactor using setSmoothFactor`, async () => { + const { wrapper } = getWrapperWithMap(component, { smoothFactor: 1.5 }); + + const newSmoothFactor = 2.2; + wrapper.vm.setSmoothFactor(newSmoothFactor); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.options.smoothFactor).toEqual(newSmoothFactor); + }); + + // NoClip + + test(`${componentName} accepts and uses noClip prop when true`, () => { + const { wrapper } = getWrapperWithMap(component, { noClip: true }); + + expect(wrapper.vm.mapObject.options.noClip).toBeTruthy(); + }); + + test(`${componentName} accepts and uses noClip prop when false`, () => { + const { wrapper } = getWrapperWithMap(component, { noClip: false }); + + expect(wrapper.vm.mapObject.options.noClip).toBeFalsy(); + }); + + test(`${componentName} updates noClip when prop changes to false`, async () => { + const { wrapper } = getWrapperWithMap(component, { noClip: true }); + + wrapper.setProps({ noClip: false }); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.options.noClip).toBeFalsy(); + }); + + test(`${componentName} updates noClip when prop changes to true`, async () => { + const { wrapper } = getWrapperWithMap(component, { noClip: false }); + + wrapper.setProps({ noClip: true }); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.options.noClip).toBeTruthy(); + }); + + test(`${componentName} updates noClip to false using setNoClip`, async () => { + const { wrapper } = getWrapperWithMap(component, { noClip: true }); + + wrapper.vm.setNoClip(false); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.options.noClip).toBeFalsy(); + }); + + test(`${componentName} updates noClip to true using setNoClip`, async () => { + const { wrapper } = getWrapperWithMap(component, { noClip: false }); + + wrapper.vm.setNoClip(true); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.options.noClip).toBeTruthy(); + }); + + // AddLatLng + + test(`${componentName} adds a point to an empty polyline`, async () => { + const { wrapper } = getWrapperWithMap(component); + + const newLatLng = [1, 2]; + wrapper.vm.addLatLng(newLatLng); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.getLatLngs()).toEqual(toLeafletLL([newLatLng])); + }); + + test(`${componentName} adds multiple points`, async () => { + const { wrapper } = getWrapperWithMap(component, { + + }); + + const newLatLngs = [[1, 2], [2, 3], [3, 4]]; + newLatLngs.forEach(ll => wrapper.vm.addLatLng(ll)); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.getLatLngs().length).toEqual(3); + }); +}; diff --git a/tests/setup.js b/tests/setup.js new file mode 100644 index 00000000..bc126474 --- /dev/null +++ b/tests/setup.js @@ -0,0 +1,13 @@ +// Perform any pre-test setup required. + +// Trick Leaflet into thinking that JSDOM has SVG support. +// See: https://stackoverflow.com/a/54384719/607408 +const createElementNSOrig = global.document.createElementNS; +global.document.createElementNS = function (uri, name) { + const element = createElementNSOrig.apply(this, arguments); + if (uri === 'http://www.w3.org/2000/svg' && name === 'svg') { + element.createSVGRect = function () {}; + } + + return element; +}; diff --git a/tests/test-helpers.js b/tests/test-helpers.js new file mode 100644 index 00000000..fe1a1d71 --- /dev/null +++ b/tests/test-helpers.js @@ -0,0 +1,47 @@ +import { createLocalVue, shallowMount } from '@vue/test-utils'; +import LMap from '@/components/LMap.vue'; + +const localVue = createLocalVue(); + +export function getMapWrapper (propsData) { + return shallowMount(LMap, { + localVue, + propsData, + sync: false // avoid warning, see + // Removing sync mode #1137 https://github.com/vuejs/vue-test-utils/issues/1137 + }); +} + +export function getWrapperWithMap (lComponent, propsData, mountOptions) { + const mapWrapper = getMapWrapper(); + + const componentCreated = lComponent.created; + const componentToMount = { + ...lComponent, + created () { + // Ensure existing created hook still runs, if it exists. + if (typeof componentCreated === 'function') { + componentCreated.bind(this)(); + } + // trick from here https://github.com/vuejs/vue-test-utils/issues/560#issuecomment-461865315 + this.$parent = mapWrapper.vm; + } + }; + + const wrapper = shallowMount(componentToMount, { + localVue, + propsData, + sync: false, // avoid warning, see + // Removing sync mode #1137 https://github.com/vuejs/vue-test-utils/issues/1137 + ...mountOptions + }); + + return { + wrapper, + mapWrapper + }; +} + +export function see (component, text) { + expect(component.html()).toContain(text); +} diff --git a/tests/unit/LMarker.spec.js b/tests/unit/LMarker.spec.js deleted file mode 100644 index 2f4e0058..00000000 --- a/tests/unit/LMarker.spec.js +++ /dev/null @@ -1,92 +0,0 @@ -import { createLocalVue, mount } from '@vue/test-utils'; -import L from 'leaflet'; -import LMarker from '@/components/LMarker.vue'; -import LMap from '@/components/LMap.vue'; - -const localVue = createLocalVue(); - -function getWrapperWithMap (component, propsData, mountOptions) { - const mapWrapper = mount(LMap, { - localVue - }); - - const wrapper = mount({ - ...component, - // trick from here https://github.com/vuejs/vue-test-utils/issues/560#issuecomment-461865315 - created () { - this.$parent = mapWrapper.vm; - } - }, { - localVue, - propsData, - sync: false, // avoid warning, see - // Removing sync mode #1137 https://github.com/vuejs/vue-test-utils/issues/1137 - ...mountOptions - }); - return { - wrapper, - mapWrapper - }; -} - -describe('LMarker.vue', () => { - test('LMarker.vue change prop latLng', async () => { - const initLatlng = L.latLng([11, 22]); - const wrapperAndMap = getWrapperWithMap(LMarker, { - latLng: initLatlng - }); - const wrapper = wrapperAndMap.wrapper; - expect(wrapper.exists()).toBe(true); - expect(wrapper.vm.mapObject.getLatLng().equals(initLatlng)).toBe(true); - const newLatLng = L.latLng([1, 1]); - wrapper.setProps({ latLng: newLatLng }); - await wrapper.vm.$nextTick(); - const curLatLng = wrapper.vm.mapObject.getLatLng(); - expect(curLatLng.equals(newLatLng)).toBe(true); - }); - - test('LMarker.vue default slot text', async () => { - const markerText = 'Hello from marker!'; - const wrapperAndMap = getWrapperWithMap(LMarker, { - latLng: [0, 0] - }, { - slots: { - default: markerText - } - }); - const wrapper = wrapperAndMap.wrapper; - const mapWrapper = wrapperAndMap.mapWrapper; - expect(mapWrapper.text()).toContain('Leaflet'); - expect(wrapper.exists()).toBe(true); - await wrapper.vm.$nextTick(); - expect(wrapper.text()).toEqual(markerText); - }); - - test('LMarker.vue draggable change', async () => { - const wrapperAndMap = getWrapperWithMap(LMarker, { - latLng: [0, 0] - }); - const wrapper = wrapperAndMap.wrapper; - const markerObject = wrapper.vm.mapObject; - expect(markerObject.dragging.enabled()).toBeFalsy(); - wrapper.setProps({ draggable: true }); - await wrapper.vm.$nextTick(); - expect(markerObject.dragging.enabled()).toBeTruthy(); - wrapper.setProps({ draggable: false }); - await wrapper.vm.$nextTick(); - expect(markerObject.dragging.enabled()).toBeFalsy(); - }); - - test('LMarker.vue not change prop latLng to null', async () => { - const initLatlng = L.latLng([11, 22]); - const wrapperAndMap = getWrapperWithMap(LMarker, { - latLng: initLatlng - }); - const wrapper = wrapperAndMap.wrapper; - expect(wrapper.exists()).toBe(true); - expect(wrapper.vm.mapObject.getLatLng().equals(initLatlng)).toBe(true); - wrapper.setProps({ latLng: null }); - await wrapper.vm.$nextTick(); - expect(wrapper.vm.mapObject.getLatLng().equals(initLatlng)).toBe(true); - }); -}); diff --git a/tests/unit/components/LCircle.spec.js b/tests/unit/components/LCircle.spec.js new file mode 100644 index 00000000..98e9b50e --- /dev/null +++ b/tests/unit/components/LCircle.spec.js @@ -0,0 +1,52 @@ +import { getWrapperWithMap } from '@/tests/test-helpers'; +import { testCircleFunctionality } from '@/tests/mixin-tests/circle-tests'; +import LCircle from '@/components/LCircle.vue'; +import L from 'leaflet'; + +describe('component: LCircle.vue', () => { + test('LCircle.vue has a mapObject', () => { + const { wrapper } = getWrapperWithMap(LCircle); + + expect(wrapper.vm.mapObject).toBeDefined(); + }); + + // Circle mixin + + testCircleFunctionality(LCircle, 'LCircle.vue'); + + // latLng property + + test('LCircle.vue accepts and uses latLng prop as an array', () => { + const latLng = [3.14, 2.79]; + const { wrapper } = getWrapperWithMap(LCircle, { latLng }); + + expect(wrapper.vm.mapObject.getLatLng()).toEqual(L.latLng(latLng)); + }); + + test('LCircle.vue accepts and uses latLng prop as a Leaflet object', () => { + const latLng = L.latLng([3.14, 2.79]); + const { wrapper } = getWrapperWithMap(LCircle, { latLng }); + + expect(wrapper.vm.mapObject.getLatLng()).toEqual(latLng); + }); + + test('LCircle.vue updates the mapObject latLng when prop is changed to an array', async () => { + const { wrapper } = getWrapperWithMap(LCircle, { latLng: [11, 22] }); + + const newLatLng = [1, 1]; + wrapper.setProps({ latLng: newLatLng }); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.getLatLng()).toEqual(L.latLng(newLatLng)); + }); + + test('LCircle.vue updates the mapObject latLng when prop is changed to an object', async () => { + const { wrapper } = getWrapperWithMap(LCircle, { latLng: [11, 22] }); + + const newLatLng = L.latLng([1, 1]); + wrapper.setProps({ latLng: newLatLng }); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.getLatLng()).toEqual(newLatLng); + }); +}); diff --git a/tests/unit/components/LCircleMarker.spec.js b/tests/unit/components/LCircleMarker.spec.js new file mode 100644 index 00000000..d876e4c6 --- /dev/null +++ b/tests/unit/components/LCircleMarker.spec.js @@ -0,0 +1,77 @@ +import { getWrapperWithMap } from '@/tests/test-helpers'; +import { testCircleFunctionality } from '@/tests/mixin-tests/circle-tests'; +import LCircleMarker from '@/components/LCircleMarker.vue'; +import L from 'leaflet'; + +describe('component: LCircleMarker.vue', () => { + test('LCircleMarker.vue has a mapObject', () => { + const { wrapper } = getWrapperWithMap(LCircleMarker); + + expect(wrapper.vm.mapObject).toBeDefined(); + }); + + // Circle mixin + + testCircleFunctionality(LCircleMarker, 'LCircleMarker.vue'); + + // latLng property + + test('LCircleMarker.vue accepts and uses latLng prop as an array', () => { + const latLng = [3.14, 2.79]; + const { wrapper } = getWrapperWithMap(LCircleMarker, { latLng }); + + expect(wrapper.vm.mapObject.getLatLng()).toEqual(L.latLng(latLng)); + }); + + test('LCircleMarker.vue accepts and uses latLng prop as a Leaflet object', () => { + const latLng = L.latLng([3.14, 2.79]); + const { wrapper } = getWrapperWithMap(LCircleMarker, { latLng }); + + expect(wrapper.vm.mapObject.getLatLng()).toEqual(latLng); + }); + + test('LCircleMarker.vue updates the mapObject latLng when prop is changed to an array', async () => { + const { wrapper } = getWrapperWithMap(LCircleMarker, { latLng: [11, 22] }); + + const newLatLng = [1, 1]; + wrapper.setProps({ latLng: newLatLng }); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.getLatLng()).toEqual(L.latLng(newLatLng)); + }); + + test('LCircleMarker.vue updates the mapObject latLng when prop is changed to an object', async () => { + const { wrapper } = getWrapperWithMap(LCircleMarker, { latLng: [11, 22] }); + + const newLatLng = L.latLng([1, 1]); + wrapper.setProps({ latLng: newLatLng }); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.getLatLng()).toEqual(newLatLng); + }); + + // Pane + + test('LCircleMarker.vue accepts and uses pane prop', () => { + const pane = 'overlayPane'; + const { wrapper } = getWrapperWithMap(LCircleMarker, { pane }); + + expect(wrapper.vm.mapObject.options.pane).toEqual(pane); + }); + + // Slot + + test('LCircleMarker.vue displays text from its default slot', async () => { + const markerText = 'O hai, Marker!'; + const { wrapper } = getWrapperWithMap(LCircleMarker, { + latLng: [0, 0] + }, { + slots: { + default: markerText + } + }); + await wrapper.vm.$nextTick(); + + expect(wrapper.text()).toEqual(markerText); + }); +}); diff --git a/tests/unit/components/LControlAttribution.spec.js b/tests/unit/components/LControlAttribution.spec.js new file mode 100644 index 00000000..363f340f --- /dev/null +++ b/tests/unit/components/LControlAttribution.spec.js @@ -0,0 +1,48 @@ +import { getWrapperWithMap, see } from '@/tests/test-helpers'; +import LControlAttribution from '@/components/LControlAttribution.vue'; + +describe('component: LControlAttribution.vue', () => { + test('LControlAttribution.vue has a mapObject', () => { + const { wrapper } = getWrapperWithMap(LControlAttribution); + + expect(wrapper.vm.mapObject).toBeDefined(); + }); + + // Leaflet Control options + + test('LControlAttribution.vue uses the "position" Leaflet option', () => { + const { wrapper } = getWrapperWithMap(LControlAttribution, { position: 'bottomleft' }); + + expect(wrapper.vm.mapObject.getPosition()).toBe('bottomleft'); + }); + + // Leaflet Control.Attribution options + + test('LControlAttribution.vue uses the "prefix" Leaflet option', () => { + const prefix = 'Custom Vue2Leaflet prefix'; + const { mapWrapper } = getWrapperWithMap(LControlAttribution, { prefix }); + + see(mapWrapper, prefix); + }); + + // Leaflet Control set* methods + + test('LControlAttribution.vue updates the mapObject when the "position" prop is set', async () => { + const { wrapper } = await getWrapperWithMap(LControlAttribution, { position: 'bottomleft' }); + wrapper.setProps({ position: 'topright' }); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.getPosition()).toBe('topright'); + }); + + // Leaflet Control.Attribution set* methods + + test('LControlAttribution.vue updates the mapObject when the "prefix" prop is set', async () => { + const { wrapper, mapWrapper } = await getWrapperWithMap(LControlAttribution); + const prefix = 'something new'; + wrapper.setProps({ prefix }); + await wrapper.vm.$nextTick(); + + see(mapWrapper, prefix); + }); +}); diff --git a/tests/unit/components/LControlLayers.spec.js b/tests/unit/components/LControlLayers.spec.js new file mode 100644 index 00000000..db91595c --- /dev/null +++ b/tests/unit/components/LControlLayers.spec.js @@ -0,0 +1,71 @@ +import { getWrapperWithMap } from '@/tests/test-helpers'; +import LControlLayers from '@/components/LControlLayers.vue'; + +describe('component: LControlLayers.vue', () => { + test('LControlLayers.vue has a mapObject', () => { + const { wrapper } = getWrapperWithMap(LControlLayers); + + expect(wrapper.vm.mapObject).toBeDefined(); + }); + + // Leaflet Control options + + test('LControlLayers.vue uses the "position" Leaflet option', () => { + const { wrapper } = getWrapperWithMap(LControlLayers, { position: 'bottomleft' }); + + expect(wrapper.vm.mapObject.getPosition()).toBe('bottomleft'); + }); + + // Leaflet Control.Layers options + + test('LControlLayers.vue uses the "collapsed" Leaflet option', () => { + const trueWrapper = getWrapperWithMap(LControlLayers, { collapsed: true }).wrapper; + const falseWrapper = getWrapperWithMap(LControlLayers, { collapsed: false }).wrapper; + + expect(trueWrapper.vm.mapObject.options.collapsed).toBeTruthy(); + expect(falseWrapper.vm.mapObject.options.collapsed).toBeFalsy(); + }); + + test('LControlLayers.vue uses the "autoZIndex" Leaflet option', () => { + const trueWrapper = getWrapperWithMap(LControlLayers, { autoZIndex: true }).wrapper; + const falseWrapper = getWrapperWithMap(LControlLayers, { autoZIndex: false }).wrapper; + + expect(trueWrapper.vm.mapObject.options.autoZIndex).toBeTruthy(); + expect(falseWrapper.vm.mapObject.options.autoZIndex).toBeFalsy(); + }); + + test('LControlLayers.vue uses the "hideSingleBase" Leaflet option', () => { + const trueWrapper = getWrapperWithMap(LControlLayers, { hideSingleBase: true }).wrapper; + const falseWrapper = getWrapperWithMap(LControlLayers, { hideSingleBase: false }).wrapper; + + expect(trueWrapper.vm.mapObject.options.hideSingleBase).toBeTruthy(); + expect(falseWrapper.vm.mapObject.options.hideSingleBase).toBeFalsy(); + }); + + test('LControlLayers.vue uses the "sortLayers" Leaflet option', () => { + const trueWrapper = getWrapperWithMap(LControlLayers, { sortLayers: true }).wrapper; + const falseWrapper = getWrapperWithMap(LControlLayers, { sortLayers: false }).wrapper; + + expect(trueWrapper.vm.mapObject.options.sortLayers).toBeTruthy(); + expect(falseWrapper.vm.mapObject.options.sortLayers).toBeFalsy(); + }); + + test('LControlLayers.vue uses the "sortFunction" Leaflet option', () => { + const sortFunction = jest.fn(); + const { wrapper } = getWrapperWithMap(LControlLayers, { sortFunction }); + + expect(wrapper.vm.mapObject.options.sortFunction).toBe(sortFunction); + }); + + // Leaflet Control set* methods + + test('LControlLayers.vue updates the mapObject when the "position" prop is set', async () => { + const { wrapper } = await getWrapperWithMap(LControlLayers, { position: 'bottomleft' }); + wrapper.setProps({ position: 'topright' }); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.getPosition()).toBe('topright'); + }); + + // TODO: Test adding and removing layers +}); diff --git a/tests/unit/components/LControlScale.spec.js b/tests/unit/components/LControlScale.spec.js new file mode 100644 index 00000000..a6995605 --- /dev/null +++ b/tests/unit/components/LControlScale.spec.js @@ -0,0 +1,54 @@ +import { getWrapperWithMap } from '@/tests/test-helpers'; +import LControlScale from '@/components/LControlScale.vue'; + +describe('component: LControlScale.vue', () => { + test('LControlScale.vue has a mapObject', () => { + const { wrapper } = getWrapperWithMap(LControlScale); + + expect(wrapper.vm.mapObject).toBeDefined(); + }); + + // Leaflet Control options + + test('LControlScale.vue uses the "position" Leaflet option', () => { + const { wrapper } = getWrapperWithMap(LControlScale, { position: 'bottomleft' }); + + expect(wrapper.vm.mapObject.getPosition()).toBe('bottomleft'); + }); + + // Leaflet Control.Scale options + + test('LControlScale.vue uses the "maxWidth" Leaflet option', () => { + const { wrapper } = getWrapperWithMap(LControlScale, { maxWidth: 37 }); + + expect(wrapper.vm.mapObject.options.maxWidth).toBe(37); + }); + + test('LControlScale.vue uses the "metric" Leaflet option', () => { + const { wrapper } = getWrapperWithMap(LControlScale, { metric: false }); + + expect(wrapper.vm.mapObject.options.metric).toBeFalsy(); + }); + + test('LControlScale.vue uses the "imperial" Leaflet option', () => { + const { wrapper } = getWrapperWithMap(LControlScale, { imperial: false }); + + expect(wrapper.vm.mapObject.options.imperial).toBeFalsy(); + }); + + test('LControlScale.vue uses the "updateWhenIdle" Leaflet option', () => { + const { wrapper } = getWrapperWithMap(LControlScale, { updateWhenIdle: true }); + + expect(wrapper.vm.mapObject.options.updateWhenIdle).toBeTruthy(); + }); + + // Leaflet Control set* methods + + test('LControlScale.vue updates the mapObject when the "position" prop is set', async () => { + const { wrapper } = getWrapperWithMap(LControlScale, { position: 'bottomleft' }); + wrapper.setProps({ position: 'topright' }); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.getPosition()).toBe('topright'); + }); +}); diff --git a/tests/unit/components/LControlZoom.spec.js b/tests/unit/components/LControlZoom.spec.js new file mode 100644 index 00000000..6e4fa6bf --- /dev/null +++ b/tests/unit/components/LControlZoom.spec.js @@ -0,0 +1,54 @@ +import { getWrapperWithMap, see } from '@/tests/test-helpers'; +import LControlZoom from '@/components/LControlZoom.vue'; + +describe('component: LControlZoom.vue', () => { + test('LControlZoom.vue has a mapObject', () => { + const { wrapper } = getWrapperWithMap(LControlZoom); + + expect(wrapper.vm.mapObject).toBeDefined(); + }); + + // Leaflet Control options + + test('LControlZoom.vue accepts and uses the Leaflet position option', () => { + const { wrapper } = getWrapperWithMap(LControlZoom, { position: 'bottomleft' }); + + expect(wrapper.vm.mapObject.getPosition()).toBe('bottomleft'); + }); + + // Leaflet Control.Zoom options + + test('LControlZoom.vue includes "zoomInText" in rendered html', async () => { + const { mapWrapper } = getWrapperWithMap(LControlZoom, { zoomInText: 'BIGGER' }); + + see(mapWrapper, 'BIGGER'); + }); + + test('LControlZoom.vue includes "zoomInTitle" in rendered html', () => { + const { mapWrapper } = getWrapperWithMap(LControlZoom, { zoomInTitle: 'zoom in more' }); + + see(mapWrapper, 'zoom in more'); + }); + + test('LControlZoom.vue includes "zoomOutText" in rendered html', () => { + const { mapWrapper } = getWrapperWithMap(LControlZoom, { zoomOutText: 'smaller' }); + + see(mapWrapper, 'smaller'); + }); + + test('LControlZoom.vue includes "zoomOutTitle" in rendered html', () => { + const { mapWrapper } = getWrapperWithMap(LControlZoom, { zoomOutTitle: 'zoom out a bit' }); + + see(mapWrapper, 'zoom out a bit'); + }); + + // Leaflet Control set* methods + + test('LControlZoom.vue updates the mapObject when the "position" prop is set', async () => { + const { wrapper } = getWrapperWithMap(LControlZoom, { position: 'bottomleft' }); + wrapper.setProps({ position: 'topright' }); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.getPosition()).toBe('topright'); + }); +}); diff --git a/tests/unit/LMap.spec.js b/tests/unit/components/LMap.spec.js similarity index 86% rename from tests/unit/LMap.spec.js rename to tests/unit/components/LMap.spec.js index 21b1800d..27494050 100644 --- a/tests/unit/LMap.spec.js +++ b/tests/unit/components/LMap.spec.js @@ -1,30 +1,21 @@ -import { createLocalVue, shallowMount } from '@vue/test-utils'; +import { getMapWrapper } from '@/tests/test-helpers'; import L from 'leaflet'; -import LMap from '@/components/LMap.vue'; -const localVue = createLocalVue(); - -function getWrapper (propsData) { - const wrapper = shallowMount(LMap, { - localVue, - propsData, - sync: false // avoid warning, see - // Removing sync mode #1137 https://github.com/vuejs/vue-test-utils/issues/1137 - }); - return wrapper; -} - -describe('LMap.vue', () => { +describe('component: LMap.vue', () => { test('LMap.vue change prop center', async () => { const mockPanTo = jest.fn(); - const wrapper = getWrapper(); + const wrapper = getMapWrapper(); + expect(wrapper.exists()).toBe(true); + wrapper.vm.mapObject.panTo = mockPanTo; const newCenter = L.latLng([1, 1]); wrapper.setProps({ center: newCenter }); await wrapper.vm.$nextTick(); + const center = wrapper.vm.lastSetCenter; const centerLatLng = L.latLng(center); + expect(centerLatLng.equals(newCenter)).toBe(true); expect(mockPanTo.mock.calls.length).toBe(1); expect(mockPanTo.mock.calls[0][0]).toBe(newCenter); @@ -33,18 +24,21 @@ describe('LMap.vue', () => { test('LMap.vue center not change', () => { const mockPanTo = jest.fn(); const center = L.latLng([0, 0]); - const wrapper = getWrapper({ center: center }); + const wrapper = getMapWrapper({ center: center }); + expect(wrapper.exists()).toBe(true); + wrapper.vm.mapObject.panTo = mockPanTo; wrapper.vm.setCenter(center); + expect(mockPanTo.mock.calls.length).toBe(0); }); test('LMap.vue setCenter should work with [0, 0]', () => { const center = [0, 0]; const mockPanTo = jest.fn(); + const wrapper = getMapWrapper({ center: [1, 1] }); - const wrapper = getWrapper({ center: [1, 1] }); wrapper.vm.mapObject.panTo = mockPanTo; wrapper.vm.setCenter(center); @@ -60,8 +54,8 @@ describe('LMap.vue', () => { const center1 = L.latLng([1, 1]); const center2 = L.latLng([2, 2]); const center3 = L.latLng([3, 3]); + const wrapper = getMapWrapper({ center: initialCenter }); - const wrapper = getWrapper({ center: initialCenter }); wrapper.vm.setCenter(center1); wrapper.vm.setCenter(center2); wrapper.vm.setCenter(center3); @@ -76,7 +70,8 @@ describe('LMap.vue', () => { test('LMap.vue initial bounds', () => { const bounds = L.latLngBounds(L.latLng([1, 1]), L.latLng([2, 2])); - const wrapper = getWrapper({ bounds: bounds }); + const wrapper = getMapWrapper({ bounds: bounds }); + expect(wrapper.vm.lastSetBounds).toEqual(bounds); }); @@ -85,12 +80,14 @@ describe('LMap.vue', () => { const newBounds = [[4, 4], [5, 5]]; const newBounds2 = L.latLngBounds(L.latLng([10, 10]), L.latLng([20, 20])); const mockFitBounds = jest.fn(); - const wrapper = getWrapper({ bounds: bounds }); + const wrapper = getMapWrapper({ bounds: bounds }); wrapper.vm.mapObject.fitBounds = mockFitBounds; + wrapper.setProps({ bounds: newBounds }); await wrapper.vm.$nextTick(); wrapper.setProps({ bounds: newBounds2 }); await wrapper.vm.$nextTick(); + expect(mockFitBounds.mock.calls.length).toBe(2); expect(mockFitBounds.mock.calls[0][0]).toEqual(L.latLngBounds(newBounds)); expect(mockFitBounds.mock.calls[0][1]).toEqual({}); @@ -102,10 +99,12 @@ describe('LMap.vue', () => { const bounds = L.latLngBounds(L.latLng([1, 1]), L.latLng([2, 2])); const sameBounds = L.latLngBounds(L.latLng([1, 1]), L.latLng([2, 2])); const mockFitBounds = jest.fn(); - const wrapper = getWrapper({ bounds: bounds }); + const wrapper = getMapWrapper({ bounds: bounds }); wrapper.vm.mapObject.fitBounds = mockFitBounds; + wrapper.setProps({ bounds: sameBounds }); await wrapper.vm.$nextTick(); + expect(mockFitBounds.mock.calls.length).toBe(0); }); @@ -120,8 +119,9 @@ describe('LMap.vue', () => { paddingTopLeft: [3, 3] }; const mockFitBounds = jest.fn(); - const wrapper = getWrapper(); + const wrapper = getMapWrapper(); wrapper.vm.mapObject.fitBounds = mockFitBounds; + wrapper.setProps({ padding: optionsPadding.padding }); wrapper.setProps({ paddingBottomRight: optionsPadding2.paddingBottomRight }); wrapper.setProps({ paddingTopLeft: optionsPadding2.paddingTopLeft }); @@ -132,6 +132,7 @@ describe('LMap.vue', () => { await wrapper.vm.$nextTick(); wrapper.setProps({ bounds: bounds2 }); await wrapper.vm.$nextTick(); + expect(mockFitBounds.mock.calls.length).toBe(2); expect(mockFitBounds.mock.calls[0][0]).toEqual(L.latLngBounds(bounds)); expect(mockFitBounds.mock.calls[0][1]).toEqual(optionsPadding); @@ -140,7 +141,7 @@ describe('LMap.vue', () => { }); test('LMap.vue no-blocking-animations options', async () => { - const wrapper = getWrapper({ + const wrapper = getMapWrapper({ noBlockingAnimations: true }); diff --git a/tests/unit/components/LMarker.spec.js b/tests/unit/components/LMarker.spec.js new file mode 100644 index 00000000..9e4663a4 --- /dev/null +++ b/tests/unit/components/LMarker.spec.js @@ -0,0 +1,75 @@ +import { getWrapperWithMap } from '@/tests/test-helpers'; +import LMarker from '@/components/LMarker.vue'; +import L from 'leaflet'; + +describe('component: LMarker.vue', () => { + test('LMarker.vue has a mapObject', () => { + const { wrapper } = getWrapperWithMap(LMarker, { + latLng: [0, 0] + }); + + expect(wrapper.exists()).toBe(true); + expect(wrapper.vm.mapObject).toBeDefined(); + }); + + test('LMarker.vue updates the mapObject latLng when props change', async () => { + const initLatlng = L.latLng([11, 22]); + const { wrapper } = getWrapperWithMap(LMarker, { + latLng: initLatlng + }); + + const markerObject = wrapper.vm.mapObject; + expect(markerObject.getLatLng().equals(initLatlng)).toBe(true); + + const newLatLng = L.latLng([1, 1]); + wrapper.setProps({ latLng: newLatLng }); + await wrapper.vm.$nextTick(); + + expect(markerObject.getLatLng().equals(newLatLng)).toBe(true); + }); + + test('LMarker.vue displays text from its default slot', async () => { + const markerText = 'Hello from marker!'; + const { wrapper } = getWrapperWithMap(LMarker, { + latLng: [0, 0] + }, { + slots: { + default: markerText + } + }); + await wrapper.vm.$nextTick(); + + expect(wrapper.text()).toEqual(markerText); + }); + + test('LMarker.vue "draggable" prop toggles mapObject dragging option', async () => { + const { wrapper } = getWrapperWithMap(LMarker, { + latLng: [0, 0], + draggable: false + }); + const markerObject = wrapper.vm.mapObject; + + expect(markerObject.dragging.enabled()).toBeFalsy(); + + wrapper.setProps({ draggable: true }); + await wrapper.vm.$nextTick(); + + expect(markerObject.dragging.enabled()).toBeTruthy(); + + wrapper.setProps({ draggable: false }); + await wrapper.vm.$nextTick(); + + expect(markerObject.dragging.enabled()).toBeFalsy(); + }); + + test('LMarker.vue does not change the mapObject latLng value when set to null', async () => { + const initLatlng = L.latLng([11, 22]); + const { wrapper } = getWrapperWithMap(LMarker, { + latLng: initLatlng + }); + + wrapper.setProps({ latLng: null }); + await wrapper.vm.$nextTick(); + expect(wrapper.vm.mapObject.getLatLng().equals(initLatlng)).toBe(true); + }); +}); diff --git a/tests/unit/components/LPolyline.spec.js b/tests/unit/components/LPolyline.spec.js new file mode 100644 index 00000000..d04d0184 --- /dev/null +++ b/tests/unit/components/LPolyline.spec.js @@ -0,0 +1,34 @@ +import { getWrapperWithMap } from '@/tests/test-helpers'; +import { testPolylineFunctionality, toLeafletLL } from '@/tests/mixin-tests/polyline-tests'; +import LPolyline from '@/components/LPolyline.vue'; + +describe('component: LPolyline.vue', () => { + test('LPolyline.vue has a mapObject', () => { + const { wrapper } = getWrapperWithMap(LPolyline); + + expect(wrapper.vm.mapObject).toBeDefined(); + }); + + // Polyline mixin + + testPolylineFunctionality(LPolyline, 'LPolyline.vue'); + + // latLngs property + + test('LPolyline.vue accepts and uses latLngss prop', () => { + const latLngs = [[3.14, 2.79], [31.4, 27.9]]; + const { wrapper } = getWrapperWithMap(LPolyline, { latLngs }); + + expect(wrapper.vm.mapObject.getLatLngs()).toEqual(toLeafletLL(latLngs)); + }); + + test('LPolyline.vue updates the mapObject latLngs when prop is changed', async () => { + const { wrapper } = getWrapperWithMap(LPolyline, { latLng: [[11, 22], [22, 33]] }); + + const newLatLngs = [[1, 1], [3, 3]]; + wrapper.setProps({ latLngs: newLatLngs }); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.mapObject.getLatLngs()).toEqual(toLeafletLL(newLatLngs)); + }); +}); diff --git a/tests/unit/utils/capitalizeFirstLetter.spec.js b/tests/unit/utils/capitalizeFirstLetter.spec.js new file mode 100644 index 00000000..3ceded16 --- /dev/null +++ b/tests/unit/utils/capitalizeFirstLetter.spec.js @@ -0,0 +1,25 @@ +import { capitalizeFirstLetter } from '@/utils/utils'; + +describe('utils: capitalizeFirstLetter', () => { + test.each([ + ['capitalize me', 'Capitalize me'], + ['toCaps', 'ToCaps'] + ])('it works with standard text', (input, expected) => { + expect(capitalizeFirstLetter(input)).toBe(expected); + }); + + test.each([ + ['Already capitalized'], + ['ALLCAPS'] + ])('it works when already capitalized', (value) => { + expect(capitalizeFirstLetter(value)).toBe(value); + }); + + test('it returns the empty string when given that', () => { + expect(capitalizeFirstLetter('')).toBe(''); + }); + + test('it returns undefined when given that', () => { + expect(capitalizeFirstLetter(undefined)).toBeUndefined(); + }); +}); diff --git a/tests/unit/utils/collectionCleaner.spec.js b/tests/unit/utils/collectionCleaner.spec.js new file mode 100644 index 00000000..00ac4e65 --- /dev/null +++ b/tests/unit/utils/collectionCleaner.spec.js @@ -0,0 +1,91 @@ +import { collectionCleaner } from '@/utils/utils'; + +describe('utils: collectionCleaner', () => { + test('it removes a key with a null value', () => { + const input = { removeMe: null }; + const output = collectionCleaner(input); + + expect(output).toBeDefined(); + expect(output.hasOwnProperty('removeMe')).toBeFalsy(); + }); + + test('it removes a key with an undefined value', () => { + const input = { removeMe: undefined }; + const output = collectionCleaner(input); + + expect(output).toBeDefined(); + expect(output.hasOwnProperty('removeMe')).toBeFalsy(); + }); + + test('it retains keys with the value false', () => { + const input = { keepMe: false }; + const output = collectionCleaner(input); + + expect(output).toBeDefined(); + expect(output.hasOwnProperty('keepMe')).toBeTruthy(); + }); + + test('it retains keys with the value 0', () => { + const input = { keepMe: 0 }; + const output = collectionCleaner(input); + + expect(output).toBeDefined(); + expect(output.hasOwnProperty('keepMe')).toBeTruthy(); + }); + + test('it retains keys with the empty string as a value', () => { + const input = { keepMe: '' }; + const output = collectionCleaner(input); + + expect(output).toBeDefined(); + expect(output.hasOwnProperty('keepMe')).toBeTruthy(); + }); + + test('it retains keys with the value NaN', () => { + const input = { keepMe: NaN }; + const output = collectionCleaner(input); + + expect(output).toBeDefined(); + expect(output.hasOwnProperty('keepMe')).toBeTruthy(); + }); + + test('it returns an empty object when given an undefined one', () => { + const output = collectionCleaner(); + + expect(output).toEqual({}); + }); + + test('it does not remove nested null properties', () => { + const expected = { a: { b: null } }; + const output = collectionCleaner(expected); + + expect(output).toEqual(expected); + }); + + test('it does not remove nested undefined properties', () => { + const expected = { a: { b: undefined } }; + const output = collectionCleaner(expected); + + expect(output).toEqual(expected); + }); + + test('it works as expected on a more complicated object', () => { + const expected = { + a: 1, + c: 'two', + e: { + f: undefined, + g: 3.14159 + } + }; + const input = { + ...expected, + b: undefined, + d: null + }; + + const output = collectionCleaner(input); + + expect(output).toEqual(expected); + }); +}); diff --git a/tests/unit/utils/debounce.spec.js b/tests/unit/utils/debounce.spec.js new file mode 100644 index 00000000..c3f8dcac --- /dev/null +++ b/tests/unit/utils/debounce.spec.js @@ -0,0 +1,89 @@ +import { debounce } from '@/utils/utils'; + +describe('utils: debounce', () => { + const debounceTime = 1000; + const sleepTime = 1500; + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + test('it calls a function only once in a given window', () => { + const fn = jest.fn(); + const debounced = debounce(fn, debounceTime); + + debounced(); + debounced(); + debounced(); + jest.advanceTimersByTime(sleepTime); + + expect(fn.mock.calls.length).toBe(1); + }); + + test('it allows multiple calls when outside the given window', () => { + const fn = jest.fn(); + const debounced = debounce(fn, debounceTime); + + debounced(); + jest.advanceTimersByTime(sleepTime); + debounced(); + debounced(); + jest.advanceTimersByTime(sleepTime); + + expect(fn.mock.calls.length).toBe(2); + }); + + test('it passes arguments to the debounced function', () => { + const fn = jest.fn(); + const debounced = debounce(fn, debounceTime); + + debounced(1, 'two', { three: 4 }); + jest.advanceTimersByTime(sleepTime); + + expect(fn.mock.calls.length).toBe(1); + + expect(fn.mock.calls[0][0]).toBe(1); + expect(fn.mock.calls[0][1]).toBe('two'); + expect(fn.mock.calls[0][2]).toEqual({ three: 4 }); + }); + + test('it only executes the final call in the time window, even with different arguments', () => { + const fn = jest.fn(); + const debounced = debounce(fn, debounceTime); + + debounced('a'); + debounced('b'); + debounced('c'); + jest.advanceTimersByTime(sleepTime); + + expect(fn.mock.calls.length).toBe(1); + expect(fn.mock.calls[0][0]).toBe('c'); + }); + + test('it keeps the window open on subsequent calls, until it first expires', () => { + const fn = jest.fn(); + const debounced = debounce(fn, debounceTime); + + // Call the function multiple times, each a little bit before the previous window + // would have expired. Even though this sequence will last 3.2 times the debounce + // window length plus the full sleep time, we expect only the final call to be + // actually executed. + debounced(1); + jest.advanceTimersByTime(debounceTime * 0.8); + debounced(2); + jest.advanceTimersByTime(debounceTime * 0.8); + debounced(3); + jest.advanceTimersByTime(debounceTime * 0.8); + debounced(4); + jest.advanceTimersByTime(debounceTime * 0.8); + debounced(5); + jest.advanceTimersByTime(sleepTime); + + expect(fn.mock.calls.length).toBe(1); + expect(fn.mock.calls[0][0]).toBe(5); + }); +}); diff --git a/tests/unit/utils/findRealParent.spec.js b/tests/unit/utils/findRealParent.spec.js new file mode 100644 index 00000000..4565e730 --- /dev/null +++ b/tests/unit/utils/findRealParent.spec.js @@ -0,0 +1,32 @@ +import { findRealParent } from '@/utils/utils'; + +describe('utils: findRealParent', () => { + test('it identifies when the given object is the parent', () => { + const realParent = { mapObject: 'exists' }; + + const foundParent = findRealParent(realParent); + + expect(foundParent).toEqual(realParent); + }); + + test('it finds a parent deeper in the hierarchy', () => { + const realParent = { mapObject: 'exists' }; + const descendent = { $parent: { $parent: realParent } }; + + const foundParent = findRealParent(descendent); + + expect(foundParent).toEqual(realParent); + }); + + test('it returns undefined when no real parent exists', () => { + const orphan = { $parent: { $parent: {} } }; + + const foundParent = findRealParent(orphan); + + expect(foundParent).toBeUndefined(); + }); + + test('it returns undefined when not given an object', () => { + expect(findRealParent()).toBeUndefined(); + }); +});