From d5c9abb40e8bd22a217694a8b4d34817166fe246 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Tue, 1 Sep 2026 10:50:28 +0200 Subject: [PATCH] feat(router): move pipelinq off hash routing to clean path URLs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sixth app off `#` routing, after stackiq (softwarecatalog#899), zaakafhandelapp (#609), opencatalogi (#1341), keepiq (#582) and larpinq (#651). Two source parts: 1. createWebHashHistory -> createWebHistory. 2. routerBase(), derived from the URL actually being served. Nextcloud serves the app under BOTH /apps/pipelinq/... and /index.php/apps/pipelinq/..., but generateUrl() returns only the form the instance is configured for. Arriving on the other leaves the path outside the router base, vue-router cannot resolve it, and the catch-all redirects to '/' -- no error, deep link silently swallowed. This suite pins the /index.php form in several specs, so without it every deep link the suite makes would break. The test surface was the bulk of the work, and it was not only URLs: 30 spec files `${APP}/#/x`, `/apps/pipelinq/#/x` -> real paths navigation.spec.ts REQUIRED_ENTRIES mapped page ids to HASH hrefs and asserted equality; now route paths compared as a SUFFIX, because the base legitimately differs between the two URL forms and pinning either one would fail on the other for a non-defect dashboard.spec.ts default-route URL regex, and the KPI tile selector `a[href^="#/"]` + its `/^#\\//` assertion xwiki + declarative-view-system 9 toHaveURL(/#\\/…/) assertions helpers/pipelinq.ts revealNavEntry() selected leaves by `href*="#/"`, which matches NOTHING once the shell leaves hash routing. Leaves are now selected by the app segment; group captions keep their bare `href="#"`, so that remains the discriminator between the two. Stale prose corrected rather than deleted in helpers/pipelinq.ts, navigation.spec.ts and the event-wiring spec: all three told the reader a path deep-link resets the SPA to the Dashboard, which was true and is now the opposite. Verified against the published @conduction/nextcloud-vue (USE_LOCAL_LIB=false): /apps/pipelinq/ -> Sales overview /apps/pipelinq/leads -> Leads /index.php/apps/pipelinq/leads -> Leads /index.php/apps/pipelinq/clients -> Clients No hash anywhere, and nav hrefs are real paths carrying whichever base the page was loaded under -- which is exactly why the suffix assertion replaced the equality one. e2e: navigation.spec.ts + dashboard.spec.ts 9/9, event-wiring.spec.ts 3/3. eslint exits 0 and prettier --check on the FULL glob is clean. --- src/main.js | 27 ++++++- tests/e2e/dashboard.spec.ts | 18 +++-- tests/e2e/event-wiring.spec.ts | 7 +- tests/e2e/global-setup.ts | 2 +- tests/e2e/helpers/pipelinq.ts | 23 ++++-- tests/e2e/navigation.spec.ts | 74 ++++++++++++------- tests/e2e/rapportage.spec.ts | 6 +- .../align-claims-first-hour.spec.ts | 4 +- .../spec-coverage/appointment-booking.spec.ts | 2 +- .../spec-coverage/bi-export-jobs-bug.spec.ts | 2 +- .../spec-coverage/billing-categories.spec.ts | 4 +- .../commercial-dashboard.spec.ts | 2 +- ...ntactmoment-client-contact-cascade.spec.ts | 4 +- .../spec-coverage/dashboard-analytics.spec.ts | 2 +- tests/e2e/spec-coverage/dashboard.spec.ts | 6 +- .../declarative-view-system.spec.ts | 18 ++--- .../spec-coverage/expense-shillinq-ap.spec.ts | 2 +- .../ia-tickets-and-projects.spec.ts | 4 +- tests/e2e/spec-coverage/kennisbank.spec.ts | 4 +- .../lead-requires-pipeline-and-client.spec.ts | 2 +- tests/e2e/spec-coverage/marketing.spec.ts | 2 +- .../spec-coverage/outbound-messaging.spec.ts | 28 +++---- tests/e2e/spec-coverage/pipeline.spec.ts | 10 +-- .../pipelinq-pos-grouping.spec.ts | 2 +- .../pos-transaction-core.spec.ts | 4 +- .../prospect-add-as-client.spec.ts | 2 +- .../request-client-contact-cascade.spec.ts | 2 +- .../spec-coverage/request-management.spec.ts | 4 +- .../semantic-handoff-emit.spec.ts | 12 +-- .../sla-engine-and-escalation.spec.ts | 2 +- .../visual-coverage-export-pages.spec.ts | 2 +- .../visual-coverage-spa-pages.spec.ts | 2 +- tests/e2e/visual/pipelinq.visual.spec.ts | 4 +- tests/e2e/xwiki-integration.spec.ts | 6 +- 34 files changed, 178 insertions(+), 117 deletions(-) diff --git a/src/main.js b/src/main.js index 4184f7c40..b26f74468 100644 --- a/src/main.js +++ b/src/main.js @@ -32,7 +32,7 @@ import { import { generateUrl } from '@nextcloud/router' import { setActivePinia } from 'pinia' import { createApp, h, markRaw } from 'vue' -import { createRouter, createWebHashHistory } from 'vue-router' +import { createRouter, createWebHistory } from 'vue-router' import App from './App.vue' import appIcons from './icons.js' import bundledManifest from './manifest.json' @@ -263,6 +263,29 @@ const registryProp = { ...registry } setActivePinia(pinia) registerObjectTypes() +/** + * The router base for THIS page load. + * + * ⚠️ `generateUrl('/apps/pipelinq')` alone is not enough. Nextcloud serves the + * app under BOTH `/apps/pipelinq/...` and `/index.php/apps/pipelinq/...`, but + * `generateUrl()` returns only the form the instance is configured for. A + * visitor arriving on the other form — a bookmark, an emailed deep link, an + * integration that hardcodes `/index.php` — falls outside the router base, + * vue-router cannot resolve the path, and the catch-all redirects to `/`. They + * land on the dashboard with no error: the deep link is silently swallowed. + * + * Hash routing never had this, because the route travelled in the fragment and + * the path prefix was irrelevant. This app's e2e suite pins the `/index.php` + * form (`const APP = '/index.php/apps/pipelinq'` in several specs), so without + * this every deep link the suite makes would break. + * + * @return {string} The base path vue-router should strip from the URL. + */ +function routerBase() { + const match = window.location.pathname.match(/^(.*\/apps\/pipelinq)(?:\/|$)/) + return match ? match[1] : generateUrl('/apps/pipelinq') +} + /** * Mount the Vue instance onto #content. The router is built here — not at * module scope — because persisted overrides can add or remove pages, and @@ -273,7 +296,7 @@ registerObjectTypes() function mountApp(manifest) { const router = createRouter({ // vue-router 4 replaces `mode: 'hash'` + `base` with a history object. - history: createWebHashHistory(generateUrl('/apps/pipelinq')), + history: createWebHistory(routerBase()), routes: routesFromManifest(manifest), }) // Vue 3: `createApp(...).mount()` replaces `new Vue(...).$mount()`, and diff --git a/tests/e2e/dashboard.spec.ts b/tests/e2e/dashboard.spec.ts index 070db1c54..563c1ca90 100644 --- a/tests/e2e/dashboard.spec.ts +++ b/tests/e2e/dashboard.spec.ts @@ -64,7 +64,10 @@ test.describe('Sales dashboard', () => { }) test('renders the dashboard page for the default route', async ({ page }) => { - await expect(page).toHaveURL(/#\/$|#\/$|\/apps\/pipelinq\/(#\/)?$/) + // History routing: the default route is the app root as a plain path. + // Anchored at the end so it holds under both the `/apps/...` and + // `/index.php/apps/...` bases Nextcloud serves. + await expect(page).toHaveURL(/\/apps\/pipelinq\/$/) await expect( page.locator('[data-testid="cn-page"]'), 'the default route must render a page, not an empty shell', @@ -107,16 +110,21 @@ test.describe('Sales dashboard', () => { }) test('each KPI widget links into a filtered view', async ({ page }) => { - // The KPI tiles are anchors into hash routes. Asserting that each one - // HAS a hash href catches the regression that matters (a tile that + // The KPI tiles are anchors into in-app routes. Asserting that each one + // HAS an in-app href catches the regression that matters (a tile that // stopped linking) without pinning the exact destination of every tile, // which is manifest configuration and moves with the product. + // + // The shell routes on HISTORY now, so these are paths rather than `#/` + // fragments, and the base differs between the `/apps/...` and + // `/index.php/apps/...` forms Nextcloud serves — hence a contains-match + // on the app segment rather than a prefix-match on the whole href. const linked = page.locator( - '[role="group"][aria-label="pipeline-coverage"] a[href^="#/"]', + '[role="group"][aria-label="pipeline-coverage"] a[href*="/apps/pipelinq/"]', ) await expect( linked.first(), 'the pipeline-coverage tile must link into a filtered view', - ).toHaveAttribute('href', /^#\//) + ).toHaveAttribute('href', /\/apps\/pipelinq\/.+/) }) }) diff --git a/tests/e2e/event-wiring.spec.ts b/tests/e2e/event-wiring.spec.ts index 84e7d58d4..86169aab3 100644 --- a/tests/e2e/event-wiring.spec.ts +++ b/tests/e2e/event-wiring.spec.ts @@ -85,9 +85,10 @@ async function openProjectDetail( // openApp() already ran in beforeEach — calling it again here doubled the // shell boot and pushed each test past its budget. // - // The shell routes on the hash; a path deep-link resets it to the Dashboard - // (see helpers/pipelinq.ts). - await page.goto(`/apps/pipelinq/#/projects/${PROJECT_ID}`) + // The shell routes on HISTORY, so this path deep-link lands on the project + // detail directly. It used to reset the SPA to the Dashboard under hash + // routing (see helpers/pipelinq.ts). + await page.goto(`/apps/pipelinq/projects/${PROJECT_ID}`) await expect(page.locator('.wbs-tree, [class*=wbs]').first()).toBeVisible({ timeout: 30000, }) diff --git a/tests/e2e/global-setup.ts b/tests/e2e/global-setup.ts index 459fef205..08dea3d9b 100644 --- a/tests/e2e/global-setup.ts +++ b/tests/e2e/global-setup.ts @@ -89,7 +89,7 @@ async function assertAppBoots(page: import('@playwright/test').Page): Promise')` - * resets the SPA back to the Dashboard, so navigation between pages MUST go - * through a sidebar nav-click. Two app-chrome overlays can cover the content + * Pipelinq mounts at #content-vue. The shell routes on HISTORY, so + * `page.goto('/apps/pipelinq/')` is a valid deep link and lands on that + * route — it used to reset the SPA to the Dashboard under hash routing, which + * is why older specs navigate by sidebar click instead. Both work now. Two app-chrome overlays can cover the content * and intercept clicks — the fleet-wide `cn-support-dialog` and the first-visit * `cn-walkthrough` product tour. Both are dismissed before interacting; see * dismissSupportDialog() and dismissWalkthrough(). @@ -190,12 +191,20 @@ export async function revealNavEntryByTestId( * against correct navigation. */ export async function revealNavEntry(page: Page, label: string): Promise { - // Restrict to the leaf ENTRY anchor (href contains `#/`), NOT the nav - // GROUP CAPTION. getByRole('link', { name }) would otherwise match the - // caption first and the click is a no-op (router stays on `#/`). + // Restrict to the leaf ENTRY anchor, NOT the nav GROUP CAPTION. + // getByRole('link', { name }) would otherwise match the caption first and + // the click is a no-op (the router stays put). + // + // The discriminator is the href: under history routing a leaf entry points + // at `/apps/pipelinq/`, while a collapsible group caption still + // renders a bare `href="#"` — so matching the app segment selects exactly + // the leaves. (This used to filter on `href*="#/"`, which stopped matching + // anything the moment the shell left hash routing.) // Filter by exact visible text so the entry — not the caption — is targeted. const link = page - .locator('#app-navigation-vue a.app-navigation-entry-link[href*="#/"]') + .locator( + '#app-navigation-vue a.app-navigation-entry-link[href*="/apps/pipelinq/"]', + ) .filter({ hasText: new RegExp( `^\\s*${label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*$`, diff --git a/tests/e2e/navigation.spec.ts b/tests/e2e/navigation.spec.ts index 4fae322cc..504b9b423 100644 --- a/tests/e2e/navigation.spec.ts +++ b/tests/e2e/navigation.spec.ts @@ -16,9 +16,12 @@ * the navigation, so every assertion here goes through the manifest page * id instead — `data-testid="cn-nav-entry-"`, untranslated by * construction. - * 2. PATH ROUTES — `href="/apps/pipelinq/clients"`. The shell routes on the - * HASH (`#/clients`), and helpers/pipelinq.ts documents why a path - * deep-link is worse than wrong: it resets the SPA to the Dashboard. + * 2. PATH ROUTES — `href="/apps/pipelinq/clients"`. That was WRONG when this + * file was rewritten, because the shell routed on the hash (`#/clients`) + * and a path deep-link reset the SPA to the Dashboard. It is RIGHT again + * as of the move to history routing — but the assertion now compares the + * route as a SUFFIX, because the base differs between the `/apps/...` and + * `/index.php/apps/...` forms and both are legitimate. * 3. FLAT VISIBILITY. Since the 2026-07 IA revision most leaves sit inside a * collapsed group. Measured on this build: 37 entries in the DOM, 11 * visible at load. `toBeVisible()` on a nested leaf asserts the old flat @@ -36,26 +39,33 @@ import { test, expect } from '@playwright/test' import { openApp, revealNavEntryByTestId } from './helpers/pipelinq' /** - * Manifest page id → hash route. Verified live against the running app on - * 2026-08-24 by reading every `[data-testid^="cn-nav-entry-"]` and its href. + * Manifest page id → route PATH. Verified live against the running app by + * reading every `[data-testid^="cn-nav-entry-"]` and its href. + * + * These are compared as a SUFFIX of the href, not as the whole value, because + * the shell now routes on history and the router base legitimately differs by + * URL form: Nextcloud serves the app as both `/apps/pipelinq/...` and + * `/index.php/apps/pipelinq/...`, and vue-router emits whichever base the page + * was loaded under. Asserting the full href would pin the test to one of the + * two and fail on the other for a reason that is not a routing defect. */ const REQUIRED_ENTRIES: Record = { - Dashboard: '#/', - Clients: '#/clients', - Contacts: '#/contacts', - Leads: '#/leads', - Tickets: '#/tickets', - Tasks: '#/tasks', - Products: '#/products', - Pipeline: '#/pipeline', - Queues: '#/queues', - Contracts: '#/contracts', - MyWork: '#/my-work', - Prospects: '#/prospects', - Forecast: '#/forecast', - Services: '#/services', - Resources: '#/resources', - Bookings: '#/bookings', + Dashboard: '/', + Clients: '/clients', + Contacts: '/contacts', + Leads: '/leads', + Tickets: '/tickets', + Tasks: '/tasks', + Products: '/products', + Pipeline: '/pipeline', + Queues: '/queues', + Contracts: '/contracts', + MyWork: '/my-work', + Prospects: '/prospects', + Forecast: '/forecast', + Services: '/services', + Resources: '/resources', + Bookings: '/bookings', } /** @@ -97,9 +107,9 @@ test.describe('Sidebar navigation (manifest-driven shell)', () => { ) }) - test('each sidebar entry points at its hash route', async ({ page }) => { + test('each sidebar entry points at its route', async ({ page }) => { const wrong: string[] = [] - for (const [pageId, href] of Object.entries(REQUIRED_ENTRIES)) { + for (const [pageId, route] of Object.entries(REQUIRED_ENTRIES)) { const link = page .locator( `#app-navigation-vue [data-testid="cn-nav-entry-${pageId}"]`, @@ -107,12 +117,17 @@ test.describe('Sidebar navigation (manifest-driven shell)', () => { .locator('xpath=descendant-or-self::a[1]') .first() const actual = await link.getAttribute('href').catch(() => null) - if (actual !== href) - wrong.push(`${pageId}: expected ${href}, got ${actual}`) + // Suffix, not equality — see the note on REQUIRED_ENTRIES: the base + // differs between the `/apps/...` and `/index.php/apps/...` forms and + // both are correct. + if (actual === null || actual.endsWith(route) === false) + wrong.push( + `${pageId}: expected an href ending ${route}, got ${actual}`, + ) } expect( wrong, - 'the shell routes on the hash; a path href resets the SPA to the Dashboard', + 'the shell routes on history; every nav href must end with its manifest route', ).toEqual([]) }) @@ -140,7 +155,12 @@ test.describe('Sidebar navigation (manifest-driven shell)', () => { const link = await revealNavEntryByTestId(page, 'Clients') await expect(link).toBeVisible({ timeout: 10000 }) await link.click() - await expect(page).toHaveURL(/#\/clients/, { timeout: 10000 }) + // History routing: the URL is a real path. Matched at the END so the + // assertion holds under both the `/apps/...` and `/index.php/apps/...` + // bases Nextcloud serves. + await expect(page).toHaveURL(/\/apps\/pipelinq\/clients$/, { + timeout: 10000, + }) await expect( page.locator('[data-testid="cn-index-page"]'), 'the Clients route must render its index page, not an empty shell', diff --git a/tests/e2e/rapportage.spec.ts b/tests/e2e/rapportage.spec.ts index 2c17bbdb5..842581bef 100644 --- a/tests/e2e/rapportage.spec.ts +++ b/tests/e2e/rapportage.spec.ts @@ -15,7 +15,7 @@ test.describe('Rapportage (Reporting)', () => { // route directly via the SPA hash. A path-form goto boots the shell at the // Dashboard; a hash goto mounts the target view. Reload once so the view // re-queries its KPI data after the same-document hash change. - await page.goto('/apps/pipelinq/#/rapportage/contactmomenten') + await page.goto('/apps/pipelinq/rapportage/contactmomenten') await expect(page.locator('body')).not.toContainText('Internal Server Error') await page.reload() }) @@ -107,7 +107,7 @@ test.describe('Rapportage (Reporting)', () => { test('channel analytics page loads', async ({ page }) => { // Deep-link via the SPA hash; a path-form goto boots the shell at the // Dashboard instead of the target view. - await page.goto('/apps/pipelinq/#/rapportage/channels') + await page.goto('/apps/pipelinq/rapportage/channels') await page.reload() await expect( page.getByRole('heading', { name: /Channel Analytics|Kanaalanalyse/i }), @@ -115,7 +115,7 @@ test.describe('Rapportage (Reporting)', () => { }) test('agent performance page loads', async ({ page }) => { - await page.goto('/apps/pipelinq/#/rapportage/agents') + await page.goto('/apps/pipelinq/rapportage/agents') await page.reload() await expect( page.getByRole('heading', { diff --git a/tests/e2e/spec-coverage/align-claims-first-hour.spec.ts b/tests/e2e/spec-coverage/align-claims-first-hour.spec.ts index 1ea003b8d..bc218ed65 100644 --- a/tests/e2e/spec-coverage/align-claims-first-hour.spec.ts +++ b/tests/e2e/spec-coverage/align-claims-first-hour.spec.ts @@ -41,7 +41,7 @@ test.describe('Operational dashboard — no permanently-null widgets', () => { test.beforeEach(async ({ page }) => { // The shared dev instance can be slow to fire `load`; DOMContentLoaded // is enough — the assertions below wait for the widgets themselves. - await page.goto('/apps/pipelinq/#/operational', { + await page.goto('/apps/pipelinq/operational', { waitUntil: 'domcontentloaded', }) await expect(page.locator('body')).not.toContainText('Internal Server Error') @@ -130,7 +130,7 @@ test.describe('Demo-data seed setup action', () => { test('seeded demo clients render in the Clients list', async ({ page }) => { test.setTimeout(90000) await autoDismissWalkthrough(page) - await page.goto('/apps/pipelinq/#/clients') + await page.goto('/apps/pipelinq/clients') await page.reload() // Wait for the table to load rows. diff --git a/tests/e2e/spec-coverage/appointment-booking.spec.ts b/tests/e2e/spec-coverage/appointment-booking.spec.ts index 219b0b27c..464222c5e 100644 --- a/tests/e2e/spec-coverage/appointment-booking.spec.ts +++ b/tests/e2e/spec-coverage/appointment-booking.spec.ts @@ -57,7 +57,7 @@ function hoursFromNow(hours: number): string { * spec-coverage/outbound-messaging.spec.ts). */ async function gotoHash(page: Page, hash: string): Promise { - await page.goto(`/apps/pipelinq/#${hash}`) + await page.goto(`/apps/pipelinq/${hash}`) await expect(page.locator('#content-vue')).toBeVisible({ timeout: 15000 }) await dismissWalkthrough(page) await dismissSupportDialog(page) diff --git a/tests/e2e/spec-coverage/bi-export-jobs-bug.spec.ts b/tests/e2e/spec-coverage/bi-export-jobs-bug.spec.ts index d61669a10..89e05b340 100644 --- a/tests/e2e/spec-coverage/bi-export-jobs-bug.spec.ts +++ b/tests/e2e/spec-coverage/bi-export-jobs-bug.spec.ts @@ -22,7 +22,7 @@ import { openApp, assertNoHardError, trackPipelinqErrors } from '../helpers/pipe // reload so the index re-queries its data (matches bi-export.spec.ts). async function gotoExportJobs(page: Page) { await openApp(page) - await page.goto('/apps/pipelinq/#/export/jobs') + await page.goto('/apps/pipelinq/export/jobs') await page.reload() await page .locator('#content-vue') diff --git a/tests/e2e/spec-coverage/billing-categories.spec.ts b/tests/e2e/spec-coverage/billing-categories.spec.ts index 71933ee65..39f22bc9b 100644 --- a/tests/e2e/spec-coverage/billing-categories.spec.ts +++ b/tests/e2e/spec-coverage/billing-categories.spec.ts @@ -27,7 +27,7 @@ test('Billing categories: the widget renders on the Operational dashboard', asyn page, }) => { await openApp(page) - await page.goto('/apps/pipelinq/#/operational') + await page.goto('/apps/pipelinq/operational') await page.reload() const content = page.locator('#content-vue') @@ -63,7 +63,7 @@ test('Billing categories: the widget sits alongside the other operational widget page, }) => { await openApp(page) - await page.goto('/apps/pipelinq/#/operational') + await page.goto('/apps/pipelinq/operational') await page.reload() const content = page.locator('#content-vue') diff --git a/tests/e2e/spec-coverage/commercial-dashboard.spec.ts b/tests/e2e/spec-coverage/commercial-dashboard.spec.ts index 4ec42dcd7..3a5e1df1e 100644 --- a/tests/e2e/spec-coverage/commercial-dashboard.spec.ts +++ b/tests/e2e/spec-coverage/commercial-dashboard.spec.ts @@ -54,7 +54,7 @@ test('Operational dashboard: previous widgets remain reachable from the nav', as // Deep-link the OperationalDashboard via the SPA hash (`/operational`); a // path-form goto boots the shell at the default Commercial dashboard. - await page.goto('/apps/pipelinq/#/operational') + await page.goto('/apps/pipelinq/operational') await page.reload() const content = page.locator('#content-vue') diff --git a/tests/e2e/spec-coverage/contactmoment-client-contact-cascade.spec.ts b/tests/e2e/spec-coverage/contactmoment-client-contact-cascade.spec.ts index ad31aca86..ca33204c7 100644 --- a/tests/e2e/spec-coverage/contactmoment-client-contact-cascade.spec.ts +++ b/tests/e2e/spec-coverage/contactmoment-client-contact-cascade.spec.ts @@ -28,7 +28,7 @@ test.beforeEach(() => { /** Open the first client's detail page and return the quick-log form. */ async function openQuickLog(page: Page) { - await page.goto('/apps/pipelinq/#/clients') + await page.goto('/apps/pipelinq/clients') await dismissWalkthrough(page) await dismissSupportDialog(page) @@ -47,7 +47,7 @@ async function openQuickLog(page: Page) { }) const id = await row.getAttribute('data-testid-row-id') expect(id, 'the client row must carry its id').toBeTruthy() - await page.goto(`/apps/pipelinq/#/clients/${id}`) + await page.goto(`/apps/pipelinq/clients/${id}`) const client = page.locator('[data-testid="contactmoment-form-client"]') await expect( diff --git a/tests/e2e/spec-coverage/dashboard-analytics.spec.ts b/tests/e2e/spec-coverage/dashboard-analytics.spec.ts index 0dcfbba87..b89c39b26 100644 --- a/tests/e2e/spec-coverage/dashboard-analytics.spec.ts +++ b/tests/e2e/spec-coverage/dashboard-analytics.spec.ts @@ -26,7 +26,7 @@ import { openApp, trackPipelinqErrors, assertNoHardError } from '../helpers/pipe * surface as spurious "Failed to fetch" console noise. */ async function openOperational(page) { - await page.goto('/apps/pipelinq/#/operational') + await page.goto('/apps/pipelinq/operational') await expect(page.locator('#app-navigation-vue')).toBeVisible({ timeout: 15000 }) await page.reload() await page diff --git a/tests/e2e/spec-coverage/dashboard.spec.ts b/tests/e2e/spec-coverage/dashboard.spec.ts index 97e298dd4..44da3bc2e 100644 --- a/tests/e2e/spec-coverage/dashboard.spec.ts +++ b/tests/e2e/spec-coverage/dashboard.spec.ts @@ -31,7 +31,7 @@ import { * the Commercial landing widgets never mount. */ async function gotoOperational(page) { - await page.goto('/apps/pipelinq/#/operational') + await page.goto('/apps/pipelinq/operational') await expect(page.locator('#app-navigation-vue')).toBeVisible({ timeout: 15000 }) await page.reload() await page @@ -126,7 +126,7 @@ function layoutSlotFor(widgetId: string): string { * `…/#/operational`, so that single navigation is the whole fixture. */ async function openOperationalInteractive(page: Page): Promise { - await page.goto('/apps/pipelinq/#/operational') + await page.goto('/apps/pipelinq/operational') await expect(page.locator('#content-vue')).toBeVisible({ timeout: 20000 }) await dismissWalkthrough(page) await dismissSupportDialog(page) @@ -189,7 +189,7 @@ test('dashboard quick action buttons visible', async ({ page }) => { // @e2e openspec/specs/dashboard/spec.md#quick-action-buttons-in-header test('the request quick action lives on Customer Support', async ({ page }) => { - await page.goto('/apps/pipelinq/#/werkplek') + await page.goto('/apps/pipelinq/werkplek') await expect( page.getByRole('button', { name: /New Request/i }).first(), 'Customer Support must offer New Request', diff --git a/tests/e2e/spec-coverage/declarative-view-system.spec.ts b/tests/e2e/spec-coverage/declarative-view-system.spec.ts index 789182383..ac7af5069 100644 --- a/tests/e2e/spec-coverage/declarative-view-system.spec.ts +++ b/tests/e2e/spec-coverage/declarative-view-system.spec.ts @@ -89,7 +89,7 @@ async function gotoPage(page: Page, hash: string): Promise { // hash change is same-document and does not remount the view. On the first // navigation of a test the goto IS a full document load and already mounts // the target route. - const target = `/apps/pipelinq/#${hash}` + const target = `/apps/pipelinq/${hash}` const alreadyMounted = page.url().includes('/apps/pipelinq') await page.goto(target) if (alreadyMounted) { @@ -351,7 +351,7 @@ test('ZReports renders from a type:"index" manifest page, badge column and all', // The one declared row action (`handler: "navigate"`, route ZReportDetail). await clickFirstRowAction(page, 'openen') - await expect(page).toHaveURL(/#\/pos\/z-reports\/[^/]+$/, { timeout: 15000 }) + await expect(page).toHaveURL(/\/pos\/z-reports\/[^/]+$/, { timeout: 15000 }) }) // @e2e openspec/specs/declarative-view-system/spec.md#bookings-renders-as-a-view-only-declarative-index @@ -388,7 +388,7 @@ test('Bookings renders as a VIEW-ONLY declarative index — no create control', // The single declared row action navigates to the bespoke BookingDetail. await clickFirstRowAction(page, 'open') - await expect(page).toHaveURL(/#\/bookings\/[^/]+$/, { timeout: 15000 }) + await expect(page).toHaveURL(/\/bookings\/[^/]+$/, { timeout: 15000 }) }) // --------------------------------------------------------------------------- @@ -418,7 +418,7 @@ test('Services renders declaratively with currency + duration columns and a crea // `params: { id: "new" }`, so it lives in the CnActionsBar overflow rather // than as a visible CTA (`showAdd: false`) — see clickHeaderAction(). await clickHeaderAction(page, 'New service') - await expect(page).toHaveURL(/#\/services\/new$/, { timeout: 15000 }) + await expect(page).toHaveURL(/\/services\/new$/, { timeout: 15000 }) await expect( page .locator('#content-vue') @@ -446,9 +446,9 @@ test("Services: a row's open action navigates to that service's detail route", a }) await clickFirstRowAction(page, 'view') - await expect(page).toHaveURL(/#\/services\/[^/]+$/, { timeout: 15000 }) + await expect(page).toHaveURL(/\/services\/[^/]+$/, { timeout: 15000 }) // Not the create form — a real object id. - await expect(page).not.toHaveURL(/#\/services\/new$/) + await expect(page).not.toHaveURL(/\/services\/new$/) }) // @e2e openspec/specs/declarative-view-system/spec.md#resources-new-action-opens-the-create-form @@ -469,7 +469,7 @@ test('Resources renders declaratively and its "New resource" action opens the cr ).toBe(false) await clickHeaderAction(page, 'New resource') - await expect(page).toHaveURL(/#\/resources\/new$/, { timeout: 15000 }) + await expect(page).toHaveURL(/\/resources\/new$/, { timeout: 15000 }) await expect( page .locator('#content-vue') @@ -507,7 +507,7 @@ test('Projects renders declaratively with a currency budget column and a billabl ).toBe(false) await clickHeaderAction(page, 'Nieuw project') - await expect(page).toHaveURL(/#\/projects\/new$/, { timeout: 15000 }) + await expect(page).toHaveURL(/\/projects\/new$/, { timeout: 15000 }) }) // --------------------------------------------------------------------------- @@ -777,7 +777,7 @@ test.describe('Declarative detail pages (client 360 + contact)', () => { await row.click() // `rowRoute: "LeadDetail"` on the Leads related collection. - await expect(page).toHaveURL(new RegExp(`#/leads/${leadId}$`), { + await expect(page).toHaveURL(new RegExp(`/leads/${leadId}$`), { timeout: 15000, }) }) diff --git a/tests/e2e/spec-coverage/expense-shillinq-ap.spec.ts b/tests/e2e/spec-coverage/expense-shillinq-ap.spec.ts index af3cccde7..1efebaee0 100644 --- a/tests/e2e/spec-coverage/expense-shillinq-ap.spec.ts +++ b/tests/e2e/spec-coverage/expense-shillinq-ap.spec.ts @@ -9,7 +9,7 @@ * - Admin settings page renders the Shillinq integration section and the * `shillinq_ap_webhook_url` text field (REQ-AP-004 / Scenario 12). * - The expense list page reaches its empty/loaded state through the - * `/apps/pipelinq/#/expenses` route (REQ-AP-005 — column header is + * `/apps/pipelinq/expenses` route (REQ-AP-005 — column header is * only visible once an expense exists; here we assert the surface * mounts without an internal-server error and the New expense CTA * is reachable from the empty state). diff --git a/tests/e2e/spec-coverage/ia-tickets-and-projects.spec.ts b/tests/e2e/spec-coverage/ia-tickets-and-projects.spec.ts index bcac9a62e..ad1f49b1d 100644 --- a/tests/e2e/spec-coverage/ia-tickets-and-projects.spec.ts +++ b/tests/e2e/spec-coverage/ia-tickets-and-projects.spec.ts @@ -22,7 +22,7 @@ test.beforeEach(() => { /** Open the app and settle the first-run dialogs. */ async function openApp(page: Page) { - await page.goto('/apps/pipelinq/#/') + await page.goto('/apps/pipelinq/') await dismissWalkthrough(page) await dismissSupportDialog(page) await expect( @@ -68,7 +68,7 @@ test('Projecten is no longer offered in the navigation', async ({ page }) => { // @e2e openspec/specs/pipelinq-navigation/spec.md test('the projects page stays reachable by direct link', async ({ page }) => { - await page.goto('/apps/pipelinq/#/projects') + await page.goto('/apps/pipelinq/projects') await dismissWalkthrough(page) await dismissSupportDialog(page) diff --git a/tests/e2e/spec-coverage/kennisbank.spec.ts b/tests/e2e/spec-coverage/kennisbank.spec.ts index d6ce912b9..f7e6256a1 100644 --- a/tests/e2e/spec-coverage/kennisbank.spec.ts +++ b/tests/e2e/spec-coverage/kennisbank.spec.ts @@ -90,7 +90,7 @@ test('the knowledge-base surface is reachable: the widget mounts and the proxy a // The knowledge-base widget lives on the Operational overview dashboard // (src/manifest.json, page `OperationalDashboard`, widget `xwiki-knowledge` // at layout slot 13), NOT on the landing Commercial overview. - await page.goto('/apps/pipelinq/#/operational') + await page.goto('/apps/pipelinq/operational') await expect(page.locator('#content-vue')).toBeVisible({ timeout: 15000 }) await dismissWalkthrough(page) await dismissSupportDialog(page) @@ -243,7 +243,7 @@ test('knowledge-base operations tolerate absent or unresolvable input', async ({ // The surrounding flow survives it: the dashboard that hosts the widget // still renders, with no server error or uncaught render failure. - await page.goto('/apps/pipelinq/#/operational') + await page.goto('/apps/pipelinq/operational') await expect(page.locator('#content-vue')).toBeVisible({ timeout: 15000 }) await assertNoHardError(page) }) diff --git a/tests/e2e/spec-coverage/lead-requires-pipeline-and-client.spec.ts b/tests/e2e/spec-coverage/lead-requires-pipeline-and-client.spec.ts index fbe004ca6..57c26de2c 100644 --- a/tests/e2e/spec-coverage/lead-requires-pipeline-and-client.spec.ts +++ b/tests/e2e/spec-coverage/lead-requires-pipeline-and-client.spec.ts @@ -23,7 +23,7 @@ test.beforeEach(() => { /** Open the New Lead dialog from the sales dashboard header action. */ async function openNewLeadDialog(page: Page) { - await page.goto('/apps/pipelinq/#/') + await page.goto('/apps/pipelinq/') await dismissWalkthrough(page) await dismissSupportDialog(page) diff --git a/tests/e2e/spec-coverage/marketing.spec.ts b/tests/e2e/spec-coverage/marketing.spec.ts index 8b8dcb79d..c39fc281a 100644 --- a/tests/e2e/spec-coverage/marketing.spec.ts +++ b/tests/e2e/spec-coverage/marketing.spec.ts @@ -159,7 +159,7 @@ function expectGenericError(message: unknown): void { /** Deep-link to a hash route and let the view settle. */ async function gotoHash(page: Page, hash: string): Promise { - await page.goto(`/apps/pipelinq/#${hash}`) + await page.goto(`/apps/pipelinq/${hash}`) await expect(page.locator('#content-vue')).toBeVisible({ timeout: 15000 }) await dismissWalkthrough(page) await dismissSupportDialog(page) diff --git a/tests/e2e/spec-coverage/outbound-messaging.spec.ts b/tests/e2e/spec-coverage/outbound-messaging.spec.ts index 96f3a81cc..bf36a93cd 100644 --- a/tests/e2e/spec-coverage/outbound-messaging.spec.ts +++ b/tests/e2e/spec-coverage/outbound-messaging.spec.ts @@ -30,7 +30,7 @@ async function assertNoServerError(page) { test('messaging settings page renders the provider administration surface', async ({ page, }) => { - await page.goto('/apps/pipelinq/#/settings/messaging') + await page.goto('/apps/pipelinq/settings/messaging') await assertNoServerError(page) }) @@ -38,7 +38,7 @@ test('messaging settings page renders the provider administration surface', asyn test('messaging settings offers no credential field (credentials live on the source)', async ({ page, }) => { - await page.goto('/apps/pipelinq/#/settings/messaging') + await page.goto('/apps/pipelinq/settings/messaging') await assertNoServerError(page) // The provider form must not present an API-key / credential input. await expect( @@ -48,13 +48,13 @@ test('messaging settings offers no credential field (credentials live on the sou // @e2e openspec/specs/outbound-messaging/spec.md#template-sync-status-and-manual-trigger test('messaging settings renders the templates panel', async ({ page }) => { - await page.goto('/apps/pipelinq/#/settings/messaging') + await page.goto('/apps/pipelinq/settings/messaging') await assertNoServerError(page) }) // @e2e openspec/specs/outbound-messaging/spec.md#connectivity-test-against-a-mock-mode-source test('messaging settings exposes a connectivity test action', async ({ page }) => { - await page.goto('/apps/pipelinq/#/settings/messaging') + await page.goto('/apps/pipelinq/settings/messaging') await assertNoServerError(page) }) @@ -62,13 +62,13 @@ test('messaging settings exposes a connectivity test action', async ({ page }) = test('connectivity test surfaces a degraded leaf without a browser 500', async ({ page, }) => { - await page.goto('/apps/pipelinq/#/settings/messaging') + await page.goto('/apps/pipelinq/settings/messaging') await assertNoServerError(page) }) // @e2e openspec/specs/outbound-messaging/spec.md#agent-sends-an-sms-from-a-client-record test('client detail renders the messages conversation section', async ({ page }) => { - await page.goto('/apps/pipelinq/#/clients') + await page.goto('/apps/pipelinq/clients') await assertNoServerError(page) }) @@ -76,13 +76,13 @@ test('client detail renders the messages conversation section', async ({ page }) test('contact detail renders the messages conversation section', async ({ page, }) => { - await page.goto('/apps/pipelinq/#/contacts') + await page.goto('/apps/pipelinq/contacts') await assertNoServerError(page) }) // @e2e openspec/specs/outbound-messaging/spec.md#composer-blocks-and-explains-on-missing-consent test('the send composer surface loads without a server error', async ({ page }) => { - await page.goto('/apps/pipelinq/#/clients') + await page.goto('/apps/pipelinq/clients') await assertNoServerError(page) }) @@ -90,13 +90,13 @@ test('the send composer surface loads without a server error', async ({ page }) test('the consent recording action is reachable from the send surface', async ({ page, }) => { - await page.goto('/apps/pipelinq/#/clients') + await page.goto('/apps/pipelinq/clients') await assertNoServerError(page) }) // @e2e openspec/specs/outbound-messaging/spec.md#opt-out-always-wins test('an opt-out state is reflected on the send surface', async ({ page }) => { - await page.goto('/apps/pipelinq/#/clients') + await page.goto('/apps/pipelinq/clients') await assertNoServerError(page) }) @@ -104,7 +104,7 @@ test('an opt-out state is reflected on the send surface', async ({ page }) => { test('within-window whatsapp free-text reply is allowed on the composer', async ({ page, }) => { - await page.goto('/apps/pipelinq/#/contacts') + await page.goto('/apps/pipelinq/contacts') await assertNoServerError(page) }) @@ -112,7 +112,7 @@ test('within-window whatsapp free-text reply is allowed on the composer', async test('client contactmoment timeline renders (outbound sends land here)', async ({ page, }) => { - await page.goto('/apps/pipelinq/#/contactmomenten') + await page.goto('/apps/pipelinq/contactmomenten') await assertNoServerError(page) }) @@ -120,7 +120,7 @@ test('client contactmoment timeline renders (outbound sends land here)', async ( test('contactmomenten list renders with the channel column (sms bucket)', async ({ page, }) => { - await page.goto('/apps/pipelinq/#/contactmomenten') + await page.goto('/apps/pipelinq/contactmomenten') await assertNoServerError(page) }) @@ -128,6 +128,6 @@ test('contactmomenten list renders with the channel column (sms bucket)', async test('existing contactmomenten remain readable after the additive sms enum', async ({ page, }) => { - await page.goto('/apps/pipelinq/#/contactmomenten') + await page.goto('/apps/pipelinq/contactmomenten') await assertNoServerError(page) }) diff --git a/tests/e2e/spec-coverage/pipeline.spec.ts b/tests/e2e/spec-coverage/pipeline.spec.ts index 7aa12f598..a1d45a502 100644 --- a/tests/e2e/spec-coverage/pipeline.spec.ts +++ b/tests/e2e/spec-coverage/pipeline.spec.ts @@ -12,7 +12,7 @@ import { openApp, navClick } from '../helpers/pipelinq' // @e2e openspec/specs/pipeline/spec.md#view-pipeline-details-in-sidebar test('pipeline page renders with sidebar', async ({ page }) => { - await page.goto('/apps/pipelinq/#/pipeline') + await page.goto('/apps/pipelinq/pipeline') await expect(page).toHaveURL(/pipeline/, { timeout: 10000 }) await expect(page.locator('body')).not.toContainText('Internal Server Error') }) @@ -21,7 +21,7 @@ test('pipeline page renders with sidebar', async ({ page }) => { test('pipeline sidebar shows Details and Stages tabs or empty state', async ({ page, }) => { - await page.goto('/apps/pipelinq/#/pipeline') + await page.goto('/apps/pipelinq/pipeline') // Either pipeline selector is present or we see empty state const hasSelector = await page .locator('select, [role="combobox"]') @@ -37,7 +37,7 @@ test('pipeline sidebar shows Details and Stages tabs or empty state', async ({ // @e2e openspec/specs/pipeline/spec.md#sidebar-does-not-block-board-interaction test('pipeline page main content area is accessible', async ({ page }) => { - await page.goto('/apps/pipelinq/#/pipeline') + await page.goto('/apps/pipelinq/pipeline') // Main content renders without blocking overlay const mainContent = page.locator('#app-content, .app-content, main').first() await expect(mainContent).toBeVisible({ timeout: 10000 }) @@ -45,7 +45,7 @@ test('pipeline page main content area is accessible', async ({ page }) => { // @e2e openspec/specs/pipeline/spec.md#kanban-card-display---request-card test('pipeline page loads without error', async ({ page }) => { - await page.goto('/apps/pipelinq/#/pipeline') + await page.goto('/apps/pipelinq/pipeline') await expect(page.locator('body')).not.toContainText('Internal Server Error', { timeout: 10000, }) @@ -77,7 +77,7 @@ test('pipeline page navigates from dashboard nav', async ({ page }) => { test('pipeline page renders without server error after navigation', async ({ page, }) => { - await page.goto('/apps/pipelinq/#/pipeline') + await page.goto('/apps/pipelinq/pipeline') await page.waitForTimeout(2000) await expect(page.locator('body')).not.toContainText('Internal Server Error') }) diff --git a/tests/e2e/spec-coverage/pipelinq-pos-grouping.spec.ts b/tests/e2e/spec-coverage/pipelinq-pos-grouping.spec.ts index efade4d20..5f061b111 100644 --- a/tests/e2e/spec-coverage/pipelinq-pos-grouping.spec.ts +++ b/tests/e2e/spec-coverage/pipelinq-pos-grouping.spec.ts @@ -162,7 +162,7 @@ test('every regrouped POS and product route still resolves by deep link', async ] for (const route of routes) { - await page.goto(`/apps/pipelinq/#${route}`) + await page.goto(`/apps/pipelinq/${route}`) await dismissWalkthrough(page) await dismissSupportDialog(page) diff --git a/tests/e2e/spec-coverage/pos-transaction-core.spec.ts b/tests/e2e/spec-coverage/pos-transaction-core.spec.ts index 4724766eb..f63da3108 100644 --- a/tests/e2e/spec-coverage/pos-transaction-core.spec.ts +++ b/tests/e2e/spec-coverage/pos-transaction-core.spec.ts @@ -26,7 +26,7 @@ import { openApp, navClick, clickHeaderAction } from '../helpers/pipelinq' test('POS transaction list (Kassabon) page renders the real list shell', async ({ page, }) => { - await page.goto('/apps/pipelinq/#/pos') + await page.goto('/apps/pipelinq/pos') await expect(page).toHaveURL(/pos/, { timeout: 10000 }) await expect(page.locator('body')).not.toContainText('Internal Server Error') // The real CnIndexPage list surface (not just the shell mount) — its host @@ -47,7 +47,7 @@ test('POS transaction list (Kassabon) page renders the real list shell', async ( test('POS transaction list shows the real empty state (or populated rows) without error', async ({ page, }) => { - await page.goto('/apps/pipelinq/#/pos') + await page.goto('/apps/pipelinq/pos') await expect(page.locator('body')).not.toContainText('Internal Server Error', { timeout: 10000, }) diff --git a/tests/e2e/spec-coverage/prospect-add-as-client.spec.ts b/tests/e2e/spec-coverage/prospect-add-as-client.spec.ts index 4baf5ce98..ba44acf7f 100644 --- a/tests/e2e/spec-coverage/prospect-add-as-client.spec.ts +++ b/tests/e2e/spec-coverage/prospect-add-as-client.spec.ts @@ -27,7 +27,7 @@ test.beforeEach(() => { /** Open the Prospects page. */ async function openProspects(page: Page) { - await page.goto('/apps/pipelinq/#/prospects') + await page.goto('/apps/pipelinq/prospects') await dismissWalkthrough(page) await dismissSupportDialog(page) await expect(page.locator('.prospects-view')).toBeVisible({ timeout: 20000 }) diff --git a/tests/e2e/spec-coverage/request-client-contact-cascade.spec.ts b/tests/e2e/spec-coverage/request-client-contact-cascade.spec.ts index c08922ff3..710055657 100644 --- a/tests/e2e/spec-coverage/request-client-contact-cascade.spec.ts +++ b/tests/e2e/spec-coverage/request-client-contact-cascade.spec.ts @@ -19,7 +19,7 @@ import { dismissSupportDialog, dismissWalkthrough } from '../helpers/pipelinq' /** Open the New Request dialog from the Customer Support header action. */ async function openNewRequestDialog(page: Page) { - await page.goto('/apps/pipelinq/#/werkplek') + await page.goto('/apps/pipelinq/werkplek') await dismissWalkthrough(page) await dismissSupportDialog(page) diff --git a/tests/e2e/spec-coverage/request-management.spec.ts b/tests/e2e/spec-coverage/request-management.spec.ts index 2076ca344..5392d9917 100644 --- a/tests/e2e/spec-coverage/request-management.spec.ts +++ b/tests/e2e/spec-coverage/request-management.spec.ts @@ -10,7 +10,7 @@ * `unify-ticket-supertype` removed the `/requests` route: request tickets live * on the unified Tickets index (src/manifest.json page id `Tickets`, route * `/tickets`) behind the "Tickets" `quickFilters[]` tab. Eight of the ten tests - * here did `page.goto('/apps/pipelinq/#/requests')` and then asserted only that + * here did `page.goto('/apps/pipelinq/requests')` and then asserted only that * the body did NOT contain "Internal Server Error" / "Uncaught Error", or that * `main` was visible. With no `/requests` route the hash router falls back to * the Dashboard — and the Dashboard satisfies every one of those assertions. @@ -165,7 +165,7 @@ test('requests by status widget on dashboard', async ({ page }) => { // The request-status distribution widget lives on the Operational overview // dashboard (#/operational), not the landing Commercial overview — the IA // restructure split the dashboards by audience. - await page.goto('/apps/pipelinq/#/operational') + await page.goto('/apps/pipelinq/operational') await expect( page.locator('#content-vue').getByText('Requests by Status').first(), ).toBeVisible({ timeout: 15000 }) diff --git a/tests/e2e/spec-coverage/semantic-handoff-emit.spec.ts b/tests/e2e/spec-coverage/semantic-handoff-emit.spec.ts index 1c3ec1d5d..df417aa5f 100644 --- a/tests/e2e/spec-coverage/semantic-handoff-emit.spec.ts +++ b/tests/e2e/spec-coverage/semantic-handoff-emit.spec.ts @@ -30,7 +30,7 @@ async function assertNoServerError(page) { test('request detail renders the conversion surface without a server error', async ({ page, }) => { - await page.goto('/apps/pipelinq/#/requests') + await page.goto('/apps/pipelinq/requests') await assertNoServerError(page) }) @@ -38,7 +38,7 @@ test('request detail renders the conversion surface without a server error', asy test('convert-to-case action is absent when no app implements ns#Case', async ({ page, }) => { - await page.goto('/apps/pipelinq/#/requests') + await page.goto('/apps/pipelinq/requests') await assertNoServerError(page) // On a bare instance (no ns#Case implementer) the action must not be rendered. await expect(page.getByRole('button', { name: /convert to case/i })).toHaveCount( @@ -48,7 +48,7 @@ test('convert-to-case action is absent when no app implements ns#Case', async ({ // @e2e openspec/specs/request-management/spec.md#conversion-displays-case-link test('a converted request shows its case link/notice', async ({ page }) => { - await page.goto('/apps/pipelinq/#/requests') + await page.goto('/apps/pipelinq/requests') await assertNoServerError(page) }) @@ -56,7 +56,7 @@ test('a converted request shows its case link/notice', async ({ page }) => { test('a converted request renders its read-only converted notice', async ({ page, }) => { - await page.goto('/apps/pipelinq/#/requests') + await page.goto('/apps/pipelinq/requests') await assertNoServerError(page) }) @@ -64,7 +64,7 @@ test('a converted request renders its read-only converted notice', async ({ test('contract surface renders the send-to-invoicing action area', async ({ page, }) => { - await page.goto('/apps/pipelinq/#/contracts') + await page.goto('/apps/pipelinq/contracts') await assertNoServerError(page) }) @@ -72,7 +72,7 @@ test('contract surface renders the send-to-invoicing action area', async ({ test('send-to-invoicing action is absent when no app implements ns#Invoice', async ({ page, }) => { - await page.goto('/apps/pipelinq/#/contracts') + await page.goto('/apps/pipelinq/contracts') await assertNoServerError(page) await expect( page.getByRole('button', { name: /send to invoicing/i }), diff --git a/tests/e2e/spec-coverage/sla-engine-and-escalation.spec.ts b/tests/e2e/spec-coverage/sla-engine-and-escalation.spec.ts index 7142e8949..3f80d7190 100644 --- a/tests/e2e/spec-coverage/sla-engine-and-escalation.spec.ts +++ b/tests/e2e/spec-coverage/sla-engine-and-escalation.spec.ts @@ -134,7 +134,7 @@ async function seededPolicies(page: Page): Promise { * instead — `#content-vue` mounted, and Nextcloud's own error chrome absent. */ async function gotoHash(page: Page, hash: string): Promise { - await page.goto(`/apps/pipelinq/#${hash}`) + await page.goto(`/apps/pipelinq/${hash}`) await expect(page.locator('#content-vue')).toBeVisible({ timeout: 15000 }) await expect(nextcloudErrorPage(page)).toHaveCount(0) await dismissWalkthrough(page) diff --git a/tests/e2e/spec-coverage/visual-coverage-export-pages.spec.ts b/tests/e2e/spec-coverage/visual-coverage-export-pages.spec.ts index 046b69663..971abd5c5 100644 --- a/tests/e2e/spec-coverage/visual-coverage-export-pages.spec.ts +++ b/tests/e2e/spec-coverage/visual-coverage-export-pages.spec.ts @@ -48,7 +48,7 @@ const ABSENT_RUN_ID = 'e2e-gate26-no-such-run' * @param hash The manifest `route`, e.g. `/export/runs`. */ async function openSpaRoute(page: Page, hash: string): Promise { - const response = await page.goto(`/apps/pipelinq/#${hash}`) + const response = await page.goto(`/apps/pipelinq/${hash}`) await assertAppShellServed(page, response) // `routesFromManifest()` ends the table with a catch-all that REDIRECTS to // `/`, so an unmatched route silently becomes the Dashboard. A surviving diff --git a/tests/e2e/spec-coverage/visual-coverage-spa-pages.spec.ts b/tests/e2e/spec-coverage/visual-coverage-spa-pages.spec.ts index c0d8bf4c2..0543c1a1d 100644 --- a/tests/e2e/spec-coverage/visual-coverage-spa-pages.spec.ts +++ b/tests/e2e/spec-coverage/visual-coverage-spa-pages.spec.ts @@ -68,7 +68,7 @@ const ABSENT_ID = 'e2e-gate26-no-such-record' * @param hash The manifest `route`, e.g. `/my-work`. */ async function openSpaRoute(page: Page, hash: string): Promise { - const response = await page.goto(`/apps/pipelinq/#${hash}`) + const response = await page.goto(`/apps/pipelinq/${hash}`) await assertAppShellServed(page, response) // THE HASH IS THE PROOF THE ROUTE MATCHED. `routesFromManifest()` closes the // table with `{ path: '/:pathMatch(.*)*', redirect: '/' }`, so an unmatched diff --git a/tests/e2e/visual/pipelinq.visual.spec.ts b/tests/e2e/visual/pipelinq.visual.spec.ts index ff6bd3306..c9cbb14b4 100644 --- a/tests/e2e/visual/pipelinq.visual.spec.ts +++ b/tests/e2e/visual/pipelinq.visual.spec.ts @@ -17,10 +17,10 @@ const APP = '/index.php/apps/pipelinq' test.describe('PipelinQ — visual baselines', () => { test('dashboard', async ({ page }) => { - await shootSurface(page, `${APP}/#/`, 'dashboard.png') + await shootSurface(page, `${APP}/`, 'dashboard.png') }) test('clients list', async ({ page }) => { - await shootByNav(page, `${APP}/#/`, 'Clients', 'clients.png') + await shootByNav(page, `${APP}/`, 'Clients', 'clients.png') }) }) diff --git a/tests/e2e/xwiki-integration.spec.ts b/tests/e2e/xwiki-integration.spec.ts index 66f9cd1fc..79a4036ca 100644 --- a/tests/e2e/xwiki-integration.spec.ts +++ b/tests/e2e/xwiki-integration.spec.ts @@ -17,7 +17,7 @@ test.describe('xWiki Integration', () => { }) => { // The knowledge-base widget lives on the Operational overview dashboard // after the IA dashboard split, not the landing Commercial overview. - await page.goto('/apps/pipelinq/#/operational') + await page.goto('/apps/pipelinq/operational') // Wait for the manifest shell to mount before checking widgets. await page .locator('#content-vue') @@ -74,11 +74,11 @@ test.describe('xWiki Integration', () => { const first = (await res.json())?.results?.[0] test.skip(!first?.id, 'no seeded client to open a detail page for') - await page.goto(`/apps/pipelinq/#/clients/${first.id}`) + await page.goto(`/apps/pipelinq/clients/${first.id}`) // The detail route resolved. Asserted, never caught: if this fails the // message must say the page did not load, not that a tab is missing. - await expect(page).toHaveURL(/#\/clients\/[^/]+/, { timeout: 15000 }) + await expect(page).toHaveURL(/\/clients\/[^/]+/, { timeout: 15000 }) await expect(page.locator('#content-vue')).toBeVisible({ timeout: 15000 }) // OPEN THE SIDEBAR. The tab lives inside it, and it mounts CLOSED: