mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat(editor): Improve feedback buttons behavior (#18247)
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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"
|
||||
|
||||
@@ -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>"
|
||||
`;
|
||||
@@ -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!',
|
||||
|
||||
Reference in New Issue
Block a user