+
@@ -42,8 +45,8 @@
@@ -98,8 +101,8 @@
@@ -126,8 +129,8 @@
diff --git a/packages/vue-instantsearch/src/components/__tests__/Pagination.js b/packages/vue-instantsearch/src/components/__tests__/Pagination.js
deleted file mode 100644
index 6a85eb5a0e..0000000000
--- a/packages/vue-instantsearch/src/components/__tests__/Pagination.js
+++ /dev/null
@@ -1,225 +0,0 @@
-/**
- * @jest-environment jsdom
- */
-
-import { mount } from '../../../test/utils';
-import { __setState } from '../../mixins/widget';
-import Pagination from '../Pagination.vue';
-import '../../../test/utils/sortedHtmlSerializer';
-
-jest.mock('../../mixins/widget');
-jest.mock('../../mixins/panel');
-
-const defaultState = {
- createURL: () => '#',
- refine: jest.fn(),
- pages: [0, 1, 2, 3, 4, 5, 6],
- nbPages: 20,
- currentRefinement: 0,
- isFirstPage: true,
- isLastPage: false,
-};
-
-it('accepts a padding prop', () => {
- __setState({
- ...defaultState,
- });
-
- const wrapper = mount(Pagination, {
- propsData: {
- padding: 5,
- },
- });
-
- expect(wrapper.vm.widgetParams.padding).toBe(5);
-});
-
-it('accepts a totalPages prop', () => {
- __setState({
- ...defaultState,
- });
-
- const wrapper = mount(Pagination, {
- propsData: {
- totalPages: 10,
- },
- });
-
- expect(wrapper.vm.widgetParams.totalPages).toBe(10);
-});
-
-it('renders correctly first page', () => {
- __setState(defaultState);
- const wrapper = mount(Pagination);
-
- expect(wrapper.html()).toMatchSnapshot();
-});
-
-it('renders correctly another page', () => {
- __setState({
- ...defaultState,
- pages: [3, 4, 5, 6, 7, 8, 9],
- nbPages: 20,
- currentRefinement: 6,
- isFirstPage: false,
- isLastPage: false,
- });
- const wrapper = mount(Pagination);
-
- expect(wrapper.html()).toMatchSnapshot();
-});
-
-it('renders correctly last page', () => {
- __setState({
- ...defaultState,
- pages: [3, 4, 5, 6, 7, 8, 9],
- nbPages: 9,
- currentRefinement: 9,
- isFirstPage: false,
- isLastPage: true,
- });
- const wrapper = mount(Pagination);
-
- expect(wrapper.html()).toMatchSnapshot();
-});
-
-it('Moves to the first page on that button', async () => {
- __setState({
- ...defaultState,
- isFirstPage: false,
- });
- const wrapper = mount(Pagination);
-
- const firstPage = wrapper.find(
- '.ais-Pagination-item--firstPage .ais-Pagination-link'
- );
- await firstPage.trigger('click');
- expect(wrapper.vm.state.refine).toHaveBeenLastCalledWith(0);
-});
-
-it('Moves to the next page on that button', async () => {
- const currentRefinement = 5;
- __setState({
- ...defaultState,
- currentRefinement,
- });
- const wrapper = mount(Pagination);
-
- const nextPage = wrapper.find(
- '.ais-Pagination-item--nextPage .ais-Pagination-link'
- );
- await nextPage.trigger('click');
- expect(wrapper.vm.state.refine).toHaveBeenLastCalledWith(
- currentRefinement + 1
- );
-});
-
-it('Moves to the last page on that button', async () => {
- const nbPages = 1000;
- __setState({
- ...defaultState,
- isLastPage: false,
- nbPages,
- });
- const wrapper = mount(Pagination);
-
- const lastPage = wrapper.find(
- '.ais-Pagination-item--lastPage .ais-Pagination-link'
- );
- await lastPage.trigger('click');
- expect(wrapper.vm.state.refine).toHaveBeenLastCalledWith(nbPages - 1);
-});
-
-it('Moves to the previous page on that button', async () => {
- const currentRefinement = 5;
- __setState({
- ...defaultState,
- isFirstPage: false,
- currentRefinement,
- });
- const wrapper = mount(Pagination);
-
- const previousPage = wrapper.find(
- '.ais-Pagination-item--previousPage .ais-Pagination-link'
- );
- await previousPage.trigger('click');
- expect(wrapper.vm.state.refine).toHaveBeenLastCalledWith(
- currentRefinement - 1
- );
-});
-
-it('implements showFirst', async () => {
- __setState({ ...defaultState });
-
- const wrapper = mount(Pagination, {
- propsData: {
- showFirst: false,
- },
- });
-
- expect(wrapper.find('.ais-Pagination-item--firstPage').exists()).toBe(false);
-
- await wrapper.setProps({
- showFirst: true,
- });
-
- expect(wrapper.find('.ais-Pagination-item--firstPage').exists()).toBe(true);
-});
-
-it('implements showPrevious', async () => {
- __setState({ ...defaultState });
-
- const wrapper = mount(Pagination, {
- propsData: {
- showPrevious: false,
- },
- });
-
- expect(wrapper.find('.ais-Pagination-item--previousPage').exists()).toBe(
- false
- );
-
- await wrapper.setProps({
- showPrevious: true,
- });
-
- expect(wrapper.find('.ais-Pagination-item--previousPage').exists()).toBe(
- true
- );
-});
-
-it('implements showLast', async () => {
- __setState({ ...defaultState });
-
- const wrapper = mount(Pagination, {
- propsData: {
- showLast: false,
- },
- });
-
- expect(wrapper.find('.ais-Pagination-item--lastPage').exists()).toBe(false);
-
- await wrapper.setProps({
- showLast: true,
- });
-
- expect(wrapper.find('.ais-Pagination-item--lastPage').exists()).toBe(true);
-});
-
-it('implements showNext', async () => {
- __setState({ ...defaultState });
-
- const wrapper = mount(Pagination, {
- propsData: {
- showNext: false,
- },
- });
-
- expect(wrapper.find('.ais-Pagination-item--nextPage').exists()).toBe(false);
-
- await wrapper.setProps({
- showNext: true,
- });
-
- expect(wrapper.find('.ais-Pagination-item--nextPage').exists()).toBe(true);
-});
diff --git a/packages/vue-instantsearch/src/components/__tests__/__snapshots__/Pagination.js.snap b/packages/vue-instantsearch/src/components/__tests__/__snapshots__/Pagination.js.snap
deleted file mode 100644
index 624de3cd83..0000000000
--- a/packages/vue-instantsearch/src/components/__tests__/__snapshots__/Pagination.js.snap
+++ /dev/null
@@ -1,282 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders correctly another page 1`] = `
-
-`;
-
-exports[`renders correctly first page 1`] = `
-
-`;
-
-exports[`renders correctly last page 1`] = `
-
-`;
diff --git a/tests/common/widgets/pagination/index.ts b/tests/common/widgets/pagination/index.ts
index 438d92e610..1c18766cbf 100644
--- a/tests/common/widgets/pagination/index.ts
+++ b/tests/common/widgets/pagination/index.ts
@@ -1,6 +1,7 @@
import { fakeAct } from '../../common';
import { createOptimisticUiTests } from './optimistic-ui';
+import { createOptionsTests } from './options';
import type { TestOptions, TestSetup } from '../../common';
import type { PaginationWidget } from 'instantsearch.js/es/widgets/pagination/pagination';
@@ -20,5 +21,6 @@ export function createPaginationWidgetTests(
describe('Pagination widget common tests', () => {
createOptimisticUiTests(setup, { act, skippedTests });
+ createOptionsTests(setup, { act, skippedTests });
});
}
diff --git a/tests/common/widgets/pagination/options.ts b/tests/common/widgets/pagination/options.ts
new file mode 100644
index 0000000000..5e1bcdc381
--- /dev/null
+++ b/tests/common/widgets/pagination/options.ts
@@ -0,0 +1,1369 @@
+import {
+ createAlgoliaSearchClient,
+ createMultiSearchResponse,
+ createSingleSearchResponse,
+} from '@instantsearch/mocks';
+import {
+ normalizeSnapshot as commonNormalizeSnapshot,
+ wait,
+} from '@instantsearch/testutils';
+import { screen } from '@testing-library/dom';
+import userEvent from '@testing-library/user-event';
+
+import type { PaginationWidgetSetup } from '.';
+import type { TestOptions } from '../../common';
+import type { MockSearchClient } from '@instantsearch/mocks';
+import type { SearchClient } from 'instantsearch.js';
+
+function normalizeSnapshot(html: string) {
+ // InstantSearch.js uses different text in the page items.
+ // @MAJOR: Standardize page item text between all flavors.
+ return commonNormalizeSnapshot(html).replace('«', '‹‹').replace('»', '››');
+}
+
+export function createOptionsTests(
+ setup: PaginationWidgetSetup,
+ { act }: Required
+) {
+ describe('options', () => {
+ test('renders with default props', async () => {
+ const searchClient = createMockedSearchClient();
+
+ await setup({
+ instantSearchOptions: {
+ indexName: 'indexName',
+ searchClient,
+ },
+ widgetParams: {},
+ });
+
+ await act(async () => {
+ await wait(0);
+ });
+
+ expect(
+ document.querySelectorAll('.ais-Pagination-item--page')
+ ).toHaveLength(7);
+ expect(
+ document.querySelectorAll('.ais-Pagination-item--page')[0]
+ ).toHaveClass('ais-Pagination-item--selected');
+ expect(
+ document.querySelector('.ais-Pagination-item--firstPage')
+ ).toHaveClass('ais-Pagination-item--disabled');
+ expect(
+ document.querySelector('.ais-Pagination-item--previousPage')
+ ).toHaveClass('ais-Pagination-item--disabled');
+ expect(
+ document.querySelector('.ais-Pagination-item--nextPage')
+ ).not.toHaveClass('ais-Pagination-item--disabled');
+ expect(
+ document.querySelector('.ais-Pagination-item--lastPage')
+ ).not.toHaveClass('ais-Pagination-item--disabled');
+
+ expect(
+ document.querySelector('.ais-Pagination')
+ ).toMatchNormalizedInlineSnapshot(
+ normalizeSnapshot,
+ `
+
+ `
+ );
+ });
+
+ test('navigates between pages', async () => {
+ const searchClient = createMockedSearchClient();
+
+ await setup({
+ instantSearchOptions: {
+ indexName: 'indexName',
+ searchClient,
+ },
+ widgetParams: {},
+ });
+
+ await act(async () => {
+ await wait(0);
+ });
+
+ expect(
+ document.querySelectorAll('.ais-Pagination-item--page')
+ ).toHaveLength(7);
+ expect(
+ document.querySelector('.ais-Pagination-item--selected')
+ ).toHaveTextContent('1');
+ expect(
+ document.querySelector('.ais-Pagination')
+ ).toMatchNormalizedInlineSnapshot(
+ normalizeSnapshot,
+ `
+
+ `
+ );
+
+ const firstPageItem = document.querySelector(
+ '.ais-Pagination-item--firstPage'
+ )!;
+ const previousPageItem = document.querySelector(
+ '.ais-Pagination-item--previousPage'
+ )!;
+ const nextPageItem = document.querySelector(
+ '.ais-Pagination-item--nextPage'
+ )!;
+ const lastPageItem = document.querySelector(
+ '.ais-Pagination-item--lastPage'
+ )!;
+
+ searchClient.search.mockClear();
+
+ // We're on page 1, "First" and "Previous" links are disabled
+ expect(firstPageItem).toHaveClass('ais-Pagination-item--disabled');
+ expect(previousPageItem).toHaveClass('ais-Pagination-item--disabled');
+
+ userEvent.click(
+ firstPageItem.querySelector('.ais-Pagination-link')!
+ );
+ userEvent.click(
+ previousPageItem.querySelector(
+ '.ais-Pagination-link'
+ )!
+ );
+
+ await act(async () => {
+ await wait(0);
+ });
+
+ expect(searchClient.search).not.toHaveBeenCalled();
+
+ // We navigate to page 2
+ userEvent.click(screen.getByText('2'));
+
+ await act(async () => {
+ await wait(0);
+ });
+
+ expect(searchClient.search).toHaveBeenLastCalledWith([
+ expect.objectContaining({
+ params: expect.objectContaining({ page: 1 }),
+ }),
+ ]);
+ expect(
+ document.querySelector('.ais-Pagination-item--selected')
+ ).toHaveTextContent('2');
+
+ // We're on page 2, "First" and "Previous" links are no longer disabled
+ expect(firstPageItem).not.toHaveClass('ais-Pagination-item--disabled');
+ expect(previousPageItem).not.toHaveClass('ais-Pagination-item--disabled');
+
+ // We click on "Next" link
+ userEvent.click(
+ nextPageItem.querySelector('.ais-Pagination-link')!
+ );
+
+ await act(async () => {
+ await wait(0);
+ });
+
+ expect(searchClient.search).toHaveBeenLastCalledWith([
+ expect.objectContaining({
+ params: expect.objectContaining({ page: 2 }),
+ }),
+ ]);
+ expect(
+ document.querySelector('.ais-Pagination-item--selected')
+ ).toHaveTextContent('3');
+
+ // We click on "Last" link
+ // InstantSearch.js uses different text in the page items.
+ // @MAJOR: Standardize page item text between all flavors.
+ userEvent.click(
+ lastPageItem.querySelector('.ais-Pagination-link')!
+ );
+
+ await act(async () => {
+ await wait(0);
+ });
+
+ expect(searchClient.search).toHaveBeenLastCalledWith([
+ expect.objectContaining({
+ params: expect.objectContaining({ page: 49 }),
+ }),
+ ]);
+ expect(
+ document.querySelector('.ais-Pagination-item--selected')
+ ).toHaveTextContent('50');
+
+ // We're on the last page, "Next" and "Last" links are disabled
+ expect(nextPageItem).toHaveClass('ais-Pagination-item--disabled');
+ expect(lastPageItem).toHaveClass('ais-Pagination-item--disabled');
+
+ searchClient.search.mockClear();
+
+ userEvent.click(
+ nextPageItem.querySelector('.ais-Pagination-link')!
+ );
+ userEvent.click(
+ lastPageItem.querySelector('.ais-Pagination-link')!
+ );
+
+ await act(async () => {
+ await wait(0);
+ });
+
+ expect(searchClient.search).not.toHaveBeenCalled();
+
+ // We click on "Previous" link
+ userEvent.click(
+ previousPageItem.querySelector(
+ '.ais-Pagination-link'
+ )!
+ );
+
+ await act(async () => {
+ await wait(0);
+ });
+
+ expect(searchClient.search).toHaveBeenLastCalledWith([
+ expect.objectContaining({
+ params: expect.objectContaining({ page: 48 }),
+ }),
+ ]);
+ expect(
+ document.querySelector('.ais-Pagination-item--selected')
+ ).toHaveTextContent('49');
+
+ // We're on page 49, "Next" and "Last" links are no longer disabled
+ expect(nextPageItem).not.toHaveClass('ais-Pagination-item--disabled');
+ expect(lastPageItem).not.toHaveClass('ais-Pagination-item--disabled');
+
+ // We click on "First" link
+ userEvent.click(
+ firstPageItem.querySelector('.ais-Pagination-link')!
+ );
+
+ await act(async () => {
+ await wait(0);
+ });
+
+ expect(searchClient.search).toHaveBeenLastCalledWith([
+ expect.objectContaining({
+ params: expect.objectContaining({ page: 0 }),
+ }),
+ ]);
+ expect(
+ document.querySelector('.ais-Pagination-item--selected')
+ ).toHaveTextContent('1');
+
+ // We're back on page 1, "First" and "Previous" links are disabled
+ expect(firstPageItem).toHaveClass('ais-Pagination-item--disabled');
+ expect(previousPageItem).toHaveClass('ais-Pagination-item--disabled');
+ });
+
+ test('does not navigate when pressing a modifier key', async () => {
+ const searchClient = createMockedSearchClient();
+
+ await setup({
+ instantSearchOptions: {
+ indexName: 'indexName',
+ searchClient,
+ },
+ widgetParams: {},
+ });
+
+ await act(async () => {
+ await wait(0);
+ });
+
+ expect(searchClient.search).toHaveBeenCalledTimes(1);
+
+ searchClient.search.mockClear();
+
+ const firstPageItem = document.querySelector(
+ '.ais-Pagination-item--firstPage'
+ )!;
+ const firstPageLink = firstPageItem.querySelector(
+ '.ais-Pagination-link'
+ )!;
+
+ userEvent.click(firstPageLink, { button: 1 });
+ userEvent.click(firstPageLink, { altKey: true });
+ userEvent.click(firstPageLink, { ctrlKey: true });
+ userEvent.click(firstPageLink, { metaKey: true });
+ userEvent.click(firstPageLink, { shiftKey: true });
+
+ const previousPageItem = document.querySelector(
+ '.ais-Pagination-item--previousPage'
+ )!;
+ const previousPageLink =
+ previousPageItem.querySelector(
+ '.ais-Pagination-link'
+ )!;
+
+ userEvent.click(previousPageLink, { button: 1 });
+ userEvent.click(previousPageLink, { altKey: true });
+ userEvent.click(previousPageLink, { ctrlKey: true });
+ userEvent.click(previousPageLink, { metaKey: true });
+ userEvent.click(previousPageLink, {
+ shiftKey: true,
+ });
+
+ const nextPageItem = document.querySelector(
+ '.ais-Pagination-item--nextPage'
+ );
+ const nextPageLink = nextPageItem!.querySelector(
+ '.ais-Pagination-link'
+ )!;
+
+ userEvent.click(nextPageLink, { button: 1 });
+ userEvent.click(nextPageLink, { altKey: true });
+ userEvent.click(nextPageLink, { ctrlKey: true });
+ userEvent.click(nextPageLink, { metaKey: true });
+ userEvent.click(nextPageLink, { shiftKey: true });
+
+ const lastPageItem = document.querySelector(
+ '.ais-Pagination-item--lastPage'
+ );
+ const lastPageLink = lastPageItem!.querySelector(
+ '.ais-Pagination-link'
+ )!;
+
+ userEvent.click(lastPageLink, { button: 1 });
+ userEvent.click(lastPageLink, { altKey: true });
+ userEvent.click(lastPageLink, { ctrlKey: true });
+ userEvent.click(lastPageLink, { metaKey: true });
+ userEvent.click(lastPageLink, { shiftKey: true });
+
+ const pageOneLink = screen.getByText('1');
+
+ userEvent.click(pageOneLink, { button: 1 });
+ userEvent.click(pageOneLink, { altKey: true });
+ userEvent.click(pageOneLink, { ctrlKey: true });
+ userEvent.click(pageOneLink, { metaKey: true });
+ userEvent.click(pageOneLink, { shiftKey: true });
+
+ expect(searchClient.search).not.toHaveBeenCalled();
+ });
+
+ test('adds items around the current one', async () => {
+ const searchClient = createMockedSearchClient();
+
+ await setup({
+ instantSearchOptions: {
+ indexName: 'indexName',
+ searchClient,
+ },
+ widgetParams: { padding: 4 },
+ });
+
+ await act(async () => {
+ await wait(0);
+ });
+
+ expect(
+ document.querySelectorAll('.ais-Pagination-item--page')
+ ).toHaveLength(9);
+
+ expect(
+ document.querySelector('.ais-Pagination')
+ ).toMatchNormalizedInlineSnapshot(
+ normalizeSnapshot,
+ `
+
+ `
+ );
+ });
+
+ test('does not add items around the current one when there are not enough pages', async () => {
+ const searchClient = createMockedSearchClient({ nbHits: 120 });
+
+ await setup({
+ instantSearchOptions: {
+ indexName: 'indexName',
+ searchClient,
+ },
+ widgetParams: { padding: 4 },
+ });
+
+ await act(async () => {
+ await wait(0);
+ });
+
+ expect(
+ document.querySelectorAll('.ais-Pagination-item--page')
+ ).toHaveLength(6);
+
+ expect(
+ document.querySelector('.ais-Pagination')
+ ).toMatchNormalizedInlineSnapshot(
+ normalizeSnapshot,
+ `
+
+ `
+ );
+ });
+
+ test('limits the total pages to display', async () => {
+ const searchClient = createMockedSearchClient();
+
+ await setup({
+ instantSearchOptions: {
+ indexName: 'indexName',
+ searchClient,
+ },
+ widgetParams: { totalPages: 4 },
+ });
+
+ await act(async () => {
+ await wait(0);
+ });
+
+ expect(
+ document.querySelectorAll('.ais-Pagination-item--page')
+ ).toHaveLength(4);
+
+ expect(
+ document.querySelector('.ais-Pagination')
+ ).toMatchNormalizedInlineSnapshot(
+ normalizeSnapshot,
+ `
+
+ `
+ );
+ });
+
+ test('hides the "First" item when `showFirst` is `false`', async () => {
+ const searchClient = createAlgoliaSearchClient({});
+
+ await setup({
+ instantSearchOptions: {
+ indexName: 'indexName',
+ searchClient,
+ },
+ widgetParams: { showFirst: false },
+ });
+
+ await act(async () => {
+ await wait(0);
+ });
+
+ expect(
+ document.querySelector('.ais-Pagination-item--firstPage')
+ ).toBeNull();
+
+ expect(
+ document.querySelector('.ais-Pagination')
+ ).toMatchNormalizedInlineSnapshot(
+ normalizeSnapshot,
+ `
+
+ `
+ );
+ });
+
+ test('hides the "Previous" item when `showPrevious` is `false`', async () => {
+ const searchClient = createAlgoliaSearchClient({});
+
+ await setup({
+ instantSearchOptions: {
+ indexName: 'indexName',
+ searchClient,
+ },
+ widgetParams: { showPrevious: false },
+ });
+
+ await act(async () => {
+ await wait(0);
+ });
+
+ expect(
+ document.querySelector('.ais-Pagination-item--previousPage')
+ ).toBeNull();
+
+ expect(
+ document.querySelector('.ais-Pagination')
+ ).toMatchNormalizedInlineSnapshot(
+ normalizeSnapshot,
+ `
+
+ `
+ );
+ });
+
+ test('hides the "Next" item when `showNext` is `false`', async () => {
+ const searchClient = createAlgoliaSearchClient({});
+
+ await setup({
+ instantSearchOptions: {
+ indexName: 'indexName',
+ searchClient,
+ },
+ widgetParams: { showNext: false },
+ });
+
+ await act(async () => {
+ await wait(0);
+ });
+
+ expect(
+ document.querySelector('.ais-Pagination-item--nextPage')
+ ).toBeNull();
+
+ expect(
+ document.querySelector('.ais-Pagination')
+ ).toMatchNormalizedInlineSnapshot(
+ normalizeSnapshot,
+ `
+
+ `
+ );
+ });
+
+ test('hides the "Last" item when `showLast` is `false`', async () => {
+ const searchClient = createAlgoliaSearchClient({});
+
+ await setup({
+ instantSearchOptions: {
+ indexName: 'indexName',
+ searchClient,
+ },
+ widgetParams: { showLast: false },
+ });
+
+ await act(async () => {
+ await wait(0);
+ });
+
+ expect(
+ document.querySelector('.ais-Pagination-item--lastPage')
+ ).toBeNull();
+
+ expect(
+ document.querySelector('.ais-Pagination')
+ ).toMatchNormalizedInlineSnapshot(
+ normalizeSnapshot,
+ `
+
+ `
+ );
+ });
+ });
+}
+
+function createMockedSearchClient({ nbHits = 1000 }: { nbHits?: number } = {}) {
+ return createAlgoliaSearchClient({
+ search: jest.fn((requests) =>
+ Promise.resolve(
+ createMultiSearchResponse(
+ ...requests.map(
+ (request: Parameters[0][number]) =>
+ createSingleSearchResponse({
+ hits: Array.from({ length: nbHits }).map((_, index) => ({
+ objectID: String(index),
+ })),
+ index: request.indexName,
+ })
+ )
+ )
+ )
+ ) as MockSearchClient['search'],
+ });
+}
diff --git a/tests/utils/normalizeSnapshot.ts b/tests/utils/normalizeSnapshot.ts
index ab93b30eb5..3fd53aa133 100644
--- a/tests/utils/normalizeSnapshot.ts
+++ b/tests/utils/normalizeSnapshot.ts
@@ -16,8 +16,8 @@ export function normalizeSnapshot(html: string) {
// Vue renders extra whitespace between span elements
.replace(/<\/span> /g, '') // Vue 2
- .replace(/(\s+)?/g, '') // Vue 3
+ .replace(/(\s+)?(\s+)?/g, '') // Vue 2
+ .replace(/(\s+)?(\s+)?/g, '') // Vue 3
// Vue renders extra whitespace after list elements
.replace(/<\/ul> <')
);