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;