@@ -23,27 +23,30 @@ import { mockClusterSysInfo, mockIdentityProvider, mockUser } from '../../helper | |||
import { IdentityProvider, Paging, SysInfoCluster } from '../../types/types'; | |||
import { User } from '../../types/users'; | |||
import { getSystemInfo } from '../system'; | |||
import { getIdentityProviders, searchUsers } from '../users'; | |||
import { createUser, getIdentityProviders, searchUsers } from '../users'; | |||
const DEFAULT_USERS = [ | |||
mockUser({ | |||
managed: true, | |||
login: 'bob.marley', | |||
name: 'Bob Marley', | |||
}), | |||
mockUser({ | |||
managed: false, | |||
login: 'alice.merveille', | |||
name: 'Alice Merveille', | |||
}), | |||
]; | |||
export default class UsersServiceMock { | |||
isManaged = true; | |||
users = [ | |||
mockUser({ | |||
managed: true, | |||
login: 'bob.marley', | |||
name: 'Bob Marley', | |||
}), | |||
mockUser({ | |||
managed: false, | |||
login: 'alice.merveille', | |||
name: 'Alice Merveille', | |||
}), | |||
]; | |||
users = cloneDeep(DEFAULT_USERS); | |||
constructor() { | |||
jest.mocked(getSystemInfo).mockImplementation(this.handleGetSystemInfo); | |||
jest.mocked(getIdentityProviders).mockImplementation(this.handleGetIdentityProviders); | |||
jest.mocked(searchUsers).mockImplementation((p) => this.handleSearchUsers(p)); | |||
jest.mocked(createUser).mockImplementation(this.handleCreateUser); | |||
} | |||
setIsManaged(managed: boolean) { | |||
@@ -67,6 +70,26 @@ export default class UsersServiceMock { | |||
return this.reply({ paging, users: this.users }); | |||
}; | |||
handleCreateUser = (data: { | |||
email?: string; | |||
local?: boolean; | |||
login: string; | |||
name: string; | |||
password?: string; | |||
scmAccount: string[]; | |||
}) => { | |||
const { email, local, login, name, scmAccount } = data; | |||
const newUser = mockUser({ | |||
email, | |||
local, | |||
login, | |||
name, | |||
scmAccounts: scmAccount, | |||
}); | |||
this.users.push(newUser); | |||
return this.reply(undefined); | |||
}; | |||
handleGetIdentityProviders = (): Promise<{ identityProviders: IdentityProvider[] }> => { | |||
return this.reply({ identityProviders: [mockIdentityProvider()] }); | |||
}; | |||
@@ -87,6 +110,11 @@ export default class UsersServiceMock { | |||
); | |||
}; | |||
reset = () => { | |||
this.isManaged = true; | |||
this.users = cloneDeep(DEFAULT_USERS); | |||
}; | |||
reply<T>(response: T): Promise<T> { | |||
return Promise.resolve(cloneDeep(response)); | |||
} |
@@ -102,10 +102,6 @@ button::-moz-focus-inner { | |||
padding: 0; | |||
} | |||
legend { | |||
color: #000; | |||
} | |||
pre, | |||
code, | |||
kbd, |
@@ -20,7 +20,7 @@ | |||
import userEvent from '@testing-library/user-event'; | |||
import * as React from 'react'; | |||
import { byRole, byText } from 'testing-library-selector'; | |||
import { byLabelText, byRole, byText } from 'testing-library-selector'; | |||
import UsersServiceMock from '../../../api/mocks/UsersServiceMock'; | |||
import { renderApp } from '../../../helpers/testReactTestingUtils'; | |||
import UsersApp from '../UsersApp'; | |||
@@ -47,8 +47,25 @@ const ui = { | |||
bobUpdateGroupButton: byRole('button', { name: 'users.update_users_groups.bob.marley' }), | |||
bobUpdateButton: byRole('button', { name: 'users.manage_user.bob.marley' }), | |||
bobRow: byRole('row', { name: 'BM Bob Marley bob.marley never' }), | |||
loginInput: byRole('textbox', { name: /login/ }), | |||
userNameInput: byRole('textbox', { name: /name/ }), | |||
passwordInput: byLabelText(/password/), | |||
scmAddButton: byRole('button', { name: 'add_verb' }), | |||
createUserDialogButton: byRole('button', { name: 'create' }), | |||
dialogSCMInputs: byRole('textbox', { name: /users.create_user.scm_account/ }), | |||
dialogSCMInput: (value?: string) => | |||
byRole('textbox', { name: `users.create_user.scm_account_${value ? `x.${value}` : 'new'}` }), | |||
deleteSCMButton: (value?: string) => | |||
byRole('button', { | |||
name: `remove_x.users.create_user.scm_account_${value ? `x.${value}` : 'new'}`, | |||
}), | |||
jackRow: byRole('row', { name: /Jack/ }), | |||
}; | |||
afterAll(() => { | |||
handler.reset(); | |||
}); | |||
describe('in non managed mode', () => { | |||
beforeEach(() => { | |||
handler.setIsManaged(false); | |||
@@ -59,6 +76,23 @@ describe('in non managed mode', () => { | |||
expect(await ui.description.find()).toBeInTheDocument(); | |||
expect(ui.createUserButton.get()).toBeEnabled(); | |||
await userEvent.click(ui.createUserButton.get()); | |||
await userEvent.type(ui.loginInput.get(), 'Login'); | |||
await userEvent.type(ui.userNameInput.get(), 'Jack'); | |||
await userEvent.type(ui.passwordInput.get(), 'Password'); | |||
// Add SCM account | |||
expect(ui.dialogSCMInputs.queryAll()).toHaveLength(0); | |||
await userEvent.click(ui.scmAddButton.get()); | |||
expect(ui.dialogSCMInputs.getAll()).toHaveLength(1); | |||
await userEvent.type(ui.dialogSCMInput().get(), 'SCM'); | |||
expect(ui.dialogSCMInput('SCM').get()).toBeInTheDocument(); | |||
// Remove SCM account | |||
await userEvent.click(ui.deleteSCMButton('SCM').get()); | |||
expect(ui.dialogSCMInputs.queryAll()).toHaveLength(0); | |||
await userEvent.click(ui.createUserDialogButton.get()); | |||
expect(ui.jackRow.get()).toBeInTheDocument(); | |||
}); | |||
it("should be able to add/remove user's group", async () => { |
@@ -240,21 +240,23 @@ export default class UserForm extends React.PureComponent<Props, State> { | |||
</div> | |||
)} | |||
<div className="modal-field"> | |||
<label>{translate('my_profile.scm_accounts')}</label> | |||
{this.state.scmAccounts.map((scm, idx) => ( | |||
<UserScmAccountInput | |||
idx={idx} | |||
key={idx} | |||
onChange={this.handleUpdateScmAccount} | |||
onRemove={this.handleRemoveScmAccount} | |||
scmAccount={scm} | |||
/> | |||
))} | |||
<div className="spacer-bottom"> | |||
<Button className="js-scm-account-add" onClick={this.handleAddScmAccount}> | |||
{translate('add_verb')} | |||
</Button> | |||
</div> | |||
<fieldset> | |||
<legend>{translate('my_profile.scm_accounts')}</legend> | |||
{this.state.scmAccounts.map((scm, idx) => ( | |||
<UserScmAccountInput | |||
idx={idx} | |||
key={idx} | |||
onChange={this.handleUpdateScmAccount} | |||
onRemove={this.handleRemoveScmAccount} | |||
scmAccount={scm} | |||
/> | |||
))} | |||
<div className="spacer-bottom"> | |||
<Button className="js-scm-account-add" onClick={this.handleAddScmAccount}> | |||
{translate('add_verb')} | |||
</Button> | |||
</div> | |||
</fieldset> | |||
<p className="note">{translate('user.login_or_email_used_as_scm_account')}</p> | |||
</div> | |||
</div> |
@@ -19,6 +19,7 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import { DeleteButton } from '../../../components/controls/buttons'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
export interface Props { | |||
idx: number; | |||
@@ -27,23 +28,30 @@ export interface Props { | |||
onRemove: (idx: number) => void; | |||
} | |||
export default class UserScmAccountInput extends React.PureComponent<Props> { | |||
handleChange = (event: React.SyntheticEvent<HTMLInputElement>) => | |||
this.props.onChange(this.props.idx, event.currentTarget.value); | |||
export default function UserScmAccountInput(props: Props) { | |||
const { idx, scmAccount } = props; | |||
handleRemove = () => this.props.onRemove(this.props.idx); | |||
const inputAriaLabel = scmAccount.trim() | |||
? translateWithParameters('users.create_user.scm_account_x', scmAccount) | |||
: translate('users.create_user.scm_account_new'); | |||
render() { | |||
return ( | |||
<div className="js-scm-account display-flex-row spacer-bottom"> | |||
<input | |||
maxLength={255} | |||
onChange={this.handleChange} | |||
type="text" | |||
value={this.props.scmAccount} | |||
/> | |||
<DeleteButton onClick={this.handleRemove} /> | |||
</div> | |||
); | |||
} | |||
return ( | |||
<div className="js-scm-account display-flex-row spacer-bottom"> | |||
<input | |||
maxLength={255} | |||
onChange={(event) => { | |||
props.onChange(idx, event.currentTarget.value); | |||
}} | |||
type="text" | |||
aria-label={inputAriaLabel} | |||
value={scmAccount} | |||
/> | |||
<DeleteButton | |||
aria-label={translateWithParameters('remove_x', inputAriaLabel)} | |||
onClick={() => { | |||
props.onRemove(idx); | |||
}} | |||
/> | |||
</div> | |||
); | |||
} |
@@ -68,19 +68,21 @@ exports[`should render correctly 1`] = ` | |||
<div | |||
className="modal-field" | |||
> | |||
<label> | |||
my_profile.scm_accounts | |||
</label> | |||
<div | |||
className="spacer-bottom" | |||
> | |||
<Button | |||
className="js-scm-account-add" | |||
onClick={[Function]} | |||
<fieldset> | |||
<legend> | |||
my_profile.scm_accounts | |||
</legend> | |||
<div | |||
className="spacer-bottom" | |||
> | |||
add_verb | |||
</Button> | |||
</div> | |||
<Button | |||
className="js-scm-account-add" | |||
onClick={[Function]} | |||
> | |||
add_verb | |||
</Button> | |||
</div> | |||
</fieldset> | |||
<p | |||
className="note" | |||
> | |||
@@ -218,19 +220,21 @@ exports[`should render correctly 2`] = ` | |||
<div | |||
className="modal-field" | |||
> | |||
<label> | |||
my_profile.scm_accounts | |||
</label> | |||
<div | |||
className="spacer-bottom" | |||
> | |||
<Button | |||
className="js-scm-account-add" | |||
onClick={[Function]} | |||
<fieldset> | |||
<legend> | |||
my_profile.scm_accounts | |||
</legend> | |||
<div | |||
className="spacer-bottom" | |||
> | |||
add_verb | |||
</Button> | |||
</div> | |||
<Button | |||
className="js-scm-account-add" | |||
onClick={[Function]} | |||
> | |||
add_verb | |||
</Button> | |||
</div> | |||
</fieldset> | |||
<p | |||
className="note" | |||
> |
@@ -165,7 +165,7 @@ recommended=Recommended | |||
refresh=Refresh | |||
reload=Reload | |||
remove=Remove | |||
remove_x=Remove {x} | |||
remove_x=Remove {0} | |||
rename=Rename | |||
replaces=Replaces | |||
reset_verb=Reset | |||
@@ -4357,6 +4357,8 @@ users.delete_user.help=A user account cannot be reactivated once their personal | |||
users.delete_user.help.link=Learn more | |||
users.delete_user.documentation=Authentication | |||
users.create_user=Create User | |||
users.create_user.scm_account_new=New SCM account | |||
users.create_user.scm_account_x=SCM account '{0}' | |||
users.update_user=Update User | |||
users.cannot_update_delegated_user=You cannot update the name and email of this user, as it is controlled by an external identity provider. | |||
users.minimum_x_characters=Minimum {0} characters |