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'); }); }); });