diff --git a/tests/browser/workspace.spec.mjs b/tests/browser/workspace.spec.mjs index 117a580..65df3fd 100644 --- a/tests/browser/workspace.spec.mjs +++ b/tests/browser/workspace.spec.mjs @@ -630,3 +630,55 @@ test('the footer is the concept app footer with production copy', async ({ page } }); +// #164: the maintainer serves ComfyUI behind a proxy prefix. Everything, the ComfyUI link included, stays on that mount. +const PROXY_MOUNT = '/ComfyBackendDirect'; +async function serveUnderProxyMount(page) { + const escaped = []; + page.on('request', item => { + const { pathname } = new URL(item.url()); + if (pathname.startsWith('/prompt_manager/')) escaped.push(pathname); + }); + await page.route(url => url.pathname.startsWith(`${PROXY_MOUNT}/`), async route => { + const url = new URL(route.request().url()); + url.pathname = url.pathname.slice(PROXY_MOUNT.length); + const response = await route.fetch({ url: url.href }); + await route.fulfill({ response }); + }); + return escaped; +} + +test('the ComfyUI link points to the root of a non-standard proxy mount', async ({ page }) => { + const escaped = await serveUnderProxyMount(page); + for (const route of ['overview', 'library', 'tags']) { + await page.goto(`${PROXY_MOUNT}${workspace}#/${route}`); + await expect(page.locator('#workspace-main > .heading h1')).toBeVisible(); + const link = page.getByRole('link', { name: 'ComfyUI', exact: true }); + await expect(link).toHaveAttribute('href', `${PROXY_MOUNT}/`); + expect(await link.evaluate(node => node.href)).toBe(new URL(`${PROXY_MOUNT}/`, page.url()).href); + } + await expect(navCount(page, 'library')).toHaveCount(1); + expect(escaped).toEqual([]); +}); + +test('a resolver failure for the ComfyUI link never stops the shell from mounting', async ({ page }) => { + await serveUnderProxyMount(page); + // Wrap the shared client as it loads so only the topbar's canvas-root lookup throws (the sender reads the same root). + await page.addInitScript(() => { + let client; + Object.defineProperty(window, 'ApiClient', { configurable: true, get: () => client, set(value) { + client = { ...value, resolveUrl: (url, ...rest) => { + if (url === '/prompt_manager/' && /topbar\.mjs/.test(new Error().stack)) throw new Error('resolver unavailable'); + return value.resolveUrl(url, ...rest); + } }; + } }); + }); + const warnings = []; + page.on('console', message => { if (message.type() === 'warning') warnings.push(message.text()); }); + await page.goto(`${PROXY_MOUNT}${workspace}#/overview`); + await expect(page.locator('#workspace-main h1')).toHaveText('Overview'); + await expect(sidebarLinks(page)).toHaveCount(routes.length); + expect(warnings.filter(text => text.includes('the ComfyUI link falls back to the page mount'))).toHaveLength(1); + await expect(page.getByRole('link', { name: 'ComfyUI', exact: true })).toHaveAttribute('href', `${PROXY_MOUNT}/`); + await sidebarLink(page, 'Prompt library').click(); + await expect(page.locator('#workspace-main h1')).toHaveText('Prompt library'); +}); diff --git a/tests/js/workspace-topbar.test.js b/tests/js/workspace-topbar.test.js index bdf797e..4255955 100644 --- a/tests/js/workspace-topbar.test.js +++ b/tests/js/workspace-topbar.test.js @@ -1,6 +1,6 @@ import test from 'node:test'; import assert from 'node:assert/strict'; -import { canvasRoot, bindCanvasLink } from '../../web/js/workspace/topbar.mjs'; +import { canvasRoot, canvasHref, bindCanvasLink } from '../../web/js/workspace/topbar.mjs'; // The shared mount-aware resolver (#164), as api-client.js implements it. const resolverAt = pathname => url => { @@ -27,6 +27,33 @@ test('the canvas root is the ComfyUI root under the page mount', () => { assert.throws(() => canvasRoot(() => 'https://elsewhere.example/'), /canvas/i); }); +test('the canvas root follows a non-standard proxy mount', () => { + assert.equal(canvasRoot(resolverAt('/ComfyBackendDirect/prompt_manager/workspace')), '/ComfyBackendDirect/'); + assert.equal(canvasHref(resolverAt('/ComfyBackendDirect/prompt_manager/workspace'), '/ComfyBackendDirect/prompt_manager/workspace'), + '/ComfyBackendDirect/'); +}); + +// A4 review: a resolver failure must never stop the shell from mounting, and the fallback stays on the page's mount. +test('a resolver failure falls back to the page mount and never to another origin', () => { + const warn = console.warn; + const warnings = []; + console.warn = (...args) => warnings.push(args); + try { + const broken = () => { throw new Error('resolver unavailable'); }; + assert.equal(canvasHref(broken, '/ComfyBackendDirect/prompt_manager/workspace'), '/ComfyBackendDirect/'); + assert.equal(canvasHref(broken, '/ComfyBackendDirect/prompt_manager/workspace/'), '/ComfyBackendDirect/'); + assert.equal(canvasHref(broken, '/prompt_manager/workspace'), '/'); + assert.equal(canvasHref(() => 'https://elsewhere.example/prompt_manager/', '/a/prompt_manager/workspace'), '/a/'); + assert.equal(canvasHref(broken, '//elsewhere.example/prompt_manager/workspace'), '/'); + assert.equal(canvasHref(broken, '/workspace'), '/'); + assert.equal(canvasHref(broken, undefined), '/'); + assert.equal(canvasHref(undefined, '/a/prompt_manager/workspace'), '/a/'); + assert.equal(warnings.length, 8); + } finally { + console.warn = warn; + } +}); + test('ComfyUI link follows its href unless a resolved pair is selected', () => { const node = link(); const opened = []; diff --git a/tools/vendor/frontend-coverage-baseline.json b/tools/vendor/frontend-coverage-baseline.json index fcdbfb2..747366b 100644 --- a/tools/vendor/frontend-coverage-baseline.json +++ b/tools/vendor/frontend-coverage-baseline.json @@ -302,7 +302,7 @@ "workspace/topbar.mjs": { "lines": 98, "branches": 98, - "functions": 81, + "functions": 83, "statements": 98 }, "workspace/viewer-state.mjs": { diff --git a/web/js/workspace/index.mjs b/web/js/workspace/index.mjs index b725a03..b57de5f 100644 --- a/web/js/workspace/index.mjs +++ b/web/js/workspace/index.mjs @@ -6,7 +6,7 @@ import { mountImages } from './images.mjs'; import { mountLibrary, mountNewPromptAction, onLibraryWrite } from './library.mjs'; import { mountVocabulary } from './vocabulary.mjs'; import { createWorkspaceIcon } from './icons.mjs'; -import { bindCanvasLink, canvasRoot } from './topbar.mjs'; +import { bindCanvasLink, canvasHref } from './topbar.mjs'; import { renderHeading } from './heading.mjs'; const { createApiClient, resolveUrl } = window.ApiClient; @@ -42,7 +42,7 @@ function mountWorkspace() { // D6: ComfyUI ↗ opens the canvas; on Images with a resolved selection it sends that pair (D5: no Activity). let images = null; scope.cleanup(bindCanvasLink(document.querySelector('.topbar .canvas-link'), { - href: canvasRoot(resolveUrl), + href: canvasHref(resolveUrl, window.location.pathname), getSelectedPair: () => images?.selectedPair?.() ?? null, sender: {open: (roles, trigger) => images?.sender.open(roles, trigger)}, })); diff --git a/web/js/workspace/topbar.mjs b/web/js/workspace/topbar.mjs index 45e500c..ea66eba 100644 --- a/web/js/workspace/topbar.mjs +++ b/web/js/workspace/topbar.mjs @@ -11,6 +11,23 @@ export function canvasRoot(resolve) { return root.replace(PROMPT_MANAGER, ''); } +const PAGE_MOUNT = /^(.*\/)prompt_manager(?:\/|$)/; + +/** + * The canvas link's href. A resolver failure must never stop the shell from mounting, so it falls back to the + * ComfyUI root read from the page's own path (the #164 proxy mount), and to the origin root when even that is not a + * same-origin root-relative path. + */ +export function canvasHref(resolve, pathname) { + try { + return canvasRoot(resolve); + } catch (error) { + console.warn('Prompt Manager: the ComfyUI link falls back to the page mount.', error); + const mount = typeof pathname === 'string' ? pathname.match(PAGE_MOUNT)?.[1] : undefined; + return mount && !mount.startsWith('//') ? mount : '/'; + } +} + const plainClick = event => event.button === 0 && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey; const isPair = roles => roles !== null && typeof roles === 'object' && typeof roles.positive === 'string' && typeof roles.negative === 'string';