From 5edd2c2614b63f0c10e06e86b75379e31e53e654 Mon Sep 17 00:00:00 2001 From: Carson Reinke Date: Thu, 30 Jan 2020 08:51:30 -0500 Subject: [PATCH 1/3] Url object `query` is not a traditional JavaScript Object on all systems, clone it instead --- assets/js/theme/common/faceted-search.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/assets/js/theme/common/faceted-search.js b/assets/js/theme/common/faceted-search.js index 5f1afcbd6c..3196dc0a64 100644 --- a/assets/js/theme/common/faceted-search.js +++ b/assets/js/theme/common/faceted-search.js @@ -370,9 +370,13 @@ class FacetedSearch { url.query[queryParams[0]] = queryParams[1]; delete url.query.page; + // Url object `query` is not a traditional JavaScript Object on all systems, clone it instead + const urlQueryParams = {}; + Object.assign(urlQueryParams, url.query); + event.preventDefault(); - urlUtils.goToUrl(Url.format({ pathname: url.pathname, search: urlUtils.buildQueryString(url.query) })); + urlUtils.goToUrl(Url.format({ pathname: url.pathname, search: urlUtils.buildQueryString(urlQueryParams) })); } onRangeSubmit(event) { @@ -392,7 +396,11 @@ class FacetedSearch { } } - urlUtils.goToUrl(Url.format({ pathname: url.pathname, search: urlUtils.buildQueryString(url.query) })); + // Url object `query` is not a traditional JavaScript Object on all systems, clone it instead + const urlQueryParams = {}; + Object.assign(urlQueryParams, url.query); + + urlUtils.goToUrl(Url.format({ pathname: url.pathname, search: urlUtils.buildQueryString(urlQueryParams) })); } onStateChange() { From 6203c378c018f3748290c12d5f01fa11078b50b6 Mon Sep 17 00:00:00 2001 From: Carson Reinke Date: Thu, 30 Jan 2020 09:06:48 -0500 Subject: [PATCH 2/3] Remove Karma, add Jest --- Gruntfile.js | 3 +- .../theme/common/collapsible-group.spec.js | 18 +- .../theme/common/collapsible.spec.js | 6 +- .../theme/common/faceted-search.spec.js | 54 +- .../test-unit/theme/common/form-utils.spec.js | 10 +- .../js/test-unit/theme/global/modal.spec.js | 25 +- babel.config.js | 17 + grunt/aliases.yml | 5 +- grunt/karma.js | 5 - grunt/run.js | 8 + jest-eventemitter2-transformer.js | 5 + jest.config.js | 202 + jest.setup.js | 23 + karma.conf.js | 48 - package-lock.json | 14133 ++++++++++------ package.json | 17 +- 16 files changed, 9080 insertions(+), 5499 deletions(-) create mode 100644 babel.config.js delete mode 100644 grunt/karma.js create mode 100644 grunt/run.js create mode 100644 jest-eventemitter2-transformer.js create mode 100644 jest.config.js create mode 100644 jest.setup.js delete mode 100644 karma.conf.js diff --git a/Gruntfile.js b/Gruntfile.js index 6af2f86771..eb4470f6be 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -8,5 +8,6 @@ module.exports = function(grunt) { } }); - grunt.registerTask('default', ['eslint', 'karma', 'scsslint', 'svgstore']) + grunt.loadNpmTasks('grunt-run'); + grunt.registerTask('default', ['eslint', 'jest', 'scsslint', 'svgstore']) }; diff --git a/assets/js/test-unit/theme/common/collapsible-group.spec.js b/assets/js/test-unit/theme/common/collapsible-group.spec.js index 7d3db8102f..c8073d4238 100644 --- a/assets/js/test-unit/theme/common/collapsible-group.spec.js +++ b/assets/js/test-unit/theme/common/collapsible-group.spec.js @@ -25,14 +25,17 @@ describe('CollapsibleGroup', () => { let childCollapsible; beforeEach(() => { - collapsible = jasmine.createSpyObj('collapsible', ['close', 'hasCollapsible']); + collapsible = { + close: jest.fn(), + hasCollapsible: jest.fn() + }; childCollapsible = {}; collapsibleGroup.openCollapsible = collapsible; }); it('should close the currently open collapsible if it does not contain the newly open collapsible', () => { - collapsible.hasCollapsible.and.returnValue(false); + collapsible.hasCollapsible.mockImplementation(() => false); collapsibleGroup.$component.trigger(CollapsibleEvents.open, [childCollapsible]); expect(collapsible.close).toHaveBeenCalled(); @@ -40,7 +43,7 @@ describe('CollapsibleGroup', () => { }); it('should not close the currently open collapsible if it contains the newly open collapsible', () => { - collapsible.hasCollapsible.and.returnValue(true); + collapsible.hasCollapsible.mockImplementation(() => true); collapsibleGroup.$component.trigger(CollapsibleEvents.open, [childCollapsible]); expect(collapsible.close).not.toHaveBeenCalled(); @@ -54,21 +57,24 @@ describe('CollapsibleGroup', () => { let childCollapsible; beforeEach(() => { - collapsible = jasmine.createSpyObj('collapsible', ['hasCollapsible']); + collapsible = { + close: jest.fn(), + hasCollapsible: jest.fn() + }; childCollapsible = {}; collapsibleGroup.openCollapsible = collapsible; }); it('should unset `openCollapsible` if it does not contain the newly open collapsible', () => { - collapsible.hasCollapsible.and.returnValue(false); + collapsible.hasCollapsible.mockImplementation(() => false); collapsibleGroup.$component.trigger(CollapsibleEvents.close, [childCollapsible]); expect(collapsibleGroup.openCollapsible).toEqual(null); }); it('should not unset `openCollapsible` if it contains the newly open collapsible', () => { - collapsible.hasCollapsible.and.returnValue(true); + collapsible.hasCollapsible.mockImplementation(() => true); collapsibleGroup.$component.trigger(CollapsibleEvents.close, [childCollapsible]); expect(collapsibleGroup.openCollapsible).not.toEqual(null); diff --git a/assets/js/test-unit/theme/common/collapsible.spec.js b/assets/js/test-unit/theme/common/collapsible.spec.js index 29787ef97d..9d5bb6ecd3 100644 --- a/assets/js/test-unit/theme/common/collapsible.spec.js +++ b/assets/js/test-unit/theme/common/collapsible.spec.js @@ -32,9 +32,9 @@ describe('Collapsible', () => { describe('when clicking on a toggle', () => { beforeEach(() => { - spyOn(collapsible, 'open'); - spyOn(collapsible, 'close'); - spyOn(collapsible, 'toggle').and.callThrough(); + jest.spyOn(collapsible, 'open').mockImplementation(() => {}); + jest.spyOn(collapsible, 'close').mockImplementation(() => {}); + jest.spyOn(collapsible, 'toggle'); }); it('should open if it is closed', () => { diff --git a/assets/js/test-unit/theme/common/faceted-search.spec.js b/assets/js/test-unit/theme/common/faceted-search.spec.js index 8ea67185b3..ca79ddeb64 100644 --- a/assets/js/test-unit/theme/common/faceted-search.spec.js +++ b/assets/js/test-unit/theme/common/faceted-search.spec.js @@ -12,7 +12,7 @@ describe('FacetedSearch', () => { let $element; beforeEach(() => { - onSearchSuccess = jasmine.createSpy('onSearchSuccess'); + onSearchSuccess = jest.fn(); requestOptions = { config: { @@ -73,9 +73,9 @@ describe('FacetedSearch', () => { beforeEach(() => { content = { html: '
Results
' }; - spyOn(facetedSearch, 'restoreCollapsedFacets'); - spyOn(facetedSearch, 'restoreCollapsedFacetItems'); - spyOn(Validators, 'setMinMaxPriceValidation'); + jest.spyOn(facetedSearch, 'restoreCollapsedFacets').mockImplementation(() => {}); + jest.spyOn(facetedSearch, 'restoreCollapsedFacetItems').mockImplementation(() => {}); + jest.spyOn(Validators, 'setMinMaxPriceValidation').mockImplementation(() => {}); }); it('should update view with content by firing registered callback', () => { @@ -108,9 +108,9 @@ describe('FacetedSearch', () => { const url = '/current/path?facet=1'; beforeEach(() => { - spyOn(api, 'getPage'); - spyOn(facetedSearch, 'refreshView'); - spyOn(urlUtils, 'getUrl').and.returnValue(url); + jest.spyOn(api, 'getPage').mockImplementation(() => {}); + jest.spyOn(facetedSearch, 'refreshView').mockImplementation(() => {}); + jest.spyOn(urlUtils, 'getUrl').mockImplementation(() => url); content = {}; }); @@ -118,11 +118,11 @@ describe('FacetedSearch', () => { it('should fetch content from remote server', function() { facetedSearch.updateView(); - expect(api.getPage).toHaveBeenCalledWith(url, requestOptions, jasmine.any(Function)); + expect(api.getPage).toHaveBeenCalledWith(url, requestOptions, expect.any(Function)); }); it('should refresh view', function() { - api.getPage.and.callFake(function(url, options, callback) { + jest.spyOn(api, 'getPage').mockImplementation(function(url, options, callback) { callback(null, content); }); @@ -153,8 +153,8 @@ describe('FacetedSearch', () => { let $navList; beforeEach(() => { - spyOn(facetedSearch, 'getMoreFacetResults'); - spyOn(facetedSearch, 'collapseFacetItems'); + jest.spyOn(facetedSearch, 'getMoreFacetResults').mockImplementation(() => {}); + jest.spyOn(facetedSearch, 'collapseFacetItems').mockImplementation(() => {}); $navList = $('#facet-brands'); }); @@ -180,11 +180,11 @@ describe('FacetedSearch', () => { beforeEach(() => { href = document.location.href; - spyOn(facetedSearch, 'updateView'); + jest.spyOn(facetedSearch, 'updateView').mockImplementation(() => {}); }); afterEach(() => { - urlUtils.goToUrl(href); + urlUtils.goToUrl('/'); }); it('should update view', () => { @@ -202,21 +202,21 @@ describe('FacetedSearch', () => { eventName = 'facetedSearch-range-submitted'; event = { currentTarget: '#facet-range-form', - preventDefault: jasmine.createSpy('preventDefault'), + preventDefault: jest.fn(), }; - spyOn(urlUtils, 'goToUrl'); - spyOn(facetedSearch.priceRangeValidator, 'areAll').and.returnValue(true); + jest.spyOn(urlUtils, 'goToUrl').mockImplementation(() => {}); + jest.spyOn(facetedSearch.priceRangeValidator, 'areAll').mockImplementation(() => true); }); it('should set `min_price` and `max_price` query param to corresponding form values if form is valid', () => { hooks.emit(eventName, event); - expect(urlUtils.goToUrl).toHaveBeenCalledWith('/context.html?min_price=0&max_price=100'); + expect(urlUtils.goToUrl).toHaveBeenCalledWith('/?min_price=0&max_price=100'); }); it('should not set `min_price` and `max_price` query param to corresponding form values if form is invalid', () => { - facetedSearch.priceRangeValidator.areAll.and.returnValue(false); + jest.spyOn(facetedSearch.priceRangeValidator, 'areAll').mockImplementation(() => false); hooks.emit(eventName, event); expect(urlUtils.goToUrl).not.toHaveBeenCalled(); @@ -237,17 +237,17 @@ describe('FacetedSearch', () => { eventName = 'facetedSearch-range-submitted'; event = { currentTarget: '#facet-range-form-with-other-facets', - preventDefault: jasmine.createSpy('preventDefault'), + preventDefault: jest.fn(), }; - spyOn(urlUtils, 'goToUrl'); - spyOn(facetedSearch.priceRangeValidator, 'areAll').and.returnValue(true); + jest.spyOn(urlUtils, 'goToUrl').mockImplementation(() => {}); + jest.spyOn(facetedSearch.priceRangeValidator, 'areAll').mockImplementation(() => true); }); it('send `min_price` and `max_price` query params if form is valid', () => { hooks.emit(eventName, event); - expect(urlUtils.goToUrl).toHaveBeenCalledWith('/context.html?brand[]=item1&brand[]=item2&min_price=0&max_price=50'); + expect(urlUtils.goToUrl).toHaveBeenCalledWith('/?brand[]=item1&brand[]=item2&min_price=0&max_price=50'); }); }); @@ -259,16 +259,16 @@ describe('FacetedSearch', () => { eventName = 'sortBy-submitted'; event = { currentTarget: '#facet-sort', - preventDefault: jasmine.createSpy('preventDefault'), + preventDefault: jest.fn(), }; - spyOn(urlUtils, 'goToUrl'); + jest.spyOn(urlUtils, 'goToUrl').mockImplementation(() => {}); }); it('should set `sort` query param to the value of selected option', () => { hooks.emit(eventName, event); - expect(urlUtils.goToUrl).toHaveBeenCalledWith('/context.html?sort=featured'); + expect(urlUtils.goToUrl).toHaveBeenCalledWith('/?sort=featured'); }); it('should prevent default event', function() { @@ -286,10 +286,10 @@ describe('FacetedSearch', () => { eventName = 'facetedSearch-facet-clicked'; event = { currentTarget: '[href="?brand=item1"]', - preventDefault: jasmine.createSpy('preventDefault'), + preventDefault: jest.fn(), }; - spyOn(urlUtils, 'goToUrl'); + jest.spyOn(urlUtils, 'goToUrl').mockImplementation(() => {}); }); it('should change the URL of window to the URL of facet item', () => { diff --git a/assets/js/test-unit/theme/common/form-utils.spec.js b/assets/js/test-unit/theme/common/form-utils.spec.js index c888fa73ed..e27fbad411 100644 --- a/assets/js/test-unit/theme/common/form-utils.spec.js +++ b/assets/js/test-unit/theme/common/form-utils.spec.js @@ -4,11 +4,11 @@ describe('Validators', () => { let validator; beforeEach(() => { - validator = jasmine.createSpyObj('validator', [ - 'add', - 'configure', - 'setMessageOptions', - ]); + validator = { + add: jest.fn(), + configure: jest.fn(), + setMessageOptions: jest.fn() + }; }); describe('setMinMaxPriceValidation', () => { diff --git a/assets/js/test-unit/theme/global/modal.spec.js b/assets/js/test-unit/theme/global/modal.spec.js index c60fa409c5..459e2ede9b 100644 --- a/assets/js/test-unit/theme/global/modal.spec.js +++ b/assets/js/test-unit/theme/global/modal.spec.js @@ -1,6 +1,6 @@ -import '../../../theme/global/jquery-migrate'; import modalFactory, { ModalEvents } from '../../../theme/global/modal'; import $ from 'jquery'; +import '../../../theme/global/jquery-migrate'; function attachHtml(html) { const $element = $(html); @@ -47,8 +47,6 @@ describe('Modal', () => { let $modalBody; beforeEach(() => { - $('body').height(500); - $modalBody = $(`