fix(settings): Stablize user list cypress tests

* Use common `data-testid` to identify elements rather than to depend on internal classes or properties
* Force clean the state for the user tests
* Move leftover acceptance tests for users from drone to cypress

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
pull/41021/head
Ferdinand Thiessen 7 months ago
parent 888473f5e2
commit 5b0c27b6da
No known key found for this signature in database
GPG Key ID: 45FAE7268762B400

@ -1621,36 +1621,6 @@ trigger:
- pull_request
- push
---
kind: pipeline
name: acceptance-users
steps:
- name: submodules
image: ghcr.io/nextcloud/continuous-integration-alpine-git:latest
commands:
- git submodule update --init
- name: acceptance-users
image: ghcr.io/nextcloud/continuous-integration-acceptance-php8.0:latest
commands:
- tests/acceptance/run-local.sh --timeout-multiplier 10 --nextcloud-server-domain acceptance-users --selenium-server selenium:4444 allow-git-repository-modifications features/users.feature
services:
- name: selenium
image: ghcr.io/nextcloud/continuous-integration-selenium:3.141.59
environment:
# Reduce default log level for Selenium server (INFO) as it is too
# verbose.
JAVA_OPTS: -Dselenium.LOGGER.level=WARNING
trigger:
branch:
- master
- stable*
event:
- pull_request
- push
---
kind: pipeline
name: acceptance-apps

@ -23,7 +23,7 @@ jobs:
steps:
- name: Checkout app
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v3.5.2
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- name: Check composer.json
id: check_composer
@ -39,8 +39,8 @@ jobs:
uses: skjnldsv/read-package-engines-version-actions@8205673bab74a63eb9b8093402fd9e0e018663a1 # v2.2
id: versions
with:
fallbackNode: "^14"
fallbackNpm: "^7"
fallbackNode: "^20"
fallbackNpm: "^9"
- name: Set up node ${{ steps.versions.outputs.nodeVersion }}
uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1

@ -45,7 +45,7 @@
:data-component="UserRow"
:data-sources="filteredUsers"
data-key="id"
data-test-id="userList"
data-cy-user-list
:item-height="rowHeight"
:style="style"
:extra-props="{

@ -23,12 +23,14 @@
<template>
<tr class="header">
<th class="header__cell header__cell--avatar"
data-cy-user-list-header-avatar
scope="col">
<span class="hidden-visually">
{{ t('settings', 'Avatar') }}
</span>
</th>
<th class="header__cell header__cell--displayname"
data-cy-user-list-header-displayname
scope="col">
<strong>
{{ t('settings', 'Display name') }}
@ -39,33 +41,40 @@
</th>
<th class="header__cell"
:class="{ 'header__cell--obfuscated': hasObfuscated }"
data-cy-user-list-header-password
scope="col">
<span>{{ passwordLabel }}</span>
</th>
<th class="header__cell"
data-cy-user-list-header-email
scope="col">
<span>{{ t('settings', 'Email') }}</span>
</th>
<th class="header__cell header__cell--large"
data-cy-user-list-header-groups
scope="col">
<span>{{ t('settings', 'Groups') }}</span>
</th>
<th v-if="subAdminsGroups.length > 0 && settings.isAdmin"
class="header__cell header__cell--large"
data-cy-user-list-header-subadmins
scope="col">
<span>{{ t('settings', 'Group admin for') }}</span>
</th>
<th class="header__cell"
data-cy-user-list-header-quota
scope="col">
<span>{{ t('settings', 'Quota') }}</span>
</th>
<th v-if="showConfig.showLanguages"
class="header__cell header__cell--large"
data-cy-user-list-header-languages
scope="col">
<span>{{ t('settings', 'Language') }}</span>
</th>
<th v-if="showConfig.showUserBackend || showConfig.showStoragePath"
class="header__cell header__cell--large"
data-cy-user-list-header-storage-location
scope="col">
<span v-if="showConfig.showUserBackend">
{{ t('settings', 'User backend') }}
@ -77,15 +86,18 @@
</th>
<th v-if="showConfig.showLastLogin"
class="header__cell"
data-cy-user-list-header-last-login
scope="col">
<span>{{ t('settings', 'Last login') }}</span>
</th>
<th class="header__cell header__cell--large header__cell--fill"
data-cy-user-list-header-manager
scope="col">
<!-- TRANSLATORS This string describes a manager in the context of an organization -->
<span>{{ t('settings', 'Manager') }}</span>
</th>
<th class="header__cell header__cell--actions"
data-cy-user-list-header-actions
scope="col">
<span class="hidden-visually">
{{ t('settings', 'User actions') }}

@ -25,8 +25,8 @@
<template>
<tr class="user-list__row"
:data-test="user.id">
<td class="row__cell row__cell--avatar">
:data-cy-user-row="user.id">
<td class="row__cell row__cell--avatar" data-cy-user-list-cell-avatar>
<NcLoadingIcon v-if="isLoadingUser"
:name="t('settings', 'Loading user …')"
:size="32" />
@ -36,12 +36,12 @@
:user="user.id" />
</td>
<td class="row__cell row__cell--displayname" data-test-id="cell-displayname">
<td class="row__cell row__cell--displayname" data-cy-user-list-cell-displayname>
<template v-if="editing && user.backendCapabilities.setDisplayName">
<NcTextField ref="displayNameField"
class="user-row-text-field"
data-test-id="input-displayName"
:data-test-loading="`${loading.displayName}`"
data-cy-user-list-input-displayname
:data-loading="loading.displayName || undefined"
:trailing-button-label="t('settings', 'Submit')"
:class="{ 'icon-loading-small': loading.displayName }"
:show-trailing-button="true"
@ -63,13 +63,13 @@
</template>
</td>
<td data-test-id="cell-password"
<td data-cy-user-list-cell-password
class="row__cell"
:class="{ 'row__cell--obfuscated': hasObfuscated }">
<template v-if="editing && settings.canChangePassword && user.backendCapabilities.setPassword">
<NcTextField class="user-row-text-field"
data-test-id="input-password"
:data-test-loading="`${loading.password}`"
data-cy-user-list-input-password
:data-loading="loading.password || undefined"
:trailing-button-label="t('settings', 'Submit')"
:class="{'icon-loading-small': loading.password}"
:show-trailing-button="true"
@ -91,10 +91,12 @@
</span>
</td>
<td class="row__cell" data-test-id="cell-email">
<td class="row__cell" data-cy-user-list-cell-email>
<template v-if="editing">
<NcTextField class="user-row-text-field"
:class="{'icon-loading-small': loading.mailAddress}"
data-cy-user-list-input-email
:data-loading="loading.mailAddress || undefined"
:show-trailing-button="true"
:trailing-button-label="t('settings', 'Submit')"
:label="t('settings', 'Set new email address')"
@ -113,13 +115,15 @@
</span>
</td>
<td class="row__cell row__cell--large row__cell--multiline" data-test-id="cell-groups">
<td class="row__cell row__cell--large row__cell--multiline" data-cy-user-list-cell-groups>
<template v-if="editing">
<label class="hidden-visually"
:for="'groups' + uniqueId">
{{ t('settings', 'Add user to group') }}
</label>
<NcSelect :input-id="'groups' + uniqueId"
<NcSelect data-cy-user-list-input-groups
:data-loading="loading.groups || undefined"
:input-id="'groups' + uniqueId"
:close-on-select="false"
:disabled="isLoadingField"
:loading="loading.groups"
@ -143,14 +147,16 @@
</td>
<td v-if="subAdminsGroups.length > 0 && settings.isAdmin"
data-test-id="cell-subadmins"
data-cy-user-list-cell-subadmins
class="row__cell row__cell--large row__cell--multiline">
<template v-if="editing && settings.isAdmin && subAdminsGroups.length > 0">
<label class="hidden-visually"
:for="'subadmins' + uniqueId">
{{ t('settings', 'Set user as admin for') }}
</label>
<NcSelect :input-id="'subadmins' + uniqueId"
<NcSelect data-cy-user-list-input-subadmins
:data-loading="loading.subadmins || undefined"
:input-id="'subadmins' + uniqueId"
:close-on-select="false"
:disabled="isLoadingField"
:loading="loading.subadmins"
@ -170,7 +176,7 @@
</span>
</td>
<td class="row__cell" data-test-id="cell-quota">
<td class="row__cell" data-cy-user-list-cell-quota>
<template v-if="editing">
<label class="hidden-visually"
:for="'quota' + uniqueId">
@ -179,6 +185,8 @@
<NcSelect v-model="editedUserQuota"
:close-on-select="true"
:create-option="validateQuota"
data-cy-user-list-input-quota
:data-loading="loading.quota || undefined"
:disabled="isLoadingField"
:loading="loading.quota"
:append-to-body="false"
@ -202,13 +210,15 @@
<td v-if="showConfig.showLanguages"
class="row__cell row__cell--large"
data-test-id="cell-language">
data-cy-user-list-cell-language>
<template v-if="editing">
<label class="hidden-visually"
:for="'language' + uniqueId">
{{ t('settings', 'Set the language') }}
</label>
<NcSelect :id="'language' + uniqueId"
data-cy-user-list-input-language
:data-loading="loading.languages || undefined"
:allow-empty="false"
:disabled="isLoadingField"
:loading="loading.languages"
@ -226,7 +236,7 @@
</td>
<td v-if="showConfig.showUserBackend || showConfig.showStoragePath"
data-test-id="cell-storageLocation"
data-cy-user-list-cell-storage-location
class="row__cell row__cell--large">
<template v-if="!isObfuscated">
<span v-if="showConfig.showUserBackend">{{ user.backend }}</span>
@ -241,11 +251,11 @@
<td v-if="showConfig.showLastLogin"
:title="userLastLoginTooltip"
class="row__cell"
data-test-id="cell-lastLogin">
data-cy-user-list-cell-last-login>
<span v-if="!isObfuscated">{{ userLastLogin }}</span>
</td>
<td class="row__cell row__cell--large row__cell--fill" data-test-id="cell-manager">
<td class="row__cell row__cell--large row__cell--fill" data-cy-user-list-cell-manager>
<template v-if="editing">
<label class="hidden-visually"
:for="'manager' + uniqueId">
@ -253,6 +263,8 @@
</label>
<NcSelect v-model="currentManager"
class="select--fill"
data-cy-user-list-input-manager
:data-loading="loading.manager || undefined"
:input-id="'manager' + uniqueId"
:close-on-select="true"
:disabled="isLoadingField"
@ -271,7 +283,7 @@
</span>
</td>
<td class="row__cell row__cell--actions" data-test-id="cell-actions">
<td class="row__cell row__cell--actions" data-cy-user-list-cell-actions>
<UserRowActions v-if="visible && !isObfuscated && canEdit && !loading.all"
:actions="userActions"
:disabled="isLoadingField"

@ -25,8 +25,7 @@
<NcActions :aria-label="t('settings', 'Toggle user actions menu')"
:disabled="disabled"
:inline="1">
<NcActionButton data-test-id="button-toggleEdit"
:data-test="`${edit}`"
<NcActionButton :data-cy-user-list-action-toggle-edit="`${edit}`"
:disabled="disabled"
@click="toggleEdit">
{{ edit ? t('settings', 'Done') : t('settings', 'Edit') }}

@ -24,7 +24,6 @@ import { User } from '@nextcloud/cypress'
import { getUserListRow, handlePasswordConfirmation } from './usersUtils'
const admin = new User('admin', 'admin')
const jdoe = new User('jdoe', 'jdoe')
const john = new User('john', '123456')
describe('Settings: Create and delete users', function() {
@ -38,7 +37,7 @@ describe('Settings: Create and delete users', function() {
cy.login(admin)
cy.listUsers().then((users) => {
cy.login(admin)
if ((users as string[]).includes('john')) {
if ((users as string[]).includes(john.userId)) {
// ensure created user is deleted
cy.deleteUser(john).login(admin)
// ensure deleted user is not present
@ -55,15 +54,15 @@ describe('Settings: Create and delete users', function() {
// see that the username is ""
cy.get('input[data-test="username"]').should('exist').and('have.value', '')
// set the username to john
cy.get('input[data-test="username"]').type('john')
cy.get('input[data-test="username"]').type(john.userId)
// see that the username is john
cy.get('input[data-test="username"]').should('have.value', 'john')
cy.get('input[data-test="username"]').should('have.value', john.userId)
// see that the password is ""
cy.get('input[type="password"]').should('exist').and('have.value', '')
// set the password to 123456
cy.get('input[type="password"]').type('123456')
cy.get('input[type="password"]').type(john.password)
// see that the password is 123456
cy.get('input[type="password"]').should('have.value', '123456')
cy.get('input[type="password"]').should('have.value', john.password)
// submit the new user form
cy.get('button[type="submit"]').click()
})
@ -72,10 +71,9 @@ describe('Settings: Create and delete users', function() {
handlePasswordConfirmation(admin.password)
// see that the created user is in the list
cy.get('tbody.user-list__body tr[data-test="john"]').within(() => {
getUserListRow(john.userId)
// see that the list of users contains the user john
cy.contains('john').should('exist')
})
.contains(john.userId).should('exist')
})
it('Can create a user with additional field data', function() {
@ -85,8 +83,8 @@ describe('Settings: Create and delete users', function() {
cy.get('form[data-test="form"]').within(() => {
// set the username
cy.get('input[data-test="username"]').should('exist').and('have.value', '')
cy.get('input[data-test="username"]').type('john')
cy.get('input[data-test="username"]').should('have.value', 'john')
cy.get('input[data-test="username"]').type(john.userId)
cy.get('input[data-test="username"]').should('have.value', john.userId)
// set the display name
cy.get('input[data-test="displayName"]').should('exist').and('have.value', '')
cy.get('input[data-test="displayName"]').type('John Smith')
@ -97,8 +95,8 @@ describe('Settings: Create and delete users', function() {
cy.get('input[data-test="email"]').should('have.value', 'john@example.org')
// set the password
cy.get('input[type="password"]').should('exist').and('have.value', '')
cy.get('input[type="password"]').type('123456')
cy.get('input[type="password"]').should('have.value', '123456')
cy.get('input[type="password"]').type(john.password)
cy.get('input[type="password"]').should('have.value', john.password)
// submit the new user form
cy.get('button[type="submit"]').click()
})
@ -107,35 +105,42 @@ describe('Settings: Create and delete users', function() {
handlePasswordConfirmation(admin.password)
// see that the created user is in the list
getUserListRow('john')
getUserListRow(john.userId)
// see that the list of users contains the user john
.contains('john')
.contains(john.userId)
.should('exist')
})
it('Can delete a user', function() {
let testUser
// create user
cy.createUser(jdoe).login(admin)
cy.createRandomUser()
.then(($user) => {
testUser = $user
})
cy.login(admin)
// ensure created user is present
cy.reload().login(admin)
// see that the user is in the list
cy.get(`tbody.user-list__body tr[data-test="${jdoe.userId}"]`).within(() => {
// see that the list of users contains the user jdoe
cy.contains(jdoe.userId).should('exist')
// open the actions menu for the user
cy.get('td.row__cell--actions button.action-item__menutoggle').click()
cy.reload().then(() => {
// see that the user is in the list
getUserListRow(testUser.userId).within(() => {
// see that the list of users contains the user testUser
cy.contains(testUser.userId).should('exist')
// open the actions menu for the user
cy.get('[data-cy-user-list-cell-actions]')
.find('button.action-item__menutoggle')
.click({ force: true })
})
// The "Delete user" action in the actions menu is shown and clicked
cy.get('.action-item__popper .action').contains('Delete user').should('exist').click({ force: true })
// And confirmation dialog accepted
cy.get('.oc-dialog button').contains(`Delete ${testUser.userId}`).click({ force: true })
// Make sure no confirmation modal is shown
handlePasswordConfirmation(admin.password)
// deleted clicked the user is not shown anymore
getUserListRow(testUser.userId).should('not.exist')
})
// The "Delete user" action in the actions menu is shown and clicked
cy.get('.action-item__popper .action').contains('Delete user').should('exist').click()
// And confirmation dialog accepted
cy.get('.oc-dialog button').contains(`Delete ${jdoe.userId}`).click()
// Make sure no confirmation modal is shown
handlePasswordConfirmation(admin.password)
// deleted clicked the user is not shown anymore
cy.get(`tbody.user-list__body tr[data-test="${jdoe.userId}"]`).should('not.exist')
})
})

@ -20,6 +20,8 @@
*
*/
import type { User } from '@nextcloud/cypress'
/**
* Assert that `element` does not exist or is not visible
* Useful in cases such as when NcModal is opened/closed rapidly
@ -33,12 +35,28 @@ export function assertNotExistOrNotVisible(element: JQuery<HTMLElement>) {
expect(doesNotExist || isNotVisible, 'does not exist or is not visible').to.be.true
}
/**
* Helper function ensure users and groups in this tests have a clean state
*/
export function clearState() {
// cleanup ignoring any failures
cy.runOccCommand('group:list --output=json').then(($result) => {
const groups = Object.keys(JSON.parse($result.stdout)).filter((name) => name !== 'admin')
groups.forEach((groupID) => cy.runOccCommand(`group:delete '${groupID}'`))
})
cy.runOccCommand('user:list --output=json').then(($result) => {
const users = Object.keys(JSON.parse($result.stdout)).filter((name) => name !== 'admin')
users.forEach((userID) => cy.runOccCommand(`user:delete '${userID}'`))
})
}
/**
* Get the settings users list
* @return Cypress chainable object
*/
export function getUserList() {
return cy.get('[data-test-id="userList"]')
return cy.get('[data-cy-user-list]')
}
/**
@ -48,7 +66,37 @@ export function getUserList() {
* @return Cypress chainable object
*/
export function getUserListRow(userId: string) {
return getUserList().find(`tr[data-test="${userId}"]`)
return getUserList().find(`[data-cy-user-row="${userId}"]`)
}
export function waitLoading(selector: string) {
// We need to make sure the element is loading, otherwise the "done loading" will succeed even if we did not start loading.
// But Cypress might also be simply too slow to catch the loading phase. Thats why we need to wait in this case.
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.get(`${selector}[data-loading]`).if().should('exist').else().wait(1000)
// https://github.com/NoriSte/cypress-wait-until/issues/75#issuecomment-572685623
cy.waitUntil(() => Cypress.$(selector).length > 0 && !Cypress.$(selector).attr('data-loading')?.length, { timeout: 10000 })
}
/**
* Toggle the edit button of the user row
* @param user The user row to edit
* @param toEdit True if it should be switch to edit mode, false to switch to read-only
*/
export function toggleEditButton(user: User, toEdit = true) {
// see that the list of users contains the user
getUserListRow(user.userId).should('exist')
// toggle the edit mode for the user
.find('[data-cy-user-list-cell-actions]')
.find(`[data-cy-user-list-action-toggle-edit="${!toEdit}"]`)
.if()
.click({ force: true })
.else()
// otherwise ensure the button is already in edit mode
.then(() => getUserListRow(user.userId)
.find(`[data-cy-user-list-action-toggle-edit="${toEdit}"]`)
.should('exist'),
)
}
/**
@ -59,7 +107,6 @@ export function handlePasswordConfirmation(adminPassword = 'admin') {
const handleModal = (context: Cypress.Chainable) => {
return context.contains('.modal-container', 'Confirm your password')
.if()
.if('visible')
.within(() => {
cy.get('input[type="password"]').type(adminPassword)
cy.get('button').contains('Confirm').click()

@ -49,9 +49,7 @@ describe('Settings: Show and hide columns', function() {
it('Can show a column', function() {
// see that the language column is not in the header
cy.get('.user-list__header tr').within(() => {
cy.contains('Language').should('not.exist')
})
cy.get('[data-cy-user-list-header-languages]').should('not.exist')
// see that the language column is not in all user rows
cy.get('tbody.user-list__body tr').each(($row) => {
@ -72,25 +70,21 @@ describe('Settings: Show and hide columns', function() {
cy.waitUntil(() => cy.get('.modal-container').should(el => assertNotExistOrNotVisible(el)))
// see that the language column is in the header
cy.get('.user-list__header tr').within(() => {
cy.contains('Language').should('exist')
})
cy.get('[data-cy-user-list-header-languages]').should('exist')
// see that the language column is in all user rows
getUserList().find('tbody tr').each(($row) => {
cy.wrap($row).get('[data-test-id="cell-language"]').should('exist')
cy.wrap($row).get('[data-cy-user-list-cell-language]').should('exist')
})
})
it('Can hide a column', function() {
// see that the last login column is in the header
cy.get('.user-list__header tr').within(() => {
cy.contains('Last login').should('exist')
})
cy.get('[data-cy-user-list-header-last-login]').should('exist')
// see that the last login column is in all user rows
getUserList().find('tbody tr').each(($row) => {
cy.wrap($row).get('[data-test-id="cell-lastLogin"]').should('exist')
cy.wrap($row).get('[data-cy-user-list-cell-last-login]').should('exist')
})
// open the settings dialog
@ -107,13 +101,11 @@ describe('Settings: Show and hide columns', function() {
cy.waitUntil(() => cy.get('.modal-container').should(el => assertNotExistOrNotVisible(el)))
// see that the last login column is not in the header
cy.get('.user-list__header tr').within(() => {
cy.contains('Last login').should('not.exist')
})
cy.get('[data-cy-user-list-header-last-login]').should('not.exist')
// see that the last login column is not in all user rows
getUserList().find('tbody tr').each(($row) => {
cy.wrap($row).get('[data-test-id="cell-lastLogin"]').should('not.exist')
cy.wrap($row).get('[data-cy-user-list-cell-last-login]').should('not.exist')
})
})
})

@ -21,61 +21,67 @@
*/
import { User } from '@nextcloud/cypress'
import { clearState, getUserListRow } from './usersUtils'
const admin = new User('admin', 'admin')
const jdoe = new User('jdoe', 'jdoe')
describe('Settings: Disable and enable users', function() {
before(function() {
cy.createUser(jdoe)
let testUser: User
beforeEach(function() {
clearState()
cy.createRandomUser().then(($user) => {
testUser = $user
})
cy.login(admin)
// open the User settings
cy.visit('/settings/users')
})
// Not guranteed to run but would be nice to cleanup
after(() => {
cy.deleteUser(jdoe)
cy.deleteUser(testUser)
})
it('Can disable the user', function() {
// ensure user is enabled
cy.enableUser(jdoe)
cy.enableUser(testUser)
// see that the user is in the list of active users
cy.get(`tbody.user-list__body tr[data-test="${jdoe.userId}"]`).within(() => {
// see that the list of users contains the user jdoe
cy.contains(jdoe.userId).should('exist')
getUserListRow(testUser.userId).within(() => {
// see that the list of users contains the user testUser
cy.contains(testUser.userId).should('exist')
// open the actions menu for the user
cy.get('td.row__cell--actions button.action-item__menutoggle').click({ scrollBehavior: 'center' })
cy.get('[data-cy-user-list-cell-actions] button.action-item__menutoggle').click({ scrollBehavior: 'center' })
})
// The "Disable user" action in the actions menu is shown and clicked
cy.get('.action-item__popper .action').contains('Disable user').should('exist').click()
// When clicked the section is not shown anymore
cy.get(`tbody.user-list__body tr[data-test="${jdoe.userId}"]`).should('not.exist')
getUserListRow(testUser.userId).should('not.exist')
// But the disabled user section now exists
cy.get('#disabled').should('exist')
// Open disabled users section
cy.get('#disabled a').click()
cy.url().should('match', /\/disabled/)
// The list of disabled users should now contain the user
cy.get(`tbody.user-list__body tr[data-test="${jdoe.userId}"]`).should('exist')
getUserListRow(testUser.userId).should('exist')
})
it('Can enable the user', function() {
// ensure user is disabled
cy.enableUser(jdoe, false)
cy.enableUser(testUser, false).reload()
// Open disabled users section
cy.get('#disabled a').click()
cy.url().should('match', /\/disabled/)
// see that the user is in the list of active users
cy.get(`tbody.user-list__body tr[data-test="${jdoe.userId}"]`).within(() => {
// see that the list of disabled users contains the user jdoe
cy.contains(jdoe.userId).should('exist')
getUserListRow(testUser.userId).within(() => {
// see that the list of disabled users contains the user testUser
cy.contains(testUser.userId).should('exist')
// open the actions menu for the user
cy.get('td.row__cell--actions button.action-item__menutoggle').click({ scrollBehavior: 'center' })
cy.get('[data-cy-user-list-cell-actions] button.action-item__menutoggle').click({ scrollBehavior: 'center' })
})
// The "Enable user" action in the actions menu is shown and clicked

@ -21,18 +21,21 @@
*/
import { User } from '@nextcloud/cypress'
import { handlePasswordConfirmation } from './usersUtils'
import { getUserListRow, handlePasswordConfirmation, toggleEditButton } from './usersUtils'
// eslint-disable-next-line n/no-extraneous-import
import randomString from 'crypto-random-string'
const admin = new User('admin', 'admin')
describe('Settings: Create and delete groups', () => {
describe('Settings: Create groups', () => {
before(() => {
cy.login(admin)
// open the User settings
cy.visit('/settings/users')
})
it('Can create a group', () => {
const groupName = randomString(7)
// open the Create group menu
cy.get('button[aria-label="Create group"]').click()
@ -40,9 +43,9 @@ describe('Settings: Create and delete groups', () => {
// see that the group name is ""
cy.get('input[placeholder="Group name"]').should('exist').and('have.value', '')
// set the group name to foo
cy.get('input[placeholder="Group name"]').type('foo')
cy.get('input[placeholder="Group name"]').type(groupName)
// see that the group name is foo
cy.get('input[placeholder="Group name"]').should('have.value', 'foo')
cy.get('input[placeholder="Group name"]').should('have.value', groupName)
// submit the group name
cy.get('input[placeholder="Group name"] ~ button').click()
})
@ -53,38 +56,170 @@ describe('Settings: Create and delete groups', () => {
// see that the created group is in the list
cy.get('ul.app-navigation__list').within(() => {
// see that the list of groups contains the group foo
cy.contains('foo').should('exist')
cy.contains(groupName).should('exist')
})
})
})
describe('Settings: Assign user to a group', { testIsolation: false }, () => {
const groupName = randomString(7)
let testUser: User
after(() => cy.deleteUser(testUser))
before(() => {
cy.createRandomUser().then((user) => {
testUser = user
})
cy.runOccCommand(`group:add '${groupName}'`)
cy.login(admin)
cy.visit('/settings/users')
})
it('see that the group is in the list', () => {
cy.get('ul.app-navigation__list').contains('li', groupName).should('exist')
cy.get('ul.app-navigation__list').contains('li', groupName).within(() => {
cy.get('.counter-bubble__counter')
.should('not.exist') // is hidden when 0
})
})
it('Can delete a group', () => {
// see that the group is in the list
it('see that the user is in the list', () => {
getUserListRow(testUser.userId)
.contains(testUser.userId)
.should('exist')
.scrollIntoView()
})
it('switch into user edit mode', () => {
toggleEditButton(testUser)
getUserListRow(testUser.userId)
.find('[data-cy-user-list-input-groups]')
.should('exist')
})
it('assign the group', () => {
// focus inside the input
getUserListRow(testUser.userId)
.find('[data-cy-user-list-input-groups] input')
.click({ force: true })
// enter the group name
getUserListRow(testUser.userId)
.find('[data-cy-user-list-input-groups] input')
.type(`${groupName.slice(0, 5)}`) // only type part as otherwise we would create a new one with the same name
cy.contains('li.vs__dropdown-option', groupName)
.click({ force: true })
handlePasswordConfirmation(admin.password)
})
it('leave the user edit mode', () => {
toggleEditButton(testUser, false)
})
it('see the group was successfully assigned', () => {
// see a new memeber
cy.get('ul.app-navigation__list')
.contains('li', groupName)
.find('.counter-bubble__counter')
.should('contain', '1')
})
it('validate the user was added on backend', () => {
cy.runOccCommand(`user:info --output=json '${testUser.userId}'`).then((output) => {
cy.wrap(output.code).should('eq', 0)
cy.wrap(JSON.parse(output.stdout)?.groups).should('include', groupName)
})
})
})
describe('Settings: Delete an empty group', { testIsolation: false }, () => {
const groupName = randomString(7)
before(() => {
cy.runOccCommand(`group:add '${groupName}'`)
cy.login(admin)
cy.visit('/settings/users')
})
it('see that the group is in the list', () => {
cy.get('ul.app-navigation__list').within(() => {
// see that the list of groups contains the group foo
cy.contains('foo').should('exist')
cy.contains(groupName).should('exist').scrollIntoView()
// open the actions menu for the group
cy.contains('li', 'foo').within(() => {
cy.get('button.action-item__menutoggle').click()
cy.contains('li', groupName).within(() => {
cy.get('button.action-item__menutoggle').click({ force: true })
})
})
})
it('can delete the group', () => {
// The "Remove group" action in the actions menu is shown and clicked
cy.get('.action-item__popper button').contains('Remove group').should('exist').click()
cy.get('.action-item__popper button').contains('Remove group').should('exist').click({ force: true })
// And confirmation dialog accepted
cy.get('.modal-container button').contains('Confirm').click()
cy.get('.modal-container button').contains('Confirm').click({ force: true })
// Make sure no confirmation modal is shown
cy.get('body').contains('.modal-container', 'Confirm your password')
.if('visible')
.then(($modal) => {
cy.wrap($modal).find('input[type="password"]').type(admin.password)
cy.wrap($modal).find('button').contains('Confirm').click()
})
handlePasswordConfirmation(admin.password)
})
// deleted group is not shown anymore
it('deleted group is not shown anymore', () => {
cy.get('ul.app-navigation__list').within(() => {
// see that the list of groups does not contain the group
cy.contains(groupName).should('not.exist')
})
// and also not in database
cy.runOccCommand('group:list --output=json').then(($response) => {
const groups: string[] = Object.keys(JSON.parse($response.stdout))
expect(groups).to.not.include(groupName)
})
})
})
describe('Settings: Delete a non empty group', () => {
let testUser: User
const groupName = randomString(7)
before(() => {
cy.runOccCommand(`group:add '${groupName}'`)
cy.createRandomUser().then(($user) => {
testUser = $user
cy.runOccCommand(`group:addUser '${groupName}' '${$user.userId}'`)
})
cy.login(admin)
cy.visit('/settings/users')
})
after(() => cy.deleteUser(testUser))
it('see that the group is in the list', () => {
// see that the list of groups contains the group
cy.get('ul.app-navigation__list').contains('li', groupName).should('exist').scrollIntoView()
})
it('can delete the group', () => {
// open the menu
cy.get('ul.app-navigation__list')
.contains('li', groupName)
.find('button.action-item__menutoggle')
.click({ force: true })
// The "Remove group" action in the actions menu is shown and clicked
cy.get('.action-item__popper button').contains('Remove group').should('exist').click({ force: true })
// And confirmation dialog accepted
cy.get('.modal-container button').contains('Confirm').click({ force: true })
// Make sure no confirmation modal is shown
handlePasswordConfirmation(admin.password)
})
it('deleted group is not shown anymore', () => {
cy.get('ul.app-navigation__list').within(() => {
// see that the list of groups does not contain the group foo
cy.contains('foo').should('not.exist')
cy.contains(groupName).should('not.exist')
})
// and also not in database
cy.runOccCommand('group:list --output=json').then(($response) => {
const groups: string[] = Object.keys(JSON.parse($response.stdout))
expect(groups).to.not.include(groupName)
})
})
})

@ -21,92 +21,260 @@
*/
import { User } from '@nextcloud/cypress'
import { getUserListRow, handlePasswordConfirmation } from './usersUtils'
import { clearState, getUserListRow, handlePasswordConfirmation, toggleEditButton, waitLoading } from './usersUtils'
const admin = new User('admin', 'admin')
const jdoe = new User('jdoe', 'jdoe')
describe('Settings: Change user properties', function() {
before(function() {
cy.createUser(jdoe)
cy.login(admin)
// open the User settings
cy.visit('/settings/users')
})
let user: User
beforeEach(function() {
// reset to read-only mode: try to find the edit button and click it if set to editing
getUserListRow(jdoe.userId)
.find('[data-test-id="cell-actions"]')
// replace with following (more error resilent) with nextcloud-vue 8
// find('[data-test-id="button-toggleEdit"][data-test="true"]')
.find('button[aria-label="Done"]')
.if()
.click({ force: true })
})
after(() => {
cy.deleteUser(jdoe)
clearState()
cy.createRandomUser().then(($user) => { user = $user })
cy.login(admin)
})
it('Can change the display name', function() {
// see that the list of users contains the user jdoe
getUserListRow(jdoe.userId).should('exist')
// toggle the edit mode for the user jdoe
.find('[data-test-id="cell-actions"]')
.find('button[aria-label="Edit"]')
// replace with following (more error resilent) with nextcloud-vue 8
// find('[data-test-id="button-toggleEdit"]')
.click({ force: true })
getUserListRow(jdoe.userId).within(() => {
// open the User settings as admin
cy.visit('/settings/users')
// toggle edit button into edit mode
toggleEditButton(user, true)
getUserListRow(user.userId).within(() => {
// set the display name
cy.get('input[data-test-id="input-displayName"]').should('exist').and('have.value', 'jdoe')
cy.get('input[data-test-id="input-displayName"]').clear()
cy.get('input[data-test-id="input-displayName"]').type('John Doe')
cy.get('input[data-test-id="input-displayName"]').should('have.value', 'John Doe')
cy.get('input[data-test-id="input-displayName"] ~ button').click()
cy.get('[data-cy-user-list-input-displayname]').should('exist').and('have.value', user.userId)
cy.get('[data-cy-user-list-input-displayname]').clear()
cy.get('[data-cy-user-list-input-displayname]').type('John Doe')
cy.get('[data-cy-user-list-input-displayname]').should('have.value', 'John Doe')
cy.get('[data-cy-user-list-input-displayname] ~ button').click()
// Make sure no confirmation modal is shown
handlePasswordConfirmation(admin.password)
// see that the display name cell is done loading
cy.get('[data-test-id="input-displayName"]').should('have.attr', 'data-test-loading', 'true')
cy.waitUntil(() => cy.get('[data-test-id="input-displayName"]').should('have.attr', 'data-test-loading', 'false'), { timeout: 10000 })
waitLoading('[data-cy-user-list-input-displayname]')
})
// Success message is shown
cy.get('.toastify.toast-success').contains(/Display.+name.+was.+successfully.+changed/i).should('exist')
})
it('Can change the password', function() {
// see that the list of users contains the user jdoe
getUserListRow(jdoe.userId).should('exist')
// toggle the edit mode for the user jdoe
.find('[data-test-id="cell-actions"]')
.find('button[aria-label="Edit"]')
// replace with following (more error resilent) with nextcloud-vue 8
// find('[data-test-id="button-toggleEdit"]')
.click({ force: true })
getUserListRow(jdoe.userId).within(() => {
// see that the password of user0 is ""
cy.get('input[type="password"]').should('exist').and('have.value', '')
// set the password for user0 to 123456
cy.get('input[type="password"]').type('123456')
// When I set the password for user0 to 123456
cy.get('input[type="password"]').should('have.value', '123456')
cy.get('input[type="password"] ~ button').click()
// open the User settings as admin
cy.visit('/settings/users')
// toggle edit button into edit mode
toggleEditButton(user, true)
getUserListRow(user.userId).within(() => {
// see that the password of user is ""
cy.get('[data-cy-user-list-input-password]').should('exist').and('have.value', '')
// set the password for user to 123456
cy.get('[data-cy-user-list-input-password]').type('123456')
// When I set the password for user to 123456
cy.get('[data-cy-user-list-input-password]').should('have.value', '123456')
cy.get('[data-cy-user-list-input-password] ~ button').click()
// Make sure no confirmation modal is shown
handlePasswordConfirmation(admin.password)
// see that the password cell for user user0 is done loading
cy.get('[data-test-id="input-password"]').should('have.attr', 'data-test-loading', 'true')
cy.waitUntil(() => cy.get('[data-test-id="input-password"]').should('have.attr', 'data-test-loading', 'false'), { timeout: 10000 })
// see that the password cell for user is done loading
waitLoading('[data-cy-user-list-input-password]')
// password input is emptied on change
cy.get('[data-test-id="input-password"]').should('have.value', '')
cy.get('[data-cy-user-list-input-password]').should('have.value', '')
})
// Success message is shown
cy.get('.toastify.toast-success').contains(/Password.+successfully.+changed/i).should('exist')
})
it('Can change the email address', function() {
// open the User settings as admin
cy.visit('/settings/users')
// toggle edit button into edit mode
toggleEditButton(user, true)
getUserListRow(user.userId).find('[data-cy-user-list-cell-email]').within(() => {
// see that the email of user is ""
cy.get('input').should('exist').and('have.value', '')
// set the email for user to mymail@example.com
cy.get('input').type('mymail@example.com')
// When I set the password for user to mymail@example.com
cy.get('input').should('have.value', 'mymail@example.com')
cy.get('input ~ button').click()
// Make sure no confirmation modal is shown
handlePasswordConfirmation(admin.password)
// see that the password cell for user is done loading
waitLoading('[data-cy-user-list-input-email]')
})
// Success message is shown
cy.get('.toastify.toast-success').contains(/Email.+successfully.+changed/i).should('exist')
})
it('Can change the user quota to a predefined one', function() {
// open the User settings as admin
cy.visit('/settings/users')
// toggle edit button into edit mode
toggleEditButton(user, true)
getUserListRow(user.userId).find('[data-cy-user-list-cell-quota]').scrollIntoView()
getUserListRow(user.userId).find('[data-cy-user-list-cell-quota] [data-cy-user-list-input-quota]').within(() => {
// see that the quota of user is unlimited
cy.get('.vs__selected').should('exist').and('contain.text', 'Unlimited')
// Open the quota selector
cy.get('[role="combobox"]').click({ force: true })
// see that there are default options for the quota
cy.get('li').then(($options) => {
expect($options).to.have.length(5)
cy.wrap($options).contains('Default quota')
cy.wrap($options).contains('Unlimited')
cy.wrap($options).contains('1 GB')
cy.wrap($options).contains('10 GB')
// select 5 GB
cy.wrap($options).contains('5 GB').click({ force: true })
// Make sure no confirmation modal is shown
handlePasswordConfirmation(admin.password)
})
// see that the quota of user is 5 GB
cy.get('.vs__selected').should('exist').and('contain.text', '5 GB')
})
// see that the changes are loading
waitLoading('[data-cy-user-list-input-quota]')
// finish editing the user
toggleEditButton(user, false)
// I see that the quota was set on the backend
cy.runOccCommand(`user:info --output=json '${user.userId}'`).then(($result) => {
expect($result.code).to.equal(0)
const info = JSON.parse($result.stdout)
expect(info?.quota).to.equal('5 GB')
})
})
it('Can change the user quota to a custom value', function() {
// open the User settings as admin
cy.visit('/settings/users')
// toggle edit button into edit mode
toggleEditButton(user, true)
getUserListRow(user.userId).find('[data-cy-user-list-cell-quota]').scrollIntoView()
getUserListRow(user.userId).find('[data-cy-user-list-cell-quota]').within(() => {
// see that the quota of user is unlimited
cy.get('.vs__selected').should('exist').and('contain.text', 'Unlimited')
// set the quota to 4 MB
cy.get('[data-cy-user-list-input-quota] input').type('4 MB{enter}')
// Make sure no confirmation modal is shown
handlePasswordConfirmation(admin.password)
// see that the quota of user is 4 MB
// TODO: Enable this after the file size handling is fixed
// cy.get('.vs__selected').should('exist').and('contain.text', '4 MB')
// see that the changes are loading
waitLoading('[data-cy-user-list-input-quota]')
})
// finish editing the user
toggleEditButton(user, false)
// I see that the quota was set on the backend
cy.runOccCommand(`user:info --output=json '${user.userId}'`).then(($result) => {
expect($result.code).to.equal(0)
// TODO: Enable this after the file size handling is fixed!!!!!!
// const info = JSON.parse($result.stdout)
// expect(info?.quota).to.equal('4 MB')
})
})
it('Can set manager of a user', function() {
// create the manager
let manager: User
cy.createRandomUser().then(($user) => { manager = $user })
// open the User settings as admin
cy.login(admin)
cy.visit('/settings/users')
// toggle edit button into edit mode
toggleEditButton(user, true)
getUserListRow(user.userId)
.find('[data-cy-user-list-cell-manager]')
.scrollIntoView()
getUserListRow(user.userId).find('[data-cy-user-list-cell-manager]').within(() => {
// see that the user has no manager
cy.get('.vs__selected').should('not.exist')
// Open the dropdown menu
cy.get('[role="combobox"]').click({ force: true })
// select the manager
cy.contains('li', manager.userId).click({ force: true })
// Handle password confirmation on time out
handlePasswordConfirmation(admin.password)
// see that the user has a manager set
cy.get('.vs__selected').should('exist').and('contain.text', manager.userId)
})
// see that the changes are loading
waitLoading('[data-cy-user-list-input-manager]')
// finish editing the user
toggleEditButton(user, false)
// validate the manager is set
cy.getUserData(user).then(($result) => expect($result.body).to.contain(`<manager>${manager.userId}</manager>`))
})
it('Can make user a subadmin of a group', function() {
// create a group
const groupName = 'userstestgroup'
cy.runOccCommand(`group:add '${groupName}'`)
// open the User settings as admin
cy.visit('/settings/users')
// toggle edit button into edit mode
toggleEditButton(user, true)
getUserListRow(user.userId).find('[data-cy-user-list-cell-subadmins]').scrollIntoView()
getUserListRow(user.userId).find('[data-cy-user-list-cell-subadmins]').within(() => {
// see that the user is no subadmin
cy.get('.vs__selected').should('not.exist')
// Open the dropdown menu
cy.get('[role="combobox"]').click({ force: true })
// select the group
cy.contains('li', groupName).click({ force: true })
// handle password confirmation on time out
handlePasswordConfirmation(admin.password)
// see that the user is subadmin of the group
cy.get('.vs__selected').should('exist').and('contain.text', groupName)
})
waitLoading('[data-cy-user-list-input-subadmins]')
// finish editing the user
toggleEditButton(user, false)
// I see that the quota was set on the backend
cy.getUserData(user).then(($response) => {
expect($response.status).to.equal(200)
const dom = (new DOMParser()).parseFromString($response.body, 'text/xml')
expect(dom.querySelector('subadmin element')?.textContent).to.contain(groupName)
})
})
})

@ -67,7 +67,7 @@ declare global {
/**
* Run an occ command in the docker container.
*/
runOccCommand(command: string): Cypress.Chainable<void>,
runOccCommand(command: string, options?: Partial<Cypress.ExecOptions>): Cypress.Chainable<Cypress.Exec>,
}
}
}
@ -131,6 +131,7 @@ Cypress.Commands.add('uploadFile', (user, fixture = 'image.jpg', mimeType = 'ima
* @param {string} target the target of the file relative to the user root
*/
Cypress.Commands.add('uploadContent', (user, blob, mimeType, target) => {
// eslint-disable-next-line cypress/unsafe-to-chain-command
cy.clearCookies()
.then(async () => {
const fileName = basename(target)
@ -216,6 +217,6 @@ Cypress.Commands.add('resetUserTheming', (user?: User) => {
}
})
Cypress.Commands.add('runOccCommand', (command: string) => {
cy.exec(`docker exec --user www-data nextcloud-cypress-tests-server php ./occ ${command}`)
Cypress.Commands.add('runOccCommand', (command: string, options?: Partial<Cypress.ExecOptions>) => {
return cy.exec(`docker exec --user www-data nextcloud-cypress-tests-server php ./occ ${command}`, options)
})

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -24,7 +24,6 @@ default:
- SettingsMenuContext
- ThemingAppContext
- ToastContext
- UsersSettingsContext
filters:
tags: "~@apache"
apache:
@ -52,7 +51,6 @@ default:
- SettingsMenuContext
- ThemingAppContext
- ToastContext
- UsersSettingsContext
filters:
tags: "@apache"
extensions:

@ -1,379 +0,0 @@
<?php
/**
*
* @copyright Copyright (c) 2017, Daniel Calviño Sánchez (danxuliu@gmail.com)
* @copyright Copyright (c) 2018, John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
* @copyright Copyright (c) 2019, Greta Doci <gretadoci@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
use Behat\Behat\Context\Context;
use PHPUnit\Framework\Assert;
use WebDriver\Key;
class UsersSettingsContext implements Context, ActorAwareInterface {
use ActorAware;
/**
* @return Locator
*/
public static function newUserForm() {
return Locator::forThe()->css('[data-test="form"]')->
describedAs("New user form in Users Settings");
}
/**
* @return Locator
*/
public static function userNameFieldForNewUser() {
return Locator::forThe()->css('[data-test="username"]')->
describedAs("User name field for new user in Users Settings");
}
/**
* @return Locator
*/
public static function displayNameFieldForNewUser() {
return Locator::forThe()->css('[data-test="displayName"]')->
describedAs("Display name field for new user in Users Settings");
}
/**
* @return Locator
*/
public static function passwordFieldForNewUser() {
return Locator::forThe()->css('[data-test="password"]')->
describedAs("Password field for new user in Users Settings");
}
/**
* @return Locator
*/
public static function newUserButton() {
return Locator::forThe()->id("new-user-button")->
describedAs("New user button in Users Settings");
}
/**
* @return Locator
*/
public static function createNewUserButton() {
return Locator::forThe()->css('[data-test="submit"]')->
describedAs("Create user button in Users Settings");
}
/**
* @return Locator
*/
public static function rowForUser($user) {
return Locator::forThe()->xpath("//tbody[contains(@class, 'user-list__body')]/tr[td[@data-test='$user']]")->
describedAs("Row for user $user in Users Settings");
}
/**
* Warning: you need to watch out for the proper classes order
*
* @return Locator
*/
public static function classCellForUser($class, $user) {
return Locator::forThe()->xpath("//*[contains(concat(' ', normalize-space(@class), ' '), ' $class ')]")->
descendantOf(self::rowForUser($user))->
describedAs("$class cell for user $user in Users Settings");
}
/**
* @return Locator
*/
public static function inputForUserInCell($cell, $user) {
return Locator::forThe()->css("input")->
descendantOf(self::classCellForUser($cell, $user))->
describedAs("$cell input for user $user in Users Settings");
}
/**
* @return Locator
*/
public static function displayNameCellForUser($user) {
return self::inputForUserInCell("displayName", $user);
}
/**
* @return Locator
*/
public static function optionInInputForUser($cell, $user) {
return Locator::forThe()->css(".vs__dropdown-option--highlight")->
describedAs("Selected $cell option in $cell input for user $user in Users Settings");
}
/**
* @return Locator
*/
public static function actionsMenuOf($user) {
return Locator::forThe()->css(".userActions .action-item:not(.action-item--single)")->
descendantOf(self::rowForUser($user))->
describedAs("Actions menu for user $user in Users Settings");
}
/**
* @return Locator
*/
public static function theAction($action, $user) {
return Locator::forThe()->xpath("//button[@aria-label = normalize-space('$action')]")->
describedAs("$action action for the user $user row in Users Settings");
}
/**
* @return Locator
*/
public static function theColumn($column) {
return Locator::forThe()->xpath("//div[@class='user-list-grid']//div[normalize-space() = '$column']")->
describedAs("The $column column in Users Settings");
}
/**
* @return Locator
*/
public static function selectedSelectOption($cell, $user) {
return Locator::forThe()->css(".vs__selected .name-parts")->
descendantOf(self::classCellForUser($cell, $user))->
describedAs("The selected option of the $cell select for the user $user in Users Settings");
}
/**
* @return Locator
*/
public static function editModeToggle($user) {
return Locator::forThe()->css(".userActions .action-items button:first-of-type")->
descendantOf(self::rowForUser($user))->
describedAs("The edit toggle button for the user $user in Users Settings");
}
/**
* @return Locator
*/
public static function editModeOn($user) {
return Locator::forThe()->css("div.user-list-grid div.row.row--editable[data-id=$user]")->
describedAs("I see the edit mode is on for the user $user in Users Settings");
}
/**
* @When I click the New user button
*/
public function iClickTheNewUserButton() {
$this->actor->find(self::newUserButton(), 10)->click();
}
/**
* @When I click the :action action in the :user actions menu
*/
public function iClickTheAction($action, $user) {
$this->actor->find(self::theAction($action, $user), 10)->click();
}
/**
* @When I open the actions menu for the user :user
*/
public function iOpenTheActionsMenuOf($user) {
$this->actor->find(self::actionsMenuOf($user), 10)->click();
}
/**
* @When I set the user name for the new user to :user
*/
public function iSetTheUserNameForTheNewUserTo($user) {
$this->actor->find(self::userNameFieldForNewUser(), 10)->setValue($user);
}
/**
* @When I set the display name for the new user to :displayName
*/
public function iSetTheDisplayNameForTheNewUserTo($displayName) {
$this->actor->find(self::displayNameFieldForNewUser(), 10)->setValue($displayName);
}
/**
* @When I set the password for the new user to :password
*/
public function iSetThePasswordForTheNewUserTo($password) {
$this->actor->find(self::passwordFieldForNewUser(), 10)->setValue($password);
}
/**
* @When I create the new user
*/
public function iCreateTheNewUser() {
$this->actor->find(self::createNewUserButton(), 10)->click();
}
/**
* @When I toggle the edit mode for the user :user
*/
public function iToggleTheEditModeForUser($user) {
$this->actor->find(self::editModeToggle($user), 10)->click();
}
/**
* @When I create user :user with password :password
*/
public function iCreateUserWithPassword($user, $password) {
$this->actor->find(self::userNameFieldForNewUser(), 10)->setValue($user);
$this->actor->find(self::passwordFieldForNewUser())->setValue($password);
$this->actor->find(self::createNewUserButton())->click();
}
/**
* @When I set the :field for :user to :value
*/
public function iSetTheFieldForUserTo($field, $user, $value) {
$this->actor->find(self::inputForUserInCell($field, $user), 2)->setValue($value . Key::ENTER);
}
/**
* Assigning/withdrawing is the same action (it toggles).
*
* @When I assign the user :user to the group :group
* @When I withdraw the user :user from the group :group
*/
public function iAssignTheUserToTheGroup($user, $group) {
$this->actor->find(self::inputForUserInCell('groups', $user))->setValue($group);
$this->actor->find(self::optionInInputForUser('groups', $user))->click();
}
/**
* @When I set the user :user quota to :quota
*/
public function iSetTheUserQuotaTo($user, $quota) {
$this->actor->find(self::inputForUserInCell('quota', $user))->setValue($quota);
$this->actor->find(self::optionInInputForUser('quota', $user))->click();
}
/**
* @Then I see that the list of users contains the user :user
*/
public function iSeeThatTheListOfUsersContainsTheUser($user) {
if (!WaitFor::elementToBeEventuallyShown(
$this->actor,
self::rowForUser($user),
$timeout = 10 * $this->actor->getFindTimeoutMultiplier())) {
Assert::fail("The user $user in the list of users is not shown yet after $timeout seconds");
}
}
/**
* @Then I see that the list of users does not contains the user :user
*/
public function iSeeThatTheListOfUsersDoesNotContainsTheUser($user) {
if (!WaitFor::elementToBeEventuallyNotShown(
$this->actor,
self::rowForUser($user),
$timeout = 10 * $this->actor->getFindTimeoutMultiplier())) {
Assert::fail("The user $user in the list of users is still shown after $timeout seconds");
}
}
/**
* @Then I see that the new user form is shown
*/
public function iSeeThatTheNewUserFormIsShown() {
if (!WaitFor::elementToBeEventuallyShown(
$this->actor,
self::newUserForm(),
$timeout = 10 * $this->actor->getFindTimeoutMultiplier())) {
Assert::fail("The new user form is not shown yet after $timeout seconds");
}
}
/**
* @Then I see that the :action action in the :user actions menu is shown
*/
public function iSeeTheAction($action, $user) {
Assert::assertTrue(
$this->actor->find(self::theAction($action, $user), 10)->isVisible());
}
/**
* @Then I see that the :column column is shown
*/
public function iSeeThatTheColumnIsShown($column) {
Assert::assertTrue(
$this->actor->find(self::theColumn($column), 10)->isVisible());
}
/**
* @Then I see that the :field of :user is :value
*/
public function iSeeThatTheFieldOfUserIs($field, $user, $value) {
Assert::assertEquals(
$this->actor->find(self::inputForUserInCell($field, $user), 10)->getValue(), $value);
}
/**
* @Then I see that the display name for the user :user is :displayName
*/
public function iSeeThatTheDisplayNameForTheUserIs($user, $displayName) {
Assert::assertEquals(
$displayName, $this->actor->find(self::displayNameCellForUser($user), 10)->getValue());
}
/**
* @Then I see that the :cell cell for user :user is done loading
*/
public function iSeeThatTheCellForUserIsDoneLoading($cell, $user) {
// It could happen that the cell for the user was done loading and thus
// the loading icon hidden again even before finding the loading icon
// started. Therefore, if the loading icon could not be found it is just
// assumed that it was already hidden again. Nevertheless, this check
// should be done anyway to ensure that the following scenario steps are
// not executed before the cell for the user was done loading.
try {
$this->actor->find(self::classCellForUser($cell . ' icon-loading-small', $user), 1);
} catch (NoSuchElementException $exception) {
echo "The loading icon for user $user was not found after " . (1 * $this->actor->getFindTimeoutMultiplier()) . " seconds, assumming that it was shown and hidden again before the check started and continuing";
return;
}
if (!WaitFor::elementToBeEventuallyNotShown(
$this->actor,
self::classCellForUser($cell . ' icon-loading-small', $user),
$timeout = 10 * $this->actor->getFindTimeoutMultiplier())) {
Assert::fail("The loading icon for user $user is still shown after $timeout seconds");
}
}
/**
* @Then I see that the user quota of :user is :quota
*/
public function iSeeThatTheuserQuotaIs($user, $quota) {
Assert::assertEquals(
$this->actor->find(self::selectedSelectOption('quota', $user), 2)->getText(), $quota);
}
/**
* @Then I see that the edit mode is on for user :user
*/
public function iSeeThatTheEditModeIsOn($user) {
if (!WaitFor::elementToBeEventuallyShown(
$this->actor,
self::editModeOn($user),
$timeout = 10 * $this->actor->getFindTimeoutMultiplier())) {
Assert::fail("The edit mode for user $user in the list of users is not on yet after $timeout seconds");
}
}
}

@ -1,77 +0,0 @@
@apache
Feature: users
Scenario: assign user to a group
Given I act as Jane
And I am logged in as the admin
And I open the User settings
# And I see that the list of users contains the user user0
# When I toggle the edit mode for the user user0
# Then I see that the edit mode is on for user user0
# disabled because we need the TAB patch:
# https://github.com/minkphp/MinkSelenium2Driver/pull/244
# When I assign the user user0 to the group admin
# Then I see that the section Admins is shown
# And I see that the section Admins has a count of 2
Scenario: create and delete a group
Given I act as Jane
And I am logged in as the admin
And I open the User settings
# And I see that the list of users contains the user user0
# disabled because we need the TAB patch:
# https://github.com/minkphp/MinkSelenium2Driver/pull/244
# And I assign the user user0 to the group Group1
# And I see that the section Group1 is shown
# And I click the "icon-delete" button on the Group1 section
# And I see that the confirmation dialog is shown
# When I click the "Yes" button of the confirmation dialog
# Then I see that the section Group1 is not shown
Scenario: delete an empty group
Given I act as Jane
And I am logged in as the admin
And I open the User settings
# disabled because we need the TAB patch:
# https://github.com/minkphp/MinkSelenium2Driver/pull/244
# And I assign the user user0 to the group Group1
# And I see that the section Group1 is shown
# And I withdraw the user user0 from the group Group1
# And I see that the section Group1 does not have a count
# And I click the "icon-delete" button on the Group1 section
# And I see that the confirmation dialog is shown
# When I click the "Yes" button of the confirmation dialog
# Then I see that the section Group1 is not shown
# Scenario: change email
# Given I act as Jane
# And I am logged in as the admin
# And I open the User settings
# And I see that the list of users contains the user user0
# And I see that the mailAddress of user0 is ""
# When I set the mailAddress for user0 to "test@nextcloud.com"
# And I see that the mailAddress cell for user user0 is done loading
# Then I see that the mailAddress of user0 is "test@nextcloud.com"
Scenario: change user quota
Given I act as Jane
And I am logged in as the admin
And I open the User settings
# And I see that the list of users contains the user user0
# When I toggle the edit mode for the user user0
# Then I see that the edit mode is on for user user0
# And I see that the user quota of user0 is Unlimited
# disabled because we need the TAB patch:
# https://github.com/minkphp/MinkSelenium2Driver/pull/244
# When I set the user user0 quota to 1GB
# And I see that the quota cell for user user0 is done loading
# Then I see that the user quota of user0 is "1 GB"
# When I set the user user0 quota to Unlimited
# And I see that the quota cell for user user0 is done loading
# Then I see that the user quota of user0 is Unlimited
# When I set the user user0 quota to 0
# And I see that the quota cell for user user0 is done loading
# Then I see that the user quota of user0 is "0 B"
# When I set the user user0 quota to Default
# And I see that the quota cell for user user0 is done loading
# Then I see that the user quota of user0 is "Default quota"
Loading…
Cancel
Save