diff --git a/README.md b/README.md index 946967d1..66d96f8b 100644 --- a/README.md +++ b/README.md @@ -251,9 +251,9 @@ match Classic and Focused. - **PromptManagerText**: Look for "Prompt Manager Text" in the PromptManager/Text category - outputs STRING for any text node 5. **Access the web interface**: - - **Admin Dashboard**: `http://localhost:8188/prompt_manager/admin` - Full management interface - - **Metadata Viewer**: `http://localhost:8188/prompt_manager/metadata.html` - Standalone PNG analysis tool - - **Simple Interface**: `http://localhost:8188/prompt_manager/` - Basic prompt browser + - **Workspace**: click **PM** in ComfyUI's top bar or **Web UI** on a node, or open `http://localhost:8188/prompt_manager/workspace` + - **Legacy pages** still work at their URLs, and System's Overview section links to them: + `/prompt_manager/admin` (dashboard), `/prompt_manager/admin#/tags` (tags), `/prompt_manager/gallery` (gallery), `/prompt_manager/gallery.html` (metadata viewer) ### Prerequisites @@ -324,9 +324,10 @@ The comprehensive gallery system provides complete access to all your ComfyUI ou #### **πŸ“ Accessing the Gallery** -1. **Open the admin interface** at `http://localhost:8188/prompt_manager/admin` -2. **Click the "πŸ–ΌοΈ Gallery" button** to open the output-wide gallery -3. **Browse all content** from your ComfyUI output directory +1. **Open the workspace's Images page** at `http://localhost:8188/prompt_manager/workspace#/images` +2. **Browse all content** from your ComfyUI output directory + +The legacy gallery stays at `http://localhost:8188/prompt_manager/gallery`. #### **🎬 Video & Image Support** diff --git a/TESTING.md b/TESTING.md index 22ed40bd..b67bf09c 100644 --- a/TESTING.md +++ b/TESTING.md @@ -2641,8 +2641,8 @@ applied), D35 (singular captions), D36 (sorted categories), D40 (the continuatio the grid) and D43 (sorted chips, checked against the concept with its chips sorted). D44 needs no mapping: the comparison runs with no Refine filter active, so the button reads "Filters" as the concept's does. Nothing else follows the grid: the legacy -"Current prompt library" link is removed from the shelf; Overview's legacy card ("Open -existing prompt library") and the sidebar still link `/prompt_manager/admin` +"Current prompt library" link is removed from the shelf; the legacy Library stays at +`/prompt_manager/admin` by direct URL, linked only from System (#525, D165) (`tests/browser/workspace.spec.mjs`). `library comparison detects mutations` injects `.card-footer { padding: 2px }` and removes `.card-meta .chip.red`. @@ -3117,8 +3117,8 @@ npm run --prefix tools/vendor test:workflows -- history-concept.spec.mjs \ ### Inspect a file page and recorded deviations (#249 local Inspect, PR L2) `#/inspect` ("Inspect a file") is a native workspace route; it no longer hands off to -the legacy metadata page, which stays reachable from Overview's "Open existing metadata -inspector" card (`/prompt_manager/gallery.html`). The page is the concept's `#metadata` +the legacy metadata page, which stays at its URL +(`/prompt_manager/gallery.html`) linked only from System (#525, D165). The page is the concept's `#metadata` (app.js `renderMetadata` a740–748, selection a1715–1729; style.css 709–722, 1180–1194, 1543–1545, 1559–1564) in the concept's own classes (`.surface`, `.file-drop`, `.spaced-20`, `.local-file-preview`, all from the synced `concept.css`), with no @@ -7029,8 +7029,8 @@ Deploying this backend needs a ComfyUI restart (Python and route changes). ## Native vocabulary UI (#236) `#/tags` ("Tags & categories", page title "Tags") is a native workspace route; it no -longer hands off to the legacy tag manager. That page stays reachable from Overview's -"Open existing tag manager" card for its own page; tag search and sort, the untagged +longer hands off to the legacy tag manager. That page stays at +`/prompt_manager/admin#/tags` linked only from System (#525, D165); tag search and sort, the untagged entry (D149) and multi-tag OR (the Library's Tag match, D150) moved to the workspace (#444); bulk prompt delete stays on the legacy admin page (bulk tags: the Prompt library's selection bar, D151). A prompt's category is changed in the Prompt library @@ -7152,3 +7152,23 @@ until acceptance. Not compared: hover states and the dialog focus ring (the page keyboard ring and link ring offsets are asserted). No coverage floor was lowered; `vocabulary-state.mjs`, `vocabulary.mjs` and `vocabulary-dialog.mjs` have additive floors in `frontend-coverage-baseline.json`. + +## Cutover entry points (#525) + +The top-bar PM button, both Web UI buttons and `/prompt_manager/web` open +`/prompt_manager/workspace#/library` (`web/js/workspace-entry.js`; `web/index.html` keeps its +own copy, pinned by `tests/js/workspace-entry.test.js`). Overview's cards open native hashes. +The legacy pages (`/prompt_manager/admin`, `admin#/tags`, `gallery`, `gallery.html`) stay at +their URLs, and admin's Metadata and Gallery buttons still open their legacy pages (maintainer +ruling; pinned by `legacy-system-links.spec.mjs`). + +- **D165** (beyond the concept, maintainer ruling 2026-10-30): System's Overview section ends + with one small line, "Legacy pages: Dashboard Β· Tags Β· Gallery Β· Metadata viewer" + (`.legacy-pages`, new tabs), outside `.settings-layout`. The concept has no such line; + `mapOverviewConcept` adds it to the page tree. +- **Load failure** (maintainer ruling 2026-10-30): `web/workspace.html` holds static + `#workspace-boot-failure` with the same four links, styled inline so it needs no stylesheet. + An inline guard shows it at `load` unless `index.mjs` set `data-workspace-booted`, so a failed + fetch of any module and a throw while starting are both covered; `workspace.spec.mjs` tests + both. Editing `workspace.html` needs a ComfyUI restart, because the server caches it. The PM button has no +page-fixture test; it is proven on the managed host (`tests/comfyui/prompt-manager.spec.mjs`). diff --git a/tests/browser/classic-node.spec.mjs b/tests/browser/classic-node.spec.mjs index 0f6cdead..bd8d0b57 100644 --- a/tests/browser/classic-node.spec.mjs +++ b/tests/browser/classic-node.spec.mjs @@ -7,7 +7,7 @@ test.beforeEach(async ({page, browserName}) => { page.classicErrors = []; page.on('pageerror', error => page.classicErrors.push(error.message)); await page.route('**/scripts/app.js', route => route.fulfill({contentType:'text/javascript', body:'export const app = window.fixtureApp;'})); - await page.route('**/scripts/api.js', route => route.fulfill({contentType:'text/javascript', body:'export const api = {fileURL: path => path, queuePrompt: async (...args) => args};'})); + await page.route('**/scripts/api.js', route => route.fulfill({contentType:'text/javascript', body:'export const api = {fileURL: path => (window.fixtureApiBase ?? "") + path, queuePrompt: async (...args) => args};'})); await page.route('**/extensions/promptmanager/prompt_manager.js', route => route.fulfill({contentType:'text/javascript', body:readFileSync(new URL('../../web/comfy/prompt_manager.js', import.meta.url),'utf8')})); await page.goto('/prompt_manager/test'); await page.evaluate(() => { @@ -64,6 +64,28 @@ test('Classic keeps immediate selection, search query and metadata serialization await expect(page.getByRole('button',{name:'🌐 Web UI',exact:true})).toBeVisible(); }); +test('Classic Web UI opens the workspace Library in the saved display mode, through the mount prefix (#525)',async({page})=>{ + await page.evaluate(()=>{window.fixtureApiBase='/mounted';window.open=(...args)=>{window.openArgs=args;return {};};}); + const webUi=page.getByRole('button',{name:'🌐 Web UI',exact:true}); + for (const [mode,args] of [ + ['newtab',['/mounted/prompt_manager/workspace#/library','_blank']], + ['popup',['/mounted/prompt_manager/workspace#/library','_blank','width=1200,height=800,scrollbars=yes,resizable=yes']], + ]) { + await page.evaluate(value=>{node.properties.webuiDisplayMode=value;},mode); + await webUi.click(); + expect(await page.evaluate(()=>window.openArgs)).toEqual(args); + } +}); + +test('Classic Web UI reports a blocked popup and keeps the canvas open (#525)',async({page})=>{ + const before=page.url(); + await page.evaluate(()=>{window.open=()=>null;}); + await page.getByRole('button',{name:'🌐 Web UI',exact:true}).click(); + await expect(page.getByText('Allow popups to open Web UI, then try again.')).toBeVisible(); + expect(page.url()).toBe(before); + await expect(page.locator('.pm-classic')).toBeVisible(); +}); + test('Classic detaches native text synchronization on removal',async({page})=>{ // The page installs the text sync (serializeValue and its listeners) in a 50 ms timer; wait for it, not for time. await expect.poll(()=>page.evaluate(()=>typeof node.widgets.find(item=>item.name==='text').serializeValue)).toBe('function'); diff --git a/tests/browser/focused-node.spec.mjs b/tests/browser/focused-node.spec.mjs index 6ab2818c..00c4667c 100644 --- a/tests/browser/focused-node.spec.mjs +++ b/tests/browser/focused-node.spec.mjs @@ -485,7 +485,7 @@ test(`${variant} respects the existing Web UI display preference`,async({page})= }); await page.evaluate(()=>node._focusedSettingsReady); await page.getByRole('button',{name:'Web UI β†—',exact:true}).click(); - expect(await page.evaluate(()=>window.openArgs)).toEqual(['/prompt_manager/web','_blank']); + expect(await page.evaluate(()=>window.openArgs)).toEqual(['/prompt_manager/workspace#/library','_blank']); }); test(`${variant} reports blocked Web UI popups without leaving the workflow`,async({page})=>{ diff --git a/tests/browser/system-concept.spec.mjs b/tests/browser/system-concept.spec.mjs index e4320b90..f616b274 100644 --- a/tests/browser/system-concept.spec.mjs +++ b/tests/browser/system-concept.spec.mjs @@ -165,6 +165,8 @@ const number = value => value.toLocaleString('en-US'); /** The concept's shell and Overview with the recorded deviations applied, given the server's real overview. */ function mapOverviewConcept(reference, overview) { + // D165: the discreet legacy-pages line follows the settings layout. + reference = { ...reference, tree: [...reference.tree, 'p.legacy-pages.small'] }; const relabel = { 'Database integrity': 'Not checked', 'Positive prompts': number(overview.library.positive_prompts), 'Negative variants': number(overview.library.negative_variants), Images: number(overview.images.indexed) }; const rows = { @@ -257,7 +259,7 @@ const UNMAPPED_PATHS = [ 'columns.surfaces.0.rows.length', 'columns.surfaces.0.tree.4', 'columns.surfaces.0.tree.length', 'columns.surfaces.1.rows.0.description', 'columns.surfaces.1.rows.1.title', 'columns.surfaces.1.rows.2.description', 'heading.actions.0', 'heading.actions.length', 'metrics.items.3.value', 'nav.label', - 'save.button.cursor', 'save.button.opacity', 'save.status.text', + 'save.button.cursor', 'save.button.opacity', 'save.status.text', 'tree.3', 'tree.length', ].sort(); test('system comparison detects mutations', async ({ page, request }) => { diff --git a/tests/browser/workspace.spec.mjs b/tests/browser/workspace.spec.mjs index dff63d85..97f89b06 100644 --- a/tests/browser/workspace.spec.mjs +++ b/tests/browser/workspace.spec.mjs @@ -11,12 +11,12 @@ const sidebarLink = (page, label) => page.locator('aside.sidebar nav').getByRole const sidebarLinks = page => page.locator('aside.sidebar nav a'); const routes = [ ['overview', 'Overview', null], - // D29: Images no longer links the legacy gallery; Overview's tool cards still do. + // D29: Images no longer links the legacy gallery. ['images', 'Images', null], - // #249 E3: the shelf no longer links the legacy Library; Overview's tool card still does. + // #249 E3: the shelf no longer links the legacy Library. ['library', 'Prompt library', null], ['tags', 'Tags & categories', null], - // #249 local Inspect: the native Inspect a file page; Overview's tool card still links the legacy metadata page. + // #249 local Inspect: the native Inspect a file page. ['inspect', 'Inspect a file', null], // #249 System S2: native System; every section native since S12 (D94 retired, asserted below). ['system', 'System', null], @@ -93,20 +93,106 @@ test('all shell destinations deep-link, reload and expose working existing appli await page.reload(); await expect(main.getByRole('heading', { level: 1, name: title, exact: true })).toBeVisible(); } - // D29 and #249 E3: the legacy gallery and Library are reached from Overview's tool cards, not from their pages. + // #525: Overview's cards open the native pages, and no workspace page links a legacy one. + for (const [action, hash, title] of [ + ['Open Images', '#/images', 'Images'], + ['Open Prompt library', '#/library', 'Prompt library'], + ['Open Tags & categories', '#/tags', 'Tags & categories'], + ['Open Inspect a file', '#/inspect', 'Inspect a file'], + ['Open System', '#/system/overview', 'System'], + ]) { + await page.goto(`${workspace}#/overview`); + const main = page.locator('#workspace-main'); + const card = main.getByRole('link', { name: action, exact: true }); + await expect(card).toHaveAttribute('href', hash); + await card.click(); + await expect(page).toHaveURL(new RegExp(`/prompt_manager/workspace${hash}$`)); + await expect(main.getByRole('heading', { level: 1, name: title, exact: true })).toBeVisible(); + } await page.goto(`${workspace}#/overview`); - const library = page.locator('#workspace-main').getByRole('link', { name: 'Open existing prompt library', exact: true }); - await expect(library).toHaveAttribute('href', '/prompt_manager/admin'); - await expect(await request.get('/prompt_manager/admin')).toBeOK(); - // The legacy metadata page stays reachable from Overview beside the native Inspect page. - const metadata = page.locator('#workspace-main').getByRole('link', { name: 'Open existing metadata inspector', exact: true }); - await expect(metadata).toHaveAttribute('href', '/prompt_manager/gallery.html'); - await expect(await request.get('/prompt_manager/gallery.html')).toBeOK(); - const gallery = page.locator('#workspace-main').getByRole('link', { name: 'Open existing image gallery', exact: true }); - await expect(gallery).toHaveAttribute('href', '/prompt_manager/gallery'); - await expect(await request.get('/prompt_manager/gallery')).toBeOK(); - await gallery.click(); - await expect(page).toHaveURL(/\/prompt_manager\/gallery$/); + await expect(page.locator('#workspace-main').getByRole('link', { name: 'Open Images', exact: true })).toBeVisible(); + await expect(page.locator('a[href*="/prompt_manager/admin"], a[href*="/prompt_manager/gallery"]')).toHaveCount(0); + await expect(page.locator('#workspace-main').getByText('Your existing tools remain available')).toHaveCount(0); + // The legacy pages themselves stay at their URLs. + for (const legacy of ['/prompt_manager/admin', '/prompt_manager/gallery', '/prompt_manager/gallery.html']) { + await expect(await request.get(legacy)).toBeOK(); + } +}); + +test('/prompt_manager/web lands on the workspace Library and Back does not bounce through it (#525)', async ({ page }) => { + await page.goto(`${workspace}#/overview`); + await page.evaluate(() => { location.href = '/prompt_manager/web'; }); + await expect(page).toHaveURL(/\/prompt_manager\/workspace#\/library$/); + const main = page.locator('#workspace-main'); + await expect(main.getByRole('heading', { level: 1, name: 'Prompt library', exact: true })).toBeVisible(); + await page.goBack(); + await expect(page).toHaveURL(/\/prompt_manager\/workspace#\/overview$/); + await expect(main.getByRole('heading', { level: 1, name: 'Overview', exact: true })).toBeVisible(); +}); + +// D165 (#525, maintainer ruling): a discreet line under System is the workspace's only visible way to the legacy pages. +test('System Overview links the legacy pages discreetly, and each one loads (#525)', async ({ page, context }) => { + await page.goto(`${workspace}#/system/overview`); + const line = page.locator('#workspace-main .legacy-pages'); + await expect(line).toBeVisible(); + await expect(line).toContainText('Legacy pages'); + const links = line.getByRole('link'); + await expect(links).toHaveCount(4); + for (const [name, url, ready] of [ + ['Dashboard', '/prompt_manager/admin', '#addPromptBtn'], + ['Tags', '/prompt_manager/admin#/tags', '#addPromptBtn'], + ['Gallery', '/prompt_manager/gallery', 'body'], + ['Metadata viewer', '/prompt_manager/gallery.html', 'body'], + ]) { + const opened = context.waitForEvent('page'); + await line.getByRole('link', { name, exact: true }).click(); + const tab = await opened; + await tab.waitForLoadState('domcontentloaded'); + const target = new URL(tab.url()); + expect(target.pathname + target.hash).toBe(url); + await expect(tab.locator(ready).first()).toBeAttached(); + await tab.close(); + } + // Only on System's Overview section, and gone with the page. + await page.locator('#workspace-main .section-nav').getByRole('button', { name: 'Appearance', exact: true }).click(); + await expect(line).toBeHidden(); + await page.goto(`${workspace}#/library`); + await expect(page.locator('#workspace-main .legacy-pages')).toHaveCount(0); +}); + +const bootFailure = page => page.locator('#workspace-boot-failure'); + +test('a healthy workspace never shows the load failure message (#525)', async ({ page }) => { + await page.goto(`${workspace}#/library`); + await expect(page.locator('#workspace-main h1')).toHaveText('Prompt library'); + await page.waitForFunction(() => document.readyState === 'complete'); + await expect(bootFailure(page)).toBeHidden(); +}); + +for (const [label, pattern] of [['the entry module', '**/js/workspace/index.mjs*'], ['a dependency', '**/js/workspace/state.mjs*']]) { + test(`when ${label} fails to load the page still offers the legacy pages (#525)`, async ({ page }) => { + const pageErrors = []; + page.on('pageerror', error => pageErrors.push(error.message)); + await page.route(pattern, route => route.abort()); + await page.goto(`${workspace}#/library`); + const failure = bootFailure(page); + await expect(failure).toBeVisible(); + await expect(failure).toHaveAttribute('role', 'alert'); + const dashboard = failure.getByRole('link', { name: 'Dashboard', exact: true }); + await expect(dashboard).toBeVisible(); + await expect(failure.getByRole('link')).toHaveCount(4); + await dashboard.click(); + await expect(page).toHaveURL(/\/prompt_manager\/admin$/); + expect(pageErrors).toEqual([]); + }); +} + +test('when a module throws while starting the page still offers the legacy pages (#525)', async ({ page }) => { + await page.route('**/js/workspace/state.mjs*', route => route.fulfill({ contentType: 'text/javascript', body: "throw new Error('boot failure');" })); + await page.goto(`${workspace}#/library`); + await expect(bootFailure(page).getByRole('link', { name: 'Dashboard', exact: true })).toBeVisible(); + // The broken module's own error is this test's premise, not a failure of the page. + page.workspaceErrors.length = 0; }); test('keyboard navigation, browser history and unknown routes remain usable', async ({ page }) => { @@ -438,7 +524,7 @@ test('System is native: the concept shell, section navigation, sections and the // D92: no "Sample configuration" chip. await expect(main.locator('.heading .chip')).toHaveCount(0); expect(await main.evaluate(node => [...node.children].map(child => `${child.localName}.${child.className}`))) - .toEqual(['div.heading', 'div.section-nav', 'div.settings-layout']); + .toEqual(['div.heading', 'div.section-nav', 'div.settings-layout', 'p.small legacy-pages']); const nav = main.getByRole('group', { name: 'System sections', exact: true }); await expect(nav.getByRole('button')).toHaveText(SYSTEM_SECTION_LABELS); await expect(nav.getByRole('button', { name: 'Overview', exact: true })).toHaveAttribute('aria-pressed', 'true'); diff --git a/tests/comfyui/focused-node.spec.mjs b/tests/comfyui/focused-node.spec.mjs index 1f261934..ff7bafdd 100644 --- a/tests/comfyui/focused-node.spec.mjs +++ b/tests/comfyui/focused-node.spec.mjs @@ -210,14 +210,14 @@ test(`${label} follows actual ComfyUI light and dark palettes`, async ({ page }, } }); -test(`${label} Web UI button opens actual standalone pages`, async ({ page }) => { +test(`${label} Web UI button opens the workspace; legacy pages still load`, async ({ page }) => { await addEditor(page, type); const popupPromise = page.waitForEvent('popup'); await page.locator('.pm-focused').getByRole('button', { name: 'Web UI β†—', exact: true }).click(); const popup = await popupPromise; await popup.waitForLoadState('domcontentloaded'); - await expect(popup).toHaveURL(/\/prompt_manager\/(web|admin)/); - await expect(popup.locator('body')).toContainText('Prompt'); + await expect(popup).toHaveURL(/\/prompt_manager\/workspace#\/library$/); + await expect(popup.locator('#workspace-main h1')).toHaveText('Prompt library'); await expect(page.locator('.pm-focused').getByRole('status').filter({ hasText: 'Web UI opened.' })).toHaveText('Web UI opened.'); await popup.goto(new URL('/prompt_manager/gallery', page.url()).href); await expect(popup.locator('#addPromptBtn')).toBeVisible(); diff --git a/tests/comfyui/prompt-manager.spec.mjs b/tests/comfyui/prompt-manager.spec.mjs index 07ae1ce6..29e4d005 100644 --- a/tests/comfyui/prompt-manager.spec.mjs +++ b/tests/comfyui/prompt-manager.spec.mjs @@ -84,7 +84,7 @@ test('all node schemas and extension entrypoints load from the real host', async }); for (const type of ['PromptManager', 'PromptManagerText']) { - test(`${type} Web UI button opens actual standalone pages`, async ({ page }) => { + test(`${type} Web UI button opens the workspace; legacy pages still load`, async ({ page }) => { await page.evaluate(async type => { const { app } = await import('/scripts/app.js'); const node = LiteGraph.createNode(type); @@ -95,14 +95,74 @@ for (const type of ['PromptManager', 'PromptManagerText']) { await page.getByRole('button', { name: '🌐 Web UI', exact: true }).click(); const popup = await popupPromise; await popup.waitForLoadState('domcontentloaded'); - await expect(popup).toHaveURL(/\/prompt_manager\/(web|admin)/); - await expect(popup.locator('body')).toContainText('Prompt'); + await expect(popup).toHaveURL(/\/prompt_manager\/workspace#\/library$/); + await expect(popup.locator('#workspace-main h1')).toHaveText('Prompt library'); await popup.goto(new URL('/prompt_manager/gallery', page.url()).href); await expect(popup.locator('#addPromptBtn')).toBeVisible(); await popup.close(); }); } +test('top-bar PM button opens the workspace in either renderer', async ({ page }) => { + const button = page.locator('.promptmanager-top-button-group button, #pm-toolbar-btn').first(); + await expect(button).toBeVisible(); + const popupPromise = page.waitForEvent('popup'); + await button.click(); + const popup = await popupPromise; + await expect(popup).toHaveURL(/\/prompt_manager\/workspace#\/library$/); + await expect(popup.locator('#workspace-main h1')).toHaveText('Prompt library'); + await popup.close(); +}); + +for (const [type, buttonName] of [['PromptManager', '🌐 Web UI'], ['PromptManagerFocused', 'Web UI β†—'], ['PromptManagerPair', 'Web UI β†—']]) { + test(`${type} and the PM button open the workspace after switching renderers both ways, a workflow reload and node removal`, async ({ page }, testInfo) => { + const id = await page.evaluate(async type => { + const { app } = await import('/scripts/app.js'); + const node = LiteGraph.createNode(type); + app.graph.add(node); + node.setSize([900, 800]); + app.canvas.centerOnNode(node); + return node.id; + }, type); + const webUi = page.getByRole('button', { name: buttonName, exact: true }); + const pm = page.locator('.promptmanager-top-button-group button, #pm-toolbar-btn').first(); + const opensWorkspace = async button => { + await expect(button).toHaveCount(1); + await expect(button).toBeVisible(); + const popupPromise = page.waitForEvent('popup'); + await button.click(); + const popup = await popupPromise; + await expect(popup).toHaveURL(/\/prompt_manager\/workspace#\/library$/); + await expect(popup.locator('#workspace-main h1')).toHaveText('Prompt library'); + await popup.close(); + }; + await opensWorkspace(webUi); + for (const renderer of [!testInfo.project.metadata.initialRenderer, testInfo.project.metadata.initialRenderer]) { + await page.evaluate(async renderer => { + const { app } = await import('/scripts/app.js'); + await app.ui.settings.setSettingValue('Comfy.VueNodes.Enabled', renderer); + app.canvas.ds.scale = .8; + app.canvas.centerOnNode(app.graph._nodes[0]); + }, renderer); + await expect.poll(() => page.evaluate(() => LiteGraph.vueNodesMode)).toBe(renderer); + await opensWorkspace(webUi); + await opensWorkspace(pm); + } + await page.evaluate(async () => { + const { app } = await import('/scripts/app.js'); + await app.loadGraphData(app.graph.serialize()); + }); + await opensWorkspace(webUi); + await opensWorkspace(pm); + await page.evaluate(async id => { + const { app } = await import('/scripts/app.js'); + app.graph.remove(app.graph.getNodeById(id)); + }, id); + await expect(webUi).toHaveCount(0); + await opensWorkspace(pm); + }); +} + test('Classic controls follow actual ComfyUI light and dark palettes', async ({ page }, testInfo) => { await page.evaluate(async () => { const { app } = await import('/scripts/app.js'); diff --git a/tests/comfyui/workspace.spec.mjs b/tests/comfyui/workspace.spec.mjs index c4c1b458..00d2c8d4 100644 --- a/tests/comfyui/workspace.spec.mjs +++ b/tests/comfyui/workspace.spec.mjs @@ -37,11 +37,11 @@ test('workspace serves real library data beside either ComfyUI renderer', async await expect(workspace.getByText('No matching images Β· 0 images', { exact: true })).toBeVisible(); await expect(workspace.locator('.gallery-body > .empty')).toHaveText('No matching images. Try another search or category.'); } - // D29: Images no longer links the legacy gallery; Overview's "Open existing image gallery" card does. + // D29: Images no longer links the legacy gallery; #525: nor does Overview. The page stays at its URL. await expect(workspace.locator('#workspace-main a[href="/prompt_manager/gallery"]')).toHaveCount(0); await workspace.evaluate(() => { location.hash = '/overview'; }); - const gallery = workspace.locator('#workspace-main').getByRole('link', { name: 'Open existing image gallery', exact: true }); - await expect(gallery).toHaveAttribute('href', '/prompt_manager/gallery'); + const imagesCard = workspace.locator('#workspace-main').getByRole('link', { name: 'Open Images', exact: true }); + await expect(imagesCard).toHaveAttribute('href', '#/images'); expect((await workspace.request.get('/prompt_manager/gallery')).ok()).toBe(true); await workspace.evaluate(() => { location.hash = '/images'; }); await workspace.reload(); diff --git a/tests/js/workspace-entry.test.js b/tests/js/workspace-entry.test.js new file mode 100644 index 00000000..aec1b674 --- /dev/null +++ b/tests/js/workspace-entry.test.js @@ -0,0 +1,34 @@ +// #525: where the canvas buttons open PromptManager (web/js/workspace-entry.js). +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const source = import('../../web/js/workspace-entry.js'); +const state = import('../../web/js/workspace/state.mjs'); + +test('the canvas entry route is the workspace Library, a native route', async () => { + const { WORKSPACE_ENTRY_ROUTE } = await source; + const { resolveRoute } = await state; + assert.equal(WORKSPACE_ENTRY_ROUTE, '/prompt_manager/workspace#/library'); + const [pathname, hash] = WORKSPACE_ENTRY_ROUTE.split('#'); + assert.equal(pathname, '/prompt_manager/workspace'); + assert.equal(resolveRoute(`#${hash}`).id, 'library'); +}); + +test('/prompt_manager/web redirects to the same route, under any mount prefix', async () => { + const { WORKSPACE_ENTRY_ROUTE } = await source; + const html = fs.readFileSync(path.join(__dirname, '../../web/index.html'), 'utf8'); + const [, pattern, flags, replacement] = html.match(/location\.replace\(\s*window\.location\.pathname\.replace\(\/(.+)\/([a-z]*), '([^']+)'\)/); + const redirect = pathname => pathname.replace(new RegExp(pattern, flags), replacement); + assert.equal(redirect('/prompt_manager/web'), WORKSPACE_ENTRY_ROUTE); + assert.equal(redirect('/prompt_manager/web/'), WORKSPACE_ENTRY_ROUTE); + assert.equal(redirect('/prompt_manager/index.html'), WORKSPACE_ENTRY_ROUTE); + assert.equal(redirect('/sub/prompt_manager/web'), `/sub${WORKSPACE_ENTRY_ROUTE}`); +}); + +test('no canvas entry point still names a legacy page', () => { + for (const file of ['web/comfy/prompt_manager.js', 'web/js/focused-node.js', 'web/js/classic-node.js']) { + const code = fs.readFileSync(path.join(__dirname, '../..', file), 'utf8'); + assert.doesNotMatch(code, /\/prompt_manager\/(?:admin|gallery|web)\b/, file); + } +}); diff --git a/tests/js/workspace-state.test.js b/tests/js/workspace-state.test.js index 526cbe70..6b818430 100644 --- a/tests/js/workspace-state.test.js +++ b/tests/js/workspace-state.test.js @@ -17,7 +17,7 @@ test('tags is a native route titled Tags; only unbuilt routes hand off to legacy const tags = resolveRoute('#/tags'); assert.equal(tags.label, 'Tags & categories'); assert.equal(routeTitle(tags), 'Tags'); - assert.equal(tags.href, '/prompt_manager/admin#/tags', 'Overview keeps the existing tag manager card'); + assert.equal(tags.href, '#/tags', 'Overview opens the native page (#525)'); for (const id of ['images', 'library', 'inspect', 'overview', 'system']) { const route = resolveRoute(`#/${id}`); assert.equal(routeTitle(route), route.label); @@ -48,7 +48,6 @@ test('system resolves its sections; #/overview stays the production hub', async // Q12: Overview's System card opens the native System Overview, with no legacy href or action. const card = routes.find(route => route.id === 'system'); assert.equal(card.href, '#/system/overview'); - assert.equal(card.native, true); assert.equal(card.action, 'Open System'); }); @@ -77,9 +76,28 @@ test('inspect is the native Inspect a file page with the concept description', a // The concept's heading description (app.js renderMetadata 743). assert.equal(inspect.description, 'A standalone file uses the same image viewer and prompt actions.'); assert.equal(isHandoff(inspect), false); - // Overview keeps its card for the legacy metadata page, which stays working. - assert.equal(inspect.href, '/prompt_manager/gallery.html'); - assert.equal(inspect.action, 'Open existing metadata inspector'); + // Overview's card opens the native page (#525); the legacy metadata page stays at its URL. + assert.equal(inspect.href, '#/inspect'); + assert.equal(inspect.action, 'Open Inspect a file'); +}); + +test('every Overview card opens its own native page, never a legacy one (#525)', async () => { + const { resolveRoute, routes } = await source; + const cards = routes.filter(route => route.href); + assert.deepEqual(cards.map(card => card.id), ['images', 'library', 'tags', 'inspect', 'system']); + for (const card of cards) { + assert.match(card.href, /^#\//, card.id); + assert.equal(resolveRoute(card.href).id, card.id); + assert.match(card.action, /^Open /, card.id); + assert.doesNotMatch(card.action, /existing/, card.id); + } +}); + +test('the legacy pages System links are the four that stay at their URLs (#525 D165)', async () => { + const { LEGACY_PAGES } = await source; + assert.deepEqual(LEGACY_PAGES.map(page => page.href), ['/prompt_manager/admin', '/prompt_manager/admin#/tags', + '/prompt_manager/gallery', '/prompt_manager/gallery.html']); + for (const page of LEGACY_PAGES) assert.ok(page.label); }); test('a job of the Activity page is its own deep link', async () => { diff --git a/tests/vendor/comfyui-config.test.mjs b/tests/vendor/comfyui-config.test.mjs index b77e7d8b..8b81a4ce 100644 --- a/tests/vendor/comfyui-config.test.mjs +++ b/tests/vendor/comfyui-config.test.mjs @@ -25,7 +25,8 @@ test('execution script discovers only the new matrix and preserves all 78 origin // + local Inspect host case (inspect-sender.spec.mjs, 2 projects), + the S10 renderer case (prompt-bridge.spec.mjs, 2 projects) // + #343 linked text: five node types, PromptSearchList, the pre-#343 workflows, legacy Primitive, moved link, // paste/clone/undo, wildcard Primitive, subgraph Primitive (text-link.spec.mjs, 2 projects) - assert.match(stock, /Total: 110 tests in 9 files/); + // + #525 the PM button and three nodes' Web UI after renderer switches and reload (prompt-manager.spec.mjs, 2 projects) + assert.match(stock, /Total: 118 tests in 9 files/); assert.doesNotMatch(stock, /execution-history/); assert.doesNotMatch(stock, /execution-outputs/); assert.doesNotMatch(stock, /library-location/); // it starts its own fresh host (#249 S6b) diff --git a/tools/vendor/frontend-coverage-baseline.json b/tools/vendor/frontend-coverage-baseline.json index 33a431e5..5cd0a1ac 100644 --- a/tools/vendor/frontend-coverage-baseline.json +++ b/tools/vendor/frontend-coverage-baseline.json @@ -107,6 +107,12 @@ "functions": 98, "statements": 98 }, + "workspace-entry.js": { + "lines": 80, + "branches": 80, + "functions": 80, + "statements": 80 + }, "save-to-library.js": { "lines": 98, "branches": 98, diff --git a/web/comfy/prompt_manager.js b/web/comfy/prompt_manager.js index 706c9d78..ddd36e5e 100644 --- a/web/comfy/prompt_manager.js +++ b/web/comfy/prompt_manager.js @@ -7,6 +7,7 @@ const { installFocusedNode, installSplitNode, installPairNode } = await import(a const { installPromptBridge } = await import(api.fileURL('/prompt_manager/js/prompt-bridge-host.js')); const { collapseLinkedWidget, isLinked, linkNoteText, primitiveTextValue, storedWidgetValue } = await import(api.fileURL('/prompt_manager/js/text-links.js')); const { normalizeSaveWidget } = await import(api.fileURL('/prompt_manager/js/save-to-library.js')); +const { WORKSPACE_ENTRY_ROUTE } = await import(api.fileURL('/prompt_manager/js/workspace-entry.js')); const promptManagerTypes = new Set(['PromptManager', 'PromptManagerText', 'PromptManagerFocused', 'PromptManagerSplit', 'PromptManagerPair']); // ============================================================================ @@ -109,7 +110,7 @@ function addPMTopBarButton() { // Create PM button const pmButton = $el("button", { classList: "comfyui-button comfyui-menu-mobile-collapse primary", - title: "PromptManager - Open Admin Dashboard", + title: "PromptManager - Open workspace", style: { display: "flex", alignItems: "center", @@ -125,7 +126,7 @@ function addPMTopBarButton() { transition: "background 0.2s", }, onclick: () => { - window.open(api.fileURL("/prompt_manager/admin"), "_blank"); + window.open(api.fileURL(WORKSPACE_ENTRY_ROUTE), "_blank"); }, onmouseenter: (e) => { e.target.style.background = "#0d9488"; }, onmouseleave: (e) => { e.target.style.background = "#14b8a6"; }, @@ -161,7 +162,7 @@ function addPMTopBarButtonLegacy() { const btn = document.createElement("button"); btn.id = "pm-toolbar-btn"; btn.innerText = "PM"; - btn.title = "Open PromptManager Admin Dashboard"; + btn.title = "Open PromptManager workspace"; // Teal button styling btn.style.cssText = ` @@ -181,7 +182,7 @@ function addPMTopBarButtonLegacy() { btn.onmouseleave = () => btn.style.background = "#14b8a6"; btn.onclick = () => { - window.open(api.fileURL("/prompt_manager/admin"), "_blank"); + window.open(api.fileURL(WORKSPACE_ENTRY_ROUTE), "_blank"); }; topBar.appendChild(btn); @@ -1184,9 +1185,7 @@ app.registerExtension({ // Method to open web interface nodeType.prototype.openWebInterface = function () { try { - // Open the web interface served via our API endpoint - const currentOrigin = window.location.origin; - const webUrl = `${currentOrigin}/prompt_manager/web`; + const webUrl = api.fileURL(WORKSPACE_ENTRY_ROUTE); if (this.properties.webuiDisplayMode === "newtab") { // Open in new tab without window options @@ -1198,7 +1197,8 @@ app.registerExtension({ "success", ); } else { - window.location.href = webUrl; + // Never navigate the canvas away: it may hold unsaved work. + this.showNotification("Allow popups to open Web UI, then try again.", "error"); } } else { // Default popup mode with specific dimensions @@ -1211,7 +1211,8 @@ app.registerExtension({ if (newWindow) { this.showNotification("Web interface opened in popup", "success"); } else { - window.location.href = webUrl; + // Never navigate the canvas away: it may hold unsaved work. + this.showNotification("Allow popups to open Web UI, then try again.", "error"); } } } catch (error) { diff --git a/web/index.html b/web/index.html index 57806727..4e7f7f88 100644 --- a/web/index.html +++ b/web/index.html @@ -3,7 +3,7 @@ - PromptManager - Redirecting to Admin + PromptManager - Opening the workspace