Merge pull request #40961 from nextcloud/fix/cypress-tests

fix(cypress): Replace flaky password-confirmation hack
This commit is contained in:
Ferdinand Thiessen 2023-10-19 22:02:28 +02:00 committed by GitHub
commit a6c450b481
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 147 additions and 143 deletions

View file

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

View file

@ -36,11 +36,12 @@
:user="user.id" />
</td>
<td class="row__cell row__cell--displayname">
<td class="row__cell row__cell--displayname" data-test-id="cell-displayname">
<template v-if="editing && user.backendCapabilities.setDisplayName">
<NcTextField ref="displayNameField"
data-test="displayNameField"
class="user-row-text-field"
data-test-id="input-displayName"
:data-test-loading="`${loading.displayName}`"
:trailing-button-label="t('settings', 'Submit')"
:class="{ 'icon-loading-small': loading.displayName }"
:show-trailing-button="true"
@ -63,10 +64,13 @@
</template>
</td>
<td class="row__cell"
<td data-test-id="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}`"
:trailing-button-label="t('settings', 'Submit')"
:class="{'icon-loading-small': loading.password}"
:show-trailing-button="true"
@ -89,7 +93,7 @@
</span>
</td>
<td class="row__cell">
<td class="row__cell" data-test-id="cell-email">
<template v-if="editing">
<NcTextField class="user-row-text-field"
:class="{'icon-loading-small': loading.mailAddress}"
@ -100,7 +104,7 @@
trailing-button-icon="arrowRight"
:value.sync="editedMail"
autocapitalize="off"
autocomplete="new-password"
autocomplete="email"
autocorrect="off"
spellcheck="false"
type="email"
@ -112,7 +116,7 @@
</span>
</td>
<td class="row__cell row__cell--large row__cell--multiline">
<td class="row__cell row__cell--large row__cell--multiline" data-test-id="cell-groups">
<template v-if="editing">
<label class="hidden-visually"
:for="'groups' + uniqueId">
@ -142,6 +146,7 @@
</td>
<td v-if="subAdminsGroups.length > 0 && settings.isAdmin"
data-test-id="cell-subadmins"
class="row__cell row__cell--large row__cell--multiline">
<template v-if="editing && settings.isAdmin && subAdminsGroups.length > 0">
<label class="hidden-visually"
@ -168,7 +173,7 @@
</span>
</td>
<td class="row__cell">
<td class="row__cell" data-test-id="cell-quota">
<template v-if="editing">
<label class="hidden-visually"
:for="'quota' + uniqueId">
@ -200,7 +205,7 @@
<td v-if="showConfig.showLanguages"
class="row__cell row__cell--large"
data-test="language">
data-test-id="cell-language">
<template v-if="editing">
<label class="hidden-visually"
:for="'language' + uniqueId">
@ -224,6 +229,7 @@
</td>
<td v-if="showConfig.showUserBackend || showConfig.showStoragePath"
data-test-id="cell-storageLocation"
class="row__cell row__cell--large">
<template v-if="!isObfuscated">
<span v-if="showConfig.showUserBackend">{{ user.backend }}</span>
@ -238,11 +244,11 @@
<td v-if="showConfig.showLastLogin"
:title="userLastLoginTooltip"
class="row__cell"
data-test="lastLogin">
data-test-id="cell-lastLogin">
<span v-if="!isObfuscated">{{ userLastLogin }}</span>
</td>
<td class="row__cell row__cell--large row__cell--fill">
<td class="row__cell row__cell--large row__cell--fill" data-test-id="cell-manager">
<template v-if="editing">
<label class="hidden-visually"
:for="'manager' + uniqueId">
@ -268,7 +274,7 @@
</span>
</td>
<td class="row__cell row__cell--actions">
<td class="row__cell row__cell--actions" data-test-id="cell-actions">
<UserRowActions v-if="visible && !isObfuscated && canEdit && !loading.all"
:actions="userActions"
:disabled="isLoadingField"

View file

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

View file

@ -19,8 +19,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
/// <reference types="cypress-if" />
import { User } from '@nextcloud/cypress'
import { getUserListRow, handlePasswordConfirmation } from './usersUtils'
const admin = new User('admin', 'admin')
const jdoe = new User('jdoe', 'jdoe')
@ -37,7 +38,7 @@ describe('Settings: Create and delete users', function() {
cy.login(admin)
cy.listUsers().then((users) => {
cy.login(admin)
if (users.includes('john')) {
if ((users as string[]).includes('john')) {
// ensure created user is deleted
cy.deleteUser(john).login(admin)
// ensure deleted user is not present
@ -67,18 +68,8 @@ describe('Settings: Create and delete users', function() {
cy.get('button[type="submit"]').click()
})
// Ignore failure if modal is not shown
cy.once('fail', (error) => {
expect(error.name).to.equal('AssertionError')
expect(error).to.have.property('node', '.modal-container')
})
// Make sure no confirmation modal is shown on top of the New user modal
cy.get('body').find('.modal-container').then(($modals) => {
if ($modals.length > 1) {
cy.wrap($modals.first()).find('input[type="password"]').type(admin.password)
cy.wrap($modals.first()).find('button').contains('Confirm').click()
}
})
// Make sure no confirmation modal is shown
handlePasswordConfirmation(admin.password)
// see that the created user is in the list
cy.get('tbody.user-list__body tr[data-test="john"]').within(() => {
@ -112,24 +103,14 @@ describe('Settings: Create and delete users', function() {
cy.get('button[type="submit"]').click()
})
// Ignore failure if modal is not shown
cy.once('fail', (error) => {
expect(error.name).to.equal('AssertionError')
expect(error).to.have.property('node', '.modal-container')
})
// Make sure no confirmation modal is shown on top of the New user modal
cy.get('body').find('.modal-container').then(($modals) => {
if ($modals.length > 1) {
cy.wrap($modals.first()).find('input[type="password"]').type(admin.password)
cy.wrap($modals.first()).find('button').contains('Confirm').click()
}
})
// Make sure no confirmation modal is shown
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')
// see that the list of users contains the user john
cy.contains('john').should('exist')
})
.contains('john')
.should('exist')
})
it('Can delete a user', function() {
@ -151,18 +132,8 @@ describe('Settings: Create and delete users', function() {
// And confirmation dialog accepted
cy.get('.oc-dialog button').contains(`Delete ${jdoe.userId}`).click()
// Ignore failure if modal is not shown
cy.once('fail', (error) => {
expect(error.name).to.equal('AssertionError')
expect(error).to.have.property('node', '.modal-container')
})
// Make sure no confirmation modal is shown
cy.get('body').find('.modal-container').then(($modal) => {
if ($modal.length > 0) {
cy.wrap($modal).find('input[type="password"]').type(admin.password)
cy.wrap($modal).find('button').contains('Confirm').click()
}
})
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

@ -22,12 +22,55 @@
/**
* Assert that `element` does not exist or is not visible
*
* Useful in cases such as when NcModal is opened/closed rapidly
* @param element Element that is inspected
*/
export function assertNotExistOrNotVisible(element: JQuery<HTMLElement>) {
const doesNotExist = element.length === 0
const isNotVisible = !element.is(':visible')
// eslint-disable-next-line no-unused-expressions
expect(doesNotExist || isNotVisible, 'does not exist or is not visible').to.be.true
}
/**
* Get the settings users list
* @return Cypress chainable object
*/
export function getUserList() {
return cy.get('[data-test-id="userList"]')
}
/**
* Get the row entry for given userId within the settings users list
*
* @param userId the user to query
* @return Cypress chainable object
*/
export function getUserListRow(userId: string) {
return getUserList().find(`tr[data-test="${userId}"]`)
}
/**
* Handle the confirm password dialog (if needed)
* @param adminPassword The admin password for the dialog
*/
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()
})
}
return cy.get('body')
.if()
.then(() => handleModal(cy.get('body')))
.else()
// Handle if inside a cy.within
.root().closest('body')
.then(($body) => handleModal(cy.wrap($body)))
}

View file

@ -21,7 +21,7 @@
*/
import { User } from '@nextcloud/cypress'
import { assertNotExistOrNotVisible } from './usersUtils.js'
import { assertNotExistOrNotVisible, getUserList } from './usersUtils.js'
const admin = new User('admin', 'admin')
@ -77,8 +77,8 @@ describe('Settings: Show and hide columns', function() {
})
// see that the language column is in all user rows
cy.get('tbody.user-list__body tr').each(($row) => {
cy.wrap($row).get('[data-test="language"]').should('exist')
getUserList().find('tbody tr').each(($row) => {
cy.wrap($row).get('[data-test-id="cell-language"]').should('exist')
})
})
@ -89,8 +89,8 @@ describe('Settings: Show and hide columns', function() {
})
// see that the last login column is in all user rows
cy.get('tbody.user-list__body tr').each(($row) => {
cy.wrap($row).get('[data-test="lastLogin"]').should('exist')
getUserList().find('tbody tr').each(($row) => {
cy.wrap($row).get('[data-test-id="cell-lastLogin"]').should('exist')
})
// open the settings dialog
@ -112,8 +112,8 @@ describe('Settings: Show and hide columns', function() {
})
// see that the last login column is not in all user rows
cy.get('tbody.user-list__body tr').each(($row) => {
cy.wrap($row).get('[data-test="lastLogin"]').should('not.exist')
getUserList().find('tbody tr').each(($row) => {
cy.wrap($row).get('[data-test-id="cell-lastLogin"]').should('not.exist')
})
})
})

View file

@ -21,6 +21,7 @@
*/
import { User } from '@nextcloud/cypress'
import { handlePasswordConfirmation } from './usersUtils'
const admin = new User('admin', 'admin')
@ -46,18 +47,8 @@ describe('Settings: Create and delete groups', () => {
cy.get('input[placeholder="Group name"] ~ button').click()
})
// Ignore failure if modal is not shown
cy.once('fail', (error) => {
expect(error.name).to.equal('AssertionError')
expect(error).to.have.property('node', '.modal-container')
})
// Make sure no confirmation modal is shown
cy.get('body').find('.modal-container').then(($modals) => {
if ($modals.length > 0) {
cy.wrap($modals.first()).find('input[type="password"]').type(admin.password)
cy.wrap($modals.first()).find('button').contains('Confirm').click()
}
})
handlePasswordConfirmation(admin.password)
// see that the created group is in the list
cy.get('ul.app-navigation__list').within(() => {
@ -82,18 +73,13 @@ describe('Settings: Create and delete groups', () => {
// And confirmation dialog accepted
cy.get('.modal-container button').contains('Confirm').click()
// Ignore failure if modal is not shown
cy.once('fail', (error) => {
expect(error.name).to.equal('AssertionError')
expect(error).to.have.property('node', '.modal-container')
})
// Make sure no confirmation modal is shown on top of the Remove group modal
cy.get('body').find('.modal-container').then(($modals) => {
if ($modals.length > 1) {
cy.wrap($modals.first()).find('input[type="password"]').type(admin.password)
cy.wrap($modals.first()).find('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(() => {

View file

@ -21,6 +21,7 @@
*/
import { User } from '@nextcloud/cypress'
import { getUserListRow, handlePasswordConfirmation } from './usersUtils'
const admin = new User('admin', 'admin')
const jdoe = new User('jdoe', 'jdoe')
@ -34,16 +35,14 @@ describe('Settings: Change user properties', function() {
})
beforeEach(function() {
cy.get(`tbody.user-list__body tr[data-test="${jdoe.userId}"]`).within(() => {
// reset edit mode for the user jdoe
cy.get('td.row__cell--actions .action-items > button:first-of-type')
.invoke('attr', 'title')
.then((title) => {
if (title === 'Done') {
cy.get('td.row__cell--actions .action-items > button:first-of-type').click()
}
})
})
// 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(() => {
@ -51,51 +50,45 @@ describe('Settings: Change user properties', function() {
})
it('Can change the display name', function() {
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')
// see that the list of users contains the user jdoe
getUserListRow(jdoe.userId).should('exist')
// toggle the edit mode for the user jdoe
cy.get('td.row__cell--actions .action-items > button:first-of-type').click()
})
.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 })
cy.get(`tbody.user-list__body tr[data-test="${jdoe.userId}"]`).within(() => {
getUserListRow(jdoe.userId).within(() => {
// set the display name
cy.get('input[data-test="displayNameField"]').should('exist').and('have.value', 'jdoe')
cy.get('input[data-test="displayNameField"]').clear()
cy.get('input[data-test="displayNameField"]').type('John Doe')
cy.get('input[data-test="displayNameField"]').should('have.value', 'John Doe')
cy.get('input[data-test="displayNameField"] ~ button').click()
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()
// Ignore failure if modal is not shown
cy.once('fail', (error) => {
expect(error.name).to.equal('AssertionError')
expect(error).to.have.property('node', '.modal-container')
})
// Make sure no confirmation modal is shown
cy.root().closest('body').find('.modal-container').then(($modal) => {
if ($modal.length > 0) {
cy.wrap($modal).find('input[type="password"]').type(admin.password)
cy.wrap($modal).find('button').contains('Confirm').click()
}
})
handlePasswordConfirmation(admin.password)
// see that the display name cell is done loading
cy.get('.user-row-text-field.icon-loading-small').should('exist')
cy.waitUntil(() => cy.get('.user-row-text-field.icon-loading-small').should('not.exist'), { timeout: 10000 })
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 })
})
// Success message is shown
cy.get('.toastify.toast-success').contains(/Display.+name.+was.+successfully.+changed/i).should('exist')
})
it('Can change the password', function() {
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')
// see that the list of users contains the user jdoe
getUserListRow(jdoe.userId).should('exist')
// toggle the edit mode for the user jdoe
cy.get('td.row__cell--actions .action-items > button:first-of-type').click()
})
.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 })
cy.get(`tbody.user-list__body tr[data-test="${jdoe.userId}"]`).within(() => {
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
@ -104,24 +97,14 @@ describe('Settings: Change user properties', function() {
cy.get('input[type="password"]').should('have.value', '123456')
cy.get('input[type="password"] ~ button').click()
// Ignore failure if modal is not shown
cy.once('fail', (error) => {
expect(error.name).to.equal('AssertionError')
expect(error).to.have.property('node', '.modal-container')
})
// Make sure no confirmation modal is shown
cy.root().closest('body').find('.modal-container').then(($modal) => {
if ($modal.length > 0) {
cy.wrap($modal).find('input[type="password"]').type(admin.password)
cy.wrap($modal).find('button').contains('Confirm').click()
}
})
handlePasswordConfirmation(admin.password)
// see that the password cell for user user0 is done loading
cy.get('.user-row-text-field.icon-loading-small').should('exist')
cy.waitUntil(() => cy.get('.user-row-text-field.icon-loading-small').should('not.exist'), { timeout: 10000 })
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 })
// password input is emptied on change
cy.get('input[type="password"]').should('have.value', '')
cy.get('[data-test-id="input-password"]').should('have.value', '')
})
// Success message is shown
cy.get('.toastify.toast-success').contains(/Password.+successfully.+changed/i).should('exist')

View file

@ -25,6 +25,7 @@ import { addCommands, User } from '@nextcloud/cypress'
import { basename } from 'path'
// Add custom commands
import 'cypress-if'
import 'cypress-wait-until'
addCommands()

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

10
package-lock.json generated
View file

@ -114,6 +114,7 @@
"babel-loader-exclude-node-modules-except": "^1.2.1",
"css-loader": "^6.8.1",
"cypress": "^13.3.0",
"cypress-if": "^1.10.5",
"cypress-wait-until": "^2.0.1",
"dockerode": "^4.0.0",
"eslint-plugin-cypress": "^2.14.0",
@ -9729,6 +9730,15 @@
"node": "^16.0.0 || ^18.0.0 || >=20.0.0"
}
},
"node_modules/cypress-if": {
"version": "1.10.5",
"resolved": "https://registry.npmjs.org/cypress-if/-/cypress-if-1.10.5.tgz",
"integrity": "sha512-7EJSTvoUM+6XumIA7T0cU69dkkdUEmncvIuFFgQ3ry57m2kXc9vtNTCdGjfnGQgMBDRR6vtx7no1ZDDg8IOICA==",
"dev": true,
"dependencies": {
"debug": "^4.3.4"
}
},
"node_modules/cypress-wait-until": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/cypress-wait-until/-/cypress-wait-until-2.0.1.tgz",

View file

@ -141,6 +141,7 @@
"babel-loader-exclude-node-modules-except": "^1.2.1",
"css-loader": "^6.8.1",
"cypress": "^13.3.0",
"cypress-if": "^1.10.5",
"cypress-wait-until": "^2.0.1",
"dockerode": "^4.0.0",
"eslint-plugin-cypress": "^2.14.0",