mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-24 00:09:32 +00:00
[Beta Menu] Shows unsaved state on browser tab title (#860)
* [Beta Menu] Shows unsaved state on browser tab title * Proper state management * Add playwright test * Fix browser tests
This commit is contained in:
@@ -108,10 +108,29 @@ class NodeLibrarySidebarTab {
|
|||||||
class ComfyMenu {
|
class ComfyMenu {
|
||||||
public readonly sideToolbar: Locator
|
public readonly sideToolbar: Locator
|
||||||
public readonly themeToggleButton: Locator
|
public readonly themeToggleButton: Locator
|
||||||
|
public readonly saveButton: Locator
|
||||||
|
|
||||||
constructor(public readonly page: Page) {
|
constructor(public readonly page: Page) {
|
||||||
this.sideToolbar = page.locator('.side-tool-bar-container')
|
this.sideToolbar = page.locator('.side-tool-bar-container')
|
||||||
this.themeToggleButton = page.locator('.comfy-vue-theme-toggle')
|
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() {
|
get nodeLibraryTab() {
|
||||||
@@ -178,6 +197,10 @@ export class ComfyPage {
|
|||||||
|
|
||||||
async setup() {
|
async setup() {
|
||||||
await this.goto()
|
await this.goto()
|
||||||
|
await this.page.evaluate(() => {
|
||||||
|
localStorage.clear()
|
||||||
|
sessionStorage.clear()
|
||||||
|
})
|
||||||
// Unify font for consistent screenshots.
|
// Unify font for consistent screenshots.
|
||||||
await this.page.addStyleTag({
|
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'
|
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'
|
||||||
|
|||||||
55
browser_tests/browserTabTitle.spec.ts
Normal file
55
browser_tests/browserTabTitle.spec.ts
Normal file
@@ -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')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -11,6 +11,7 @@ import { useWorkflowStore } from '@/stores/workflowStore'
|
|||||||
import { useTitle } from '@vueuse/core'
|
import { useTitle } from '@vueuse/core'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
const DEFAULT_TITLE = 'ComfyUI'
|
||||||
const executionStore = useExecutionStore()
|
const executionStore = useExecutionStore()
|
||||||
const executionText = computed(() =>
|
const executionText = computed(() =>
|
||||||
executionStore.isIdle ? '' : `[${executionStore.executionProgress}%]`
|
executionStore.isIdle ? '' : `[${executionStore.executionProgress}%]`
|
||||||
@@ -22,12 +23,18 @@ const betaMenuEnabled = computed(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const workflowStore = useWorkflowStore()
|
const workflowStore = useWorkflowStore()
|
||||||
const workflowNameText = computed(
|
const isUnsavedText = computed(() =>
|
||||||
() =>
|
workflowStore.previousWorkflowUnsaved ? ' *' : ''
|
||||||
(betaMenuEnabled.value ? workflowStore.activeWorkflow?.name : undefined) ??
|
|
||||||
'ComfyUI'
|
|
||||||
)
|
)
|
||||||
|
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)
|
useTitle(title)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { ComfyAsyncDialog } from '../components/asyncDialog'
|
|||||||
import { trimJsonExt } from '@/utils/formatUtil'
|
import { trimJsonExt } from '@/utils/formatUtil'
|
||||||
import type { ComfyApp } from '@/scripts/app'
|
import type { ComfyApp } from '@/scripts/app'
|
||||||
import type { ComfyComponent } from '../components'
|
import type { ComfyComponent } from '../components'
|
||||||
|
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||||
|
|
||||||
export class ComfyWorkflowsMenu {
|
export class ComfyWorkflowsMenu {
|
||||||
#first = true
|
#first = true
|
||||||
@@ -68,7 +69,11 @@ export class ComfyWorkflowsMenu {
|
|||||||
this.unsaved = prop(this, 'unsaved', classList.unsaved, (v) => {
|
this.unsaved = prop(this, 'unsaved', classList.unsaved, (v) => {
|
||||||
classList.unsaved = v
|
classList.unsaved = v
|
||||||
this.button.classList = classList
|
this.button.classList = classList
|
||||||
setStorageValue('Comfy.PreviousWorkflowUnsaved', v)
|
setStorageValue('Comfy.PreviousWorkflowUnsaved', String(v))
|
||||||
|
|
||||||
|
if (this.app.vueAppReady) {
|
||||||
|
useWorkflowStore().previousWorkflowUnsaved = v
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ export function prop<T>(
|
|||||||
return defaultValue
|
return defaultValue
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getStorageValue(id) {
|
export function getStorageValue(id: string) {
|
||||||
const clientId = api.clientId ?? api.initialClientId
|
const clientId = api.clientId ?? api.initialClientId
|
||||||
return (
|
return (
|
||||||
(clientId && sessionStorage.getItem(`${id}:${clientId}`)) ??
|
(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
|
const clientId = api.clientId ?? api.initialClientId
|
||||||
if (clientId) {
|
if (clientId) {
|
||||||
sessionStorage.setItem(`${id}:${clientId}`, value)
|
sessionStorage.setItem(`${id}:${clientId}`, value)
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { ComfyWorkflow } from '@/scripts/workflows'
|
import { ComfyWorkflow } from '@/scripts/workflows'
|
||||||
|
import { getStorageValue } from '@/scripts/utils'
|
||||||
|
|
||||||
export const useWorkflowStore = defineStore('workflow', () => {
|
export const useWorkflowStore = defineStore('workflow', () => {
|
||||||
const activeWorkflow = ref<ComfyWorkflow | null>(null)
|
const activeWorkflow = ref<ComfyWorkflow | null>(null)
|
||||||
|
const previousWorkflowUnsaved = ref<boolean>(
|
||||||
|
Boolean(getStorageValue('Comfy.PreviousWorkflowUnsaved'))
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
activeWorkflow
|
activeWorkflow,
|
||||||
|
previousWorkflowUnsaved
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ export function appendJsonExt(path: string) {
|
|||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
export function trimJsonExt(path: string) {
|
export function trimJsonExt(path?: string) {
|
||||||
return path.replace(/\.json$/, '')
|
return path?.replace(/\.json$/, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
export function highlightQuery(text: string, query: string) {
|
export function highlightQuery(text: string, query: string) {
|
||||||
|
|||||||
Reference in New Issue
Block a user