fix(cypress): adapt NavigationHeader and theming specs to waffle launcher

Signed-off-by: Peter Ringelmann <peter.ringelmann@nextcloud.com>
This commit is contained in:
Peter Ringelmann 2026-05-07 15:13:54 +02:00 committed by Peter R.
parent 831989b770
commit c36d64d06a
4 changed files with 123 additions and 29 deletions

View file

@ -175,9 +175,15 @@ describe('Remove the default background with a bright color', function() {
})
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
.getNavigationEntries()
.find('img')
.logo()
.find('.logo')
.then((el) => {
let ret = true
el.each(function() {

View file

@ -35,9 +35,14 @@ describe('User theming set app order', () => {
const appOrder = ['Dashboard', 'Files']
appOrderList.assertAppOrder(appOrder)
// Check the top app menu order
navigationHeader.getNavigationEntries()
.each((entry, index) => expect(entry).contain.text(appOrder[index]!))
// 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', () => {
@ -59,9 +64,14 @@ describe('User theming set app order', () => {
.scrollIntoView()
appOrderList.assertAppOrder(appOrder)
// Check the top app menu order
navigationHeader.getNavigationEntries()
.each((entry, index) => expect(entry).contain.text(appOrder[index]!))
// 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)
})
})
})
})
@ -140,9 +150,13 @@ describe('User theming set app order with default app', () => {
cy.reload()
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]!))
// 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)
})
})
})
})
@ -219,9 +233,12 @@ describe('User theming reset app order', () => {
const appOrder = ['Dashboard', 'Files']
appOrderList.assertAppOrder(appOrder)
// Check the top app menu order
navigationHeader.getNavigationEntries()
.each((entry, index) => expect(entry).contain.text(appOrder[index]!))
// 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', () => {
@ -263,9 +280,12 @@ describe('User theming reset app order', () => {
it('See the app order is restored', () => {
const appOrder = ['Dashboard', 'Files']
appOrderList.assertAppOrder(appOrder)
// Check the top app menu order
navigationHeader.getNavigationEntries()
.each((entry, index) => expect(entry).contain.text(appOrder[index]!))
// 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', () => {

View file

@ -131,7 +131,13 @@ describe('User select a bright custom color and remove background', function() {
})
it('See the header being inverted', function() {
cy.waitUntil(() => navigationHeader.getNavigationEntries().find('img').then((el) => {
// 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)'
@ -157,7 +163,9 @@ describe('User select a bright custom color and remove background', function() {
})
it('See the header NOT being inverted this time', function() {
cy.waitUntil(() => navigationHeader.getNavigationEntries().find('img').then((el) => {
// 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'

View file

@ -4,7 +4,11 @@
*/
/**
* Page object model for the Nextcloud navigation header
* Page object model for the Nextcloud navigation header.
*
* The app launcher (waffle menu) is an NcPopover whose content is teleported
* to <body>, so the menu items do not live inside the <nav> element. Selectors
* for the menu entries scope to the popover rather than the nav.
*/
export class NavigationHeader {
/**
@ -23,35 +27,91 @@ export class NavigationHeader {
}
/**
* Locator of the app navigation bar
* Locator of the app navigation bar.
*
* The accessible name is just "Applications" since the waffle redesign;
* the previous label "Applications menu" is gone.
*/
navigation() {
return this.header()
.findByRole('navigation', { name: 'Applications menu' })
.findByRole('navigation', { name: 'Applications' })
}
/**
* The toggle for the navigation overflow menu
* Open the waffle launcher popover.
*
* Idempotent: if the popover is already open the click is skipped, so
* callers can invoke this defensively at the start of any helper that
* needs the menu items in the DOM.
*/
openMenu() {
this.navigation()
.find('.app-menu__waffle')
.then(($trigger) => {
if ($trigger.attr('aria-expanded') !== 'true') {
cy.wrap($trigger).click()
}
})
// Popover is teleported to <body>, so query from the document root.
cy.get('.app-menu__popover').should('be.visible')
return this.popover()
}
/**
* Close the waffle launcher popover.
*
* Sends Escape rather than clicking outside: NcPopover's focus trap is
* active while the menu is open, so a stray click can land on a tile.
*/
closeMenu() {
cy.get('body').type('{esc}')
cy.get('.app-menu__popover').should('not.exist')
}
/**
* Locator for the popover content (the teleported grid wrapper).
*
* Scoping menu-item queries here is mandatory: the popover is rendered
* outside the <nav>, so `.within(navigation())` would find nothing.
*/
popover() {
return cy.get('[role="menu"][aria-label="Apps"]')
}
/**
* The waffle trigger that toggles the launcher.
*
* @deprecated The old "overflow" affordance is gone; this now points at
* the waffle button so existing call sites keep compiling. Prefer
* {@link openMenu} / {@link closeMenu} in new code.
*/
overflowNavigationToggle() {
return this.navigation()
.find('.app-menu__waffle')
}
/**
* Get all navigation entries
* Get all navigation entries in the launcher.
*
* Opens the popover first if it is not already open; the entries do not
* exist in the DOM otherwise. Each entry is rendered as an `<a role="menuitem">`.
*/
getNavigationEntries() {
return this.navigation()
.findAllByRole('listitem')
this.openMenu()
return this.popover().findAllByRole('menuitem')
}
/**
* Get the navigation entry for a given app
* Get the navigation entry for a given app.
*
* Each tile's accessible name comes from the `<a title="...">` attribute
* and the inner `.app-item__label`, so `findByRole('menuitem', { name })`
* matches reliably.
*
* @param name The app name
*/
getNavigationEntry(name: string) {
return this.navigation()
.findByRole('listitem', { name })
this.openMenu()
return this.popover().findByRole('menuitem', { name })
}
}