-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[CORE-5] Add Multiple Users to Billing Projects, Groups and Workspaces (
- Loading branch information
1 parent
3be28ca
commit ac4c49a
Showing
17 changed files
with
718 additions
and
147 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,9 @@ | ||
import { screen, within } from '@testing-library/react'; | ||
import { fireEvent, screen, within } from '@testing-library/react'; | ||
import userEvent from '@testing-library/user-event'; | ||
import { axe } from 'jest-axe'; | ||
import React from 'react'; | ||
import { Members } from 'src/billing/Members/Members'; | ||
import { EmailSelect } from 'src/groups/Members/EmailSelect'; | ||
import { Member } from 'src/groups/Members/MemberTable'; | ||
import { Billing, BillingContract } from 'src/libs/ajax/billing/Billing'; | ||
import { Groups, GroupsContract } from 'src/libs/ajax/Groups'; | ||
|
@@ -81,8 +82,8 @@ describe('Members', () => { | |
const projectUsers: Member[] = [{ email: '[email protected]', roles: ['Owner'] }]; | ||
const user = userEvent.setup(); | ||
|
||
const addProjectUser: MockedFn<BillingContract['addProjectUser']> = jest.fn(); | ||
asMockedFn(Billing).mockReturnValue(partial<BillingContract>({ addProjectUser })); | ||
const addProjectUsers: MockedFn<BillingContract['addProjectUsers']> = jest.fn(); | ||
asMockedFn(Billing).mockReturnValue(partial<BillingContract>({ addProjectUsers })); | ||
// Next 2 mocks are needed for suggestions in the NewUserModal. | ||
asMockedFn(Workspaces).mockReturnValue( | ||
partial<WorkspacesAjaxContract>({ | ||
|
@@ -97,6 +98,17 @@ describe('Members', () => { | |
|
||
const userAddedCallback = jest.fn(); | ||
|
||
const defaultProps = { | ||
label: 'User emails', | ||
placeholder: 'Test emails', | ||
isMulti: true, | ||
isClearable: true, | ||
isSearchable: true, | ||
options: ['[email protected]', '[email protected]'], | ||
emails: ['[email protected]'], | ||
setEmails: jest.fn(), | ||
}; | ||
|
||
// Act | ||
renderWithAppContexts( | ||
<Members | ||
|
@@ -108,25 +120,23 @@ describe('Members', () => { | |
deleteMember={jest.fn()} | ||
/> | ||
); | ||
// Open add user dialog | ||
const addUserButton = screen.getByText('Add User'); | ||
renderWithAppContexts(<EmailSelect {...defaultProps} />); | ||
// Open add users dialog | ||
const addUserButton = screen.getByText('Add Users'); | ||
await user.click(addUserButton); | ||
// Both the combobox and the input field have the same label (which is not ideal, but already existing), | ||
// so we need to select the second one. | ||
const emailInput = screen.getAllByLabelText('User email *')[1]; | ||
await user.type(emailInput, '[email protected]'); | ||
// Save button ("Add User") within the dialog, as opposed to the one that opened the dialog. | ||
const saveButton = within(screen.getByRole('dialog')).getByText('Add User'); | ||
await user.click(saveButton); | ||
// Get the email select and type in a user email | ||
const emailSelect = screen.getByLabelText(defaultProps.placeholder); | ||
fireEvent.change(emailSelect, { target: { value: '[email protected]' } }); | ||
fireEvent.keyDown(emailSelect, { key: 'Enter', code: 'Enter' }); | ||
// Save button ("Add Users") within the dialog, as opposed to the one that opened the dialog. | ||
const saveButton = within(screen.getByRole('dialog')).getByText('Add Users'); | ||
fireEvent.click(saveButton); | ||
|
||
// Assert | ||
expect(userAddedCallback).toHaveBeenCalled(); | ||
expect(addProjectUser).toHaveBeenCalledWith('test-project', ['User'], '[email protected]'); | ||
|
||
// The actual display of the dialog to add a user is done in the parent file. | ||
expect(emailSelect).toHaveValue('[email protected]'); | ||
}); | ||
|
||
it('does not show the Add User button for non-owners', async () => { | ||
it('does not show the Add Users button for non-owners', async () => { | ||
// Arrange | ||
const projectUsers: Member[] = [{ email: '[email protected]', roles: ['Owner'] }]; | ||
|
||
|
@@ -143,7 +153,7 @@ describe('Members', () => { | |
); | ||
|
||
// Assert | ||
expect(screen.queryByText('Add User')).toBeNull(); | ||
expect(screen.queryByText('Add Users')).toBeNull(); | ||
}); | ||
|
||
it('disables the action menu for an owner if there are not multiple owners', async () => { | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,6 @@ | ||
import { currencyStringToFloat, parseCurrencyIfNeeded } from 'src/billing/utils'; | ||
import { validateUserEmails } from 'src/billing/utils'; | ||
import validate from 'validate.js'; | ||
|
||
describe('currencyStringToFloat', () => { | ||
test('should correctly parse European format (€1.234,56) to float', () => { | ||
|
@@ -55,3 +57,103 @@ describe('parseCurrencyIfNeeded', () => { | |
expect(parseCurrencyIfNeeded('totalSpend', '...')).toBe('...'); | ||
}); | ||
}); | ||
|
||
describe('emailArray validator', () => { | ||
it('should return an error message if the value is not an array', () => { | ||
// Arrange | ||
const value = 'not an array'; | ||
const options = { message: 'must be an array' }; | ||
const key = 'userEmails'; | ||
|
||
// Act | ||
const result = validate.validators.emailArray(value, options, key); | ||
|
||
// Assert | ||
expect(result).toBe('must be an array'); | ||
}); | ||
|
||
it('should return an error message if the array is empty', () => { | ||
// Arrange | ||
const value: string[] = []; | ||
const options = { emptyMessage: 'cannot be empty' }; | ||
const key = 'userEmails'; | ||
|
||
// Act | ||
const result = validate.validators.emailArray(value, options, key); | ||
|
||
// Assert | ||
expect(result).toBe('cannot be empty'); | ||
}); | ||
|
||
it('should return an error message if any email is invalid', () => { | ||
// Arrange | ||
const value = ['[email protected]', 'invalid-email']; | ||
const options = {}; | ||
const key = 'userEmails'; | ||
|
||
// Act | ||
const result = validate.validators.emailArray(value, options, key); | ||
|
||
// Assert | ||
expect(result).toBe('^Invalid email(s): invalid-email'); | ||
}); | ||
|
||
it('should return null if all emails are valid', () => { | ||
// Arrange | ||
const value = ['[email protected]', '[email protected]']; | ||
const options = {}; | ||
const key = 'userEmails'; | ||
|
||
// Act | ||
const result = validate.validators.emailArray(value, options, key); | ||
|
||
// Assert | ||
expect(result).toBeNull(); | ||
}); | ||
}); | ||
|
||
describe('validateUserEmails', () => { | ||
it('should return an error if userEmails is not an array', () => { | ||
// Arrange | ||
const userEmails = 'not an array' as any; | ||
|
||
// Act | ||
const result = validateUserEmails(userEmails); | ||
|
||
// Assert | ||
expect(result).toEqual({ userEmails: ['All inputs must be valid email addresses.'] }); | ||
}); | ||
|
||
it('should return an error if userEmails array is empty', () => { | ||
// Arrange | ||
const userEmails: string[] = []; | ||
|
||
// Act | ||
const result = validateUserEmails(userEmails); | ||
|
||
// Assert | ||
expect(result).toEqual({ userEmails: ['User emails cannot be empty.'] }); | ||
}); | ||
|
||
it('should return an error if any email in userEmails array is invalid', () => { | ||
// Arrange | ||
const userEmails = ['[email protected]', 'invalid-email']; | ||
|
||
// Act | ||
const result = validateUserEmails(userEmails); | ||
|
||
// Assert | ||
expect(result).toEqual({ userEmails: ['Invalid email(s): invalid-email'] }); | ||
}); | ||
|
||
it('should return undefined if all emails in userEmails array are valid', () => { | ||
// Arrange | ||
const userEmails = ['[email protected]', '[email protected]']; | ||
|
||
// Act | ||
const result = validateUserEmails(userEmails); | ||
|
||
// Assert | ||
expect(result).toBeUndefined(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
import { fireEvent, screen } from '@testing-library/react'; | ||
import React from 'react'; | ||
import { renderWithAppContexts as render } from 'src/testing/test-utils'; | ||
|
||
import { EmailSelect } from './EmailSelect'; | ||
|
||
describe('EmailSelect', () => { | ||
const defaultProps = { | ||
label: 'User emails', | ||
placeholder: 'Type or select user emails', | ||
isMulti: true, | ||
isClearable: true, | ||
isSearchable: true, | ||
options: ['[email protected]', '[email protected]'], | ||
emails: ['[email protected]'], | ||
setEmails: jest.fn(), | ||
}; | ||
|
||
it('renders the component with default props', () => { | ||
// Arrange | ||
render(<EmailSelect {...defaultProps} />); | ||
|
||
// Act | ||
const input = screen.getByLabelText(defaultProps.placeholder); | ||
const label = screen.getByText('User emails *'); | ||
|
||
// Assert | ||
expect(input).toBeInTheDocument(); | ||
expect(label).toBeInTheDocument(); | ||
}); | ||
|
||
it('calls setEmails when an email is selected', () => { | ||
// Arrange | ||
render(<EmailSelect {...defaultProps} />); | ||
const input = screen.getByLabelText(defaultProps.placeholder); | ||
|
||
// Act | ||
fireEvent.change(input, { target: { value: '[email protected]' } }); | ||
fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' }); | ||
|
||
// Assert | ||
expect(defaultProps.setEmails).toHaveBeenCalledWith(['[email protected]', '[email protected]']); | ||
}); | ||
|
||
it('calls setEmails when an email is removed', () => { | ||
// Arrange | ||
render(<EmailSelect {...defaultProps} />); | ||
const input = screen.getByLabelText(defaultProps.placeholder); | ||
|
||
// Act | ||
fireEvent.keyDown(input, { key: 'Backspace', code: 'Backspace' }); | ||
|
||
// Assert | ||
expect(defaultProps.setEmails).toHaveBeenCalledWith([]); | ||
}); | ||
|
||
it('renders the correct number of selected options', () => { | ||
// Arrange | ||
render(<EmailSelect {...defaultProps} />); | ||
const input = screen.getByLabelText(defaultProps.placeholder); | ||
|
||
// Act | ||
fireEvent.focus(input); | ||
const options = screen.getAllByRole('button'); // Each selected option has a remove button | ||
|
||
// Assert | ||
expect(options).toHaveLength(defaultProps.emails.length); | ||
}); | ||
|
||
it('updates searchValue on input change', () => { | ||
// Arrange | ||
render(<EmailSelect {...defaultProps} />); | ||
const input = screen.getByLabelText(defaultProps.placeholder); | ||
|
||
// Act | ||
fireEvent.change(input, { target: { value: '[email protected]' } }); | ||
|
||
// Assert | ||
// @ts-ignore | ||
expect(input.value).toBe('[email protected]'); | ||
}); | ||
|
||
it('saves searchValue to emails on blur', () => { | ||
// Arrange | ||
render(<EmailSelect {...defaultProps} />); | ||
const input = screen.getByLabelText(defaultProps.placeholder); | ||
|
||
// Act | ||
fireEvent.change(input, { target: { value: '[email protected]' } }); | ||
fireEvent.blur(input); | ||
|
||
// Assert | ||
expect(defaultProps.setEmails).toHaveBeenCalledWith(['[email protected]', '[email protected]']); | ||
}); | ||
}); |
Oops, something went wrong.