mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-04 19:49:09 +00:00
Compare commits
2 Commits
fix/subgra
...
feat/publi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1be1ac71f4 | ||
|
|
00dda88a40 |
106
browser_tests/fixtures/components/PublishDialog.ts
Normal file
106
browser_tests/fixtures/components/PublishDialog.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
|
||||
import { TestIds } from '@e2e/fixtures/selectors'
|
||||
|
||||
import { BaseDialog } from './BaseDialog'
|
||||
|
||||
export class PublishDialog extends BaseDialog {
|
||||
readonly nav: Locator
|
||||
readonly footer: Locator
|
||||
readonly savePrompt: Locator
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page, TestIds.publish.dialog)
|
||||
this.nav = this.root.getByTestId(TestIds.publish.nav)
|
||||
this.footer = this.root.getByTestId(TestIds.publish.footer)
|
||||
this.savePrompt = this.root.getByTestId(TestIds.publish.savePrompt)
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the publish dialog via the dialog service's showPublishDialog(),
|
||||
* which uses Vite-bundled lazy imports that work in both dev and production.
|
||||
*/
|
||||
async open(): Promise<void> {
|
||||
await this.page.evaluate(async () => {
|
||||
const store = window.app!.extensionManager as {
|
||||
dialog: { showPublishDialog: () => Promise<void> }
|
||||
}
|
||||
await store.dialog.showPublishDialog()
|
||||
})
|
||||
await this.waitForVisible()
|
||||
}
|
||||
|
||||
// Step content locators
|
||||
|
||||
get describeStep(): Locator {
|
||||
return this.root.getByTestId(TestIds.publish.describeStep)
|
||||
}
|
||||
|
||||
get finishStep(): Locator {
|
||||
return this.root.getByTestId(TestIds.publish.finishStep)
|
||||
}
|
||||
|
||||
get profilePrompt(): Locator {
|
||||
return this.root.getByTestId(TestIds.publish.profilePrompt)
|
||||
}
|
||||
|
||||
get gateFlow(): Locator {
|
||||
return this.root.getByTestId(TestIds.publish.gateFlow)
|
||||
}
|
||||
|
||||
// Describe step locators
|
||||
|
||||
get nameInput(): Locator {
|
||||
return this.describeStep.getByRole('textbox').first()
|
||||
}
|
||||
|
||||
get descriptionTextarea(): Locator {
|
||||
return this.describeStep.locator('textarea')
|
||||
}
|
||||
|
||||
get tagsInput(): Locator {
|
||||
return this.describeStep.locator('[role="list"]').first()
|
||||
}
|
||||
|
||||
tagSuggestion(name: string): Locator {
|
||||
return this.describeStep.getByText(name, { exact: true })
|
||||
}
|
||||
|
||||
// Footer button locators
|
||||
|
||||
get backButton(): Locator {
|
||||
return this.footer.getByRole('button', { name: 'Back' })
|
||||
}
|
||||
|
||||
get nextButton(): Locator {
|
||||
return this.footer.getByRole('button', { name: 'Next' })
|
||||
}
|
||||
|
||||
get publishButton(): Locator {
|
||||
return this.footer.getByRole('button', { name: 'Publish to ComfyHub' })
|
||||
}
|
||||
|
||||
// Nav locators
|
||||
|
||||
navStep(label: string): Locator {
|
||||
return this.nav.getByRole('button', { name: label })
|
||||
}
|
||||
|
||||
currentNavStep(): Locator {
|
||||
return this.nav.locator('[aria-current="step"]')
|
||||
}
|
||||
|
||||
// Navigation helpers
|
||||
|
||||
async goNext(): Promise<void> {
|
||||
await this.nextButton.click()
|
||||
}
|
||||
|
||||
async goBack(): Promise<void> {
|
||||
await this.backButton.click()
|
||||
}
|
||||
|
||||
async goToStep(label: string): Promise<void> {
|
||||
await this.navStep(label).click()
|
||||
}
|
||||
}
|
||||
200
browser_tests/fixtures/helpers/PublishApiHelper.ts
Normal file
200
browser_tests/fixtures/helpers/PublishApiHelper.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import type { Page, Route } from '@playwright/test'
|
||||
|
||||
import type {
|
||||
AssetInfo,
|
||||
HubAssetUploadUrlResponse,
|
||||
HubLabelInfo,
|
||||
HubLabelListResponse,
|
||||
HubProfile,
|
||||
WorkflowPublishInfo
|
||||
} from '@comfyorg/ingest-types'
|
||||
|
||||
import type { ShareableAssetsResponse } from '@/schemas/apiSchema'
|
||||
|
||||
const DEFAULT_PROFILE: HubProfile = {
|
||||
username: 'testuser',
|
||||
display_name: 'Test User',
|
||||
description: 'A test creator',
|
||||
avatar_url: undefined
|
||||
}
|
||||
|
||||
const DEFAULT_TAG_LABELS: HubLabelInfo[] = [
|
||||
{ name: 'anime', display_name: 'anime', type: 'tag' },
|
||||
{ name: 'upscale', display_name: 'upscale', type: 'tag' },
|
||||
{ name: 'faceswap', display_name: 'faceswap', type: 'tag' },
|
||||
{ name: 'img2img', display_name: 'img2img', type: 'tag' },
|
||||
{ name: 'controlnet', display_name: 'controlnet', type: 'tag' }
|
||||
]
|
||||
|
||||
const DEFAULT_PUBLISH_RESPONSE: WorkflowPublishInfo = {
|
||||
workflow_id: 'test-workflow-id-456',
|
||||
share_id: 'test-share-id-123',
|
||||
publish_time: new Date().toISOString(),
|
||||
listed: true,
|
||||
assets: []
|
||||
}
|
||||
|
||||
const DEFAULT_UPLOAD_URL_RESPONSE: HubAssetUploadUrlResponse = {
|
||||
upload_url: 'https://mock-s3.example.com/upload',
|
||||
public_url: 'https://mock-s3.example.com/asset.png',
|
||||
token: 'mock-upload-token'
|
||||
}
|
||||
|
||||
export class PublishApiHelper {
|
||||
private routeHandlers: Array<{
|
||||
pattern: string
|
||||
handler: (route: Route) => Promise<void>
|
||||
}> = []
|
||||
|
||||
constructor(private readonly page: Page) {}
|
||||
|
||||
async mockProfile(profile: HubProfile | null): Promise<void> {
|
||||
await this.addRoute('**/hub/profiles/me', async (route) => {
|
||||
if (route.request().method() !== 'GET') {
|
||||
await route.continue()
|
||||
return
|
||||
}
|
||||
if (profile === null) {
|
||||
await route.fulfill({ status: 404, body: 'Not found' })
|
||||
} else {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(profile)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async mockTagLabels(
|
||||
labels: HubLabelInfo[] = DEFAULT_TAG_LABELS
|
||||
): Promise<void> {
|
||||
const response: HubLabelListResponse = { labels }
|
||||
await this.addRoute('**/hub/labels**', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(response)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async mockPublishStatus(
|
||||
status: 'unpublished' | WorkflowPublishInfo
|
||||
): Promise<void> {
|
||||
await this.addRoute('**/userdata/*/publish', async (route) => {
|
||||
if (route.request().method() !== 'GET') {
|
||||
await route.continue()
|
||||
return
|
||||
}
|
||||
if (status === 'unpublished') {
|
||||
await route.fulfill({ status: 404, body: 'Not found' })
|
||||
} else {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(status)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async mockShareableAssets(assets: AssetInfo[] = []): Promise<void> {
|
||||
const response: ShareableAssetsResponse = { assets }
|
||||
await this.addRoute('**/assets/from-workflow', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(response)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async mockPublishWorkflow(
|
||||
response: WorkflowPublishInfo = DEFAULT_PUBLISH_RESPONSE
|
||||
): Promise<void> {
|
||||
await this.addRoute('**/hub/workflows', async (route) => {
|
||||
if (route.request().method() !== 'POST') {
|
||||
await route.continue()
|
||||
return
|
||||
}
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(response)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async mockPublishWorkflowError(
|
||||
statusCode = 500,
|
||||
message = 'Failed to publish workflow'
|
||||
): Promise<void> {
|
||||
await this.addRoute('**/hub/workflows', async (route) => {
|
||||
if (route.request().method() !== 'POST') {
|
||||
await route.continue()
|
||||
return
|
||||
}
|
||||
await route.fulfill({
|
||||
status: statusCode,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ message })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async mockUploadUrl(
|
||||
response: HubAssetUploadUrlResponse = DEFAULT_UPLOAD_URL_RESPONSE
|
||||
): Promise<void> {
|
||||
await this.addRoute('**/hub/assets/upload-url', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(response)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async setupDefaultMocks(options?: {
|
||||
hasProfile?: boolean
|
||||
hasPrivateAssets?: boolean
|
||||
}): Promise<void> {
|
||||
const { hasProfile = true, hasPrivateAssets = false } = options ?? {}
|
||||
|
||||
await this.mockProfile(hasProfile ? DEFAULT_PROFILE : null)
|
||||
await this.mockTagLabels()
|
||||
await this.mockPublishStatus('unpublished')
|
||||
await this.mockShareableAssets(
|
||||
hasPrivateAssets
|
||||
? [
|
||||
{
|
||||
id: 'asset-1',
|
||||
name: 'my_model.safetensors',
|
||||
preview_url: '',
|
||||
storage_url: '',
|
||||
model: true,
|
||||
public: false,
|
||||
in_library: true
|
||||
}
|
||||
]
|
||||
: []
|
||||
)
|
||||
await this.mockPublishWorkflow()
|
||||
await this.mockUploadUrl()
|
||||
}
|
||||
|
||||
async cleanup(): Promise<void> {
|
||||
for (const { pattern, handler } of this.routeHandlers) {
|
||||
await this.page.unroute(pattern, handler)
|
||||
}
|
||||
this.routeHandlers = []
|
||||
}
|
||||
|
||||
private async addRoute(
|
||||
pattern: string,
|
||||
handler: (route: Route) => Promise<void>
|
||||
): Promise<void> {
|
||||
this.routeHandlers.push({ pattern, handler })
|
||||
await this.page.route(pattern, handler)
|
||||
}
|
||||
}
|
||||
@@ -106,6 +106,16 @@ export const TestIds = {
|
||||
errors: {
|
||||
imageLoadError: 'error-loading-image',
|
||||
videoLoadError: 'error-loading-video'
|
||||
},
|
||||
publish: {
|
||||
dialog: 'publish-dialog',
|
||||
savePrompt: 'publish-save-prompt',
|
||||
describeStep: 'publish-describe-step',
|
||||
finishStep: 'publish-finish-step',
|
||||
footer: 'publish-footer',
|
||||
profilePrompt: 'publish-profile-prompt',
|
||||
nav: 'publish-nav',
|
||||
gateFlow: 'publish-gate-flow'
|
||||
}
|
||||
} as const
|
||||
|
||||
@@ -133,3 +143,4 @@ export type TestIdValue =
|
||||
| (typeof TestIds.user)[keyof typeof TestIds.user]
|
||||
| (typeof TestIds.queue)[keyof typeof TestIds.queue]
|
||||
| (typeof TestIds.errors)[keyof typeof TestIds.errors]
|
||||
| (typeof TestIds.publish)[keyof typeof TestIds.publish]
|
||||
|
||||
380
browser_tests/tests/dialogs/publishDialog.spec.ts
Normal file
380
browser_tests/tests/dialogs/publishDialog.spec.ts
Normal file
@@ -0,0 +1,380 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
|
||||
import { PublishDialog } from '@e2e/fixtures/components/PublishDialog'
|
||||
import { PublishApiHelper } from '@e2e/fixtures/helpers/PublishApiHelper'
|
||||
|
||||
test.describe('Publish dialog - wizard navigation', () => {
|
||||
let dialog: PublishDialog
|
||||
let publishApi: PublishApiHelper
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
dialog = new PublishDialog(comfyPage.page)
|
||||
publishApi = new PublishApiHelper(comfyPage.page)
|
||||
|
||||
await comfyPage.featureFlags.setFlags({
|
||||
comfyhub_upload_enabled: true,
|
||||
comfyhub_profile_gate_enabled: true
|
||||
})
|
||||
|
||||
await publishApi.setupDefaultMocks()
|
||||
|
||||
await comfyPage.menu.topbar.saveWorkflow('test-publish-wf')
|
||||
// Handle overwrite confirmation if the file already exists
|
||||
const overwriteDialog = comfyPage.page.locator(
|
||||
'.p-dialog:has-text("Overwrite")'
|
||||
)
|
||||
if (await overwriteDialog.isVisible()) {
|
||||
await comfyPage.confirmDialog.click('overwrite')
|
||||
}
|
||||
|
||||
await dialog.open()
|
||||
})
|
||||
|
||||
test('opens on the Describe step by default', async () => {
|
||||
await expect(dialog.describeStep).toBeVisible()
|
||||
await expect(dialog.nameInput).toBeVisible()
|
||||
await expect(dialog.descriptionTextarea).toBeVisible()
|
||||
})
|
||||
|
||||
test('pre-fills workflow name from active workflow', async () => {
|
||||
await expect(dialog.nameInput).toHaveValue(/test-publish-wf/)
|
||||
})
|
||||
|
||||
test('Next button navigates to Examples step', async () => {
|
||||
await dialog.goNext()
|
||||
await expect(dialog.describeStep).toBeHidden()
|
||||
// Examples step should show thumbnail toggle and upload area
|
||||
await expect(dialog.root.getByText('Select a thumbnail')).toBeVisible()
|
||||
})
|
||||
|
||||
test('Back button returns to Describe step from Examples', async () => {
|
||||
await dialog.goNext()
|
||||
await expect(dialog.describeStep).toBeHidden()
|
||||
|
||||
await dialog.goBack()
|
||||
await expect(dialog.describeStep).toBeVisible()
|
||||
})
|
||||
|
||||
test('navigates through all steps to Finish', async () => {
|
||||
await dialog.goNext() // → Examples
|
||||
await dialog.goNext() // → Finish
|
||||
await expect(dialog.finishStep).toBeVisible()
|
||||
await expect(dialog.publishButton).toBeVisible()
|
||||
})
|
||||
|
||||
test('clicking nav step navigates directly', async () => {
|
||||
await dialog.goToStep('Finish publishing')
|
||||
await expect(dialog.finishStep).toBeVisible()
|
||||
|
||||
await dialog.goToStep('Describe your workflow')
|
||||
await expect(dialog.describeStep).toBeVisible()
|
||||
})
|
||||
|
||||
test('closes dialog via Escape key', async ({ comfyPage }) => {
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
await expect(dialog.root).toBeHidden()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Publish dialog - Describe step', () => {
|
||||
let dialog: PublishDialog
|
||||
let publishApi: PublishApiHelper
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
dialog = new PublishDialog(comfyPage.page)
|
||||
publishApi = new PublishApiHelper(comfyPage.page)
|
||||
|
||||
await comfyPage.featureFlags.setFlags({
|
||||
comfyhub_upload_enabled: true,
|
||||
comfyhub_profile_gate_enabled: true
|
||||
})
|
||||
|
||||
await publishApi.setupDefaultMocks()
|
||||
await comfyPage.menu.topbar.saveWorkflow('test-describe-wf')
|
||||
const overwriteDialog = comfyPage.page.locator(
|
||||
'.p-dialog:has-text("Overwrite")'
|
||||
)
|
||||
if (await overwriteDialog.isVisible()) {
|
||||
await comfyPage.confirmDialog.click('overwrite')
|
||||
}
|
||||
|
||||
await dialog.open()
|
||||
})
|
||||
|
||||
test('allows editing the workflow name', async () => {
|
||||
await dialog.nameInput.clear()
|
||||
await dialog.nameInput.fill('My Custom Workflow')
|
||||
await expect(dialog.nameInput).toHaveValue('My Custom Workflow')
|
||||
})
|
||||
|
||||
test('allows editing the description', async () => {
|
||||
await dialog.descriptionTextarea.fill('A great workflow for anime art')
|
||||
await expect(dialog.descriptionTextarea).toHaveValue(
|
||||
'A great workflow for anime art'
|
||||
)
|
||||
})
|
||||
|
||||
test('displays tag suggestions from mocked API', async () => {
|
||||
await expect(dialog.root.getByText('anime')).toBeVisible()
|
||||
await expect(dialog.root.getByText('upscale')).toBeVisible()
|
||||
})
|
||||
|
||||
// TODO: 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 () => {
|
||||
await dialog.root.getByText('anime').click()
|
||||
|
||||
const activeTags = dialog.describeStep.locator('[role="list"]').first()
|
||||
await expect(activeTags.getByText('anime')).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Publish dialog - Examples step', () => {
|
||||
let dialog: PublishDialog
|
||||
let publishApi: PublishApiHelper
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
dialog = new PublishDialog(comfyPage.page)
|
||||
publishApi = new PublishApiHelper(comfyPage.page)
|
||||
|
||||
await comfyPage.featureFlags.setFlags({
|
||||
comfyhub_upload_enabled: true,
|
||||
comfyhub_profile_gate_enabled: true
|
||||
})
|
||||
|
||||
await publishApi.setupDefaultMocks()
|
||||
await comfyPage.menu.topbar.saveWorkflow('test-examples-wf')
|
||||
const overwriteDialog = comfyPage.page.locator(
|
||||
'.p-dialog:has-text("Overwrite")'
|
||||
)
|
||||
if (await overwriteDialog.isVisible()) {
|
||||
await comfyPage.confirmDialog.click('overwrite')
|
||||
}
|
||||
|
||||
await dialog.open()
|
||||
await dialog.goNext() // Navigate to Examples step
|
||||
})
|
||||
|
||||
test('shows thumbnail type toggle options', async () => {
|
||||
await expect(dialog.root.getByText('Image', { exact: true })).toBeVisible()
|
||||
await expect(dialog.root.getByText('Video', { exact: true })).toBeVisible()
|
||||
await expect(
|
||||
dialog.root.getByText('Image comparison', { exact: true })
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('shows example image upload tile', async () => {
|
||||
await expect(
|
||||
dialog.root.getByRole('button', { name: 'Upload example image' })
|
||||
).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Publish dialog - Finish step with profile', () => {
|
||||
let dialog: PublishDialog
|
||||
let publishApi: PublishApiHelper
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
dialog = new PublishDialog(comfyPage.page)
|
||||
publishApi = new PublishApiHelper(comfyPage.page)
|
||||
|
||||
await comfyPage.featureFlags.setFlags({
|
||||
comfyhub_upload_enabled: true,
|
||||
comfyhub_profile_gate_enabled: true
|
||||
})
|
||||
|
||||
await publishApi.setupDefaultMocks({ hasProfile: true })
|
||||
await comfyPage.menu.topbar.saveWorkflow('test-finish-wf')
|
||||
const overwriteDialog = comfyPage.page.locator(
|
||||
'.p-dialog:has-text("Overwrite")'
|
||||
)
|
||||
if (await overwriteDialog.isVisible()) {
|
||||
await comfyPage.confirmDialog.click('overwrite')
|
||||
}
|
||||
|
||||
await dialog.open()
|
||||
await dialog.goToStep('Finish publishing')
|
||||
})
|
||||
|
||||
test('shows profile card with username', async () => {
|
||||
await expect(dialog.finishStep).toBeVisible()
|
||||
await expect(dialog.root.getByText('@testuser')).toBeVisible()
|
||||
await expect(dialog.root.getByText('Test User')).toBeVisible()
|
||||
})
|
||||
|
||||
test('publish button is enabled when no private assets', async () => {
|
||||
await expect(dialog.publishButton).toBeEnabled()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Publish dialog - Finish step with private assets', () => {
|
||||
let dialog: PublishDialog
|
||||
let publishApi: PublishApiHelper
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
dialog = new PublishDialog(comfyPage.page)
|
||||
publishApi = new PublishApiHelper(comfyPage.page)
|
||||
|
||||
await comfyPage.featureFlags.setFlags({
|
||||
comfyhub_upload_enabled: true,
|
||||
comfyhub_profile_gate_enabled: true
|
||||
})
|
||||
|
||||
await publishApi.setupDefaultMocks({
|
||||
hasProfile: true,
|
||||
hasPrivateAssets: true
|
||||
})
|
||||
await comfyPage.menu.topbar.saveWorkflow('test-assets-wf')
|
||||
const overwriteDialog = comfyPage.page.locator(
|
||||
'.p-dialog:has-text("Overwrite")'
|
||||
)
|
||||
if (await overwriteDialog.isVisible()) {
|
||||
await comfyPage.confirmDialog.click('overwrite')
|
||||
}
|
||||
|
||||
await dialog.open()
|
||||
await dialog.goToStep('Finish publishing')
|
||||
})
|
||||
|
||||
test('publish button is disabled until assets acknowledged', async () => {
|
||||
await expect(dialog.finishStep).toBeVisible()
|
||||
await expect(dialog.publishButton).toBeDisabled()
|
||||
|
||||
// Check the acknowledge checkbox
|
||||
const checkbox = dialog.finishStep.getByRole('checkbox')
|
||||
await checkbox.check()
|
||||
|
||||
await expect(dialog.publishButton).toBeEnabled()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Publish dialog - no profile', () => {
|
||||
let dialog: PublishDialog
|
||||
let publishApi: PublishApiHelper
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
dialog = new PublishDialog(comfyPage.page)
|
||||
publishApi = new PublishApiHelper(comfyPage.page)
|
||||
|
||||
await comfyPage.featureFlags.setFlags({
|
||||
comfyhub_upload_enabled: true,
|
||||
comfyhub_profile_gate_enabled: true
|
||||
})
|
||||
|
||||
await publishApi.setupDefaultMocks({ hasProfile: false })
|
||||
await comfyPage.menu.topbar.saveWorkflow('test-noprofile-wf')
|
||||
const overwriteDialog = comfyPage.page.locator(
|
||||
'.p-dialog:has-text("Overwrite")'
|
||||
)
|
||||
if (await overwriteDialog.isVisible()) {
|
||||
await comfyPage.confirmDialog.click('overwrite')
|
||||
}
|
||||
|
||||
await dialog.open()
|
||||
await dialog.goToStep('Finish publishing')
|
||||
})
|
||||
|
||||
test('shows profile creation prompt when user has no profile', async () => {
|
||||
await expect(dialog.profilePrompt).toBeVisible()
|
||||
await expect(
|
||||
dialog.root.getByText('Create a profile to publish to ComfyHub')
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('clicking create profile CTA shows profile creation form', async () => {
|
||||
await dialog.root.getByRole('button', { name: 'Create a profile' }).click()
|
||||
await expect(dialog.gateFlow).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Publish dialog - unsaved workflow', () => {
|
||||
let dialog: PublishDialog
|
||||
let publishApi: PublishApiHelper
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
dialog = new PublishDialog(comfyPage.page)
|
||||
publishApi = new PublishApiHelper(comfyPage.page)
|
||||
|
||||
await comfyPage.featureFlags.setFlags({
|
||||
comfyhub_upload_enabled: true,
|
||||
comfyhub_profile_gate_enabled: true
|
||||
})
|
||||
|
||||
await publishApi.setupDefaultMocks()
|
||||
// Don't save workflow — open dialog on the default temporary workflow
|
||||
})
|
||||
|
||||
test('shows save prompt for temporary workflow', async ({ comfyPage }) => {
|
||||
// Create a new workflow to ensure it's temporary
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
|
||||
await dialog.open()
|
||||
|
||||
await expect(dialog.savePrompt).toBeVisible()
|
||||
await expect(
|
||||
dialog.root.getByText('You must save your workflow before publishing')
|
||||
).toBeVisible()
|
||||
// Nav should be hidden when save is required
|
||||
await expect(dialog.nav).toBeHidden()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Publish dialog - submission', () => {
|
||||
let dialog: PublishDialog
|
||||
let publishApi: PublishApiHelper
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
dialog = new PublishDialog(comfyPage.page)
|
||||
publishApi = new PublishApiHelper(comfyPage.page)
|
||||
|
||||
await comfyPage.featureFlags.setFlags({
|
||||
comfyhub_upload_enabled: true,
|
||||
comfyhub_profile_gate_enabled: true
|
||||
})
|
||||
})
|
||||
|
||||
test('successful publish closes dialog', async ({ comfyPage }) => {
|
||||
await publishApi.setupDefaultMocks({ hasProfile: true })
|
||||
await comfyPage.menu.topbar.saveWorkflow('test-submit-wf')
|
||||
const overwriteDialog = comfyPage.page.locator(
|
||||
'.p-dialog:has-text("Overwrite")'
|
||||
)
|
||||
if (await overwriteDialog.isVisible()) {
|
||||
await comfyPage.confirmDialog.click('overwrite')
|
||||
}
|
||||
|
||||
await dialog.open()
|
||||
await dialog.goToStep('Finish publishing')
|
||||
await expect(dialog.finishStep).toBeVisible()
|
||||
|
||||
await dialog.publishButton.click()
|
||||
await expect(dialog.root).toBeHidden({ timeout: 10_000 })
|
||||
})
|
||||
|
||||
test('failed publish shows error toast', async ({ comfyPage }) => {
|
||||
await publishApi.setupDefaultMocks({ hasProfile: true })
|
||||
// Override publish mock with error response
|
||||
await publishApi.mockPublishWorkflowError(500, 'Internal error')
|
||||
|
||||
await comfyPage.menu.topbar.saveWorkflow('test-submit-fail-wf')
|
||||
const overwriteDialog = comfyPage.page.locator(
|
||||
'.p-dialog:has-text("Overwrite")'
|
||||
)
|
||||
if (await overwriteDialog.isVisible()) {
|
||||
await comfyPage.confirmDialog.click('overwrite')
|
||||
}
|
||||
|
||||
await dialog.open()
|
||||
await dialog.goToStep('Finish publishing')
|
||||
await expect(dialog.finishStep).toBeVisible()
|
||||
|
||||
await dialog.publishButton.click()
|
||||
|
||||
// Error toast should appear
|
||||
await expect(comfyPage.page.locator('.p-toast-message-error')).toBeVisible({
|
||||
timeout: 10_000
|
||||
})
|
||||
// Dialog should remain open
|
||||
await expect(dialog.root).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<div class="flex min-h-0 flex-1 flex-col gap-6 px-6 py-4">
|
||||
<div
|
||||
data-testid="publish-describe-step"
|
||||
class="flex min-h-0 flex-1 flex-col gap-6 px-6 py-4"
|
||||
>
|
||||
<label class="flex flex-col gap-2">
|
||||
<span class="text-sm text-base-foreground">
|
||||
{{ $t('comfyHubPublish.workflowName') }}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<div class="flex min-h-0 flex-1 flex-col gap-8 px-6 py-4">
|
||||
<div
|
||||
data-testid="publish-finish-step"
|
||||
class="flex min-h-0 flex-1 flex-col gap-8 px-6 py-4"
|
||||
>
|
||||
<section class="flex flex-col gap-4">
|
||||
<span class="text-sm text-base-foreground">
|
||||
{{ $t('comfyHubPublish.shareAs') }}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<div class="flex min-h-0 flex-1 flex-col gap-4 px-6 py-4">
|
||||
<div
|
||||
data-testid="publish-profile-prompt"
|
||||
class="flex min-h-0 flex-1 flex-col gap-4 px-6 py-4"
|
||||
>
|
||||
<p class="text-sm text-base-foreground">
|
||||
{{ $t('comfyHubPublish.createProfileToPublish') }}
|
||||
</p>
|
||||
|
||||
@@ -21,7 +21,11 @@
|
||||
|
||||
<template #header />
|
||||
<template #content>
|
||||
<div v-if="needsSave" class="flex flex-col gap-4 p-6">
|
||||
<div
|
||||
v-if="needsSave"
|
||||
data-testid="publish-save-prompt"
|
||||
class="flex flex-col gap-4 p-6"
|
||||
>
|
||||
<p class="m-0 text-sm text-muted-foreground">
|
||||
{{ $t('comfyHubPublish.unsavedDescription') }}
|
||||
</p>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<footer
|
||||
data-testid="publish-footer"
|
||||
class="flex shrink items-center justify-end gap-4 border-t border-border-default px-6 py-4"
|
||||
>
|
||||
<Button v-if="!isFirstStep" size="lg" @click="$emit('back')">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<nav class="flex flex-col gap-6 px-3 py-4">
|
||||
<nav data-testid="publish-nav" class="flex flex-col gap-6 px-3 py-4">
|
||||
<ol class="flex list-none flex-col p-0">
|
||||
<li
|
||||
v-for="step in steps"
|
||||
|
||||
@@ -30,6 +30,8 @@ const lazyComfyOrgHeader = () =>
|
||||
import('@/components/dialog/header/ComfyOrgHeader.vue')
|
||||
const lazyCloudNotificationContent = () =>
|
||||
import('@/platform/cloud/notification/components/CloudNotificationContent.vue')
|
||||
const lazyPublishDialog = () =>
|
||||
import('@/platform/workflow/sharing/components/publish/ComfyHubPublishDialog.vue')
|
||||
|
||||
export type ConfirmationDialogType =
|
||||
| 'default'
|
||||
@@ -591,10 +593,28 @@ export const useDialogService = () => {
|
||||
})
|
||||
}
|
||||
|
||||
async function showPublishDialog(): Promise<void> {
|
||||
const { default: ComfyHubPublishDialog } = await lazyPublishDialog()
|
||||
const key = 'global-comfyhub-publish'
|
||||
showLayoutDialog({
|
||||
key,
|
||||
component: ComfyHubPublishDialog,
|
||||
props: {
|
||||
onClose: () => dialogStore.closeDialog({ key })
|
||||
},
|
||||
dialogComponentProps: {
|
||||
pt: {
|
||||
root: { 'data-testid': 'publish-dialog' }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
showExecutionErrorDialog,
|
||||
showApiNodesSignInDialog,
|
||||
showSignInDialog,
|
||||
showPublishDialog,
|
||||
showSubscriptionRequiredDialog,
|
||||
showTopUpCreditsDialog,
|
||||
showUpdatePasswordDialog,
|
||||
|
||||
Reference in New Issue
Block a user