From fb253f90a74f2d9d29ee145315d137fcd980db8f Mon Sep 17 00:00:00 2001 From: Teofanis Papadopulos Date: Fri, 25 Aug 2023 20:16:59 +0100 Subject: [PATCH] feat: add `preserveQuery` option to Wizard --- README.md | 1 + __tests__/components/Wizard.spec.jsx | 126 +++++++++++++++++++++++++++ src/components/Wizard.js | 16 +++- 3 files changed, 141 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 92291c6..c07c804 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,7 @@ A higher order component that adds [`context.wizard`](#contextwizard) as a `wiza * `step` (object): Describes the current step with structure: `{ id: string }`. * `steps` (array): Array of `step` objects in the order they were declared within ``. * `history` (object): The backing [`history`](https://github.com/ReactTraining/history#properties) object. +* `preserveQuery` (boolean): Whether or not to preserve the query string when navigating between steps. * `next()` (function): Moves to the next step in order. * `previous()` (function): Moves to the previous step in order. * `go(n)` (function): Moves `n` steps in history. diff --git a/__tests__/components/Wizard.spec.jsx b/__tests__/components/Wizard.spec.jsx index 69c3619..86db13b 100644 --- a/__tests__/components/Wizard.spec.jsx +++ b/__tests__/components/Wizard.spec.jsx @@ -203,4 +203,130 @@ describe('Wizard', () => { mounted.unmount(); }); }); + + describe('with existing history and preserving search params', () => { + let wizard; + let mounted; + + const mockReplace = jest.fn(); + const mockPush = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('initially at /gryffindor with ?foo=bar', () => { + const history = { + replace: mockReplace, + listen: () => () => null, + location: { + pathname: '/gryffindor', + search: '?foo=bar', + }, + }; + + beforeEach(() => { + mounted = mount( + + + {prop => { + wizard = prop; + return null; + }} + + + +
+ + +
+ + +
+ + + + ); + }); + + it('should preserve query when calling next', () => { + wizard.history.push = mockPush; + wizard.next(); + expect(mockPush).toBeCalledWith({ pathname: '/slytherin', search: '?foo=bar' }); + }); + + it('should preserve query when calling replace', () => { + wizard.replace('hufflepuff'); + expect(mockReplace).toBeCalledWith({ pathname: '/hufflepuff', search: '?foo=bar' }); + }); + + it('should produce the correct URL string when preserving search params', () => { + wizard.replace('hufflepuff'); + const callArgs = mockReplace.mock.calls[0][0]; + const actualURL = `${callArgs.pathname}${callArgs.search}`; + expect(actualURL).toBe('/hufflepuff?foo=bar'); + }); + + it('should not add search params if none existed initially when calling push', () => { + history.location.search = ''; + wizard.push('hufflepuff'); + expect(mockPush).toBeCalledWith({ pathname: '/hufflepuff', search: '' }); + }); + }); + + describe('initially at /slytherin with ?quidditch=true', () => { + const history = { + replace: mockReplace, + listen: () => () => null, + location: { + pathname: '/slytherin', + search: '?quidditch=true', + }, + }; + + beforeEach(() => { + mounted = mount( + + + {prop => { + wizard = prop; + return null; + }} + + + +
+ + +
+ + +
+ + + + ); + }); + + it('should preserve query when calling next', () => { + wizard.history.push = jest.fn(); + wizard.next(); + expect(wizard.history.push).toBeCalledWith({ + pathname: '/hufflepuff', + search: '?quidditch=true', + }); + }); + + it('should produce the correct URL string when preserving search params', () => { + wizard.replace('gryffindor'); + const callArgs = mockReplace.mock.calls[0][0]; + const actualURL = `${callArgs.pathname}${callArgs.search}`; + expect(actualURL).toBe('/gryffindor?quidditch=true'); + }); + }); + + afterEach(() => { + mounted.unmount(); + }); + }); }); diff --git a/src/components/Wizard.js b/src/components/Wizard.js index 6fc076d..eba1b62 100644 --- a/src/components/Wizard.js +++ b/src/components/Wizard.js @@ -97,9 +97,19 @@ class Wizard extends Component { }); }; - set = step => this.history.push(`${this.basename}${step}`); + constructPath = step => { + if (this.props.preserveQuery) { + return { + ...this.history.location, + pathname: `${this.basename}${step}`, + }; + } + return `${this.basename}${step}`; + }; + push = (step = this.nextStep) => this.set(step); - replace = (step = this.nextStep) => this.history.replace(`${this.basename}${step}`); + set = step => this.history.push(this.constructPath(step)); + replace = (step = this.nextStep) => this.history.replace(this.constructPath(step)); pushPrevious = (step = this.previousStep) => this.set(step); next = () => { @@ -122,6 +132,7 @@ class Wizard extends Component { Wizard.propTypes = { basename: PropTypes.string, + preserveQuery: PropTypes.bool, history: PropTypes.shape({ // disabling due to lost context // eslint-disable-next-line react/forbid-prop-types @@ -141,6 +152,7 @@ Wizard.propTypes = { Wizard.defaultProps = { basename: '', + preserveQuery: false, history: null, onNext: null, render: null,