Router: Modify /share and /imagewizard routing

Insights offers 'quickstarts', which can be used to provide
mini-tutorials in a sidebar.

Unfortunately, these quickstarts change our URL... they add an optional
query parameter related to the quickstart. The process of doing so
destroys our router's `location`, setting it to undefined.

We have been using the location state to store the GUID of the image,
needed when opening the wizard via the `Recreate image` action or when
opening the share modal.

As a workaround, we can simply accept that the quickstarts will change
our URL and destroy our router's location. Instead, we now put the image
id (its UUID) in the route itself. We can access it in the components as
necessary via the useParams hook.
This commit is contained in:
lucasgarfield 2023-04-17 19:00:46 +02:00 committed by Lucas Garfield
parent a81fb72523
commit bc1435994d
12 changed files with 176 additions and 182 deletions

View file

@ -1,12 +1,10 @@
import '@testing-library/jest-dom';
import React from 'react';
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { rest } from 'msw';
import api from '../../../api.js';
import CreateImageWizard from '../../../Components/CreateImageWizard/CreateImageWizard';
import { PROVISIONING_SOURCES_ENDPOINT } from '../../../constants.js';
import { mockRepositoryResults } from '../../fixtures/repositories';
import { server } from '../../mocks/server.js';
@ -73,7 +71,7 @@ describe('Step Upload to Azure', () => {
const user = userEvent.setup();
const setUp = async () => {
renderWithReduxRouter(<CreateImageWizard />);
renderWithReduxRouter('imagewizard', {});
// select aws as upload destination
const azureTile = screen.getByTestId('upload-azure');
azureTile.click();

View file

@ -1,5 +1,4 @@
import '@testing-library/jest-dom';
import React from 'react';
import {
act,
@ -12,7 +11,6 @@ import userEvent from '@testing-library/user-event';
import { rest } from 'msw';
import api from '../../../api.js';
import CreateImageWizard from '../../../Components/CreateImageWizard/CreateImageWizard';
import {
RHEL_8,
RHEL_9,
@ -22,9 +20,24 @@ import { mockRepositoryResults } from '../../fixtures/repositories';
import { server } from '../../mocks/server.js';
import { renderWithReduxRouter } from '../../testUtils';
let history = undefined;
let router = undefined;
let store = undefined;
const mockComposes = {
meta: {
count: 0,
},
data: [],
};
// Mocking getComposes is necessary because in many tests we call navigate()
// to navigate to the images table (via useNavigate hook), which will in turn
// result in a call to getComposes. If it is not mocked, tests fail due to MSW
// being unable to resolve that endpoint.
jest
.spyOn(api, 'getComposes')
.mockImplementation(() => Promise.resolve(mockComposes));
function getBackButton() {
const back = screen.getByRole('button', { name: /Back/ });
return back;
@ -171,7 +184,7 @@ beforeAll(() => {
afterEach(() => {
jest.clearAllMocks();
history = undefined;
router = undefined;
});
// restore global mock
@ -181,7 +194,7 @@ afterAll(() => {
describe('Create Image Wizard', () => {
test('renders component', () => {
renderWithReduxRouter(<CreateImageWizard />);
renderWithReduxRouter('imagewizard', {});
// check heading
screen.getByRole('heading', { name: /Create image/ });
@ -199,9 +212,7 @@ describe('Create Image Wizard', () => {
describe('Step Upload to AWS', () => {
const user = userEvent.setup();
const setUp = () => {
const view = renderWithReduxRouter(<CreateImageWizard />);
history = view.history;
store = view.store;
({ router, store } = renderWithReduxRouter('imagewizard', {}));
// select aws as upload destination
const awsTile = screen.getByTestId('upload-aws');
@ -331,7 +342,7 @@ describe('Step Upload to AWS', () => {
// returns back to the landing page
await waitFor(() =>
expect(history.location.pathname).toBe('/insights/image-builder')
expect(router.state.location.pathname).toBe('/insights/image-builder')
);
expect(store.getState().composes.allIds).toEqual([
'edbae1c2-62bc-42c1-ae0c-3110ab718f5a',
@ -343,7 +354,7 @@ describe('Step Upload to AWS', () => {
describe('Step Packages', () => {
const user = userEvent.setup();
const setUp = async () => {
history = renderWithReduxRouter(<CreateImageWizard />).history;
({ router } = renderWithReduxRouter('imagewizard', {}));
// select aws as upload destination
const awsTile = screen.getByTestId('upload-aws');
@ -605,7 +616,7 @@ describe('Step Packages', () => {
describe('Step Custom repositories', () => {
const user = userEvent.setup();
const setUp = async () => {
history = renderWithReduxRouter(<CreateImageWizard />).history;
({ router } = renderWithReduxRouter('imagewizard', {}));
// select aws as upload destination
const awsTile = screen.getByTestId('upload-aws');
@ -741,9 +752,7 @@ describe('Click through all steps', () => {
.mockImplementation(() => Promise.resolve(mockRepositoryResults));
const setUp = async () => {
const view = renderWithReduxRouter(<CreateImageWizard />);
history = view.history;
store = view.store;
({ router, store } = renderWithReduxRouter('imagewizard', {}));
};
test('with valid values', async () => {
@ -1082,7 +1091,7 @@ describe('Click through all steps', () => {
// returns back to the landing page
await waitFor(() =>
expect(history.location.pathname).toBe('/insights/image-builder')
expect(router.state.location.pathname).toBe('/insights/image-builder')
);
expect(store.getState().composes.allIds).toEqual(ids);
// set test timeout of 10 seconds

View file

@ -1,7 +1,5 @@
import '@testing-library/jest-dom';
import React from 'react';
import {
act,
screen,
@ -12,12 +10,26 @@ import {
import userEvent from '@testing-library/user-event';
import api from '../../../api.js';
import CreateImageWizard from '../../../Components/CreateImageWizard/CreateImageWizard';
import { RHEL_8 } from '../../../constants.js';
import { renderWithReduxRouter } from '../../testUtils';
let history = undefined;
let store = undefined;
let router = undefined;
const mockComposes = {
meta: {
count: 0,
},
data: [],
};
// Mocking getComposes is necessary because in many tests we call navigate()
// to navigate to the images table (via useNavigate hook), which will in turn
// result in a call to getComposes. If it is not mocked, tests fail due to MSW
// being unable to resolve that endpoint.
jest
.spyOn(api, 'getComposes')
.mockImplementation(() => Promise.resolve(mockComposes));
function getBackButton() {
const back = screen.getByRole('button', { name: /Back/ });
@ -34,10 +46,9 @@ function getCancelButton() {
return cancel;
}
function verifyCancelButton(cancel, history) {
function verifyCancelButton(cancel, router) {
cancel.click();
expect(history.location.pathname).toBe('/insights/image-builder');
expect(router.state.location.pathname).toBe('/insights/image-builder');
}
const mockPkgResultAlpha = {
@ -138,7 +149,7 @@ beforeAll(() => {
afterEach(() => {
jest.clearAllMocks();
history = undefined;
router = undefined;
});
// restore global mock
@ -148,7 +159,7 @@ afterAll(() => {
describe('Create Image Wizard', () => {
test('renders component', () => {
renderWithReduxRouter(<CreateImageWizard />);
renderWithReduxRouter('imagewizard', {});
// check heading
screen.getByRole('heading', { name: /Create image/ });
@ -165,7 +176,7 @@ describe('Create Image Wizard', () => {
describe('Step Image output', () => {
const user = userEvent.setup();
const setUp = () => {
history = renderWithReduxRouter(<CreateImageWizard />).history;
({ router } = renderWithReduxRouter('imagewizard', {}));
const imageOutputLink = screen.getByRole('button', {
name: 'Image output',
@ -191,7 +202,7 @@ describe('Step Image output', () => {
setUp();
const cancel = getCancelButton();
verifyCancelButton(cancel, history);
verifyCancelButton(cancel, router);
});
test('target environment is required', () => {
@ -300,7 +311,7 @@ describe('Step Image output', () => {
describe('Step Upload to AWS', () => {
const user = userEvent.setup();
const setUp = () => {
history = renderWithReduxRouter(<CreateImageWizard />).history;
({ router } = renderWithReduxRouter('imagewizard', {}));
// select aws as upload destination
const awsTile = screen.getByTestId('upload-aws');
@ -338,7 +349,7 @@ describe('Step Upload to AWS', () => {
setUp();
const cancel = getCancelButton();
verifyCancelButton(cancel, history);
verifyCancelButton(cancel, router);
});
test('the aws account id fieldis shown and required', () => {
@ -354,7 +365,7 @@ describe('Step Upload to AWS', () => {
describe('Step Upload to Google', () => {
const user = userEvent.setup();
const setUp = () => {
history = renderWithReduxRouter(<CreateImageWizard />).history;
({ router } = renderWithReduxRouter('imagewizard', {}));
// select aws as upload destination
const awsTile = screen.getByTestId('upload-google');
@ -392,7 +403,7 @@ describe('Step Upload to Google', () => {
setUp();
const cancel = getCancelButton();
verifyCancelButton(cancel, history);
verifyCancelButton(cancel, router);
});
test('the google account id field is shown and required', () => {
@ -419,7 +430,7 @@ describe('Step Upload to Google', () => {
describe('Step Upload to Azure', () => {
const user = userEvent.setup();
const setUp = () => {
history = renderWithReduxRouter(<CreateImageWizard />).history;
({ router } = renderWithReduxRouter('imagewizard', {}));
// select aws as upload destination
const awsTile = screen.getByTestId('upload-azure');
@ -468,7 +479,7 @@ describe('Step Upload to Azure', () => {
setUp();
const cancel = getCancelButton();
verifyCancelButton(cancel, history);
verifyCancelButton(cancel, router);
});
test('the azure upload fields are shown and required', () => {
@ -494,7 +505,7 @@ describe('Step Upload to Azure', () => {
describe('Step Registration', () => {
const user = userEvent.setup();
const setUp = async () => {
history = renderWithReduxRouter(<CreateImageWizard />).history;
({ router } = renderWithReduxRouter('imagewizard', {}));
// select aws as upload destination
const awsTile = screen.getByTestId('upload-aws');
@ -533,7 +544,7 @@ describe('Step Registration', () => {
await setUp();
const cancel = getCancelButton();
verifyCancelButton(cancel, history);
verifyCancelButton(cancel, router);
});
test('should allow registering with rhc', async () => {
@ -674,7 +685,7 @@ describe('Step Registration', () => {
describe('Step File system configuration', () => {
const user = userEvent.setup();
const setUp = async () => {
history = renderWithReduxRouter(<CreateImageWizard />).history;
({ router } = renderWithReduxRouter('imagewizard', {}));
// select aws as upload destination
const awsTile = screen.getByTestId('upload-aws');
@ -742,7 +753,7 @@ describe('Step File system configuration', () => {
describe('Step Packages', () => {
const user = userEvent.setup();
const setUp = async () => {
history = renderWithReduxRouter(<CreateImageWizard />).history;
({ router } = renderWithReduxRouter('imagewizard', {}));
// select aws as upload destination
const awsTile = screen.getByTestId('upload-aws');
@ -788,7 +799,7 @@ describe('Step Packages', () => {
await setUp();
const cancel = getCancelButton();
verifyCancelButton(cancel, history);
verifyCancelButton(cancel, router);
});
test('should display search bar and button', async () => {
@ -1128,7 +1139,7 @@ describe('Step Packages', () => {
describe('Step Details', () => {
const user = userEvent.setup();
const setUp = async () => {
history = renderWithReduxRouter(<CreateImageWizard />).history;
({ router } = renderWithReduxRouter('imagewizard', {}));
// select aws as upload destination
const awsTile = screen.getByTestId('upload-aws');
@ -1175,7 +1186,7 @@ describe('Step Details', () => {
describe('Step Review', () => {
const user = userEvent.setup();
const setUp = async () => {
history = renderWithReduxRouter(<CreateImageWizard />).history;
({ router } = renderWithReduxRouter('imagewizard', {}));
// select aws as upload destination
const awsTile = screen.getByTestId('upload-aws');
@ -1206,7 +1217,7 @@ describe('Step Review', () => {
// eslint-disable-next-line no-unused-vars
const setUpCentOS = async () => {
history = renderWithReduxRouter(<CreateImageWizard />).history;
({ router } = renderWithReduxRouter('imagewizard', {}));
const releaseMenu = screen.getByRole('button', {
name: /options menu/i,
@ -1264,7 +1275,7 @@ describe('Step Review', () => {
await setUp();
const cancel = screen.getByRole('button', { name: /Cancel/ });
verifyCancelButton(cancel, history);
verifyCancelButton(cancel, router);
});
test('has Registration expandable section for rhel', async () => {
@ -1312,45 +1323,12 @@ describe('Step Review', () => {
await user.click(fscExpandable);
screen.getByText('Configuration type');
});
test('can pass location to recreate on review step', () => {
const initialLocation = {
state: {
composeRequest: {
distribution: RHEL_8,
image_name: 'MyImageName',
image_requests: [
{
architecture: 'x86_64',
image_type: 'guest-image',
upload_request: {
type: 'aws.s3',
options: {},
},
},
],
customizations: {},
},
initialStep: 'review',
},
};
history = renderWithReduxRouter(
<CreateImageWizard />,
{},
initialLocation
).history;
screen.getByText('Virtualization - Guest image (.qcow2)');
screen.getByText('Register the system later');
screen.getByText('MyImageName');
});
});
describe('Click through all steps', () => {
const user = userEvent.setup();
const setUp = async () => {
const view = renderWithReduxRouter(<CreateImageWizard />);
history = view.history;
store = view.store;
({ router, store } = renderWithReduxRouter('imagewizard', {}));
};
test('with valid values', async () => {
@ -1784,7 +1762,7 @@ describe('Click through all steps', () => {
// returns back to the landing page
await waitFor(() =>
expect(history.location.pathname).toBe('/insights/image-builder')
expect(router.state.location.pathname).toBe('/insights/image-builder')
);
expect(store.getState().composes.allIds).toEqual(ids);
// set test timeout of 10 seconds
@ -1794,12 +1772,7 @@ describe('Click through all steps', () => {
describe('Keyboard accessibility', () => {
const user = userEvent.setup();
const setUp = async () => {
const view = renderWithReduxRouter(
<CreateImageWizard />,
{},
'/imagewizard'
);
history = view.history;
({ router } = renderWithReduxRouter('imagewizard', {}));
};
const clickNext = () => {
@ -1907,7 +1880,7 @@ describe('Keyboard accessibility', () => {
const awsTile = screen.getByTestId('upload-aws');
await user.click(awsTile);
await user.keyboard('{escape}');
expect(history.location.pathname).toBe('/insights/image-builder');
expect(router.state.location.pathname).toBe('/insights/image-builder');
});
test('pressing Enter does not advance the wizard', async () => {

View file

@ -7,7 +7,6 @@ import { BrowserRouter } from 'react-router-dom';
import api from '../../../api.js';
import { ImageBuildStatus } from '../../../Components/ImagesTable/ImageBuildStatus';
import ImageLink from '../../../Components/ImagesTable/ImageLink';
import ImagesTable from '../../../Components/ImagesTable/ImagesTable';
import Target from '../../../Components/ImagesTable/Target';
import '@testing-library/jest-dom';
import { RHEL_8 } from '../../../constants.js';
@ -463,7 +462,7 @@ jest.spyOn(api, 'getCloneStatus').mockImplementation((id) => {
describe('Images Table', () => {
const user = userEvent.setup();
test('render ImagesTable', async () => {
const view = renderWithReduxRouter(<ImagesTable />, {});
const view = renderWithReduxRouter('', {});
const table = await screen.findByTestId('images-table');
@ -541,7 +540,7 @@ describe('Images Table', () => {
});
test('check recreate action', async () => {
const { history } = renderWithReduxRouter(<ImagesTable />, {});
const { router } = renderWithReduxRouter('', {});
// get rows
const table = await screen.findByTestId('images-table');
@ -560,17 +559,13 @@ describe('Images Table', () => {
});
await user.click(recreateButton);
expect(history.location.pathname).toBe(
'/insights/image-builder/imagewizard'
expect(router.state.location.pathname).toBe(
`/insights/image-builder/imagewizard/${imageId}`
);
expect(history.location.state.composeRequest).toStrictEqual(
mockComposes.data.find((c) => c.id === imageId).request
);
expect(history.location.state.initialStep).toBe('review');
});
test('check download compose request action', async () => {
renderWithReduxRouter(<ImagesTable />, {});
renderWithReduxRouter('', {});
// get rows
const table = await screen.findByTestId('images-table');
@ -608,7 +603,7 @@ describe('Images Table', () => {
});
test('check expandable row toggle', async () => {
renderWithReduxRouter(<ImagesTable />, {});
renderWithReduxRouter('', {});
const table = await screen.findByTestId('images-table');
const { getAllByRole } = within(table);
@ -632,7 +627,7 @@ describe('Images Table', () => {
});
test('check error details', async () => {
renderWithReduxRouter(<ImagesTable />, {});
renderWithReduxRouter('', {});
const table = await screen.findByTestId('images-table');
const { getAllByRole } = within(table);
@ -656,7 +651,7 @@ describe('Images Table', () => {
describe('Images Table Toolbar', () => {
test('render toolbar', async () => {
renderWithReduxRouter(<ImagesTable />, {});
renderWithReduxRouter('', {});
await screen.findByTestId('images-table');
// check create image button
@ -670,7 +665,7 @@ describe('Images Table Toolbar', () => {
describe('Clones table', () => {
test('renders clones table', async () => {
const view = renderWithReduxRouter(<ImagesTable />);
const view = renderWithReduxRouter('', {});
const table = await screen.findByTestId('images-table');

View file

@ -1,9 +1,6 @@
import React from 'react';
import { screen } from '@testing-library/react';
import api from '../../../api.js';
import LandingPage from '../../../Components/LandingPage/LandingPage';
import { renderWithReduxRouter } from '../../testUtils';
jest.mock('../../../store/actions/actions', () => {
@ -31,7 +28,7 @@ beforeAll(() => {
describe('Landing Page', () => {
test('renders page heading', async () => {
renderWithReduxRouter(<LandingPage />);
renderWithReduxRouter('', {});
const composeImage = jest.spyOn(api, 'getVersion');
composeImage.mockResolvedValue({ version: '1.0' });
@ -40,7 +37,7 @@ describe('Landing Page', () => {
});
test('renders EmptyState child component', async () => {
renderWithReduxRouter(<LandingPage />);
renderWithReduxRouter('', {});
// check action loads
screen.getByTestId('create-image-action');

View file

@ -1,14 +1,27 @@
import React from 'react';
import '@testing-library/jest-dom';
import { act, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import api from '../../../api.js';
import ShareImageModal from '../../../Components/ShareImageModal/ShareImageModal';
import { RHEL_8 } from '../../../constants.js';
import { renderWithReduxRouter } from '../../testUtils';
beforeAll(() => {
global.insights = {
chrome: {
isBeta: () => {
return false;
},
isProd: () => {
return true;
},
getEnvironment: () => {
return 'prod';
},
},
};
});
const mockComposes = {
count: 1,
allIds: ['1579d95b-8f1d-4982-8c53-8c2afa4ab04c'],
@ -153,20 +166,12 @@ const mockState = {
notifications: [],
};
const mockLocation = {
state: {
composeId: '1579d95b-8f1d-4982-8c53-8c2afa4ab04c',
},
};
let view;
let history;
let store;
const composeId = '1579d95b-8f1d-4982-8c53-8c2afa4ab04c';
describe('Create Share To Regions Modal', () => {
const user = userEvent.setup();
test('validation', async () => {
renderWithReduxRouter(<ShareImageModal />, mockState, mockLocation);
renderWithReduxRouter(`share/${composeId}`, mockState);
const shareButton = screen.getByRole('button', { name: /share/i });
expect(shareButton).toBeDisabled();
@ -198,34 +203,31 @@ describe('Create Share To Regions Modal', () => {
});
test('cancel button redirects to landing page', async () => {
view = renderWithReduxRouter(<ShareImageModal />, mockState, mockLocation);
history = view.history;
const { router } = renderWithReduxRouter(`share/${composeId}`, mockState);
const cancelButton = screen.getByRole('button', { name: /cancel/i });
cancelButton.click();
// returns back to the landing page
await waitFor(() =>
expect(history.location.pathname).toBe('/insights/image-builder')
expect(router.state.location.pathname).toBe('/insights/image-builder')
);
});
test('close button redirects to landing page', async () => {
view = renderWithReduxRouter(<ShareImageModal />, mockState, mockLocation);
history = view.history;
const { router } = renderWithReduxRouter(`share/${composeId}`, mockState);
const closeButton = screen.getByRole('button', { name: /close/i });
closeButton.click();
// returns back to the landing page
await waitFor(() =>
expect(history.location.pathname).toBe('/insights/image-builder')
expect(router.state.location.pathname).toBe('/insights/image-builder')
);
});
test('select options disabled correctly based on status and region', async () => {
renderWithReduxRouter(<ShareImageModal />, mockState, mockLocation);
history = view.history;
renderWithReduxRouter(`share/${composeId}`, mockState);
const selectToggle = screen.getByRole('button', { name: /options menu/i });
// eslint-disable-next-line testing-library/no-unnecessary-act
@ -261,9 +263,10 @@ describe('Create Share To Regions Modal', () => {
});
test('cloning an image results in successful store updates', async () => {
view = renderWithReduxRouter(<ShareImageModal />, mockState, mockLocation);
history = view.history;
store = view.store;
const { router, store } = renderWithReduxRouter(
`share/${composeId}`,
mockState
);
const selectToggle = screen.getByRole('button', { name: /options menu/i });
await user.click(selectToggle);
@ -289,7 +292,7 @@ describe('Create Share To Regions Modal', () => {
// returns back to the landing page
await waitFor(() =>
expect(history.location.pathname).toBe('/insights/image-builder')
expect(router.state.location.pathname).toBe('/insights/image-builder')
);
// Clone has been added to its parent's list of clones