diff --git a/web/satellite/src/components/common/PagesBlock.vue b/web/satellite/src/components/common/PagesBlock.vue new file mode 100644 index 000000000..581250c51 --- /dev/null +++ b/web/satellite/src/components/common/PagesBlock.vue @@ -0,0 +1,78 @@ +// Copyright (C) 2019 Storj Labs, Inc. +// See LICENSE for copying information. + + + + + + diff --git a/web/satellite/src/components/common/Pagination.vue b/web/satellite/src/components/common/Pagination.vue new file mode 100644 index 000000000..3b6fc8efd --- /dev/null +++ b/web/satellite/src/components/common/Pagination.vue @@ -0,0 +1,262 @@ +// Copyright (C) 2019 Storj Labs, Inc. +// See LICENSE for copying information. + + + + + + diff --git a/web/satellite/src/types/fetch.d.ts b/web/satellite/src/types/fetch.d.ts index 7e609238f..a2eb03995 100644 --- a/web/satellite/src/types/fetch.d.ts +++ b/web/satellite/src/types/fetch.d.ts @@ -8,3 +8,7 @@ declare type Answer = { message: any; }; }; + +declare type OnPageClickCallback = (index: number) => Promise; + +declare type CheckSelected = (index: number) => boolean; diff --git a/web/satellite/src/types/pagination.ts b/web/satellite/src/types/pagination.ts new file mode 100644 index 000000000..4db58d97f --- /dev/null +++ b/web/satellite/src/types/pagination.ts @@ -0,0 +1,22 @@ +// Copyright (C) 2019 Storj Labs, Inc. +// See LICENSE for copying information. + +declare type OnPageClickCallback = (search: number) => Promise; + +export class Page { + private readonly pageIndex: number = 1; + private readonly onClick: OnPageClickCallback; + + constructor(index: number, callback: OnPageClickCallback) { + this.pageIndex = index; + this.onClick = callback; + } + + public get index() { + return this.pageIndex; + } + + public async select(): Promise { + await this.onClick(this.pageIndex); + } +} diff --git a/web/satellite/tests/unit/common/PagesBlock.spec.ts b/web/satellite/tests/unit/common/PagesBlock.spec.ts new file mode 100644 index 000000000..416869c9d --- /dev/null +++ b/web/satellite/tests/unit/common/PagesBlock.spec.ts @@ -0,0 +1,56 @@ +// Copyright (C) 2019 Storj Labs, Inc. +// See LICENSE for copying information. + +import { mount, shallowMount } from '@vue/test-utils'; +import * as sinon from 'sinon'; +import PagesBlock from '@/components/common/PagesBlock.vue'; +import { Page } from '@/types/pagination'; + +describe('Pagination.vue', () => { + it('renders correctly without props', () => { + const wrapper = shallowMount(PagesBlock); + + expect(wrapper).toMatchSnapshot(); + }); + + it('renders correctly with props', () => { + const callbackSpy = sinon.spy(); + const pagesArray: Page[] = []; + const SELECTED_PAGE_INDEX: number = 3; + + for (let i = 1; i <= 4; i++) { + pagesArray.push(new Page(i, callbackSpy)); + } + + const wrapper = shallowMount(PagesBlock, { + propsData: { + pages: pagesArray, + checkSelected: (i: number) => i === SELECTED_PAGE_INDEX + } + }); + + expect(wrapper).toMatchSnapshot(); + expect(wrapper.findAll('span').length).toBe(4); + expect(wrapper.findAll('span').at(2).classes().includes('selected')).toBe(true); + }); + + it('behaves correctly on page click', async () => { + const callbackSpy = sinon.spy(); + let pagesArray: Page[] = []; + + for (let i = 1; i <= 3; i++) { + pagesArray.push(new Page(i, callbackSpy)); + } + + const wrapper = shallowMount(PagesBlock, { + propsData: { + pages: pagesArray, + checkSelected: () => false + } + }); + + wrapper.findAll('span').at(1).trigger('click'); + + expect(callbackSpy.callCount).toBe(1); + }); +}); diff --git a/web/satellite/tests/unit/common/Pagination.spec.ts b/web/satellite/tests/unit/common/Pagination.spec.ts new file mode 100644 index 000000000..8c37224c4 --- /dev/null +++ b/web/satellite/tests/unit/common/Pagination.spec.ts @@ -0,0 +1,309 @@ +// Copyright (C) 2019 Storj Labs, Inc. +// See LICENSE for copying information. + +import { mount, shallowMount } from '@vue/test-utils'; +import * as sinon from 'sinon'; +import Pagination from '@/components/common/Pagination.vue'; + +describe('Pagination.vue', () => { + it('renders correctly', () => { + const wrapper = shallowMount(Pagination); + + expect(wrapper).toMatchSnapshot(); + }); + + it('renders correctly with props', () => { + const wrapper = shallowMount(Pagination, { + propsData: { + totalPageCount: 10, + onPageClickCallback: () => new Promise(() => false) + }, + mocks: { + $route: { + query: { + pageNumber: 2 + } + }, + $router: { + replace: () => false + } + } + }); + + expect(wrapper).toMatchSnapshot(); + }); + + it('inits correctly with totalPageCount equals 10 and current pageNumber in first block', () => { + const wrapper = mount(Pagination, { + propsData: { + totalPageCount: 10, + onPageClickCallback: () => new Promise(() => false) + }, + mocks: { + $route: { + query: { + pageNumber: 2 + } + }, + $router: { + replace: () => false + } + } + }); + + const wrapperData = wrapper.vm.$data; + + expect(wrapperData.currentPageNumber).toBe(2); + expect(wrapperData.pagesArray.length).toBe(10); + expect(wrapperData.firstBlockPages.length).toBe(3); + expect(wrapperData.middleBlockPages.length).toBe(0); + expect(wrapperData.lastBlockPages.length).toBe(1); + expect(wrapper.findAll('span').at(1).classes().includes('selected')).toBe(true); + }); + + it('inits correctly with totalPageCount equals 10 and current pageNumber in middle block', () => { + const wrapper = shallowMount(Pagination, { + propsData: { + totalPageCount: 12, + onPageClickCallback: () => new Promise(() => false) + }, + mocks: { + $route: { + query: { + pageNumber: 5 + } + }, + $router: { + replace: () => false + } + } + }); + + const wrapperData = wrapper.vm.$data; + + expect(wrapperData.currentPageNumber).toBe(5); + expect(wrapperData.pagesArray.length).toBe(12); + expect(wrapperData.firstBlockPages.length).toBe(1); + expect(wrapperData.middleBlockPages.length).toBe(3); + expect(wrapperData.lastBlockPages.length).toBe(1); + }); + + it('inits correctly with totalPageCount equals 10 and current pageNumber in last block', () => { + const wrapper = shallowMount(Pagination, { + propsData: { + totalPageCount: 13, + onPageClickCallback: () => new Promise(() => false) + }, + mocks: { + $route: { + query: { + pageNumber: 12 + } + }, + $router: { + replace: () => false + } + } + }); + + const wrapperData = wrapper.vm.$data; + + expect(wrapperData.currentPageNumber).toBe(12); + expect(wrapperData.pagesArray.length).toBe(13); + expect(wrapperData.firstBlockPages.length).toBe(1); + expect(wrapperData.middleBlockPages.length).toBe(0); + expect(wrapperData.lastBlockPages.length).toBe(3); + }); + + it('inits correctly with totalPageCount equals 4 and no current pageNumber in query', () => { + const wrapper = shallowMount(Pagination, { + propsData: { + totalPageCount: 4, + onPageClickCallback: () => new Promise(() => false) + }, + mocks: { + $route: { + query: { + pageNumber: null + } + }, + $router: { + replace: () => false + } + } + }); + + const wrapperData = wrapper.vm.$data; + + expect(wrapperData.currentPageNumber).toBe(1); + expect(wrapperData.pagesArray.length).toBe(4); + expect(wrapperData.firstBlockPages.length).toBe(4); + expect(wrapperData.middleBlockPages.length).toBe(0); + expect(wrapperData.lastBlockPages.length).toBe(0); + }); + + it('behaves correctly on page click', async () => { + const routerReplaceSpy = sinon.spy(); + const callbackSpy = sinon.stub(); + + const wrapper = mount(Pagination, { + propsData: { + totalPageCount: 9, + onPageClickCallback: callbackSpy + }, + mocks: { + $route: { + query: { + pageNumber: null + } + }, + $router: { + replace: routerReplaceSpy + } + } + }); + + const wrapperData = wrapper.vm.$data; + + wrapper.findAll('span').at(2).trigger('click'); + await expect(callbackSpy.callCount).toBe(1); + + expect(routerReplaceSpy.callCount).toBe(1); + expect(wrapperData.currentPageNumber).toBe(3); + expect(wrapperData.firstBlockPages.length).toBe(1); + expect(wrapperData.middleBlockPages.length).toBe(3); + expect(wrapperData.lastBlockPages.length).toBe(1); + }); + + it('behaves correctly on next page button click', async () => { + const routerReplaceSpy = sinon.spy(); + const callbackSpy = sinon.stub(); + + const wrapper = mount(Pagination, { + propsData: { + totalPageCount: 9, + onPageClickCallback: callbackSpy + }, + mocks: { + $route: { + query: { + pageNumber: null + } + }, + $router: { + replace: routerReplaceSpy + } + } + }); + + const wrapperData = wrapper.vm.$data; + + wrapper.findAll('.pagination-container__button').at(1).trigger('click'); + await expect(callbackSpy.callCount).toBe(1); + + expect(routerReplaceSpy.callCount).toBe(1); + expect(wrapperData.currentPageNumber).toBe(2); + expect(wrapperData.firstBlockPages.length).toBe(3); + expect(wrapperData.middleBlockPages.length).toBe(0); + expect(wrapperData.lastBlockPages.length).toBe(1); + }); + + it('behaves correctly on previous page button click', async () => { + const routerReplaceSpy = sinon.spy(); + const callbackSpy = sinon.stub(); + + const wrapper = mount(Pagination, { + propsData: { + totalPageCount: 9, + onPageClickCallback: callbackSpy + }, + mocks: { + $route: { + query: { + pageNumber: 8 + } + }, + $router: { + replace: routerReplaceSpy + } + } + }); + + const wrapperData = wrapper.vm.$data; + + wrapper.findAll('.pagination-container__button').at(0).trigger('click'); + await expect(callbackSpy.callCount).toBe(1); + + expect(routerReplaceSpy.callCount).toBe(2); + expect(wrapperData.currentPageNumber).toBe(7); + expect(wrapperData.firstBlockPages.length).toBe(1); + expect(wrapperData.middleBlockPages.length).toBe(3); + expect(wrapperData.lastBlockPages.length).toBe(1); + }); + + it('behaves correctly on previous page button click when current is 1', async () => { + const routerReplaceSpy = sinon.spy(); + const callbackSpy = sinon.stub(); + + const wrapper = mount(Pagination, { + propsData: { + totalPageCount: 9, + onPageClickCallback: callbackSpy + }, + mocks: { + $route: { + query: { + pageNumber: null + } + }, + $router: { + replace: routerReplaceSpy + } + } + }); + + const wrapperData = wrapper.vm.$data; + + wrapper.findAll('.pagination-container__button').at(0).trigger('click'); + await expect(callbackSpy.callCount).toBe(0); + + expect(routerReplaceSpy.callCount).toBe(0); + expect(wrapperData.currentPageNumber).toBe(1); + expect(wrapperData.firstBlockPages.length).toBe(3); + expect(wrapperData.middleBlockPages.length).toBe(0); + expect(wrapperData.lastBlockPages.length).toBe(1); + }); + + it('behaves correctly on next page button click when current is last', async () => { + const routerReplaceSpy = sinon.spy(); + const callbackSpy = sinon.stub(); + + const wrapper = mount(Pagination, { + propsData: { + totalPageCount: 9, + onPageClickCallback: callbackSpy + }, + mocks: { + $route: { + query: { + pageNumber: 9 + } + }, + $router: { + replace: routerReplaceSpy + } + } + }); + + const wrapperData = wrapper.vm.$data; + + wrapper.findAll('.pagination-container__button').at(1).trigger('click'); + await expect(callbackSpy.callCount).toBe(0); + + expect(routerReplaceSpy.callCount).toBe(1); + expect(wrapperData.currentPageNumber).toBe(9); + expect(wrapperData.firstBlockPages.length).toBe(1); + expect(wrapperData.middleBlockPages.length).toBe(0); + expect(wrapperData.lastBlockPages.length).toBe(3); + }); +}); diff --git a/web/satellite/tests/unit/common/__snapshots__/PagesBlock.spec.ts.snap b/web/satellite/tests/unit/common/__snapshots__/PagesBlock.spec.ts.snap new file mode 100644 index 000000000..8ed35ee5f --- /dev/null +++ b/web/satellite/tests/unit/common/__snapshots__/PagesBlock.spec.ts.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Pagination.vue renders correctly with props 1`] = `
1234
`; + +exports[`Pagination.vue renders correctly without props 1`] = `
`; diff --git a/web/satellite/tests/unit/common/__snapshots__/Pagination.spec.ts.snap b/web/satellite/tests/unit/common/__snapshots__/Pagination.spec.ts.snap new file mode 100644 index 000000000..d33fe20e4 --- /dev/null +++ b/web/satellite/tests/unit/common/__snapshots__/Pagination.spec.ts.snap @@ -0,0 +1,42 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Pagination.vue renders correctly 1`] = ` +
+
+
+ +
+
+ + + + + +
+
+ + +
+
+
+`; + +exports[`Pagination.vue renders correctly with props 1`] = ` +
+
+
+ +
+
+ ... + + + +
+
+ + +
+
+
+`;