diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml
index a321652b..82593f8b 100644
--- a/.github/workflows/code-quality.yml
+++ b/.github/workflows/code-quality.yml
@@ -110,7 +110,37 @@ jobs:
# Register on application-type imports — OR PR #1464) and the lifecycle
# engine (ObjectTransitionedEvent) that OpenBuild's repair steps + the
# Newman publish step need; `main` predates both. See openbuild#29.
- additional-apps: '[{"repo":"ConductionNL/openregister","app":"openregister","ref":"development"}]'
+ #
+ # DOCUDESK IS HERE BECAUSE THREE E2E SPECS ASSERT AGAINST ITS API AND
+ # THERE WAS NO WAY FOR THEM TO PASS WITHOUT IT.
+ #
+ # `tests/e2e/spec-coverage/docudesk-document-templates.spec.ts` drives the
+ # builder's Documents section: attach a template, preview it, and warn
+ # when the attached template was deleted. All three go through Docudesk's
+ # own REST surface (`GET/POST /apps/docudesk/api/templates`,
+ # `GET /apps/docudesk/api/templates/{id}`,
+ # `POST /apps/docudesk/api/templates/{id}/preview`). With Docudesk absent
+ # every one of those is a Nextcloud router 404, and the run said so
+ # plainly: `[globalSetup] docudesk not installed — template fixtures
+ # skipped`, then three failures reading `Expected: 200 / Received: 404`.
+ # That is not an app defect and no code change in this repository could
+ # have fixed it — the dependency was simply not installed.
+ #
+ # `tests/e2e/global-setup.ts` already knows what to do once it IS: it
+ # configures Docudesk's `template_register`/`template_schema` through
+ # Docudesk's own `POST /api/settings` and seeds the two template fixtures
+ # the specs attach to, and it degrades to a log line when Docudesk is
+ # missing. So this input is the whole fix.
+ #
+ # `ref: main` — Docudesk has no `development` branch (verified by listing
+ # the repo's branches; it publishes `main` and `beta` only), and `main`
+ # carries every route above.
+ #
+ # Only the API is exercised. `additional-apps` clones and runs `composer
+ # install` but never builds an app's frontend, so Docudesk's own JS bundle
+ # is absent here — which is fine, because no spec in this suite opens a
+ # Docudesk page.
+ additional-apps: '[{"repo":"ConductionNL/openregister","app":"openregister","ref":"development"},{"repo":"ConductionNL/docudesk","app":"docudesk","ref":"main"}]'
enable-sbom: true
# Integration Tests (Newman) stays OFF here, deliberately, and unlike the
diff --git a/playwright.config.ts b/playwright.config.ts
index a7f8bc7c..d8bc22a7 100644
--- a/playwright.config.ts
+++ b/playwright.config.ts
@@ -38,6 +38,15 @@ export default defineConfig({
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 1 : 0,
workers: 1,
+ // Mirror of tests/e2e/playwright.config.ts. CI does not pick this file up
+ // (`playwright-test-path: tests/e2e` makes the workflow's first lookup hit
+ // the config next to the specs), but it IS the documented fallback the
+ // shared workflow uses when that file is absent — and the job it would run
+ // under carries `timeout-minutes: 45`. A cancellation at the job cap is not
+ // a verdict: no tally, and the report is never written so the artifact
+ // upload finds nothing. Stopping ourselves below the cap fails with numbers
+ // attached instead.
+ globalTimeout: 36 * 60 * 1000,
globalSetup: './tests/e2e/global-setup.ts',
reporter: process.env.CI ? [['github'], ['html', { open: 'never' }]] : 'list',
use: {
@@ -56,7 +65,12 @@ export default defineConfig({
// (no Location header is emitted, the page stays on /login).
// Specs that need OCS-APIRequest set it on their explicit `request`
// calls (e.g. versionRouting.spec.ts, applicationDetailOverview.spec.ts).
- trace: 'on-first-retry',
+ // `retain-on-failure`, not `on-first-retry`: this config's `retries` is
+ // 1 only on CI and 0 locally, so on a developer box the old setting
+ // produced no trace for any failure at all — and CI does not use this
+ // config (see tests/e2e/playwright.config.ts). Keeping the two files in
+ // step so a local reproduction has the same evidence a CI run does.
+ trace: 'retain-on-failure',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
headless: true,
diff --git a/src/dialogs/DocumentTemplateAttachmentDialog.vue b/src/dialogs/DocumentTemplateAttachmentDialog.vue
index 67430768..d1a0d798 100644
--- a/src/dialogs/DocumentTemplateAttachmentDialog.vue
+++ b/src/dialogs/DocumentTemplateAttachmentDialog.vue
@@ -21,10 +21,30 @@
@update:open="$emit('update:open', $event)"
@closing="onClose">
+
{{ t('openbuild', 'Docudesk is not installed or enabled on this instance. The template list cannot be loaded.') }}
-
+
{{ t('openbuild', 'The attached template no longer exists in Docudesk. Pick another template or detach.') }}
diff --git a/tests/dialogs/DocumentTemplateAttachmentDialog.spec.js b/tests/dialogs/DocumentTemplateAttachmentDialog.spec.js
index 6b82b4a7..b7952424 100644
--- a/tests/dialogs/DocumentTemplateAttachmentDialog.spec.js
+++ b/tests/dialogs/DocumentTemplateAttachmentDialog.spec.js
@@ -66,3 +66,53 @@ describe('DocumentTemplateAttachmentDialog — preview sanitization', () => {
expect(out).toContain('
a ')
})
})
+
+/**
+ * The two `.ob-document-attach__warn` paragraphs are mutually exclusive claims
+ * about the same subject: "Docudesk is not installed" and "the template you
+ * attached no longer exists IN Docudesk". The second is not knowable when the
+ * first is true.
+ *
+ * They were authored as two independent `v-if`s, and both rendered together
+ * whenever `docudeskAvailable` arrived late — the normal case, since
+ * PageDesignerHost initialises it to `true` and resolves the real value
+ * asynchronously. The dialog's `open` watcher then ran its snapshot refresh,
+ * took a 404 from a route that does not exist, set `templateMissing`, and the
+ * late `false` stacked the absence message on top.
+ *
+ * Playwright saw it as `strict mode violation: locator('.ob-document-attach__warn')
+ * resolved to 2 elements` in run 31083894467. These assertions pin the
+ * invariant directly so the next regression is caught in milliseconds by the
+ * unit suite instead of in a 19-minute browser run.
+ */
+describe('DocumentTemplateAttachmentDialog — mutually exclusive warnings', () => {
+ /**
+ * Mount the dialog and drive it into the both-warnings-eligible state.
+ *
+ * @param {boolean} docudeskAvailable Value of the capability prop.
+ * @return {Promise
} The mounted wrapper.
+ */
+ async function warnState(docudeskAvailable) {
+ const wrapper = mount(DocumentTemplateAttachmentDialog, {
+ propsData: { docudeskAvailable },
+ stubs: baseStubs,
+ })
+ // `templateMissing` is what a 404 from the snapshot refresh sets.
+ await wrapper.setData({ templateMissing: true })
+ return wrapper
+ }
+
+ it('renders exactly one warning when Docudesk is absent AND a template 404d', async () => {
+ const wrapper = await warnState(false)
+ const warns = wrapper.findAll('.ob-document-attach__warn')
+ expect(warns).toHaveLength(1)
+ expect(warns[0].text()).toContain('not installed')
+ })
+
+ it('renders the deleted-template warning when Docudesk IS available', async () => {
+ const wrapper = await warnState(true)
+ const warns = wrapper.findAll('.ob-document-attach__warn')
+ expect(warns).toHaveLength(1)
+ expect(warns[0].text()).toContain('no longer exists')
+ })
+})
diff --git a/tests/e2e/automations-rbac.spec.ts b/tests/e2e/automations-rbac.spec.ts
index c93ed1bd..1a7e4ae3 100644
--- a/tests/e2e/automations-rbac.spec.ts
+++ b/tests/e2e/automations-rbac.spec.ts
@@ -27,6 +27,11 @@ const EDITOR_USER = process.env.NC_RBAC_EDITOR_USER ?? 'rbac-editor'
const EDITOR_PASS = process.env.NC_RBAC_EDITOR_PASS ?? 'RbacEditor-1!'
const OWNER_USER = process.env.NC_RBAC_OWNER_USER ?? 'rbac-owner'
const OWNER_PASS = process.env.NC_RBAC_OWNER_PASS ?? 'RbacOwner-1!'
+// Admin credentials for the capability probe below ONLY. Same resolution the
+// config uses for `use.httpCredentials`; the tests themselves deliberately run
+// as non-admins.
+const ADMIN_USER = process.env.NC_ADMIN_USER ?? 'admin'
+const ADMIN_PASS = process.env.NC_ADMIN_PASSWORD ?? process.env.NC_ADMIN_PASS ?? 'admin'
// The seeded `hello-world` fixture only ever carries a single `production`
// version (see lib/Command/SeedHelloWorldFixture.php) — there is no draft
// version to author on, so REQ-AUTD-008's "editor authors on draft, gets 403
@@ -46,24 +51,48 @@ const APP_SLUG = process.env.NC_RBAC_TEST_SLUG ?? 'rbac-automations-app'
const APP_TITLE_PATTERN = new RegExp(APP_SLUG.replace(/-/g, '.?'), 'i')
/**
- * Same schema-slug-collision guard as automations.spec.ts (duplicated here
- * since each e2e spec file is self-contained) — see that file's
- * `automationSchemaIsUsable()` doc comment for the full root-cause writeup.
- * On this shared instance the openbuild `automation` schema slug collides
- * with a pre-existing, unrelated schema of the same slug from another app,
- * so `POST`ing a real automation payload 400s regardless of which
- * Application/Version it targets — this blocks BOTH scenarios in this file
- * (composing one, and toggling a pre-existing one) identically.
+ * Is openbuild's `automation` schema readable and shaped as this suite expects?
+ *
+ * THIS PROBE USED TO REPORT `false` HERE AND `true` EVERYWHERE ELSE, IN THE
+ * SAME RUN, AGAINST THE SAME INSTANCE.
+ *
+ * It is a copy of `automations.spec.ts`'s helper, and it carried that file's
+ * reason with it: "the openbuild `automation` schema slug collides with a
+ * pre-existing schema of the same slug on this shared instance — automation
+ * CREATE/SAVE 400s regardless of app/version". Run 31083894467 disproves that
+ * for CI outright. Seven tests in `automations.spec.ts` sit behind the very
+ * same guard and PASSED, composing and saving real automations end to end
+ * (REQ-AUTD-002 ×3, -003, -005, -006, -007). Both tests in THIS file skipped.
+ *
+ * The discriminator is not the instance, it is the auth context. This describe
+ * declares `test.use({ storageState: { cookies: [], origins: [] } })` so each
+ * test can log in as a non-admin — which also makes the `request` fixture
+ * anonymous. The probe's read of `api/schemas/automation` was therefore
+ * refused, `resp.ok()` was false, and the helper reported the refusal as a
+ * fact about the schema. A guard that returns "the feature is broken" when it
+ * means "I could not look" produces exactly this: a permanent skip with a
+ * confident, wrong explanation attached.
+ *
+ * Two changes. The probe authenticates with the admin credentials the config
+ * already uses for `httpCredentials`, independently of whatever storageState
+ * the test is running under. And a NON-OK response is now a thrown error
+ * rather than a `false`, so "cannot read the schema" fails the run loudly
+ * instead of silently becoming "the schema is unusable" — the failure mode
+ * this helper just spent a release exhibiting.
*
* @param request Playwright APIRequestContext (fixture-provided).
- * @return {Promise} True when automation CREATE/SAVE is usable.
+ * @return {Promise} True when the schema reads back with the expected shape.
*/
async function automationSchemaIsUsable(request: APIRequestContext): Promise {
+ const auth = Buffer.from(`${ADMIN_USER}:${ADMIN_PASS}`).toString('base64')
const resp = await request.get(`${NEXTCLOUD_URL}/index.php/apps/openregister/api/schemas/automation`, {
- headers: { 'OCS-APIRequest': 'true' },
+ headers: { 'OCS-APIRequest': 'true', Authorization: `Basic ${auth}` },
})
if (resp.ok() === false) {
- return false
+ throw new Error(
+ `automationSchemaIsUsable: could not read api/schemas/automation — HTTP ${resp.status()}. `
+ + 'This is a broken probe, not a verdict about the schema; it must not be reported as one.',
+ )
}
const schema = await resp.json()
return schema?.properties?.trigger?.type === 'object'
@@ -93,7 +122,7 @@ test.describe('automation-designer — RBAC (REQ-AUTD-008)', () => {
test.use({ storageState: { cookies: [], origins: [] } })
test('editor authors + enables an automation on a non-production (draft) version', async ({ page, request }) => {
- test.skip(await automationSchemaIsUsable(request) === false, 'openbuild `automation` schema slug collides with a pre-existing schema of the same slug on this shared instance — automation CREATE/SAVE 400s regardless of app/version; see automationSchemaIsUsable()')
+ test.skip(await automationSchemaIsUsable(request) === false, 'openbuild `automation` schema does not read back with a `trigger` object property — see automationSchemaIsUsable() for why this must be a real verdict and not a failed lookup')
await loginAs(page, EDITOR_USER, EDITOR_PASS)
await page.goto(`${NEXTCLOUD_URL}/apps/openbuild/automations`)
await page.waitForSelector('.automations-page', { timeout: 20_000 })
@@ -121,7 +150,7 @@ test.describe('automation-designer — RBAC (REQ-AUTD-008)', () => {
})
test('editor gets 403 enabling on the production version; owner succeeds', async ({ page, browser, request }) => {
- test.skip(await automationSchemaIsUsable(request) === false, 'requires a pre-existing automation on the production version, which cannot be created — openbuild `automation` schema slug collides with a pre-existing schema of the same slug on this shared instance; see automationSchemaIsUsable()')
+ test.skip(await automationSchemaIsUsable(request) === false, 'openbuild `automation` schema does not read back with a `trigger` object property — see automationSchemaIsUsable() for why this must be a real verdict and not a failed lookup')
await loginAs(page, EDITOR_USER, EDITOR_PASS)
await page.goto(`${NEXTCLOUD_URL}/apps/openbuild/automations`)
await page.waitForSelector('.automations-page', { timeout: 20_000 })
diff --git a/tests/e2e/bootstrap-openbuild.e2e.spec.ts b/tests/e2e/bootstrap-openbuild.e2e.spec.ts
index bd8d3ced..b6d8a2e6 100644
--- a/tests/e2e/bootstrap-openbuild.e2e.spec.ts
+++ b/tests/e2e/bootstrap-openbuild.e2e.spec.ts
@@ -17,14 +17,28 @@ import { test, expect } from '@playwright/test'
* - Playwright browsers installed (`npx playwright install --with-deps`).
*/
test.describe('bootstrap-openbuild hello-world', () => {
- // QUARANTINED (Conduction/openbuild#41): openbuild admin UI not functional in this build — builder host blank (BuilderHostView unresolved by nc-vue CnPageRenderer) / no detail/editor/version pages. Re-enable when #41 is fixed.
- test.skip('renders the three seeded hello-message objects on the index page', async ({ page }) => {
+ // UN-QUARANTINED 2026-08-06. The recorded reason — "#41: builder host blank
+ // (BuilderHostView unresolved by nc-vue CnPageRenderer)" — is the SAME
+ // sentence builder-host.spec.ts carries above its own un-quarantine note
+ // saying it "no longer holds". builder-host.spec.ts's first test performs
+ // this identical journey (goto /apps/openbuild/builder/hello-world, then
+ // assert the same three seeded titles) and PASSES in CI — measured in run
+ // 31083894467. One of the two files was simply never revisited.
+ test('renders the three seeded hello-message objects on the index page', async ({ page }) => {
await page.goto('/apps/openbuild/builder/hello-world')
// The SPA needs a moment to fetch the manifest and resolve the index page.
// The hello-world manifest's index page lists `hello-message` objects with
// the title, body and @self.created columns.
- await expect(page).toHaveURL(/\/index\.php\/apps\/openbuild\/builder\/hello-world/)
+ //
+ // The `/index.php` prefix is OPTIONAL and is not what this test is about.
+ // Nextcloud emits it only when `htaccess.IgnoreFrontController` is off;
+ // CI turns it ON (tests/e2e/ci-seed.sh gates on the served page reporting
+ // `modRewriteWorking:true`), so the pretty form is what the router
+ // produces there and the old anchored regex could not have matched. The
+ // app path is still asserted in full — only the webroot style, an
+ // instance-configuration artifact, is allowed to vary.
+ await expect(page).toHaveURL(/(\/index\.php)?\/apps\/openbuild\/builder\/hello-world/)
// Seed bodies — anchored on the canonical strings written by
// SeedHelloWorld::buildSampleMessages(). At minimum the page must
diff --git a/tests/e2e/builder-undo-redo.spec.ts b/tests/e2e/builder-undo-redo.spec.ts
index 0f059c73..7b647642 100644
--- a/tests/e2e/builder-undo-redo.spec.ts
+++ b/tests/e2e/builder-undo-redo.spec.ts
@@ -31,6 +31,7 @@
import { test, expect, type Page } from '@playwright/test'
import { ensureApp as ensureAppFixture, dismissOverlays, suppressSupportDialog } from './support/appFixture'
+import { ensureVersionChain } from './support/versionChain'
// PLAYWRIGHT_BASE_URL wins — see tests/e2e/support/baseUrl.ts.
import { E2E_BASE_URL as BASE_URL } from './support/baseUrl'
@@ -244,18 +245,30 @@ test.describe('builder-undo-redo — page designer (REQ-BUR-001..004)', () => {
})
test('REQ-BUR-004: a version switch resets the session history', async ({ page }) => {
- // Requires a second seeded ApplicationVersion (e.g. "staging") for
- // pw-undo-redo, same precondition class as versionRouting.spec.ts's
- // 9.1/9.3 — skip gracefully rather than fail when it isn't seeded.
- const versionCheck = await page.request.get(
- `${BASE_URL}/index.php/apps/openbuild/api/applications/${APP_SLUG}/versions/staging`,
- ).catch(() => null)
- if (!versionCheck || versionCheck.status() !== 200) {
- test.skip(true, `SKIP: ApplicationVersion "staging" not seeded for ${APP_SLUG} — seed one to exercise this scenario`)
- return
- }
-
- await page.goto(`${BASE_URL}/apps/openbuild/builder/${APP_SLUG}/pages`, { waitUntil: 'domcontentloaded' })
+ // THIS TEST USED TO SKIP ITSELF, AND THE REASON WAS FALSE.
+ //
+ // It probed `GET .../versions/staging`, found nothing, and skipped with
+ // "ApplicationVersion 'staging' not seeded — seed one to exercise this
+ // scenario". Nothing in CI was ever going to seed it, so the skip was
+ // permanent: a guard on a precondition the suite could satisfy for
+ // itself, phrased as a fact about the environment.
+ //
+ // `tests/e2e/support/versionChain.ts::ensureVersionChain()` provisions
+ // development -> staging -> production on demand and is proven working
+ // in this same job — versionRouting.spec.ts drives `?_version=staging`
+ // through it and passes (9.1 / 9.2 / 9.3, run 31083894467). So the
+ // scenario was drivable all along; it simply never asked.
+ //
+ // A DEDICATED SLUG, not `pw-undo-redo`. The other tests in this describe
+ // open `/pages` with no `?_version=`, so their default-version
+ // resolution depends on how many versions the app has. Growing a chain
+ // on the shared slug would leave them running against a different app
+ // shape on every run AFTER the first — a fixture that changes what its
+ // neighbours test is worse than the skip it replaces.
+ const CHAIN_SLUG = 'pw-undo-redo-chain'
+ await ensureVersionChain(page, CHAIN_SLUG, 'PW Undo Redo Chain')
+
+ await page.goto(`${BASE_URL}/apps/openbuild/builder/${CHAIN_SLUG}/pages`, { waitUntil: 'domcontentloaded' })
await expect(page.locator('.page-designer__left')).toBeVisible({ timeout: 15_000 })
await page.locator('.page-list-editor__add').click()
@@ -264,7 +277,7 @@ test.describe('builder-undo-redo — page designer (REQ-BUR-001..004)', () => {
await expect(pageDesignerButton(page, 'Undo')).toBeEnabled()
await page.goto(
- `${BASE_URL}/apps/openbuild/builder/${APP_SLUG}/pages?_version=staging`,
+ `${BASE_URL}/apps/openbuild/builder/${CHAIN_SLUG}/pages?_version=staging`,
{ waitUntil: 'domcontentloaded' },
)
await expect(page.locator('.page-designer__left')).toBeVisible({ timeout: 15_000 })
diff --git a/tests/e2e/playwright.config.ts b/tests/e2e/playwright.config.ts
index c8e780bd..5e364ff9 100644
--- a/tests/e2e/playwright.config.ts
+++ b/tests/e2e/playwright.config.ts
@@ -159,7 +159,26 @@ export default defineConfig({
// HTML page loads — which breaks the browser-based login redirect (no
// Location header is emitted, the page stays on /login). Specs that
// need it set it on their explicit `request` calls.
- trace: 'on-first-retry',
+ // `on-first-retry` WROTE NOTHING IN THIS JOB, EVER.
+ //
+ // This config sets `retries: 0` (see above, deliberately). Playwright
+ // only records a trace on a RETRY under that mode, and a retry can
+ // never happen, so the trace file was never produced — while the
+ // workflow's trace-upload step dutifully ran, found nothing, and said
+ // so quietly under `if-no-files-found: ignore`. Two settings that are
+ // each individually defensible combined into an instrument that is
+ // switched off: every red run since this job existed had a screenshot
+ // and a video but no trace, which is the one artifact that carries the
+ // network log and the DOM at each step.
+ //
+ // `retain-on-failure` records every test and keeps the trace only for
+ // the ones that fail — no dependence on retries at all. The output
+ // lands under `outputDir` (APP_ROOT/test-results), which IS globbed by
+ // the shared workflow's upload step (it uploads both
+ // `server/apps//test-results/` and
+ // `server/apps//tests/e2e/test-results/`), so the traces actually
+ // leave the runner.
+ trace: 'retain-on-failure',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
headless: true,
diff --git a/tests/e2e/spec-coverage/application-detail-overview.spec.ts b/tests/e2e/spec-coverage/application-detail-overview.spec.ts
index 279917cb..7348dc03 100644
--- a/tests/e2e/spec-coverage/application-detail-overview.spec.ts
+++ b/tests/e2e/spec-coverage/application-detail-overview.spec.ts
@@ -73,28 +73,48 @@ test.skip('REQ-OBADO-001 — detail page renders the app icon from the Applicati
})
// @e2e application-detail-overview::register-widget-deep-links-to-openregister
-// QUARANTINED (Conduction/openbuild#41): openbuild admin UI not functional in this build — no application detail / icon / template-clone UI renders. Re-enable when #41 is fixed.
-test.skip('REQ-OBADO-006 — Register widget shows an "Open in OpenRegister" link on detail page', async ({ page }) => {
+// UN-QUARANTINED 2026-08-06. The #41 reason ("no application detail UI
+// renders") is stale: `src/components/applicationDetail/widgets/RegisterWidget.vue`
+// exists, renders exactly this affordance, and has been under active
+// development through 2026-07-29, while this file was last touched at the
+// original 2026-06-04 quarantine commit. The detail route itself is driven and
+// asserted by applicationDetailOverview.spec.ts, which passes in CI.
+test('REQ-OBADO-006 — Register widget shows an "Open in OpenRegister" link on detail page', async ({ page }) => {
// @e2e application-detail-overview::register-widget-deep-links-to-openregister
test.skip(!LIVE, 'Requires live dev env with the ApplicationDetailHeader cockpit built — set OPENBUILD_E2E_LIVE=1')
await gotoHelloWorldDetail(page)
await expect(page.locator('main')).toBeVisible({ timeout: 10_000 })
- // The Register widget should have an "Open in OpenRegister" link
- const openRegisterLink = page
- .locator('a, button')
- .filter({ hasText: /open.*openregister|openregister/i })
- .first()
-
+ // SCOPED TO THE REGISTER WIDGET, not to the page.
+ //
+ // The old locator was `page.locator('a, button').filter({ hasText:
+ // /open.*openregister|openregister/i }).first()` — every anchor and button
+ // on the page, narrowed by a substring the Nextcloud app menu's own
+ // "Register" entry and any breadcrumb could satisfy. A `.first()` over a
+ // page-wide set passes for whatever happens to render first, which means it
+ // would have kept passing with the Register widget deleted. Anchoring to
+ // `.ob-register-widget` makes the assertion be about the widget the
+ // requirement names.
+ const widget = page.locator('.ob-register-widget')
+ await expect(widget, 'the Register widget must render on the detail page').toBeVisible({ timeout: 15_000 })
+
+ const openRegister = widget.getByRole('button', { name: /open in openregister/i })
await expect(
- openRegisterLink,
- 'Register widget must have an "Open in OpenRegister" link',
+ openRegister,
+ 'Register widget must expose the "Open in OpenRegister" affordance',
).toBeVisible({ timeout: 10_000 })
- // The href should point to /apps/openregister/...
- const href = await openRegisterLink.getAttribute('href')
- if (href) {
- expect(href, 'link must point to the OpenRegister app').toMatch(/\/apps\/openregister/)
- }
+ // The affordance navigates by assigning `window.location.href` (see
+ // RegisterWidget.openInOpenRegister) rather than by rendering an anchor, so
+ // the target is asserted on the NAVIGATION REQUEST the click issues. An
+ // `getAttribute('href')` here would be null and — as the old body did —
+ // skipped by an `if (href)`, i.e. asserted nothing about where it goes.
+ const navigation = page.waitForRequest(
+ (req) => req.isNavigationRequest() && /\/apps\/openregister\//.test(req.url()),
+ { timeout: 10_000 },
+ )
+ await openRegister.click()
+ const target = (await navigation).url()
+ expect(target, 'the deep link must target the OpenRegister app').toMatch(/\/apps\/openregister\/registers\//)
})
diff --git a/tests/e2e/spec-coverage/page-designer-ui.spec.ts b/tests/e2e/spec-coverage/page-designer-ui.spec.ts
index 52be0452..f4696893 100644
--- a/tests/e2e/spec-coverage/page-designer-ui.spec.ts
+++ b/tests/e2e/spec-coverage/page-designer-ui.spec.ts
@@ -43,14 +43,16 @@ const PAGE_DESIGNER = (slug: string) => `${BASE}/apps/openbuild/builder/${slug}/
// REQ-OBPDUI-001 — Controlled designer orchestrates pages, menu, undo/redo
// ---------------------------------------------------------------------------
-// QUARANTINED (Conduction/openbuild#41): the openbuild builder/virtual-app surface is not
-// functional in this build — PageDesignerHost mounts but the virtual-app load returns 500
-// ("Failed to load the virtual app: Request failed with status code 500"), so the designer
-// panes never render. Same #41 family as openbuild-runtime.spec.ts / page-designer.spec.ts.
-// Re-enable when #41 is fixed (UI coverage is referenced by the page-designer-ui spec; the
-// host contract is also covered by Vitest unit tests in the meantime).
+// UN-QUARANTINED 2026-08-06. The recorded reason was that the virtual-app load
+// 500s so the designer panes never render. That is contradicted by this job's
+// own results: the NEXT test in this very file opens the same
+// `PAGE_DESIGNER('hello-world')` URL, waits for the same `.page-designer-host`
+// selector, and passes — as do every page-editor-coverage.spec.ts scenario and
+// the docudesk builder specs, all of which drive
+// `/apps/openbuild/builder//pages`. The 500 was fixed weeks ago; this
+// file was last touched 2026-06-06 and simply never rechecked.
// @e2e page-designer-ui::page-designer-renders-three-pane-layout
-test.skip('REQ-OBPDUI-001 — page designer route renders the three-pane layout', async ({ page }) => {
+test('REQ-OBPDUI-001 — page designer route renders the three-pane layout', async ({ page }) => {
// @e2e page-designer-ui::page-designer-renders-three-pane-layout
await page.goto(PAGE_DESIGNER('hello-world'))
await expect(page.locator('.page-designer-host'), 'designer must load').toBeVisible({ timeout: 15_000 })
@@ -76,19 +78,33 @@ test('REQ-OBPDUI-001 — undo button is disabled when no edits have been made',
// REQ-OBPDUI-002 — Route hosts resolve slug + version and persist manifest
// ---------------------------------------------------------------------------
-// QUARANTINED (Conduction/openbuild#41): builder/virtual-app surface non-functional in this
-// build — the host renders a "Failed to load the virtual app: Request failed with status
-// code 500" error and a "Version not found" empty-state, so the route cannot resolve a slug.
-// Re-enable when #41 is fixed.
+// UN-QUARANTINED 2026-08-06 — same stale reason as REQ-OBPDUI-001 above, same
+// contradicting evidence: the neighbouring `unknown ?_version` test drives this
+// exact route and passes.
// @e2e page-designer-ui::page-designer-route-renders-for-valid-slug
-test.skip('REQ-OBPDUI-002 — PageDesignerHost route renders for a known slug', async ({ page }) => {
+test('REQ-OBPDUI-002 — PageDesignerHost route renders for a known slug', async ({ page }) => {
// @e2e page-designer-ui::page-designer-route-renders-for-valid-slug
await page.goto(PAGE_DESIGNER('hello-world'))
await expect(page.locator('main'), 'main content must load').toBeVisible({ timeout: 15_000 })
- // The page must not be a 404 error page.
- const body = await page.textContent('body')
- expect(body).not.toMatch(/404|not found/i)
+ // The route RESOLVED THE SLUG — that is the requirement, and it is asserted
+ // positively rather than by scanning the whole page for the word "404".
+ //
+ // The old body did the latter, and it was an assertion that could not
+ // distinguish success from failure: `page.textContent('body')` on this route
+ // includes the Nextcloud chrome and every string the designer renders, so
+ // any legitimate "not found" copy — the version-not-found empty state the
+ // very next test asserts EXISTS, for one — would have failed it, while a
+ // blank designer with no error text would have passed it. Asserting the
+ // host actually mounted for this slug is both stricter and honest.
+ await expect(
+ page.locator('.page-designer-host'),
+ 'the designer host must mount for a known slug',
+ ).toBeVisible({ timeout: 15_000 })
+ await expect(
+ page.getByText(/version not found/i),
+ 'a known slug must NOT land on the version-not-found state',
+ ).toHaveCount(0)
})
// @e2e page-designer-ui::unknown-version-renders-not-found-state
diff --git a/tests/e2e/spec-coverage/page-editor-coverage.spec.ts b/tests/e2e/spec-coverage/page-editor-coverage.spec.ts
index 1e787372..04c36a24 100644
--- a/tests/e2e/spec-coverage/page-editor-coverage.spec.ts
+++ b/tests/e2e/spec-coverage/page-editor-coverage.spec.ts
@@ -410,22 +410,52 @@ test('REQ-PEC-006 — Create, configure, save and render a wiki page', async ({
// row happens to render first.
//
// Bind hello-world's OWN register and schema by slug, not `{ index: 1 }`.
- // The register select lists every register on the instance (175 of them
- // here) sorted by title, so index 1 resolved to Nextcloud's `directory`
+ // The register select lists every register on the instance (175 of them on a
+ // dev box) sorted by title, so index 1 resolved to Nextcloud's `directory`
// register, whose `nc-user` schema declares no properties this picker
// offers. The Content/Title field rows then rendered a `` holding
// nothing but their "— default: body —" placeholder, and `selectOrFill`'s
// `{ index: 1 }` fallback failed with "did not find some options" — a real
// dead end, not a race. The seeded `hello-message` schema declares
- // `id` + `body`, so the field mapping below has something to bind to.
+ // `title` + `body`, so the field mapping below has something to bind to.
+ //
+ // THE SLUGS WERE WRONG, AND WRONG IN A WAY ONLY CI COULD SHOW.
+ //
+ // This used to bind `openbuild-hello-world-production` /
+ // `hello-world-production-hello-message` and failed with `did not find some
+ // options` (run 31083894467) — the same symptom as the `{ index: 1 }` bug
+ // above, but a different cause: on CI THOSE SLUGS DO NOT EXIST. The seeded
+ // fixture is created by `occ openbuild:seed-hello-world-fixture`, not by the
+ // creation wizard, and it deliberately does not mint a per-version register:
+ // SeedHelloWorldFixture writes `register: 'openbuild-hello-world'` on the
+ // version as METADATA ONLY and puts the manifest, the `hello-message` schema
+ // and the three sample objects in the shared `openbuild` register — its own
+ // comment says so, and the hello-world manifest's index, detail and form
+ // pages all carry `config.register = 'openbuild'`. `ci-seed.sh` prints the
+ // instance's registers, and that list is `[…, 'openbuild', …]` with no
+ // `openbuild-hello-world-production` anywhere.
+ //
+ // Those names come from a WIZARD-created app (RegisterWidget builds exactly
+ // `openbuild-{slug}-{version}`), which a developer's long-lived instance
+ // accumulates and a fresh CI instance never has. So the test was pinned to
+ // a fixture shape that only existed on the machine it was written on.
+ //
+ // Bound to what the fixture actually provides. This is the same requirement
+ // — a wiki page bound to a real register + schema with its content and title
+ // fields mapped to real schema properties — asserted against the pair the
+ // seeded app genuinely uses, and it is now identical on CI and locally.
const registerSelect = editor.locator('.wiki-page-editor__group-row', { hasText: /^\s*Register\b/ }).locator('select')
- await registerSelect.selectOption('openbuild-hello-world-production')
+ await expect(
+ registerSelect.locator('option[value="openbuild"]'),
+ "the seeded app's register must be offered by the register picker",
+ ).toHaveCount(1, { timeout: 10_000 })
+ await registerSelect.selectOption('openbuild')
const schemaSelect = editor.locator('.wiki-page-editor__group-row', { hasText: 'Schema' }).locator('select').first()
await expect(
- schemaSelect.locator('option[value="hello-world-production-hello-message"]'),
+ schemaSelect.locator('option[value="hello-message"]'),
"the seeded app's register must offer its hello-message schema",
).toHaveCount(1, { timeout: 10_000 })
- await schemaSelect.selectOption('hello-world-production-hello-message')
+ await schemaSelect.selectOption('hello-message')
// contentField/titleField render as a schema-property once a
// register + schema are bound (task 5.1); fall back to free-text input
@@ -449,10 +479,10 @@ test('REQ-PEC-006 — Create, configure, save and render a wiki page', async ({
// merely "not empty" — that is what a lossless round-trip means.
await expect(
reopened.locator('.wiki-page-editor__group-row', { hasText: /^\s*Register\b/ }).locator('select'),
- ).toHaveValue('openbuild-hello-world-production')
+ ).toHaveValue('openbuild')
await expect(
reopened.locator('.wiki-page-editor__group-row', { hasText: /^\s*Schema\b/ }).locator('select'),
- ).toHaveValue('hello-world-production-hello-message')
+ ).toHaveValue('hello-message')
await expect(
reopened.locator('.wiki-page-editor__group-row', { hasText: /^\s*Content field\b/ }).locator('select'),
).toHaveValue('body')