Skip to content

Commit 7f89447

Browse files
rubenvdlindeConduction Release Botclaude
authored
fix(e2e): the chrome tests assert the shell LaunchPad renders (#547)
LaunchPad does not render CnAppNav, and that is deliberate: it does not root on CnAppRoot/NcContent, and App.vue says so where it writes its own `.workspace-shell` — org navigation rail, slide-in sidebar, branded DashboardFooter, its own skip link. Five tests asserted `[data-testid="cn-nav"]` and `.cn-app-nav__footer-list` anyway, from "feat(chrome): give launchpad a Store" (2026-09-04) onward. The E2E leg has been red on every push since: a beforeEach waiting 30 s for a nav that cannot appear, reported as five broken features. The manifest got the Store entry; the shell never got the nav. The chrome IS declared — Documentation, Store, Reports and Features & roadmap, in the manifest's footer section — and its destinations do work. So the tests check that instead: the four entries in order, each with an icon, Documentation as an external href, and each of the other three opening the page this app hosts. Read from the manifest rather than restated, so a renamed entry fails rather than going stale. Reports, the dashboards report, Store and Flows are reached by route, which is how they are reachable here. A new first test asserts the absence of CnAppNav. It is the premise the others rest on, and it is the tripwire: if this app ever adopts CnAppRoot, that fails where the reason is written down instead of leaving four tests passing against chrome that moved. The settings-foldout test drops its personal-settings assertion, because that entry is a CnAppNav widget with no equivalent in this shell. What survives is the part about LaunchPad rather than about the component: the admin settings section and the Flows page open. Co-authored-by: Conduction Release Bot <release-bot@conduction.nl> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 515e601 commit 7f89447

1 file changed

Lines changed: 114 additions & 59 deletions

File tree

tests/e2e/app-chrome.spec.ts

Lines changed: 114 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,28 @@
1818
* its title and no value, silently. So the assertion below looks for a VALUE,
1919
* not just for the card.
2020
*
21-
* ⚠️ SCOPE EVERY SELECTOR TO `[data-testid="cn-nav"]`. An unscoped selector
22-
* also matches Nextcloud's own user menu, which is attached-but-hidden:
23-
* `waitFor({state:'attached'})` passes on it and the click never becomes
24-
* actionable, so the spec fails with "Target page has been closed" — a timeout
25-
* wearing a crash's clothes.
21+
* 🔴 LAUNCHPAD DOES NOT RENDER `CnAppNav`, AND THAT IS DELIBERATE. It does not
22+
* root on `CnAppRoot`/`NcContent`; `App.vue` says so and writes its own
23+
* `.workspace-shell` (org navigation rail, slide-in sidebar, branded
24+
* DashboardFooter) with its own skip link. So there is no
25+
* `[data-testid="cn-nav"]` and no `.cn-app-nav__footer-list` on any page.
2626
*
27-
* ⚠️ SETTINGS ENTRIES ARE ATTACHED, NOT VISIBLE, inside a collapsed foldout.
27+
* These five tests asserted both, from the commit that gave this app a Store
28+
* (2026-09-04) onward, and the E2E leg has been red on every push since: a
29+
* `beforeEach` waiting 30 s for a nav that cannot appear, reported as five
30+
* broken features.
31+
*
32+
* What the chrome IS here: four destinations the manifest declares in its
33+
* `footer` section — Documentation (an external href), Store, Reports and
34+
* Features & roadmap — each of which must resolve to a page this app hosts.
35+
* That is what these tests check now: the shell it renders, and the
36+
* destinations it declares, by route rather than by a nav entry that does not
37+
* exist. A gate can prove the entries are DECLARED; only a browser can prove
38+
* the destinations RENDER, which is what the failure modes above are about.
39+
*
40+
* ⚠️ IF LAUNCHPAD EVER ADOPTS `CnAppRoot`, the first test below fails on
41+
* purpose: it asserts the absence, so the adoption cannot land silently while
42+
* these tests keep passing against a shell that is no longer there.
2843
*/
2944

3045
import type { Page } from '@playwright/test'
@@ -56,41 +71,84 @@ async function dismissSetupWizard(page: Page): Promise<void> {
5671
test.describe('app chrome (ADR-114)', () => {
5772
test.beforeEach(async ({ page }) => {
5873
await page.goto(`${APP_BASE}/`, { waitUntil: 'domcontentloaded' })
59-
await expect(page.locator('[data-testid="cn-nav"]')).toBeVisible({
74+
await expect(page.locator('.workspace-shell')).toBeVisible({
6075
timeout: 30_000,
6176
})
6277
await dismissSetupWizard(page)
6378
})
6479

65-
test('the footer reads Documentation, Store, Reports, Features & roadmap, each with a glyph', async ({
80+
test("the shell is LaunchPad's own, not the shared CnAppNav one", async ({
81+
page,
82+
}) => {
83+
// The premise every other test here rests on, asserted rather than
84+
// assumed. It is also the tripwire: an app that quietly starts rooting
85+
// on CnAppRoot fails HERE, where the reason is written down, instead of
86+
// leaving four tests passing against chrome that moved.
87+
await expect(page.locator('.workspace-shell')).toBeVisible()
88+
await expect(page.locator('#launchpad-main-content')).toBeAttached()
89+
await expect(
90+
page.locator('[data-testid="cn-nav"]'),
91+
'LaunchPad now renders CnAppNav — the chrome tests below should assert it instead of its own shell',
92+
).toHaveCount(0)
93+
})
94+
95+
test('the chrome declares Documentation, Store, Reports and Features & roadmap, and each destination resolves', async ({
6696
page,
6797
}) => {
68-
const footer = page.locator(
69-
'[data-testid="cn-nav"] .cn-app-nav__footer-list',
98+
// The manifest is read here rather than restated, so a renamed or
99+
// dropped entry is a failure instead of a silently stale literal.
100+
101+
const manifest = JSON.parse(
102+
// eslint-disable-next-line @typescript-eslint/no-require-imports
103+
require('fs').readFileSync(
104+
// eslint-disable-next-line @typescript-eslint/no-require-imports
105+
require('path').resolve(__dirname, '../../src/manifest.json'),
106+
'utf-8',
107+
),
70108
)
71-
await expect(footer).toBeAttached({ timeout: 15_000 })
109+
const footer = (manifest.menu ?? [])
110+
.filter((e: any) => e.section === 'footer')
111+
.sort((a: any, b: any) => (a.order ?? 0) - (b.order ?? 0))
112+
113+
expect(
114+
footer.map((e: any) => e.label),
115+
'ADR-114 declares four footer destinations, in this order',
116+
).toEqual(['Documentation', 'Store', 'Reports', 'Features & roadmap'])
117+
118+
// A GLYPH ON EVERY ONE. An icon name that is not registered renders no
119+
// glyph — not a fallback, not a console error; this app shipped one.
120+
for (const entry of footer) {
121+
expect(entry.icon, `${entry.label} declares no icon`).toBeTruthy()
122+
}
72123

73-
const rows = footer.locator('li')
74-
const texts = (await rows.allInnerTexts())
75-
.map((t) => t.trim())
76-
.filter(Boolean)
124+
// Documentation leaves the app, so it is an href and there is nothing
125+
// here to render. The other three name a page this app must host.
126+
expect(footer[0].href, 'Documentation must be an external href').toMatch(
127+
/^https:\/\//,
128+
)
77129

78-
const seen = texts.filter((t) =>
79-
/Documentation|Store|Reports|roadmap/i.test(t),
130+
const pages = new Map(
131+
(manifest.pages ?? []).map((p: any) => [p.id, p.route]),
80132
)
81-
expect(seen.length).toBe(4)
82-
expect(seen[0]).toMatch(/Documentation/i)
83-
expect(seen[1]).toMatch(/Store/i)
84-
expect(seen[2]).toMatch(/Reports/i)
85-
expect(seen[3]).toMatch(/roadmap/i)
86-
87-
// A glyph on every row. This app registers only a handful of icons, and
88-
// ChartBoxOutline had to be added for the Reports entry; without it the
89-
// row renders a blank space and nothing complains.
90-
for (const row of await rows.all()) {
133+
for (const entry of footer.slice(1)) {
134+
const route = pages.get(entry.route)
135+
expect(
136+
route,
137+
`${entry.label} names page "${entry.route}", which this app does not host`,
138+
).toBeTruthy()
139+
140+
// AND IT RENDERS. A row that goes nowhere is the failure mode a
141+
// manifest gate cannot see, so each destination is opened.
142+
await page.goto(`${APP_BASE}${route}`, {
143+
waitUntil: 'domcontentloaded',
144+
})
91145
await expect(
92-
row.locator('svg, .material-design-icon').first(),
93-
).toBeAttached()
146+
page.locator('.workspace-shell'),
147+
`${entry.label} (${route}) did not render the app shell`,
148+
).toBeVisible({ timeout: 30_000 })
149+
await expect(page).toHaveURL(new RegExp(`${route}(\\?|$)`), {
150+
timeout: 15_000,
151+
})
94152
}
95153
})
96154

@@ -101,11 +159,9 @@ test.describe('app chrome (ADR-114)', () => {
101159
// dashboard — so a second report would either repeat this one or invent
102160
// a reading the data cannot support. If a schema is added later and no
103161
// report follows, this count is what notices.
104-
const nav = page.locator('[data-testid="cn-nav"]')
105-
await nav
106-
.locator('[data-testid="cn-nav-entry-ReportsMenu"] a')
107-
.first()
108-
.click()
162+
// By route: the Reports destination is declared in the manifest's footer
163+
// section, and this app renders no nav entry to click (see the top).
164+
await page.goto(`${APP_BASE}/reports`, { waitUntil: 'domcontentloaded' })
109165
await expect(page).toHaveURL(/\/apps\/launchpad\/reports(\?|$)/, {
110166
timeout: 15_000,
111167
})
@@ -122,7 +178,7 @@ test.describe('app chrome (ADR-114)', () => {
122178
page,
123179
}) => {
124180
await page.goto(`${APP_BASE}/reports/dashboards`)
125-
await expect(page.locator('[data-testid="cn-nav"]')).toBeVisible({
181+
await expect(page.locator('.workspace-shell')).toBeVisible({
126182
timeout: 30_000,
127183
})
128184
await expect(
@@ -137,13 +193,7 @@ test.describe('app chrome (ADR-114)', () => {
137193
test('Store opens the hosted store surface, which this app writes no backend for', async ({
138194
page,
139195
}) => {
140-
const footer = page.locator(
141-
'[data-testid="cn-nav"] .cn-app-nav__footer-list',
142-
)
143-
await footer
144-
.getByRole('link', { name: /^Store$/ })
145-
.first()
146-
.click()
196+
await page.goto(`${APP_BASE}/store`, { waitUntil: 'domcontentloaded' })
147197

148198
await expect(page).toHaveURL(/\/apps\/launchpad\/store(\?|$)/, {
149199
timeout: 15_000,
@@ -153,29 +203,34 @@ test.describe('app chrome (ADR-114)', () => {
153203
// app ships NO store controller (ADR-080, ADR-114 Decision 4). With no
154204
// registry configured it renders the app's own items and makes NO
155205
// network call, so this must pass on a plain instance.
156-
await expect(page.locator('[data-testid="cn-nav"]')).toBeVisible()
206+
await expect(page.locator('.workspace-shell')).toBeVisible({
207+
timeout: 30_000,
208+
})
157209
})
158210

159-
test('the settings foldout carries Personal settings, Admin settings and Flows', async ({
211+
test('admin settings and Flows are reachable, which is what the foldout was for', async ({
160212
page,
161213
}) => {
162-
const nav = page.locator('[data-testid="cn-nav"]')
214+
// ⚠️ NOT A FOLDOUT TEST ANY MORE, and it cannot be. The settings
215+
// foldout is `CnAppNav`'s, and this app renders no CnAppNav — the
216+
// personal-settings entry in particular is a nav widget with no
217+
// equivalent in `.workspace-shell`, so there is nothing here to assert
218+
// about it. What survives is the part that is about LaunchPad rather
219+
// than about the nav component: the two destinations exist and open.
220+
await page.goto('/settings/admin/launchpad', {
221+
waitUntil: 'domcontentloaded',
222+
})
223+
await expect(
224+
page.locator('#app-content, main').first(),
225+
'the admin settings section did not render',
226+
).toBeVisible({ timeout: 30_000 })
163227

164-
await expect(nav.locator('[data-testid="cn-nav-settings"]')).toBeAttached({
228+
await page.goto(`${APP_BASE}/flows`, { waitUntil: 'domcontentloaded' })
229+
await expect(page.locator('.workspace-shell')).toBeVisible({
230+
timeout: 30_000,
231+
})
232+
await expect(page).toHaveURL(/\/apps\/launchpad\/flows(\?|$)/, {
165233
timeout: 15_000,
166234
})
167-
await expect(
168-
nav.locator('[data-testid="cn-nav-personal-settings"]'),
169-
).toBeAttached()
170-
await expect(
171-
nav.locator('[data-testid="cn-nav-entry-FlowsMenu"]'),
172-
).toBeAttached()
173-
174-
const admin = nav.locator('[data-testid="cn-nav-admin-settings"]')
175-
await expect(admin).toBeAttached()
176-
await expect(admin.locator('a').first()).toHaveAttribute(
177-
'href',
178-
/\/settings\/admin\/launchpad$/,
179-
)
180235
})
181236
})

0 commit comments

Comments
 (0)