mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-14 19:27:09 +00:00
## Summary App mode now uses the same sidenav as the graph view instead of its own custom navigation, and switching between modes happens through a new animated graph/app toggle shared by both views. ## Changes - **What**: - LinearView hosts the standard `SideToolbar`, filtered to the Assets and Apps tabs via new `visibleTabIds`/`forceConnected` props. The bottom-panel and shortcut toggles hide in app mode, and GraphCanvas/SubgraphBreadcrumb hide their instances so only one toolbar and toggle render at a time. - The workflow actions dropdown trigger is now a two-segment graph/app toggle: the inactive segment switches modes directly, the active one opens the actions menu as before, with a FLIP animation on switch. - Each view renders its own toggle, so switching unmounts one and mounts the other. A `displayLinearMode` ref in canvasStore trails `linearMode` by two frames so the incoming toggle animates from the old mode instead of popping in already-switched. - AppModeToolbar slims down to the toggle plus a "Build an app" button; the per-icon Assets/Apps/Share buttons are covered by the sidenav. - The Apps tab gains "Create" header and empty-state actions wired to `Comfy.NewBlankWorkflow`. - App-mode feedback moves into `SidebarHelpCenterIcon` as a Typeform popover with a load-error fallback, replacing `TypeformPopoverButton` and `LinearFeedback` and simplifying LinearPreview to a single `OutputHistory`. - Tests for the toggle, sidebar tabs, help-center fallback, preview, and view; screenshots regenerated. ## Review Focus - The two-frame `displayLinearMode` lag — timing-sensitive; rapid toggling cancels the pending frame chain so the wrong mode never flashes. - `SideToolbar` now serves both views: confirm only one instance renders and the new props behave. ## Screenshots (if applicable) https://github.com/user-attachments/assets/9ad1eb4f-9370-4975-b3f1-b271482f2bf7 --------- Co-authored-by: github-actions <github-actions@github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
154 lines
4.4 KiB
TypeScript
154 lines
4.4 KiB
TypeScript
import { render, screen } from '@testing-library/vue'
|
|
import userEvent from '@testing-library/user-event'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { createI18n } from 'vue-i18n'
|
|
|
|
import SidebarHelpCenterIcon from './SidebarHelpCenterIcon.vue'
|
|
|
|
const typeformState = vi.hoisted(() => ({
|
|
typeformError: false,
|
|
isValidTypeformId: true
|
|
}))
|
|
|
|
const embedSpy = vi.hoisted(() => vi.fn())
|
|
|
|
const canvasState = vi.hoisted(() => ({ linearMode: true }))
|
|
|
|
const helpCenterSpies = vi.hoisted(() => ({ toggleHelpCenter: vi.fn() }))
|
|
|
|
vi.mock('@/platform/surveys/useTypeformEmbed', async () => {
|
|
const { computed } = await import('vue')
|
|
return {
|
|
useTypeformEmbed: (containerRef: unknown, formId: string) => {
|
|
embedSpy(containerRef, formId)
|
|
return {
|
|
typeformError: computed(() => typeformState.typeformError),
|
|
isValidTypeformId: computed(() => typeformState.isValidTypeformId),
|
|
// The real composable echoes the id it validated.
|
|
typeformId: computed(() => formId)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
vi.mock('@/composables/useHelpCenter', async () => {
|
|
const { ref } = await import('vue')
|
|
return {
|
|
useHelpCenter: () => ({
|
|
shouldShowRedDot: ref(false),
|
|
toggleHelpCenter: helpCenterSpies.toggleHelpCenter
|
|
})
|
|
}
|
|
})
|
|
|
|
vi.mock('@/platform/settings/settingStore', () => ({
|
|
useSettingStore: () => ({ get: () => 'left' })
|
|
}))
|
|
|
|
vi.mock('@/renderer/core/canvas/canvasStore', async () => {
|
|
const { computed, reactive } = await import('vue')
|
|
return {
|
|
useCanvasStore: () =>
|
|
reactive({ linearMode: computed(() => canvasState.linearMode) })
|
|
}
|
|
})
|
|
|
|
const FEEDBACK_LOAD_ERROR =
|
|
'Failed to load feedback form. Please try again later.'
|
|
|
|
const i18n = createI18n({
|
|
legacy: false,
|
|
locale: 'en',
|
|
messages: {
|
|
en: {
|
|
menu: { help: 'Help' },
|
|
sideToolbar: { helpCenter: 'Help Center' },
|
|
linearMode: {
|
|
giveFeedback: 'Give feedback',
|
|
feedbackLoadError: FEEDBACK_LOAD_ERROR
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
function renderIcon() {
|
|
const user = userEvent.setup()
|
|
const result = render(SidebarHelpCenterIcon, {
|
|
props: { isSmall: false },
|
|
global: {
|
|
plugins: [i18n],
|
|
directives: { tooltip: {} },
|
|
stubs: {
|
|
Popover: {
|
|
template: '<div><slot name="button" /><slot /></div>'
|
|
}
|
|
}
|
|
}
|
|
})
|
|
return { ...result, user }
|
|
}
|
|
|
|
describe('SidebarHelpCenterIcon', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
typeformState.typeformError = false
|
|
typeformState.isValidTypeformId = true
|
|
canvasState.linearMode = true
|
|
})
|
|
|
|
it('mounts the Typeform embed container wired to the feedback form', () => {
|
|
renderIcon()
|
|
|
|
const embed = screen.getByTestId('feedback-embed')
|
|
expect(embed).toHaveAttribute('data-tf-widget', 'jmmzmlKw')
|
|
expect(embedSpy).toHaveBeenCalledWith(expect.anything(), 'jmmzmlKw')
|
|
expect(screen.queryByText(FEEDBACK_LOAD_ERROR)).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('shows the localized fallback instead of the embed when loading fails', () => {
|
|
typeformState.typeformError = true
|
|
renderIcon()
|
|
|
|
expect(screen.getByText(FEEDBACK_LOAD_ERROR)).toBeInTheDocument()
|
|
expect(screen.queryByTestId('feedback-embed')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('shows the localized fallback when the form id is invalid', () => {
|
|
typeformState.isValidTypeformId = false
|
|
renderIcon()
|
|
|
|
expect(screen.getByText(FEEDBACK_LOAD_ERROR)).toBeInTheDocument()
|
|
expect(screen.queryByTestId('feedback-embed')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('does not open the help center from the feedback button in app mode', async () => {
|
|
const { user } = renderIcon()
|
|
|
|
await user.click(screen.getByRole('button', { name: 'Give feedback' }))
|
|
|
|
expect(helpCenterSpies.toggleHelpCenter).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('shows the help center button instead of the feedback popover in graph mode', () => {
|
|
canvasState.linearMode = false
|
|
renderIcon()
|
|
|
|
expect(
|
|
screen.getByRole('button', { name: 'Help Center' })
|
|
).toBeInTheDocument()
|
|
expect(
|
|
screen.queryByRole('button', { name: 'Give feedback' })
|
|
).not.toBeInTheDocument()
|
|
expect(screen.queryByTestId('feedback-embed')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('toggles the help center on click in graph mode', async () => {
|
|
canvasState.linearMode = false
|
|
const { user } = renderIcon()
|
|
|
|
await user.click(screen.getByRole('button', { name: 'Help Center' }))
|
|
|
|
expect(helpCenterSpies.toggleHelpCenter).toHaveBeenCalled()
|
|
})
|
|
})
|