0
0
Fork 0
mirror of https://github.com/nextcloud/server.git synced 2025-03-14 16:33:21 +00:00

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>
This commit is contained in:
Ferdinand Thiessen 2023-10-18 15:19:11 +02:00
parent 888473f5e2
commit 5b0c27b6da
No known key found for this signature in database
GPG key ID: 45FAE7268762B400
20 changed files with 566 additions and 677 deletions

View file

@ -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

View file

@ -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

View file

@ -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="{

View file

@ -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') }}

View file

@ -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"

View file

@ -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') }}

View file

@ -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)
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 })
})
// 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()
// 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')
})
})

View file

@ -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()

View file

@ -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')
})
})
})

View file

@ -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

View file

@ -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')
})
})
it('Can delete a group', () => {
// 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')
// open the actions menu for the group
cy.contains('li', 'foo').within(() => {
cy.get('button.action-item__menutoggle').click()
})
})
// The "Remove group" action in the actions menu is shown and clicked
cy.get('.action-item__popper button').contains('Remove group').should('exist').click()
// And confirmation dialog accepted
cy.get('.modal-container button').contains('Confirm').click()
// 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()
})
// 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('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('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(groupName).should('exist').scrollIntoView()
// open the actions menu for the group
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({ 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
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(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)
})
})
})

View file

@ -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 })
// open the User settings as admin
cy.visit('/settings/users')
getUserListRow(jdoe.userId).within(() => {
// 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 })
// open the User settings as admin
cy.visit('/settings/users')
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()
// 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)
})
})
})

View file

@ -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)
})

Binary file not shown.

Binary file not shown.

View file

@ -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:

View file

@ -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");
}
}
}

View file

@ -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"