feat(editor): Improve feedback buttons behavior (#18247)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Mutasem Aldmour
2025-08-14 10:05:36 +02:00
committed by GitHub
parent 59a08eed72
commit 83c3a98cf4
8 changed files with 925 additions and 75 deletions

View File

@@ -0,0 +1,277 @@
import { render, fireEvent, waitFor } from '@testing-library/vue';
import { createPinia, setActivePinia } from 'pinia';
import { nextTick } from 'vue';
import MessageRating from './MessageRating.vue';
const stubs = ['n8n-button', 'n8n-icon-button', 'n8n-input'];
// Mock i18n to return keys instead of translated text
vi.mock('@n8n/design-system/composables/useI18n', () => ({
useI18n: () => ({
t: (key: string) => key,
}),
}));
beforeEach(() => {
setActivePinia(createPinia());
});
describe('MessageRating', () => {
it('should render correctly with default props', () => {
const wrapper = render(MessageRating, {
global: { stubs },
});
expect(
wrapper.container.querySelector('[data-test-id="message-thumbs-up-button"]'),
).toBeTruthy();
expect(
wrapper.container.querySelector('[data-test-id="message-thumbs-down-button"]'),
).toBeTruthy();
expect(wrapper.html()).toMatchSnapshot();
});
describe('rating interactions', () => {
it('should emit feedback when thumbs up is clicked', async () => {
const wrapper = render(MessageRating, {
global: { stubs },
});
const upButton = wrapper.container.querySelector('[data-test-id="message-thumbs-up-button"]');
await fireEvent.click(upButton!);
expect(wrapper.emitted()).toHaveProperty('feedback');
expect(wrapper.emitted().feedback[0]).toEqual([{ rating: 'up' }]);
});
it('should emit feedback when thumbs down is clicked', async () => {
const wrapper = render(MessageRating, {
global: { stubs },
});
const downButton = wrapper.container.querySelector(
'[data-test-id="message-thumbs-down-button"]',
);
await fireEvent.click(downButton!);
expect(wrapper.emitted()).toHaveProperty('feedback');
expect(wrapper.emitted().feedback[0]).toEqual([{ rating: 'down' }]);
});
it('should hide rating buttons and show success after thumbs up', async () => {
const wrapper = render(MessageRating, {
global: { stubs },
});
const upButton = wrapper.container.querySelector('[data-test-id="message-thumbs-up-button"]');
await fireEvent.click(upButton!);
await nextTick();
expect(
wrapper.container.querySelector('[data-test-id="message-thumbs-up-button"]'),
).toBeFalsy();
expect(
wrapper.container.querySelector('[data-test-id="message-thumbs-down-button"]'),
).toBeFalsy();
expect(wrapper.getByText('assistantChat.builder.success')).toBeTruthy();
});
it('should hide rating buttons and show feedback form after thumbs down when showFeedback is true', async () => {
const wrapper = render(MessageRating, {
props: { showFeedback: true },
global: { stubs },
});
const downButton = wrapper.container.querySelector(
'[data-test-id="message-thumbs-down-button"]',
);
await fireEvent.click(downButton!);
await nextTick();
expect(
wrapper.container.querySelector('[data-test-id="message-thumbs-up-button"]'),
).toBeFalsy();
expect(
wrapper.container.querySelector('[data-test-id="message-thumbs-down-button"]'),
).toBeFalsy();
expect(
wrapper.container.querySelector('[data-test-id="message-feedback-input"]'),
).toBeTruthy();
expect(
wrapper.container.querySelector('[data-test-id="message-submit-feedback-button"]'),
).toBeTruthy();
});
it('should show success immediately after thumbs down when showFeedback is false', async () => {
const wrapper = render(MessageRating, {
props: { showFeedback: false },
global: { stubs },
});
const downButton = wrapper.container.querySelector(
'[data-test-id="message-thumbs-down-button"]',
);
await fireEvent.click(downButton!);
await nextTick();
expect(
wrapper.container.querySelector('[data-test-id="message-feedback-input"]'),
).toBeFalsy();
expect(wrapper.getByText('assistantChat.builder.success')).toBeTruthy();
});
});
describe('feedback form interactions', () => {
it('should submit feedback and show success', async () => {
const wrapper = render(MessageRating, {
props: { showFeedback: true },
global: { stubs: ['n8n-button', 'n8n-icon-button'] }, // Don't stub n8n-input
});
const downButton = wrapper.container.querySelector(
'[data-test-id="message-thumbs-down-button"]',
);
await fireEvent.click(downButton!);
await nextTick();
// Find the actual textarea element within the N8nInput component
const textarea = wrapper.container.querySelector(
'textarea[data-test-id="message-feedback-input"]',
);
await fireEvent.update(textarea!, 'This is my feedback about the response');
await nextTick();
const submitButton = wrapper.container.querySelector(
'[data-test-id="message-submit-feedback-button"]',
);
await fireEvent.click(submitButton!);
await nextTick();
expect(wrapper.emitted().feedback).toHaveLength(2);
expect(wrapper.emitted().feedback[1]).toEqual([
{ feedback: 'This is my feedback about the response' },
]);
expect(
wrapper.container.querySelector('[data-test-id="message-feedback-input"]'),
).toBeFalsy();
expect(wrapper.getByText('assistantChat.builder.success')).toBeTruthy();
});
it('should cancel feedback and return to rating buttons', async () => {
const wrapper = render(MessageRating, {
props: { showFeedback: true },
global: { stubs },
});
const downButton = wrapper.container.querySelector(
'[data-test-id="message-thumbs-down-button"]',
);
await fireEvent.click(downButton!);
await nextTick();
const cancelButton = wrapper.container.querySelector(
'n8n-button-stub[label="generic.cancel"]',
);
await fireEvent.click(cancelButton!);
await nextTick();
expect(
wrapper.container.querySelector('[data-test-id="message-feedback-input"]'),
).toBeFalsy();
expect(
wrapper.container.querySelector('[data-test-id="message-thumbs-up-button"]'),
).toBeTruthy();
expect(
wrapper.container.querySelector('[data-test-id="message-thumbs-down-button"]'),
).toBeTruthy();
});
it('should focus feedback input after thumbs down in regular mode', async () => {
const wrapper = render(MessageRating, {
props: { showFeedback: true, style: 'regular' },
global: { stubs },
});
const downButton = wrapper.container.querySelector(
'[data-test-id="message-thumbs-down-button"]',
);
await fireEvent.click(downButton!);
await waitFor(() => {
const feedbackInput = wrapper.container.querySelector(
'[data-test-id="message-feedback-input"]',
);
expect(feedbackInput).toBeTruthy();
});
});
it('should clear feedback text when cancelling', async () => {
const wrapper = render(MessageRating, {
props: { showFeedback: true },
global: { stubs },
});
const downButton = wrapper.container.querySelector(
'[data-test-id="message-thumbs-down-button"]',
);
await fireEvent.click(downButton!);
await nextTick();
const cancelButton = wrapper.container.querySelector(
'n8n-button-stub[label="generic.cancel"]',
);
await fireEvent.click(cancelButton!);
await nextTick();
const downButtonAgain = wrapper.container.querySelector(
'[data-test-id="message-thumbs-down-button"]',
);
await fireEvent.click(downButtonAgain!);
await nextTick();
const feedbackInputAfter = wrapper.container.querySelector(
'[data-test-id="message-feedback-input"]',
);
expect(feedbackInputAfter?.getAttribute('modelvalue')).toBe('');
});
});
describe('textarea rows based on style', () => {
it('should have 5 rows for regular style', async () => {
const wrapper = render(MessageRating, {
props: { showFeedback: true, style: 'regular' },
global: { stubs },
});
const downButton = wrapper.container.querySelector(
'[data-test-id="message-thumbs-down-button"]',
);
await fireEvent.click(downButton!);
await nextTick();
const feedbackInput = wrapper.container.querySelector(
'[data-test-id="message-feedback-input"]',
);
expect(feedbackInput?.getAttribute('rows')).toBe('5');
});
it('should have 3 rows for minimal style', async () => {
const wrapper = render(MessageRating, {
props: { showFeedback: true, style: 'minimal' },
global: { stubs },
});
const downButton = wrapper.container.querySelector(
'[data-test-id="message-thumbs-down-button"]',
);
await fireEvent.click(downButton!);
await nextTick();
const feedbackInput = wrapper.container.querySelector(
'[data-test-id="message-feedback-input"]',
);
expect(feedbackInput?.getAttribute('rows')).toBe('3');
});
});
});

View File

@@ -25,6 +25,7 @@ const emit = defineEmits<{
const { t } = useI18n();
const showRatingButtons = ref(true);
const showFeedbackArea = ref(false);
const feedbackInput = ref<HTMLInputElement | null>(null);
const showSuccess = ref(false);
const selectedRating = ref<'up' | 'down' | null>(null);
const feedback = ref('');
@@ -34,8 +35,13 @@ function onRateButton(rating: 'up' | 'down') {
showRatingButtons.value = false;
emit('feedback', { rating });
if (props.showFeedback) {
if (props.showFeedback && rating === 'down') {
showFeedbackArea.value = true;
setTimeout(() => {
if (feedbackInput.value) {
feedbackInput.value.focus();
}
}, 0);
} else {
showSuccess.value = true;
}
@@ -100,6 +106,7 @@ function onCancelFeedback() {
<div v-if="showFeedbackArea" :class="$style.feedbackContainer">
<N8nInput
ref="feedbackInput"
v-model="feedback"
:class="$style.feedbackInput"
type="textarea"

View File

@@ -0,0 +1,12 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`MessageRating > should render correctly with default props 1`] = `
"<div class="rating regular">
<div class="buttons">
<n8n-button-stub icon="thumbs-up" block="false" element="button" label="assistantChat.builder.thumbsUp" square="false" active="false" disabled="false" loading="false" outline="false" size="small" text="false" type="secondary" data-test-id="message-thumbs-up-button"></n8n-button-stub>
<n8n-button-stub icon="thumbs-down" block="false" element="button" label="assistantChat.builder.thumbsDown" square="false" active="false" disabled="false" loading="false" outline="false" size="small" text="false" type="secondary" data-test-id="message-thumbs-down-button"></n8n-button-stub>
</div>
<!--v-if-->
<!--v-if-->
</div>"
`;

View File

@@ -40,7 +40,7 @@ export default {
'assistantChat.builder.configuredNodes': 'Configured nodes',
'assistantChat.builder.thumbsUp': 'Helpful',
'assistantChat.builder.thumbsDown': 'Not helpful',
'assistantChat.builder.feedbackPlaceholder': 'Tell us about your experience',
'assistantChat.builder.feedbackPlaceholder': 'What went wrong?',
'assistantChat.builder.success': 'Thank you for your feedback!',
'assistantChat.builder.submit': 'Submit feedback',
'assistantChat.builder.workflowGenerated1': 'Your workflow was created successfully!',