Fix feedback UI localization in LikertScale component (#9253)

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
Graham Neubig 2025-06-20 11:50:39 -07:00 committed by GitHub
parent 078534c2ab
commit 8badcb7b35
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 111 additions and 12 deletions

View File

@ -0,0 +1,95 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { renderWithProviders } from "test-utils";
import { LikertScale } from "#/components/features/feedback/likert-scale";
import { I18nKey } from "#/i18n/declaration";
// Mock the mutation hook
vi.mock("#/hooks/mutation/use-submit-conversation-feedback", () => ({
useSubmitConversationFeedback: () => ({
mutate: vi.fn(),
}),
}));
describe("LikertScale", () => {
const user = userEvent.setup();
afterEach(() => {
vi.clearAllMocks();
});
it("should render with proper localized text for rating prompt", () => {
renderWithProviders(<LikertScale eventId={1} />);
// Check that the rating prompt is displayed with proper translation key
expect(screen.getByText(I18nKey.FEEDBACK$RATE_AGENT_PERFORMANCE)).toBeInTheDocument();
});
it("should show localized feedback reasons when rating is 3 or below", async () => {
renderWithProviders(<LikertScale eventId={1} />);
// Click on a rating of 3 (which should show reasons)
const threeStarButton = screen.getAllByRole("button")[2]; // 3rd button (rating 3)
await user.click(threeStarButton);
// Wait for reasons to appear
await waitFor(() => {
expect(screen.getByText(I18nKey.FEEDBACK$SELECT_REASON)).toBeInTheDocument();
});
// Check that all feedback reasons are properly localized
expect(screen.getByText(I18nKey.FEEDBACK$REASON_MISUNDERSTOOD_INSTRUCTION)).toBeInTheDocument();
expect(screen.getByText(I18nKey.FEEDBACK$REASON_FORGOT_CONTEXT)).toBeInTheDocument();
expect(screen.getByText(I18nKey.FEEDBACK$REASON_UNNECESSARY_CHANGES)).toBeInTheDocument();
expect(screen.getByText(I18nKey.FEEDBACK$REASON_OTHER)).toBeInTheDocument();
});
it("should show countdown message with proper localization", async () => {
renderWithProviders(<LikertScale eventId={1} />);
// Click on a rating of 2 (which should show reasons and countdown)
const twoStarButton = screen.getAllByRole("button")[1]; // 2nd button (rating 2)
await user.click(twoStarButton);
// Wait for countdown to appear
await waitFor(() => {
expect(screen.getByText(I18nKey.FEEDBACK$SELECT_REASON_COUNTDOWN)).toBeInTheDocument();
});
});
it("should show thank you message after submission", () => {
renderWithProviders(
<LikertScale eventId={1} initiallySubmitted={true} initialRating={4} />
);
// Check that thank you message is displayed with proper translation key
expect(screen.getByText(I18nKey.FEEDBACK$THANK_YOU_FOR_FEEDBACK)).toBeInTheDocument();
});
it("should render all 5 star rating buttons", () => {
renderWithProviders(<LikertScale eventId={1} />);
// Check that all 5 star buttons are rendered
const starButtons = screen.getAllByRole("button");
expect(starButtons).toHaveLength(5);
// Check that each button has proper aria-label
for (let i = 1; i <= 5; i++) {
expect(screen.getByLabelText(`Rate ${i} stars`)).toBeInTheDocument();
}
});
it("should not show reasons for ratings above 3", async () => {
renderWithProviders(<LikertScale eventId={1} />);
// Click on a rating of 5 (which should NOT show reasons)
const fiveStarButton = screen.getAllByRole("button")[4]; // 5th button (rating 5)
await user.click(fiveStarButton);
// Wait a bit to ensure reasons don't appear
await waitFor(() => {
expect(screen.queryByText(I18nKey.FEEDBACK$SELECT_REASON)).not.toBeInTheDocument();
});
});
});

View File

@ -1,6 +1,7 @@
import React, { useState, useEffect, useContext } from "react";
import { useTranslation } from "react-i18next";
import { cn } from "#/utils/utils";
import i18n from "#/i18n";
import { I18nKey } from "#/i18n/declaration";
import { useSubmitConversationFeedback } from "#/hooks/mutation/use-submit-conversation-feedback";
import { ScrollContext } from "#/context/scroll-context";
@ -14,19 +15,14 @@ interface LikertScaleProps {
initialReason?: string;
}
const FEEDBACK_REASONS = [
i18n.t("FEEDBACK$REASON_MISUNDERSTOOD_INSTRUCTION"),
i18n.t("FEEDBACK$REASON_FORGOT_CONTEXT"),
i18n.t("FEEDBACK$REASON_UNNECESSARY_CHANGES"),
i18n.t("FEEDBACK$REASON_OTHER"),
];
export function LikertScale({
eventId,
initiallySubmitted = false,
initialRating,
initialReason,
}: LikertScaleProps) {
const { t } = useTranslation();
const [selectedRating, setSelectedRating] = useState<number | null>(
initialRating || null,
);
@ -43,6 +39,14 @@ export function LikertScale({
// Get scroll context
const scrollContext = useContext(ScrollContext);
// Define feedback reasons using the translation hook
const FEEDBACK_REASONS = [
t(I18nKey.FEEDBACK$REASON_MISUNDERSTOOD_INSTRUCTION),
t(I18nKey.FEEDBACK$REASON_FORGOT_CONTEXT),
t(I18nKey.FEEDBACK$REASON_UNNECESSARY_CHANGES),
t(I18nKey.FEEDBACK$REASON_OTHER),
];
// If scrollContext is undefined, we're not inside a ScrollProvider
const scrollToBottom = scrollContext?.scrollDomToBottom;
const autoScroll = scrollContext?.autoScroll;
@ -188,8 +192,8 @@ export function LikertScale({
<div className="mt-3 flex flex-col gap-1">
<div className="text-sm text-gray-500 mb-1">
{isSubmitted
? i18n.t("FEEDBACK$THANK_YOU_FOR_FEEDBACK")
: i18n.t("FEEDBACK$RATE_AGENT_PERFORMANCE")}
? t(I18nKey.FEEDBACK$THANK_YOU_FOR_FEEDBACK)
: t(I18nKey.FEEDBACK$RATE_AGENT_PERFORMANCE)}
</div>
<div className="flex flex-col gap-1">
<span className="flex gap-2 items-center flex-wrap">
@ -220,11 +224,11 @@ export function LikertScale({
{showReasons && !isSubmitted && (
<div className="mt-1 flex flex-col gap-1">
<div className="text-xs text-gray-500 mb-1">
{i18n.t("FEEDBACK$SELECT_REASON")}
{t(I18nKey.FEEDBACK$SELECT_REASON)}
</div>
{countdown > 0 && (
<div className="text-xs text-gray-400 mb-1 italic">
{i18n.t("FEEDBACK$SELECT_REASON_COUNTDOWN", {
{t(I18nKey.FEEDBACK$SELECT_REASON_COUNTDOWN, {
countdown,
})}
</div>