Files
ComfyUI_frontend/browser_tests/tests/dialogs/publishDialog.spec.ts
Dante 8108967d49 feat(dialog): migrate Prompt + Confirmation dialogs to Reka-UI (Phase 1) (#12041)
## Summary

Phase 1 of the dialog migration kicked off in #11719. Migrates the two
simplest production dialogs — `PromptDialogContent` and
`ConfirmationDialogContent` — from PrimeVue `Dialog` onto the Reka-UI
primitives landed in Phase 0. Public API of `useDialogService` /
`dialogStore` is unchanged.

Parent:
[FE-571](https://linear.app/comfyorg/issue/FE-571/dialog-system-migration-primevue-reka-ui-parent)
This phase:
[FE-573](https://linear.app/comfyorg/issue/FE-573/phase-1-migrate-promptdialog-confirmationdialog-closes-11688)
Predecessor: #11719 (merged at `0788e7139`)

Refs #11688 (closed manually after Phase 0; the actual user-visible
max-width fix ships in this PR)

## Changes

### `src/services/dialogService.ts`
| Call site | Renderer | Size | Width override |
| --- | --- | --- | --- |
| `prompt()` | `'reka'` | `md` | — |
| `confirm()` | `'reka'` | `md` | — |
| `showBillingComingSoonDialog()` | `'reka'` | `sm` | `contentClass:
'max-w-[360px]'` |

### `src/components/dialog/content/ConfirmationDialogContent.vue`
- Drops `import Message from 'primevue/message'` — the only PrimeVue
dependency in the component
- Replaces `<Message>` with a Tailwind `role="status"` alert keeping the
`pi pi-info-circle` icon and muted-foreground severity

### `src/stores/dialogStore.ts` +
`src/components/dialog/GlobalDialog.vue`
- Adds `contentClass?: HTMLAttributes['class']` on
`CustomDialogComponentProps`
- Forwards it to `<DialogContent :class="...">` on the Reka branch
(PrimeVue path keeps using `pt`)

## Why this scope

1. **Smallest content surface** — `PromptDialogContent` is 43 LOC; the
only PrimeVue dependency in `ConfirmationDialogContent` is the
`<Message>` info banner.
2. **Closes #11688 ergonomics** — Reka's `md` size = `max-w-xl` (576px /
36rem), exactly the max-width the issue reporter asked for.
3. **Three known callers** — all in `dialogService.ts`. No other callers
needed to change.
4. **Renderer branch is already proven by Phase 0**; this PR just flips
the flag.

## Visual proof

Verified live in Storybook (`Components / Dialog / Dialog → Default` and
`… → All Sizes`) at viewport `1920×1080`. DOM inspection confirms the
rendered widths match the design intent:

| Story | size | Rendered width | Computed `max-width` |
| --- | --- | --- | --- |
| `Default` | `md` | **576 px** | **576 px (= 36rem)** |
| `All Sizes` (sm slot) | `sm` | 384 px | 384 px (= 24rem) |

The `md` measurement directly answers the #11688 reporter screenshot
(1558 px wide PrimeVue dialog → 576 px Reka dialog on the same display).
Local screenshot artifacts (not committed):
`temp/screenshots/phase1-md-576px-1920w.png`,
`temp/screenshots/phase1-md-allsizes-1920w.png`,
`temp/screenshots/phase1-sm-384px-1920w.png` — drag-drop into the PR
body before marking ready for review.

## Quality gates

- [x] `pnpm typecheck` — clean
- [x] `pnpm lint` — clean
- [x] `pnpm format` — applied (oxfmt)
- [x] `pnpm test:unit` (touched files): **26/26 passed**
- `ConfirmationDialogContent.test.ts` (9 tests, no longer needs PrimeVue
plugin)
  - `PromptDialogContent.test.ts` (5 tests, unchanged)
- `GlobalDialog.test.ts` (9 tests, Phase 0 coverage still passes after
the contentClass forwarder addition)
- `dialogService.renderer.test.ts` **new** — 3 tests asserting each call
site sets `renderer: 'reka'` (regression net)
- [ ] `pnpm test:browser:local --grep "@mobile confirm dialog"` —
**could not run locally** (no ComfyUI Python backend on `localhost:8188`
in this session); CI will gate the existing fixture, which is already
renderer-agnostic (`getByRole('dialog')` + `getByRole('button', ...)` in
`browser_tests/fixtures/components/ConfirmDialog.ts`).

## Public API impact

None. `useDialogService().prompt(...)` / `confirm(...)` /
`showBillingComingSoonDialog(...)` keep their existing signatures.
Custom-node extensions calling `app.extensionManager.dialog.*` continue
to work.

## Out of scope (later phases)

- `ErrorDialogContent`, `NodeSearchBox`, `SecretFormDialog`,
`VideoHelpDialog`, `CustomizationDialog` — Phase 2 (FE-574)
- Settings dialog — Phase 3 (FE-575)
- Manager dialog — Phase 4 (FE-576)
- `ConfirmDialog` callers (`SecretsPanel`, `BaseWorkflowsSidebarTab`) —
Phase 5 (FE-577)
- Removing PrimeVue `Dialog` imports + `<style>` cleanup in
`GlobalDialog.vue` — Phase 6 (FE-578)
- Legacy `ComfyDialog` (`src/scripts/ui/dialog.ts`)
- Deduplicating `Dialogue.vue` / `ImageLightbox.vue`


## Screenshot
<img width="865" height="497" alt="Screenshot 2026-05-08 at 4 35 45 PM"
src="https://github.com/user-attachments/assets/6aead2ad-2e0b-478a-9154-bb632a6bf3d1"
/>
<img width="1363" height="964" alt="Screenshot 2026-05-08 at 4 38 16 PM"
src="https://github.com/user-attachments/assets/10647752-a063-4901-a206-842799cc5d7a"
/>
<img width="889" height="486" alt="Screenshot 2026-05-08 at 4 46 57 PM"
src="https://github.com/user-attachments/assets/81899a81-205a-46f2-bddd-7639624607f6"
/>



## Test plan

- [x] Unit: 26/26 pass on touched files
- [ ] CI: `@mobile confirm dialog` spec on the migrated path
- [ ] Manual (post-CI on a real backend): open prompt and confirm
dialogs on 1920×1080 viewport, verify ≤ 36rem max-width, ESC closes,
backdrop click closes, Enter submits prompt, focus trap holds
- [ ] Manual: open Billing Coming Soon dialog — verify it stays at the
existing `max-w-[360px]` width
2026-05-08 12:11:06 +00:00

307 lines
10 KiB
TypeScript

import { expect } from '@playwright/test'
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
import type { PublishDialog } from '@e2e/fixtures/components/PublishDialog'
import { publishFixture as test } from '@e2e/fixtures/helpers/PublishApiHelper'
const PUBLISH_FEATURE_FLAGS = {
comfyhub_upload_enabled: true,
comfyhub_profile_gate_enabled: true
} as const
async function saveAndOpenPublishDialog(
comfyPage: ComfyPage,
dialog: PublishDialog,
workflowName: string
): Promise<void> {
await comfyPage.menu.topbar.saveWorkflow(workflowName)
const overwriteDialog = comfyPage.page
.getByRole('dialog')
.filter({ hasText: 'Overwrite' })
// Bounded wait: point-in-time isVisible() can miss dialogs that open
// slightly after saveWorkflow() resolves.
try {
await overwriteDialog.waitFor({ state: 'visible', timeout: 500 })
await comfyPage.confirmDialog.click('overwrite')
} catch {
// No overwrite dialog — workflow name was unique.
}
await dialog.open()
}
test.describe('Publish dialog - wizard navigation', () => {
test.beforeEach(async ({ comfyPage, publishApi, publishDialog }) => {
await comfyPage.featureFlags.setFlags(PUBLISH_FEATURE_FLAGS)
await publishApi.setupDefaultMocks()
await saveAndOpenPublishDialog(comfyPage, publishDialog, 'test-publish-wf')
})
test('opens on the Describe step by default', async ({ publishDialog }) => {
await expect(publishDialog.describeStep).toBeVisible()
await expect(publishDialog.nameInput).toBeVisible()
await expect(publishDialog.descriptionTextarea).toBeVisible()
})
test('pre-fills workflow name from active workflow', async ({
publishDialog
}) => {
await expect(publishDialog.nameInput).toHaveValue(/test-publish-wf/)
})
test('Next button navigates to Examples step', async ({ publishDialog }) => {
await publishDialog.goNext()
await expect(publishDialog.describeStep).toBeHidden()
// Examples step should show thumbnail toggle and upload area
await expect(
publishDialog.root.getByText('Select a thumbnail')
).toBeVisible()
})
test('Back button returns to Describe step from Examples', async ({
publishDialog
}) => {
await publishDialog.goNext()
await expect(publishDialog.describeStep).toBeHidden()
await publishDialog.goBack()
await expect(publishDialog.describeStep).toBeVisible()
})
test('navigates through all steps to Finish', async ({ publishDialog }) => {
await publishDialog.goNext() // → Examples
await publishDialog.goNext() // → Finish
await expect(publishDialog.finishStep).toBeVisible()
await expect(publishDialog.publishButton).toBeVisible()
})
test('clicking nav step navigates directly', async ({ publishDialog }) => {
await publishDialog.goToStep('Finish publishing')
await expect(publishDialog.finishStep).toBeVisible()
await publishDialog.goToStep('Describe your workflow')
await expect(publishDialog.describeStep).toBeVisible()
})
test('closes dialog via Escape key', async ({ comfyPage, publishDialog }) => {
await comfyPage.page.keyboard.press('Escape')
await expect(publishDialog.root).toBeHidden()
})
})
test.describe('Publish dialog - Describe step', () => {
test.beforeEach(async ({ comfyPage, publishApi, publishDialog }) => {
await comfyPage.featureFlags.setFlags(PUBLISH_FEATURE_FLAGS)
await publishApi.setupDefaultMocks()
await saveAndOpenPublishDialog(comfyPage, publishDialog, 'test-describe-wf')
})
test('allows editing the workflow name', async ({ publishDialog }) => {
await publishDialog.nameInput.clear()
await publishDialog.nameInput.fill('My Custom Workflow')
await expect(publishDialog.nameInput).toHaveValue('My Custom Workflow')
})
test('allows editing the description', async ({ publishDialog }) => {
await publishDialog.descriptionTextarea.fill(
'A great workflow for anime art'
)
await expect(publishDialog.descriptionTextarea).toHaveValue(
'A great workflow for anime art'
)
})
test('displays tag suggestions from mocked API', async ({
publishDialog
}) => {
await expect(publishDialog.root.getByText('anime')).toBeVisible()
await expect(publishDialog.root.getByText('upscale')).toBeVisible()
})
// TODO(#11548): Tag click emits update:tags but the tag does not appear in
// the active list during E2E. Needs investigation of the parent state
// binding.
test.fixme('clicking a tag suggestion adds it', async ({ publishDialog }) => {
await publishDialog.root.getByText('anime').click()
await expect(publishDialog.tagsInput.getByText('anime')).toBeVisible()
})
})
test.describe('Publish dialog - Examples step', () => {
test.beforeEach(async ({ comfyPage, publishApi, publishDialog }) => {
await comfyPage.featureFlags.setFlags(PUBLISH_FEATURE_FLAGS)
await publishApi.setupDefaultMocks()
await saveAndOpenPublishDialog(comfyPage, publishDialog, 'test-examples-wf')
await publishDialog.goNext() // Navigate to Examples step
})
test('shows thumbnail type toggle options', async ({ publishDialog }) => {
await expect(
publishDialog.root.getByText('Image', { exact: true })
).toBeVisible()
await expect(
publishDialog.root.getByText('Video', { exact: true })
).toBeVisible()
await expect(
publishDialog.root.getByText('Image comparison', { exact: true })
).toBeVisible()
})
test('shows example image upload tile', async ({ publishDialog }) => {
await expect(
publishDialog.root.getByRole('button', { name: 'Upload example image' })
).toBeVisible()
})
})
test.describe('Publish dialog - Finish step with profile', () => {
test.beforeEach(async ({ comfyPage, publishApi, publishDialog }) => {
await comfyPage.featureFlags.setFlags(PUBLISH_FEATURE_FLAGS)
await publishApi.setupDefaultMocks({ hasProfile: true })
await saveAndOpenPublishDialog(comfyPage, publishDialog, 'test-finish-wf')
await publishDialog.goToStep('Finish publishing')
})
test('shows profile card with username', async ({ publishDialog }) => {
await expect(publishDialog.finishStep).toBeVisible()
await expect(publishDialog.root.getByText('@testuser')).toBeVisible()
await expect(publishDialog.root.getByText('Test User')).toBeVisible()
})
test('publish button is enabled when no private assets', async ({
publishDialog
}) => {
await expect(publishDialog.publishButton).toBeEnabled()
})
})
test.describe('Publish dialog - Finish step with private assets', () => {
test.beforeEach(async ({ comfyPage, publishApi, publishDialog }) => {
await comfyPage.featureFlags.setFlags(PUBLISH_FEATURE_FLAGS)
await publishApi.setupDefaultMocks({
hasProfile: true,
hasPrivateAssets: true
})
await saveAndOpenPublishDialog(comfyPage, publishDialog, 'test-assets-wf')
await publishDialog.goToStep('Finish publishing')
})
test('publish button is disabled until assets acknowledged', async ({
publishDialog
}) => {
await expect(publishDialog.finishStep).toBeVisible()
await expect(publishDialog.publishButton).toBeDisabled()
const checkbox = publishDialog.finishStep.getByRole('checkbox')
await checkbox.check()
await expect(publishDialog.publishButton).toBeEnabled()
})
})
test.describe('Publish dialog - no profile', () => {
test.beforeEach(async ({ comfyPage, publishApi, publishDialog }) => {
await comfyPage.featureFlags.setFlags(PUBLISH_FEATURE_FLAGS)
await publishApi.setupDefaultMocks({ hasProfile: false })
await saveAndOpenPublishDialog(
comfyPage,
publishDialog,
'test-noprofile-wf'
)
await publishDialog.goToStep('Finish publishing')
})
test('shows profile creation prompt when user has no profile', async ({
publishDialog
}) => {
await expect(publishDialog.profilePrompt).toBeVisible()
await expect(
publishDialog.root.getByText('Create a profile to publish to ComfyHub')
).toBeVisible()
})
test('clicking create profile CTA shows profile creation form', async ({
publishDialog
}) => {
await publishDialog.root
.getByRole('button', { name: 'Create a profile' })
.click()
await expect(publishDialog.gateFlow).toBeVisible()
})
})
test.describe('Publish dialog - unsaved workflow', () => {
test.beforeEach(async ({ comfyPage, publishApi }) => {
await comfyPage.featureFlags.setFlags(PUBLISH_FEATURE_FLAGS)
await publishApi.setupDefaultMocks()
// Don't save workflow — open dialog on the default temporary workflow
})
test('shows save prompt for temporary workflow', async ({
comfyPage,
publishDialog
}) => {
// Create a new workflow to ensure it's temporary
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
await publishDialog.open()
await expect(publishDialog.savePrompt).toBeVisible()
await expect(
publishDialog.root.getByText(
'You must save your workflow before publishing'
)
).toBeVisible()
// Nav should be hidden when save is required
await expect(publishDialog.nav).toBeHidden()
})
})
test.describe('Publish dialog - submission', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.featureFlags.setFlags(PUBLISH_FEATURE_FLAGS)
})
test('successful publish closes dialog', async ({
comfyPage,
publishApi,
publishDialog
}) => {
await publishApi.setupDefaultMocks({ hasProfile: true })
await saveAndOpenPublishDialog(comfyPage, publishDialog, 'test-submit-wf')
await publishDialog.goToStep('Finish publishing')
await expect(publishDialog.finishStep).toBeVisible()
await publishDialog.publishButton.click()
await expect(publishDialog.root).toBeHidden({ timeout: 10_000 })
})
test('failed publish shows error toast', async ({
comfyPage,
publishApi,
publishDialog
}) => {
await publishApi.setupDefaultMocks({ hasProfile: true })
// Override publish mock with error response
await publishApi.mockPublishWorkflowError(500, 'Internal error')
await saveAndOpenPublishDialog(
comfyPage,
publishDialog,
'test-submit-fail-wf'
)
await publishDialog.goToStep('Finish publishing')
await expect(publishDialog.finishStep).toBeVisible()
await publishDialog.publishButton.click()
// Error toast should appear
await expect(comfyPage.toast.visibleToasts.first()).toBeVisible({
timeout: 10_000
})
// Dialog should remain open
await expect(publishDialog.root).toBeVisible()
})
})