mirror of
https://github.com/nextcloud/server.git
synced 2026-06-11 09:42:09 -04:00
test: migrate appstore tests to PlayWright
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
This commit is contained in:
parent
ae8d311a33
commit
9fc19ac7f5
17 changed files with 504 additions and 1827 deletions
|
|
@ -1,279 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023-2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { User } from '@nextcloud/e2e-test-server/cypress'
|
||||
import { handlePasswordConfirmation } from '../core-utils.ts'
|
||||
|
||||
const admin = new User('admin', 'admin')
|
||||
|
||||
describe('Settings: App management', { testIsolation: true }, () => {
|
||||
beforeEach(() => {
|
||||
// disable QA if already enabled
|
||||
cy.runOccCommand('app:disable -n testing')
|
||||
// enable notification if already disabled
|
||||
cy.runOccCommand('app:enable -n updatenotification')
|
||||
|
||||
// I am logged in as the admin
|
||||
cy.login(admin)
|
||||
|
||||
// Intercept the apps list request
|
||||
cy.intercept('GET', '/ocs/v2.php/apps/appstore/api/v1/apps').as('fetchAppsList')
|
||||
|
||||
// I open the Apps management
|
||||
cy.visit('/settings/apps/installed')
|
||||
|
||||
// Wait for the apps list to load
|
||||
cy.wait('@fetchAppsList')
|
||||
})
|
||||
|
||||
it('Can enable an installed app', () => {
|
||||
cy.intercept('POST', '/ocs/v2.php/apps/appstore/api/v1/apps/enable').as('enableApp')
|
||||
|
||||
cy.findByRole('table').should('exist')
|
||||
// Wait for the app list to load
|
||||
.contains('tr', 'QA testing', { timeout: 10000 })
|
||||
.should('exist')
|
||||
.findByRole('button', { name: 'Enable' })
|
||||
// I enable the "QA testing" app
|
||||
.click({ force: true })
|
||||
|
||||
handlePasswordConfirmation(admin.password)
|
||||
|
||||
cy.wait('@enableApp')
|
||||
|
||||
// Wait until we see the disable button for the app
|
||||
cy.findByRole('table').should('exist')
|
||||
.contains('tr', 'QA testing')
|
||||
.should('exist')
|
||||
// I see the disable button for the app
|
||||
.findByRole('button', { name: 'Disable' })
|
||||
.should('be.visible')
|
||||
|
||||
// Change to enabled apps view
|
||||
cy.findByRole('navigation', { name: 'Appstore categories' })
|
||||
.within(() => {
|
||||
cy.findByRole('link', { name: 'Active apps' })
|
||||
.should('be.visible')
|
||||
.click({ force: true })
|
||||
})
|
||||
|
||||
cy.url().should('match', /settings\/apps\/enabled$/)
|
||||
// I see that the "QA testing" app has been enabled
|
||||
cy.findByRole('table')
|
||||
.contains('tr', 'QA testing')
|
||||
})
|
||||
|
||||
it('Can disable an installed app', () => {
|
||||
cy.intercept('POST', '/ocs/v2.php/apps/appstore/api/v1/apps/disable').as('disableApp')
|
||||
|
||||
cy.findByRole('table')
|
||||
.should('exist')
|
||||
// Wait for the app list to load
|
||||
.contains('tr', 'Update notification', { timeout: 10000 })
|
||||
.should('exist')
|
||||
// I disable the "Update notification" app
|
||||
.findByRole('button', { name: 'Disable' })
|
||||
.click({ force: true })
|
||||
|
||||
handlePasswordConfirmation(admin.password)
|
||||
cy.wait('@disableApp')
|
||||
|
||||
// Wait until we see the disable button for the app
|
||||
cy.findByRole('table').should('exist')
|
||||
.contains('tr', 'Update notification')
|
||||
.should('exist')
|
||||
// I see the enable button for the app
|
||||
.findByRole('button', { name: 'Enable' })
|
||||
.should('exist')
|
||||
|
||||
// Change to disabled apps view
|
||||
cy.findByRole('navigation', { name: 'Appstore categories' })
|
||||
.within(() => {
|
||||
cy.findByRole('link', { name: 'Disabled apps' }).click({ force: true })
|
||||
})
|
||||
cy.url().should('match', /settings\/apps\/disabled$/)
|
||||
|
||||
// I see that the "Update notification" app has been disabled
|
||||
cy.findByRole('table')
|
||||
.contains('tr', 'Update notification')
|
||||
})
|
||||
|
||||
it('Browse enabled apps', () => {
|
||||
// When I open the "Active apps" section
|
||||
cy.findByRole('navigation', { name: 'Appstore categories' })
|
||||
.within(() => {
|
||||
cy.findByRole('link', { name: 'Active apps' })
|
||||
.should('be.visible')
|
||||
.click({ force: true })
|
||||
})
|
||||
|
||||
// Then I see that the current section is "Active apps"
|
||||
cy.url().should('match', /settings\/apps\/enabled$/)
|
||||
cy.findByRole('navigation', { name: 'Appstore categories' })
|
||||
.within(() => {
|
||||
cy.findByRole('link', { name: 'Active apps', current: 'page' })
|
||||
.should('be.visible')
|
||||
})
|
||||
|
||||
// I see that there are only enabled apps
|
||||
cy.findByRole('table')
|
||||
.should('exist')
|
||||
.find('tr button')
|
||||
.each(($action) => {
|
||||
cy.wrap($action).should('not.contain', 'Enable')
|
||||
})
|
||||
})
|
||||
|
||||
it('Browse disabled apps', () => {
|
||||
// When I open the "Active Disabled" section
|
||||
cy.findByRole('navigation', { name: 'Appstore categories' })
|
||||
.within(() => {
|
||||
cy.findByRole('link', { name: 'Disabled apps' })
|
||||
.as('disabledAppsLink')
|
||||
.should('be.visible')
|
||||
.and('not.have.attr', 'aria-current')
|
||||
cy.get('@disabledAppsLink')
|
||||
.click({ force: true })
|
||||
})
|
||||
|
||||
// Then I see that the current section is "Disabled apps"
|
||||
cy.url().should('match', /settings\/apps\/disabled$/)
|
||||
cy.findByRole('navigation', { name: 'Appstore categories' })
|
||||
.within(() => {
|
||||
cy.findByRole('link', { name: 'Disabled apps', current: 'page' })
|
||||
.should('be.visible')
|
||||
})
|
||||
|
||||
// I see that there are only disabled apps
|
||||
cy.findByRole('table')
|
||||
.should('exist')
|
||||
.find('tr button')
|
||||
.each(($action) => {
|
||||
cy.wrap($action).should('not.contain', 'Disable')
|
||||
})
|
||||
})
|
||||
|
||||
it('Browse app bundles', () => {
|
||||
// When I open the "App bundles" section
|
||||
cy.findByRole('navigation', { name: 'Appstore categories' })
|
||||
.within(() => {
|
||||
cy.findByRole('link', { name: 'App bundles' })
|
||||
.as('appBundlesLink')
|
||||
.should('be.visible')
|
||||
.and('not.have.attr', 'aria-current')
|
||||
cy.get('@appBundlesLink')
|
||||
.click({ force: true })
|
||||
})
|
||||
|
||||
// Then I see that the current section is "App bundles"
|
||||
cy.url().should('match', /settings\/apps\/bundles$/)
|
||||
cy.findByRole('navigation', { name: 'Appstore categories' })
|
||||
.within(() => {
|
||||
cy.findByRole('link', { name: 'App bundles', current: 'page' })
|
||||
.should('be.visible')
|
||||
})
|
||||
|
||||
// I see the app bundles
|
||||
cy.findByRole('heading', { name: 'Enterprise bundle' })
|
||||
.should('be.visible')
|
||||
cy.findByRole('heading', { name: 'Education bundle' })
|
||||
.should('be.visible')
|
||||
})
|
||||
|
||||
it('View app details', () => {
|
||||
// When I click on the "QA testing" app
|
||||
cy.findByRole('table')
|
||||
.contains('a', 'QA testing')
|
||||
.click({ force: true })
|
||||
// I see that the app details are shown
|
||||
cy.get('#app-sidebar-vue')
|
||||
.should('be.visible')
|
||||
.find('.app-sidebar-header__info')
|
||||
.should('contain', 'QA testing')
|
||||
cy.get('#app-sidebar-vue').contains('a', 'View in store').should('exist')
|
||||
cy.get('#app-sidebar-vue')
|
||||
.findByRole('button', { name: 'Enable' })
|
||||
.should('be.visible')
|
||||
cy.get('#app-sidebar-vue')
|
||||
.findByRole('button', { name: 'Remove' })
|
||||
.should('be.visible')
|
||||
cy.get('#app-sidebar-vue').contains(/Version \d+\.\d+\.\d+/).should('be.visible')
|
||||
})
|
||||
|
||||
it('Limit app usage to group', () => {
|
||||
// When I open the "Active apps" section
|
||||
cy.findByRole('navigation', { name: 'Appstore categories' })
|
||||
.within(() => {
|
||||
cy.findByRole('link', { name: 'Active apps' })
|
||||
.should('be.visible')
|
||||
.click({ force: true })
|
||||
})
|
||||
|
||||
// Then I see that the current section is "Active apps"
|
||||
cy.url().should('match', /settings\/apps\/enabled$/)
|
||||
|
||||
// Then I select the app
|
||||
cy.findByRole('table')
|
||||
.should('exist')
|
||||
.contains('tr a', 'Dashboard', { timeout: 10000 })
|
||||
.click()
|
||||
|
||||
// Then I enable "limit app to group"
|
||||
cy.findByRole('button', { name: 'Limit to groups' })
|
||||
.click()
|
||||
|
||||
// Then I select a group
|
||||
cy.findByRole('dialog')
|
||||
.should('be.visible')
|
||||
.within(() => {
|
||||
cy.get('input')
|
||||
.should('be.focused')
|
||||
.type('admin')
|
||||
})
|
||||
cy.findByRole('option', { name: /admin/ })
|
||||
.click()
|
||||
cy.findByRole('button', { name: 'Save' })
|
||||
.click()
|
||||
|
||||
handlePasswordConfirmation(admin.password)
|
||||
|
||||
cy.get('#app-sidebar-vue')
|
||||
.findByRole('list', { name: 'Limited to groups' })
|
||||
.findByRole('listitem', { name: /admin/ })
|
||||
.should('be.visible')
|
||||
|
||||
// Then I disable the group limitation
|
||||
cy.get('#app-sidebar-vue')
|
||||
.findByRole('button', { name: 'Limit to groups' })
|
||||
.click()
|
||||
cy.findByRole('dialog')
|
||||
.should('be.visible')
|
||||
.within(() => {
|
||||
cy.findByRole('button', { name: 'Deselect admin' })
|
||||
.should('be.visible')
|
||||
.click()
|
||||
cy.findByRole('button', { name: 'Save' })
|
||||
.click()
|
||||
})
|
||||
|
||||
handlePasswordConfirmation(admin.password)
|
||||
|
||||
cy.get('#app-sidebar-vue')
|
||||
.findByRole('list', { name: 'Limited to groups' })
|
||||
.should('not.exist')
|
||||
})
|
||||
|
||||
/*
|
||||
* TODO: Improve testing with app store as external API
|
||||
* The following scenarios require the files_antivirus and calendar app
|
||||
* being present in the app store with support for the current server version
|
||||
* Ideally we would have either a dummy app store endpoint with some test apps
|
||||
* or even an app store instance running somewhere to properly test this.
|
||||
* This is also a requirement to properly test updates of apps
|
||||
*/
|
||||
// TODO: View app details for app store apps
|
||||
// TODO: Install an app from the app store
|
||||
// TODO: Show section from app store
|
||||
})
|
||||
|
|
@ -1,166 +0,0 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
const themesToTest = ['light', 'dark', 'light-highcontrast', 'dark-highcontrast']
|
||||
|
||||
const testCases = {
|
||||
'Main text': {
|
||||
foregroundColors: [
|
||||
'color-main-text',
|
||||
// 'color-text-light', deprecated
|
||||
// 'color-text-lighter', deprecated
|
||||
'color-text-maxcontrast',
|
||||
],
|
||||
backgroundColors: [
|
||||
'color-main-background',
|
||||
'color-background-hover',
|
||||
'color-background-dark',
|
||||
// 'color-background-darker', this should only be used for elements not for text
|
||||
],
|
||||
},
|
||||
'blurred background': {
|
||||
foregroundColors: [
|
||||
'color-main-text',
|
||||
'color-text-maxcontrast-blur',
|
||||
],
|
||||
backgroundColors: [
|
||||
'color-main-background-blur',
|
||||
],
|
||||
},
|
||||
Primary: {
|
||||
foregroundColors: [
|
||||
'color-primary-text',
|
||||
],
|
||||
backgroundColors: [
|
||||
// 'color-primary-default', this should only be used for elements not for text!
|
||||
// 'color-primary-hover', this should only be used for elements and not for text!
|
||||
'color-primary',
|
||||
],
|
||||
},
|
||||
'Primary light': {
|
||||
foregroundColors: [
|
||||
'color-primary-light-text',
|
||||
],
|
||||
backgroundColors: [
|
||||
'color-primary-light',
|
||||
'color-primary-light-hover',
|
||||
],
|
||||
},
|
||||
'Primary element': {
|
||||
foregroundColors: [
|
||||
'color-primary-element-text',
|
||||
'color-primary-element-text-dark',
|
||||
],
|
||||
backgroundColors: [
|
||||
'color-primary-element',
|
||||
'color-primary-element-hover',
|
||||
],
|
||||
},
|
||||
'Primary element light': {
|
||||
foregroundColors: [
|
||||
'color-primary-element-light-text',
|
||||
],
|
||||
backgroundColors: [
|
||||
'color-primary-element-light',
|
||||
'color-primary-element-light-hover',
|
||||
],
|
||||
},
|
||||
'Severity information texts': {
|
||||
foregroundColors: [
|
||||
'color-error-text',
|
||||
'color-warning-text',
|
||||
'color-success-text',
|
||||
'color-info-text',
|
||||
],
|
||||
backgroundColors: [
|
||||
'color-main-background',
|
||||
'color-background-hover',
|
||||
],
|
||||
},
|
||||
// only most important severity colors are supported on the blur
|
||||
'Severity information on blur': {
|
||||
foregroundColors: [
|
||||
'color-error-text',
|
||||
'color-success-text',
|
||||
],
|
||||
backgroundColors: [
|
||||
'color-main-background-blur',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a wrapper element with color and background set
|
||||
*
|
||||
* @param foreground The foreground color (css variable without leading --)
|
||||
* @param background The background color
|
||||
*/
|
||||
function createTestCase(foreground: string, background: string) {
|
||||
const wrapper = document.createElement('div')
|
||||
wrapper.style.padding = '14px'
|
||||
wrapper.style.color = `var(--${foreground})`
|
||||
wrapper.style.backgroundColor = `var(--${background})`
|
||||
if (background.includes('blur')) {
|
||||
wrapper.style.backdropFilter = 'var(--filter-background-blur)'
|
||||
}
|
||||
|
||||
const testCase = document.createElement('div')
|
||||
testCase.innerText = `${foreground} ${background}`
|
||||
testCase.setAttribute('data-cy-testcase', '')
|
||||
|
||||
wrapper.appendChild(testCase)
|
||||
return wrapper
|
||||
}
|
||||
|
||||
describe('Accessibility of Nextcloud theming colors', () => {
|
||||
for (const theme of themesToTest) {
|
||||
context(`Theme: ${theme}`, () => {
|
||||
before(() => {
|
||||
cy.createRandomUser().then(($user) => {
|
||||
// set user theme
|
||||
cy.runOccCommand(`user:setting -- '${$user.userId}' theming enabled-themes '[\\"${theme}\\"]'`)
|
||||
cy.login($user)
|
||||
cy.visit('/')
|
||||
cy.injectAxe({ axeCorePath: 'node_modules/axe-core/axe.min.js' })
|
||||
})
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.document().then((doc) => {
|
||||
// Unset background image and thus use background-color for testing blur background (images do not work with axe-core)
|
||||
doc.body.style.backgroundImage = 'unset'
|
||||
|
||||
const root = doc.querySelector('#content')
|
||||
if (root === null) {
|
||||
throw new Error('No test root found')
|
||||
}
|
||||
root.innerHTML = ''
|
||||
})
|
||||
})
|
||||
|
||||
for (const [name, { backgroundColors, foregroundColors }] of Object.entries(testCases)) {
|
||||
context(`Accessibility of CSS color variables for ${name}`, () => {
|
||||
for (const foreground of foregroundColors) {
|
||||
for (const background of backgroundColors) {
|
||||
it(`color contrast of ${foreground} on ${background}`, () => {
|
||||
cy.document().then((doc) => {
|
||||
const element = createTestCase(foreground, background)
|
||||
const root = doc.querySelector('#content')
|
||||
|
||||
expect(root).not.to.be.undefined
|
||||
|
||||
root!.appendChild(element)
|
||||
|
||||
cy.checkA11y('[data-cy-testcase]', {
|
||||
runOnly: ['color-contrast'],
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
@ -1,385 +0,0 @@
|
|||
/*!
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { User } from '@nextcloud/e2e-test-server/cypress'
|
||||
import { NavigationHeader } from '../../pages/NavigationHeader.ts'
|
||||
import {
|
||||
defaultBackground,
|
||||
defaultPrimary,
|
||||
pickColor,
|
||||
validateBodyThemingCss,
|
||||
validateUserThemingDefaultCss,
|
||||
} from './themingUtils.ts'
|
||||
|
||||
const admin = new User('admin', 'admin')
|
||||
|
||||
describe('Remove the default background and restore it', { testIsolation: false }, function() {
|
||||
before(function() {
|
||||
// Just in case previous test failed
|
||||
cy.resetAdminTheming()
|
||||
cy.login(admin)
|
||||
})
|
||||
|
||||
it('See the admin theming section', function() {
|
||||
cy.visit('/settings/admin/theming')
|
||||
cy.findByRole('heading', { name: 'Background and color' })
|
||||
.should('exist')
|
||||
.scrollIntoView()
|
||||
})
|
||||
|
||||
it('Remove the default background', function() {
|
||||
cy.intercept('*/apps/theming/ajax/updateStylesheet').as('removeBackground')
|
||||
cy.intercept('*/apps/theming/theme/default.css?*').as('cssLoaded')
|
||||
|
||||
cy.findByRole('checkbox', { name: /remove background image/i })
|
||||
.should('exist')
|
||||
.should('not.be.checked')
|
||||
.check({ force: true })
|
||||
|
||||
cy.wait('@removeBackground')
|
||||
cy.wait('@cssLoaded')
|
||||
|
||||
cy.window()
|
||||
.should(() => validateBodyThemingCss(defaultPrimary, null))
|
||||
cy.waitUntil(() => cy.window().then((win) => {
|
||||
const backgroundPlain = getComputedStyle(win.document.body).getPropertyValue('--image-background')
|
||||
return backgroundPlain !== ''
|
||||
}))
|
||||
})
|
||||
|
||||
it('Screenshot the login page and validate login page', function() {
|
||||
cy.logout()
|
||||
cy.visit('/')
|
||||
|
||||
cy.window()
|
||||
.should(() => validateBodyThemingCss(defaultPrimary, null))
|
||||
cy.screenshot()
|
||||
})
|
||||
|
||||
it('Undo theming settings and validate login page again', function() {
|
||||
cy.resetAdminTheming()
|
||||
cy.visit('/')
|
||||
|
||||
cy.window()
|
||||
.should(() => validateBodyThemingCss())
|
||||
cy.screenshot()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Remove the default background with a custom background color', function() {
|
||||
let selectedColor = ''
|
||||
|
||||
before(function() {
|
||||
// Just in case previous test failed
|
||||
cy.resetAdminTheming()
|
||||
cy.login(admin)
|
||||
})
|
||||
|
||||
it('See the admin theming section', function() {
|
||||
cy.visit('/settings/admin/theming')
|
||||
cy.findByRole('heading', { name: 'Background and color' })
|
||||
.should('exist')
|
||||
.scrollIntoView()
|
||||
})
|
||||
|
||||
it('Change the background color', function() {
|
||||
cy.intercept('*/apps/theming/ajax/updateStylesheet').as('setColor')
|
||||
cy.intercept('*/apps/theming/theme/default.css?*').as('cssLoaded')
|
||||
|
||||
pickColor(cy.findByRole('button', { name: /Background color/ }))
|
||||
.then((color) => {
|
||||
selectedColor = color
|
||||
})
|
||||
|
||||
cy.wait('@setColor')
|
||||
cy.wait('@cssLoaded')
|
||||
|
||||
cy.window()
|
||||
.should(() => validateBodyThemingCss(
|
||||
defaultPrimary,
|
||||
defaultBackground,
|
||||
selectedColor,
|
||||
))
|
||||
})
|
||||
|
||||
it('Remove the default background', function() {
|
||||
cy.intercept('*/apps/theming/ajax/updateStylesheet').as('removeBackground')
|
||||
|
||||
cy.findByRole('checkbox', { name: /remove background image/i })
|
||||
.should('exist')
|
||||
.should('not.be.checked')
|
||||
.check({ force: true })
|
||||
cy.wait('@removeBackground')
|
||||
})
|
||||
|
||||
it('Screenshot the login page and validate login page', function() {
|
||||
cy.logout()
|
||||
cy.visit('/')
|
||||
|
||||
cy.window()
|
||||
.should(() => validateBodyThemingCss(defaultPrimary, null, selectedColor))
|
||||
cy.screenshot()
|
||||
})
|
||||
|
||||
it('Undo theming settings and validate login page again', function() {
|
||||
cy.resetAdminTheming()
|
||||
cy.visit('/')
|
||||
|
||||
cy.window()
|
||||
.should(() => validateBodyThemingCss())
|
||||
cy.screenshot()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Remove the default background with a bright color', function() {
|
||||
const navigationHeader = new NavigationHeader()
|
||||
let selectedColor = ''
|
||||
|
||||
before(function() {
|
||||
// Just in case previous test failed
|
||||
cy.resetAdminTheming()
|
||||
cy.resetUserTheming(admin)
|
||||
cy.login(admin)
|
||||
})
|
||||
|
||||
it('See the admin theming section', function() {
|
||||
cy.visit('/settings/admin/theming')
|
||||
cy.findByRole('heading', { name: 'Background and color' })
|
||||
.should('exist')
|
||||
.scrollIntoView()
|
||||
})
|
||||
|
||||
it('Remove the default background', function() {
|
||||
cy.intercept('*/apps/theming/ajax/updateStylesheet').as('removeBackground')
|
||||
cy.findByRole('checkbox', { name: /remove background image/i })
|
||||
.check({ force: true })
|
||||
cy.wait('@removeBackground')
|
||||
})
|
||||
|
||||
it('Change the background color', function() {
|
||||
cy.intercept('*/apps/theming/ajax/updateStylesheet').as('setColor')
|
||||
cy.intercept('*/apps/theming/theme/default.css?*').as('cssLoaded')
|
||||
|
||||
pickColor(cy.findByRole('button', { name: /Background color/ }), 4)
|
||||
.then((color) => {
|
||||
selectedColor = color
|
||||
})
|
||||
|
||||
cy.wait('@setColor')
|
||||
cy.wait('@cssLoaded')
|
||||
|
||||
cy.window()
|
||||
.should(() => validateBodyThemingCss(defaultPrimary, null, selectedColor))
|
||||
})
|
||||
|
||||
it('See the header being inverted', function() {
|
||||
// Probe the Nextcloud logo: it carries the same
|
||||
// `var(--background-image-invert-if-bright)` filter and is always
|
||||
// present in the header. The waffle launcher's current-app icon only
|
||||
// renders when an app is active, which isn't the case on settings,
|
||||
// and the in-popover tiles use a fixed brightness/invert filter
|
||||
// regardless of theme so they're not a valid inversion probe.
|
||||
cy.waitUntil(() => navigationHeader
|
||||
.logo()
|
||||
.find('.logo')
|
||||
.then((el) => {
|
||||
let ret = true
|
||||
el.each(function() {
|
||||
ret = ret && window.getComputedStyle(this).filter === 'invert(1)'
|
||||
})
|
||||
return ret
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
describe('Disable user theming and enable it back', function() {
|
||||
before(function() {
|
||||
// Just in case previous test failed
|
||||
cy.resetAdminTheming()
|
||||
cy.login(admin)
|
||||
})
|
||||
|
||||
it('See the admin theming section', function() {
|
||||
cy.visit('/settings/admin/theming')
|
||||
cy.findByRole('heading', { name: 'Background and color' })
|
||||
.should('exist')
|
||||
.scrollIntoView()
|
||||
})
|
||||
|
||||
it('Disable user background theming', function() {
|
||||
cy.intercept('*/apps/theming/ajax/updateStylesheet').as('disableUserTheming')
|
||||
|
||||
cy.findByRole('checkbox', { name: /Disable user theming/ })
|
||||
.should('exist')
|
||||
.and('not.be.checked')
|
||||
.check({ force: true })
|
||||
|
||||
cy.wait('@disableUserTheming')
|
||||
})
|
||||
|
||||
it('Login as user', function() {
|
||||
cy.logout()
|
||||
cy.createRandomUser().then((user) => {
|
||||
cy.login(user)
|
||||
})
|
||||
})
|
||||
|
||||
it('User cannot not change background settings', function() {
|
||||
cy.visit('/settings/user/theming')
|
||||
cy.contains('Customization has been disabled by your administrator').should('exist')
|
||||
})
|
||||
})
|
||||
|
||||
describe('The user default background settings reflect the admin theming settings', function() {
|
||||
let selectedColor = ''
|
||||
|
||||
before(function() {
|
||||
// Just in case previous test failed
|
||||
cy.resetAdminTheming()
|
||||
cy.login(admin)
|
||||
})
|
||||
|
||||
after(function() {
|
||||
cy.resetAdminTheming()
|
||||
})
|
||||
|
||||
it('See the admin theming section', function() {
|
||||
cy.visit('/settings/admin/theming')
|
||||
cy.findByRole('heading', { name: 'Background and color' })
|
||||
.should('exist')
|
||||
.scrollIntoView()
|
||||
})
|
||||
|
||||
it('Change the default background', function() {
|
||||
cy.intercept('*/apps/theming/ajax/uploadImage').as('setBackground')
|
||||
cy.intercept('*/apps/theming/theme/default.css?*').as('cssLoaded')
|
||||
|
||||
cy.fixture('image.jpg', null).as('background')
|
||||
cy.get('input[type="file"][name="background"]')
|
||||
.should('exist')
|
||||
.selectFile('@background', { force: true })
|
||||
|
||||
cy.wait('@setBackground')
|
||||
cy.wait('@cssLoaded')
|
||||
|
||||
cy.window()
|
||||
.should(() => validateBodyThemingCss(
|
||||
defaultPrimary,
|
||||
'/apps/theming/image/background?v=',
|
||||
null,
|
||||
))
|
||||
})
|
||||
|
||||
it('Change the background color', function() {
|
||||
cy.intercept('*/apps/theming/ajax/updateStylesheet').as('setColor')
|
||||
cy.intercept('*/apps/theming/theme/default.css?*').as('cssLoaded')
|
||||
|
||||
pickColor(cy.findByRole('button', { name: /Background color/ }))
|
||||
.then((color) => {
|
||||
selectedColor = color
|
||||
})
|
||||
|
||||
cy.wait('@setColor')
|
||||
cy.wait('@cssLoaded')
|
||||
|
||||
cy.window()
|
||||
.should(() => validateBodyThemingCss(
|
||||
defaultPrimary,
|
||||
'/apps/theming/image/background?v=',
|
||||
selectedColor,
|
||||
))
|
||||
})
|
||||
|
||||
it('Login page should match admin theming settings', function() {
|
||||
cy.logout()
|
||||
cy.visit('/')
|
||||
|
||||
cy.window()
|
||||
.should(() => validateBodyThemingCss(
|
||||
defaultPrimary,
|
||||
'/apps/theming/image/background?v=',
|
||||
selectedColor,
|
||||
))
|
||||
})
|
||||
|
||||
it('Login as user', function() {
|
||||
cy.createRandomUser().then((user) => {
|
||||
cy.login(user)
|
||||
})
|
||||
})
|
||||
|
||||
it('See the user background settings', function() {
|
||||
cy.visit('/settings/user/theming')
|
||||
cy.findByRole('heading', { name: 'Background and color' })
|
||||
.scrollIntoView()
|
||||
})
|
||||
|
||||
it('Default user background settings should match admin theming settings', function() {
|
||||
cy.findByRole('button', { name: 'Default background' })
|
||||
.should('exist')
|
||||
.and('have.attr', 'aria-pressed', 'true')
|
||||
|
||||
cy.window()
|
||||
.should(() => validateUserThemingDefaultCss(
|
||||
selectedColor,
|
||||
'/apps/theming/image/background?v=',
|
||||
))
|
||||
})
|
||||
})
|
||||
|
||||
describe('The user default background settings reflect the admin theming settings with background removed', function() {
|
||||
before(function() {
|
||||
// Just in case previous test failed
|
||||
cy.resetAdminTheming()
|
||||
cy.login(admin)
|
||||
})
|
||||
|
||||
after(function() {
|
||||
cy.resetAdminTheming()
|
||||
})
|
||||
|
||||
it('See the admin theming section', function() {
|
||||
cy.visit('/settings/admin/theming')
|
||||
cy.findByRole('heading', { name: 'Background and color' })
|
||||
.should('exist')
|
||||
.scrollIntoView()
|
||||
})
|
||||
|
||||
it('Remove the default background', function() {
|
||||
cy.intercept('*/apps/theming/ajax/updateStylesheet').as('removeBackground')
|
||||
cy.findByRole('checkbox', { name: /remove background image/i })
|
||||
.check({ force: true })
|
||||
cy.wait('@removeBackground')
|
||||
})
|
||||
|
||||
it('Login page should match admin theming settings', function() {
|
||||
cy.logout()
|
||||
cy.visit('/')
|
||||
|
||||
cy.window()
|
||||
.should(() => validateBodyThemingCss(defaultPrimary, null))
|
||||
})
|
||||
|
||||
it('Login as user', function() {
|
||||
cy.createRandomUser().then((user) => {
|
||||
cy.login(user)
|
||||
})
|
||||
})
|
||||
|
||||
it('See the user background settings', function() {
|
||||
cy.visit('/settings/user/theming')
|
||||
cy.findByRole('heading', { name: 'Background and color' })
|
||||
.scrollIntoView()
|
||||
})
|
||||
|
||||
it('Default user background settings should match admin theming settings', function() {
|
||||
cy.findByRole('button', { name: 'Default background' })
|
||||
.should('exist')
|
||||
.and('have.attr', 'aria-pressed', 'true')
|
||||
|
||||
cy.window()
|
||||
.should(() => validateUserThemingDefaultCss(defaultPrimary, null))
|
||||
})
|
||||
})
|
||||
|
|
@ -1,219 +0,0 @@
|
|||
/*!
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { User } from '@nextcloud/e2e-test-server/cypress'
|
||||
|
||||
const admin = new User('admin', 'admin')
|
||||
|
||||
describe('Admin theming: Setting custom project URLs', function() {
|
||||
this.beforeEach(() => {
|
||||
// Just in case previous test failed
|
||||
cy.resetAdminTheming()
|
||||
cy.login(admin)
|
||||
cy.visit('/settings/admin/theming')
|
||||
cy.intercept('POST', '**/apps/theming/ajax/updateStylesheet').as('updateTheming')
|
||||
})
|
||||
|
||||
it('Setting the web link', () => {
|
||||
cy.findByRole('textbox', { name: /web link/i })
|
||||
.and('have.attr', 'type', 'url')
|
||||
.as('input')
|
||||
.scrollIntoView()
|
||||
cy.get('@input')
|
||||
.should('be.visible')
|
||||
.type('{selectAll}http://example.com/path?query#fragment{enter}')
|
||||
|
||||
cy.wait('@updateTheming')
|
||||
|
||||
cy.logout()
|
||||
|
||||
cy.visit('/')
|
||||
cy.contains('a', 'Nextcloud')
|
||||
.should('be.visible')
|
||||
.and('have.attr', 'href', 'http://example.com/path?query#fragment')
|
||||
})
|
||||
|
||||
it('Setting the legal notice link', () => {
|
||||
cy.findByRole('textbox', { name: /legal notice link/i })
|
||||
.should('exist')
|
||||
.and('have.attr', 'type', 'url')
|
||||
.as('input')
|
||||
.scrollIntoView()
|
||||
cy.get('@input')
|
||||
.type('http://example.com/path?query#fragment{enter}')
|
||||
|
||||
cy.wait('@updateTheming')
|
||||
|
||||
cy.logout()
|
||||
|
||||
cy.visit('/')
|
||||
cy.contains('a', /legal notice/i)
|
||||
.should('be.visible')
|
||||
.and('have.attr', 'href', 'http://example.com/path?query#fragment')
|
||||
})
|
||||
|
||||
it('Setting the privacy policy link', () => {
|
||||
cy.findByRole('textbox', { name: /privacy policy link/i })
|
||||
.should('exist')
|
||||
.as('input')
|
||||
.scrollIntoView()
|
||||
cy.get('@input')
|
||||
.should('have.attr', 'type', 'url')
|
||||
.type('http://privacy.local/path?query#fragment{enter}')
|
||||
|
||||
cy.wait('@updateTheming')
|
||||
|
||||
cy.logout()
|
||||
|
||||
cy.visit('/')
|
||||
cy.contains('a', /privacy policy/i)
|
||||
.should('be.visible')
|
||||
.and('have.attr', 'href', 'http://privacy.local/path?query#fragment')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Admin theming: Web link corner cases', function() {
|
||||
this.beforeEach(() => {
|
||||
// Just in case previous test failed
|
||||
cy.resetAdminTheming()
|
||||
cy.login(admin)
|
||||
cy.visit('/settings/admin/theming')
|
||||
cy.intercept('POST', '**/apps/theming/ajax/updateStylesheet').as('updateTheming')
|
||||
})
|
||||
|
||||
it('Already URL encoded', () => {
|
||||
cy.findByRole('textbox', { name: /web link/i })
|
||||
.and('have.attr', 'type', 'url')
|
||||
.as('input')
|
||||
.scrollIntoView()
|
||||
cy.get('@input')
|
||||
.should('be.visible')
|
||||
.type('{selectAll}http://example.com/%22path%20with%20space%22{enter}')
|
||||
|
||||
cy.wait('@updateTheming')
|
||||
|
||||
cy.logout()
|
||||
|
||||
cy.visit('/')
|
||||
cy.contains('a', 'Nextcloud')
|
||||
.should('be.visible')
|
||||
.and('have.attr', 'href', 'http://example.com/%22path%20with%20space%22')
|
||||
})
|
||||
|
||||
it('URL with double quotes', () => {
|
||||
cy.findByRole('textbox', { name: /web link/i })
|
||||
.and('have.attr', 'type', 'url')
|
||||
.as('input')
|
||||
.scrollIntoView()
|
||||
cy.get('@input')
|
||||
.should('be.visible')
|
||||
.type('{selectAll}http://example.com/"path"{enter}')
|
||||
|
||||
cy.wait('@updateTheming')
|
||||
|
||||
cy.logout()
|
||||
|
||||
cy.visit('/')
|
||||
cy.contains('a', 'Nextcloud')
|
||||
.should('be.visible')
|
||||
.and('have.attr', 'href', 'http://example.com/%22path%22')
|
||||
})
|
||||
|
||||
it('URL with double quotes and already encoded', () => {
|
||||
cy.findByRole('textbox', { name: /web link/i })
|
||||
.and('have.attr', 'type', 'url')
|
||||
.as('input')
|
||||
.scrollIntoView()
|
||||
cy.get('@input')
|
||||
.should('be.visible')
|
||||
.type('{selectAll}http://example.com/"the%20path"{enter}')
|
||||
|
||||
cy.wait('@updateTheming')
|
||||
|
||||
cy.logout()
|
||||
|
||||
cy.visit('/')
|
||||
cy.contains('a', 'Nextcloud')
|
||||
.should('be.visible')
|
||||
.and('have.attr', 'href', 'http://example.com/%22the%20path%22')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Admin theming: Change the login fields then reset them', function() {
|
||||
const name = 'ABCdef123'
|
||||
const url = 'https://example.com'
|
||||
const slogan = 'Testing is fun'
|
||||
|
||||
before(function() {
|
||||
// Just in case previous test failed
|
||||
cy.resetAdminTheming()
|
||||
cy.login(admin)
|
||||
})
|
||||
|
||||
it('See the admin theming section', function() {
|
||||
cy.visit('/settings/admin/theming')
|
||||
cy.findByRole('heading', { name: /^Theming/, level: 2 })
|
||||
.should('exist')
|
||||
.scrollIntoView()
|
||||
})
|
||||
|
||||
it('Change the name field', function() {
|
||||
cy.intercept('*/apps/theming/ajax/updateStylesheet').as('updateFields')
|
||||
|
||||
// Name
|
||||
cy.findByRole('textbox', { name: 'Name' })
|
||||
.should('be.visible')
|
||||
.type(`{selectall}${name}{enter}`)
|
||||
cy.wait('@updateFields')
|
||||
|
||||
// Url
|
||||
cy.findByRole('textbox', { name: 'Web link' })
|
||||
.should('be.visible')
|
||||
.type(`{selectall}${url}{enter}`)
|
||||
cy.wait('@updateFields')
|
||||
|
||||
// Slogan
|
||||
cy.findByRole('textbox', { name: 'Slogan' })
|
||||
.should('be.visible')
|
||||
.type(`{selectall}${slogan}{enter}`)
|
||||
cy.wait('@updateFields')
|
||||
})
|
||||
|
||||
it('Ensure undo button presence', function() {
|
||||
cy.findAllByRole('button', { name: /undo changes/i })
|
||||
.should('have.length', 3)
|
||||
})
|
||||
|
||||
it('Validate login screen changes', function() {
|
||||
cy.logout()
|
||||
cy.visit('/')
|
||||
|
||||
cy.get('[data-login-form-headline]').should('contain.text', name)
|
||||
cy.get('footer p a').should('have.text', name)
|
||||
cy.get('footer p a').should('have.attr', 'href', url)
|
||||
cy.get('footer p').should('contain.text', `– ${slogan}`)
|
||||
})
|
||||
|
||||
it('Undo theming settings', function() {
|
||||
cy.login(admin)
|
||||
cy.visit('/settings/admin/theming')
|
||||
cy.findAllByRole('button', { name: /undo changes/i })
|
||||
.each((button) => {
|
||||
cy.intercept('*/apps/theming/ajax/undoChanges').as('undoField')
|
||||
cy.wrap(button).click()
|
||||
cy.wait('@undoField')
|
||||
})
|
||||
cy.logout()
|
||||
})
|
||||
|
||||
it('Validate login screen changes again', function() {
|
||||
cy.visit('/')
|
||||
|
||||
cy.get('[data-login-form-headline]').should('not.contain.text', name)
|
||||
cy.get('footer p a').should('not.have.text', name)
|
||||
cy.get('footer p a').should('not.have.attr', 'href', url)
|
||||
cy.get('footer p').should('not.contain.text', `– ${slogan}`)
|
||||
})
|
||||
})
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
/*!
|
||||
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { User } from '@nextcloud/e2e-test-server/cypress'
|
||||
import {
|
||||
defaultBackground,
|
||||
defaultPrimary,
|
||||
pickColor,
|
||||
validateBodyThemingCss,
|
||||
} from './themingUtils.ts'
|
||||
|
||||
const admin = new User('admin', 'admin')
|
||||
|
||||
describe('Change the primary color and reset it', function() {
|
||||
let selectedColor = ''
|
||||
|
||||
before(function() {
|
||||
// Just in case previous test failed
|
||||
cy.resetAdminTheming()
|
||||
cy.login(admin)
|
||||
})
|
||||
|
||||
it('See the admin theming section', function() {
|
||||
cy.visit('/settings/admin/theming')
|
||||
cy.findByRole('heading', { name: 'Background and color' })
|
||||
.should('exist')
|
||||
.scrollIntoView()
|
||||
})
|
||||
|
||||
it('Change the primary color', function() {
|
||||
cy.intercept('*/apps/theming/ajax/updateStylesheet').as('setColor')
|
||||
|
||||
pickColor(cy.findByRole('button', { name: /Primary color/ }))
|
||||
.then((color) => {
|
||||
selectedColor = color
|
||||
})
|
||||
|
||||
cy.wait('@setColor')
|
||||
cy.waitUntil(() => validateBodyThemingCss(
|
||||
selectedColor,
|
||||
defaultBackground,
|
||||
defaultPrimary,
|
||||
))
|
||||
})
|
||||
|
||||
it('Screenshot the login page and validate login page', function() {
|
||||
cy.logout()
|
||||
cy.visit('/')
|
||||
|
||||
cy.waitUntil(() => validateBodyThemingCss(
|
||||
selectedColor,
|
||||
defaultBackground,
|
||||
defaultPrimary,
|
||||
))
|
||||
cy.screenshot()
|
||||
})
|
||||
|
||||
it('Undo theming settings and validate login page again', function() {
|
||||
cy.resetAdminTheming()
|
||||
cy.visit('/')
|
||||
|
||||
cy.waitUntil(validateBodyThemingCss)
|
||||
cy.screenshot()
|
||||
})
|
||||
})
|
||||
|
|
@ -1,131 +0,0 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { User } from '@nextcloud/e2e-test-server/cypress'
|
||||
import { NavigationHeader } from '../../pages/NavigationHeader.ts'
|
||||
|
||||
const admin = new User('admin', 'admin')
|
||||
|
||||
after(() => cy.resetAdminTheming())
|
||||
|
||||
describe('Admin theming set default apps', () => {
|
||||
const navigationHeader = new NavigationHeader()
|
||||
|
||||
before(function() {
|
||||
// Just in case previous test failed
|
||||
cy.resetAdminTheming()
|
||||
cy.login(admin)
|
||||
})
|
||||
|
||||
it('See the current default app is the dashboard', () => {
|
||||
// check default route
|
||||
cy.visit('/')
|
||||
cy.url().should('match', /apps\/dashboard/)
|
||||
|
||||
// Also check the top logo link
|
||||
navigationHeader.logo().click()
|
||||
cy.url().should('match', /apps\/dashboard/)
|
||||
})
|
||||
|
||||
it('See the default app settings', () => {
|
||||
cy.visit('/settings/admin/theming')
|
||||
|
||||
cy.get('.settings-section').contains('Navigation bar settings').should('exist')
|
||||
getDefaultAppSwitch().should('exist')
|
||||
getDefaultAppSwitch().scrollIntoView()
|
||||
})
|
||||
|
||||
it('Toggle the "use custom default app" switch', () => {
|
||||
getDefaultAppSwitch().should('not.be.checked')
|
||||
cy.findByRole('region', { name: 'Global default app' })
|
||||
.should('not.exist')
|
||||
|
||||
getDefaultAppSwitch().check({ force: true })
|
||||
getDefaultAppSwitch().should('be.checked')
|
||||
cy.findByRole('region', { name: 'Global default app' })
|
||||
.should('exist')
|
||||
})
|
||||
|
||||
it('See the default app combobox', () => {
|
||||
cy.findByRole('region', { name: 'Global default app' })
|
||||
.should('exist')
|
||||
.findByRole('combobox')
|
||||
.as('defaultAppSelect')
|
||||
.scrollIntoView()
|
||||
|
||||
cy.get('@defaultAppSelect')
|
||||
.findByText('Dashboard')
|
||||
.should('be.visible')
|
||||
cy.get('@defaultAppSelect')
|
||||
.findByText('Files')
|
||||
.should('be.visible')
|
||||
})
|
||||
|
||||
it('See the default app order selector', () => {
|
||||
cy.findByRole('region', { name: 'Global default app' })
|
||||
.should('exist')
|
||||
cy.findByRole('list', { name: 'Navigation bar app order' })
|
||||
.should('exist')
|
||||
.findAllByRole('listitem')
|
||||
.should('have.length', 2)
|
||||
.then((elements) => {
|
||||
const appIDs = elements.map((idx, el) => el.innerText.trim()).get()
|
||||
expect(appIDs).to.deep.eq(['Dashboard', 'Files'])
|
||||
})
|
||||
})
|
||||
|
||||
it('Change the default app', () => {
|
||||
cy.findByRole('list', { name: 'Navigation bar app order' })
|
||||
.should('exist')
|
||||
.as('appOrderSelector')
|
||||
.scrollIntoView()
|
||||
|
||||
cy.get('@appOrderSelector')
|
||||
.findAllByRole('listitem')
|
||||
.filter((_, e) => !!e.innerText.match(/Files/i))
|
||||
.findByRole('button', { name: 'Move up' })
|
||||
.as('moveFilesUpButton')
|
||||
|
||||
cy.get('@moveFilesUpButton').should('be.visible')
|
||||
cy.get('@moveFilesUpButton').click()
|
||||
cy.get('@moveFilesUpButton').should('not.exist')
|
||||
})
|
||||
|
||||
it('See the default app is changed', () => {
|
||||
cy.findByRole('list', { name: 'Navigation bar app order' })
|
||||
.findAllByRole('listitem')
|
||||
.then((elements) => {
|
||||
const appIDs = elements.map((idx, el) => el.innerText.trim()).get()
|
||||
expect(appIDs).to.deep.eq(['Files', 'Dashboard'])
|
||||
})
|
||||
|
||||
// Check the redirect to the default app works
|
||||
cy.request({ url: '/', followRedirect: false }).then((response) => {
|
||||
expect(response.status).to.eq(302)
|
||||
expect(response).to.have.property('headers')
|
||||
expect(response.headers.location).to.contain('/apps/files')
|
||||
})
|
||||
})
|
||||
|
||||
it('Toggle the "use custom default app" switch back to reset the default apps', () => {
|
||||
cy.visit('/settings/admin/theming')
|
||||
getDefaultAppSwitch().scrollIntoView()
|
||||
|
||||
getDefaultAppSwitch().should('be.checked')
|
||||
getDefaultAppSwitch().uncheck({ force: true })
|
||||
getDefaultAppSwitch().should('be.not.checked')
|
||||
|
||||
// Check the redirect to the default app works
|
||||
cy.request({ url: '/', followRedirect: false }).then((response) => {
|
||||
expect(response.status).to.eq(302)
|
||||
expect(response).to.have.property('headers')
|
||||
expect(response.headers.location).to.contain('/apps/dashboard')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function getDefaultAppSwitch() {
|
||||
return cy.findByRole('checkbox', { name: 'Use custom default app' })
|
||||
}
|
||||
|
|
@ -1,302 +0,0 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { User } from '@nextcloud/e2e-test-server/cypress'
|
||||
|
||||
import { NavigationHeader } from '../../pages/NavigationHeader.ts'
|
||||
import { SettingsAppOrderList } from '../../pages/SettingsAppOrderList.ts'
|
||||
import { installTestApp, uninstallTestApp } from '../../support/commonUtils.ts'
|
||||
|
||||
before(() => uninstallTestApp())
|
||||
|
||||
describe('User theming set app order', () => {
|
||||
const navigationHeader = new NavigationHeader()
|
||||
const appOrderList = new SettingsAppOrderList()
|
||||
let user: User
|
||||
|
||||
before(() => {
|
||||
cy.resetAdminTheming()
|
||||
// Create random user for this test
|
||||
cy.createRandomUser().then(($user) => {
|
||||
user = $user
|
||||
cy.login($user)
|
||||
})
|
||||
})
|
||||
|
||||
after(() => cy.deleteUser(user))
|
||||
|
||||
it('See the app order settings', () => {
|
||||
visitAppOrderSettings()
|
||||
})
|
||||
|
||||
it('See that the dashboard app is the first one', () => {
|
||||
const appOrder = ['Dashboard', 'Files']
|
||||
appOrderList.assertAppOrder(appOrder)
|
||||
|
||||
// Check the top app menu order. The launcher grid appends a synthetic
|
||||
// "More apps" / "App store" tile to the user's apps, so iterate
|
||||
// positionally only over the real-app prefix.
|
||||
navigationHeader.getNavigationEntries().then(($entries) => {
|
||||
appOrder.forEach((name, index) => {
|
||||
expect($entries.eq(index)).to.contain.text(name)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('Change the app order', () => {
|
||||
appOrderList.interceptAppOrder()
|
||||
appOrderList.getAppOrderList()
|
||||
.scrollIntoView()
|
||||
appOrderList.getUpButtonForApp('Files')
|
||||
.should('be.visible')
|
||||
.click()
|
||||
appOrderList.waitForAppOrderUpdate()
|
||||
|
||||
appOrderList.assertAppOrder(['Files', 'Dashboard'])
|
||||
})
|
||||
|
||||
it('See the app menu order is changed', () => {
|
||||
cy.reload()
|
||||
const appOrder = ['Files', 'Dashboard']
|
||||
appOrderList.getAppOrderList()
|
||||
.scrollIntoView()
|
||||
appOrderList.assertAppOrder(appOrder)
|
||||
|
||||
// Check the top app menu order. Idempotent open in the page object
|
||||
// re-opens the popover after the reload above. The synthetic trailing
|
||||
// tile is ignored by iterating only over the expected app names.
|
||||
navigationHeader.getNavigationEntries().then(($entries) => {
|
||||
appOrder.forEach((name, index) => {
|
||||
expect($entries.eq(index)).to.contain.text(name)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('User theming set app order with default app', () => {
|
||||
const appOrderList = new SettingsAppOrderList()
|
||||
const navigationHeader = new NavigationHeader()
|
||||
let user: User
|
||||
|
||||
before(() => {
|
||||
cy.resetAdminTheming()
|
||||
// install a third app
|
||||
installTestApp()
|
||||
// set files as default app
|
||||
cy.runOccCommand('config:system:set --value \'files\' defaultapp')
|
||||
|
||||
// Create random user for this test
|
||||
cy.createRandomUser().then(($user) => {
|
||||
user = $user
|
||||
cy.login($user)
|
||||
})
|
||||
})
|
||||
|
||||
after(() => {
|
||||
cy.deleteUser(user)
|
||||
uninstallTestApp()
|
||||
})
|
||||
|
||||
it('See files is the default app', () => {
|
||||
// Check the redirect to the default app works
|
||||
cy.request({ url: '/', followRedirect: false }).then((response) => {
|
||||
expect(response.status).to.eq(302)
|
||||
expect(response).to.have.property('headers')
|
||||
expect(response.headers.location).to.contain('/apps/files')
|
||||
})
|
||||
})
|
||||
|
||||
it('See the app order settings: files is the first one', () => {
|
||||
visitAppOrderSettings()
|
||||
|
||||
const appOrder = ['Files', 'Dashboard', 'Test App 2', 'Test App']
|
||||
appOrderList.getAppOrderList()
|
||||
.scrollIntoView()
|
||||
appOrderList.assertAppOrder(appOrder)
|
||||
})
|
||||
|
||||
it('Can not change the default app', () => {
|
||||
appOrderList.getUpButtonForApp('Files').should('not.exist')
|
||||
appOrderList.getDownButtonForApp('Files').should('not.exist')
|
||||
appOrderList.getUpButtonForApp('Dashboard').should('not.exist')
|
||||
// but can move down
|
||||
appOrderList.getDownButtonForApp('Dashboard').should('be.visible')
|
||||
})
|
||||
|
||||
it('Can see the correct buttons for other apps', () => {
|
||||
appOrderList.getUpButtonForApp('Test App 2').should('be.visible')
|
||||
appOrderList.getDownButtonForApp('Test App 2').should('be.visible')
|
||||
appOrderList.getUpButtonForApp('Test App').should('be.visible')
|
||||
appOrderList.getDownButtonForApp('Test App').should('not.exist')
|
||||
})
|
||||
|
||||
it('Change the order of the other apps', () => {
|
||||
appOrderList.interceptAppOrder()
|
||||
appOrderList.getUpButtonForApp('Test App').click()
|
||||
appOrderList.waitForAppOrderUpdate()
|
||||
appOrderList.getUpButtonForApp('Test App').click()
|
||||
appOrderList.waitForAppOrderUpdate()
|
||||
|
||||
// Can't get up anymore, files is enforced as default app
|
||||
appOrderList.getUpButtonForApp('Test App').should('not.exist')
|
||||
|
||||
// Check the app order settings UI
|
||||
appOrderList.assertAppOrder(['Files', 'Test App', 'Dashboard', 'Test App 2'])
|
||||
})
|
||||
|
||||
it('See the app menu order is changed', () => {
|
||||
cy.reload()
|
||||
|
||||
const appOrder = ['Files', 'Test App', 'Dashboard', 'Test App 2']
|
||||
// Check the top app menu order. See note above: the launcher appends
|
||||
// a synthetic tile that we skip by iterating positionally.
|
||||
navigationHeader.getNavigationEntries().then(($entries) => {
|
||||
appOrder.forEach((name, index) => {
|
||||
expect($entries.eq(index)).to.contain.text(name)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('User theming app order list accessibility', () => {
|
||||
const appOrderList = new SettingsAppOrderList()
|
||||
let user: User
|
||||
|
||||
before(() => {
|
||||
cy.resetAdminTheming()
|
||||
installTestApp()
|
||||
// Create random user for this test
|
||||
cy.createRandomUser().then(($user) => {
|
||||
user = $user
|
||||
cy.login($user)
|
||||
})
|
||||
})
|
||||
|
||||
after(() => {
|
||||
uninstallTestApp()
|
||||
cy.deleteUser(user)
|
||||
})
|
||||
|
||||
it('click the first button', () => {
|
||||
visitAppOrderSettings()
|
||||
appOrderList.interceptAppOrder()
|
||||
appOrderList.getDownButtonForApp('Dashboard')
|
||||
.should('be.visible')
|
||||
.scrollIntoView()
|
||||
appOrderList.getDownButtonForApp('Dashboard')
|
||||
.focus()
|
||||
appOrderList.getDownButtonForApp('Dashboard')
|
||||
.click()
|
||||
appOrderList.waitForAppOrderUpdate()
|
||||
})
|
||||
|
||||
it('see the same app kept the focus', () => {
|
||||
appOrderList.getDownButtonForApp('Dashboard').should('have.focus')
|
||||
})
|
||||
|
||||
it('click the last button', () => {
|
||||
appOrderList.interceptAppOrder()
|
||||
appOrderList.getUpButtonForApp('Dashboard')
|
||||
.should('be.visible')
|
||||
.focus()
|
||||
appOrderList.getUpButtonForApp('Dashboard').click()
|
||||
appOrderList.waitForAppOrderUpdate()
|
||||
})
|
||||
|
||||
it('see the same app kept the focus', () => {
|
||||
appOrderList.getUpButtonForApp('Dashboard').should('not.exist')
|
||||
appOrderList.getDownButtonForApp('Dashboard').should('have.focus')
|
||||
})
|
||||
})
|
||||
|
||||
describe('User theming reset app order', () => {
|
||||
const appOrderList = new SettingsAppOrderList()
|
||||
const navigationHeader = new NavigationHeader()
|
||||
let user: User
|
||||
|
||||
before(() => {
|
||||
cy.resetAdminTheming()
|
||||
// Create random user for this test
|
||||
cy.createRandomUser().then(($user) => {
|
||||
user = $user
|
||||
cy.login($user)
|
||||
})
|
||||
})
|
||||
|
||||
after(() => cy.deleteUser(user))
|
||||
|
||||
it('See that the dashboard app is the first one', () => {
|
||||
visitAppOrderSettings()
|
||||
|
||||
const appOrder = ['Dashboard', 'Files']
|
||||
appOrderList.assertAppOrder(appOrder)
|
||||
|
||||
// Check the top app menu order. See note above on the synthetic tile.
|
||||
navigationHeader.getNavigationEntries().then(($entries) => {
|
||||
appOrder.forEach((name, index) => {
|
||||
expect($entries.eq(index)).to.contain.text(name)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('See the reset button is disabled', () => {
|
||||
appOrderList.getResetButton()
|
||||
.scrollIntoView()
|
||||
appOrderList.getResetButton()
|
||||
.should('be.disabled')
|
||||
})
|
||||
|
||||
it('Change the app order', () => {
|
||||
appOrderList.interceptAppOrder()
|
||||
appOrderList.getUpButtonForApp('Files')
|
||||
.should('be.visible')
|
||||
.click()
|
||||
appOrderList.waitForAppOrderUpdate()
|
||||
|
||||
appOrderList.assertAppOrder(['Files', 'Dashboard'])
|
||||
})
|
||||
|
||||
it('See the reset button is no longer disabled', () => {
|
||||
appOrderList.getResetButton()
|
||||
.scrollIntoView()
|
||||
appOrderList.getResetButton()
|
||||
.should('be.visible')
|
||||
.and('be.enabled')
|
||||
})
|
||||
|
||||
it('Reset the app order', () => {
|
||||
cy.intercept('GET', '/ocs/v2.php/core/navigation/apps').as('loadApps')
|
||||
appOrderList.interceptAppOrder()
|
||||
appOrderList.getResetButton().click({ force: true })
|
||||
|
||||
cy.wait('@updateAppOrder')
|
||||
.its('request.body')
|
||||
.should('have.property', 'configValue', '[]')
|
||||
cy.wait('@loadApps')
|
||||
})
|
||||
|
||||
it('See the app order is restored', () => {
|
||||
const appOrder = ['Dashboard', 'Files']
|
||||
appOrderList.assertAppOrder(appOrder)
|
||||
// Check the top app menu order. See note above on the synthetic tile.
|
||||
navigationHeader.getNavigationEntries().then(($entries) => {
|
||||
appOrder.forEach((name, index) => {
|
||||
expect($entries.eq(index)).to.contain.text(name)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('See the reset button is disabled again', () => {
|
||||
appOrderList.getResetButton()
|
||||
.should('be.disabled')
|
||||
})
|
||||
})
|
||||
|
||||
function visitAppOrderSettings() {
|
||||
cy.visit('/settings/user/theming')
|
||||
cy.findByRole('heading', { name: /Navigation bar settings/ })
|
||||
.should('exist')
|
||||
.scrollIntoView()
|
||||
}
|
||||
|
|
@ -1,270 +0,0 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { User } from '@nextcloud/e2e-test-server/cypress'
|
||||
import { NavigationHeader } from '../../pages/NavigationHeader.ts'
|
||||
import { defaultPrimary, pickColor, validateBodyThemingCss } from './themingUtils.ts'
|
||||
|
||||
const admin = new User('admin', 'admin')
|
||||
|
||||
describe('User default background settings', function() {
|
||||
before(function() {
|
||||
cy.resetAdminTheming()
|
||||
cy.resetUserTheming(admin)
|
||||
cy.createRandomUser().then((user: User) => {
|
||||
cy.login(user)
|
||||
})
|
||||
})
|
||||
|
||||
it('See the user background settings', function() {
|
||||
cy.visit('/settings/user/theming')
|
||||
cy.findByRole('heading', { name: /Appearance and accessibility settings/ })
|
||||
.should('be.visible')
|
||||
})
|
||||
|
||||
it('Default is selected on new users', function() {
|
||||
cy.findByRole('button', { name: 'Default background', pressed: true })
|
||||
.should('exist')
|
||||
.scrollIntoView()
|
||||
})
|
||||
})
|
||||
|
||||
describe('User select shipped backgrounds and remove background', function() {
|
||||
before(function() {
|
||||
cy.createRandomUser().then((user: User) => {
|
||||
cy.login(user)
|
||||
})
|
||||
})
|
||||
|
||||
it('See the user background settings', function() {
|
||||
cy.visit('/settings/user/theming')
|
||||
cy.findByRole('heading', { name: /Background and color/ })
|
||||
.should('exist')
|
||||
.scrollIntoView()
|
||||
})
|
||||
|
||||
it('Select a shipped background', function() {
|
||||
const background = 'anatoly-mikhaltsov-butterfly-wing-scale.jpg'
|
||||
const backgroundName = 'Background picture of a red-ish butterfly wing under microscope'
|
||||
cy.intercept('*/apps/theming/background/shipped').as('setBackground')
|
||||
|
||||
// Select background
|
||||
cy.findByRole('button', { name: backgroundName, pressed: false })
|
||||
.click()
|
||||
cy.findByRole('button', { name: backgroundName, pressed: true })
|
||||
.should('be.visible')
|
||||
|
||||
// Validate changed background and primary
|
||||
cy.wait('@setBackground')
|
||||
cy.waitUntil(() => validateBodyThemingCss('#a53c17', background, '#652e11'))
|
||||
})
|
||||
|
||||
it('Select a bright shipped background', function() {
|
||||
const background = 'bernie-cetonia-aurata-take-off-composition.jpg'
|
||||
const backgroundName = 'Montage of a cetonia aurata bug that takes off with white background'
|
||||
cy.intercept('*/apps/theming/background/shipped').as('setBackground')
|
||||
|
||||
cy.findByRole('button', { name: backgroundName, pressed: false })
|
||||
.click()
|
||||
cy.findByRole('button', { name: backgroundName, pressed: true })
|
||||
.should('be.visible')
|
||||
|
||||
// Validate changed background and primary
|
||||
cy.wait('@setBackground')
|
||||
cy.waitUntil(() => validateBodyThemingCss('#56633d', background, '#dee0d3'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('User select a custom color', function() {
|
||||
before(function() {
|
||||
cy.createRandomUser().then((user: User) => {
|
||||
cy.login(user)
|
||||
})
|
||||
})
|
||||
|
||||
it('See the user background settings', function() {
|
||||
cy.visit('/settings/user/theming')
|
||||
cy.findByRole('heading', { name: /Background and color/ })
|
||||
.should('exist')
|
||||
.scrollIntoView()
|
||||
})
|
||||
|
||||
it('Select a custom color', function() {
|
||||
cy.intercept('*/apps/theming/background/color').as('clearBackground')
|
||||
|
||||
// Clear background
|
||||
pickColor(cy.findByRole('button', { name: 'Plain background' }), 7)
|
||||
|
||||
// Validate clear background
|
||||
cy.wait('@clearBackground')
|
||||
cy.waitUntil(() => validateBodyThemingCss(defaultPrimary, null, '#3794ac'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('User select a bright custom color and remove background', function() {
|
||||
const navigationHeader = new NavigationHeader()
|
||||
|
||||
before(function() {
|
||||
cy.createRandomUser().then((user: User) => {
|
||||
cy.login(user)
|
||||
})
|
||||
})
|
||||
|
||||
it('See the user background settings', function() {
|
||||
cy.visit('/settings/user/theming')
|
||||
cy.findByRole('heading', { name: /Background and color/ })
|
||||
.should('exist')
|
||||
.scrollIntoView()
|
||||
})
|
||||
|
||||
it('Remove background', function() {
|
||||
cy.intercept('*/apps/theming/background/color').as('clearBackground')
|
||||
|
||||
// Clear background
|
||||
pickColor(cy.findByRole('button', { name: 'Plain background' }), 4)
|
||||
|
||||
// Validate clear background
|
||||
cy.wait('@clearBackground')
|
||||
cy.waitUntil(() => validateBodyThemingCss(defaultPrimary, null, '#ddcb55'))
|
||||
})
|
||||
|
||||
it('See the header being inverted', function() {
|
||||
// Probe the Nextcloud logo: it carries the same
|
||||
// `var(--background-image-invert-if-bright)` filter and is always
|
||||
// present in the header. The waffle launcher's current-app icon only
|
||||
// renders when an app is active, which isn't the case on settings,
|
||||
// and the in-popover tiles use a fixed brightness/invert filter
|
||||
// regardless of theme so they're not a valid inversion probe.
|
||||
cy.waitUntil(() => navigationHeader.logo().find('.logo').then((el) => {
|
||||
let ret = true
|
||||
el.each(function() {
|
||||
ret = ret && window.getComputedStyle(this).filter === 'invert(1)'
|
||||
})
|
||||
return ret
|
||||
}))
|
||||
})
|
||||
|
||||
it('Select another but non-bright shipped background', function() {
|
||||
const background = 'anatoly-mikhaltsov-butterfly-wing-scale.jpg'
|
||||
const backgroundName = 'Background picture of a red-ish butterfly wing under microscope'
|
||||
cy.intercept('*/apps/theming/background/shipped').as('setBackground')
|
||||
|
||||
// Select background
|
||||
cy.findByRole('button', { name: backgroundName, pressed: false })
|
||||
.click()
|
||||
cy.findByRole('button', { name: backgroundName, pressed: true })
|
||||
.should('be.visible')
|
||||
|
||||
// Validate changed background and primary
|
||||
cy.wait('@setBackground')
|
||||
cy.waitUntil(() => validateBodyThemingCss('#a53c17', background, '#652e11'))
|
||||
})
|
||||
|
||||
it('See the header NOT being inverted this time', function() {
|
||||
// Probe the Nextcloud logo: see the inverted-header test above for
|
||||
// why we don't probe the menu icons.
|
||||
cy.waitUntil(() => navigationHeader.logo().find('.logo').then((el) => {
|
||||
let ret = true
|
||||
el.each(function() {
|
||||
ret = ret && window.getComputedStyle(this).filter === 'none'
|
||||
})
|
||||
return ret
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
describe('User select a custom background', function() {
|
||||
const image = 'image.jpg'
|
||||
before(function() {
|
||||
cy.createRandomUser().then((user: User) => {
|
||||
cy.uploadFile(user, image, 'image/jpeg')
|
||||
cy.login(user)
|
||||
})
|
||||
})
|
||||
|
||||
it('See the user background settings', function() {
|
||||
cy.visit('/settings/user/theming')
|
||||
cy.findByRole('heading', { name: /Background and color/ })
|
||||
.should('exist')
|
||||
.scrollIntoView()
|
||||
})
|
||||
|
||||
it('Select a custom background', function() {
|
||||
cy.intercept('*/apps/theming/background/custom').as('setBackground')
|
||||
|
||||
// Pick background
|
||||
cy.findByRole('button', { name: 'Custom background' }).click()
|
||||
cy.findByRole('dialog')
|
||||
.should('be.visible')
|
||||
.findAllByRole('row')
|
||||
.contains(image)
|
||||
.click()
|
||||
cy.findByRole('button', { name: 'Select background' }).click()
|
||||
|
||||
// Wait for background to be set
|
||||
cy.wait('@setBackground')
|
||||
cy.waitUntil(() => validateBodyThemingCss(defaultPrimary, 'apps/theming/background?v=', '#2f2221'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('User changes settings and reload the page', function() {
|
||||
const image = 'image.jpg'
|
||||
|
||||
before(function() {
|
||||
cy.createRandomUser().then((user: User) => {
|
||||
cy.uploadFile(user, image, 'image/jpeg')
|
||||
cy.login(user)
|
||||
})
|
||||
})
|
||||
|
||||
it('See the user background settings', function() {
|
||||
cy.visit('/settings/user/theming')
|
||||
cy.findByRole('heading', { name: /Background and color/ })
|
||||
.should('exist')
|
||||
.scrollIntoView()
|
||||
})
|
||||
|
||||
it('Select a custom background', function() {
|
||||
cy.intercept('*/apps/theming/background/custom').as('setBackground')
|
||||
|
||||
// Pick background
|
||||
cy.findByRole('button', { name: 'Custom background' }).click()
|
||||
cy.findByRole('dialog')
|
||||
.should('be.visible')
|
||||
.findAllByRole('row')
|
||||
.contains(image)
|
||||
.click()
|
||||
cy.findByRole('button', { name: 'Select background' }).click()
|
||||
|
||||
// Wait for background to be set
|
||||
cy.wait('@setBackground')
|
||||
cy.waitUntil(() => validateBodyThemingCss(defaultPrimary, 'apps/theming/background?v=', '#2f2221'))
|
||||
})
|
||||
|
||||
it('Select a custom background color', function() {
|
||||
cy.intercept('*/apps/theming/background/color').as('clearBackground')
|
||||
|
||||
// Clear background
|
||||
pickColor(cy.findByRole('button', { name: 'Plain background' }), 5)
|
||||
|
||||
// Validate clear background
|
||||
cy.wait('@clearBackground')
|
||||
cy.waitUntil(() => validateBodyThemingCss(defaultPrimary, null, '#a5b872'))
|
||||
})
|
||||
|
||||
it('Select a custom primary color', function() {
|
||||
cy.intercept('/ocs/v2.php/apps/provisioning_api/api/v1/config/users/theming/primary_color').as('setPrimaryColor')
|
||||
|
||||
pickColor(cy.findByRole('button', { name: 'Primary color' }), 2)
|
||||
|
||||
cy.wait('@setPrimaryColor')
|
||||
cy.waitUntil(() => validateBodyThemingCss('#c98879', null, '#a5b872'))
|
||||
})
|
||||
|
||||
it('Reload the page and validate persistent changes', function() {
|
||||
cy.reload()
|
||||
cy.waitUntil(() => validateBodyThemingCss('#c98879', null, '#a5b872'))
|
||||
})
|
||||
})
|
||||
217
tests/playwright/e2e/appstore/admin-settings-apps.spec.ts
Normal file
217
tests/playwright/e2e/appstore/admin-settings-apps.spec.ts
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { expect } from '@playwright/test'
|
||||
import { test } from '../../support/fixtures/admin-appstore-page.ts'
|
||||
import { handlePasswordConfirmation } from '../../support/utils/password-confirmation.ts'
|
||||
import { runOcc } from '@nextcloud/e2e-test-server'
|
||||
|
||||
test.describe('Settings: App management', () => {
|
||||
test.beforeEach(async ({ page, appstorePage }) => {
|
||||
// Disable QA testing app if already enabled
|
||||
expect(await runOcc(['app:disable', 'testing']))
|
||||
.toMatch(/(No such app enabled|testing .+ disabled)/)
|
||||
// Enable update notification app if disabled
|
||||
expect(await runOcc(['app:enable', 'updatenotification']))
|
||||
.toMatch(/(updatenotification already enabled|updatenotification .+ enabled)/)
|
||||
|
||||
// Open the installed apps page
|
||||
await appstorePage.openInstalledApps()
|
||||
|
||||
// Wait for the apps table to load
|
||||
await appstorePage.appsTable().waitFor({ state: 'visible', timeout: 10000 })
|
||||
})
|
||||
|
||||
test('Can enable an installed app', async ({ page, appstorePage }) => {
|
||||
// Intercept the enable app request
|
||||
const enableRequest = page.waitForResponse(
|
||||
(response) => response.url().includes('/ocs/v2.php/apps/appstore/api/v1/apps/enable'),
|
||||
)
|
||||
|
||||
// Find and click the enable button for the QA testing app
|
||||
await expect(appstorePage.appsTable()).toBeVisible()
|
||||
const qaTestingRow = appstorePage.appRow('QA testing')
|
||||
await expect(qaTestingRow).toBeVisible({ timeout: 10000 })
|
||||
|
||||
await appstorePage.enableButton('QA testing').click({ force: true })
|
||||
|
||||
// Handle password confirmation if needed
|
||||
await handlePasswordConfirmation(page, 'admin')
|
||||
|
||||
// Wait for the API request
|
||||
await enableRequest
|
||||
|
||||
// Wait until we see the disable button for the app
|
||||
await expect(appstorePage.appsTable()).toBeVisible()
|
||||
await expect(appstorePage.appRow('QA testing')).toBeVisible()
|
||||
await expect(appstorePage.disableButton('QA testing')).toBeVisible()
|
||||
|
||||
// Change to enabled apps view
|
||||
await appstorePage.openEnabledApps()
|
||||
|
||||
// Verify the app appears in the enabled list
|
||||
await expect(appstorePage.appRow('QA testing')).toBeVisible()
|
||||
})
|
||||
|
||||
test('Can disable an installed app', async ({ page, appstorePage }) => {
|
||||
// Intercept the disable app request
|
||||
const disableRequest = page.waitForResponse(
|
||||
(response) => response.url().includes('/ocs/v2.php/apps/appstore/api/v1/apps/disable'),
|
||||
)
|
||||
|
||||
// Find and click the disable button for the Update notification app
|
||||
await expect(appstorePage.appsTable()).toBeVisible()
|
||||
const updateRow = appstorePage.appRow('Update notification')
|
||||
await expect(updateRow).toBeVisible({ timeout: 10000 })
|
||||
|
||||
await appstorePage.disableButton('Update notification').click({ force: true })
|
||||
|
||||
// Handle password confirmation if needed
|
||||
await handlePasswordConfirmation(page, 'admin')
|
||||
|
||||
// Wait for the API request
|
||||
await disableRequest
|
||||
|
||||
// Wait until we see the enable button for the app
|
||||
await expect(appstorePage.appsTable()).toBeVisible()
|
||||
await expect(appstorePage.appRow('Update notification')).toBeVisible()
|
||||
await expect(appstorePage.enableButton('Update notification')).toBeVisible()
|
||||
|
||||
// Change to disabled apps view
|
||||
await appstorePage.openDisabledApps()
|
||||
|
||||
// Verify the app appears in the disabled list
|
||||
await expect(appstorePage.appRow('Update notification')).toBeVisible()
|
||||
})
|
||||
|
||||
test('Browse enabled apps', async ({ appstorePage }) => {
|
||||
// Open the "Active apps" section
|
||||
await appstorePage.openEnabledApps()
|
||||
|
||||
// Verify the URL is correct
|
||||
await expect(appstorePage.navigationLink('Active apps')).toHaveAttribute('aria-current', 'page')
|
||||
|
||||
// Verify that there are only enabled apps (all have "Disable" button, no "Enable" button)
|
||||
await expect(appstorePage.appsTable()).toBeVisible()
|
||||
|
||||
// Get all rows and verify each has a disable button and no enable button
|
||||
const rows = appstorePage.appsTable().locator('tr')
|
||||
const rowCount = await rows.count()
|
||||
|
||||
for (let i = 1; i < rowCount; i++) { // Skip header row
|
||||
const row = rows.nth(i)
|
||||
const enableButton = row.getByRole('button', { name: 'Enable' })
|
||||
|
||||
// Enabled apps should not have an "Enable" button
|
||||
await expect(enableButton).not.toBeVisible()
|
||||
}
|
||||
})
|
||||
|
||||
test('Browse disabled apps', async ({ appstorePage }) => {
|
||||
// Open the "Disabled apps" section
|
||||
await appstorePage.openDisabledApps()
|
||||
|
||||
// Verify the current section is "Disabled apps"
|
||||
await expect(appstorePage.navigationLink('Disabled apps')).toHaveAttribute('aria-current', 'page')
|
||||
|
||||
// Verify that there are only disabled apps (all have "Enable" button, no "Disable" button)
|
||||
await expect(appstorePage.appsTable()).toBeVisible()
|
||||
|
||||
// Get all rows and verify each has an enable button and no disable button
|
||||
const rows = appstorePage.appsTable().locator('tr')
|
||||
const rowCount = await rows.count()
|
||||
|
||||
for (let i = 1; i < rowCount; i++) { // Skip header row
|
||||
const row = rows.nth(i)
|
||||
const disableButton = row.getByRole('button', { name: 'Disable' })
|
||||
|
||||
// Disabled apps should not have a "Disable" button
|
||||
await expect(disableButton).not.toBeVisible()
|
||||
}
|
||||
})
|
||||
|
||||
test('Browse app bundles', async ({ appstorePage }) => {
|
||||
// Open the "App bundles" section
|
||||
await appstorePage.openBundles()
|
||||
|
||||
// Verify the current section is "App bundles"
|
||||
await expect(appstorePage.navigationLink('App bundles')).toHaveAttribute('aria-current', 'page')
|
||||
|
||||
// Verify we see the app bundles
|
||||
await expect(appstorePage.enterpriseBundleHeading()).toBeVisible()
|
||||
await expect(appstorePage.educationBundleHeading()).toBeVisible()
|
||||
})
|
||||
|
||||
test('View app details', async ({ appstorePage }) => {
|
||||
// Click on the "QA testing" app
|
||||
await appstorePage.appLink('QA testing').click({ force: true })
|
||||
|
||||
// Verify the app details sidebar is shown
|
||||
const sidebar = appstorePage.appSidebar()
|
||||
await expect(sidebar).toBeVisible()
|
||||
await expect(appstorePage.appSidebarHeader()).toContainText('QA testing')
|
||||
|
||||
// Verify the sidebar contains expected elements
|
||||
await expect(appstorePage.viewInStoreLink()).toBeVisible()
|
||||
await expect(appstorePage.appSidebarEnableButton()).toBeVisible()
|
||||
await expect(appstorePage.removeButton()).toBeVisible()
|
||||
|
||||
// Verify version information is displayed
|
||||
await expect(appstorePage.versionText()).toBeVisible()
|
||||
})
|
||||
|
||||
test('Limit app usage to group', async ({ appstorePage, page }) => {
|
||||
// Open the "Active apps" section
|
||||
await appstorePage.openEnabledApps()
|
||||
|
||||
// Select the updatenotification app
|
||||
await appstorePage.appLink('Update Notification').scrollIntoViewIfNeeded()
|
||||
await appstorePage.appLink('Update Notification').click()
|
||||
|
||||
// Click the "Limit to groups" button
|
||||
await appstorePage.limitToGroupsButton().click()
|
||||
|
||||
// The dialog should be visible
|
||||
const dialog = appstorePage.groupDialog()
|
||||
await expect(dialog).toBeVisible()
|
||||
|
||||
// Type "admin" in the search field
|
||||
const searchInput = appstorePage.groupSearchInput()
|
||||
await expect(searchInput).toBeFocused()
|
||||
await searchInput.fill('admin')
|
||||
|
||||
// Select the admin option from the dropdown
|
||||
await appstorePage.groupOption('admin').click()
|
||||
|
||||
// Click the Save button
|
||||
await appstorePage.dialogSaveButton().click()
|
||||
|
||||
// Handle password confirmation
|
||||
await handlePasswordConfirmation(page, 'admin')
|
||||
|
||||
// Verify the group is now in the "Limited to groups" list
|
||||
const limitedList = appstorePage.limitedToGroupsList()
|
||||
await expect(limitedList).toBeVisible()
|
||||
await expect(limitedList.getByRole('listitem', { name: /admin/ })).toBeVisible()
|
||||
|
||||
// Now disable the group limitation
|
||||
await appstorePage.limitToGroupsButton().click()
|
||||
|
||||
// The dialog should be visible again
|
||||
await expect(dialog).toBeVisible()
|
||||
|
||||
// Click the deselect button for the admin group
|
||||
await appstorePage.deselectGroupButton('admin').click()
|
||||
|
||||
// Click Save
|
||||
await appstorePage.dialogSaveButton().click()
|
||||
|
||||
// Handle password confirmation
|
||||
await handlePasswordConfirmation(page, 'admin')
|
||||
|
||||
// Verify the "Limited to groups" list is no longer visible
|
||||
await expect(appstorePage.limitedToGroupsList()).toHaveCount(0)
|
||||
})
|
||||
})
|
||||
|
|
@ -10,8 +10,6 @@ import { test } from '../../support/fixtures/admin-theming-page.ts'
|
|||
const admin = new User('admin', 'admin')
|
||||
|
||||
test.describe('Admin theming branding settings', () => {
|
||||
test.describe.configure({ mode: 'serial' })
|
||||
|
||||
test.beforeEach(async ({ adminThemingPage }) => {
|
||||
await adminThemingPage.reset()
|
||||
await adminThemingPage.open()
|
||||
|
|
|
|||
|
|
@ -44,8 +44,8 @@ test.describe('Admin theming set default apps', () => {
|
|||
await expect(adminThemingPage.defaultAppSwitch()).toBeChecked()
|
||||
await expect(adminThemingPage.defaultAppRegion()).toBeVisible()
|
||||
|
||||
await expect(adminThemingPage.defaultAppSelect().getByText('Dashboard')).toBeVisible()
|
||||
await expect(adminThemingPage.defaultAppSelect().getByText('Files')).toBeVisible()
|
||||
await expect(adminThemingPage.defaultAppSelectedValue('Dashboard')).toBeVisible()
|
||||
await expect(adminThemingPage.defaultAppSelectedValue('Files')).toBeVisible()
|
||||
|
||||
await expect(adminThemingPage.appOrderEntries()).toHaveCount(2)
|
||||
await expect(adminThemingPage.appOrderEntries().nth(0)).toContainText('Dashboard')
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ test('User can change personal app order', async ({ page }) => {
|
|||
await expect(userThemingPage.appOrderEntries().nth(0)).toContainText('Dashboard')
|
||||
await expect(userThemingPage.appOrderEntries().nth(1)).toContainText('Files')
|
||||
|
||||
await navigationHeader.openMenu()
|
||||
await expect(navigationHeader.navigationEntries().nth(0)).toContainText('Dashboard')
|
||||
await expect(navigationHeader.navigationEntries().nth(1)).toContainText('Files')
|
||||
|
||||
|
|
@ -39,6 +40,7 @@ test('User can change personal app order', async ({ page }) => {
|
|||
const reloadedOrder = (await userThemingPage.appOrderEntries().allInnerTexts()).map((entry) => entry.trim())
|
||||
expect(reloadedOrder).toContain('Dashboard')
|
||||
expect(reloadedOrder).toContain('Files')
|
||||
await navigationHeader.openMenu()
|
||||
await expect(navigationHeader.navigationEntries().nth(0)).toContainText(reloadedOrder[0]!)
|
||||
await expect(navigationHeader.navigationEntries().nth(1)).toContainText(reloadedOrder[1]!)
|
||||
})
|
||||
|
|
|
|||
14
tests/playwright/support/fixtures/admin-appstore-page.ts
Normal file
14
tests/playwright/support/fixtures/admin-appstore-page.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { test as adminSessionTest } from './admin-session.ts'
|
||||
import { AppstorePage } from '../sections/AppstorePage.ts'
|
||||
|
||||
export const test = adminSessionTest.extend<{ appstorePage: AppstorePage }>({
|
||||
appstorePage: async ({ page }, use) => {
|
||||
const appstorePage = new AppstorePage(page)
|
||||
await use(appstorePage)
|
||||
},
|
||||
})
|
||||
|
|
@ -24,7 +24,7 @@ export class AdminThemingPage {
|
|||
})
|
||||
const requestToken = (await tokenResponse.json()).token
|
||||
|
||||
const response = await this.page.request.post('/apps/theming/ajax/undoAllChanges', {
|
||||
const response = await this.page.request.post('./apps/theming/ajax/undoAllChanges', {
|
||||
headers: {
|
||||
requesttoken: requestToken,
|
||||
},
|
||||
|
|
@ -44,7 +44,15 @@ export class AdminThemingPage {
|
|||
}
|
||||
|
||||
defaultAppSelect(): Locator {
|
||||
return this.defaultAppRegion().getByRole('combobox')
|
||||
// NcSelect appends the dropdown listbox to <body> (appendToBody: true), so it cannot
|
||||
// be reached via a scoped locator. Return the selected-options wrapper instead, which
|
||||
// stays inline and contains the visible selected-value tag spans.
|
||||
return this.defaultAppRegion().locator('.vs__selected-options')
|
||||
}
|
||||
|
||||
defaultAppSelectedValue(name: string): Locator {
|
||||
// NcSelect renders each selected value as a tag with a "Deselect <name>" button.
|
||||
return this.defaultAppRegion().getByRole('button', { name: `Deselect ${name}` })
|
||||
}
|
||||
|
||||
appOrderList(): Locator {
|
||||
|
|
|
|||
199
tests/playwright/support/sections/AppstorePage.ts
Normal file
199
tests/playwright/support/sections/AppstorePage.ts
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
|
||||
export class AppstorePage {
|
||||
constructor(private readonly page: Page) {}
|
||||
|
||||
/**
|
||||
* Opens the main appstore page
|
||||
*/
|
||||
async openAppstore() {
|
||||
await this.page.goto('settings/apps')
|
||||
await this.appsTable().waitFor({ state: 'visible' })
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the installed apps page
|
||||
*/
|
||||
async openInstalledApps() {
|
||||
await this.page.goto('settings/apps/installed')
|
||||
await this.appsTable().waitFor({ state: 'visible' })
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the enabled apps page
|
||||
*/
|
||||
async openEnabledApps() {
|
||||
await this.navigationLink('Active apps').click()
|
||||
await this.page.waitForURL(/settings\/apps\/enabled$/)
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the disabled apps page
|
||||
*/
|
||||
async openDisabledApps() {
|
||||
await this.navigationLink('Disabled apps').click()
|
||||
await this.page.waitForURL(/settings\/apps\/disabled$/)
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the app bundles page
|
||||
*/
|
||||
async openBundles() {
|
||||
await this.navigationLink('App bundles').click()
|
||||
await this.page.waitForURL(/settings\/apps\/bundles$/)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the apps table element
|
||||
*/
|
||||
appsTable(): Locator {
|
||||
return this.page.getByRole('table')
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a specific app row by app name
|
||||
*/
|
||||
appRow(appName: string): Locator {
|
||||
return this.appsTable().locator('tr').filter({ hasText: appName }).first()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the enable button for a specific app
|
||||
*/
|
||||
enableButton(appName: string): Locator {
|
||||
return this.appRow(appName).getByRole('button', { name: 'Enable' })
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the disable button for a specific app
|
||||
*/
|
||||
disableButton(appName: string): Locator {
|
||||
return this.appRow(appName).getByRole('button', { name: 'Disable' })
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the app link in the table
|
||||
*/
|
||||
appLink(appName: string): Locator {
|
||||
return this.appsTable().getByRole('link', { name: appName })
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the navigation link in the appstore sidebar
|
||||
*/
|
||||
navigationLink(name: string): Locator {
|
||||
return this.page.getByRole('navigation', { name: 'Appstore categories' }).getByRole('link', { name })
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the app sidebar
|
||||
*/
|
||||
appSidebar(): Locator {
|
||||
return this.page.locator('#app-sidebar-vue')
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the app sidebar header
|
||||
*/
|
||||
appSidebarHeader(): Locator {
|
||||
return this.appSidebar().locator('.app-sidebar-header__info')
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the "Enable" button in the app sidebar (not the table row).
|
||||
* Use this when checking the sidebar after clicking an app link.
|
||||
*/
|
||||
appSidebarEnableButton(): Locator {
|
||||
return this.appSidebar().getByRole('button', { name: 'Enable' })
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the "View in store" link in the sidebar
|
||||
*/
|
||||
viewInStoreLink(): Locator {
|
||||
return this.appSidebar().getByRole('link', { name: 'View in store' })
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the "Remove" button in the sidebar
|
||||
*/
|
||||
removeButton(): Locator {
|
||||
return this.appSidebar().getByRole('button', { name: 'Remove' })
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the "Limit to groups" button
|
||||
*/
|
||||
limitToGroupsButton(): Locator {
|
||||
return this.appSidebar().getByRole('button', { name: 'Limit to groups' })
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the "Limited to groups" list
|
||||
*/
|
||||
limitedToGroupsList(): Locator {
|
||||
return this.appSidebar().getByRole('list', { name: 'Limited to groups' })
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the group dialog
|
||||
*/
|
||||
groupDialog(): Locator {
|
||||
return this.page.getByRole('dialog')
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the save button in the dialog
|
||||
*/
|
||||
dialogSaveButton(): Locator {
|
||||
return this.groupDialog().getByRole('button', { name: 'Save' })
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the deselect button for a group
|
||||
*/
|
||||
deselectGroupButton(groupName: string): Locator {
|
||||
return this.groupDialog().getByRole('button', { name: `Deselect ${groupName}` })
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the group search input.
|
||||
* NcSelectUsers uses role="combobox" on the search input, not role="textbox".
|
||||
*/
|
||||
groupSearchInput(): Locator {
|
||||
return this.groupDialog().locator('input').first()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the enterprise bundle heading
|
||||
*/
|
||||
enterpriseBundleHeading(): Locator {
|
||||
return this.page.getByRole('heading', { name: 'Enterprise bundle' })
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the education bundle heading
|
||||
*/
|
||||
educationBundleHeading(): Locator {
|
||||
return this.page.getByRole('heading', { name: 'Education bundle' })
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the version text from sidebar
|
||||
*/
|
||||
versionText(): Locator {
|
||||
return this.appSidebar().getByText(/Version \d+\.\d+\.\d+/)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a group option from the dropdown
|
||||
*/
|
||||
groupOption(groupName: string): Locator {
|
||||
return this.page.getByRole('option', { name: new RegExp(groupName) })
|
||||
}
|
||||
}
|
||||
|
|
@ -17,10 +17,34 @@ export class NavigationHeaderPage {
|
|||
}
|
||||
|
||||
navigation(): Locator {
|
||||
return this.header.getByRole('navigation', { name: 'Applications menu' })
|
||||
return this.header.getByRole('navigation', { name: 'Applications' })
|
||||
}
|
||||
|
||||
private waffleButton(): Locator {
|
||||
return this.navigation().locator('.app-menu__waffle')
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the waffle launcher popover.
|
||||
* The app entries only exist in the DOM while the popover is open.
|
||||
*/
|
||||
async openMenu(): Promise<void> {
|
||||
const isOpen = await this.waffleButton().getAttribute('aria-expanded') === 'true'
|
||||
if (!isOpen) {
|
||||
await this.waffleButton().click()
|
||||
}
|
||||
await this.popover().waitFor({ state: 'visible' })
|
||||
}
|
||||
|
||||
popover(): Locator {
|
||||
return this.page.locator('[role="menu"][aria-label="Apps"]')
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns navigation entries from the waffle popover.
|
||||
* Call {@link openMenu} first — entries are only in the DOM while the popover is open.
|
||||
*/
|
||||
navigationEntries(): Locator {
|
||||
return this.navigation().getByRole('listitem')
|
||||
return this.popover().getByRole('menuitem')
|
||||
}
|
||||
}
|
||||
|
|
|
|||
34
tests/playwright/support/utils/password-confirmation.ts
Normal file
34
tests/playwright/support/utils/password-confirmation.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { Page } from '@playwright/test'
|
||||
|
||||
/**
|
||||
* Handle the password confirmation dialog if it appears
|
||||
*
|
||||
* @param page - The Playwright page object
|
||||
* @param password - The password to enter (default: 'admin')
|
||||
*/
|
||||
export async function handlePasswordConfirmation(page: Page, password = 'admin') {
|
||||
const dialog = page.locator('.modal-container:has-text("Authentication required")')
|
||||
|
||||
try {
|
||||
// Check if the dialog exists within a short timeout
|
||||
const dialogVisible = await dialog.isVisible({ timeout: 500 }).catch(() => false)
|
||||
|
||||
if (dialogVisible) {
|
||||
// Fill the password field
|
||||
await dialog.locator('input[type="password"]').fill(password)
|
||||
|
||||
// Click the confirm button
|
||||
await dialog.getByRole('button', { name: 'Confirm' }).click()
|
||||
|
||||
// Wait for the dialog to disappear
|
||||
await dialog.waitFor({ state: 'hidden' })
|
||||
}
|
||||
} catch (error) {
|
||||
// Dialog didn't appear, which is fine - some operations might not require confirmation
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue