From 7a8d9038f230ba433f2773c02992a211a322ebd4 Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Fri, 3 Sep 2021 20:12:20 -0400 Subject: [PATCH] feat(backend): add cancellable Pagination change & revert on error - in previous code, if an error happens on the backend server while querying, the Pagination would still be changed and we had no clue of the previous page number (or page size change), this PR bring this functionality that if an error occurs it will rollback to previous Pagination - when using a Backend Service, you can prevent the Pagination via `onBeforePaginationChange` while on a local (in-memory) it would be via the DataView `onBeforePagingInfoChanged` event --- .../src/examples/example09.html | 15 ++- .../src/examples/example09.ts | 26 ++++- .../src/examples/example15.html | 17 ++- .../src/examples/example15.ts | 20 +++- .../__tests__/pagination.service.spec.ts | 53 ++++++++- .../src/services/backendUtility.service.ts | 2 +- .../common/src/services/pagination.service.ts | 87 +++++++++++---- .../src/slick-pagination.component.ts | 6 +- test/cypress/integration/example09.spec.js | 105 +++++++++++++++++- test/cypress/integration/example15.spec.js | 105 +++++++++++++++++- 10 files changed, 387 insertions(+), 49 deletions(-) diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example09.html b/examples/webpack-demo-vanilla-bundle/src/examples/example09.html index 607c10303..dcb4a5a78 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example09.html +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example09.html @@ -10,8 +10,10 @@

- NOTE: The last column (filter & sort) will always throw an error and its only purpose is to demo what would happen - when you encounter a backend server error (the UI should rollback to previous state before you did the action). + NOTE: For demo purposes, the last column (filter & sort) will always throw an + error and its only purpose is to demo what would happen when you encounter a backend server error + (the UI should rollback to previous state before you did the action). + Also changing Page Size to 50,000 will also throw which again is for demo purposes.
@@ -26,6 +28,11 @@
+

@@ -35,10 +42,10 @@
diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example09.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example09.ts index 2bbf7996e..4fdb05447 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example09.ts +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example09.ts @@ -21,6 +21,7 @@ export class Example09 { errorStatusClass = 'hidden'; status = ''; statusClass = 'is-success'; + isPageErrorTest = false; constructor() { this._bindingEventService = new BindingEventService(); @@ -36,7 +37,7 @@ export class Example09 { // this._bindingEventService.bind(gridContainerElm, 'onafterexporttoexcel', () => console.log('onAfterExportToExcel')); this.sgb = new Slicker.GridBundle(gridContainerElm, this.columnDefinitions, { ...ExampleGridOptions, ...this.gridOptions }, []); - // you can optionally cancel the Sort, Filter + // you can optionally cancel the Filtering, Sorting or Pagination with code shown below // this._bindingEventService.bind(gridContainerElm, 'onbeforesort', (e) => { // e.preventDefault(); // return false; @@ -46,6 +47,10 @@ export class Example09 { // this.sgb.filterService.resetToPreviousSearchFilters(); // optionally reset filter input value // return false; // }); + // this._bindingEventService.bind(gridContainerElm, 'onbeforepaginationchange', (e) => { + // e.preventDefault(); + // return false; + // }); } dispose() { @@ -100,7 +105,7 @@ export class Example09 { enableRowSelection: true, enablePagination: true, // you could optionally disable the Pagination pagination: { - pageSizes: [10, 20, 50, 100, 500], + pageSizes: [10, 20, 50, 100, 500, 50000], pageSize: defaultPageSize, }, presets: { @@ -192,9 +197,17 @@ export class Example09 { let countTotalItems = 100; const columnFilters = {}; + if (this.isPageErrorTest) { + this.isPageErrorTest = false; + throw new Error('Server timed out trying to retrieve data for the last page'); + } + for (const param of queryParams) { if (param.includes('$top=')) { top = +(param.substring('$top='.length)); + if (top === 50000) { + throw new Error('Server timed out retrieving 50,000 rows'); + } } if (param.includes('$skip=')) { skip = +(param.substring('$skip='.length)); @@ -234,14 +247,14 @@ export class Example09 { // simular a backend error when trying to sort on the "Company" field if (filterBy.includes('company')) { - throw new Error('Cannot filter by the field "Company"'); + throw new Error('Server could not filter using the field "Company"'); } } } // simular a backend error when trying to sort on the "Company" field if (orderBy.includes('company')) { - throw new Error('Cannot sort by the field "Company"'); + throw new Error('Server could not sort using the field "Company"'); } const sort = orderBy.includes('asc') @@ -342,6 +355,11 @@ export class Example09 { ]); } + throwPageChangeError() { + this.isPageErrorTest = true; + this.sgb.paginationService.goToLastPage(); + } + // THE FOLLOWING METHODS ARE ONLY FOR DEMO PURPOSES DO NOT USE THIS CODE // --- diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example15.html b/examples/webpack-demo-vanilla-bundle/src/examples/example15.html index 53ba2fb42..74eba5c01 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example15.html +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example15.html @@ -10,9 +10,11 @@

- NOTE: The last column (filter & sort) will always throw an error and its only purpose is to demo what would happen - when you encounter a backend server error (the UI should rollback to previous state before you did the action). -
+ NOTE: For demo purposes, the last column (filter & sort) will always throw an + error and its only purpose is to demo what would happen when you encounter a backend server error + (the UI should rollback to previous state before you did the action). + Also changing Page Size to 50,000 will also throw which again is for demo purposes. +s
+

@@ -39,10 +46,10 @@
diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example15.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example15.ts index 0445638d2..7c5b1eb91 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example15.ts +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example15.ts @@ -26,6 +26,7 @@ export class Example15 { status = ''; statusClass = 'is-success'; isOtherGenderAdded = false; + isPageErrorTest = false; genderCollection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; constructor() { @@ -108,7 +109,7 @@ export class Example15 { enableRowSelection: true, enablePagination: true, // you could optionally disable the Pagination pagination: { - pageSizes: [10, 20, 50, 100, 500], + pageSizes: [10, 20, 50, 100, 500, 50000], pageSize: defaultPageSize, }, presets: { @@ -227,9 +228,17 @@ export class Example15 { let countTotalItems = 100; const columnFilters = {}; + if (this.isPageErrorTest) { + this.isPageErrorTest = false; + throw new Error('Server timed out trying to retrieve data for the last page'); + } + for (const param of queryParams) { if (param.includes('$top=')) { top = +(param.substring('$top='.length)); + if (top === 50000) { + throw new Error('Server timed out retrieving 50,000 rows'); + } } if (param.includes('$skip=')) { skip = +(param.substring('$skip='.length)); @@ -269,14 +278,14 @@ export class Example15 { // simular a backend error when trying to sort on the "Company" field if (filterBy.includes('company')) { - throw new Error('Cannot filter by the field "Company"'); + throw new Error('Server could not filter using the field "Company"'); } } } // simular a backend error when trying to sort on the "Company" field if (orderBy.includes('company')) { - throw new Error('Cannot sort by the field "Company"'); + throw new Error('Server could not sort using the field "Company"'); } const sort = orderBy.includes('asc') @@ -378,6 +387,11 @@ export class Example15 { ]); } + throwPageChangeError() { + this.isPageErrorTest = true; + this.sgb.paginationService.goToLastPage(); + } + // THE FOLLOWING METHODS ARE ONLY FOR DEMO PURPOSES DO NOT USE THIS CODE // --- diff --git a/packages/common/src/services/__tests__/pagination.service.spec.ts b/packages/common/src/services/__tests__/pagination.service.spec.ts index fd3e64ad2..071191be3 100644 --- a/packages/common/src/services/__tests__/pagination.service.spec.ts +++ b/packages/common/src/services/__tests__/pagination.service.spec.ts @@ -326,17 +326,18 @@ describe('PaginationService', () => { expect(spy).toHaveBeenCalledWith(4, undefined); }); - it('should not expect "processOnPageChanged" method to be called when we are already on same page', () => { + it('should not expect "processOnPageChanged" method to be called when we are already on same page', async () => { const spy = jest.spyOn(service, 'processOnPageChanged'); mockGridOption.pagination!.pageNumber = 2; service.init(gridStub, mockGridOption.pagination as Pagination, mockGridOption.backendServiceApi); - service.goToPageNumber(2); + const output = await service.goToPageNumber(2); expect(service.dataFrom).toBe(26); expect(service.dataTo).toBe(50); expect(service.getCurrentPageNumber()).toBe(2); expect(spy).not.toHaveBeenCalled(); + expect(output).toBeFalsy(); }); }); @@ -348,12 +349,14 @@ describe('PaginationService', () => { options: { columnDefinitions: [{ id: 'name', field: 'name' }] as Column[], datasetName: 'user', - } + }, + onError: jest.fn(), }; }); afterEach(() => { jest.clearAllMocks(); + jest.spyOn(mockPubSub, 'publish').mockReturnValue(true); }); it('should execute "preProcess" method when defined', () => { @@ -366,10 +369,25 @@ describe('PaginationService', () => { expect(spy).toHaveBeenCalled(); }); - it('should execute "process" method and catch error when process Promise rejects', async () => { + it('should NOT execute anything and return a Promise with Pagination before calling the change', async () => { + const pubSubSpy = jest.spyOn(mockPubSub, 'publish').mockReturnValue(false); + + const preProcessSpy = jest.fn(); + mockGridOption.backendServiceApi!.preProcess = preProcessSpy; + + service.init(gridStub, mockGridOption.pagination as Pagination, mockGridOption.backendServiceApi); + const output = await service.processOnPageChanged(1); + + expect(output).toBeTruthy(); + expect(pubSubSpy).toHaveBeenCalled(); + expect(preProcessSpy).not.toHaveBeenCalled(); + }); + + it('should execute "process" method and catch error when process Promise rejects and there is no "onError" defined', async () => { const mockError = { error: '404' }; const postSpy = jest.fn(); mockGridOption.backendServiceApi!.process = postSpy; + mockGridOption.backendServiceApi!.onError = undefined; jest.spyOn(mockBackendService, 'processOnPaginationChanged').mockReturnValue('backend query'); jest.spyOn(mockGridOption.backendServiceApi as BackendServiceApi, 'process').mockReturnValue(Promise.reject(mockError)); const backendErrorSpy = jest.spyOn(backendUtilityServiceStub, 'onBackendError'); @@ -385,6 +403,7 @@ describe('PaginationService', () => { it('should execute "process" method and catch error when process Observable fails', async () => { const mockError = 'observable error'; const postSpy = jest.fn(); + mockGridOption.backendServiceApi!.onError = undefined; mockGridOption.backendServiceApi!.process = postSpy; jest.spyOn(mockBackendService, 'processOnPaginationChanged').mockReturnValue('backend query'); jest.spyOn(mockGridOption.backendServiceApi as BackendServiceApi, 'process').mockReturnValue(throwError(mockError)); @@ -591,6 +610,32 @@ describe('PaginationService', () => { }); }); + describe('resetToPreviousPagination method', () => { + it('should call "changeItemPerPage" when page size is different', () => { + const changeItemSpy = jest.spyOn(service, 'changeItemPerPage'); + const refreshSpy = jest.spyOn(service, 'refreshPagination'); + + service.init(gridStub, mockGridOption.pagination as Pagination, mockGridOption.backendServiceApi); + service.changeItemPerPage(100, null, false); // change without triggering event to simulate a change + service.resetToPreviousPagination(); + + expect(changeItemSpy).toHaveBeenCalled(); + expect(refreshSpy).toHaveBeenCalled(); + }); + + it('should call "goToPageNumber" when page size is different', () => { + const changeItemSpy = jest.spyOn(service, 'goToPageNumber'); + const refreshSpy = jest.spyOn(service, 'refreshPagination'); + + service.init(gridStub, mockGridOption.pagination as Pagination, mockGridOption.backendServiceApi); + service.goToPageNumber(100, null, false); // change without triggering event to simulate a change + service.resetToPreviousPagination(); + + expect(changeItemSpy).toHaveBeenCalled(); + expect(refreshSpy).toHaveBeenCalled(); + }); + }); + // processOnItemAddedOrRemoved is private but we can spy on recalculateFromToIndexes describe('processOnItemAddedOrRemoved private method', () => { afterEach(() => { diff --git a/packages/common/src/services/backendUtility.service.ts b/packages/common/src/services/backendUtility.service.ts index 90798e93f..96ddc849c 100644 --- a/packages/common/src/services/backendUtility.service.ts +++ b/packages/common/src/services/backendUtility.service.ts @@ -43,7 +43,7 @@ export class BackendUtilityService { /** On a backend service api error, we will run the "onError" if there is 1 provided or just throw back the error when nothing is provided */ onBackendError(e: any, backendApi: BackendServiceApi) { - if (backendApi?.onError) { + if (typeof backendApi?.onError === 'function') { backendApi.onError(e); } else { throw e; diff --git a/packages/common/src/services/pagination.service.ts b/packages/common/src/services/pagination.service.ts index c76f127c1..485f258a4 100644 --- a/packages/common/src/services/pagination.service.ts +++ b/packages/common/src/services/pagination.service.ts @@ -33,6 +33,7 @@ export class PaginationService { protected _totalItems = 0; protected _availablePageSizes: number[] = []; protected _paginationOptions!: Pagination; + protected _previousPagination?: Pagination; protected _subscriptions: EventSubscription[] = []; /** SlickGrid Grid object */ @@ -43,7 +44,7 @@ export class PaginationService { /** Getter of SlickGrid DataView object */ get dataView(): SlickDataView | undefined { - return (this.grid?.getData && this.grid.getData()) as SlickDataView; + return this.grid?.getData?.() ?? {} as SlickDataView; } set paginationOptions(paginationOptions: Pagination) { @@ -109,6 +110,7 @@ export class PaginationService { (this._eventHandler as SlickEventHandler>).subscribe(onPagingInfoChangedHandler, (_e, pagingInfo) => { if (this._totalItems !== pagingInfo.totalRows) { this.updateTotalItems(pagingInfo.totalRows); + this._previousPagination = { pageNumber: pagingInfo.pageNum, pageSize: pagingInfo.pageSize, pageSizes: this.availablePageSizes, totalItems: pagingInfo.totalRows }; } }); setTimeout(() => { @@ -126,13 +128,16 @@ export class PaginationService { // Subscribe to any dataview row count changed so that when Adding/Deleting item(s) through the DataView // that would trigger a refresh of the pagination numbers if (this.dataView) { - this._subscriptions.push(this.pubSubService.subscribe(`onItemAdded`, (items: any | any[]) => { - this.processOnItemAddedOrRemoved(items, true); - })); + this._subscriptions.push(this.pubSubService.subscribe(`onItemAdded`, (items: any | any[]) => this.processOnItemAddedOrRemoved(items, true))); this._subscriptions.push(this.pubSubService.subscribe(`onItemDeleted`, (items: any | any[]) => this.processOnItemAddedOrRemoved(items, false))); } this.refreshPagination(false, false, true); + + // also keep reference to current pagination in case we need to rollback + const pagination = this.getFullPagination(); + this._previousPagination = { pageNumber: pagination.pageNumber, pageSize: pagination.pageSize, pageSizes: pagination.pageSizes, totalItems: this.totalItems }; + this._initialized = true; } @@ -174,32 +179,32 @@ export class PaginationService { return this._itemsPerPage; } - changeItemPerPage(itemsPerPage: number, event?: any): Promise { + changeItemPerPage(itemsPerPage: number, event?: any, triggerChangeEvent = true): Promise { this._pageNumber = 1; this._pageCount = Math.ceil(this._totalItems / itemsPerPage); this._itemsPerPage = itemsPerPage; - return this.processOnPageChanged(this._pageNumber, event); + return triggerChangeEvent ? this.processOnPageChanged(this._pageNumber, event) : Promise.resolve(this.getFullPagination()); } - goToFirstPage(event?: any): Promise { + goToFirstPage(event?: any, triggerChangeEvent = true): Promise { this._pageNumber = 1; - return this.processOnPageChanged(this._pageNumber, event); + return triggerChangeEvent ? this.processOnPageChanged(this._pageNumber, event) : Promise.resolve(this.getFullPagination()); } - goToLastPage(event?: any): Promise { + goToLastPage(event?: any, triggerChangeEvent = true): Promise { this._pageNumber = this._pageCount || 1; - return this.processOnPageChanged(this._pageNumber || 1, event); + return triggerChangeEvent ? this.processOnPageChanged(this._pageNumber || 1, event) : Promise.resolve(this.getFullPagination()); } - goToNextPage(event?: any): Promise { + goToNextPage(event?: any, triggerChangeEvent = true): Promise { if (this._pageNumber < this._pageCount) { this._pageNumber++; - return this.processOnPageChanged(this._pageNumber, event); + return triggerChangeEvent ? this.processOnPageChanged(this._pageNumber, event) : Promise.resolve(this.getFullPagination()); } - return new Promise(resolve => resolve(false)); + return Promise.resolve(false); } - goToPageNumber(pageNumber: number, event?: any): Promise { + goToPageNumber(pageNumber: number, event?: any, triggerChangeEvent = true): Promise { const previousPageNumber = this._pageNumber; if (pageNumber < 1) { @@ -211,17 +216,17 @@ export class PaginationService { } if (this._pageNumber !== previousPageNumber) { - return this.processOnPageChanged(this._pageNumber, event); + return triggerChangeEvent ? this.processOnPageChanged(this._pageNumber, event) : Promise.resolve(this.getFullPagination()); } - return new Promise(resolve => resolve(false)); + return Promise.resolve(false); } - goToPreviousPage(event?: any): Promise { + goToPreviousPage(event?: any, triggerChangeEvent = true): Promise { if (this._pageNumber > 1) { this._pageNumber--; - return this.processOnPageChanged(this._pageNumber, event); + return triggerChangeEvent ? this.processOnPageChanged(this._pageNumber, event) : Promise.resolve(this.getFullPagination()); } - return new Promise(resolve => resolve(false)); + return Promise.resolve(false); } refreshPagination(isPageNumberReset = false, triggerChangedEvent = true, triggerInitializedEvent = false) { @@ -277,6 +282,8 @@ export class PaginationService { if (triggerInitializedEvent && !dequal(previousPagination, this.getFullPagination())) { this.pubSubService.publish(`onPaginationPresetsInitialized`, this.getFullPagination()); } + const pagination = this.getFullPagination(); + this._previousPagination = { pageNumber: pagination.pageNumber, pageSize: pagination.pageSize, pageSizes: pagination.pageSizes, totalItems: this.totalItems }; } /** Reset the Pagination to first page and recalculate necessary numbers */ @@ -319,6 +326,11 @@ export class PaginationService { } processOnPageChanged(pageNumber: number, event?: Event | undefined): Promise { + if (this.pubSubService.publish('onBeforePaginationChange', this.getFullPagination()) === false) { + this.resetToPreviousPagination(); + return Promise.resolve(this.getFullPagination()); + } + return new Promise((resolve, reject) => { this.recalculateFromToIndexes(); @@ -347,21 +359,31 @@ export class PaginationService { process .then((processResult: any) => { this.backendUtilities?.executeBackendProcessesCallback(startTime, processResult, this._backendServiceApi as BackendServiceApi, this._totalItems); + const pagination = this.getFullPagination(); + this._previousPagination = { pageNumber: pagination.pageNumber, pageSize: pagination.pageSize, pageSizes: pagination.pageSizes, totalItems: this.totalItems }; resolve(this.getFullPagination()); }) .catch((error) => { + this.resetToPreviousPagination(); this.backendUtilities?.onBackendError(error, this._backendServiceApi as BackendServiceApi); - reject(process); + if (!this._backendServiceApi?.onError || !this.backendUtilities?.onBackendError) { + reject(process); + } }); } else if (this.rxjs?.isObservable(process)) { this._subscriptions.push( (process as Observable).subscribe( (processResult: any) => { + const pagination = this.getFullPagination(); + this._previousPagination = { pageNumber: pagination.pageNumber, pageSize: pagination.pageSize, pageSizes: pagination.pageSizes, totalItems: this.totalItems }; resolve(this.backendUtilities?.executeBackendProcessesCallback(startTime, processResult, this._backendServiceApi as BackendServiceApi, this._totalItems)); }, (error: any) => { + this.resetToPreviousPagination(); this.backendUtilities?.onBackendError(error, this._backendServiceApi as BackendServiceApi); - reject(process); + if (!this._backendServiceApi?.onError || !this.backendUtilities?.onBackendError) { + reject(process); + } } ) ); @@ -396,6 +418,29 @@ export class PaginationService { } } + /** + * Reset (revert) to previous pagination, it could be because you prevented `onBeforePaginationChange`, `onBeforePagingInfoChanged` from DataView OR a Backend Error was thrown. + * It will reapply the previous filter state in the UI. + */ + resetToPreviousPagination() { + const hasPageNumberChange = this._previousPagination?.pageNumber !== this.getFullPagination().pageNumber; + const hasPageSizeChange = this._previousPagination?.pageSize !== this.getFullPagination().pageSize; + + if (hasPageSizeChange) { + this.changeItemPerPage(this._previousPagination?.pageSize ?? 0, null, false); + } + if (hasPageNumberChange) { + this.goToPageNumber(this._previousPagination?.pageNumber ?? 0, null, false); + } + + // refresh the pagination in the UI + // and re-update the Backend query string without triggering an actual query + if (hasPageNumberChange || hasPageSizeChange) { + this.refreshPagination(); + this._backendServiceApi?.service?.updatePagination?.(this._previousPagination?.pageNumber ?? 0, this._previousPagination?.pageSize ?? 0); + } + } + updateTotalItems(totalItems: number, triggerChangedEvent = false) { this._totalItems = totalItems; if (this._paginationOptions) { diff --git a/packages/pagination-component/src/slick-pagination.component.ts b/packages/pagination-component/src/slick-pagination.component.ts index 1c0bfc365..aeb69cd9a 100644 --- a/packages/pagination-component/src/slick-pagination.component.ts +++ b/packages/pagination-component/src/slick-pagination.component.ts @@ -37,15 +37,15 @@ export class SlickPaginationComponent { this._bindingHelper.querySelectorPrefix = `.${this.gridUid} `; this.currentPagination = this.paginationService.getFullPagination(); - this._enableTranslate = this.gridOptions && this.gridOptions.enableTranslate || false; - this._locales = this.gridOptions && this.gridOptions.locales || Constants.locales; + this._enableTranslate = this.gridOptions?.enableTranslate ?? false; + this._locales = this.gridOptions?.locales ?? Constants.locales; if (this._enableTranslate && (!this.translaterService || !this.translaterService.translate)) { throw new Error('[Slickgrid-Universal] requires a Translate Service to be installed and configured when the grid option "enableTranslate" is enabled.'); } this.translatePaginationTexts(this._locales); - if (this._enableTranslate && this.pubSubService && this.pubSubService.subscribe) { + if (this._enableTranslate && this.pubSubService?.subscribe) { const translateEventName = this.translaterService?.eventName ?? 'onLanguageChange'; this._subscriptions.push( this.pubSubService.subscribe(translateEventName, () => this.translatePaginationTexts(this._locales)) diff --git a/test/cypress/integration/example09.spec.js b/test/cypress/integration/example09.spec.js index c9facbb7f..a0f6f2c24 100644 --- a/test/cypress/integration/example09.spec.js +++ b/test/cypress/integration/example09.spec.js @@ -640,7 +640,7 @@ describe('Example 09 - OData Grid', { retries: 1 }, () => { .should('not.exist'); // wait for the query to finish - cy.get('[data-test=error-status]').should('contain', 'Cannot sort by the field "Company"'); + cy.get('[data-test=error-status]').should('contain', 'Server could not sort using the field "Company"'); cy.get('[data-test=status]').should('contain', 'ERROR!!'); // same query string as prior test @@ -676,7 +676,7 @@ describe('Example 09 - OData Grid', { retries: 1 }, () => { .type('Core'); // wait for the query to finish - cy.get('[data-test=error-status]').should('contain', 'Cannot filter by the field "Company"'); + cy.get('[data-test=error-status]').should('contain', 'Server could not filter using the field "Company"'); cy.get('[data-test=status]').should('contain', 'ERROR!!'); cy.get('[data-test=odata-query-result]') @@ -710,6 +710,107 @@ describe('Example 09 - OData Grid', { retries: 1 }, () => { .should(($span) => { expect($span.text()).to.eq(`$top=10&$orderby=Name desc&$filter=(Gender eq 'female')`); }); + + cy.get('[data-test=page-number-input]') + .invoke('val') + .then(pageNumber => expect(pageNumber).to.eq('1')); + + cy.get('[data-test=page-count]') + .contains('5'); + + cy.get('[data-test=item-from]') + .contains('1'); + + cy.get('[data-test=item-to]') + .contains('10'); + + cy.get('[data-test=total-items]') + .contains('50'); + }); + + it('should display error when clicking on the "Throw Error..." button and not expect query and page to change', () => { + cy.get('[data-test="throw-page-error-btn"]').click({ force: true }); + cy.wait(50); + + cy.get('[data-test=error-status]').should('contain', 'Server timed out trying to retrieve data for the last page'); + cy.get('[data-test=status]').should('contain', 'ERROR!!'); + + cy.get('[data-test=odata-query-result]') + .should(($span) => { + expect($span.text()).to.eq(`$top=10&$orderby=Name desc&$filter=(Gender eq 'female')`); + }); + + cy.get('[data-test=page-number-input]') + .invoke('val') + .then(pageNumber => expect(pageNumber).to.eq('1')); + + cy.get('[data-test=page-count]') + .contains('5'); + + cy.get('[data-test=item-from]') + .contains('1'); + + cy.get('[data-test=item-to]') + .contains('10'); + + cy.get('[data-test=total-items]') + .contains('50'); + }); + + it('should display error when trying to change items per to 50,000 items and expect query & page to remain the same', () => { + cy.get('#items-per-page-label').select('50000'); + + cy.get('[data-test=error-status]').should('contain', 'Server timed out retrieving 50,000 rows'); + cy.get('[data-test=status]').should('contain', 'ERROR!!'); + + cy.get('[data-test=odata-query-result]') + .should(($span) => { + expect($span.text()).to.eq(`$top=10&$orderby=Name desc&$filter=(Gender eq 'female')`); + }); + + cy.get('[data-test=page-number-input]') + .invoke('val') + .then(pageNumber => expect(pageNumber).to.eq('1')); + + cy.get('[data-test=page-count]') + .contains('5'); + + cy.get('[data-test=item-from]') + .contains('1'); + + cy.get('[data-test=item-to]') + .contains('10'); + + cy.get('[data-test=total-items]') + .contains('50'); + }); + + it('should now go to next page without anymore problems and query & page should change as normal', () => { + cy.get('.icon-seek-next').click(); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=odata-query-result]') + .should(($span) => { + expect($span.text()).to.eq(`$top=10&$skip=10&$orderby=Name desc&$filter=(Gender eq 'female')`); + }); + + cy.get('[data-test=page-number-input]') + .invoke('val') + .then(pageNumber => expect(pageNumber).to.eq('2')); + + cy.get('[data-test=page-count]') + .contains('5'); + + cy.get('[data-test=item-from]') + .contains('11'); + + cy.get('[data-test=item-to]') + .contains('20'); + + cy.get('[data-test=total-items]') + .contains('50'); }); }); }); diff --git a/test/cypress/integration/example15.spec.js b/test/cypress/integration/example15.spec.js index 83445f4e4..bdad6cd0a 100644 --- a/test/cypress/integration/example15.spec.js +++ b/test/cypress/integration/example15.spec.js @@ -743,7 +743,7 @@ describe('Example 15 - OData Grid using RxJS', { retries: 1 }, () => { .should('not.exist'); // wait for the query to finish - cy.get('[data-test=error-status]').should('contain', 'Cannot sort by the field "Company"'); + cy.get('[data-test=error-status]').should('contain', 'Server could not sort using the field "Company"'); cy.get('[data-test=status]').should('contain', 'ERROR!!'); // same query string as prior test @@ -779,7 +779,7 @@ describe('Example 15 - OData Grid using RxJS', { retries: 1 }, () => { .type('Core'); // wait for the query to finish - cy.get('[data-test=error-status]').should('contain', 'Cannot filter by the field "Company"'); + cy.get('[data-test=error-status]').should('contain', 'Server could not filter using the field "Company"'); cy.get('[data-test=status]').should('contain', 'ERROR!!'); cy.get('[data-test=odata-query-result]') @@ -814,6 +814,107 @@ describe('Example 15 - OData Grid using RxJS', { retries: 1 }, () => { .should(($span) => { expect($span.text()).to.eq(`$top=10&$orderby=Name desc&$filter=(Gender eq 'female')`); }); + + cy.get('[data-test=page-number-input]') + .invoke('val') + .then(pageNumber => expect(pageNumber).to.eq('1')); + + cy.get('[data-test=page-count]') + .contains('5'); + + cy.get('[data-test=item-from]') + .contains('1'); + + cy.get('[data-test=item-to]') + .contains('10'); + + cy.get('[data-test=total-items]') + .contains('50'); + }); + + it('should display error when clicking on the "Throw Error..." button and not expect query and page to change', () => { + cy.get('[data-test="throw-page-error-btn"]').click({ force: true }); + cy.wait(50); + + cy.get('[data-test=error-status]').should('contain', 'Server timed out trying to retrieve data for the last page'); + cy.get('[data-test=status]').should('contain', 'ERROR!!'); + + cy.get('[data-test=odata-query-result]') + .should(($span) => { + expect($span.text()).to.eq(`$top=10&$orderby=Name desc&$filter=(Gender eq 'female')`); + }); + + cy.get('[data-test=page-number-input]') + .invoke('val') + .then(pageNumber => expect(pageNumber).to.eq('1')); + + cy.get('[data-test=page-count]') + .contains('5'); + + cy.get('[data-test=item-from]') + .contains('1'); + + cy.get('[data-test=item-to]') + .contains('10'); + + cy.get('[data-test=total-items]') + .contains('50'); + }); + + it('should display error when trying to change items per to 50,000 items and expect query & page to remain the same', () => { + cy.get('#items-per-page-label').select('50000'); + + cy.get('[data-test=error-status]').should('contain', 'Server timed out retrieving 50,000 rows'); + cy.get('[data-test=status]').should('contain', 'ERROR!!'); + + cy.get('[data-test=odata-query-result]') + .should(($span) => { + expect($span.text()).to.eq(`$top=10&$orderby=Name desc&$filter=(Gender eq 'female')`); + }); + + cy.get('[data-test=page-number-input]') + .invoke('val') + .then(pageNumber => expect(pageNumber).to.eq('1')); + + cy.get('[data-test=page-count]') + .contains('5'); + + cy.get('[data-test=item-from]') + .contains('1'); + + cy.get('[data-test=item-to]') + .contains('10'); + + cy.get('[data-test=total-items]') + .contains('50'); + }); + + it('should now go to next page without anymore problems and query & page should change as normal', () => { + cy.get('.icon-seek-next').click(); + + // wait for the query to finish + cy.get('[data-test=status]').should('contain', 'finished'); + + cy.get('[data-test=odata-query-result]') + .should(($span) => { + expect($span.text()).to.eq(`$top=10&$skip=10&$orderby=Name desc&$filter=(Gender eq 'female')`); + }); + + cy.get('[data-test=page-number-input]') + .invoke('val') + .then(pageNumber => expect(pageNumber).to.eq('2')); + + cy.get('[data-test=page-count]') + .contains('5'); + + cy.get('[data-test=item-from]') + .contains('11'); + + cy.get('[data-test=item-to]') + .contains('20'); + + cy.get('[data-test=total-items]') + .contains('50'); }); }); });