From 4e0b528f8ecadfb2a988b71aa0930153af60b23e Mon Sep 17 00:00:00 2001 From: Ghislain Beaulac Date: Mon, 5 Aug 2019 14:47:54 -0400 Subject: [PATCH] fix(presets): Grid State & Presets stopped working for columns - there was mainly 3 problems 1. ColumnPicker & GridMenu were re-registered but their instances were not overwritten and because of that, the onColumnsChanged in the GridState was never triggering anymore because the subscribe was on the instance that no longer existed 2. we were using the setColumns with allColumns, that was a regression introduced by previous commit, this in terms was cancelling any presets of columns from coming in 3. header menu was not triggering any changes, hiding a column and/or sorting a column had no effect on the Grid State. - Example 15 is the reference for Grid State & Presets --- .circleci/config.yml | 16 +- src/app/examples/grid-menu.component.html | 2 + src/app/examples/grid-menu.component.ts | 9 +- src/app/examples/grid-state.component.html | 45 +-- src/app/examples/grid-state.component.ts | 21 +- .../components/angular-slickgrid.component.ts | 2 +- .../__tests__/headerMenuExtension.spec.ts | 1 + .../extensions/columnPickerExtension.ts | 2 +- .../extensions/extensionUtility.ts | 4 +- .../extensions/gridMenuExtension.ts | 2 +- .../extensions/headerMenuExtension.ts | 27 +- .../__tests__/extension.service.spec.ts | 14 +- .../__tests__/gridState.service.spec.ts | 32 +- .../services/__tests__/sort.service.spec.ts | 16 +- .../services/extension.service.ts | 20 +- .../services/gridState.service.ts | 37 ++- .../services/shared.service.ts | 2 + .../services/sort.service.ts | 43 ++- test/cypress/integration/example16.spec.js | 306 ++++++++++++++++++ test/cypress/integration/example9.spec.js | 32 +- test/cypress/package.json | 2 +- test/cypress/support/commands.js | 33 ++ test/cypress/tsconfig.json | 7 + 23 files changed, 571 insertions(+), 104 deletions(-) create mode 100644 test/cypress/integration/example16.spec.js create mode 100644 test/cypress/tsconfig.json diff --git a/.circleci/config.yml b/.circleci/config.yml index 5b7b17849..334f6cb8d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,15 +7,15 @@ jobs: steps: - checkout - restore_cache: - key: angular-slickgrid-build-{{ .Branch }}-{{ checksum "package.json" }} - - run: yarn install + key: angular-slickgrid-build-{{ .Branch }}-{{ checksum "yarn.lock" }} + - run: yarn install --frozen-lockfile - run: name: Install Jest JUnit coverage reporter command: yarn add --dev jest-junit - save_cache: - key: angular-slickgrid-build-{{ .Branch }}-{{ checksum "package.json" }} + key: angular-slickgrid-build-{{ .Branch }}-{{ checksum "yarn.lock" }} paths: - - "node_modules" + - ~/.cache/yarn - run: name: Run Jest tests with JUnit as reporter command: ./node_modules/.bin/jest --config test/jest.config.js --ci --runInBand --collectCoverage=true --reporters=default --reporters=jest-junit @@ -31,17 +31,17 @@ jobs: - restore_cache: name: Restoring Cache for Cypress keys: - - e2e-tests-{{ .Branch }}-{{ checksum "package.json" }} + - e2e-tests-{{ .Branch }}-{{ checksum "yarn.lock" }} - run: - name: Installing Cypress dependencies with yarn + name: Installing Cypress dependencies command: | cd test/cypress yarn install --frozen-lockfile - save_cache: name: Saving Cache for Cypress - key: e2e-tests-{{ .Branch }}-{{ checksum "package.json" }} + key: e2e-tests-{{ .Branch }}-{{ checksum "yarn.lock" }} paths: - - "test/cypress/node_modules" + - ~/.cache/yarn - run: name: Running Cypress E2E tests with JUnit reporter command: | diff --git a/src/app/examples/grid-menu.component.html b/src/app/examples/grid-menu.component.html index da5fbef28..b2fbb156a 100644 --- a/src/app/examples/grid-menu.component.html +++ b/src/app/examples/grid-menu.component.html @@ -15,6 +15,8 @@

{{title}}

Switch Language + Locale: {{selectedLanguage + '.json'}}
this.selectedLanguage = nextLocale); } toggleGridMenu(e) { diff --git a/src/app/examples/grid-state.component.html b/src/app/examples/grid-state.component.html index aaf36c96f..304d87dc3 100644 --- a/src/app/examples/grid-state.component.html +++ b/src/app/examples/grid-state.component.html @@ -1,22 +1,29 @@
-

{{title}}

-
+

{{title}}

+
- - + + + Locale: {{selectedLanguage + '.json'}} - - -
+ + +
diff --git a/src/app/examples/grid-state.component.ts b/src/app/examples/grid-state.component.ts index df70661b1..12f5df33e 100644 --- a/src/app/examples/grid-state.component.ts +++ b/src/app/examples/grid-state.component.ts @@ -51,10 +51,14 @@ export class GridStateComponent implements OnInit { ngOnInit(): void { const presets = JSON.parse(localStorage[LOCAL_STORAGE_KEY] || null); - // use some Grid State preset defaults if you wish + // use some Grid State preset defaults if you wish or just restore from Locale Storage // presets = presets || this.useDefaultPresets(); - this.defineGrid(presets); + + // always start with English for Cypress E2E tests to be consistent + const defaultLang = 'en'; + this.translate.use(defaultLang); + this.selectedLanguage = defaultLang; } /** Clear the Grid State from Local Storage and reset the grid to it's original state */ @@ -100,7 +104,6 @@ export class GridStateComponent implements OnInit { filter: { collection: multiSelectFilterArray, model: Filters.multipleSelect, - searchTerms: [1, 33, 44, 50, 66], // default selection // we could add certain option(s) to the "multiple-select" plugin filterOptions: { maxHeight: 250, @@ -141,7 +144,13 @@ export class GridStateComponent implements OnInit { enableCheckboxSelector: true, enableFiltering: true, enableTranslate: true, - i18n: this.translate + i18n: this.translate, + columnPicker: { + hideForceFitButton: true + }, + gridMenu: { + hideForceFitButton: true + }, }; // reload the Grid State with the grid options presets @@ -196,8 +205,8 @@ export class GridStateComponent implements OnInit { } switchLanguage() { - this.selectedLanguage = (this.selectedLanguage === 'en') ? 'fr' : 'en'; - this.translate.use(this.selectedLanguage); + const nextLocale = (this.selectedLanguage === 'en') ? 'fr' : 'en'; + this.translate.use(nextLocale).subscribe(() => this.selectedLanguage = nextLocale); } useDefaultPresets() { diff --git a/src/app/modules/angular-slickgrid/components/angular-slickgrid.component.ts b/src/app/modules/angular-slickgrid/components/angular-slickgrid.component.ts index 2a6e38034..625328c4c 100644 --- a/src/app/modules/angular-slickgrid/components/angular-slickgrid.component.ts +++ b/src/app/modules/angular-slickgrid/components/angular-slickgrid.component.ts @@ -326,7 +326,7 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy, OnIn this.bindBackendCallbackFunctions(this.gridOptions); } - this.gridStateService.init(this.grid, this.extensionService, this.filterService, this.sortService); + this.gridStateService.init(this.grid); this.onAngularGridCreated.emit({ // Slick Grid & DataView objects diff --git a/src/app/modules/angular-slickgrid/extensions/__tests__/headerMenuExtension.spec.ts b/src/app/modules/angular-slickgrid/extensions/__tests__/headerMenuExtension.spec.ts index 273a9fa0e..dadbc1a6c 100644 --- a/src/app/modules/angular-slickgrid/extensions/__tests__/headerMenuExtension.spec.ts +++ b/src/app/modules/angular-slickgrid/extensions/__tests__/headerMenuExtension.spec.ts @@ -15,6 +15,7 @@ const filterServiceStub = { const sortServiceStub = { clearSorting: jest.fn(), + emitSortChanged: jest.fn(), getCurrentColumnSorts: jest.fn(), onBackendSortChanged: jest.fn(), onLocalSortChanged: jest.fn(), diff --git a/src/app/modules/angular-slickgrid/extensions/columnPickerExtension.ts b/src/app/modules/angular-slickgrid/extensions/columnPickerExtension.ts index 39a9978f1..41af707ab 100644 --- a/src/app/modules/angular-slickgrid/extensions/columnPickerExtension.ts +++ b/src/app/modules/angular-slickgrid/extensions/columnPickerExtension.ts @@ -46,7 +46,7 @@ export class ColumnPickerExtension implements Extension { this.sharedService.gridOptions.columnPicker.columnTitle = this.sharedService.gridOptions.columnPicker.columnTitle || columnTitle; this.sharedService.gridOptions.columnPicker.forceFitTitle = this.sharedService.gridOptions.columnPicker.forceFitTitle || forceFitTitle; this.sharedService.gridOptions.columnPicker.syncResizeTitle = this.sharedService.gridOptions.columnPicker.syncResizeTitle || syncResizeTitle; - this._addon = new Slick.Controls.ColumnPicker(this.sharedService.columnDefinitions, this.sharedService.grid, this.sharedService.gridOptions); + this._addon = new Slick.Controls.ColumnPicker(this.sharedService.allColumns, this.sharedService.grid, this.sharedService.gridOptions); if (this.sharedService.grid && this.sharedService.gridOptions.enableColumnPicker) { if (this.sharedService.gridOptions.columnPicker.onExtensionRegistered) { diff --git a/src/app/modules/angular-slickgrid/extensions/extensionUtility.ts b/src/app/modules/angular-slickgrid/extensions/extensionUtility.ts index 996dd50b8..0dbcadbf3 100644 --- a/src/app/modules/angular-slickgrid/extensions/extensionUtility.ts +++ b/src/app/modules/angular-slickgrid/extensions/extensionUtility.ts @@ -15,8 +15,8 @@ export class ExtensionUtility { * @param array input * @param index */ - arrayRemoveItemByIndex(array: any[], index: number) { - return array.filter((el: any, i: number) => index !== i); + arrayRemoveItemByIndex(array: T[], index: number): T[] { + return array.filter((el: T, i: number) => index !== i); } /** diff --git a/src/app/modules/angular-slickgrid/extensions/gridMenuExtension.ts b/src/app/modules/angular-slickgrid/extensions/gridMenuExtension.ts index bb21ea9e3..b2aea4c3f 100644 --- a/src/app/modules/angular-slickgrid/extensions/gridMenuExtension.ts +++ b/src/app/modules/angular-slickgrid/extensions/gridMenuExtension.ts @@ -88,7 +88,7 @@ export class GridMenuExtension implements Extension { this.extensionUtility.translateItems(this.sharedService.gridOptions.gridMenu.customItems, 'titleKey', 'title'); this.extensionUtility.sortItems(this.sharedService.gridOptions.gridMenu.customItems, 'positionOrder'); - this._addon = new Slick.Controls.GridMenu(this.sharedService.columnDefinitions, this.sharedService.grid, this.sharedService.gridOptions); + this._addon = new Slick.Controls.GridMenu(this.sharedService.allColumns, this.sharedService.grid, this.sharedService.gridOptions); // hook all events if (this.sharedService.grid && this.sharedService.gridOptions.gridMenu) { diff --git a/src/app/modules/angular-slickgrid/extensions/headerMenuExtension.ts b/src/app/modules/angular-slickgrid/extensions/headerMenuExtension.ts index cebf2c4ad..07a619258 100644 --- a/src/app/modules/angular-slickgrid/extensions/headerMenuExtension.ts +++ b/src/app/modules/angular-slickgrid/extensions/headerMenuExtension.ts @@ -4,6 +4,8 @@ import { Constants } from '../constants'; import { Column, ColumnSort, + CurrentSorter, + EmitterType, Extension, ExtensionName, GridOption, @@ -192,10 +194,11 @@ export class HeaderMenuExtension implements Extension { hideColumn(column: Column) { if (this.sharedService.grid && this.sharedService.grid.getColumns && this.sharedService.grid.setColumns && this.sharedService.grid.getColumnIndex) { const columnIndex = this.sharedService.grid.getColumnIndex(column.id); - const currentColumns = this.sharedService.grid.getColumns(); + const currentColumns = this.sharedService.grid.getColumns() as Column[]; const visibleColumns = this.extensionUtility.arrayRemoveItemByIndex(currentColumns, columnIndex); this.sharedService.visibleColumns = visibleColumns; this.sharedService.grid.setColumns(visibleColumns); + this.sharedService.onColumnsChanged.next(visibleColumns); } } @@ -333,12 +336,16 @@ export class HeaderMenuExtension implements Extension { // get previously sorted columns const sortedColsWithoutCurrent: ColumnSort[] = this.sortService.getCurrentColumnSorts(args.column.id + ''); + let emitterType: EmitterType; + // add to the column array, the column sorted by the header menu sortedColsWithoutCurrent.push({ sortCol: args.column, sortAsc: isSortingAsc }); if (this.sharedService.gridOptions.backendServiceApi) { this.sortService.onBackendSortChanged(event, { multiColumnSort: true, sortCols: sortedColsWithoutCurrent, grid: this.sharedService.grid }); + emitterType = EmitterType.remote; } else if (this.sharedService.dataView) { this.sortService.onLocalSortChanged(this.sharedService.grid, this.sharedService.dataView, sortedColsWithoutCurrent); + emitterType = EmitterType.local; } else { // when using customDataView, we will simply send it as a onSort event with notify const isMultiSort = this.sharedService && this.sharedService.gridOptions && this.sharedService.gridOptions.multiColumnSort || false; @@ -354,7 +361,23 @@ export class HeaderMenuExtension implements Extension { sortCol: col && col.sortCol, }; }); - this.sharedService.grid.setSortColumns(newSortColumns); // add sort icon in UI + + // add sort icon in UI + this.sharedService.grid.setSortColumns(newSortColumns); + + // if we have an emitter type set, we will emit a sort changed + // for the Grid State Service to see the change. + // We also need to pass current sorters changed to the emitSortChanged method + if (emitterType) { + const currentLocalSorters: CurrentSorter[] = []; + newSortColumns.forEach((sortCol) => { + currentLocalSorters.push({ + columnId: sortCol.columnId + '', + direction: sortCol.sortAsc ? 'ASC' : 'DESC' + }); + }); + this.sortService.emitSortChanged(emitterType, currentLocalSorters); + } } } } diff --git a/src/app/modules/angular-slickgrid/services/__tests__/extension.service.spec.ts b/src/app/modules/angular-slickgrid/services/__tests__/extension.service.spec.ts index 93c304b6b..bad23c2df 100644 --- a/src/app/modules/angular-slickgrid/services/__tests__/extension.service.spec.ts +++ b/src/app/modules/angular-slickgrid/services/__tests__/extension.service.spec.ts @@ -588,6 +588,9 @@ describe('ExtensionService', () => { }); it('should re-register the Column Picker when enable and method is called with new column definition collection provided as argument', () => { + const instanceMock = { onColumnsChanged: jest.fn() }; + const extensionMock = { name: ExtensionName.columnPicker, addon: null, instance: null, class: null } as ExtensionModel; + const expectedExtension = { name: ExtensionName.columnPicker, addon: instanceMock, instance: instanceMock, class: null } as ExtensionModel; const gridOptionsMock = { enableColumnPicker: true } as GridOption; const columnsMock = [ { id: 'field1', field: 'field1', headerKey: 'HELLO' }, @@ -595,6 +598,7 @@ describe('ExtensionService', () => { ] as Column[]; jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock); jest.spyOn(SharedService.prototype, 'grid', 'get').mockReturnValue(gridStub); + const spyGetExt = jest.spyOn(service, 'getExtensionByName').mockReturnValue(extensionMock); const spyCpDispose = jest.spyOn(extensionColumnPickerStub, 'dispose'); const spyCpRegister = jest.spyOn(extensionColumnPickerStub, 'register'); const spyAllCols = jest.spyOn(SharedService.prototype, 'allColumns', 'set'); @@ -609,6 +613,9 @@ describe('ExtensionService', () => { }); it('should re-register the Grid Menu when enable and method is called with new column definition collection provided as argument', () => { + const instanceMock = { onColumnsChanged: jest.fn() }; + const extensionMock = { name: ExtensionName.gridMenu, addon: null, instance: null, class: null } as ExtensionModel; + const expectedExtension = { name: ExtensionName.gridMenu, addon: instanceMock, instance: instanceMock, class: null } as ExtensionModel; const gridOptionsMock = { enableGridMenu: true } as GridOption; const columnsMock = [ { id: 'field1', field: 'field1', headerKey: 'HELLO' }, @@ -616,13 +623,18 @@ describe('ExtensionService', () => { ] as Column[]; jest.spyOn(SharedService.prototype, 'gridOptions', 'get').mockReturnValue(gridOptionsMock); jest.spyOn(SharedService.prototype, 'grid', 'get').mockReturnValue(gridStub); + const spyGetExt = jest.spyOn(service, 'getExtensionByName').mockReturnValue(extensionMock); const spyGmDispose = jest.spyOn(extensionGridMenuStub, 'dispose'); - const spyGmRegister = jest.spyOn(extensionGridMenuStub, 'register'); + const spyGmRegister = jest.spyOn(extensionGridMenuStub, 'register').mockReturnValue(instanceMock); const spyAllCols = jest.spyOn(SharedService.prototype, 'allColumns', 'set'); const setColumnsSpy = jest.spyOn(gridStub, 'setColumns'); service.renderColumnHeaders(columnsMock); + expect(expectedExtension).toEqual(expectedExtension); + expect(spyGetExt).toHaveBeenCalled(); + expect(expectedExtension).toEqual(expectedExtension); + expect(spyGetExt).toHaveBeenCalled(); expect(spyGmDispose).toHaveBeenCalled(); expect(spyGmRegister).toHaveBeenCalled(); expect(spyAllCols).toHaveBeenCalledWith(columnsMock); diff --git a/src/app/modules/angular-slickgrid/services/__tests__/gridState.service.spec.ts b/src/app/modules/angular-slickgrid/services/__tests__/gridState.service.spec.ts index cedee1601..5019ad7d6 100644 --- a/src/app/modules/angular-slickgrid/services/__tests__/gridState.service.spec.ts +++ b/src/app/modules/angular-slickgrid/services/__tests__/gridState.service.spec.ts @@ -4,6 +4,7 @@ import { Subject } from 'rxjs'; import { ExtensionService } from '../extension.service'; import { FilterService } from '../filter.service'; import { GridStateService } from '../gridState.service'; +import { SharedService } from '../shared.service'; import { SortService } from '../sort.service'; import { BackendService, @@ -57,13 +58,12 @@ const sortServiceStub = { describe('GridStateService', () => { let service: GridStateService; + let sharedService: SharedService; beforeEach(() => { - TestBed.configureTestingModule({ - providers: [GridStateService] - }); - service = TestBed.get(GridStateService); - service.init(gridStub, extensionServiceStub, filterServiceStub, sortServiceStub); + sharedService = new SharedService(); + service = new GridStateService(extensionServiceStub, filterServiceStub, sharedService, sortServiceStub); + service.init(gridStub); }); afterEach(() => { @@ -89,7 +89,7 @@ describe('GridStateService', () => { const filterSpy = jest.spyOn(filterServiceStub.onFilterChanged, 'subscribe'); const sortSpy = jest.spyOn(sortServiceStub.onSortChanged, 'subscribe'); - service.init(gridStub, extensionServiceStub, filterServiceStub, sortServiceStub); + service.init(gridStub); expect(gridStateSpy).toHaveBeenCalled(); expect(filterSpy).toHaveBeenCalled(); @@ -134,7 +134,7 @@ describe('GridStateService', () => { const extensionSpy = jest.spyOn(extensionServiceStub, 'getExtensionByName').mockReturnValue(extensionMock); const rxOnChangeSpy = jest.spyOn(service.onGridStateChanged, 'next'); - service.init(gridStub, extensionServiceStub, filterServiceStub, sortServiceStub); + service.init(gridStub); slickgridEvent.notify({ columns: columnsMock }, new Slick.EventData(), gridStub); expect(gridStateSpy).toHaveBeenCalled(); @@ -157,7 +157,7 @@ describe('GridStateService', () => { const gridStateSpy = jest.spyOn(service, 'getCurrentGridState').mockReturnValue(gridStateMock); const rxOnChangeSpy = jest.spyOn(service.onGridStateChanged, 'next'); - service.init(gridStub, extensionServiceStub, filterServiceStub, sortServiceStub); + service.init(gridStub); gridStub.onColumnsReordered.notify({ impactedColumns: columnsMock }, new Slick.EventData(), gridStub); service.resetColumns(); @@ -462,5 +462,21 @@ describe('GridStateService', () => { sortServiceStub.onSortCleared.next(true); expect(rxOnChangeSpy).toHaveBeenCalledWith(stateChangeMock); }); + + it('should trigger a "gridStateService:changed" event when ShareService "onColumnsChanged" is triggered', () => { + columnsMock = [{ id: 'field1', field: 'field1', width: 100, cssClass: 'red' }] as Column[]; + currentColumnsMock = [{ columnId: 'field1', cssClass: 'red', headerCssClass: '', width: 100 }] as CurrentColumn[]; + const gridStateMock = { columns: currentColumnsMock, filters: [], sorters: [] } as GridState; + const stateChangeMock = { change: { newValues: currentColumnsMock, type: GridStateType.columns }, gridState: gridStateMock } as GridStateChange; + const rxOnChangeSpy = jest.spyOn(service.onGridStateChanged, 'next'); + const getCurGridStateSpy = jest.spyOn(service, 'getCurrentGridState').mockReturnValue(gridStateMock); + const getAssocCurColSpy = jest.spyOn(service, 'getAssociatedCurrentColumns').mockReturnValue(currentColumnsMock); + + sharedService.onColumnsChanged.next(columnsMock); + + expect(getCurGridStateSpy).toHaveBeenCalled(); + expect(getAssocCurColSpy).toHaveBeenCalled(); + expect(rxOnChangeSpy).toHaveBeenCalledWith(stateChangeMock); + }); }); }); diff --git a/src/app/modules/angular-slickgrid/services/__tests__/sort.service.spec.ts b/src/app/modules/angular-slickgrid/services/__tests__/sort.service.spec.ts index abcd39c6b..61a5c07db 100644 --- a/src/app/modules/angular-slickgrid/services/__tests__/sort.service.spec.ts +++ b/src/app/modules/angular-slickgrid/services/__tests__/sort.service.spec.ts @@ -3,10 +3,11 @@ import { Column, ColumnSort, CurrentSorter, + EmitterType, + FieldType, GridOption, SlickEventHandler, SortChangedArgs, - FieldType, } from '../../models'; import { Sorters } from '../../sorters'; import { SortService } from '../sort.service'; @@ -270,6 +271,19 @@ describe('SortService', () => { }); }); + describe('emitSortChanged method', () => { + it('should have same current sort changed when it is passed as argument to the emitSortChanged method', () => { + const localSorterMock = { columnId: 'field1', direction: 'DESC' } as CurrentSorter; + const rxOnSortSpy = jest.spyOn(service.onSortChanged, 'next'); + + service.emitSortChanged(EmitterType.local, [localSorterMock]); + const currentLocalSorters = service.getCurrentLocalSorters(); + + expect(currentLocalSorters).toEqual([localSorterMock]); + expect(rxOnSortSpy).toHaveBeenCalledWith(currentLocalSorters); + }); + }); + describe('onBackendSortChanged method', () => { const spyProcess = jest.fn(); const spyPreProcess = jest.fn(); diff --git a/src/app/modules/angular-slickgrid/services/extension.service.ts b/src/app/modules/angular-slickgrid/services/extension.service.ts index f44439e8c..9a554ed61 100644 --- a/src/app/modules/angular-slickgrid/services/extension.service.ts +++ b/src/app/modules/angular-slickgrid/services/extension.service.ts @@ -356,20 +356,30 @@ export class ExtensionService { if (Array.isArray(collection) && this.sharedService.grid && this.sharedService.grid.setColumns) { if (collection.length > this.sharedService.allColumns.length) { this.sharedService.allColumns = collection; - this.sharedService.grid.setColumns(collection); - } else { - this.sharedService.grid.setColumns(this.sharedService.allColumns); } + this.sharedService.grid.setColumns(collection); } + // dispose of previous Column Picker instance, then re-register it and don't forget to overwrite previous instance ref if (this.sharedService.gridOptions.enableColumnPicker) { this.columnPickerExtension.dispose(); - this.columnPickerExtension.register(); + const instance = this.columnPickerExtension.register(); + const extension = this.getExtensionByName(ExtensionName.columnPicker); + if (extension) { + extension.addon = instance; + extension.instance = instance; + } } + // dispose of previous Grid Menu instance, then re-register it and don't forget to overwrite previous instance ref if (this.sharedService.gridOptions.enableGridMenu) { this.gridMenuExtension.dispose(); - this.gridMenuExtension.register(); + const instance = this.gridMenuExtension.register(); + const extension = this.getExtensionByName(ExtensionName.gridMenu); + if (extension) { + extension.addon = instance; + extension.instance = instance; + } } } diff --git a/src/app/modules/angular-slickgrid/services/gridState.service.ts b/src/app/modules/angular-slickgrid/services/gridState.service.ts index 8a5bf82c2..ff815680b 100644 --- a/src/app/modules/angular-slickgrid/services/gridState.service.ts +++ b/src/app/modules/angular-slickgrid/services/gridState.service.ts @@ -9,24 +9,25 @@ import { GridState, GridStateChange, GridStateType, + SlickEventHandler, } from './../models/index'; import { ExtensionService } from './extension.service'; import { FilterService } from './filter.service'; import { SortService } from './sort.service'; import { Subject, Subscription } from 'rxjs'; import { unsubscribeAllObservables } from './utilities'; +import { SharedService } from './shared.service'; +import { Injectable } from '@angular/core'; // using external non-typed js libraries declare var Slick: any; +@Injectable() export class GridStateService { - private _eventHandler = new Slick.EventHandler(); + private _eventHandler: SlickEventHandler; private _columns: Column[] = []; private _currentColumns: CurrentColumn[] = []; private _grid: any; - private extensionService: ExtensionService; - private filterService: FilterService; - private sortService: SortService; private subscriptions: Subscription[] = []; onGridStateChanged = new Subject(); @@ -35,19 +36,21 @@ export class GridStateService { return (this._grid && this._grid.getOptions) ? this._grid.getOptions() : {}; } + constructor( + private extensionService: ExtensionService, + private filterService: FilterService, + private sharedService: SharedService, + private sortService: SortService + ) { + this._eventHandler = new Slick.EventHandler(); + } + /** - * Initialize the Export Service + * Initialize the Grid State Service * @param grid - * @param filterService - * @param sortService - * @param dataView */ - init(grid: any, extensionService: ExtensionService, filterService: FilterService, sortService: SortService): void { + init(grid: any): void { this._grid = grid; - this.extensionService = extensionService; - this.filterService = filterService; - this.sortService = sortService; - this.subscribeToAllGridChanges(grid); } @@ -261,6 +264,14 @@ export class GridStateService { // subscribe to Column Resize & Reordering this.bindSlickGridEventToGridStateChange('onColumnsReordered', grid); this.bindSlickGridEventToGridStateChange('onColumnsResized', grid); + + // subscribe to HeaderMenu (hide column) + this.subscriptions.push( + this.sharedService.onColumnsChanged.subscribe((visibleColumns: Column[]) => { + const currentColumns: CurrentColumn[] = this.getAssociatedCurrentColumns(visibleColumns); + this.onGridStateChanged.next({ change: { newValues: currentColumns, type: GridStateType.columns }, gridState: this.getCurrentGridState() }); + }) + ); } // -- diff --git a/src/app/modules/angular-slickgrid/services/shared.service.ts b/src/app/modules/angular-slickgrid/services/shared.service.ts index b7af8a987..aed4ceabe 100644 --- a/src/app/modules/angular-slickgrid/services/shared.service.ts +++ b/src/app/modules/angular-slickgrid/services/shared.service.ts @@ -1,4 +1,5 @@ import { Column, GridOption } from '../models'; +import { Subject } from 'rxjs'; export class SharedService { private _allColumns: Column[]; @@ -7,6 +8,7 @@ export class SharedService { private _grid: any; private _gridOptions: GridOption; private _visibleColumns: Column[]; + onColumnsChanged = new Subject(); // -- // public diff --git a/src/app/modules/angular-slickgrid/services/sort.service.ts b/src/app/modules/angular-slickgrid/services/sort.service.ts index 8f82aa7ca..7e0d4dcbd 100644 --- a/src/app/modules/angular-slickgrid/services/sort.service.ts +++ b/src/app/modules/angular-slickgrid/services/sort.service.ts @@ -134,6 +134,27 @@ export class SortService { } } + /** + * A simple function that is binded to the subscriber and emit a change when the sort is called. + * Other services, like Pagination, can then subscribe to it. + * @param sender + */ + emitSortChanged(sender: EmitterType, currentLocalSorters?: CurrentSorter[]) { + if (sender === EmitterType.remote && this._gridOptions && this._gridOptions.backendServiceApi) { + let currentSorters: CurrentSorter[] = []; + const backendService = this._gridOptions.backendServiceApi.service; + if (backendService && backendService.getCurrentSorters) { + currentSorters = backendService.getCurrentSorters() as CurrentSorter[]; + } + this.onSortChanged.next(currentSorters); + } else if (sender === EmitterType.local) { + if (currentLocalSorters) { + this._currentLocalSorters = currentLocalSorters; + } + this.onSortChanged.next(this.getCurrentLocalSorters()); + } + } + getCurrentLocalSorters(): CurrentSorter[] { return this._currentLocalSorters; } @@ -266,26 +287,4 @@ export class SortService { } return SortDirectionNumber.neutral; } - - // -- - // private functions - // ------------------ - - /** - * A simple function that is binded to the subscriber and emit a change when the sort is called. - * Other services, like Pagination, can then subscribe to it. - * @param sender - */ - private emitSortChanged(sender: EmitterType) { - if (sender === EmitterType.remote && this._gridOptions && this._gridOptions.backendServiceApi) { - let currentSorters: CurrentSorter[] = []; - const backendService = this._gridOptions.backendServiceApi.service; - if (backendService && backendService.getCurrentSorters) { - currentSorters = backendService.getCurrentSorters() as CurrentSorter[]; - } - this.onSortChanged.next(currentSorters); - } else if (sender === EmitterType.local) { - this.onSortChanged.next(this.getCurrentLocalSorters()); - } - } } diff --git a/test/cypress/integration/example16.spec.js b/test/cypress/integration/example16.spec.js new file mode 100644 index 000000000..9a7db1329 --- /dev/null +++ b/test/cypress/integration/example16.spec.js @@ -0,0 +1,306 @@ +describe('Example 16: Grid State & Presets using Local Storage', () => { + const fullEnglishTitles = ['', 'Title', 'Description', 'Duration', '% Complete', 'Start', 'Completed']; + const fullFrenchTitles = ['', 'Titre', 'Description', 'Durée', '% Complete', 'Début', 'Terminé']; + + beforeEach(() => { + cy.restoreLocalStorage(); + }); + + afterEach(() => { + cy.saveLocalStorage(); + }); + + it('should display Example 16 title', () => { + cy.visit(`${Cypress.config('baseExampleUrl')}/gridstate`); + cy.get('h2').should('contain', 'Example 16: Grid State & Presets using Local Storage'); + + cy.clearLocalStorage(); + cy.get('[data-test=reset-button]').click(); + }); + + it('should have exact Column Titles in the grid', () => { + cy.get('#grid16') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullEnglishTitles[index])); + }); + + it('should drag "Title" column to 3rd position in the grid', () => { + const expectedTitles = ['', 'Description', 'Duration', 'Title', '% Complete', 'Start', 'Completed']; + + cy.get('.slick-header-columns') + .children('.slick-header-column:nth(1)') + .should('contain', 'Title') + .trigger('mousedown', 'bottom', { which: 1 }); + + cy.get('.slick-header-columns') + .children('.slick-header-column:nth(3)') + .should('contain', 'Duration') + .trigger('mousemove', 'bottomRight') + .trigger('mouseup', 'bottomRight', { force: true }); + + cy.get('#grid16') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(expectedTitles[index])); + }); + + // -- + // Cypress does not yet implement the .hover() method and this test won't work until then + xit('should resize "Title" column and make it wider', () => { + cy.get('.slick-header-columns') + .children('.slick-header-column:nth(3)') + .should('contain', 'Title'); + + cy.get('.slick-header-columns') + .children('.slick-header-column:nth(3)') + .find('.slick-resizable-handle') + .trigger('mouseover', -2, 50, { force: true }) + .should('be.visible') + .invoke('show') + .hover() + .trigger('mousedown', -2, 50, { which: 1, force: true }); + + cy.get('.slick-header-columns') + .children('.slick-header-column:nth(5)') + .trigger('mousemove', 'bottomLeft') + .trigger('mouseup', 'bottomLeft', { force: true }); + }); + + it('should hide the "Start" column from the Column Picker', () => { + const expectedTitles = ['', 'Description', 'Duration', 'Title', '% Complete', 'Start', 'Completed']; + + cy.get('#grid16') + .find('.slick-header-column') + .first() + .trigger('mouseover') + .trigger('contextmenu') + .invoke('show'); + + cy.get('.slick-columnpicker') + .find('.slick-columnpicker-list') + .children() + .each(($child, index) => { + if (index === 0) { + expect($child[0].className).to.eq('hidden'); + expect($child[0].offsetHeight).to.eq(0); + expect($child[0].offsetWidth).to.eq(0); + } + + expect($child.text()).to.eq(expectedTitles[index]); + }); + + cy.get('.slick-columnpicker') + .find('.slick-columnpicker-list') + .children('li:nth-child(6)') + .children('label') + .should('contain', 'Start') + .click(); + + cy.get('.slick-columnpicker:visible') + .find('span.close') + .trigger('click') + .click(); + }); + + it('should filter certain tasks', () => { + cy.get('.grid-canvas') + .find('.slick-row') + .should('be.visible'); + + cy.get('.filter-title input') + .type('Task 1') + }); + + it('should click on "Title" column to sort it Ascending', () => { + const tasks = ['Task 1', 'Task 10', 'Task 100', 'Task 101']; + + cy.get('.slick-header-columns') + .children('.slick-header-column:nth(3)') + .click(); + + cy.get('.slick-header-columns') + .children('.slick-header-column:nth(3)') + .find('.slick-sort-indicator.slick-sort-indicator-asc') + .should('be.visible'); + + cy.get('#grid16') + .find('.slick-row') + .each(($row, index) => { + if (index > tasks.length - 1) { + return; + } + cy.wrap($row).children('.slick-cell:nth(3)') + .should('contain', tasks[index]); + }); + }); + + it('should hover over the "Duration" column click on "Sort Descending" command', () => { + cy.get('.slick-header-columns') + .children('.slick-header-column:nth(2)') + .trigger('mouseover') + .children('.slick-header-menubutton') + .should('be.hidden') + .invoke('show') + .click(); + + cy.get('.slick-header-menu') + .should('be.visible') + .children('.slick-header-menuitem:nth-child(2)') + .children('.slick-header-menucontent') + .should('contain', 'Sort Descending') + .click(); + + cy.get('.slick-header-columns') + .children('.slick-header-column:nth(2)') + .find('.slick-sort-indicator.slick-sort-indicator-desc') + .should('be.visible'); + + cy.get('.slick-header-columns') + .children('.slick-header-column:nth(2)') + .find('.slick-sort-indicator-numbered') + .should('be.visible') + .should('contain', '2'); + + cy.get('.slick-header-columns') + .children('.slick-header-column:nth(3)') + .find('.slick-sort-indicator-numbered') + .should('be.visible') + .should('contain', '1'); + + cy.reload(); + }); + + it('should expect the same Grid State to persist after the page got reloaded', () => { + const expectedTitles = ['', 'Description', 'Duration', 'Title', '% Complete', 'Completed']; + + cy.get('#grid16') + .find('.grid-canvas') + .find('.slick-row') + .should('be.visible'); + + cy.get('#grid16') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.find('.slick-column-name').text()).to.eq(expectedTitles[index])); + }); + + it('should have French titles in Column Picker after switching to Language', () => { + const expectedTitles = ['', 'Description', 'Durée', 'Titre', '% Complete', 'Début', 'Terminé']; + + cy.get('[data-test=language-button]') + .click(); + + cy.get('[data-test=selected-locale]') + .should('contain', 'fr.json'); + + cy.get('#grid16') + .find('.slick-header-column') + .first() + .trigger('mouseover') + .trigger('contextmenu') + .invoke('show'); + + cy.get('.slick-columnpicker') + .find('.slick-columnpicker-list') + .children() + .each(($child, index) => { + if (index === 0) { + expect($child[0].className).to.eq('hidden'); + expect($child[0].offsetHeight).to.eq(0); + expect($child[0].offsetWidth).to.eq(0); + } + + expect($child.text()).to.eq(expectedTitles[index]); + }); + + cy.get('.slick-columnpicker:visible') + .find('span.close') + .trigger('click') + .click(); + }); + + it('should have French titles in Grid Menu after switching to Language', () => { + const expectedTitles = ['', 'Description', 'Durée', 'Titre', '% Complete', 'Début', 'Terminé']; + + cy.get('#grid16') + .find('button.slick-gridmenu-button') + .trigger('click') + .click(); + + cy.get('.slick-gridmenu') + .find('.slick-gridmenu-list') + .children() + .each(($child, index) => { + if (index === 0) { + expect($child[0].className).to.eq('hidden'); + expect($child[0].offsetHeight).to.eq(0); + expect($child[0].offsetWidth).to.eq(0); + } + + expect($child.text()).to.eq(expectedTitles[index]); + }); + + cy.get('.slick-gridmenu:visible') + .find('span.close') + .trigger('click') + .click(); + }); + + it('should hover over the "Terminé" column and click on "Cacher la colonne" remove the column from grid', () => { + const expectedTitles = ['', 'Description', 'Durée', 'Titre', '% Complete']; + + cy.get('.slick-header-columns') + .children('.slick-header-column:nth(5)') + .trigger('mouseover') + .children('.slick-header-menubutton') + .should('be.hidden') + .invoke('show') + .click(); + + cy.get('.slick-header-menu') + .should('be.visible') + .children('.slick-header-menuitem:nth-child(6)') + .children('.slick-header-menucontent') + .should('contain', 'Cacher la colonne') + .click(); + + cy.get('#grid16') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.find('.slick-column-name').text()).to.eq(expectedTitles[index])); + + cy.reload(); + }); + + it('should expect the same Grid State to persist after the page got reloaded, however we always load in English', () => { + const expectedTitles = ['', 'Description', 'Duration', 'Title', '% Complete']; + + cy.get('#grid16') + .find('.grid-canvas') + .find('.slick-row') + .should('be.visible'); + + cy.get('.slick-header-columns') + .children('.slick-header-column:nth(2)') + .find('.slick-sort-indicator-numbered') + .should('be.visible') + .should('contain', '2'); + + cy.get('.slick-header-columns') + .children('.slick-header-column:nth(3)') + .find('.slick-sort-indicator.slick-sort-indicator-asc') + .should('be.visible'); + + cy.get('.slick-header-columns') + .children('.slick-header-column:nth(3)') + .find('.slick-sort-indicator-numbered') + .should('be.visible') + .should('contain', '1'); + + cy.get('#grid16') + .find('.slick-header-columns') + .children() + .each(($child, index) => expect($child.find('.slick-column-name').text()).to.eq(expectedTitles[index])); + }); +}); diff --git a/test/cypress/integration/example9.spec.js b/test/cypress/integration/example9.spec.js index 1d57b83d7..ebeb68b22 100644 --- a/test/cypress/integration/example9.spec.js +++ b/test/cypress/integration/example9.spec.js @@ -2,13 +2,12 @@ describe('Example 9 - Grid Menu', () => { const fullEnglishTitles = ['Title', 'Duration', '% Complete', 'Start', 'Finish', 'Completed']; const fullFrenchTitles = ['Titre', 'Durée', '% Complete', 'Début', 'Fin', 'Terminé']; - describe('use English locale', () => { - - it('should display Example 9 title', () => { - cy.visit(`${Cypress.config('baseExampleUrl')}/gridmenu`); - cy.get('h2').should('contain', 'Example 9: Grid Menu Control'); - }); + it('should display Example 9 title', () => { + cy.visit(`${Cypress.config('baseExampleUrl')}/gridmenu`); + cy.get('h2').should('contain', 'Example 9: Grid Menu Control'); + }); + describe('use English locale', () => { it('should have exact Column Titles in the grid', () => { cy.get('#grid9') .find('.slick-header-columns') @@ -17,6 +16,8 @@ describe('Example 9 - Grid Menu', () => { }); it('should hover over the Title column and click on "Hide Column" command and remove 1st column from grid', () => { + const smallerTitleList = fullEnglishTitles.slice(1); + cy.get('#grid9') .find('.slick-header-column') .first() @@ -33,7 +34,6 @@ describe('Example 9 - Grid Menu', () => { .should('contain', 'Hide Column') .click(); - const smallerTitleList = fullEnglishTitles.slice(1); cy.get('#grid9') .find('.slick-header-columns') .children() @@ -67,6 +67,8 @@ describe('Example 9 - Grid Menu', () => { }); it('should hover over the Title column and click on "Hide Column" command and remove 1st column from grid', () => { + const smallerTitleList = fullEnglishTitles.slice(1); + cy.get('#grid9') .find('.slick-header-column') .first() @@ -83,7 +85,6 @@ describe('Example 9 - Grid Menu', () => { .should('contain', 'Hide Column') .click(); - const smallerTitleList = fullEnglishTitles.slice(1); cy.get('#grid9') .find('.slick-header-columns') .children() @@ -121,6 +122,9 @@ describe('Example 9 - Grid Menu', () => { cy.get('[data-test=language]') .click(); + cy.get('[data-test=selected-locale]') + .should('contain', 'fr.json'); + cy.get('#grid9') .find('.slick-header-columns') .children() @@ -128,6 +132,8 @@ describe('Example 9 - Grid Menu', () => { }); it('should hover over the Title column and click on "Cacher la colonne" command and remove 1st column from grid', () => { + const smallerTitleList = fullFrenchTitles.slice(1); + cy.get('#grid9') .find('.slick-header-column') .first() @@ -144,7 +150,6 @@ describe('Example 9 - Grid Menu', () => { .should('contain', 'Cacher la colonne') .click(); - const smallerTitleList = fullFrenchTitles.slice(1); cy.get('#grid9') .find('.slick-header-columns') .children() @@ -172,6 +177,8 @@ describe('Example 9 - Grid Menu', () => { }); it('should hover over the Title column and click on "Hide Column" command and remove 1st column from grid', () => { + const smallerTitleList = fullFrenchTitles.slice(1); + cy.get('#grid9') .find('.slick-header-column') .first() @@ -188,7 +195,6 @@ describe('Example 9 - Grid Menu', () => { .should('contain', 'Cacher la colonne') .click(); - const smallerTitleList = fullFrenchTitles.slice(1); cy.get('#grid9') .find('.slick-header-columns') .children() @@ -208,6 +214,12 @@ describe('Example 9 - Grid Menu', () => { .should('contain', 'Titre') .click(); + cy.get('#grid9') + .get('.slick-gridmenu:visible') + .find('span.close') + .trigger('click') + .click(); + cy.get('#grid9') .find('.slick-header-columns') .children() diff --git a/test/cypress/package.json b/test/cypress/package.json index 4189e6e53..8e3fa0239 100644 --- a/test/cypress/package.json +++ b/test/cypress/package.json @@ -11,7 +11,7 @@ "author": "Ghislain B.", "license": "MIT", "devDependencies": { - "cypress": "^3.4.0", + "cypress": "^3.4.1", "mocha": "^5.2.0", "mochawesome": "^3.1.2", "mochawesome-merge": "^1.0.7", diff --git a/test/cypress/support/commands.js b/test/cypress/support/commands.js index c1f5a772e..ea7159881 100644 --- a/test/cypress/support/commands.js +++ b/test/cypress/support/commands.js @@ -23,3 +23,36 @@ // // -- This is will overwrite an existing command -- // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) + +Cypress.Commands.add('triggerHover', (elements) => { + console.log(elements) + elements.each((index, element) => { + fireEvent(element, 'mouseover'); + }); + + function fireEvent(element, event) { + if (element.fireEvent) { + element.fireEvent('on' + event); + } else { + var evObj = document.createEvent('Events'); + + evObj.initEvent(event, true, false); + + element.dispatchEvent(evObj); + } + } +}); + +let LOCAL_STORAGE_MEMORY = {}; + +Cypress.Commands.add("saveLocalStorage", () => { + Object.keys(localStorage).forEach(key => { + LOCAL_STORAGE_MEMORY[key] = localStorage[key]; + }); +}); + +Cypress.Commands.add("restoreLocalStorage", () => { + Object.keys(LOCAL_STORAGE_MEMORY).forEach(key => { + localStorage.setItem(key, LOCAL_STORAGE_MEMORY[key]); + }); +}); diff --git a/test/cypress/tsconfig.json b/test/cypress/tsconfig.json new file mode 100644 index 000000000..c3f8ab311 --- /dev/null +++ b/test/cypress/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.json", + "include": [ + "../node_modules/cypress", + "*/*.ts" + ] +}