add save as tests

further simplify/share between tests
This commit is contained in:
pythongosssss
2026-03-28 10:20:15 -07:00
parent 2a30282741
commit 24f252a84b
3 changed files with 257 additions and 52 deletions

View File

@@ -53,6 +53,11 @@ export class BuilderSaveAsHelper {
.filter({ hasText: 'Close' })
}
/** The X button to dismiss the success dialog without any action. */
get dismissButton(): Locator {
return this.successDialog.locator('button.p-dialog-close-button')
}
get exitBuilderButton(): Locator {
return this.successDialog.getByRole('button', { name: 'Exit builder' })
}

View File

@@ -118,6 +118,26 @@ export class WorkflowHelper {
})
}
async getActiveWorkflowInitialMode(): Promise<string | null | undefined> {
return this.comfyPage.page.evaluate(() => {
return (window.app!.extensionManager as WorkspaceStore).workflow
.activeWorkflow?.initialMode
})
}
async getLinearModeFromGraph(): Promise<boolean | undefined> {
return this.comfyPage.page.evaluate(() => {
return window.app!.rootGraph.extra?.linearMode as boolean | undefined
})
}
async getOpenWorkflowCount(): Promise<number> {
return this.comfyPage.page.evaluate(() => {
return (window.app!.extensionManager as WorkspaceStore).workflow.workflows
.length
})
}
async isCurrentWorkflowModified(): Promise<boolean | undefined> {
return this.comfyPage.page.evaluate(() => {
return (window.app!.extensionManager as WorkspaceStore).workflow

View File

@@ -2,9 +2,59 @@ import {
comfyPageFixture as test,
comfyExpect as expect
} from '../fixtures/ComfyPage'
import type { ComfyPage } from '../fixtures/ComfyPage'
import type { AppModeHelper } from '../fixtures/helpers/AppModeHelper'
import { setupBuilder } from '../helpers/builderTestUtils'
import { fitToViewInstant } from '../helpers/fitToView'
/**
* Open the save-as dialog, fill name + view type, click save,
* and wait for the success dialog.
*/
async function builderSaveAs(
appMode: AppModeHelper,
workflowName: string,
viewType: 'App' | 'Node graph'
) {
await appMode.footer.saveAsButton.click()
await expect(appMode.saveAs.nameInput).toBeVisible({ timeout: 5000 })
await appMode.saveAs.fillAndSave(workflowName, viewType)
await expect(appMode.saveAs.successMessage).toBeVisible({ timeout: 5000 })
}
/**
* Load a different workflow, then reopen the named one from the sidebar.
* Caller must ensure the page is in graph mode (not builder or app mode)
* before calling.
*/
async function openWorkflowFromSidebar(comfyPage: ComfyPage, name: string) {
await comfyPage.workflow.loadWorkflow('default')
await comfyPage.nextFrame()
const { workflowsTab } = comfyPage.menu
await workflowsTab.open()
await workflowsTab.getPersistedItem(name).dblclick()
await comfyPage.nextFrame()
await expect(async () => {
const path = await comfyPage.workflow.getActiveWorkflowPath()
expect(path).toContain(name)
}).toPass({ timeout: 5000 })
}
/**
* After a first save, open save-as again from the chevron,
* fill name + view type, and save.
*/
async function reSaveAs(
appMode: AppModeHelper,
workflowName: string,
viewType: 'App' | 'Node graph'
) {
await appMode.footer.openSaveAsFromChevron()
await expect(appMode.saveAs.nameInput).toBeVisible({ timeout: 5000 })
await appMode.saveAs.fillAndSave(workflowName, viewType)
}
test.describe('Builder save flow', { tag: ['@ui'] }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
@@ -21,10 +71,9 @@ test.describe('Builder save flow', { tag: ['@ui'] }, () => {
})
test('Save as dialog appears for unsaved workflow', async ({ comfyPage }) => {
const { appMode } = comfyPage
const { saveAs } = appMode
const { saveAs } = comfyPage.appMode
await setupBuilder(comfyPage)
await appMode.footer.saveAsButton.click()
await comfyPage.appMode.footer.saveAsButton.click()
await expect(saveAs.dialog).toBeVisible({ timeout: 5000 })
await expect(saveAs.nameInput).toBeVisible()
@@ -35,27 +84,16 @@ test.describe('Builder save flow', { tag: ['@ui'] }, () => {
test('Save as dialog allows entering filename and saving', async ({
comfyPage
}) => {
const { appMode } = comfyPage
const { saveAs } = appMode
await setupBuilder(comfyPage)
await appMode.footer.saveAsButton.click()
await expect(saveAs.dialog).toBeVisible({ timeout: 5000 })
await saveAs.nameInput.fill(`${Date.now()} builder-save-test`)
await expect(saveAs.saveButton).toBeEnabled()
await saveAs.saveButton.click()
await expect(saveAs.successMessage).toBeVisible({ timeout: 5000 })
await builderSaveAs(comfyPage.appMode, `${Date.now()} builder-save`, 'App')
})
test('Save as dialog disables save when filename is empty', async ({
comfyPage
}) => {
const { appMode } = comfyPage
const { saveAs } = appMode
const { saveAs } = comfyPage.appMode
await setupBuilder(comfyPage)
await appMode.footer.saveAsButton.click()
await comfyPage.appMode.footer.saveAsButton.click()
await expect(saveAs.dialog).toBeVisible({ timeout: 5000 })
await saveAs.nameInput.fill('')
@@ -63,10 +101,9 @@ test.describe('Builder save flow', { tag: ['@ui'] }, () => {
})
test('View type can be toggled in save-as dialog', async ({ comfyPage }) => {
const { appMode } = comfyPage
const { saveAs } = appMode
const { saveAs } = comfyPage.appMode
await setupBuilder(comfyPage)
await appMode.footer.saveAsButton.click()
await comfyPage.appMode.footer.saveAsButton.click()
await expect(saveAs.dialog).toBeVisible({ timeout: 5000 })
@@ -80,11 +117,10 @@ test.describe('Builder save flow', { tag: ['@ui'] }, () => {
})
test('Builder step navigation works correctly', async ({ comfyPage }) => {
const { appMode } = comfyPage
const { footer } = appMode
const { footer } = comfyPage.appMode
await setupBuilder(comfyPage)
await appMode.steps.goToInputs()
await comfyPage.appMode.steps.goToInputs()
await expect(footer.backButton).toBeDisabled()
await expect(footer.nextButton).toBeEnabled()
@@ -108,37 +144,28 @@ test.describe('Builder save flow', { tag: ['@ui'] }, () => {
})
test('Exit builder button exits builder mode', async ({ comfyPage }) => {
const { appMode } = comfyPage
await setupBuilder(comfyPage)
await expect(appMode.steps.toolbar).toBeVisible()
await appMode.footer.exitBuilder()
await expect(appMode.steps.toolbar).not.toBeVisible()
await expect(comfyPage.appMode.steps.toolbar).toBeVisible()
await comfyPage.appMode.footer.exitBuilder()
await expect(comfyPage.appMode.steps.toolbar).not.toBeVisible()
})
test('Save button directly saves for previously saved workflow', async ({
comfyPage
}) => {
const { appMode } = comfyPage
const { footer, saveAs } = appMode
const { footer, saveAs } = comfyPage.appMode
await setupBuilder(comfyPage)
// First save via save-as to make it non-temporary
await footer.saveAsButton.click()
await expect(saveAs.dialog).toBeVisible({ timeout: 5000 })
await saveAs.nameInput.fill(`${Date.now()} builder-direct-save`)
await saveAs.saveButton.click()
await expect(saveAs.successMessage).toBeVisible({ timeout: 5000 })
await builderSaveAs(comfyPage.appMode, `${Date.now()} direct-save`, 'App')
await saveAs.closeButton.click()
await comfyPage.nextFrame()
// Modify the workflow so the save button becomes enabled
await appMode.steps.goToInputs()
await appMode.select.deleteInput('seed')
await comfyPage.appMode.steps.goToInputs()
await comfyPage.appMode.select.deleteInput('seed')
await expect(footer.saveButton).toBeEnabled({ timeout: 5000 })
// Now click save - should save directly without dialog
await footer.saveButton.click()
await comfyPage.nextFrame()
@@ -149,21 +176,13 @@ test.describe('Builder save flow', { tag: ['@ui'] }, () => {
test('Split button chevron opens save-as for saved workflow', async ({
comfyPage
}) => {
const { appMode } = comfyPage
const { footer, saveAs } = appMode
const { footer, saveAs } = comfyPage.appMode
await setupBuilder(comfyPage)
// First save via save-as to make it non-temporary
await footer.saveAsButton.click()
await expect(saveAs.dialog).toBeVisible({ timeout: 5000 })
await saveAs.nameInput.fill(`${Date.now()} builder-split-btn`)
await saveAs.saveButton.click()
await expect(saveAs.successMessage).toBeVisible({ timeout: 5000 })
await builderSaveAs(comfyPage.appMode, `${Date.now()} split-btn`, 'App')
await saveAs.closeButton.click()
await comfyPage.nextFrame()
// Open save-as from chevron
await footer.openSaveAsFromChevron()
await expect(saveAs.title).toBeVisible({ timeout: 5000 })
@@ -173,15 +192,176 @@ test.describe('Builder save flow', { tag: ['@ui'] }, () => {
test('Connect output popover appears when no outputs selected', async ({
comfyPage
}) => {
const { appMode } = comfyPage
await comfyPage.workflow.loadWorkflow('default')
await fitToViewInstant(comfyPage)
await appMode.enterBuilder()
await comfyPage.appMode.enterBuilder()
await appMode.footer.saveAsButton.click()
await comfyPage.appMode.footer.saveAsButton.click()
await expect(
comfyPage.page.getByText('Connect an output', { exact: false })
).toBeVisible({ timeout: 5000 })
})
test('save as app produces correct extension and linearMode', async ({
comfyPage
}) => {
await setupBuilder(comfyPage)
await builderSaveAs(comfyPage.appMode, `${Date.now()} app-ext`, 'App')
const path = await comfyPage.workflow.getActiveWorkflowPath()
expect(path).toContain('.app.json')
const linearMode = await comfyPage.workflow.getLinearModeFromGraph()
expect(linearMode).toBe(true)
})
test('save as node graph produces correct extension and linearMode', async ({
comfyPage
}) => {
await setupBuilder(comfyPage)
await builderSaveAs(
comfyPage.appMode,
`${Date.now()} graph-ext`,
'Node graph'
)
const path = await comfyPage.workflow.getActiveWorkflowPath()
expect(path).toMatch(/\.json$/)
expect(path).not.toContain('.app.json')
const linearMode = await comfyPage.workflow.getLinearModeFromGraph()
expect(linearMode).toBe(false)
})
test('save as app View App button enters app mode', async ({ comfyPage }) => {
await setupBuilder(comfyPage)
await builderSaveAs(comfyPage.appMode, `${Date.now()} app-view`, 'App')
await comfyPage.appMode.saveAs.viewAppButton.click()
await comfyPage.nextFrame()
expect(await comfyPage.workflow.getActiveWorkflowActiveAppMode()).toBe(
'app'
)
})
test('save as node graph Exit builder exits builder mode', async ({
comfyPage
}) => {
await setupBuilder(comfyPage)
await builderSaveAs(
comfyPage.appMode,
`${Date.now()} graph-exit`,
'Node graph'
)
await comfyPage.appMode.saveAs.exitBuilderButton.click()
await comfyPage.nextFrame()
await expect(comfyPage.appMode.steps.toolbar).not.toBeVisible()
})
test('save as with different mode does not modify the original workflow', async ({
comfyPage
}) => {
const { appMode } = comfyPage
await setupBuilder(comfyPage)
const originalName = `${Date.now()} original`
await builderSaveAs(appMode, originalName, 'App')
const originalPath = await comfyPage.workflow.getActiveWorkflowPath()
expect(originalPath).toContain('.app.json')
await appMode.saveAs.closeButton.click()
await comfyPage.nextFrame()
// Re-save as node graph — creates a copy
await reSaveAs(appMode, `${Date.now()} copy`, 'Node graph')
await expect(appMode.saveAs.successMessage).toBeVisible({ timeout: 5000 })
const newPath = await comfyPage.workflow.getActiveWorkflowPath()
expect(newPath).not.toBe(originalPath)
expect(newPath).not.toContain('.app.json')
// Dismiss success dialog, exit app mode, reopen the original
await appMode.saveAs.dismissButton.click()
await comfyPage.nextFrame()
await appMode.toggleAppMode()
await openWorkflowFromSidebar(comfyPage, originalName)
const linearMode = await comfyPage.workflow.getLinearModeFromGraph()
expect(linearMode).toBe(true)
})
test('save as with same name and same mode overwrites in place', async ({
comfyPage
}) => {
const { appMode } = comfyPage
const name = `${Date.now()} overwrite`
await setupBuilder(comfyPage)
await builderSaveAs(appMode, name, 'App')
await appMode.saveAs.closeButton.click()
await comfyPage.nextFrame()
const pathAfterFirst = await comfyPage.workflow.getActiveWorkflowPath()
await reSaveAs(appMode, name, 'App')
await comfyPage.nextFrame()
const pathAfterSecond = await comfyPage.workflow.getActiveWorkflowPath()
expect(pathAfterSecond).toBe(pathAfterFirst)
})
test('save as with same name but different mode creates a new file', async ({
comfyPage
}) => {
const { appMode } = comfyPage
const name = `${Date.now()} mode-change`
await setupBuilder(comfyPage)
await builderSaveAs(appMode, name, 'App')
const pathAfterFirst = await comfyPage.workflow.getActiveWorkflowPath()
expect(pathAfterFirst).toContain('.app.json')
await appMode.saveAs.closeButton.click()
await comfyPage.nextFrame()
await reSaveAs(appMode, name, 'Node graph')
await expect(appMode.saveAs.successMessage).toBeVisible({ timeout: 5000 })
const pathAfterSecond = await comfyPage.workflow.getActiveWorkflowPath()
expect(pathAfterSecond).not.toBe(pathAfterFirst)
expect(pathAfterSecond).toMatch(/\.json$/)
expect(pathAfterSecond).not.toContain('.app.json')
})
test('save as app workflow reloads in app mode', async ({ comfyPage }) => {
const name = `${Date.now()} reload-app`
await setupBuilder(comfyPage)
await builderSaveAs(comfyPage.appMode, name, 'App')
await comfyPage.appMode.saveAs.dismissButton.click()
await comfyPage.nextFrame()
await comfyPage.appMode.footer.exitBuilder()
await openWorkflowFromSidebar(comfyPage, name)
const mode = await comfyPage.workflow.getActiveWorkflowInitialMode()
expect(mode).toBe('app')
})
test('save as node graph workflow reloads in node graph mode', async ({
comfyPage
}) => {
const name = `${Date.now()} reload-graph`
await setupBuilder(comfyPage)
await builderSaveAs(comfyPage.appMode, name, 'Node graph')
await comfyPage.appMode.saveAs.dismissButton.click()
await comfyPage.nextFrame()
await comfyPage.appMode.toggleAppMode()
await openWorkflowFromSidebar(comfyPage, name)
const mode = await comfyPage.workflow.getActiveWorkflowInitialMode()
expect(mode).toBe('graph')
})
})