fix(workspace): keep the canvas link on the proxy mount
A resolver failure no longer stops the shell from mounting: the ComfyUI link falls back to the root read from the page's own mount path (never another origin). Tests cover a non-standard /ComfyBackendDirect mount, including that no request escapes it.
This commit is contained in:
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
+1
-1
@@ -302,7 +302,7 @@
|
||||
"workspace/topbar.mjs": {
|
||||
"lines": 98,
|
||||
"branches": 98,
|
||||
"functions": 81,
|
||||
"functions": 83,
|
||||
"statements": 98
|
||||
},
|
||||
"workspace/viewer-state.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)},
|
||||
}));
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user