Files
ComfyUI_frontend/browser_tests/tests/dialog.spec.ts
AustinMroz 988a546721 Add missing dialog tests (#11133)
Leveraging the fancy coverage functionality of #10930, this PR aims to
add coverage to missing dialogue models.

This has proven quite constructive as many of the dialogues have since
been shown to be bugged.
- The APINodes sign in dialog that displays when attempting to run a
workflow containing Partner nodes while not logged in was intended to
display a list of nodes required to execute the workflow. The import for
this component was forgotten in the original commit (#3532) and the
backing component was later knipped
- Error dialogs resulting are intended to display the file responsible
for the error, but the prop was accidentally left out during the
refactoring of #3265
- ~~The node library migration (#8548) failed to include the 'Edit
Blueprint' button, and had incorrect sizing and color on the 'Delete
Blueprint' button.~~
- On request, the library button changes were spun out to a separate PR

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11133-Add-missing-dialog-tests-33e6d73d3650812cb142d610461adcd4)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-04-14 14:31:31 -07:00

316 lines
11 KiB
TypeScript

import { expect } from '@playwright/test'
import type { Keybinding } from '@/platform/keybindings/types'
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
import { SignInDialog } from '@e2e/fixtures/components/SignInDialog'
import { ApiSignin } from '@e2e/fixtures/components/ApiSignin'
import { CloudNotification } from '@e2e/fixtures/components/CloudNotification'
import { UpdatePassword } from '@e2e/fixtures/components/UpdatePassword'
import { DefaultGraphPositions } from '@e2e/fixtures/constants/defaultGraphPositions'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Settings', () => {
test('@mobile Should be visible on mobile', async ({ comfyPage }) => {
await comfyPage.page.keyboard.press('Control+,')
const settingsDialog = comfyPage.page.getByTestId('settings-dialog')
await expect(settingsDialog).toBeVisible()
const contentArea = settingsDialog.locator('main')
await expect(contentArea).toBeVisible()
await expect
.poll(() => contentArea.evaluate((el) => el.clientHeight))
.toBeGreaterThan(30)
})
test('Can open settings with hotkey', async ({ comfyPage }) => {
await comfyPage.page.keyboard.down('ControlOrMeta')
await comfyPage.page.keyboard.press(',')
await comfyPage.page.keyboard.up('ControlOrMeta')
const settingsLocator = comfyPage.page.getByTestId('settings-dialog')
await expect(settingsLocator).toBeVisible()
await comfyPage.page.keyboard.press('Escape')
await expect(settingsLocator).toBeHidden()
})
test('Can change canvas zoom speed setting', async ({ comfyPage }) => {
const maxSpeed = 2.5
await comfyPage.settings.setSetting('Comfy.Graph.ZoomSpeed', maxSpeed)
await test.step('Setting should persist', async () => {
await expect
.poll(() => comfyPage.settings.getSetting('Comfy.Graph.ZoomSpeed'))
.toBe(maxSpeed)
})
})
test('Should persist keybinding setting', async ({ comfyPage }) => {
// Open the settings dialog
await comfyPage.page.keyboard.press('Control+,')
// Open the keybinding tab
const settingsDialog = comfyPage.page.getByTestId('settings-dialog')
await expect(settingsDialog).toBeVisible()
await settingsDialog
.locator('nav [role="button"]', { hasText: 'Keybinding' })
.click()
await expect(
comfyPage.page.getByPlaceholder('Search Keybindings...')
).toBeVisible()
// Focus the 'New Blank Workflow' row
const newBlankWorkflowRow = comfyPage.page.locator('tr', {
has: comfyPage.page.getByRole('cell', { name: 'New Blank Workflow' })
})
await newBlankWorkflowRow.click()
// Click add keybinding button (New Blank Workflow has no default keybinding)
const addKeybindingButton = newBlankWorkflowRow.locator(
'.icon-\\[lucide--plus\\]'
)
await addKeybindingButton.click()
// Set new keybinding
const input = comfyPage.page.getByPlaceholder('Enter your keybind')
await input.press('Alt+n')
const requestPromise = comfyPage.page.waitForRequest(
(req) =>
req.url().includes('/api/settings') &&
!req.url().includes('/api/settings/') &&
req.method() === 'POST'
)
// Save keybinding
const saveButton = comfyPage.page
.getByLabel('Modify keybinding')
.getByText('Save')
await saveButton.click()
const request = await requestPromise
const expectedSetting: Keybinding = {
commandId: 'Comfy.NewBlankWorkflow',
combo: {
key: 'n',
ctrl: false,
alt: true,
shift: false
}
}
expect(request.postData()).toContain(JSON.stringify(expectedSetting))
})
})
test.describe('Support', () => {
test('Should open external zendesk link with OSS tag', async ({
comfyPage
}) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
// Prevent loading the external page
await comfyPage.page
.context()
.route('https://support.comfy.org/**', (route) =>
route.fulfill({ body: '<html></html>', contentType: 'text/html' })
)
const popupPromise = comfyPage.page.waitForEvent('popup')
await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Support'])
const popup = await popupPromise
const url = new URL(popup.url())
expect(url.hostname).toBe('support.comfy.org')
expect(url.searchParams.get('tf_42243568391700')).toBe('oss')
await popup.close()
})
})
test.describe('Signin dialog', () => {
test('Paste content to signin dialog should not paste node on canvas', async ({
comfyPage
}) => {
const nodeNum = await comfyPage.nodeOps.getNodeCount()
await comfyPage.canvas.click({
position: DefaultGraphPositions.emptyLatentWidgetClick
})
await comfyPage.page.mouse.move(10, 10)
await comfyPage.nextFrame()
await comfyPage.clipboard.copy()
const textBox = comfyPage.widgetTextBox
await textBox.click()
await textBox.fill('test_password')
await textBox.press('Control+a')
await textBox.press('Control+c')
await comfyPage.page.evaluate(() => {
void window.app!.extensionManager.dialog.showSignInDialog()
})
const input = comfyPage.page.locator('#comfy-org-sign-in-password')
await input.waitFor({ state: 'visible' })
await input.press('Control+v')
await expect(input).toHaveValue('test_password')
await expect.poll(() => comfyPage.nodeOps.getNodeCount()).toBe(nodeNum)
})
test('Sign-in dialog resolves true on login', async ({ comfyPage }) => {
const dialog = new SignInDialog(comfyPage.page)
const { result: dialogResult } = await dialog.openWithResult()
await dialog.emailInput.fill('test@example.com')
await dialog.passwordInput.fill('TestPassword123!')
await expect(dialog.root).toBeVisible()
await dialog.signInButton.click()
await expect(dialog.root).toBeHidden()
expect(await dialogResult).toBe(true)
})
test('Sign-in dialog resolves false when closed without sign-in', async ({
comfyPage
}) => {
const dialog = new SignInDialog(comfyPage.page)
const { result: dialogResult } = await dialog.openWithResult()
await dialog.close()
await expect(dialog.root).toBeHidden()
expect(await dialogResult).toBe(false)
})
})
test('API Nodes sign-in dialog', async ({ comfyPage }) => {
const dialog = new ApiSignin(comfyPage.page)
const { result: dialogResult } = await dialog.open([
'FluxProGenerate',
'StableDiffusion3Generate'
])
await expect(dialog.root.getByText('FluxProGenerate')).toBeVisible()
await expect(dialog.root.getByText('StableDiffusion3Generate')).toBeVisible()
await dialog.cancel.click()
await expect(dialog.root).toBeHidden()
expect(await dialogResult).toBe(false)
})
test.describe('Update password dialog', () => {
test('Should only allow submission when inputs are valid', async ({
comfyPage
}) => {
const dialog = new UpdatePassword(comfyPage.page)
await dialog.open()
await dialog.confirm.click()
await expect(dialog.root, 'Check that password exists').toBeVisible()
const testPassword = 'Unguessable Password #2'
await dialog.password.fill(testPassword)
await dialog.confirm.click()
await expect(dialog.root, 'Check that inputs match').toBeVisible()
await dialog.confirmPassword.fill(testPassword)
await dialog.confirm.click()
await expect(dialog.root, 'Dialog closes after submission').toBeHidden()
})
})
test.describe('Cloud notification dialog', () => {
test('Should display cloud notification and navigate to comfy.org on Explore', async ({
comfyPage
}) => {
const dialog = new CloudNotification(comfyPage.page)
await dialog.open()
await expect(
dialog.root.getByText('Run ComfyUI in the Cloud')
).toBeVisible()
const popupPromise = comfyPage.page.waitForEvent('popup')
await dialog.toCloud.click()
const popup = await popupPromise
expect(new URL(popup.url()).hostname).toContain('comfy.org')
await popup.close()
await expect(dialog.root).toBeHidden()
})
test('Should close when Continue Locally is clicked', async ({
comfyPage
}) => {
const dialog = new CloudNotification(comfyPage.page)
await dialog.open()
await dialog.back.click()
await expect(dialog.root).toBeHidden()
})
})
test('Blueprint overwrite', { tag: ['@subgraph'] }, async ({ comfyPage }) => {
const blueprintName = `test-blueprint-overwrite-${Date.now()}`
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.settings.setSetting(
'Comfy.Workflow.WarnBlueprintOverwrite',
true
)
const tab = comfyPage.menu.nodeLibraryTabV2
await test.step('Publish a basic subgraph', async () => {
const ksampler = await comfyPage.vueNodes.getFixtureByTitle('KSampler')
await comfyPage.contextMenu.openForVueNode(ksampler.header)
await comfyPage.contextMenu.clickMenuItemExact('Convert to Subgraph')
await comfyPage.subgraph.publishSubgraph(blueprintName)
await tab.open()
await tab.getFolder('My Blueprints').click()
await tab.getFolder('User').click()
})
const steps = comfyPage.vueNodes.getWidgetByName('KSampler', 'steps')
await test.step('Edit the published subgraph', async () => {
const blueprintNode = tab.getNode(blueprintName)
await expect(blueprintNode, 'blueprint visible in library').toBeVisible()
await blueprintNode.getByRole('button', { name: 'Edit' }).click()
await steps.waitFor({ state: 'visible' })
})
const confirmDialog = comfyPage.confirmDialog.root
const { incrementButton } = comfyPage.vueNodes.getInputNumberControls(steps)
const dirtyGraphAndSave = async () => {
await incrementButton.click()
await comfyPage.page.keyboard.press('Control+s')
}
await test.step('No dialog: user prompted on publish', async () => {
await dirtyGraphAndSave()
await comfyPage.nextFrame()
await expect(confirmDialog).toBeHidden()
})
await test.step('Should show dialog', async () => {
await comfyPage.subgraph.setSaveUnpromptedOnActiveBlueprint()
await dirtyGraphAndSave()
const { noWarnOverwriteToggle } = comfyPage.confirmDialog
await expect(noWarnOverwriteToggle).toBeVisible()
await test.step('Disable overwrite warning', async () => {
await noWarnOverwriteToggle.check()
await expect(confirmDialog.getByText(/Re-enable in/i)).toBeVisible()
await comfyPage.confirmDialog.click('overwrite')
})
})
await test.step('No dialog: disabled by setting', async () => {
await comfyPage.subgraph.setSaveUnpromptedOnActiveBlueprint()
await dirtyGraphAndSave()
await comfyPage.nextFrame()
await expect(confirmDialog).toBeHidden()
})
})