From a8056e2c9dc162c84a0e25dd330e0d36d90ec7ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 26 Jan 2021 08:07:45 +0100 Subject: [PATCH] Admin: Fixes so form values are filled in from backend (#30544) * Admin: Fixes so form values are filled in from backend * Chore: tidy up the imports --- .../app/features/users/SignupInvited.test.tsx | 127 ++++++++++++++++++ public/app/features/users/SignupInvited.tsx | 37 ++--- 2 files changed, 139 insertions(+), 25 deletions(-) create mode 100644 public/app/features/users/SignupInvited.test.tsx diff --git a/public/app/features/users/SignupInvited.test.tsx b/public/app/features/users/SignupInvited.test.tsx new file mode 100644 index 00000000000..8d796469230 --- /dev/null +++ b/public/app/features/users/SignupInvited.test.tsx @@ -0,0 +1,127 @@ +import React from 'react'; +import * as redux from 'react-redux'; +import { render, screen, waitFor, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import SignupInvitedPage from './SignupInvited'; +import { backendSrv } from '../../core/services/backend_srv'; + +jest.mock('app/core/core', () => ({ + contextSrv: { + user: { orgName: 'Invited to Org Name' }, + }, +})); + +jest.mock('@grafana/runtime', () => ({ + ...((jest.requireActual('@grafana/runtime') as unknown) as object), + getBackendSrv: () => backendSrv, +})); + +const defaultGet = { + email: 'some.user@localhost', + name: 'Some User', + invitedBy: 'Invited By User', + username: 'someuser', +}; + +async function setupTestContext({ get = defaultGet }: { get?: typeof defaultGet | null } = {}) { + jest.clearAllMocks(); + + const reduxSpy = jest.spyOn(redux, 'useSelector'); + reduxSpy.mockReturnValue('some code'); + + const getSpy = jest.spyOn(backendSrv, 'get'); + getSpy.mockResolvedValue(get); + + const postSpy = jest.spyOn(backendSrv, 'post'); + postSpy.mockResolvedValue([]); + + render(); + + await waitFor(() => expect(getSpy).toHaveBeenCalled()); + expect(getSpy).toHaveBeenCalledTimes(1); + + return { getSpy, postSpy }; +} + +describe('SignupInvitedPage', () => { + describe('when initialized but invite data has not been retrieved yet', () => { + it('then it should not render', async () => { + await setupTestContext({ get: null }); + + expect(screen.queryByText(/email/i)).not.toBeInTheDocument(); + }); + }); + + describe('when initialized and invite data has been retrieved', () => { + it('then the greeting should be correct', async () => { + await setupTestContext(); + + expect( + screen.getByRole('heading', { + name: /hello some user\./i, + }) + ).toBeInTheDocument(); + }); + + it('then the invited by should be correct', async () => { + await setupTestContext(); + + const view = screen.getByText( + /has invited you to join grafana and the organization please complete the following and choose a password to accept your invitation and continue:/i + ); + + expect(within(view).getByText(/invited by user/i)).toBeInTheDocument(); + }); + + it('then the organization invited to should be correct', async () => { + await setupTestContext(); + + const view = screen.getByText( + /has invited you to join grafana and the organization please complete the following and choose a password to accept your invitation and continue:/i + ); + + expect(within(view).getByText(/invited to org name/i)).toBeInTheDocument(); + }); + + it('then the form should include form data', async () => { + await setupTestContext(); + + expect(screen.getByPlaceholderText(/email@example\.com/i)).toHaveValue('some.user@localhost'); + expect(screen.getByPlaceholderText(/name \(optional\)/i)).toHaveValue('Some User'); + expect(screen.getByPlaceholderText(/username/i)).toHaveValue('some.user@localhost'); + expect(screen.getByPlaceholderText(/password/i)).toHaveValue(''); + }); + }); + + describe('when user submits the form and the required fields are not filled in', () => { + it('then required fields should show error messages and nothing should be posted', async () => { + const { postSpy } = await setupTestContext({ get: { email: '', invitedBy: '', name: '', username: '' } }); + + userEvent.click(screen.getByRole('button', { name: /sign up/i })); + + await waitFor(() => expect(screen.getByText(/email is required/i)).toBeInTheDocument()); + expect(screen.getByText(/username is required/i)).toBeInTheDocument(); + expect(screen.getByText(/password is required/i)).toBeInTheDocument(); + expect(postSpy).toHaveBeenCalledTimes(0); + }); + }); + + describe('when user submits the form and the required fields are filled in', () => { + it('then correct form data should be posted', async () => { + const { postSpy } = await setupTestContext(); + + await userEvent.type(screen.getByPlaceholderText(/password/i), 'pass@word1'); + userEvent.click(screen.getByRole('button', { name: /sign up/i })); + + await waitFor(() => expect(postSpy).toHaveBeenCalledTimes(1)); + expect(postSpy).toHaveBeenCalledWith('/api/user/invite/complete', { + email: 'some.user@localhost', + name: 'Some User', + username: 'some.user@localhost', + password: 'pass@word1', + inviteCode: 'some code', + }); + }); + }); +}); diff --git a/public/app/features/users/SignupInvited.tsx b/public/app/features/users/SignupInvited.tsx index 9988e63d392..e1ba3a54f21 100644 --- a/public/app/features/users/SignupInvited.tsx +++ b/public/app/features/users/SignupInvited.tsx @@ -1,23 +1,13 @@ import React, { FC, useState } from 'react'; -import { hot } from 'react-hot-loader'; -import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux'; -import { StoreState } from 'app/types'; -import { updateLocation } from 'app/core/actions'; +import { useSelector } from 'react-redux'; import { getBackendSrv } from '@grafana/runtime'; import { Button, Field, Form, Input } from '@grafana/ui'; import { useAsync } from 'react-use'; + +import { StoreState } from 'app/types'; import Page from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/core'; import { getConfig } from 'app/core/config'; -import { UrlQueryValue } from '@grafana/data'; - -interface ConnectedProps { - code?: UrlQueryValue; -} - -interface DispatchProps { - updateLocation: typeof updateLocation; -} interface FormModel { email: string; @@ -38,12 +28,13 @@ const navModel = { }, }; -const SingupInvitedPageUnconnected: FC = ({ code }) => { +export const SignupInvitedPage: FC = () => { + const code = useSelector((state: StoreState) => state.location.routeParams.code); const [initFormModel, setInitFormModel] = useState(); const [greeting, setGreeting] = useState(); const [invitedBy, setInvitedBy] = useState(); useAsync(async () => { - const invite = await getBackendSrv().get('/api/user/invite/' + code); + const invite = await getBackendSrv().get(`/api/user/invite/${code}`); setInitFormModel({ email: invite.email, name: invite.name, @@ -52,13 +43,17 @@ const SingupInvitedPageUnconnected: FC = ({ code setGreeting(invite.name || invite.email || invite.username); setInvitedBy(invite.invitedBy); - }, []); + }, [code]); const onSubmit = async (formData: FormModel) => { await getBackendSrv().post('/api/user/invite/complete', { ...formData, inviteCode: code }); window.location.href = getConfig().appSubUrl + '/'; }; + if (!initFormModel) { + return null; + } + return ( @@ -110,12 +105,4 @@ const SingupInvitedPageUnconnected: FC = ({ code ); }; -const mapStateToProps: MapStateToProps = (state: StoreState) => ({ - code: state.location.routeParams.code, -}); - -const mapDispatchToProps: MapDispatchToProps = { - updateLocation, -}; - -export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(SingupInvitedPageUnconnected)); +export default SignupInvitedPage;