From 3d89092b83e49e47204980aa65d4994948e8f68f Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Sun, 2 Aug 2026 13:33:44 +0200 Subject: [PATCH] =?UTF-8?q?test(e2e):=20record=20why=20version-rollback=20?= =?UTF-8?q?cannot=20be=20driven=20=E2=80=94=20the=20sidebar,=20not=20the?= =?UTF-8?q?=20contract?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ran it against the shared dev instance. It never reached its assertions, and the reason is not the rollback contract: aside.app-sidebar width 0 (closed) section.app-sidebar__tab[role=tabpanel] display:none button[aria-label="Open sidebar"] present AND visible -> clicking it TIMES OUT on actionability, at 1280x720 AND at 1920x1080 The sidebar will not open, so every tab-scoped assertion is unreachable — even though the tab's content is mounted underneath it. That is a UI defect to chase on its own, not something a selector change fixes. The product fix this spec depends on IS confirmed on both instances: `?tab=history` deep-links the tab and `.version-history__row` count is 3, where the panel previously rendered empty for every app. The data is right; only the chrome is unreachable. Three dead ends recorded so the next attempt does not repeat them: - getByText('Version history') resolves the tab button's LABEL SPAN, which is display:none once the tab strip collapses to icons — waiting on its visibility waits forever (18 resolutions, all "hidden"). FIVE nodes carry that exact text; the deepest is the panel's own

. - getByRole('tab', …) finds nothing — these are not ARIA tabs. - [aria-label*="sidebar" i] matches "Close sidebar" FIRST; the control is labelled exactly "Open sidebar". And the one route that does work: /applications/{uuid}?tab=history mounts the tab content directly. Also carried over from the same run, both measured rather than guessed: - a 150s budget for this describe, from GET /api/applications taking ~6.9s on the shared box against ~0.3s on a disposable one (28 apps, 200+ schemas); - the sidebar's open/closed state is per-user UI state and differs between instances, so a fixed "open it first" or "click it directly" step is wrong either way. Stays skipped: its assertions remain unexecuted, and shipping an unexecuted spec as coverage is the failure mode this file already documents three times. --- tests/e2e/version-rollback.spec.ts | 104 ++++++++++++++++++++++++----- 1 file changed, 88 insertions(+), 16 deletions(-) diff --git a/tests/e2e/version-rollback.spec.ts b/tests/e2e/version-rollback.spec.ts index 5921f0b4..e6f39ee1 100644 --- a/tests/e2e/version-rollback.spec.ts +++ b/tests/e2e/version-rollback.spec.ts @@ -69,18 +69,94 @@ async function appRecord(page: import('@playwright/test').Page): Promise clicking it TIMES OUT on actionability, at 1280x720 and at 1920x1080 +// +// So the sidebar will not open, and every tab-scoped assertion is unreachable +// even though the tab's content is mounted underneath it. That is an instance / +// UI defect to chase on its own, not something a selector change fixes. +// +// Three dead ends recorded so the next attempt skips them: +// - `getByText('Version history')` resolves the tab BUTTON'S LABEL SPAN, which +// is display:none once the tab strip collapses to icons. Waiting on its +// visibility waits forever (18 resolutions, all "hidden"). There are FIVE +// nodes with that exact text; the deepest is the panel's own

. +// - `getByRole('tab', …)` finds nothing — these are not ARIA tabs. +// - `[aria-label*="sidebar" i]` matches "Close sidebar" first; the control is +// labelled exactly "Open sidebar". +// +// The one thing that DOES work, and is the way in for a rewrite: +// /apps/openbuild/applications/{uuid}?tab=history +// mounts the tab content directly. Assert against the DOM it produces, or fix +// the sidebar first. + +/** + * Open the app detail page and reveal its "Version history" sidebar tab. + * + * The sidebar's open/closed state is NOT the same on every instance — it is + * per-user UI state, so it differs between a freshly seeded fixture box and a + * long-lived shared one. Both were observed within an hour: + * + * disposable instance : tabs already open; clicking the toggle TIMES OUT + * shared dev instance : tabs present in the DOM but "element is not visible" + * + * So neither "click the toggle first" nor "click the tab directly" works + * everywhere. This probes the tab and only opens the sidebar when it is hidden. + * + * ⚠️ This does NOT currently succeed on the shared dev instance — the sidebar + * refuses to open there at all (see the block comment above). Kept because the + * probe-then-open shape is right and the dead ends are documented; a rewrite + * should either fix the sidebar or drive `?tab=history` directly. + * + * @param page Playwright page. + * @param uuid The application uuid. + * @return {Promise} + */ +async function openVersionHistory(page: import('@playwright/test').Page, uuid: string): Promise { + await page.goto(`${BASE_URL}/apps/openbuild/applications/${uuid}`, { waitUntil: 'domcontentloaded' }) + await page.waitForLoadState('networkidle', { timeout: 60_000 }).catch(() => {}) + + // Target the TAB, not its label. `getByText('Version history')` resolves to + // the `` inside the tab button, and + // that span is display:none whenever the tab strip collapses to icons — which + // it does at this viewport on the shared instance. The span being hidden says + // nothing about the tab being reachable, so waiting on its visibility waits + // forever (18 resolutions, all "hidden"). + const tab = page.getByRole('tab', { name: /version history/i }).first() + + if (!(await tab.isVisible().catch(() => false))) { + // The control is a button labelled exactly "Open sidebar". An + // `[aria-label*="sidebar" i]` match is NOT good enough — "Close sidebar" + // is also present and matches first. + await page.getByRole('button', { name: 'Open sidebar', exact: true }) + .click({ timeout: 15_000 }) + .catch(() => {}) + await expect(tab, 'the Version history tab must become reachable').toBeVisible({ timeout: 20_000 }) + } + await tab.click({ timeout: 20_000 }) +} + test.describe.skip('openbuild-versioning — rollback (REQ-OBV-003)', () => { + // The default 30s cannot cover this on a loaded instance. Measured on the + // shared dev box (28 applications, 200+ schemas), a single + // GET /api/applications takes ~6.9s — against ~0.3s on a disposable + // fixture instance — and this spec needs several, plus two page loads and + // a version-chain seed. The budget is set from that measurement rather + // than raised until it passes. + test.describe.configure({ timeout: 150_000 }) + test.beforeEach(async ({ page }) => { await suppressSupportDialog(page) await page.goto(`${BASE_URL}/apps/openbuild/`, { waitUntil: 'domcontentloaded' }) @@ -88,9 +164,7 @@ test.describe.skip('openbuild-versioning — rollback (REQ-OBV-003)', () => { }) test('the version history tab lists the chain', async ({ page }) => { - await page.goto(`${BASE_URL}/apps/openbuild/applications/${await appUuid(page)}`, { waitUntil: 'domcontentloaded' }) - await page.waitForLoadState('networkidle', { timeout: 30_000 }).catch(() => {}) - await page.getByText('Version history', { exact: true }).first().click({ timeout: 10_000 }) + await openVersionHistory(page, await appUuid(page)) // The regression guard: this panel used to render `.version-history__empty` // for every app because of the applicationUuid filter described above. @@ -103,10 +177,8 @@ test.describe.skip('openbuild-versioning — rollback (REQ-OBV-003)', () => { test('rolling back copies the snapshot manifest onto the app as a draft', async ({ page }) => { const before = await appRecord(page) - await page.goto(`${BASE_URL}/apps/openbuild/applications/${await appUuid(page)}`, { waitUntil: 'domcontentloaded' }) - await page.waitForLoadState('networkidle', { timeout: 30_000 }).catch(() => {}) - await page.getByText('Version history', { exact: true }).first().click({ timeout: 10_000 }) - await expect(page.locator('.version-history__row').first()).toBeVisible({ timeout: 15_000 }) + await openVersionHistory(page, await appUuid(page)) + await expect(page.locator('.version-history__row').first()).toBeVisible({ timeout: 20_000 }) // "Roll back" only renders on a NON-production row (`v-if="!isProduction(row)"`), // so this also proves the terminal production version offers no rollback.