Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add User Feedback buttons #2275

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
68 changes: 68 additions & 0 deletions browser_tests/dialog.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,71 @@ test.describe('Settings', () => {
expect(request.postData()).toContain(JSON.stringify(expectedSetting))
})
})

test.describe('Feedback dialog', () => {
test('Should open from about panel badge', async ({ comfyPage }) => {
// Go to about panel page in settings
const settings = comfyPage.settingDialog
await settings.open()
await settings.goToAboutPanel()

// Click feedback button
const feedbackButton = settings.root
.locator('a')
.filter({ hasText: 'Feedback' })
await feedbackButton.click({ force: true })

// Verify feedback dialog content is visible
const feedbackHeader = comfyPage.page.getByRole('heading', {
name: 'Feedback'
})
await expect(feedbackHeader).toBeVisible()
})

test('Should open from topmenu help command', async ({ comfyPage }) => {
// Open feedback dialog from top menu
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Feedback'])

// Verify feedback dialog content is visible
const feedbackHeader = comfyPage.page.getByRole('heading', {
name: 'Feedback'
})
await expect(feedbackHeader).toBeVisible()
})

test('Should close when close button clicked', async ({ comfyPage }) => {
// Open feedback dialog
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Feedback'])

const feedbackHeader = comfyPage.page.getByRole('heading', {
name: 'Feedback'
})

// Close feedback dialog
await comfyPage.page
.getByLabel('', { exact: true })
.getByLabel('Close')
.click()
await feedbackHeader.waitFor({ state: 'hidden' })

// Verify dialog is closed
await expect(feedbackHeader).not.toBeVisible()
})

test('Should update rating icons when selecting rating', async ({
comfyPage
}) => {
// Open feedback dialog
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Feedback'])

// Test rating interaction
const stars = comfyPage.page.locator('.pi-star')
await stars.nth(3).click()
await expect(
comfyPage.page.getByLabel('Rating').locator('i').nth(3)
).toHaveClass(/pi-star-fill/)
})
})
4 changes: 4 additions & 0 deletions browser_tests/fixtures/components/SettingDialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { Page } from '@playwright/test'
export class SettingDialog {
constructor(public readonly page: Page) {}

get root() {
return this.page.locator('div.settings-container')
}

async open() {
const button = this.page.locator('button.comfy-settings-btn:visible')
await button.click()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
</template>
<ReportIssuePanel
v-if="sendReportOpen"
:title="$t('issueReport.submitErrorReport')"
error-type="graphExecutionError"
:extra-fields="[stackTraceField]"
:tags="{ exceptionMessage: props.error.exception_message }"
Expand Down
54 changes: 54 additions & 0 deletions src/components/dialog/content/FeedbackDialogContent.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<template>
<div class="p-8 h-full">
<Card>
<template #content>
<div class="flex flex-col items-center">
<h3 class="text-4xl">{{ $t('menuLabels.Feedback') }}</h3>
<Rating
v-model="rating"
class="flex justify-center"
:aria-label="$t('issueReport.rating')"
>
<template #onicon>
<i class="pi pi-star-fill text-4xl"></i>
</template>
<template #officon>
<i class="pi pi-star text-4xl hover:text-highlight" />
</template>
</Rating>
</div>
</template>
</Card>
</div>
<div>
<ReportIssuePanel
error-type="Feedback"
:title="$t('issueReport.feedbackTitle')"
:extra-fields="[ratingField]"
:default-fields="['SystemStats', 'Settings']"
/>
</div>
</template>

<script setup lang="ts">
import Card from 'primevue/card'
import Rating from 'primevue/rating'
import { computed, ref } from 'vue'

import type { ReportField } from '@/types/issueReportTypes'

import ReportIssuePanel from './error/ReportIssuePanel.vue'

const rating = ref(null)

const ratingField = computed<ReportField>(() => {
return {
label: 'rating',
value: 'Rating',
optIn: false,
data: {
rating: rating.value
}
}
})
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// @ts-strict-ignore
import { mount } from '@vue/test-utils'
import PrimeVue from 'primevue/config'
import Tooltip from 'primevue/tooltip'
import { beforeAll, describe, expect, it, vi } from 'vitest'
import { createApp } from 'vue'
import { createI18n } from 'vue-i18n'

import enMessages from '@/locales/en/main.json'
import type { ReportField } from '@/types/issueReportTypes'

import FeedbackDialogContent from '../FeedbackDialogContent.vue'

const i18n = createI18n({
legacy: false,
locale: 'en',
messages: {
en: enMessages
}
})

vi.mock('@/components/error/ReportIssuePanel.vue', () => ({
default: {
name: 'ReportIssuePanel',
props: ['errorType', 'title', 'extraFields', 'defaultFields'],
template: '<div><slot /></div>'
}
}))

vi.mock('primevue/usetoast', () => ({
useToast: vi.fn(() => ({
add: vi.fn()
}))
}))

describe('FeedbackDialogContent', () => {
beforeAll(() => {
const app = createApp({})
app.use(PrimeVue)
app.use(i18n)
})

const mountComponent = (): any =>
mount(FeedbackDialogContent, {
global: {
plugins: [PrimeVue, i18n],
directives: { tooltip: Tooltip }
}
})

describe('Rating functionality', () => {
it('initializes rating to null', () => {
const wrapper = mountComponent()
expect(wrapper.vm.rating).toBeNull()
})

it('updates rating when a star is clicked', async () => {
const wrapper = mountComponent()
const ratingComponent = wrapper.findComponent({ name: 'Rating' })

// Click the 4th start (out of 5)
await ratingComponent.findAll('i').at(3)?.trigger('click')

// Verify the rating has been updated
expect(wrapper.vm.rating).toBe(4)
})
})

describe('ReportIssuePanel integration', () => {
it('passes correct props to ReportIssuePanel', () => {
const wrapper = mountComponent()
const reportIssuePanel = wrapper.findComponent({
name: 'ReportIssuePanel'
})

expect(reportIssuePanel.props()).toMatchObject({
errorType: 'Feedback',
title: enMessages['issueReport']['feedbackTitle'],
defaultFields: ['SystemStats', 'Settings']
})
})

it('includes rating in extraFields when updated', async () => {
const wrapper = mountComponent()
const reportIssuePanel = wrapper.findComponent({
name: 'ReportIssuePanel'
})

// Click the 5th star (out of 5)
const ratingComponent = wrapper.findComponent({ name: 'Rating' })
await ratingComponent.findAll('i').at(4)?.trigger('click')

const expectedExtraFields: ReportField[] = [
{
label: 'rating',
value: 'Rating',
optIn: false,
data: { rating: 5 }
}
]
expect(reportIssuePanel.props('extraFields')).toEqual(expectedExtraFields)
})

it('includes rating in extraFields as null when no rating is selected', () => {
const wrapper = mountComponent()
const reportIssuePanel = wrapper.findComponent({
name: 'ReportIssuePanel'
})

const expectedExtraFields: ReportField[] = [
{
label: 'rating',
value: 'Rating',
optIn: false,
data: { rating: null }
}
]
expect(reportIssuePanel.props('extraFields')).toEqual(expectedExtraFields)
})

it('resets the rating to null on re-render', async () => {
const wrapper = mountComponent()

// Simulate rating selection
wrapper.vm.rating = 4
expect(wrapper.vm.rating).toBe(4)

// Re-mount the component
wrapper.unmount()
const newWrapper = mountComponent()

// Verify rating is reset
expect(newWrapper.vm.rating).toBeNull()
})

it('passes all expected extraFields to ReportIssuePanel', () => {
const wrapper = mountComponent()
const reportIssuePanel = wrapper.findComponent({
name: 'ReportIssuePanel'
})

const extraFields = reportIssuePanel.props('extraFields') as ReportField[]

expect(extraFields).toContainEqual(
expect.objectContaining({
label: 'rating',
value: 'Rating',
optIn: false,
data: { rating: null }
})
)
})
})
})
8 changes: 6 additions & 2 deletions src/components/dialog/content/error/ReportIssuePanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<Panel>
<template #header>
<div class="flex items-center gap-2">
<span class="font-bold">{{ $t('issueReport.submitErrorReport') }}</span>
<span class="font-bold">{{ title }}</span>
</div>
</template>
<template #footer>
Expand All @@ -19,6 +19,7 @@
</template>
<div class="p-4 mt-4 border border-round surface-border shadow-1">
<CheckboxGroup
v-if="reportCheckboxes.length"
v-model="selection"
class="gap-4 mb-4"
:checkboxes="reportCheckboxes"
Expand Down Expand Up @@ -76,6 +77,7 @@ const props = defineProps<{
defaultFields?: DefaultField[]
extraFields?: ReportField[]
tags?: Record<string, string>
title?: string
}>()
const {
defaultFields = ['Workflow', 'Logs', 'SystemStats', 'Settings'],
Expand Down Expand Up @@ -116,7 +118,9 @@ const defaultReportCheckboxes = [
{ label: t('g.settings'), value: 'Settings' }
]
const reportCheckboxes = computed(() => [
...(props.extraFields?.map(({ label, value }) => ({ label, value })) ?? []),
...(props.extraFields
?.filter(({ optIn }) => optIn)
.map(({ label, value }) => ({ label, value })) ?? []),
...defaultReportCheckboxes.filter(({ value }) =>
defaultFields.includes(value as DefaultField)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ type ReportIssuePanelProps = {
errorType: string
defaultFields?: DefaultField[]
extraFields?: ReportField[]
tags?: Record<string, string>
title?: string
}

const i18n = createI18n({
Expand Down
18 changes: 14 additions & 4 deletions src/components/dialog/content/setting/AboutPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@
<div class="space-y-2">
<a
v-for="badge in aboutPanelStore.badges"
:key="badge.url"
:href="badge.url"
target="_blank"
:key="badge.label"
@click="handleBadgeClick(badge, $event)"
rel="noopener noreferrer"
class="about-badge inline-flex items-center no-underline"
:title="badge.url"
:title="badge.type === 'url' ? badge.url : null"
>
<Tag class="mr-2">
<template #icon>
Expand All @@ -36,13 +35,24 @@ import { onMounted } from 'vue'

import SystemStatsPanel from '@/components/common/SystemStatsPanel.vue'
import { useAboutPanelStore } from '@/stores/aboutPanelStore'
import { useCommandStore } from '@/stores/commandStore'
import { useSystemStatsStore } from '@/stores/systemStatsStore'
import type { AboutPageBadge } from '@/types/comfy'

import PanelTemplate from './PanelTemplate.vue'

const systemStatsStore = useSystemStatsStore()
const aboutPanelStore = useAboutPanelStore()

const handleBadgeClick = (badge: AboutPageBadge, event: MouseEvent) => {
if (badge.type === 'command') {
event.preventDefault()
useCommandStore().execute(badge.command)
} else {
window.open(badge.url, '_blank')
}
}

onMounted(async () => {
if (!systemStatsStore.systemStats) {
await systemStatsStore.fetchSystemStats()
Expand Down
2 changes: 1 addition & 1 deletion src/constants/coreMenuCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,5 @@ export const CORE_MENU_COMMANDS = [
'Comfy.Help.OpenComfyOrgDiscord'
]
],
[['Help'], ['Comfy.Help.AboutComfyUI']]
[['Help'], ['Comfy.Help.AboutComfyUI', 'Comfy.Feedback']]
]
Loading
Loading