aboutsummaryrefslogtreecommitdiffstats
path: root/cypress
diff options
context:
space:
mode:
authorFerdinand Thiessen <opensource@fthiessen.de>2024-07-10 00:37:42 +0200
committerFerdinand Thiessen <opensource@fthiessen.de>2024-07-10 01:35:25 +0200
commit12e1c29245792dc9a24a0d6757abf3b1f71eb1a7 (patch)
tree0a0333fe4d4f7a4b5ef1cb15e2cda71d6cb3a034 /cypress
parentd82565d67d071dce0208323decc455da9d8625ef (diff)
downloadnextcloud-server-12e1c29245792dc9a24a0d6757abf3b1f71eb1a7.tar.gz
nextcloud-server-12e1c29245792dc9a24a0d6757abf3b1f71eb1a7.zip
test: Adjust cypress tests to use reusable POM for header navigation
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
Diffstat (limited to 'cypress')
-rw-r--r--cypress/dockerNode.ts13
-rw-r--r--cypress/e2e/theming/admin-settings.cy.ts21
-rw-r--r--cypress/e2e/theming/admin-settings_default-app.cy.ts91
-rw-r--r--cypress/e2e/theming/user-settings_app-order.cy.ts (renamed from cypress/e2e/theming/navigation-bar-settings.cy.ts)205
-rw-r--r--cypress/e2e/theming/user-settings_background.cy.ts (renamed from cypress/e2e/theming/user-background.cy.ts)29
-rw-r--r--cypress/pages/NavigationHeader.ts58
6 files changed, 258 insertions, 159 deletions
diff --git a/cypress/dockerNode.ts b/cypress/dockerNode.ts
index 530a013aa2c..180ff7894c6 100644
--- a/cypress/dockerNode.ts
+++ b/cypress/dockerNode.ts
@@ -150,22 +150,19 @@ export const applyChangesToNextcloud = async function() {
'./remote.php',
'./status.php',
'./version.php',
- ]
-
- let needToApplyChanges = false
-
- folderPaths.forEach((folderPath) => {
- const fullPath = path.join(htmlPath, folderPath)
+ ].filter((folderPath) => {
+ const fullPath = path.resolve(__dirname, '..', folderPath)
if (existsSync(fullPath)) {
- needToApplyChanges = true
console.log(`├─ Copying ${folderPath}`)
+ return true
}
+ return false
})
// Don't try to apply changes, when there are none. Otherwise we
// still execute the 'chown' command, which is not needed.
- if (!needToApplyChanges) {
+ if (folderPaths.length === 0) {
console.log('└─ No local changes found to apply')
return
}
diff --git a/cypress/e2e/theming/admin-settings.cy.ts b/cypress/e2e/theming/admin-settings.cy.ts
index 26bc6d26611..4207b98f711 100644
--- a/cypress/e2e/theming/admin-settings.cy.ts
+++ b/cypress/e2e/theming/admin-settings.cy.ts
@@ -13,6 +13,7 @@ import {
validateUserThemingDefaultCss,
expectBackgroundColor,
} from './themingUtils'
+import { NavigationHeader } from '../../pages/NavigationHeader'
const admin = new User('admin', 'admin')
@@ -225,6 +226,7 @@ describe('Remove the default background with a custom background color', functio
})
describe('Remove the default background with a bright color', function() {
+ const navigationHeader = new NavigationHeader()
let selectedColor = ''
before(function() {
@@ -271,15 +273,16 @@ describe('Remove the default background with a bright color', function() {
it('See the header being inverted', function() {
cy.waitUntil(() =>
- cy.window().then((win) => {
- const firstEntry = win.document.querySelector(
- '.app-menu-main li img',
- )
- if (!firstEntry) {
- return false
- }
- return getComputedStyle(firstEntry).filter === 'invert(1)'
- }),
+ navigationHeader
+ .getNavigationEntries()
+ .find('img')
+ .then((el) => {
+ let ret = true
+ el.each(function() {
+ ret = ret && window.getComputedStyle(this).filter === 'invert(1)'
+ })
+ return ret
+ })
)
})
})
diff --git a/cypress/e2e/theming/admin-settings_default-app.cy.ts b/cypress/e2e/theming/admin-settings_default-app.cy.ts
new file mode 100644
index 00000000000..702f737bc15
--- /dev/null
+++ b/cypress/e2e/theming/admin-settings_default-app.cy.ts
@@ -0,0 +1,91 @@
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { User } from '@nextcloud/cypress'
+import { NavigationHeader } from '../../pages/NavigationHeader'
+
+const admin = new User('admin', 'admin')
+
+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')
+ cy.get('[data-cy-switch-default-app]').should('exist')
+ cy.get('[data-cy-switch-default-app]').scrollIntoView()
+ })
+
+ it('Toggle the "use custom default app" switch', () => {
+ cy.get('[data-cy-switch-default-app] input').should('not.be.checked')
+ cy.get('[data-cy-switch-default-app] .checkbox-content').click()
+ cy.get('[data-cy-switch-default-app] input').should('be.checked')
+ })
+
+ it('See the default app order selector', () => {
+ cy.get('[data-cy-app-order] [data-cy-app-order-element]').then(elements => {
+ const appIDs = elements.map((idx, el) => el.getAttribute('data-cy-app-order-element')).get()
+ expect(appIDs).to.deep.eq(['dashboard', 'files'])
+ })
+ })
+
+ it('Change the default app', () => {
+ cy.get('[data-cy-app-order] [data-cy-app-order-element="files"]').scrollIntoView()
+
+ cy.get('[data-cy-app-order] [data-cy-app-order-element="files"] [data-cy-app-order-button="up"]').should('be.visible')
+ cy.get('[data-cy-app-order] [data-cy-app-order-element="files"] [data-cy-app-order-button="up"]').click()
+ cy.get('[data-cy-app-order] [data-cy-app-order-element="files"] [data-cy-app-order-button="up"]').should('not.be.visible')
+
+ })
+
+ it('See the default app is changed', () => {
+ cy.get('[data-cy-app-order] [data-cy-app-order-element]').then(elements => {
+ const appIDs = elements.map((idx, el) => el.getAttribute('data-cy-app-order-element')).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')
+ cy.get('[data-cy-switch-default-app]').scrollIntoView()
+
+ cy.get('[data-cy-switch-default-app] input').should('be.checked')
+ cy.get('[data-cy-switch-default-app] .checkbox-content').click()
+ cy.get('[data-cy-switch-default-app] input').should('be.not.checked')
+ })
+
+ it('See the default app is changed back to default', () => {
+ // 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')
+ })
+ })
+})
diff --git a/cypress/e2e/theming/navigation-bar-settings.cy.ts b/cypress/e2e/theming/user-settings_app-order.cy.ts
index fea72d454b0..7e6efa7d0ea 100644
--- a/cypress/e2e/theming/navigation-bar-settings.cy.ts
+++ b/cypress/e2e/theming/user-settings_app-order.cy.ts
@@ -5,89 +5,19 @@
import { User } from '@nextcloud/cypress'
import { installTestApp, uninstallTestApp } from '../../support/commonUtils'
+import { NavigationHeader } from '../../pages/NavigationHeader'
-const admin = new User('admin', 'admin')
-
-describe('Admin theming set default apps', () => {
- before(function() {
- // Just in case previous test failed
- cy.resetAdminTheming()
- cy.login(admin)
- })
-
- it('See the current default app is the dashboard', () => {
- cy.visit('/')
- cy.url().should('match', /apps\/dashboard/)
-
- // Also check the top logo link
- cy.get('#nextcloud').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')
- cy.get('[data-cy-switch-default-app]').should('exist')
- cy.get('[data-cy-switch-default-app]').scrollIntoView()
- })
-
- it('Toggle the "use custom default app" switch', () => {
- cy.get('[data-cy-switch-default-app] input').should('not.be.checked')
- cy.get('[data-cy-switch-default-app] .checkbox-content').click()
- cy.get('[data-cy-switch-default-app] input').should('be.checked')
- })
-
- it('See the default app order selector', () => {
- cy.get('[data-cy-app-order] [data-cy-app-order-element]').then(elements => {
- const appIDs = elements.map((idx, el) => el.getAttribute('data-cy-app-order-element')).get()
- expect(appIDs).to.deep.eq(['dashboard', 'files'])
- })
- })
-
- it('Change the default app', () => {
- cy.get('[data-cy-app-order] [data-cy-app-order-element="files"]').scrollIntoView()
-
- cy.get('[data-cy-app-order] [data-cy-app-order-element="files"] [data-cy-app-order-button="up"]').should('be.visible')
- cy.get('[data-cy-app-order] [data-cy-app-order-element="files"] [data-cy-app-order-button="up"]').click()
- cy.get('[data-cy-app-order] [data-cy-app-order-element="files"] [data-cy-app-order-button="up"]').should('not.be.visible')
-
- })
-
- it('See the default app is changed', () => {
- cy.get('[data-cy-app-order] [data-cy-app-order-element]').then(elements => {
- const appIDs = elements.map((idx, el) => el.getAttribute('data-cy-app-order-element')).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')
- cy.get('[data-cy-switch-default-app]').scrollIntoView()
-
- cy.get('[data-cy-switch-default-app] input').should('be.checked')
- cy.get('[data-cy-switch-default-app] .checkbox-content').click()
- cy.get('[data-cy-switch-default-app] input').should('be.not.checked')
- })
+/**
+ * Intercept setting the app order as `updateAppOrder`
+ */
+function interceptAppOrder() {
+ cy.intercept('POST', '/ocs/v2.php/apps/provisioning_api/api/v1/config/users/core/apporder').as('updateAppOrder')
+}
- it('See the default app is changed back to default', () => {
- // 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')
- })
- })
-})
+before(() => uninstallTestApp())
describe('User theming set app order', () => {
+ const navigationHeader = new NavigationHeader()
let user: User
before(() => {
@@ -109,40 +39,43 @@ describe('User theming set app order', () => {
})
it('See that the dashboard app is the first one', () => {
+ const appOrder = ['Dashboard', 'Files']
// Check the app order settings UI
- cy.get('[data-cy-app-order] [data-cy-app-order-element]').then(elements => {
- const appIDs = elements.map((idx, el) => el.getAttribute('data-cy-app-order-element')).get()
- expect(appIDs).to.deep.eq(['dashboard', 'files'])
- })
+ cy.get('[data-cy-app-order] [data-cy-app-order-element]')
+ .each((element, index) => expect(element).to.contain.text(appOrder[index]))
// Check the top app menu order
- cy.get('.app-menu-main .app-menu-entry').then(elements => {
- const appIDs = elements.map((idx, el) => el.getAttribute('data-app-id')).get()
- expect(appIDs).to.deep.eq(['dashboard', 'files'])
- })
+ navigationHeader.getNavigationEntries()
+ .each((entry, index) => expect(entry).contain.text(appOrder[index]))
})
it('Change the app order', () => {
+ interceptAppOrder()
cy.get('[data-cy-app-order] [data-cy-app-order-element="files"] [data-cy-app-order-button="up"]').should('be.visible')
cy.get('[data-cy-app-order] [data-cy-app-order-element="files"] [data-cy-app-order-button="up"]').click()
cy.get('[data-cy-app-order] [data-cy-app-order-element="files"] [data-cy-app-order-button="up"]').should('not.be.visible')
+ cy.wait('@updateAppOrder')
- cy.get('[data-cy-app-order] [data-cy-app-order-element]').then(elements => {
- const appIDs = elements.map((idx, el) => el.getAttribute('data-cy-app-order-element')).get()
- expect(appIDs).to.deep.eq(['files', 'dashboard'])
- })
+ const appOrder = ['Files', 'Dashboard']
+ cy.get('[data-cy-app-order] [data-cy-app-order-element]')
+ .each((element, index) => expect(element).to.contain.text(appOrder[index]))
})
it('See the app menu order is changed', () => {
cy.reload()
- cy.get('.app-menu-main .app-menu-entry').then(elements => {
- const appIDs = elements.map((idx, el) => el.getAttribute('data-app-id')).get()
- expect(appIDs).to.deep.eq(['files', 'dashboard'])
- })
+ const appOrder = ['Files', 'Dashboard']
+ // Check the app order settings UI
+ cy.get('[data-cy-app-order] [data-cy-app-order-element]')
+ .each((element, index) => expect(element).to.contain.text(appOrder[index]))
+
+ // Check the top app menu order
+ navigationHeader.getNavigationEntries()
+ .each((entry, index) => expect(entry).contain.text(appOrder[index]))
})
})
describe('User theming set app order with default app', () => {
+ const navigationHeader = new NavigationHeader()
let user: User
before(() => {
@@ -176,11 +109,11 @@ describe('User theming set app order with default app', () => {
it('See the app order settings: files is the first one', () => {
cy.visit('/settings/user/theming')
cy.get('[data-cy-app-order]').scrollIntoView()
- cy.get('[data-cy-app-order] [data-cy-app-order-element]').then(elements => {
- expect(elements).to.have.length(4)
- const appIDs = elements.map((idx, el) => el.getAttribute('data-cy-app-order-element')).get()
- expect(appIDs).to.deep.eq(['files', 'dashboard', 'testapp1', 'testapp'])
- })
+
+ const appOrder = ['Files', 'Dashboard', 'Test App 2', 'Test App']
+ // Check the app order settings UI
+ cy.get('[data-cy-app-order] [data-cy-app-order-element]')
+ .each((element, index) => expect(element).to.contain.text(appOrder[index]))
})
it('Can not change the default app', () => {
@@ -195,32 +128,31 @@ describe('User theming set app order with default app', () => {
})
it('Change the order of the other apps', () => {
- cy.intercept('POST', '**/apps/provisioning_api/api/v1/config/users/core/apporder').as('setAppOrder')
+ interceptAppOrder()
// Move the testapp up twice, it should be the first one after files
cy.get('[data-cy-app-order] [data-cy-app-order-element="testapp"] [data-cy-app-order-button="up"]').click()
- cy.wait('@setAppOrder')
+ cy.wait('@updateAppOrder')
cy.get('[data-cy-app-order] [data-cy-app-order-element="testapp"] [data-cy-app-order-button="up"]').click()
- cy.wait('@setAppOrder')
+ cy.wait('@updateAppOrder')
// Can't get up anymore, files is enforced as default app
cy.get('[data-cy-app-order] [data-cy-app-order-element="testapp"] [data-cy-app-order-button="up"]').should('not.be.visible')
// Check the final list order
- cy.get('[data-cy-app-order] [data-cy-app-order-element]').then(elements => {
- expect(elements).to.have.length(4)
- const appIDs = elements.map((idx, el) => el.getAttribute('data-cy-app-order-element')).get()
- expect(appIDs).to.deep.eq(['files', 'testapp', 'dashboard', 'testapp1'])
- })
+ const appOrder = ['Files', 'Test App', 'Dashboard', 'Test App 2']
+ // Check the app order settings UI
+ cy.get('[data-cy-app-order] [data-cy-app-order-element]')
+ .each((element, index) => expect(element).to.contain.text(appOrder[index]))
})
it('See the app menu order is changed', () => {
cy.reload()
- cy.get('.app-menu-main .app-menu-entry').then(elements => {
- expect(elements).to.have.length(4)
- const appIDs = elements.map((idx, el) => el.getAttribute('data-app-id')).get()
- expect(appIDs).to.deep.eq(['files', 'testapp', 'dashboard', 'testapp1'])
- })
+
+ const appOrder = ['Files', 'Test App', 'Dashboard', 'Test App 2']
+ // Check the top app menu order
+ navigationHeader.getNavigationEntries()
+ .each((entry, index) => expect(entry).contain.text(appOrder[index]))
})
})
@@ -247,8 +179,10 @@ describe('User theming app order list accessibility', () => {
})
it('click the first button', () => {
+ interceptAppOrder()
cy.get('[data-cy-app-order] [data-cy-app-order-element="dashboard"] [data-cy-app-order-button="down"]').should('be.visible').focus()
cy.get('[data-cy-app-order] [data-cy-app-order-element="dashboard"] [data-cy-app-order-button="down"]').click()
+ cy.wait('@updateAppOrder')
})
it('see the same app kept the focus', () => {
@@ -259,8 +193,10 @@ describe('User theming app order list accessibility', () => {
})
it('click the last button', () => {
+ interceptAppOrder()
cy.get('[data-cy-app-order] [data-cy-app-order-element="dashboard"] [data-cy-app-order-button="up"]').should('be.visible').focus()
cy.get('[data-cy-app-order] [data-cy-app-order-element="dashboard"] [data-cy-app-order-button="up"]').click()
+ cy.wait('@updateAppOrder')
})
it('see the same app kept the focus', () => {
@@ -272,6 +208,7 @@ describe('User theming app order list accessibility', () => {
})
describe('User theming reset app order', () => {
+ const navigationHeader = new NavigationHeader()
let user: User
before(() => {
@@ -293,17 +230,14 @@ describe('User theming reset app order', () => {
})
it('See that the dashboard app is the first one', () => {
+ const appOrder = ['Dashboard', 'Files']
// Check the app order settings UI
- cy.get('[data-cy-app-order] [data-cy-app-order-element]').then(elements => {
- const appIDs = elements.map((idx, el) => el.getAttribute('data-cy-app-order-element')).get()
- expect(appIDs).to.deep.eq(['dashboard', 'files'])
- })
+ cy.get('[data-cy-app-order] [data-cy-app-order-element]')
+ .each((element, index) => expect(element).to.contain.text(appOrder[index]))
// Check the top app menu order
- cy.get('.app-menu-main .app-menu-entry').then(elements => {
- const appIDs = elements.map((idx, el) => el.getAttribute('data-app-id')).get()
- expect(appIDs).to.deep.eq(['dashboard', 'files'])
- })
+ navigationHeader.getNavigationEntries()
+ .each((entry, index) => expect(entry).contain.text(appOrder[index]))
})
it('See the reset button is disabled', () => {
@@ -312,15 +246,17 @@ describe('User theming reset app order', () => {
})
it('Change the app order', () => {
+ interceptAppOrder()
cy.get('[data-cy-app-order] [data-cy-app-order-element="files"] [data-cy-app-order-button="up"]').should('be.visible')
cy.get('[data-cy-app-order] [data-cy-app-order-element="files"] [data-cy-app-order-button="up"]').click()
cy.get('[data-cy-app-order] [data-cy-app-order-element="files"] [data-cy-app-order-button="up"]').should('not.be.visible')
+ cy.wait('@updateAppOrder')
// Check the app order settings UI
- cy.get('[data-cy-app-order] [data-cy-app-order-element]').then(elements => {
- const appIDs = elements.map((idx, el) => el.getAttribute('data-cy-app-order-element')).get()
- expect(appIDs).to.deep.eq(['files', 'dashboard'])
- })
+ const appOrder = ['Files', 'Dashboard']
+ // Check the app order settings UI
+ cy.get('[data-cy-app-order] [data-cy-app-order-element]')
+ .each((element, index) => expect(element).to.contain.text(appOrder[index]))
})
it('See the reset button is no longer disabled', () => {
@@ -329,14 +265,25 @@ describe('User theming reset app order', () => {
})
it('Reset the app order', () => {
+ cy.intercept('GET', '/ocs/v2.php/core/navigation/apps').as('loadApps')
+ interceptAppOrder()
cy.get('[data-test-id="btn-apporder-reset"]').click({ force: true })
+
+ cy.wait('@updateAppOrder')
+ .its('request.body')
+ .should('have.property', 'configValue', '[]')
+ cy.wait('@loadApps')
})
it('See the app order is restored', () => {
- cy.get('[data-cy-app-order] [data-cy-app-order-element]').then(elements => {
- const appIDs = elements.map((idx, el) => el.getAttribute('data-cy-app-order-element')).get()
- expect(appIDs).to.deep.eq(['dashboard', 'files'])
- })
+ const appOrder = ['Dashboard', 'Files']
+ // Check the app order settings UI
+ cy.get('[data-cy-app-order] [data-cy-app-order-element]')
+ .each((element, index) => expect(element).to.contain.text(appOrder[index]))
+
+ // Check the top app menu order
+ navigationHeader.getNavigationEntries()
+ .each((entry, index) => expect(entry).contain.text(appOrder[index]))
})
it('See the reset button is disabled again', () => {
diff --git a/cypress/e2e/theming/user-background.cy.ts b/cypress/e2e/theming/user-settings_background.cy.ts
index 445fb8a34f6..8abcb5bace1 100644
--- a/cypress/e2e/theming/user-background.cy.ts
+++ b/cypress/e2e/theming/user-settings_background.cy.ts
@@ -4,7 +4,8 @@
*/
import { User } from '@nextcloud/cypress'
-import { defaultPrimary, defaultBackground, pickRandomColor, validateBodyThemingCss } from './themingUtils'
+import { defaultPrimary, defaultBackground, validateBodyThemingCss } from './themingUtils'
+import { NavigationHeader } from '../../pages/NavigationHeader'
const admin = new User('admin', 'admin')
@@ -122,6 +123,8 @@ describe('User select a custom color', function() {
})
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)
@@ -159,12 +162,12 @@ describe('User select a bright custom color and remove background', function() {
})
it('See the header being inverted', function() {
- cy.waitUntil(() => cy.window().then((win) => {
- const firstEntry = win.document.querySelector('.app-menu-main li img')
- if (!firstEntry) {
- return false
- }
- return getComputedStyle(firstEntry).filter === 'invert(1)'
+ cy.waitUntil(() => navigationHeader.getNavigationEntries().find('img').then((el) => {
+ let ret = true
+ el.each(function() {
+ ret = ret && window.getComputedStyle(this).filter === 'invert(1)'
+ })
+ return ret
}))
})
@@ -181,12 +184,12 @@ describe('User select a bright custom color and remove background', function() {
})
it('See the header NOT being inverted this time', function() {
- cy.waitUntil(() => cy.window().then((win) => {
- const firstEntry = win.document.querySelector('.app-menu-main li')
- if (!firstEntry) {
- return false
- }
- return getComputedStyle(firstEntry).filter === 'none'
+ cy.waitUntil(() => navigationHeader.getNavigationEntries().find('img').then((el) => {
+ let ret = true
+ el.each(function() {
+ ret = ret && window.getComputedStyle(this).filter === 'none'
+ })
+ return ret
}))
})
})
diff --git a/cypress/pages/NavigationHeader.ts b/cypress/pages/NavigationHeader.ts
new file mode 100644
index 00000000000..5441b75de88
--- /dev/null
+++ b/cypress/pages/NavigationHeader.ts
@@ -0,0 +1,58 @@
+/*!
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+/**
+ * Page object model for the Nextcloud navigation header
+ */
+export class NavigationHeader {
+
+ /**
+ * Locator of the header bar wrapper
+ */
+ header() {
+ return cy.get('header#header')
+ }
+
+ /**
+ * Locator for the logo navigation entry (entry redirects to default app)
+ */
+ logo() {
+ return this.header()
+ .find('#nextcloud')
+ }
+
+ /**
+ * Locator of the app navigation bar
+ */
+ navigation() {
+ return this.header()
+ .findByRole('navigation', { name: 'Applications menu' })
+ }
+
+ /**
+ * The toggle for the navigation overflow menu
+ */
+ overflowNavigationToggle() {
+ return this.navigation()
+ }
+
+ /**
+ * Get all navigation entries
+ */
+ getNavigationEntries() {
+ return this.navigation()
+ .findAllByRole('listitem')
+ }
+
+ /**
+ * Get the navigation entry for a given app
+ * @param name The app name
+ */
+ getNavigationEntry(name: string) {
+ return this.navigation()
+ .findByRole('listitem', { name })
+ }
+
+}