diff --git a/browser_tests/ComfyPage.ts b/browser_tests/ComfyPage.ts index 3d8deeab2..bb807c955 100644 --- a/browser_tests/ComfyPage.ts +++ b/browser_tests/ComfyPage.ts @@ -108,10 +108,29 @@ class NodeLibrarySidebarTab { class ComfyMenu { public readonly sideToolbar: Locator public readonly themeToggleButton: Locator + public readonly saveButton: Locator constructor(public readonly page: Page) { this.sideToolbar = page.locator('.side-tool-bar-container') this.themeToggleButton = page.locator('.comfy-vue-theme-toggle') + this.saveButton = page + .locator('button[title="Save the current workflow"]') + .nth(0) + } + + async saveWorkflow(name: string) { + const acceptDialog = async (dialog) => { + await dialog.accept(name) + } + this.page.on('dialog', acceptDialog) + + await this.saveButton.click() + + // Wait a moment to ensure the dialog has been handled + await this.page.waitForTimeout(300) + + // Remove the dialog listener + this.page.off('dialog', acceptDialog) } get nodeLibraryTab() { @@ -178,6 +197,10 @@ export class ComfyPage { async setup() { await this.goto() + await this.page.evaluate(() => { + localStorage.clear() + sessionStorage.clear() + }) // Unify font for consistent screenshots. await this.page.addStyleTag({ url: 'https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap' diff --git a/browser_tests/browserTabTitle.spec.ts b/browser_tests/browserTabTitle.spec.ts new file mode 100644 index 000000000..d8944cd03 --- /dev/null +++ b/browser_tests/browserTabTitle.spec.ts @@ -0,0 +1,55 @@ +import { expect } from '@playwright/test' +import { comfyPageFixture as test } from './ComfyPage' + +test.describe('Browser tab title', () => { + test.describe('Beta Menu', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + }) + + test.afterEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + }) + + test('Can display workflow name', async ({ comfyPage }) => { + const workflowName = await comfyPage.page.evaluate(async () => { + return window['app'].workflowManager.activeWorkflow.name + }) + // Note: unsaved workflow name is always prepended with "*". + expect(await comfyPage.page.title()).toBe(`*${workflowName}`) + }) + + test('Can display workflow name with unsaved changes', async ({ + comfyPage + }) => { + const workflowName = await comfyPage.page.evaluate(async () => { + return window['app'].workflowManager.activeWorkflow.name + }) + // Note: unsaved workflow name is always prepended with "*". + expect(await comfyPage.page.title()).toBe(`*${workflowName}`) + + await comfyPage.menu.saveWorkflow('test') + expect(await comfyPage.page.title()).toBe('test') + + const textBox = comfyPage.widgetTextBox + await textBox.fill('Hello World') + await comfyPage.clickEmptySpace() + expect(await comfyPage.page.title()).toBe(`*test`) + + // Delete the saved workflow for cleanup. + await comfyPage.page.evaluate(async () => { + window['app'].workflowManager.activeWorkflow.delete() + }) + }) + }) + + test.describe('Legacy Menu', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + }) + + test('Can display default title', async ({ comfyPage }) => { + expect(await comfyPage.page.title()).toBe('ComfyUI') + }) + }) +}) diff --git a/src/components/BrowserTabTitle.vue b/src/components/BrowserTabTitle.vue index f5289dd2a..a8348aa57 100644 --- a/src/components/BrowserTabTitle.vue +++ b/src/components/BrowserTabTitle.vue @@ -11,6 +11,7 @@ import { useWorkflowStore } from '@/stores/workflowStore' import { useTitle } from '@vueuse/core' import { computed } from 'vue' +const DEFAULT_TITLE = 'ComfyUI' const executionStore = useExecutionStore() const executionText = computed(() => executionStore.isIdle ? '' : `[${executionStore.executionProgress}%]` @@ -22,12 +23,18 @@ const betaMenuEnabled = computed( ) const workflowStore = useWorkflowStore() -const workflowNameText = computed( - () => - (betaMenuEnabled.value ? workflowStore.activeWorkflow?.name : undefined) ?? - 'ComfyUI' +const isUnsavedText = computed(() => + workflowStore.previousWorkflowUnsaved ? ' *' : '' ) +const workflowNameText = computed(() => { + const workflowName = workflowStore.activeWorkflow?.name + return workflowName ? isUnsavedText.value + workflowName : DEFAULT_TITLE +}) -const title = computed(() => executionText.value + workflowNameText.value) +const title = computed( + () => + executionText.value + + (betaMenuEnabled.value ? workflowNameText.value : DEFAULT_TITLE) +) useTitle(title) diff --git a/src/scripts/ui/menu/workflows.ts b/src/scripts/ui/menu/workflows.ts index 33478a58c..f3d044c81 100644 --- a/src/scripts/ui/menu/workflows.ts +++ b/src/scripts/ui/menu/workflows.ts @@ -9,6 +9,7 @@ import { ComfyAsyncDialog } from '../components/asyncDialog' import { trimJsonExt } from '@/utils/formatUtil' import type { ComfyApp } from '@/scripts/app' import type { ComfyComponent } from '../components' +import { useWorkflowStore } from '@/stores/workflowStore' export class ComfyWorkflowsMenu { #first = true @@ -68,7 +69,11 @@ export class ComfyWorkflowsMenu { this.unsaved = prop(this, 'unsaved', classList.unsaved, (v) => { classList.unsaved = v this.button.classList = classList - setStorageValue('Comfy.PreviousWorkflowUnsaved', v) + setStorageValue('Comfy.PreviousWorkflowUnsaved', String(v)) + + if (this.app.vueAppReady) { + useWorkflowStore().previousWorkflowUnsaved = v + } }) } diff --git a/src/scripts/utils.ts b/src/scripts/utils.ts index 6b349f8a8..27648c3a3 100644 --- a/src/scripts/utils.ts +++ b/src/scripts/utils.ts @@ -153,7 +153,7 @@ export function prop( return defaultValue } -export function getStorageValue(id) { +export function getStorageValue(id: string) { const clientId = api.clientId ?? api.initialClientId return ( (clientId && sessionStorage.getItem(`${id}:${clientId}`)) ?? @@ -161,7 +161,7 @@ export function getStorageValue(id) { ) } -export function setStorageValue(id, value) { +export function setStorageValue(id: string, value: string) { const clientId = api.clientId ?? api.initialClientId if (clientId) { sessionStorage.setItem(`${id}:${clientId}`, value) diff --git a/src/stores/workflowStore.ts b/src/stores/workflowStore.ts index ad81ae410..4e34342ba 100644 --- a/src/stores/workflowStore.ts +++ b/src/stores/workflowStore.ts @@ -1,11 +1,16 @@ import { defineStore } from 'pinia' import { ref } from 'vue' import { ComfyWorkflow } from '@/scripts/workflows' +import { getStorageValue } from '@/scripts/utils' export const useWorkflowStore = defineStore('workflow', () => { const activeWorkflow = ref(null) + const previousWorkflowUnsaved = ref( + Boolean(getStorageValue('Comfy.PreviousWorkflowUnsaved')) + ) return { - activeWorkflow + activeWorkflow, + previousWorkflowUnsaved } }) diff --git a/src/utils/formatUtil.ts b/src/utils/formatUtil.ts index 496c64ae6..da1c103ff 100644 --- a/src/utils/formatUtil.ts +++ b/src/utils/formatUtil.ts @@ -30,8 +30,8 @@ export function appendJsonExt(path: string) { return path } -export function trimJsonExt(path: string) { - return path.replace(/\.json$/, '') +export function trimJsonExt(path?: string) { + return path?.replace(/\.json$/, '') } export function highlightQuery(text: string, query: string) {