diff --git a/backend/app/conversations/feedback/routes/routes.py b/backend/app/conversations/feedback/routes/routes.py
index 607dbc372..91172385e 100644
--- a/backend/app/conversations/feedback/routes/routes.py
+++ b/backend/app/conversations/feedback/routes/routes.py
@@ -14,6 +14,7 @@
from app.context_vars import session_id_ctx_var, user_id_ctx_var, client_id_ctx_var
from app.conversations.feedback.services.errors import InvalidOptionError, InvalidQuestionError, QuestionsFileError
from app.conversations.feedback.services.service import IUserFeedbackService, UserFeedbackService, NewFeedbackSpec
+from app.conversations.feedback.services.types import QuestionsConfig
from app.errors.constants import NO_PERMISSION_FOR_SESSION
from app.errors.errors import UnauthorizedSessionAccessError
from app.metrics.services.get_metrics_service import get_metrics_service
@@ -72,6 +73,32 @@ def add_user_feedback_routes(users_router: APIRouter, auth: Authentication):
"""
router = APIRouter(prefix="/feedback", tags=["users-feedback"])
+ @router.get("/questions",
+ status_code=HTTPStatus.OK,
+ response_model=QuestionsConfig,
+ responses={
+ HTTPStatus.INTERNAL_SERVER_ERROR: {"model": HTTPErrorResponse}
+ },
+ name="get questions configuration",
+ description="Get the questions configuration for the feedback form"
+ )
+ async def _get_questions_config(
+ user_feedback_service: IUserFeedbackService = Depends(_get_user_feedback_service)
+ ) -> QuestionsConfig:
+ """
+ Get the questions configuration for the feedback form.
+
+ :param user_feedback_service: Service for managing user feedback
+ :return: The questions configuration
+ :raises HTTPException: If there's an error loading the questions
+ """
+ try:
+ questions_data = await user_feedback_service.get_questions_config()
+ return questions_data
+ except QuestionsFileError as e:
+ logger.exception(e)
+ raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Failed to load questions configuration")
+
@router.patch("",
status_code=HTTPStatus.OK,
response_model=FeedbackResponse,
diff --git a/backend/app/conversations/feedback/routes/test_routes.py b/backend/app/conversations/feedback/routes/test_routes.py
index 4d72254b1..17b726c97 100644
--- a/backend/app/conversations/feedback/routes/test_routes.py
+++ b/backend/app/conversations/feedback/routes/test_routes.py
@@ -12,11 +12,11 @@
from fastapi import FastAPI, APIRouter
from fastapi.testclient import TestClient
-from app.conversations.feedback.services.errors import InvalidQuestionError, InvalidOptionError
+from app.conversations.feedback.services.errors import InvalidQuestionError, InvalidOptionError, QuestionsFileError
from app.conversations.feedback.services.service import IUserFeedbackService
from app.conversations.feedback.services.types import NewFeedbackSpec, NewFeedbackVersionSpec, NewFeedbackItemSpec, \
Feedback, \
- Version, FeedbackItem, Answer
+ Version, FeedbackItem, Answer, QuestionsConfig
from app.users.auth import UserInfo
from app.users.get_user_preferences_repository import get_user_preferences_repository
from app.users.repositories import IUserPreferenceRepository
@@ -84,6 +84,9 @@ async def upsert_user_feedback(self, user_id: str, session_id: int, feedback: Fe
async def get_answered_questions(self, user_id: str) -> list[int]:
raise NotImplementedError()
+ async def get_questions_config(self) -> QuestionsConfig:
+ raise NotImplementedError()
+
mocked_feedback_service = MockedFeedbackService()
# Mock the user preferences repository
@@ -351,3 +354,55 @@ async def test_upsert_feedback_payload_too_large(self, authenticated_client_with
assert "payload" in response.json()["detail"].lower()
# AND the service's upsert_user_feedback method was not called
_upsert_spy.assert_not_called()
+
+ @pytest.mark.asyncio
+ async def test_get_questions_config_success(self, authenticated_client_with_mocks: TestClientWithMocks):
+ client, mocked_service, _, _ = authenticated_client_with_mocks
+ # GIVEN a session ID for which the questions configuration is requested
+ given_session_id = 123
+
+ # GIVEN a valid questions configuration
+ given_config: QuestionsConfig = {
+ "test_question": {
+ "question_text": "Test question",
+ "description": "Test description",
+ "comment_placeholder": "Test placeholder",
+ "type": "yes_no",
+ "show_comments_on": "yes"
+ }
+ }
+ mocked_service.get_questions_config = AsyncMock(return_value=given_config)
+
+ # WHEN getting the questions configuration
+ response = client.get(f"/conversations/{given_session_id}/feedback/questions",)
+
+ # THEN the response should be OK
+ assert response.status_code == HTTPStatus.OK
+
+ # AND the response should contain the questions configuration
+ actual_config = response.json()
+ assert actual_config == given_config
+
+ # AND the service's get_questions_config method was called
+ mocked_service.get_questions_config.assert_called_once()
+
+ @pytest.mark.asyncio
+ async def test_get_questions_config_error(self, authenticated_client_with_mocks: TestClientWithMocks):
+ client, mocked_service, _, _ = authenticated_client_with_mocks
+ # GIVEN a session ID for which the questions configuration is requested
+ given_session_id = 123
+
+ # GIVEN the service raises a QuestionsFileError
+ mocked_service.get_questions_config = AsyncMock(side_effect=QuestionsFileError("Test error"))
+
+ # WHEN getting the questions configuration
+ response = client.get(f"/conversations/{given_session_id}/feedback/questions",)
+
+ # THEN the response should be INTERNAL_SERVER_ERROR
+ assert response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR
+
+ # AND the response should contain the error message
+ assert response.json()["detail"] == "Failed to load questions configuration"
+
+ # AND the service's get_questions_config method was called
+ mocked_service.get_questions_config.assert_called_once()
diff --git a/backend/app/conversations/feedback/services/questions-en.json b/backend/app/conversations/feedback/services/questions-en.json
index 905b951d6..d53d033e5 100644
--- a/backend/app/conversations/feedback/services/questions-en.json
+++ b/backend/app/conversations/feedback/services/questions-en.json
@@ -2,17 +2,29 @@
"interaction_ease": {
"question_text": "How easy or difficult was it to interact with Compass and understand its responses?",
"description": "This question is used to measure the Customer Effort Score (CES). The response is on a scale from 1 (Very difficult) to 5 (Very easy).",
- "comment_placeholder": "Please explain your rating and share any suggestions for improving the ease of interaction with Compass."
+ "comment_placeholder": "Please explain your rating and share any suggestions for improving the ease of interaction with Compass.",
+ "low_rating_label": "Very difficult",
+ "high_rating_label": "Very easy",
+ "type": "rating",
+ "max_rating": 5,
+ "display_rating": true
},
"clarity_of_skills": {
"question_text": "Did Compass help you gain a clearer understanding of your skills? If not, why?",
"description": "This question aims to measure Perceived Usefulness (PU). The user can answer TRUE (indicating Compass helped) or FALSE (indicating Compass did not help), with an optional text field for additional comments.",
- "comment_placeholder": "Please share why Compass did not help you gain a clearer understanding of your skills."
+ "comment_placeholder": "Please share why Compass did not help you gain a clearer understanding of your skills.",
+ "type": "yes_no",
+ "show_comments_on": "no"
},
"satisfaction_with_compass": {
"question_text": "How satisfied are you with Compass?",
"description": "This question is used to measure the Customer Satisfaction Score (CSAT). The response is on a scale from 1 (Very dissatisfied) to 5 (Very satisfied).",
- "comment_placeholder": null
+ "comment_placeholder": null,
+ "type": "rating",
+ "max_rating": 5,
+ "display_rating": true,
+ "low_rating_label": "Very dissatisfied",
+ "high_rating_label": "Very satisfied"
},
"work_experience_accuracy": {
"question_text": "Were there any aspects of your work experience information identified by Compass that were inaccurate?",
@@ -25,31 +37,47 @@
"other": "Other"
},
"description": "This question to identify specific inaccuracies in the work experience information. The response is an array of selected option keys, with optional additional comments.",
- "comment_placeholder": "Please provide more details about the inaccuracies in your work experience information identified by Compass."
+ "comment_placeholder": "Please provide more details about the inaccuracies in your work experience information identified by Compass.",
+ "type": "checkbox",
+ "low_rating_label": "Inaccurate",
+ "high_rating_label": "Very accurate"
},
"incorrect_skills": {
"question_text": "Are there any skills that Compass incorrectly identified for you?",
"description": "This question aims to indentify incorrectly identified skills The user can answer TRUE (indicating incorrect skills) or FALSE (indicating no incorrect skills). In case of TRUE, the user can provide additional comments.",
- "comment_placeholder": "Please list any skills that Compass incorrectly identified for you."
+ "comment_placeholder": "Please list any skills that Compass incorrectly identified for you.",
+ "type": "yes_no",
+ "show_comments_on": "yes"
},
"missing_skills": {
"question_text": "Are there any skills you have that Compass missed and did not identify?",
"description": "This question aims to identify missing skills. Responses should be TRUE (indicating missing skills) or FALSE (indicating no missing skills). In case of TRUE, the user can provide additional comments.",
- "comment_placeholder": "Please list any skills you have that Compass did not identify."
+ "comment_placeholder": "Please list any skills you have that Compass did not identify.",
+ "type": "yes_no",
+ "show_comments_on": "yes"
},
"perceived_bias": {
"question_text": "Did you feel that Compass treated you unfairly by making assumptions about your background, language, or other personal characteristics? If so, please describe your experience.",
"description": "This question aims to identify perceived bias. Responses should be TRUE (indicating bias) or FALSE (indicating no bias), with an optional text field for additional comments.",
- "comment_placeholder": "Please share more details about your experience. Include specific examples of when you felt Compass treated you unfairly or made assumptions about your background, language, or other personal characteristics."
+ "comment_placeholder": "Please share more details about your experience. Include specific examples of when you felt Compass treated you unfairly or made assumptions about your background, language, or other personal characteristics.",
+ "type": "yes_no",
+ "show_comments_on": "yes"
},
"recommendation": {
"question_text": "How likely are you to recommend Compass to other job seekers?",
"description": "This question is used to measure the Net Promoter Score (NPS). The response is on a scale from 1 (Not at all likely) to 5 (Extremely likely).",
- "comment_placeholder": null
+ "comment_placeholder": null,
+ "type": "rating",
+ "max_rating": 5,
+ "display_rating": true,
+ "low_rating_label": "Unlikely",
+ "high_rating_label": "Likely"
},
"additional_feedback": {
"question_text": "Please share any additional feedback or suggestions you have for improving Compass.",
"description": "Used to collect any additional feedback or suggestions for improving Compass from the user. The response is in the form of a text.",
- "comment_placeholder": "We'd love to hear your thoughts! Please share any additional feedback or suggestions you have for improving Compass."
+ "comment_placeholder": "We'd love to hear your thoughts! Please share any additional feedback or suggestions you have for improving Compass.",
+ "type": "rating",
+ "display_rating": false
}
}
diff --git a/backend/app/conversations/feedback/services/service.py b/backend/app/conversations/feedback/services/service.py
index f6662df00..758f769ec 100644
--- a/backend/app/conversations/feedback/services/service.py
+++ b/backend/app/conversations/feedback/services/service.py
@@ -5,14 +5,15 @@
import json
import logging
from abc import ABC, abstractmethod
-from typing import Any, Dict, cast
+from typing import Any, Dict
from pathlib import Path
from app.conversations.feedback.repository import IUserFeedbackRepository
from app.app_config import get_application_config
from app.metrics.services.service import IMetricsService
from app.metrics.types import FeedbackProvidedEvent, FeedbackRatingValueEvent, FeedbackTypeLiteral
-from .types import Feedback, NewFeedbackSpec, FeedbackItem, Version, AnsweredQuestions
+from .types import Feedback, NewFeedbackSpec, FeedbackItem, Version, AnsweredQuestions, QuestionsConfig, Question, YesNoQuestion, RatingQuestion, \
+ CheckboxQuestion
from .errors import (
InvalidQuestionError,
QuestionsFileError
@@ -91,6 +92,11 @@ def calculate_ces_value(answer: int) -> int:
async def load_questions() -> Dict[str, Any]:
+ """
+ Load questions from the JSON file.
+ :return: Dictionary containing questions data
+ :raises QuestionsFileError: If there's an error loading the questions file
+ """
global questions_cache
if not questions_cache:
questions_file = Path(__file__).parent / "questions-en.json"
@@ -134,6 +140,16 @@ async def get_answered_questions(self, user_id: str) -> AnsweredQuestions:
"""
raise NotImplementedError()
+ @abstractmethod
+ async def get_questions_config(self) -> QuestionsConfig:
+ """
+ Get the questions configuration for the feedback form.
+
+ :return: The questions configuration
+ :raises QuestionsFileError: If there's an error loading the questions file
+ """
+ raise NotImplementedError()
+
class UserFeedbackService(IUserFeedbackService):
"""
@@ -145,6 +161,32 @@ def __init__(self, user_feedback_repository: IUserFeedbackRepository, metrics_se
self._user_feedback_repository: IUserFeedbackRepository = user_feedback_repository
self._metrics_service: IMetricsService = metrics_service
+ async def get_questions_config(self) -> QuestionsConfig:
+ """
+ Get the questions configuration for the feedback form.
+
+ :return: The questions configuration
+ :raises QuestionsFileError: If there's an error loading the questions file
+ """
+ questions_data = await load_questions()
+ if not questions_data:
+ raise QuestionsFileError("No questions data available")
+
+ # Convert the raw questions data to the proper Pydantic models
+ config: QuestionsConfig = {}
+ for question_id, question_data in questions_data.items():
+ question_type = question_data.get("type")
+ if question_type == "yes_no":
+ config[question_id] = YesNoQuestion(**question_data)
+ elif question_type == "rating":
+ config[question_id] = RatingQuestion(**question_data)
+ elif question_type == "checkbox":
+ config[question_id] = CheckboxQuestion(**question_data)
+ else:
+ raise QuestionsFileError(f"Invalid question type '{question_type}' for question '{question_id}'")
+
+ return config
+
async def upsert_user_feedback(self, user_id: str, session_id: int, feedback_spec: NewFeedbackSpec) -> Feedback:
questions_data = await load_questions()
if not questions_data:
@@ -225,7 +267,7 @@ async def _capture_metrics(self, user_id: str, session_id: int, feedback_items:
logger.error(f"Rating value {item.answer.rating_numeric} for question {item.question_id} is out of range (1-5)")
continue
feedback_type: FeedbackTypeLiteral
- match item.question_id:
+ match item.question_id: # TODO: check unreachable
case "recommendation":
feedback_type = "NPS"
value = calculate_nps_value(item.answer.rating_numeric)
diff --git a/backend/app/conversations/feedback/services/test_service.py b/backend/app/conversations/feedback/services/test_service.py
index 611789867..4df2f6ea4 100644
--- a/backend/app/conversations/feedback/services/test_service.py
+++ b/backend/app/conversations/feedback/services/test_service.py
@@ -20,7 +20,8 @@
from app.conversations.feedback.services.service import UserFeedbackService
from app.conversations.feedback.services.service import questions_cache
from app.conversations.feedback.services.types import Feedback, FeedbackItem, Version, Answer, NewFeedbackSpec, \
- NewFeedbackItemSpec, NewFeedbackVersionSpec, SimplifiedAnswer
+ NewFeedbackItemSpec, NewFeedbackVersionSpec, SimplifiedAnswer, QuestionsConfig, YesNoQuestion, RatingQuestion, \
+ CheckboxQuestion
from app.metrics.constants import EventType
from app.metrics.services.service import IMetricsService
from app.metrics.types import FeedbackProvidedEvent, FeedbackRatingValueEvent, AbstractCompassMetricEvent
@@ -524,3 +525,85 @@ async def test_record_multiple_feedback_rating_value_events(self,
assert actual_events[2].value == 1
assert actual_events[3].feedback_type == "CES"
assert actual_events[3].value == 0
+
+
+@pytest.mark.asyncio
+async def test_get_questions_config_success(_mock_feedback_repository: IUserFeedbackRepository,
+ _mock_metrics_service: IMetricsService):
+ # GIVEN a service instance
+ service = UserFeedbackService(
+ user_feedback_repository=_mock_feedback_repository,
+ metrics_service=_mock_metrics_service
+ )
+
+ # WHEN getting the questions configuration
+ config = await service.get_questions_config()
+
+ # THEN the configuration should be properly loaded and converted to Pydantic models
+ assert isinstance(config, dict)
+ for question_id, question in config.items():
+ assert question_id in actual_questions_json
+ raw_question = actual_questions_json[question_id]
+
+ # Check base fields
+ assert question.question_text == raw_question["question_text"]
+ assert question.description == raw_question["description"]
+ assert question.comment_placeholder == raw_question["comment_placeholder"]
+ assert question.type == raw_question["type"]
+
+ # Check type-specific fields
+ if isinstance(question, YesNoQuestion):
+ assert question.show_comments_on == raw_question["show_comments_on"]
+ elif isinstance(question, RatingQuestion):
+ assert question.max_rating == raw_question.get("max_rating")
+ assert question.display_rating == raw_question.get("display_rating")
+ assert question.low_rating_label == raw_question.get("low_rating_label")
+ assert question.high_rating_label == raw_question.get("high_rating_label")
+ elif isinstance(question, CheckboxQuestion):
+ assert question.options == raw_question["options"]
+ assert question.low_rating_label == raw_question.get("low_rating_label")
+ assert question.high_rating_label == raw_question.get("high_rating_label")
+
+
+@pytest.mark.asyncio
+async def test_get_questions_config_invalid_type(_mock_feedback_repository: IUserFeedbackRepository,
+ _mock_metrics_service: IMetricsService):
+ # GIVEN a service instance
+ service = UserFeedbackService(
+ user_feedback_repository=_mock_feedback_repository,
+ metrics_service=_mock_metrics_service
+ )
+
+ # AND a question with an invalid type
+ invalid_question = {
+ "question_text": "Test question",
+ "description": "Test description",
+ "comment_placeholder": "Test placeholder",
+ "type": "invalid_type"
+ }
+
+ # WHEN loading questions with an invalid type
+ with patch('app.conversations.feedback.services.service.load_questions',
+ return_value={"test_question": invalid_question}):
+ # THEN a QuestionsFileError should be raised
+ with pytest.raises(QuestionsFileError) as exc_info:
+ await service.get_questions_config()
+ assert "Invalid question type" in str(exc_info.value)
+
+
+@pytest.mark.asyncio
+async def test_get_questions_config_empty(_mock_feedback_repository: IUserFeedbackRepository,
+ _mock_metrics_service: IMetricsService):
+ # GIVEN a service instance
+ service = UserFeedbackService(
+ user_feedback_repository=_mock_feedback_repository,
+ metrics_service=_mock_metrics_service
+ )
+
+ # AND no questions data available
+ with patch('app.conversations.feedback.services.service.load_questions',
+ return_value=None):
+ # THEN a QuestionsFileError should be raised
+ with pytest.raises(QuestionsFileError) as exc_info:
+ await service.get_questions_config()
+ assert "No questions data available" in str(exc_info.value)
diff --git a/backend/app/conversations/feedback/services/types.py b/backend/app/conversations/feedback/services/types.py
index 3a7a97347..40d1adddf 100644
--- a/backend/app/conversations/feedback/services/types.py
+++ b/backend/app/conversations/feedback/services/types.py
@@ -2,6 +2,8 @@
Module containing type definitions for the feedback feature.
"""
from datetime import datetime
+from enum import Enum
+from typing import TypeAlias, Literal
from typing import TypeAlias
from pydantic import BaseModel, Field
@@ -10,6 +12,66 @@
from common_libs.time_utilities import get_now
+class QuestionType(str, Enum):
+ """Type of feedback question."""
+ YES_NO = "yes_no"
+ RATING = "rating"
+ CHECKBOX = "checkbox"
+
+
+class BaseQuestion(BaseModel):
+ """Base model for all question types."""
+ question_text: str
+ """Text of the question"""
+ description: str
+ """Description of the question"""
+ comment_placeholder: str | None
+ """Placeholder text for comments"""
+ type: QuestionType
+ """Type of the question"""
+
+ class Config:
+ extra = "forbid"
+
+
+class YesNoQuestion(BaseQuestion):
+ """Model for yes/no questions."""
+ type: Literal[QuestionType.YES_NO]
+ show_comments_on: Literal["yes", "no"]
+ """Whether to show comments field on yes or no answer"""
+
+
+class RatingQuestion(BaseQuestion):
+ """Model for rating questions."""
+ type: Literal[QuestionType.RATING]
+ max_rating: int | None = None
+ """Maximum rating value"""
+ display_rating: bool | None = None
+ """Whether to display the rating"""
+ low_rating_label: str | None = None
+ """Label for low rating"""
+ high_rating_label: str | None = None
+ """Label for high rating"""
+
+
+class CheckboxQuestion(BaseQuestion):
+ """Model for checkbox questions."""
+ type: Literal[QuestionType.CHECKBOX]
+ options: dict[str, str]
+ """Available options as key-value pairs"""
+ low_rating_label: str | None = None
+ """Label for low rating"""
+ high_rating_label: str | None = None
+ """Label for high rating"""
+
+
+Question = YesNoQuestion | RatingQuestion | CheckboxQuestion
+"""Union type for all question types"""
+
+QuestionsConfig = dict[str, Question]
+"""Type alias for the questions configuration"""
+
+
class Answer(BaseModel):
"""
The core model for storing user answers to feedback questions.
diff --git a/frontend-new/.storybook/preview.tsx b/frontend-new/.storybook/preview.tsx
index 7c4606e70..7666d88fa 100644
--- a/frontend-new/.storybook/preview.tsx
+++ b/frontend-new/.storybook/preview.tsx
@@ -22,6 +22,7 @@ import SnackbarProvider from "../src/theme/SnackbarProvider/SnackbarProvider";
import { IsOnlineContext } from "../src/app/isOnlineProvider/IsOnlineProvider";
import { initSentry } from "../src/sentryInit";
import { ChatProvider } from "../src/chat/ChatContext";
+import { FeedbackProvider } from "../src/feedback/overallFeedback/feedbackContext/FeedbackContext";
const preview: Preview = {
parameters: {
@@ -119,9 +120,11 @@ export const decorators = [
diff --git a/frontend-new/package.json b/frontend-new/package.json
index eaf530d19..8d164174f 100644
--- a/frontend-new/package.json
+++ b/frontend-new/package.json
@@ -152,4 +152,4 @@
"last 1 safari version"
]
}
-}
+}
\ No newline at end of file
diff --git a/frontend-new/src/chat/Chat.test.tsx b/frontend-new/src/chat/Chat.test.tsx
index bc79af9ff..a2f4e354c 100644
--- a/frontend-new/src/chat/Chat.test.tsx
+++ b/frontend-new/src/chat/Chat.test.tsx
@@ -185,6 +185,15 @@ jest.mock("src/chat/ChatContext", () => {
}
})
+// mock the feedback context
+jest.mock("src/feedback/overallFeedback/feedbackContext/FeedbackContext", () => {
+ const actual = jest.requireActual("src/feedback/overallFeedback/feedbackContext/FeedbackContext");
+ return {
+ ...actual,
+ FeedbackProvider: jest.fn().mockImplementation(({ children }) =>
{children}
)
+ }
+})
+
describe("Chat", () => {
// ExperienceService methods to be mocked
const mockGetExperiences = jest.fn();
diff --git a/frontend-new/src/chat/Chat.tsx b/frontend-new/src/chat/Chat.tsx
index c554873b5..e50f90c3c 100644
--- a/frontend-new/src/chat/Chat.tsx
+++ b/frontend-new/src/chat/Chat.tsx
@@ -37,6 +37,7 @@ import { CompassChatMessageProps } from "./chatMessage/compassChatMessage/Compas
import {
CONVERSATION_CONCLUSION_CHAT_MESSAGE_TYPE
} from "./chatMessage/conversationConclusionChatMessage/ConversationConclusionChatMessage";
+import { FeedbackProvider } from "src/feedback/overallFeedback/feedbackContext/FeedbackContext";
export const INACTIVITY_TIMEOUT = 3 * 60 * 1000; // in milliseconds
// Set the interval to check every TIMEOUT/3,
@@ -412,88 +413,89 @@ const Chat: React.FC = ({ showInactiveSessionAlert = false, disableIn
{isLoggingOut ? (
) : (
-
- {
- // expect(screen.getByTestId(DATA_TEST_ID.CHAT_CONTAINER)).toHaveAttribute("is-initialized", "true");
- // });
- // This technique can solve the "Warning: An update to Chat inside a test was not wrapped in act(...)" warning.
- is-initialized={`${initialized}`}
+
+
-
- setNewConversationDialog(true)}
- experiencesExplored={exploredExperiences}
- exploredExperiencesNotification={exploredExperiencesNotification}
- setExploredExperiencesNotification={setExploredExperiencesNotification}
- conversationCompleted={conversationCompleted}
- timeUntilNotification={timeUntilFeedbackNotification}
- progressPercentage={currentPhase.percentage}
- />
-
-
-
+ {
+ // expect(screen.getByTestId(DATA_TEST_ID.CHAT_CONTAINER)).toHaveAttribute("is-initialized", "true");
+ // });
+ // This technique can solve the "Warning: An update to Chat inside a test was not wrapped in act(...)" warning.
+ is-initialized={`${initialized}`}
+ >
+
+ setNewConversationDialog(true)}
+ experiencesExplored={exploredExperiences}
+ exploredExperiencesNotification={exploredExperiencesNotification}
+ setExploredExperiencesNotification={setExploredExperiencesNotification}
+ conversationCompleted={conversationCompleted}
+ timeUntilNotification={timeUntilFeedbackNotification}
+ progressPercentage={currentPhase.percentage}
+ />
+
+
+
+
+
+
+
+ {showBackdrop && }
+
+
+
-
-
-
- {showBackdrop && }
-
-
+ {newConversationDialog && (
+
+ Once you start a new conversation, all messages from the current conversation will be lost forever.
+
+
+ Are you sure you want to start a new conversation?
+ >
+ }
+ onCancel={() => setNewConversationDialog(false)}
+ onConfirm={handleConfirmNewConversation}
+ onDismiss={() => setNewConversationDialog(false)}
+ cancelButtonText="Cancel"
+ confirmButtonText="Yes, I'm sure"
/>
-
-
-
- {newConversationDialog && (
-
- Once you start a new conversation, all messages from the current conversation will be lost forever.
-
-
- Are you sure you want to start a new conversation?
- >
- }
- onCancel={() => setNewConversationDialog(false)}
- onConfirm={handleConfirmNewConversation}
- onDismiss={() => setNewConversationDialog(false)}
- cancelButtonText="Cancel"
- confirmButtonText="Yes, I'm sure"
- />
- )}
-
+ )}
+
+
)}
);
diff --git a/frontend-new/src/chat/ChatContext.test.tsx b/frontend-new/src/chat/ChatContext.test.tsx
index 4045c71fc..8a1b81d55 100644
--- a/frontend-new/src/chat/ChatContext.test.tsx
+++ b/frontend-new/src/chat/ChatContext.test.tsx
@@ -4,7 +4,7 @@ import "src/_test_utilities/consoleMock";
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import { ChatProvider, useChatContext } from "./ChatContext";
-import { FeedbackStatus } from "src/feedback/overallFeedback/feedbackForm/FeedbackForm";
+import { FeedbackStatus } from "src/feedback/overallFeedback/overallFeedbackForm/OverallFeedbackForm";
import { PersistentStorageService } from "src/app/PersistentStorageService/PersistentStorageService";
import { ConversationMessageSender } from "src/chat/ChatService/ChatService.types";
import CompassChatMessage, { CompassChatMessageProps } from "src/chat/chatMessage/compassChatMessage/CompassChatMessage";
diff --git a/frontend-new/src/chat/ChatContext.tsx b/frontend-new/src/chat/ChatContext.tsx
index 59d0c898a..520e0658b 100644
--- a/frontend-new/src/chat/ChatContext.tsx
+++ b/frontend-new/src/chat/ChatContext.tsx
@@ -1,5 +1,5 @@
import React, { createContext, useContext, ReactNode, useState, useMemo } from 'react';
-import { FeedbackStatus } from 'src/feedback/overallFeedback/feedbackForm/FeedbackForm';
+import { FeedbackStatus } from 'src/feedback/overallFeedback/overallFeedbackForm/OverallFeedbackForm';
import { PersistentStorageService } from 'src/app/PersistentStorageService/PersistentStorageService';
import { IChatMessage } from "src/chat/Chat.types";
@@ -30,7 +30,7 @@ export const ChatProvider: React.FC = ({ children, handleOpen
handleOpenExperiencesDrawer,
removeMessage,
addMessage,
- feedbackStatus,
+ feedbackStatus, //TODO: move to feedback context
setFeedbackStatus,
isAccountConverted,
setIsAccountConverted: (converted: boolean) => {
diff --git a/frontend-new/src/chat/chatMessage/conversationConclusionChatMessage/ConversationConclusionChatMessage.stories.tsx b/frontend-new/src/chat/chatMessage/conversationConclusionChatMessage/ConversationConclusionChatMessage.stories.tsx
index cd06dca60..527aee09f 100644
--- a/frontend-new/src/chat/chatMessage/conversationConclusionChatMessage/ConversationConclusionChatMessage.stories.tsx
+++ b/frontend-new/src/chat/chatMessage/conversationConclusionChatMessage/ConversationConclusionChatMessage.stories.tsx
@@ -1,7 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react";
import ConversationConclusionChatMessage from "src/chat/chatMessage/conversationConclusionChatMessage/ConversationConclusionChatMessage";
import { ChatProvider, useChatContext } from "src/chat/ChatContext";
-import { FeedbackStatus } from "src/feedback/overallFeedback/feedbackForm/FeedbackForm";
+import { FeedbackStatus } from "src/feedback/overallFeedback/overallFeedbackForm/OverallFeedbackForm";
import { useEffect } from "react";
const withChatContext = (feedbackStatus: FeedbackStatus) => (Story: any) => {
diff --git a/frontend-new/src/chat/chatMessage/conversationConclusionChatMessage/conversationConclusionFooter/ConversationConclusionFooter.stories.tsx b/frontend-new/src/chat/chatMessage/conversationConclusionChatMessage/conversationConclusionFooter/ConversationConclusionFooter.stories.tsx
index e176b0b54..58214f033 100644
--- a/frontend-new/src/chat/chatMessage/conversationConclusionChatMessage/conversationConclusionFooter/ConversationConclusionFooter.stories.tsx
+++ b/frontend-new/src/chat/chatMessage/conversationConclusionChatMessage/conversationConclusionFooter/ConversationConclusionFooter.stories.tsx
@@ -2,7 +2,7 @@ import React, { ReactNode, useMemo } from "react";
import { Meta, StoryObj } from "@storybook/react";
import ConversationConclusionFooter from "./ConversationConclusionFooter";
import { ChatProvider, useChatContext } from "src/chat/ChatContext";
-import { FeedbackItem, QUESTION_KEYS } from "src/feedback/overallFeedback/overallFeedbackService/OverallFeedback.service.types";
+import { FeedbackItem } from "src/feedback/overallFeedback/overallFeedbackService/OverallFeedback.service.types";
import AuthenticationStateService from "src/auth/services/AuthenticationState.service";
import { TabiyaUser } from "src/auth/auth.types";
import UserPreferencesStateService from "src/userPreferences/UserPreferencesStateService";
@@ -12,7 +12,14 @@ import {
} from "src/userPreferences/UserPreferencesService/userPreferences.types";
import { PersistentStorageService } from "src/app/PersistentStorageService/PersistentStorageService";
import { getBackendUrl } from "src/envService";
-import { FeedbackStatus } from "src/feedback/overallFeedback/feedbackForm/FeedbackForm";
+import { FeedbackStatus } from "src/feedback/overallFeedback/overallFeedbackForm/OverallFeedbackForm";
+
+import {
+ CUSTOMER_SATISFACTION_QUESTION_KEY
+} from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/customerSatisfactionRating/constants";
+import {
+ ADDITIONAL_FEEDBACK_QUESTION_ID
+} from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/additionalFeedback/constants";
// Mock feedback data
const mockFeedbackInProgress: FeedbackItem[] = [
@@ -62,10 +69,10 @@ const StorybookWrapper = ({
const answeredQuestions = [];
if (hasSubmittedFeedback) {
- answeredQuestions.push(QUESTION_KEYS.OVERALL_SATISFACTION);
+ answeredQuestions.push(ADDITIONAL_FEEDBACK_QUESTION_ID);
}
if (hasSubmittedCustomerSatisfactionRating) {
- answeredQuestions.push(QUESTION_KEYS.CUSTOMER_SATISFACTION);
+ answeredQuestions.push(CUSTOMER_SATISFACTION_QUESTION_KEY);
}
// Mock the session feedback on window
diff --git a/frontend-new/src/chat/chatMessage/conversationConclusionChatMessage/conversationConclusionFooter/ConversationConclusionFooter.test.tsx b/frontend-new/src/chat/chatMessage/conversationConclusionChatMessage/conversationConclusionFooter/ConversationConclusionFooter.test.tsx
index 019d28e50..2790915ad 100644
--- a/frontend-new/src/chat/chatMessage/conversationConclusionChatMessage/conversationConclusionFooter/ConversationConclusionFooter.test.tsx
+++ b/frontend-new/src/chat/chatMessage/conversationConclusionChatMessage/conversationConclusionFooter/ConversationConclusionFooter.test.tsx
@@ -14,15 +14,19 @@ import authenticationStateService from "src/auth/services/AuthenticationState.se
import { mockBrowserIsOnLine } from "src/_test_utilities/mockBrowserIsOnline";
import CustomerSatisfactionRating, {
DATA_TEST_ID as CUSTOMER_SATISFACTION_RATING_DATA_TEST_ID,
-} from "src/feedback/overallFeedback/feedbackForm/components/customerSatisfactionRating/CustomerSatisfaction";
+} from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/customerSatisfactionRating/CustomerSatisfaction";
+import { FeedbackProvider } from "src/feedback/overallFeedback/feedbackContext/FeedbackContext";
+import { resetAllMethodMocks } from "src/_test_utilities/resetAllMethodMocks";
+import { DATA_TEST_ID as OVERALL_FEEDBACK_FORM_DATA_TEST_ID } from "src/feedback/overallFeedback/overallFeedbackForm/OverallFeedbackForm";
+import { DATA_TEST_ID as ANONYMOUS_ACCOUNT_CONVERSION_DIALOG_DATA_TEST_ID } from "src/auth/components/anonymousAccountConversionDialog/AnonymousAccountConversionDialog";
// Mock external dependencies
jest.mock("src/app/PersistentStorageService/PersistentStorageService");
jest.mock("src/auth/services/AuthenticationState.service");
// mock the customer satisfaction component
-jest.mock("src/feedback/overallFeedback/feedbackForm/components/customerSatisfactionRating/CustomerSatisfaction", () => {
- const actual = jest.requireActual("src/feedback/overallFeedback/feedbackForm/components/customerSatisfactionRating/CustomerSatisfaction");
+jest.mock("src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/customerSatisfactionRating/CustomerSatisfaction", () => {
+ const actual = jest.requireActual("src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/customerSatisfactionRating/CustomerSatisfaction");
return {
...actual,
__esModule: true,
@@ -30,17 +34,37 @@ jest.mock("src/feedback/overallFeedback/feedbackForm/components/customerSatisfac
};
});
+jest.mock("src/feedback/overallFeedback/overallFeedbackForm/OverallFeedbackForm", () => {
+ const actual = jest.requireActual("src/feedback/overallFeedback/overallFeedbackForm/OverallFeedbackForm");
+ return {
+ ...actual,
+ __esModule: true,
+ default: jest.fn(() => ),
+ };
+})
+
+jest.mock("src/auth/components/anonymousAccountConversionDialog/AnonymousAccountConversionDialog", () => {
+ const actual = jest.requireActual("src/auth/components/anonymousAccountConversionDialog/AnonymousAccountConversionDialog");
+ return {
+ ...actual,
+ __esModule: true,
+ default: jest.fn(() => ),
+ };
+});
+
describe("ConversationConclusionFooter", () => {
const givenMockHandleOpenExperiencesDrawer = jest.fn();
const givenMockRemoveMessage = jest.fn();
const givenMockAddMessage = jest.fn();
const givenMockSetFeedbackStatus = jest.fn();
- const renderWithChatProvider = () => {
+ const renderWithProviders = () => {
return render(
-
-
- ,
+
+
+
+
+
);
};
@@ -56,6 +80,15 @@ describe("ConversationConclusionFooter", () => {
(authenticationStateService.getInstance as jest.Mock).mockReturnValue({
getUser: () => ({ name: "Test User", email: "test@example.com" }),
});
+
+ const givenSessionId = 1234;
+ jest.spyOn(UserPreferencesStateService.getInstance(), "getActiveSessionId").mockReturnValue(givenSessionId);
+ jest.spyOn(UserPreferencesStateService.getInstance(), "activeSessionHasOverallFeedback").mockReturnValue(false);
+ jest.spyOn(UserPreferencesStateService.getInstance(), "activeSessionHasCustomerSatisfactionRating").mockReturnValue(false);
+ });
+
+ afterEach(() => {
+ resetAllMethodMocks(UserPreferencesStateService.getInstance())
});
describe("render tests", () => {
@@ -65,7 +98,7 @@ describe("ConversationConclusionFooter", () => {
jest.spyOn(UserPreferencesStateService.getInstance(), "activeSessionHasOverallFeedback").mockReturnValueOnce(false);
// WHEN the component is rendered
- renderWithChatProvider();
+ renderWithProviders();
// THEN expect no errors or warnings to have occurred
expect(console.error).not.toHaveBeenCalled();
@@ -84,13 +117,12 @@ describe("ConversationConclusionFooter", () => {
});
describe("feedback", () => {
-
test("should not show feedback request if rating is not submitted", () => {
// GIVEN the user has not submitted a customer satisfaction rating
jest.spyOn(UserPreferencesStateService.getInstance(), "activeSessionHasCustomerSatisfactionRating").mockReturnValueOnce(false);
// WHEN the component is rendered
- renderWithChatProvider();
+ renderWithProviders();
// THEN expect the feedback message not to be displayed
expect(screen.queryByTestId(DATA_TEST_ID.FEEDBACK_MESSAGE_TEXT)).not.toBeInTheDocument();
@@ -107,7 +139,7 @@ describe("ConversationConclusionFooter", () => {
jest.spyOn(UserPreferencesStateService.getInstance(), "activeSessionHasCustomerSatisfactionRating").mockReturnValueOnce(true);
// WHEN the component is rendered
- renderWithChatProvider();
+ renderWithProviders();
// THEN expect the feedback message to be displayed
expect(screen.getByTestId(DATA_TEST_ID.FEEDBACK_MESSAGE_TEXT)).toBeInTheDocument();
@@ -141,7 +173,7 @@ describe("ConversationConclusionFooter", () => {
jest.spyOn(UserPreferencesStateService.getInstance(), "activeSessionHasOverallFeedback").mockReturnValueOnce(false);
// WHEN the component is rendered
- renderWithChatProvider();
+ renderWithProviders();
// THEN expect the feedback in progress message to be displayed
expect(screen.getByTestId(DATA_TEST_ID.FEEDBACK_IN_PROGRESS_MESSAGE)).toBeInTheDocument();
@@ -157,14 +189,14 @@ describe("ConversationConclusionFooter", () => {
jest.spyOn(UserPreferencesStateService.getInstance(), "activeSessionHasCustomerSatisfactionRating").mockReturnValueOnce(true);
// WHEN the component is rendered
- renderWithChatProvider();
+ renderWithProviders();
// AND the feedback button is clicked
const feedbackButton = screen.getByTestId(DATA_TEST_ID.FEEDBACK_FORM_BUTTON);
await userEvent.click(feedbackButton);
// THEN expect the feedback form dialog to be open
- expect(screen.getByTestId("feedback-form-dialog-c6ba52ec-c1de-46ac-950b-f5354c6785ac")).toBeInTheDocument();
+ expect(screen.getByTestId(OVERALL_FEEDBACK_FORM_DATA_TEST_ID.OVERALL_FEEDBACK_FORM_DIALOG)).toBeInTheDocument();
});
test("should maintain feedback status when form is closed", async () => {
@@ -175,14 +207,14 @@ describe("ConversationConclusionFooter", () => {
jest.spyOn(UserPreferencesStateService.getInstance(), "activeSessionHasCustomerSatisfactionRating").mockReturnValueOnce(true);
// WHEN the component is rendered
- renderWithChatProvider();
+ renderWithProviders();
// AND the feedback button is clicked
const feedbackButton = screen.getByTestId(DATA_TEST_ID.FEEDBACK_FORM_BUTTON);
await userEvent.click(feedbackButton);
// AND the form is closed
- const closeButton = screen.getByTestId("feedback-form-dialog-button-c6ba52ec-c1de-46ac-950b-f5354c6785ac");
+ const closeButton = screen.getByTestId(OVERALL_FEEDBACK_FORM_DATA_TEST_ID.OVERALL_FEEDBACK_FORM_DIALOG);
await userEvent.click(closeButton);
// THEN expect the feedback status to remain unchanged
@@ -196,7 +228,7 @@ describe("ConversationConclusionFooter", () => {
jest.spyOn(UserPreferencesStateService.getInstance(), "activeSessionHasCustomerSatisfactionRating").mockReturnValueOnce(false);
// WHEN the component is rendered
- renderWithChatProvider();
+ renderWithProviders();
// THEN expect the customer satisfaction rating component to be displayed
expect(screen.getByTestId(CUSTOMER_SATISFACTION_RATING_DATA_TEST_ID.CUSTOMER_SATISFACTION_RATING_CONTAINER)).toBeInTheDocument();
@@ -211,7 +243,7 @@ describe("ConversationConclusionFooter", () => {
});
// WHEN the component is rendered
- renderWithChatProvider();
+ renderWithProviders();
// THEN expect the create account message not to be displayed
expect(screen.queryByTestId(DATA_TEST_ID.CREATE_ACCOUNT_MESSAGE)).not.toBeInTheDocument();
@@ -229,7 +261,7 @@ describe("ConversationConclusionFooter", () => {
// WHEN the component is rendered with account not converted
(PersistentStorageService.getAccountConverted as jest.Mock).mockReturnValue(false);
- renderWithChatProvider();
+ renderWithProviders();
// THEN expect the create account message to be displayed
expect(screen.getByTestId(DATA_TEST_ID.CREATE_ACCOUNT_MESSAGE)).toBeInTheDocument();
@@ -242,7 +274,7 @@ describe("ConversationConclusionFooter", () => {
test("should show verification message for users with converted account", () => {
// GIVEN the component is rendered with account converted
(PersistentStorageService.getAccountConverted as jest.Mock).mockReturnValue(true);
- renderWithChatProvider();
+ renderWithProviders();
// THEN expect the verification message to be displayed
expect(screen.getByTestId(DATA_TEST_ID.VERIFICATION_REMINDER_MESSAGE)).toBeInTheDocument();
@@ -258,14 +290,14 @@ describe("ConversationConclusionFooter", () => {
// WHEN the component is rendered with account not converted
(PersistentStorageService.getAccountConverted as jest.Mock).mockReturnValue(false);
- renderWithChatProvider();
+ renderWithProviders();
// AND the create account link is clicked
const createAccountLink = screen.getByTestId(DATA_TEST_ID.CREATE_ACCOUNT_LINK);
await userEvent.click(createAccountLink);
// THEN expect the account conversion dialog to be displayed
- expect(screen.getByRole("dialog")).toBeInTheDocument();
+ expect(screen.getByTestId(ANONYMOUS_ACCOUNT_CONVERSION_DIALOG_DATA_TEST_ID.DIALOG)).toBeInTheDocument();
});
test("should submit customer satisfaction rating when rating is submitted", async () => {
@@ -273,7 +305,7 @@ describe("ConversationConclusionFooter", () => {
jest.spyOn(UserPreferencesStateService.getInstance(), "activeSessionHasCustomerSatisfactionRating").mockReturnValueOnce(false);
// WHEN the component is rendered
- renderWithChatProvider();
+ renderWithProviders();
// THEN expect the Customer Satisfaction Component to be shown
expect(screen.getByTestId(CUSTOMER_SATISFACTION_RATING_DATA_TEST_ID.CUSTOMER_SATISFACTION_RATING_CONTAINER)).toBeInTheDocument();
@@ -303,7 +335,7 @@ describe("ConversationConclusionFooter", () => {
jest.spyOn(UserPreferencesStateService.getInstance(), "activeSessionHasOverallFeedback").mockReturnValueOnce(false);
// WHEN the component is rendered
- renderWithChatProvider();
+ renderWithProviders();
// AND the experiences drawer button is clicked
const experiencesDrawerButton = screen.getByTestId(DATA_TEST_ID.EXPERIENCES_DRAWER_BUTTON);
@@ -326,7 +358,7 @@ describe("ConversationConclusionFooter", () => {
jest.spyOn(UserPreferencesStateService.getInstance(), "activeSessionHasCustomerSatisfactionRating").mockReturnValueOnce(true);
// WHEN the component is rendered
- renderWithChatProvider();
+ renderWithProviders();
// THEN expect the link to be disabled
const customLink = screen.getByTestId(testId);
@@ -367,7 +399,7 @@ describe("ConversationConclusionFooter", () => {
jest.spyOn(UserPreferencesStateService.getInstance(), "activeSessionHasCustomerSatisfactionRating").mockReturnValueOnce(true);
// WHEN the component is rendered
- renderWithChatProvider();
+ renderWithProviders();
// THEN expect the in progress button to be disabled
const inProgressButton = screen.getByTestId(DATA_TEST_ID.FEEDBACK_IN_PROGRESS_BUTTON);
@@ -387,12 +419,13 @@ describe("ConversationConclusionFooter", () => {
// GIVEN the browser is offline
mockBrowserIsOnLine(false);
// AND the user is anonymous
+
(authenticationStateService.getInstance as jest.Mock).mockReturnValue({
getUser: () => ({ name: null, email: null }),
});
// WHEN the component is rendered
- renderWithChatProvider();
+ renderWithProviders();
// THEN expect the create account button to be disabled
const createAccountLink = screen.getByTestId(DATA_TEST_ID.CREATE_ACCOUNT_LINK);
@@ -415,7 +448,7 @@ describe("ConversationConclusionFooter", () => {
jest.spyOn(UserPreferencesStateService.getInstance(), "activeSessionHasCustomerSatisfactionRating").mockReturnValueOnce(true);
// WHEN the component is rendered
- renderWithChatProvider();
+ renderWithProviders();
// THEN expect the thank you message to be displayed
expect(screen.getByTestId(DATA_TEST_ID.THANK_YOU_FOR_RATING_MESSAGE)).toBeInTheDocument();
@@ -433,7 +466,7 @@ describe("ConversationConclusionFooter", () => {
jest.spyOn(UserPreferencesStateService.getInstance(), "activeSessionHasCustomerSatisfactionRating").mockReturnValueOnce(true);
// WHEN the component is rendered
- renderWithChatProvider();
+ renderWithProviders();
// THEN expect the thank you message to be displayed
expect(screen.getByTestId(DATA_TEST_ID.THANK_YOU_FOR_FEEDBACK_MESSAGE)).toBeInTheDocument();
diff --git a/frontend-new/src/chat/chatMessage/conversationConclusionChatMessage/conversationConclusionFooter/ConversationConclusionFooter.tsx b/frontend-new/src/chat/chatMessage/conversationConclusionChatMessage/conversationConclusionFooter/ConversationConclusionFooter.tsx
index e09ab1b45..73959e10e 100644
--- a/frontend-new/src/chat/chatMessage/conversationConclusionChatMessage/conversationConclusionFooter/ConversationConclusionFooter.tsx
+++ b/frontend-new/src/chat/chatMessage/conversationConclusionChatMessage/conversationConclusionFooter/ConversationConclusionFooter.tsx
@@ -2,17 +2,17 @@ import React, { useState, useEffect } from "react";
import { Box, Typography, useTheme } from "@mui/material";
import CustomLink from "src/theme/CustomLink/CustomLink";
import { FIXED_MESSAGES_TEXT } from "src/chat/util";
-import FeedbackForm, {
+import OverallFeedbackForm, {
FeedbackStatus,
FeedbackCloseEvent,
-} from "src/feedback/overallFeedback/feedbackForm/FeedbackForm";
+} from "src/feedback/overallFeedback/overallFeedbackForm/OverallFeedbackForm";
import { useChatContext } from "src/chat/ChatContext";
import UserPreferencesStateService from "src/userPreferences/UserPreferencesStateService";
import { FeedbackItem } from "src/feedback/overallFeedback/overallFeedbackService/OverallFeedback.service.types";
import { PersistentStorageService } from "src/app/PersistentStorageService/PersistentStorageService";
import authenticationStateService from "src/auth/services/AuthenticationState.service";
import AnonymousAccountConversionDialog from "src/auth/components/anonymousAccountConversionDialog/AnonymousAccountConversionDialog";
-import CustomerSatisfactionRating from "src/feedback/overallFeedback/feedbackForm/components/customerSatisfactionRating/CustomerSatisfaction";
+import CustomerSatisfactionRating from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/customerSatisfactionRating/CustomerSatisfaction";
import PermIdentityIcon from "@mui/icons-material/PermIdentity";
import FeedbackOutlinedIcon from "@mui/icons-material/FeedbackOutlined";
import BadgeOutlinedIcon from "@mui/icons-material/BadgeOutlined";
@@ -203,7 +203,7 @@ const ConversationConclusionFooter: React.FC = () => {
)}
-
+ setShowConversionDialog(false)}
diff --git a/frontend-new/src/feedback/overallFeedback/feedbackContext/FeedbackContext.test.tsx b/frontend-new/src/feedback/overallFeedback/feedbackContext/FeedbackContext.test.tsx
new file mode 100644
index 000000000..ec0bfe93d
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/feedbackContext/FeedbackContext.test.tsx
@@ -0,0 +1,235 @@
+//silence chatty console logs
+import "src/_test_utilities/consoleMock";
+import React from "react";
+import { render, screen, waitFor, fireEvent } from "src/_test_utilities/test-utils";
+import { FeedbackProvider, useFeedback } from "src/feedback/overallFeedback/feedbackContext/FeedbackContext";
+import { FeedbackItem } from "src/feedback/overallFeedback/overallFeedbackService/OverallFeedback.service.types";
+import { PersistentStorageService } from "src/app/PersistentStorageService/PersistentStorageService";
+import { mockQuestionsConfig } from "src/feedback/overallFeedback/overallFeedbackForm/overallFeedbackForm.test.utils";
+
+// Mock the persistent storage service
+jest.mock("src/app/PersistentStorageService/PersistentStorageService", () => ({
+ PersistentStorageService: {
+ getOverallFeedback: jest.fn(),
+ setOverallFeedback: jest.fn(),
+ clearOverallFeedback: jest.fn(),
+ },
+}));
+
+// Test component that uses the feedback context
+const TestComponent: React.FC = () => {
+ const { questionsConfig, setQuestionsConfig, answers, handleAnswerChange, clearAnswers } = useFeedback();
+ return (
+
+
+ {questionsConfig ? "Config loaded" : "No config"}
+
+
+
+ {answers.length} answers
+
+
+
+
+ );
+};
+
+describe("FeedbackContext", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ (PersistentStorageService.getOverallFeedback as jest.Mock).mockReturnValue([]);
+ });
+
+ test("should provide initial null questionsConfig", () => {
+ // WHEN rendering a component that uses the feedback context
+ render(
+
+
+
+ );
+
+ // THEN expect the initial config status to be "No config"
+ expect(screen.getByTestId("config-status")).toHaveTextContent("No config");
+ });
+
+ test("should update questionsConfig when setQuestionsConfig is called", async () => {
+ // WHEN rendering a component that uses the feedback context
+ render(
+
+
+
+ );
+
+ // AND clicking the set config button
+ screen.getByTestId("set-config-button").click();
+
+ // THEN expect the config status to be "Config loaded"
+ await waitFor(() => {
+ expect(screen.getByTestId("config-status")).toHaveTextContent("Config loaded");
+ });
+ });
+
+ test("should throw error when useFeedback is used outside of FeedbackProvider", () => {
+ // GIVEN a component that uses the feedback context without a provider
+ const consoleError = jest.spyOn(console, "error").mockImplementation(() => {});
+
+ // WHEN rendering the component
+ expect(() => {
+ render();
+ }).toThrow("useFeedback must be used within a FeedbackProvider");
+
+ // THEN expect the error to be logged
+ expect(consoleError).toHaveBeenCalled();
+ consoleError.mockRestore();
+ });
+
+ test("should initialize answers from persistent storage", () => {
+ // GIVEN some existing feedback in persistent storage
+ const mockFeedback: FeedbackItem[] = [
+ {
+ question_id: "test1",
+ simplified_answer: {
+ rating_numeric: 5,
+ comment: "test comment 1"
+ }
+ },
+ {
+ question_id: "test2",
+ simplified_answer: {
+ rating_numeric: 4,
+ comment: "test comment 2"
+ }
+ }
+ ];
+ (PersistentStorageService.getOverallFeedback as jest.Mock).mockReturnValue(mockFeedback);
+
+ // WHEN rendering a component that uses the feedback context
+ render(
+
+
+
+ );
+
+ // THEN expect the answers count to be 2
+ expect(screen.getByTestId("answers-count")).toHaveTextContent("2 answers");
+ });
+
+ test("should handle adding a new answer", () => {
+ // WHEN rendering a component that uses the feedback context
+ render(
+
+
+
+ );
+
+ // AND clicking the add answer button
+ fireEvent.click(screen.getByTestId("add-answer-button"));
+
+ // THEN expect the answers count to be 1
+ expect(screen.getByTestId("answers-count")).toHaveTextContent("1 answers");
+ // AND expect the persistent storage to be updated
+ expect(PersistentStorageService.setOverallFeedback).toHaveBeenCalledWith([
+ {
+ question_id: "test",
+ simplified_answer: {
+ rating_numeric: 5,
+ comment: "test comment"
+ }
+ }
+ ]);
+ });
+
+ test("should handle updating an existing answer", () => {
+ // GIVEN an existing answer in the context
+ const mockFeedback: FeedbackItem[] = [
+ {
+ question_id: "test",
+ simplified_answer: {
+ rating_numeric: 3,
+ comment: "old comment"
+ }
+ }
+ ];
+ (PersistentStorageService.getOverallFeedback as jest.Mock).mockReturnValue(mockFeedback);
+
+ // WHEN rendering a component that uses the feedback context
+ render(
+
+
+
+ );
+
+ // AND clicking the add answer button to update the existing answer
+ fireEvent.click(screen.getByTestId("add-answer-button"));
+
+ // THEN expect the answers count to still be 1
+ expect(screen.getByTestId("answers-count")).toHaveTextContent("1 answers");
+ // AND expect the persistent storage to be updated with the new answer
+ expect(PersistentStorageService.setOverallFeedback).toHaveBeenCalledWith([
+ {
+ question_id: "test",
+ simplified_answer: {
+ rating_numeric: 5,
+ comment: "test comment"
+ }
+ }
+ ]);
+ });
+
+ test("should handle clearing answers", () => {
+ // GIVEN some existing answers in the context
+ const mockFeedback: FeedbackItem[] = [
+ {
+ question_id: "test1",
+ simplified_answer: {
+ rating_numeric: 5,
+ comment: "test comment 1"
+ }
+ },
+ {
+ question_id: "test2",
+ simplified_answer: {
+ rating_numeric: 4,
+ comment: "test comment 2"
+ }
+ }
+ ];
+ (PersistentStorageService.getOverallFeedback as jest.Mock).mockReturnValue(mockFeedback);
+
+ // WHEN rendering a component that uses the feedback context
+ render(
+
+
+
+ );
+
+ // AND clicking the clear answers button
+ fireEvent.click(screen.getByTestId("clear-answers-button"));
+
+ // THEN expect the answers count to be 0
+ expect(screen.getByTestId("answers-count")).toHaveTextContent("0 answers");
+ // AND expect the persistent storage to be cleared
+ expect(PersistentStorageService.clearOverallFeedback).toHaveBeenCalled();
+ });
+});
\ No newline at end of file
diff --git a/frontend-new/src/feedback/overallFeedback/feedbackContext/FeedbackContext.tsx b/frontend-new/src/feedback/overallFeedback/feedbackContext/FeedbackContext.tsx
new file mode 100644
index 000000000..8ff0f23b6
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/feedbackContext/FeedbackContext.tsx
@@ -0,0 +1,100 @@
+import React, { createContext, useContext, useState, useMemo, useEffect } from "react";
+import { QuestionsConfig } from "src/feedback/overallFeedback/overallFeedbackForm/overallFeedbackForm.types";
+import OverallFeedbackService from "src/feedback/overallFeedback/overallFeedbackService/OverallFeedback.service";
+import UserPreferencesStateService from "src/userPreferences/UserPreferencesStateService";
+import { FeedbackError } from "src/error/commonErrors";
+import { FeedbackItem } from "src/feedback/overallFeedback/overallFeedbackService/OverallFeedback.service.types";
+import { PersistentStorageService } from "src/app/PersistentStorageService/PersistentStorageService";
+
+interface FeedbackContextType {
+ questionsConfig: QuestionsConfig | null;
+ setQuestionsConfig: (config: QuestionsConfig) => void;
+ isLoading: boolean;
+ error: Error | null;
+ answers: FeedbackItem[];
+ handleAnswerChange: (feedback: FeedbackItem) => void;
+ clearAnswers: () => void;
+}
+
+const FeedbackContext = createContext(undefined);
+
+export const FeedbackProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
+ const [questionsConfig, setQuestionsConfig] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [answers, setAnswers] = useState(() => {
+ return PersistentStorageService.getOverallFeedback();
+ });
+
+ useEffect(() => {
+ const fetchQuestionsConfig = async () => {
+ try {
+ setIsLoading(true);
+
+ const activeSessionId = UserPreferencesStateService.getInstance().getActiveSessionId()
+ if (!activeSessionId) {
+ throw new FeedbackError('No active session found');
+ }
+ const service = OverallFeedbackService.getInstance();
+ const config = await service.getQuestionsConfig(activeSessionId);
+ setQuestionsConfig(config);
+ setError(null);
+ } catch (err) {
+ setError(err instanceof Error ? err : new Error('Failed to fetch questions configuration'));
+ console.error('Error fetching questions configuration:', err);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ fetchQuestionsConfig();
+ }, []);
+
+ const handleAnswerChange = (feedback: FeedbackItem) => {
+ setAnswers((prevAnswers) => {
+ const existingIndex = prevAnswers.findIndex((item) => item.question_id === feedback.question_id);
+
+ let updatedAnswers;
+ if (existingIndex !== -1) {
+ updatedAnswers = [...prevAnswers];
+ updatedAnswers[existingIndex] = feedback;
+ } else {
+ updatedAnswers = [...prevAnswers, feedback];
+ }
+
+ // Save updated answers to persistent storage
+ PersistentStorageService.setOverallFeedback(updatedAnswers);
+
+ return updatedAnswers;
+ });
+ };
+
+ const clearAnswers = () => {
+ PersistentStorageService.clearOverallFeedback();
+ setAnswers([]);
+ };
+
+ const value = useMemo(() => ({
+ questionsConfig,
+ setQuestionsConfig,
+ isLoading,
+ error,
+ answers,
+ handleAnswerChange,
+ clearAnswers
+ }), [questionsConfig, isLoading, error, answers]);
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useFeedback = () => {
+ const context = useContext(FeedbackContext);
+ if (context === undefined) {
+ throw new Error("useFeedback must be used within a FeedbackProvider");
+ }
+ return context;
+};
\ No newline at end of file
diff --git a/frontend-new/src/feedback/overallFeedback/feedbackForm/FeedbackForm.stories.tsx b/frontend-new/src/feedback/overallFeedback/feedbackForm/FeedbackForm.stories.tsx
deleted file mode 100644
index b05c55d54..000000000
--- a/frontend-new/src/feedback/overallFeedback/feedbackForm/FeedbackForm.stories.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import { Meta, type StoryObj } from "@storybook/react";
-import { action } from "@storybook/addon-actions";
-import FeedbackForm from "src/feedback/overallFeedback/feedbackForm/FeedbackForm";
-
-const meta: Meta = {
- title: "Feedback/FeedbackForm",
- component: FeedbackForm,
- tags: ["autodocs"],
- args: {
- notifyOnClose: action("notifyOnClose"),
- },
- argTypes: {},
-};
-
-export default meta;
-
-type Story = StoryObj;
-
-export const Shown: Story = {
- args: {
- isOpen: true,
- },
-};
diff --git a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/checkboxQuestion/CheckboxQuestion.stories.tsx b/frontend-new/src/feedback/overallFeedback/feedbackForm/components/checkboxQuestion/CheckboxQuestion.stories.tsx
deleted file mode 100644
index 0d81c2c94..000000000
--- a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/checkboxQuestion/CheckboxQuestion.stories.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import { Meta, StoryObj } from "@storybook/react";
-import CheckboxQuestion from "src/feedback/overallFeedback/feedbackForm/components/checkboxQuestion/CheckboxQuestion";
-import { action } from "@storybook/addon-actions";
-
-const meta: Meta = {
- title: "Feedback/CheckboxQuestion",
- component: CheckboxQuestion,
- tags: ["autodocs"],
- args: {
- notifyChange: (selectedOptions, comments) => {
- action("notifyChange")(selectedOptions, comments);
- },
- },
-};
-
-export default meta;
-
-type Story = StoryObj;
-
-export const Shown: Story = {
- args: {
- questionId: "accuracy_relevance",
- questionText: "How accurate and relevant was the information provided?",
- options: [
- { key: "accurate", value: "Accurate" },
- { key: "relevant", value: "Relevant" },
- { key: "upToDate", value: "Up-to-date" },
- { key: "easyToUnderstand", value: "Easy to understand" },
- ],
- selectedOptions: [],
- placeholder: "Please provide comments",
- },
-};
diff --git a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/customRating/CustomRating.stories.tsx b/frontend-new/src/feedback/overallFeedback/feedbackForm/components/customRating/CustomRating.stories.tsx
deleted file mode 100644
index 1ba933d94..000000000
--- a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/customRating/CustomRating.stories.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import { Meta, StoryObj } from "@storybook/react";
-import CustomRating from "src/feedback/overallFeedback/feedbackForm/components/customRating/CustomRating";
-import { action } from "@storybook/addon-actions";
-
-const meat: Meta = {
- title: "Feedback/CustomRating",
- component: CustomRating,
- tags: ["autodocs"],
- args: {
- notifyChange: (value, comments) => {
- action("notifyChange")(value, comments);
- },
- },
-};
-
-export default meat;
-
-type Story = StoryObj;
-
-export const Shown: Story = {
- args: {
- questionText: "How easy was it to interact with the system?",
- questionId: "interaction_ease",
- lowRatingLabel: "Not easy",
- highRatingLabel: "Very easy",
- placeholder: "Please provide comments",
- },
-};
-
-export const ShownWithNoRating: Story = {
- args: {
- questionText: "How easy was it to interact with the system?",
- questionId: "interaction_ease",
- displayRating: false,
- placeholder: "Please provide comments",
- },
-};
diff --git a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/customerSatisfactionRating/CustomerSatisfaction.stories.tsx b/frontend-new/src/feedback/overallFeedback/feedbackForm/components/customerSatisfactionRating/CustomerSatisfaction.stories.tsx
deleted file mode 100644
index 53dcd18d6..000000000
--- a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/customerSatisfactionRating/CustomerSatisfaction.stories.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import { Meta, type StoryObj } from "@storybook/react";
-import { action } from "@storybook/addon-actions";
-import CustomerSatisfactionRating from "./CustomerSatisfaction";
-
-const meta: Meta = {
- title: "Feedback/CustomerSatisfactionRating",
- component: CustomerSatisfactionRating,
- tags: ["autodocs"],
- args: {
- notifyOnCustomerSatisfactionRatingSubmitted: action("notifyOnCustomerSatisfactionRatingSubmitted"),
- }
-};
-
-export default meta;
-
-type Story = StoryObj;
-
-export const Default: Story = {};
\ No newline at end of file
diff --git a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/customerSatisfactionRating/__snapshots__/CustomerSatisfaction.test.tsx.snap b/frontend-new/src/feedback/overallFeedback/feedbackForm/components/customerSatisfactionRating/__snapshots__/CustomerSatisfaction.test.tsx.snap
deleted file mode 100644
index dea08d7f2..000000000
--- a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/customerSatisfactionRating/__snapshots__/CustomerSatisfaction.test.tsx.snap
+++ /dev/null
@@ -1,7 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`CustomerSatisfactionRating should render component successfully 1`] = `
-
-`;
diff --git a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/feedbackFormContent/FeedbackFormContent.stories.tsx b/frontend-new/src/feedback/overallFeedback/feedbackForm/components/feedbackFormContent/FeedbackFormContent.stories.tsx
deleted file mode 100644
index 7429d494e..000000000
--- a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/feedbackFormContent/FeedbackFormContent.stories.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import { Meta, type StoryObj } from "@storybook/react";
-import FeedbackContent from "src/feedback/overallFeedback/feedbackForm/components/feedbackFormContent/FeedbackFormContent";
-
-const meta: Meta = {
- title: "Feedback/FeedbackContent",
- component: FeedbackContent,
- tags: ["autodocs"],
- args: {
- notifySubmit: () => {},
- },
-};
-export default meta;
-
-type Story = StoryObj;
-
-export const Shown: Story = {
- args: {},
-};
diff --git a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/feedbackFormContent/FeedbackFormContent.test.tsx b/frontend-new/src/feedback/overallFeedback/feedbackForm/components/feedbackFormContent/FeedbackFormContent.test.tsx
deleted file mode 100644
index eae8d98ba..000000000
--- a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/feedbackFormContent/FeedbackFormContent.test.tsx
+++ /dev/null
@@ -1,344 +0,0 @@
-// mute the console
-import "src/_test_utilities/consoleMock";
-
-import React from "react";
-import { render, screen, act, fireEvent } from "src/_test_utilities/test-utils";
-import { DATA_TEST_ID as CUSTOM_RATING_DATA_TEST_ID } from "src/feedback/overallFeedback/feedbackForm/components/customRating/CustomRating";
-import { DATA_TEST_ID as YES_NO_DATA_TEST_ID } from "src/feedback/overallFeedback/feedbackForm/components/yesNoQuestion/YesNoQuestion";
-import FeedbackFormContent, {
- DATA_TEST_ID,
-} from "src/feedback/overallFeedback/feedbackForm/components/feedbackFormContent/FeedbackFormContent";
-import { DATA_TEST_ID as CHECKBOX_DATA_TEST_ID } from "src/feedback/overallFeedback/feedbackForm/components/checkboxQuestion/CheckboxQuestion";
-import feedbackFormContentSteps from "src/feedback/overallFeedback/feedbackForm/components/feedbackFormContent/feedbackFormContentSteps";
-import { DATA_TEST_ID as COMMENT_TEXT_FIELD_TEST_ID } from "src/feedback/overallFeedback/feedbackForm/components/commentTextField/CommentTextField";
-import { useSwipeable } from "react-swipeable";
-import { mockBrowserIsOnLine } from "src/_test_utilities/mockBrowserIsOnline";
-
-// mock the swipeable hook
-jest.mock("react-swipeable");
-
-// mock the framer-motion library
-jest.mock("framer-motion", () => ({
- motion: {
- div: ({ children, ...props }: { children: React.ReactElement }) =>
{children}
,
- },
- AnimatePresence: ({ children }: { children: React.ReactElement }) => <>{children}>,
-}));
-
-describe("FeedbackFormContent", () => {
- beforeEach(() => {
- jest.clearAllMocks();
- jest.resetModules();
- });
-
- test("should render component successfully", () => {
- // GIVEN the component
- const givenFeedbackFormContent = ;
-
- // WHEN the component is rendered
- render(givenFeedbackFormContent);
-
- // THEN expect no errors or warning to have occurred
- expect(console.error).not.toHaveBeenCalled();
- expect(console.warn).not.toHaveBeenCalled();
- // AND the feedback form content to be in the document
- const feedbackFormContent = screen.getByTestId(DATA_TEST_ID.FEEDBACK_FORM_CONTENT);
- expect(feedbackFormContent).toBeInTheDocument();
- // AND the feedback form content title to be in the document
- expect(screen.getByTestId(DATA_TEST_ID.FEEDBACK_FORM_CONTENT_TITLE)).toBeInTheDocument();
- // AND the feedback form content divider to be in the document
- expect(screen.getByTestId(DATA_TEST_ID.FEEDBACK_FORM_CONTENT_DIVIDER)).toBeInTheDocument();
- // AND the feedback form content questions to be in the document
- expect(screen.getByTestId(DATA_TEST_ID.FEEDBACK_FORM_CONTENT_QUESTIONS)).toBeInTheDocument();
- // AND the feedback form next button to be in the document
- expect(screen.getByTestId(DATA_TEST_ID.FEEDBACK_FORM_NEXT_BUTTON)).toBeInTheDocument();
- // AND the feedback form back button to be in the document
- expect(screen.getByTestId(DATA_TEST_ID.FEEDBACK_FORM_BACK_BUTTON)).toBeInTheDocument();
- // AND to match the snapshot
- expect(feedbackFormContent).toMatchSnapshot();
- });
-
- describe("action tests", () => {
- test("should call handleNext when next button is clicked", () => {
- // GIVEN the component
- const givenFeedbackFormContent = ;
- // AND the component is rendered
- render(givenFeedbackFormContent);
-
- // WHEN the next button is clicked
- const nextButton = screen.getByTestId(DATA_TEST_ID.FEEDBACK_FORM_NEXT_BUTTON);
- fireEvent.click(nextButton);
-
- // THEN expect to go to the next step
- expect(screen.getByTestId(DATA_TEST_ID.FEEDBACK_FORM_CONTENT_TITLE)).toHaveTextContent(
- feedbackFormContentSteps[1].label
- );
-
- // AND no errors or warnings to be shown
- expect(console.warn).not.toHaveBeenCalled();
- expect(console.error).not.toHaveBeenCalled();
- });
-
- test("should call handlePrevious when back button is clicked", () => {
- // GIVEN the component
- const givenFeedbackFormContent = ;
- // AND the component is rendered
- render(givenFeedbackFormContent);
-
- // WHEN the next button is clicked to move to the next step
- const nextButton = screen.getByTestId(DATA_TEST_ID.FEEDBACK_FORM_NEXT_BUTTON);
- fireEvent.click(nextButton);
- // AND the back button is clicked
- const backButton = screen.getByTestId(DATA_TEST_ID.FEEDBACK_FORM_BACK_BUTTON);
- fireEvent.click(backButton);
-
- // THEN expect to go to the previous step
- expect(screen.getByTestId(DATA_TEST_ID.FEEDBACK_FORM_CONTENT_TITLE)).toHaveTextContent(
- feedbackFormContentSteps[0].label
- );
-
- // AND no errors or warnings to be shown
- expect(console.warn).not.toHaveBeenCalled();
- expect(console.error).not.toHaveBeenCalled();
- });
-
- test("should call handleSubmit with exact answer data when answering each step", () => {
- // GIVEN the FeedbackFormContent component
- const mockHandleSubmit = jest.fn();
- const givenFeedbackFormContent = ;
- // AND the component is rendered
- render(givenFeedbackFormContent);
-
- // WHEN on the first step, select a checkbox option
- const checkboxInput = screen.getAllByTestId(CHECKBOX_DATA_TEST_ID.CHECKBOX_OPTION)[0];
- fireEvent.click(checkboxInput);
-
- // AND move to next step
- const nextButton = screen.getByTestId(DATA_TEST_ID.FEEDBACK_FORM_NEXT_BUTTON);
- fireEvent.click(nextButton);
-
- // AND on the second step, answer the yes/no question
- const yesNoInput = screen.getAllByTestId(YES_NO_DATA_TEST_ID.RADIO_YES)[1];
- fireEvent.click(yesNoInput);
-
- // AND move to next step
- fireEvent.click(nextButton);
-
- // AND on the last step, provide a custom rating comment
- const customRatingInput = screen.getAllByTestId(CUSTOM_RATING_DATA_TEST_ID.CUSTOM_RATING_ICON)[4];
- fireEvent.click(customRatingInput);
-
- // AND submit the form
- fireEvent.click(nextButton);
-
- // THEN expect mockHandleSubmit to have been called exactly once
- expect(mockHandleSubmit).toHaveBeenCalledTimes(1);
-
- // AND expect the exact answer data structure
- const submittedAnswers = mockHandleSubmit.mock.calls[0][0];
- expect(submittedAnswers).toHaveLength(3);
-
- // First step answer (checkbox)
- expect(submittedAnswers[0]).toEqual({
- question_id: feedbackFormContentSteps[0].questions[1].questionId,
- simplified_answer: {
- comment: "",
- rating_boolean: undefined,
- rating_numeric: undefined,
- selected_options_keys: [feedbackFormContentSteps[0].questions[1].options![0].key],
- },
- });
-
- // Second step answer (yes/no)
- expect(submittedAnswers[1]).toEqual({
- question_id: feedbackFormContentSteps[1].questions[1].questionId,
- simplified_answer: {
- comment: "",
- rating_boolean: true,
- rating_numeric: undefined,
- selected_options_keys: undefined,
- },
- });
-
- // Last step answer (custom rating)
- expect(submittedAnswers[2]).toEqual({
- question_id: feedbackFormContentSteps[2].questions[0].questionId,
- simplified_answer: {
- comment: "",
- rating_boolean: undefined,
- rating_numeric: 5,
- selected_options_keys: undefined,
- }
- });
-
- // AND no errors or warnings to be shown
- expect(console.warn).not.toHaveBeenCalled();
- expect(console.error).not.toHaveBeenCalled();
- });
-
- test("should call handleAnswerChange when a question is answered", () => {
- // GIVEN the FeedbackFormContent component
- const givenFeedbackFormContent = ;
- // AND the component is rendered
- render(givenFeedbackFormContent);
-
- // WHEN question is answered
- const yesNoInput = screen.getByTestId(YES_NO_DATA_TEST_ID.RADIO_YES);
- fireEvent.click(yesNoInput);
- const input = screen.getByTestId(COMMENT_TEXT_FIELD_TEST_ID.COMMENT_TEXT_FIELD);
- fireEvent.change(input, { target: { value: "This is a comment" } });
-
- // THEN expect the answer to be saved
- expect(input).toHaveValue("This is a comment");
-
- // AND no errors or warnings to be shown
- expect(console.warn).not.toHaveBeenCalled();
- expect(console.error).not.toHaveBeenCalled();
- });
-
- test("should go to the next step when swiping left", () => {
- // GIVEN the component is rendered
- render();
- // AND the swipe handlers
- const swipeHandlers = (useSwipeable as jest.Mock).mock.calls[0][0];
-
- // WHEN the component is swiped left
- act(() => {
- swipeHandlers.onSwipedLeft();
- });
-
- // THEN expect to go to the next step
- expect(screen.getByTestId(DATA_TEST_ID.FEEDBACK_FORM_CONTENT_TITLE)).toHaveTextContent(
- feedbackFormContentSteps[1].label
- );
-
- // AND WHEN the component is swiped left for again
- act(() => {
- swipeHandlers.onSwipedLeft();
- });
-
- // THEN expect to go to the next step
- expect(screen.getByTestId(DATA_TEST_ID.FEEDBACK_FORM_CONTENT_TITLE)).toHaveTextContent(
- feedbackFormContentSteps[2].label
- );
-
- // AND no errors or warnings to be shown
- expect(console.warn).not.toHaveBeenCalled();
- expect(console.error).not.toHaveBeenCalled();
- });
-
- test("should go to the previous step when swiping right", () => {
- // GIVEN the component is rendered
- render();
-
- // WHEN the component is swiped left twice
- act(() => {
- (useSwipeable as jest.Mock).mock.calls[0][0].onSwipedLeft();
- (useSwipeable as jest.Mock).mock.calls[0][0].onSwipedLeft();
- });
-
- // THEN expect to go to the third
- expect(screen.getByTestId(DATA_TEST_ID.FEEDBACK_FORM_CONTENT_TITLE)).toHaveTextContent(
- feedbackFormContentSteps[2].label
- );
-
- // WHEN the component is swiped right
- act(() => {
- (useSwipeable as jest.Mock).mock.calls.at(-1)[0].onSwipedRight();
- });
-
- // THEN expect to go to the previous step
- expect(screen.getByTestId(DATA_TEST_ID.FEEDBACK_FORM_CONTENT_TITLE)).toHaveTextContent(
- feedbackFormContentSteps[1].label
- );
-
- // AND WHEN the component is swiped right again
- act(() => {
- (useSwipeable as jest.Mock).mock.calls.at(-1)[0].onSwipedRight();
- });
-
- // THEN expect to go to the previous step
- expect(screen.getByTestId(DATA_TEST_ID.FEEDBACK_FORM_CONTENT_TITLE)).toHaveTextContent(
- feedbackFormContentSteps[0].label
- );
-
- // AND no errors or warnings to be shown
- expect(console.warn).not.toHaveBeenCalled();
- expect(console.error).not.toHaveBeenCalled();
- });
-
- test("should do nothing when swiping right on the first step", () => {
- // GIVEN the component is rendered
- render();
-
- // WHEN the component is swiped right on the first step
- act(() => {
- (useSwipeable as jest.Mock).mock.calls[0][0].onSwipedRight();
- });
-
- // THEN expect to stay on the first step
- expect(screen.getByTestId(DATA_TEST_ID.FEEDBACK_FORM_CONTENT_TITLE)).toHaveTextContent(
- feedbackFormContentSteps[0].label
- );
- });
-
- test("should do nothing when swiping left on the last step", () => {
- // GIVEN the component is rendered
- render();
-
- // WHEN the component is swiped left to reach the last step
- act(() => {
- (useSwipeable as jest.Mock).mock.calls[0][0].onSwipedLeft();
- });
- act(() => {
- (useSwipeable as jest.Mock).mock.calls[1][0].onSwipedLeft();
- });
-
- // THEN expect to reach the last step
- const lastStepTitle = feedbackFormContentSteps[2].label;
- const lastStepTitleElement = screen.getByTestId(DATA_TEST_ID.FEEDBACK_FORM_CONTENT_TITLE);
- expect(lastStepTitleElement).toHaveTextContent(lastStepTitle);
-
- // WHEN the component is swiped left on the last step
- act(() => {
- (useSwipeable as jest.Mock).mock.calls[2][0].onSwipedLeft();
- });
-
- // THEN expect to stay on the last step
- expect(lastStepTitleElement).toHaveTextContent(lastStepTitle);
-
- // AND no errors or warnings to be shown
- expect(console.warn).not.toHaveBeenCalled();
- expect(console.error).not.toHaveBeenCalled();
- });
-
- test("should enable/disable the submit button when the browser online status changes", async () => {
- // GIVEN the browser is offline
- mockBrowserIsOnLine(false);
-
- // WHEN the component is rendered
- render();
- // AND we are on the last step
- act(() => {
- (useSwipeable as jest.Mock).mock.calls[0][0].onSwipedLeft();
- (useSwipeable as jest.Mock).mock.calls[0][0].onSwipedLeft();
- });
- // AND a question is answered
- const customRating = screen.getAllByTestId(CUSTOM_RATING_DATA_TEST_ID.CUSTOM_RATING_ICON)[4];
- fireEvent.click(customRating);
-
- // THEN expect the submit button to be disabled
- const submitButton = screen.getByTestId(DATA_TEST_ID.FEEDBACK_FORM_NEXT_BUTTON);
- expect(submitButton).toBeDisabled();
-
- // WHEN the browser goes online
- mockBrowserIsOnLine(true);
-
- // THEN expect the submit button to be enabled
- expect(submitButton).toBeEnabled();
- // AND expect no errors or warnings to be logged
- expect(console.warn).not.toHaveBeenCalled();
- expect(console.error).not.toHaveBeenCalled();
- });
- });
-});
diff --git a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/feedbackFormContent/__snapshots__/FeedbackFormContent.test.tsx.snap b/frontend-new/src/feedback/overallFeedback/feedbackForm/components/feedbackFormContent/__snapshots__/FeedbackFormContent.test.tsx.snap
deleted file mode 100644
index 24945ea3f..000000000
--- a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/feedbackFormContent/__snapshots__/FeedbackFormContent.test.tsx.snap
+++ /dev/null
@@ -1,428 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`FeedbackFormContent should render component successfully 1`] = `
-
-
-
-
- Bias & Experience Accuracy
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
diff --git a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/feedbackFormContent/feedbackFormContentSteps.ts b/frontend-new/src/feedback/overallFeedback/feedbackForm/components/feedbackFormContent/feedbackFormContentSteps.ts
deleted file mode 100644
index 78ee4aceb..000000000
--- a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/feedbackFormContent/feedbackFormContentSteps.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-import questions from "src/feedback/overallFeedback/feedbackForm/questions-en.json";
-import {
- DetailedQuestion,
- QuestionType,
- YesNoEnum,
-} from "src/feedback/overallFeedback/feedbackForm/feedbackForm.types";
-
-interface Step {
- label: string;
- questions: DetailedQuestion[];
-}
-
-const feedbackFormContentSteps: Step[] = [
- {
- label: "Bias & Experience Accuracy",
- questions: [
- {
- type: QuestionType.YesNo,
- questionId: "perceived_bias",
- questionText: questions["perceived_bias"].question_text,
- showCommentsOn: YesNoEnum.Yes,
- placeholder: questions["perceived_bias"].comment_placeholder,
- },
- {
- type: QuestionType.Checkbox,
- questionId: "work_experience_accuracy",
- questionText: questions["work_experience_accuracy"].question_text,
- options: Object.entries(questions["work_experience_accuracy"].options).map(([key, value]) => ({ key, value })),
- lowRatingLabel: "Inaccurate",
- highRatingLabel: "Very accurate",
- placeholder: questions["work_experience_accuracy"].comment_placeholder,
- },
- ],
- },
- {
- label: "Skill Accuracy",
- questions: [
- {
- type: QuestionType.YesNo,
- questionId: "clarity_of_skills",
- questionText: questions["clarity_of_skills"].question_text,
- showCommentsOn: YesNoEnum.No,
- placeholder: questions["clarity_of_skills"].comment_placeholder,
- },
- {
- type: QuestionType.YesNo,
- questionId: "incorrect_skills",
- questionText: questions["incorrect_skills"].question_text,
- showCommentsOn: YesNoEnum.Yes,
- placeholder: questions["incorrect_skills"].comment_placeholder,
- },
- {
- type: QuestionType.YesNo,
- questionId: "missing_skills",
- questionText: questions["missing_skills"].question_text,
- showCommentsOn: YesNoEnum.Yes,
- placeholder: questions["missing_skills"].comment_placeholder,
- },
- ],
- },
- {
- label: "Final feedback",
- questions: [
- {
- type: QuestionType.Rating,
- questionId: "interaction_ease",
- questionText: questions["interaction_ease"].question_text,
- lowRatingLabel: "Difficult",
- highRatingLabel: "Easy",
- maxRating: 5,
- placeholder: questions["interaction_ease"].comment_placeholder,
- },
- {
- type: QuestionType.Rating,
- questionId: "recommendation",
- questionText: questions["recommendation"].question_text,
- lowRatingLabel: "Unlikely",
- highRatingLabel: "Likely",
- maxRating: 5,
- },
- {
- type: QuestionType.Rating,
- questionId: "additional_feedback",
- questionText: questions["additional_feedback"].question_text,
- displayRating: false,
- placeholder: questions["additional_feedback"].comment_placeholder,
- },
- ],
- },
-];
-
-export default feedbackFormContentSteps;
diff --git a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/stepsComponent/StepsComponent.test.tsx b/frontend-new/src/feedback/overallFeedback/feedbackForm/components/stepsComponent/StepsComponent.test.tsx
deleted file mode 100644
index 1d3bb741f..000000000
--- a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/stepsComponent/StepsComponent.test.tsx
+++ /dev/null
@@ -1,115 +0,0 @@
-// mute the console
-import "src/_test_utilities/consoleMock";
-
-import { fireEvent } from "@testing-library/react";
-import { render, screen } from "src/_test_utilities/test-utils";
-import StepsComponent, {
- DATA_TEST_ID,
-} from "src/feedback/overallFeedback/feedbackForm/components/stepsComponent/StepsComponent";
-import { FeedbackItem } from "src/feedback/overallFeedback/overallFeedbackService/OverallFeedback.service.types";
-import {
- DetailedQuestion,
- QuestionType,
- YesNoEnum,
-} from "src/feedback/overallFeedback/feedbackForm/feedbackForm.types";
-import { DATA_TEST_ID as CUSTOM_RATING_DATA_TEST_ID } from "src/feedback/overallFeedback/feedbackForm/components/customRating/CustomRating";
-import { DATA_TEST_ID as YES_NO_QUESTION_DATA_TEST_ID } from "src/feedback/overallFeedback/feedbackForm/components/yesNoQuestion/YesNoQuestion";
-import { DATA_TEST_ID as CHECKBOX_QUESTION_DATA_TEST_ID } from "src/feedback/overallFeedback/feedbackForm/components/checkboxQuestion/CheckboxQuestion";
-
-describe("StepsComponent", () => {
- const mockQuestions: DetailedQuestion[] = [
- {
- questionId: "q1",
- type: QuestionType.Checkbox,
- questionText: "Select options",
- options: [
- { key: "option1", value: "option1" },
- { key: "option2", value: "option2" },
- ],
- },
- {
- questionId: "q2",
- type: QuestionType.Rating,
- questionText: "Rate this",
- displayRating: true,
- lowRatingLabel: "Low",
- highRatingLabel: "High",
- maxRating: 5,
- },
- {
- questionId: "q3",
- type: QuestionType.YesNo,
- questionText: "Yes or No?",
- showCommentsOn: YesNoEnum.Yes,
- },
- ];
-
- const mockAnswers: FeedbackItem[] = [
- { question_id: "q1", simplified_answer: { selected_options_keys: ["option1"] } },
- { question_id: "q2", simplified_answer: { rating_numeric: 3 } },
- { question_id: "q3", simplified_answer: { rating_boolean: true, comment: "Yes comment" }},
- ];
-
- const mockOnChange = jest.fn();
-
- test("should render component successfully", () => {
- // Given the component
- const givenStepsComponent = (
-
- );
-
- // When the component is rendered
- render(givenStepsComponent);
-
- // THEN expect no errors or warning to have occurred
- expect(console.error).not.toHaveBeenCalled();
- expect(console.warn).not.toHaveBeenCalled();
- // AND expect the component to be in the document
- const stepsComponent = screen.getByTestId(DATA_TEST_ID.STEPS_COMPONENT);
- expect(stepsComponent).toBeInTheDocument();
- // AND the rating question to be in the document
- expect(screen.getByTestId(CUSTOM_RATING_DATA_TEST_ID.CUSTOM_RATING_CONTAINER)).toBeInTheDocument();
- // AND the yes/no question to be in the document
- expect(screen.getByTestId(YES_NO_QUESTION_DATA_TEST_ID.FORM_CONTROL)).toBeInTheDocument();
- // AND the checkbox question to be in the document
- expect(screen.getByTestId(CHECKBOX_QUESTION_DATA_TEST_ID.FORM_CONTROL)).toBeInTheDocument();
- // AND to match the snapshot
- expect(stepsComponent).toMatchSnapshot();
- });
-
- test("should call onChange when checkbox question is answered", () => {
- // Given the component is rendered
- render();
-
- // When the checkbox question is answered
- const checkbox = screen.getAllByTestId(CHECKBOX_QUESTION_DATA_TEST_ID.CHECKBOX_OPTION)[0];
- fireEvent.click(checkbox);
-
- // Then expect onChange to be called
- expect(mockOnChange).toHaveBeenCalled();
- });
-
- test("should call onChange when rating question is answered", () => {
- // Given the component is rendered
- render();
-
- // When the rating question is answered
- const starsIcon = screen.getAllByTestId(CUSTOM_RATING_DATA_TEST_ID.CUSTOM_RATING_ICON)[4];
- fireEvent.click(starsIcon);
-
- // Then expect onChange to be called
- expect(mockOnChange).toHaveBeenCalled();
- });
-
- test("should call onChange when yes/no question is answered", () => {
- // Given the component is rendered
- render();
-
- // When the yes/no question is answered
- const radioNo = screen.getByTestId(YES_NO_QUESTION_DATA_TEST_ID.RADIO_NO);
- fireEvent.click(radioNo);
-
- // Then expect onChange to be called
- expect(mockOnChange).toHaveBeenCalled();
- });
-});
diff --git a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/stepsComponent/StepsComponent.tsx b/frontend-new/src/feedback/overallFeedback/feedbackForm/components/stepsComponent/StepsComponent.tsx
deleted file mode 100644
index ab1dce33e..000000000
--- a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/stepsComponent/StepsComponent.tsx
+++ /dev/null
@@ -1,104 +0,0 @@
-import React from "react";
-import { Box } from "@mui/material";
-import { DetailedQuestion, QuestionType } from "src/feedback/overallFeedback/feedbackForm/feedbackForm.types";
-import {
- SimplifiedAnswer,
- FeedbackItem,
-} from "src/feedback/overallFeedback/overallFeedbackService/OverallFeedback.service.types";
-import CustomRating from "src/feedback/overallFeedback/feedbackForm/components/customRating/CustomRating";
-import YesNoQuestion from "src/feedback/overallFeedback/feedbackForm/components/yesNoQuestion/YesNoQuestion";
-import CheckboxQuestion from "src/feedback/overallFeedback/feedbackForm/components/checkboxQuestion/CheckboxQuestion";
-import { useIsSmallOrShortScreen } from "src/feedback/overallFeedback/feedbackForm/useIsSmallOrShortScreen";
-
-interface StepProps {
- questions: DetailedQuestion[];
- feedbackItems: FeedbackItem[];
- onChange: (data: FeedbackItem) => void;
-}
-
-const uniqueId = "3bf900a2-f9fa-4b28-8c20-8bc21570635c";
-
-export const DATA_TEST_ID = {
- STEPS_COMPONENT: `steps-component-${uniqueId}`,
-};
-
-const StepsComponent: React.FC = ({ questions, feedbackItems, onChange }) => {
- const isSmallOrShortScreen = useIsSmallOrShortScreen()
-
- const getAnswerByQuestionId = (questionId: string): SimplifiedAnswer | undefined => {
- return feedbackItems.find((item: FeedbackItem) => item.question_id === questionId)?.simplified_answer;
- };
-
- const handleInputChange = (questionId: string, value: SimplifiedAnswer) => {
- const formattedData: FeedbackItem = {
- question_id: questionId,
- simplified_answer: value,
- };
- onChange(formattedData);
- };
-
- return (
- (isSmallOrShortScreen ? theme.tabiyaSpacing.xl * 3 : theme.tabiyaSpacing.xl * 1.2)}
- data-testid={DATA_TEST_ID.STEPS_COMPONENT}
- >
- {questions.map((question) => {
- const answer = getAnswerByQuestionId(question.questionId) || {};
-
- return (
-
- {question.type === QuestionType.Checkbox && (
-
- handleInputChange(question.questionId, { selected_options_keys: selectedOptions, comment: comments })
- }
- comments={answer.comment ?? ""}
- placeholder={question.placeholder}
- />
- )}
- {question.type === QuestionType.Rating && (
-
- handleInputChange(question.questionId, { rating_numeric: value, comment: comments })
- }
- lowRatingLabel={question.lowRatingLabel ?? ""}
- highRatingLabel={question.highRatingLabel ?? ""}
- comments={answer.comment ?? ""}
- maxRating={question.maxRating ?? 5}
- placeholder={question.placeholder}
- />
- )}
- {question.type === QuestionType.YesNo && (
-
- handleInputChange(question.questionId, { rating_boolean: value, comment: comments })
- }
- showCommentsOn={question.showCommentsOn ?? undefined}
- comments={answer.comment ?? ""}
- placeholder={question.placeholder}
- />
- )}
-
- );
- })}
-
- );
-};
-
-export default StepsComponent;
diff --git a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/yesNoQuestion/YesNoQuestion.stories.tsx b/frontend-new/src/feedback/overallFeedback/feedbackForm/components/yesNoQuestion/YesNoQuestion.stories.tsx
deleted file mode 100644
index d4dc47e21..000000000
--- a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/yesNoQuestion/YesNoQuestion.stories.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import { Meta, StoryObj } from "@storybook/react";
-import YesNoQuestion from "src/feedback/overallFeedback/feedbackForm/components/yesNoQuestion/YesNoQuestion";
-import { YesNoEnum } from "src/feedback/overallFeedback/feedbackForm/feedbackForm.types";
-import { action } from "@storybook/addon-actions";
-
-const meta: Meta = {
- title: "Feedback/YesNoQuestion",
- component: YesNoQuestion,
- tags: ["autodocs"],
- args: {
- notifyChange: (value, comments) => {
- action("notifyChange")(value, comments);
- },
- },
-};
-
-export default meta;
-
-type Story = StoryObj;
-
-export const ShowCommentsWhenYesSelected: Story = {
- args: {
- questionText: "Is this a question?",
- questionId: "is_question",
- showCommentsOn: YesNoEnum.Yes,
- placeholder: "Please provide comments",
- },
-};
-
-export const ShowCommentsWhenNoSelected: Story = {
- args: {
- questionText: "Is this not a question?",
- questionId: "is_not_question",
- showCommentsOn: YesNoEnum.No,
- placeholder: "Please provide comments",
- },
-};
diff --git a/frontend-new/src/feedback/overallFeedback/feedbackForm/feedbackForm.types.ts b/frontend-new/src/feedback/overallFeedback/feedbackForm/feedbackForm.types.ts
deleted file mode 100644
index 58cef58ca..000000000
--- a/frontend-new/src/feedback/overallFeedback/feedbackForm/feedbackForm.types.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-export interface Option {
- key: string;
- value: string;
-}
-
-export enum QuestionType {
- Rating = "rating",
- YesNo = "yesNo",
- Checkbox = "checkbox",
-}
-
-export enum YesNoEnum {
- Yes = "yes",
- No = "no",
-}
-
-export interface BaseQuestion {
- type: QuestionType;
- questionId: string;
- questionText: string;
- placeholder?: string;
-}
-
-export interface DetailedQuestion extends BaseQuestion {
- lowRatingLabel?: string;
- highRatingLabel?: string;
- showCommentsOn?: YesNoEnum;
- displayRating?: boolean;
- options?: Option[];
- maxRating?: number;
-}
diff --git a/frontend-new/src/feedback/overallFeedback/feedbackForm/questions-en.json b/frontend-new/src/feedback/overallFeedback/feedbackForm/questions-en.json
deleted file mode 100644
index 905b951d6..000000000
--- a/frontend-new/src/feedback/overallFeedback/feedbackForm/questions-en.json
+++ /dev/null
@@ -1,55 +0,0 @@
-{
- "interaction_ease": {
- "question_text": "How easy or difficult was it to interact with Compass and understand its responses?",
- "description": "This question is used to measure the Customer Effort Score (CES). The response is on a scale from 1 (Very difficult) to 5 (Very easy).",
- "comment_placeholder": "Please explain your rating and share any suggestions for improving the ease of interaction with Compass."
- },
- "clarity_of_skills": {
- "question_text": "Did Compass help you gain a clearer understanding of your skills? If not, why?",
- "description": "This question aims to measure Perceived Usefulness (PU). The user can answer TRUE (indicating Compass helped) or FALSE (indicating Compass did not help), with an optional text field for additional comments.",
- "comment_placeholder": "Please share why Compass did not help you gain a clearer understanding of your skills."
- },
- "satisfaction_with_compass": {
- "question_text": "How satisfied are you with Compass?",
- "description": "This question is used to measure the Customer Satisfaction Score (CSAT). The response is on a scale from 1 (Very dissatisfied) to 5 (Very satisfied).",
- "comment_placeholder": null
- },
- "work_experience_accuracy": {
- "question_text": "Were there any aspects of your work experience information identified by Compass that were inaccurate?",
- "options": {
- "experience_title": "Incorrect Experience title",
- "experience_dates": "Incorrect start/end dates",
- "experience_location": "Incorrect location",
- "duplicate_experience": "Duplicate Experiences",
- "missing_experience": "Missing Experiences",
- "other": "Other"
- },
- "description": "This question to identify specific inaccuracies in the work experience information. The response is an array of selected option keys, with optional additional comments.",
- "comment_placeholder": "Please provide more details about the inaccuracies in your work experience information identified by Compass."
- },
- "incorrect_skills": {
- "question_text": "Are there any skills that Compass incorrectly identified for you?",
- "description": "This question aims to indentify incorrectly identified skills The user can answer TRUE (indicating incorrect skills) or FALSE (indicating no incorrect skills). In case of TRUE, the user can provide additional comments.",
- "comment_placeholder": "Please list any skills that Compass incorrectly identified for you."
- },
- "missing_skills": {
- "question_text": "Are there any skills you have that Compass missed and did not identify?",
- "description": "This question aims to identify missing skills. Responses should be TRUE (indicating missing skills) or FALSE (indicating no missing skills). In case of TRUE, the user can provide additional comments.",
- "comment_placeholder": "Please list any skills you have that Compass did not identify."
- },
- "perceived_bias": {
- "question_text": "Did you feel that Compass treated you unfairly by making assumptions about your background, language, or other personal characteristics? If so, please describe your experience.",
- "description": "This question aims to identify perceived bias. Responses should be TRUE (indicating bias) or FALSE (indicating no bias), with an optional text field for additional comments.",
- "comment_placeholder": "Please share more details about your experience. Include specific examples of when you felt Compass treated you unfairly or made assumptions about your background, language, or other personal characteristics."
- },
- "recommendation": {
- "question_text": "How likely are you to recommend Compass to other job seekers?",
- "description": "This question is used to measure the Net Promoter Score (NPS). The response is on a scale from 1 (Not at all likely) to 5 (Extremely likely).",
- "comment_placeholder": null
- },
- "additional_feedback": {
- "question_text": "Please share any additional feedback or suggestions you have for improving Compass.",
- "description": "Used to collect any additional feedback or suggestions for improving Compass from the user. The response is in the form of a text.",
- "comment_placeholder": "We'd love to hear your thoughts! Please share any additional feedback or suggestions you have for improving Compass."
- }
-}
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/OverallFeedbackForm.stories.tsx b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/OverallFeedbackForm.stories.tsx
new file mode 100644
index 000000000..bd9cb703b
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/OverallFeedbackForm.stories.tsx
@@ -0,0 +1,100 @@
+import { Meta, type StoryObj } from "@storybook/react";
+import { action } from "@storybook/addon-actions";
+import OverallFeedbackForm from "src/feedback/overallFeedback/overallFeedbackForm/OverallFeedbackForm";
+import { FeedbackProvider } from "src/feedback/overallFeedback/feedbackContext/FeedbackContext";
+import { getBackendUrl } from "src/envService";
+import UserPreferencesStateService from "src/userPreferences/UserPreferencesStateService";
+import AuthenticationServiceFactory from "src/auth/services/Authentication.service.factory";
+import AuthenticationStateService from "src/auth/services/AuthenticationState.service";
+import { TabiyaUser } from "src/auth/auth.types";
+import AuthenticationService from "src/auth/services/Authentication.service";
+import { mockQuestionsConfig } from "src/feedback/overallFeedback/overallFeedbackForm/overallFeedbackForm.test.utils";
+
+// Mock authentication service to provide token validation and user info for API calls
+class MockAuthenticationService extends AuthenticationService {
+ private static instance: MockAuthenticationService;
+
+ private constructor() {
+ super();
+ }
+
+ static getInstance(): MockAuthenticationService {
+ if (!this.instance) {
+ this.instance = new this();
+ }
+ return this.instance;
+ }
+
+ async refreshToken(): Promise {}
+ cleanup(): void {}
+ async logout(): Promise {}
+ getUser(token: string): TabiyaUser | null {
+ return { id: "1", name: "Test User", email: "test@example.com" } as TabiyaUser;
+ }
+ getToken(): string {
+ return "foo token";
+ }
+ isTokenValid(token: string): { isValid: boolean; decodedToken: any; failureCause?: string } {
+ return { isValid: true, decodedToken: { sub: "1", email: "test@example.com" } };
+ }
+}
+
+const meta: Meta = {
+ title: "Feedback/OverallFeedback/OverallFeedbackForm",
+ component: OverallFeedbackForm,
+ tags: ["autodocs"],
+ args: {
+ notifyOnClose: action("notifyOnClose"),
+ },
+ parameters: {
+ mockData: [
+ // Mock the questions config endpoint that FeedbackProvider uses to fetch question data
+ {
+ url: getBackendUrl() + "/conversations/123/feedback/questions",
+ method: "GET",
+ status: 200,
+ response: mockQuestionsConfig,
+ },
+ // Mock the feedback submission endpoint that OverallFeedbackService uses to save ratings
+ {
+ url: getBackendUrl() + "/conversations/123/feedback",
+ method: "PATCH",
+ status: 200,
+ response: {
+ message: "Feedback submitted successfully",
+ },
+ },
+ ],
+ },
+ decorators: [
+ (Story) => {
+ // Mock session ID for API endpoint construction
+ const mockUserPrefsService = UserPreferencesStateService.getInstance();
+ mockUserPrefsService.getActiveSessionId = () => 123;
+
+ // Mock auth state for API authentication and user identification
+ const mockAuthStateService = AuthenticationStateService.getInstance();
+ mockAuthStateService.getToken = () => "foo token";
+ mockAuthStateService.getUser = () => ({ id: "1", name: "Test User", email: "test@example.com" } as TabiyaUser);
+
+ // Mock auth service factory to provide our mock auth service
+ AuthenticationServiceFactory.getCurrentAuthenticationService = () => MockAuthenticationService.getInstance();
+
+ return (
+
+
+
+ );
+ },
+ ],
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Shown: Story = {
+ args: {
+ isOpen: true,
+ },
+};
diff --git a/frontend-new/src/feedback/overallFeedback/feedbackForm/FeedbackForm.test.tsx b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/OverallFeedbackForm.test.tsx
similarity index 72%
rename from frontend-new/src/feedback/overallFeedback/feedbackForm/FeedbackForm.test.tsx
rename to frontend-new/src/feedback/overallFeedback/overallFeedbackForm/OverallFeedbackForm.test.tsx
index 7f0f8ac4e..150b7f37c 100644
--- a/frontend-new/src/feedback/overallFeedback/feedbackForm/FeedbackForm.test.tsx
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/OverallFeedbackForm.test.tsx
@@ -1,10 +1,10 @@
// mute the console
import "src/_test_utilities/consoleMock";
-import FeedbackForm, { DATA_TEST_ID, FeedbackCloseEvent } from "src/feedback/overallFeedback/feedbackForm/FeedbackForm";
+import OverallFeedbackForm, { DATA_TEST_ID, FeedbackCloseEvent } from "src/feedback/overallFeedback/overallFeedbackForm/OverallFeedbackForm";
import { render, screen } from "src/_test_utilities/test-utils";
import { act, fireEvent, waitFor } from "@testing-library/react";
-import FeedbackFormContent from "src/feedback/overallFeedback/feedbackForm/components/feedbackFormContent/FeedbackFormContent";
+import FormContent from "./components/formContent/FormContent";
import UserPreferencesStateService from "src/userPreferences/UserPreferencesStateService";
import { useSnackbar } from "src/theme/SnackbarProvider/SnackbarProvider";
import {
@@ -14,6 +14,9 @@ import {
import OverallFeedbackService from "src/feedback/overallFeedback/overallFeedbackService/OverallFeedback.service";
import { FeedbackError } from "src/error/commonErrors";
import { FeedbackResponse } from "src/feedback/overallFeedback/overallFeedbackService/OverallFeedback.service.types";
+import { FeedbackProvider } from "src/feedback/overallFeedback/feedbackContext/FeedbackContext";
+import React from "react";
+import { mockQuestionsConfig } from "src/feedback/overallFeedback/overallFeedbackForm/overallFeedbackForm.test.utils";
// mock the snackbar provider
jest.mock("src/theme/SnackbarProvider/SnackbarProvider", () => {
@@ -38,8 +41,8 @@ jest.mock("src/userPreferences/UserPreferencesStateService", () => ({
}));
// mock the feedback form content
-jest.mock("src/feedback/overallFeedback/feedbackForm/components/feedbackFormContent/FeedbackFormContent", () => {
- const actual = jest.requireActual("src/feedback/overallFeedback/feedbackForm/components/feedbackFormContent/FeedbackFormContent");
+jest.mock("src/feedback/overallFeedback/overallFeedbackForm/components/formContent/FormContent", () => {
+ const actual = jest.requireActual("src/feedback/overallFeedback/overallFeedbackForm/components/formContent/FormContent");
return {
...actual,
__esModule: true,
@@ -47,6 +50,14 @@ jest.mock("src/feedback/overallFeedback/feedbackForm/components/feedbackFormCont
};
});
+// Mock OverallFeedbackService.getQuestionsConfig
+jest.spyOn(OverallFeedbackService.getInstance(), "getQuestionsConfig").mockResolvedValue(mockQuestionsConfig);
+
+// Helper to wrap in FeedbackProvider
+const renderWithFeedbackProvider = (ui: React.ReactElement) => {
+ return render({ui});
+};
+
const mockFeedbackResponse: FeedbackResponse = {
id: "foo",
version: {
@@ -62,10 +73,10 @@ const mockFeedbackResponse: FeedbackResponse = {
created_at: new Date().toISOString()
}
-describe("FeedbackForm", () => {
+describe("OverallFeedbackForm", () => {
test("should render component successfully", () => {
// GIVEN the component
- const givenFeedbackForm = ;
+ const givenFeedbackForm = ;
// WHEN the component is rendered
render(givenFeedbackForm);
@@ -74,16 +85,16 @@ describe("FeedbackForm", () => {
expect(console.error).not.toHaveBeenCalled();
expect(console.warn).not.toHaveBeenCalled();
// AND the feedback form dialog to be in the document
- const feedbackFormContainer = screen.getByTestId(DATA_TEST_ID.FEEDBACK_FORM_DIALOG);
+ const feedbackFormContainer = screen.getByTestId(DATA_TEST_ID.OVERALL_FEEDBACK_FORM_DIALOG);
expect(feedbackFormContainer).toBeInTheDocument();
// AND the feedback form dialog title to be in the document
- expect(screen.getByTestId(DATA_TEST_ID.FEEDBACK_FORM_DIALOG_TITLE)).toBeInTheDocument();
+ expect(screen.getByTestId(DATA_TEST_ID.OVERALL_FEEDBACK_FORM_DIALOG_TITLE)).toBeInTheDocument();
// AND the feedback form dialog button to be in the document
- expect(screen.getByTestId(DATA_TEST_ID.FEEDBACK_FORM_DIALOG_BUTTON)).toBeInTheDocument();
+ expect(screen.getByTestId(DATA_TEST_ID.OVERALL_FEEDBACK_FORM_DIALOG_BUTTON)).toBeInTheDocument();
// AND the feedback form dialog icon button to be in the document
- expect(screen.getByTestId(DATA_TEST_ID.FEEDBACK_FORM_DIALOG_ICON_BUTTON)).toBeInTheDocument();
+ expect(screen.getByTestId(DATA_TEST_ID.OVERALL_FEEDBACK_FORM_DIALOG_ICON_BUTTON)).toBeInTheDocument();
// AND the feedback form dialog content to be in the document
- expect(screen.getByTestId(DATA_TEST_ID.FEEDBACK_FORM_DIALOG_CONTENT)).toBeInTheDocument();
+ expect(screen.getByTestId(DATA_TEST_ID.OVERALL_FEEDBACK_FORM_DIALOG_CONTENT)).toBeInTheDocument();
// AND to match the snapshot
expect(feedbackFormContainer).toMatchSnapshot();
});
@@ -91,19 +102,19 @@ describe("FeedbackForm", () => {
test("should call handleClose when close button is clicked", () => {
// GIVEN the component
const mockHandleClose = jest.fn();
- const givenFeedbackForm = ;
+ const givenFeedbackForm = ;
// AND the component is rendered
- render(givenFeedbackForm);
+ renderWithFeedbackProvider(givenFeedbackForm);
// WHEN the close button is clicked
- const closeButton = screen.getByTestId(DATA_TEST_ID.FEEDBACK_FORM_DIALOG_ICON_BUTTON);
+ const closeButton = screen.getByTestId(DATA_TEST_ID.OVERALL_FEEDBACK_FORM_DIALOG_ICON_BUTTON);
fireEvent.click(closeButton);
// THEN expect the notifyOnClose function to have been called
expect(mockHandleClose).toHaveBeenCalled();
});
- describe("FeedbackForm handleFeedbackSubmit", () => {
+ describe("OverallFeedbackForm handleFeedbackSubmit", () => {
const mockSendFeedback = jest.fn();
beforeEach(() => {
@@ -116,10 +127,10 @@ describe("FeedbackForm", () => {
jest.spyOn(OverallFeedbackService.getInstance(), "sendFeedback").mockResolvedValueOnce(mockFeedbackResponse)
// AND the component is rendered
const mockHandleClose = jest.fn();
- const givenFeedbackForm = ;
- render(givenFeedbackForm);
+ const givenFeedbackForm = ;
+ renderWithFeedbackProvider(givenFeedbackForm);
// AND when the submit button is clicked
- const submitCallback = (FeedbackFormContent as jest.Mock).mock.calls.at(-1)[0].notifySubmit;
+ const submitCallback = (FormContent as jest.Mock).mock.calls.at(-1)[0].notifySubmit;
await act(async () => {
submitCallback([
{
@@ -160,9 +171,9 @@ describe("FeedbackForm", () => {
});
// WHEN the component is rendered
- render();
+ renderWithFeedbackProvider();
// AND when the submit button is clicked
- const submitCallback = (FeedbackFormContent as jest.Mock).mock.calls.at(-1)[0].notifySubmit;
+ const submitCallback = (FormContent as jest.Mock).mock.calls.at(-1)[0].notifySubmit;
await act(async () => {
submitCallback([
{
diff --git a/frontend-new/src/feedback/overallFeedback/feedbackForm/FeedbackForm.tsx b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/OverallFeedbackForm.tsx
similarity index 76%
rename from frontend-new/src/feedback/overallFeedback/feedbackForm/FeedbackForm.tsx
rename to frontend-new/src/feedback/overallFeedback/overallFeedbackForm/OverallFeedbackForm.tsx
index b3d598569..3530d1905 100644
--- a/frontend-new/src/feedback/overallFeedback/feedbackForm/FeedbackForm.tsx
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/OverallFeedbackForm.tsx
@@ -1,6 +1,6 @@
import React from "react";
import { Dialog, DialogContent, DialogTitle, Typography, useTheme } from "@mui/material";
-import FeedbackFormContent from "src/feedback/overallFeedback/feedbackForm/components/feedbackFormContent/FeedbackFormContent";
+import FormContent from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/FormContent";
import PrimaryIconButton from "src/theme/PrimaryIconButton/PrimaryIconButton";
import CloseIcon from "@mui/icons-material/Close";
import OverallFeedbackService from "src/feedback/overallFeedback/overallFeedbackService/OverallFeedback.service";
@@ -9,7 +9,7 @@ import { FeedbackItem } from "src/feedback/overallFeedback/overallFeedbackServic
import { Backdrop } from "src/theme/Backdrop/Backdrop";
import UserPreferencesStateService from "src/userPreferences/UserPreferencesStateService";
import { FeedbackError } from "src/error/commonErrors";
-import { useIsSmallOrShortScreen } from "src/feedback/overallFeedback/feedbackForm/useIsSmallOrShortScreen";
+import { useIsSmallOrShortScreen } from "src/feedback/overallFeedback/overallFeedbackForm/useIsSmallOrShortScreen";
export interface FeedbackFormProps {
isOpen: boolean;
@@ -30,14 +30,14 @@ export enum FeedbackCloseEvent {
const uniqueId = "c6ba52ec-c1de-46ac-950b-f5354c6785ac";
export const DATA_TEST_ID = {
- FEEDBACK_FORM_DIALOG: `feedback-form-dialog-${uniqueId}`,
- FEEDBACK_FORM_DIALOG_TITLE: `feedback-form-dialog-title-${uniqueId}`,
- FEEDBACK_FORM_DIALOG_BUTTON: `feedback-form-dialog-button-${uniqueId}`,
- FEEDBACK_FORM_DIALOG_ICON_BUTTON: `feedback-form-dialog-icon-button-${uniqueId}`,
- FEEDBACK_FORM_DIALOG_CONTENT: `feedback-form-dialog-content-${uniqueId}`,
+ OVERALL_FEEDBACK_FORM_DIALOG: `overall-feedback-form-dialog-${uniqueId}`,
+ OVERALL_FEEDBACK_FORM_DIALOG_TITLE: `overall-feedback-form-dialog-title-${uniqueId}`,
+ OVERALL_FEEDBACK_FORM_DIALOG_BUTTON: `overall-feedback-form-dialog-button-${uniqueId}`,
+ OVERALL_FEEDBACK_FORM_DIALOG_ICON_BUTTON: `overall-feedback-form-dialog-icon-button-${uniqueId}`,
+ OVERALL_FEEDBACK_FORM_DIALOG_CONTENT: `overall-feedback-form-dialog-content-${uniqueId}`,
};
-const FeedbackForm: React.FC = ({ isOpen, notifyOnClose }) => {
+const OverallFeedbackForm: React.FC = ({ isOpen, notifyOnClose }) => {
const theme = useTheme();
const { enqueueSnackbar } = useSnackbar();
const isSmallOrShortScreen = useIsSmallOrShortScreen();
@@ -49,7 +49,6 @@ const FeedbackForm: React.FC = ({ isOpen, notifyOnClose }) =>
const handleFeedbackSubmit = async (formData: FeedbackItem[]): Promise => {
setIsSubmitting(true);
- notifyOnClose(FeedbackCloseEvent.SUBMIT);
try {
const userPreferences = UserPreferencesStateService.getInstance().getUserPreferences();
if (!userPreferences?.sessions.length) {
@@ -61,6 +60,7 @@ const FeedbackForm: React.FC = ({ isOpen, notifyOnClose }) =>
await overallFeedbackService.sendFeedback(sessionId, formData);
enqueueSnackbar("Feedback submitted successfully!", { variant: "success" });
+ notifyOnClose(FeedbackCloseEvent.SUBMIT);
} catch (error) {
console.error(new FeedbackError("Failed to submit feedback", error));
enqueueSnackbar("Failed to submit feedback. Please try again later.", { variant: "error" });
@@ -77,7 +77,7 @@ const FeedbackForm: React.FC = ({ isOpen, notifyOnClose }) =>
maxWidth="sm"
fullWidth={true}
fullScreen={isSmallOrShortScreen}
- data-testid={DATA_TEST_ID.FEEDBACK_FORM_DIALOG}
+ data-testid={DATA_TEST_ID.OVERALL_FEEDBACK_FORM_DIALOG}
sx={{
"& .MuiDialog-paper": {
height: isSmallOrShortScreen ? "100%" : "85%",
@@ -101,20 +101,20 @@ const FeedbackForm: React.FC = ({ isOpen, notifyOnClose }) =>
paddingBottom: 0,
}}
>
-
+
Help us improve!
-
+ = ({ isOpen, notifyOnClose }) =>
: theme.fixedSpacing(theme.tabiyaSpacing.md),
}}
>
-
+
@@ -130,4 +130,4 @@ const FeedbackForm: React.FC = ({ isOpen, notifyOnClose }) =>
);
};
-export default FeedbackForm;
+export default OverallFeedbackForm;
diff --git a/frontend-new/src/feedback/overallFeedback/feedbackForm/__snapshots__/FeedbackForm.test.tsx.snap b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/__snapshots__/OverallFeedbackForm.test.tsx.snap
similarity index 82%
rename from frontend-new/src/feedback/overallFeedback/feedbackForm/__snapshots__/FeedbackForm.test.tsx.snap
rename to frontend-new/src/feedback/overallFeedback/overallFeedbackForm/__snapshots__/OverallFeedbackForm.test.tsx.snap
index 36be63abd..899836444 100644
--- a/frontend-new/src/feedback/overallFeedback/feedbackForm/__snapshots__/FeedbackForm.test.tsx.snap
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/__snapshots__/OverallFeedbackForm.test.tsx.snap
@@ -1,9 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`FeedbackForm should render component successfully 1`] = `
+exports[`OverallFeedbackForm should render component successfully 1`] = `
onChange({
+ question_id: "additional_feedback",
+ simplified_answer: { rating_numeric: 4, comment: "Overall good experience" }
+ })}
+ />
+ ),
+ };
+});
+
+// Helper function to wrap components with FeedbackProvider
+const renderWithFeedbackProvider = (ui: React.ReactElement) => {
+ return render(
+
+ {ui}
+
+ );
+};
+
+describe("FormContent", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ resetAllMethodMocks(UserPreferencesStateService.getInstance());
+ resetAllMethodMocks(OverallFeedbackService.getInstance());
+ (PersistentStorageService.getOverallFeedback as jest.Mock).mockReturnValue([]);
+ // Mock the active session ID
+ jest.spyOn(UserPreferencesStateService.getInstance(), "getActiveSessionId").mockReturnValue(1);
+ // Mock the questions config
+ jest.spyOn(OverallFeedbackService.getInstance(), "getQuestionsConfig").mockResolvedValue(mockQuestionsConfig);
+ });
+
+ test("should render component successfully", () => {
+ // GIVEN the component
+ const givenFeedbackFormContent = ;
+
+ // WHEN the component is rendered
+ renderWithFeedbackProvider(givenFeedbackFormContent);
+
+ // THEN the feedback form content to be in the document
+ const feedbackFormContent = screen.getByTestId(DATA_TEST_ID.FEEDBACK_FORM_CONTENT);
+ expect(feedbackFormContent).toBeInTheDocument();
+ // AND the feedback form content title to be in the document
+ expect(screen.getByTestId(DATA_TEST_ID.FEEDBACK_FORM_CONTENT_TITLE)).toBeInTheDocument();
+ // AND the feedback form content divider to be in the document
+ expect(screen.getByTestId(DATA_TEST_ID.FEEDBACK_FORM_CONTENT_DIVIDER)).toBeInTheDocument();
+ // AND the feedback form content questions to be in the document
+ expect(screen.getByTestId(DATA_TEST_ID.FEEDBACK_FORM_CONTENT_QUESTIONS)).toBeInTheDocument();
+ // AND the feedback form next button to be in the document
+ expect(screen.getByTestId(DATA_TEST_ID.FEEDBACK_FORM_NEXT_BUTTON)).toBeInTheDocument();
+ // AND the feedback form back button to be in the document
+ expect(screen.getByTestId(DATA_TEST_ID.FEEDBACK_FORM_BACK_BUTTON)).toBeInTheDocument();
+ // AND to match the snapshot
+ expect(feedbackFormContent).toMatchSnapshot();
+ });
+
+ describe("action tests", () => {
+ test("should call handleNext when next button is clicked", () => {
+ // GIVEN the component
+ const givenFeedbackFormContent = ;
+ // AND the component is rendered
+ renderWithFeedbackProvider(givenFeedbackFormContent);
+
+ // WHEN the next button is clicked
+ const nextButton = screen.getByTestId(DATA_TEST_ID.FEEDBACK_FORM_NEXT_BUTTON);
+ fireEvent.click(nextButton);
+
+ // THEN expect to go to the next step
+ expect(screen.getByTestId(DATA_TEST_ID.FEEDBACK_FORM_CONTENT_TITLE)).toHaveTextContent("Skill Accuracy");
+ });
+
+ test("should call handlePrevious when back button is clicked", () => {
+ // GIVEN the component
+ const givenFeedbackFormContent = ;
+ // AND the component is rendered
+ renderWithFeedbackProvider(givenFeedbackFormContent);
+
+ // WHEN the next button is clicked to move to the next step
+ const nextButton = screen.getByTestId(DATA_TEST_ID.FEEDBACK_FORM_NEXT_BUTTON);
+ fireEvent.click(nextButton);
+ // AND the back button is clicked
+ const backButton = screen.getByTestId(DATA_TEST_ID.FEEDBACK_FORM_BACK_BUTTON);
+ fireEvent.click(backButton);
+
+ // THEN expect to go to the previous step
+ expect(screen.getByTestId(DATA_TEST_ID.FEEDBACK_FORM_CONTENT_TITLE)).toHaveTextContent("Bias & Experience Accuracy");
+ });
+
+ test.todo("should call handleSubmit with exact answer data when answering each step");
+
+ test.todo("should handle swipe navigation");
+
+ test("should not allow to swipe past the last step", () => {
+ // GIVEN the FormContent component
+ const givenFeedbackFormContent = ;
+ // AND the component is rendered
+ renderWithFeedbackProvider(givenFeedbackFormContent);
+
+ // WHEN we move to the last step
+ const nextButton = screen.getByTestId(DATA_TEST_ID.FEEDBACK_FORM_NEXT_BUTTON);
+ fireEvent.click(nextButton);
+ fireEvent.click(nextButton);
+
+ // AND try to swipe left
+ const swipeHandlers = (useSwipeable as jest.Mock).mock.results[0].value;
+ act(() => {
+ swipeHandlers.onSwipedLeft();
+ });
+
+ // THEN expect to stay on the last step
+ expect(screen.getByTestId(DATA_TEST_ID.FEEDBACK_FORM_CONTENT_TITLE)).toHaveTextContent("Final feedback");
+ });
+
+ test("should not allow to swipe before the first step", () => {
+ // GIVEN the FormContent component
+ const givenFeedbackFormContent = ;
+ // AND the component is rendered
+ renderWithFeedbackProvider(givenFeedbackFormContent);
+
+ // WHEN we try to swipe right
+ const swipeHandlers = (useSwipeable as jest.Mock).mock.results[0].value;
+ act(() => {
+ swipeHandlers.onSwipedRight();
+ });
+
+ // THEN expect to stay on the first step
+ expect(screen.getByTestId(DATA_TEST_ID.FEEDBACK_FORM_CONTENT_TITLE)).toHaveTextContent("Bias & Experience Accuracy");
+ });
+ });
+});
diff --git a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/feedbackFormContent/FeedbackFormContent.tsx b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/FormContent.tsx
similarity index 67%
rename from frontend-new/src/feedback/overallFeedback/feedbackForm/components/feedbackFormContent/FeedbackFormContent.tsx
rename to frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/FormContent.tsx
index c8aff71ce..4a90e943b 100644
--- a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/feedbackFormContent/FeedbackFormContent.tsx
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/FormContent.tsx
@@ -5,13 +5,19 @@ import MobileStepper from "@mui/material/MobileStepper";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import PrimaryButton from "src/theme/PrimaryButton/PrimaryButton";
-import feedbackFormContentSteps from "src/feedback/overallFeedback/feedbackForm/components/feedbackFormContent/feedbackFormContentSteps";
-import StepsComponent from "src/feedback/overallFeedback/feedbackForm/components/stepsComponent/StepsComponent";
-import { PersistentStorageService } from "src/app/PersistentStorageService/PersistentStorageService";
import { FeedbackItem } from "src/feedback/overallFeedback/overallFeedbackService/OverallFeedback.service.types";
import SecondaryButton from "src/theme/SecondaryButton/SecondaryButton";
import { useSwipeable } from "react-swipeable";
import { AnimatePresence, motion } from "framer-motion";
+import PerceivedBiasQuestion from "./questionComponents/perceivedBias/PerceivedBiasQuestion";
+import WorkExperienceAccuracyQuestion from "./questionComponents/workExperienceAccuracy/WorkExperienceAccuracyQuestion";
+import ClarityOfSkillsQuestion from "./questionComponents/clarityOfSkills/ClarityOfSkillsQuestion";
+import IncorrectSkillsQuestion from "./questionComponents/incorrectSkills/IncorrectSkillsQuestion";
+import MissingSkillsQuestion from "./questionComponents/missingSkills/MissingSkillsQuestion";
+import InteractionEaseQuestion from "./questionComponents/interactionEase/InteractionEaseQuestion";
+import RecommendationQuestion from "./questionComponents/recommendation/RecommendationQuestion";
+import AdditionalFeedbackQuestion from "./questionComponents/additionalFeedback/AdditionalFeedbackQuestion";
+import { useFeedback } from "src/feedback/overallFeedback/feedbackContext/FeedbackContext";
export const SLIDE_DURATION = 0.3;
@@ -30,24 +36,21 @@ export const DATA_TEST_ID = {
FEEDBACK_FORM_BACK_BUTTON: `feedback-form-back-button-${uniqueId}`,
};
-const FeedbackFormContent: React.FC = ({ notifySubmit }) => {
+const FormContent: React.FC = ({ notifySubmit }) => {
const theme = useTheme();
const [activeStep, setActiveStep] = useState(0);
- const [answers, setAnswers] = useState(() => {
- return PersistentStorageService.getOverallFeedback();
- });
+ const { answers, handleAnswerChange, clearAnswers } = useFeedback();
// We want to know the previous step to know the direction of the swipe
// if the previous tep is greater than the active step, the swipe is to the left
// if the previous step is less than the active step, the swipe is to the right
const [prevStep, setPrevStep] = useState(activeStep);
- const maxSteps = feedbackFormContentSteps.length;
+ const maxSteps = 3; // Total number of question groups
const handleNext = () => {
if (activeStep === maxSteps - 1) {
notifySubmit(answers);
- PersistentStorageService.clearOverallFeedback();
- setAnswers([]);
+ clearAnswers();
} else {
setPrevStep(activeStep);
setActiveStep((prev) => prev + 1);
@@ -74,25 +77,6 @@ const FeedbackFormContent: React.FC = ({ notifySubmit
preventScrollOnSwipe: true,
});
- const handleAnswerChange = (feedback: FeedbackItem) => {
- setAnswers((prevAnswers) => {
- const existingIndex = prevAnswers.findIndex((item) => item.question_id === feedback.question_id);
-
- let updatedAnswers;
- if (existingIndex !== -1) {
- updatedAnswers = [...prevAnswers];
- updatedAnswers[existingIndex] = feedback;
- } else {
- updatedAnswers = [...prevAnswers, feedback];
- }
-
- // Save updated answers to persistent storage
- PersistentStorageService.setOverallFeedback(updatedAnswers);
-
- return updatedAnswers;
- });
- };
-
// Check if there is at least one answer
const hasAnswers = Object.keys(answers).length > 0;
@@ -120,6 +104,63 @@ const FeedbackFormContent: React.FC = ({ notifySubmit
}),
};
+ const renderQuestionGroup = () => {
+ switch (activeStep) {
+ case 0:
+ return (
+ <>
+
+ Bias & Experience Accuracy
+
+
+
+ >
+ );
+ case 1:
+ return (
+ <>
+
+ Skill Accuracy
+
+
+
+
+ >
+ );
+ case 2:
+ return (
+ <>
+
+ Final feedback
+
+
+
+
+ >
+ );
+ default:
+ return null;
+ }
+ };
+
return (
= ({ notifySubmit
bottom: 0,
}}
>
-
- {feedbackFormContentSteps[activeStep].label}
- = ({ notifySubmit
}}
data-testid={DATA_TEST_ID.FEEDBACK_FORM_CONTENT_QUESTIONS}
>
-
+ {renderQuestionGroup()}
@@ -218,4 +246,4 @@ const FeedbackFormContent: React.FC = ({ notifySubmit
);
};
-export default FeedbackFormContent;
+export default FormContent;
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/__snapshots__/FormContent.test.tsx.snap b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/__snapshots__/FormContent.test.tsx.snap
new file mode 100644
index 000000000..fb0954617
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/__snapshots__/FormContent.test.tsx.snap
@@ -0,0 +1,83 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`FormContent should render component successfully 1`] = `
+
+
+
+
+
+ Bias & Experience Accuracy
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/additionalFeedback/AdditionalFeedbackQuestion.stories.tsx b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/additionalFeedback/AdditionalFeedbackQuestion.stories.tsx
new file mode 100644
index 000000000..3d3159f1c
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/additionalFeedback/AdditionalFeedbackQuestion.stories.tsx
@@ -0,0 +1,141 @@
+import { Meta, StoryObj } from "@storybook/react";
+import AdditionalFeedbackQuestion from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/additionalFeedback/AdditionalFeedbackQuestion";
+import { action } from "@storybook/addon-actions";
+import { FeedbackProvider } from "src/feedback/overallFeedback/feedbackContext/FeedbackContext";
+import { getBackendUrl } from "src/envService";
+import { QuestionsConfig, QuestionType } from "src/feedback/overallFeedback/overallFeedbackForm/overallFeedbackForm.types";
+import UserPreferencesStateService from "src/userPreferences/UserPreferencesStateService";
+import AuthenticationServiceFactory from "src/auth/services/Authentication.service.factory";
+import AuthenticationStateService from "src/auth/services/AuthenticationState.service";
+import { TabiyaUser } from "src/auth/auth.types";
+import AuthenticationService from "src/auth/services/Authentication.service";
+
+// Mock authentication service to provide token validation and user info for API calls
+class MockAuthenticationService extends AuthenticationService {
+ private static instance: MockAuthenticationService;
+
+ private constructor() {
+ super();
+ }
+
+ static getInstance(): MockAuthenticationService {
+ if (!this.instance) {
+ this.instance = new this();
+ }
+ return this.instance;
+ }
+
+ async refreshToken(): Promise {}
+ cleanup(): void {}
+ async logout(): Promise {}
+ getUser(token: string): TabiyaUser | null {
+ return { id: "1", name: "Test User", email: "test@example.com" } as TabiyaUser;
+ }
+ getToken(): string {
+ return "foo token";
+ }
+ isTokenValid(token: string): { isValid: boolean; decodedToken: any; failureCause?: string } {
+ return { isValid: true, decodedToken: { sub: "1", email: "test@example.com" } };
+ }
+}
+
+// Mock questions config that matches the backend response structure
+const mockQuestionsConfig: QuestionsConfig = {
+ additional_feedback: {
+ question_id: "additional_feedback",
+ question_text: "Please share any additional feedback",
+ description: "Please share any additional feedback",
+ type: QuestionType.Rating,
+ comment_placeholder: "Please share your thoughts",
+ low_rating_label: "Not helpful",
+ high_rating_label: "Very helpful",
+ max_rating: 5,
+ display_rating: true,
+ },
+ // Add other required fields with dummy values
+ satisfaction_with_compass: {} as any,
+ perceived_bias: {} as any,
+ work_experience_accuracy: {} as any,
+ clarity_of_skills: {} as any,
+ incorrect_skills: {} as any,
+ missing_skills: {} as any,
+ interaction_ease: {} as any,
+ recommendation: {} as any,
+};
+
+const meta: Meta = {
+ title: "Feedback/OverallFeedback/QuestionComponents/AdditionalFeedbackQuestion",
+ component: AdditionalFeedbackQuestion,
+ tags: ["autodocs"],
+ args: {
+ feedbackItems: [],
+ onChange: (data) => {
+ action("onChange")(data);
+ },
+ },
+ parameters: {
+ mockData: [
+ // Mock the questions config endpoint that FeedbackProvider uses to fetch question data
+ {
+ url: getBackendUrl() + "/conversations/123/feedback/questions",
+ method: "GET",
+ status: 200,
+ response: mockQuestionsConfig,
+ },
+ // Mock the feedback submission endpoint that OverallFeedbackService uses to save ratings
+ {
+ url: getBackendUrl() + "/conversations/123/feedback",
+ method: "PATCH",
+ status: 200,
+ response: {
+ message: "Feedback submitted successfully",
+ },
+ },
+ ],
+ },
+ decorators: [
+ (Story) => {
+ // Mock session ID for API endpoint construction
+ const mockUserPrefsService = UserPreferencesStateService.getInstance();
+ mockUserPrefsService.getActiveSessionId = () => 123;
+
+ // Mock auth state for API authentication and user identification
+ const mockAuthStateService = AuthenticationStateService.getInstance();
+ mockAuthStateService.getToken = () => "foo token";
+ mockAuthStateService.getUser = () => ({ id: "1", name: "Test User", email: "test@example.com" } as TabiyaUser);
+
+ // Mock auth service factory to provide our mock auth service
+ AuthenticationServiceFactory.getCurrentAuthenticationService = () => MockAuthenticationService.getInstance();
+
+ return (
+
+
+
+ );
+ },
+ ],
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ feedbackItems: [],
+ },
+};
+
+export const WithExistingFeedback: Story = {
+ args: {
+ feedbackItems: [
+ {
+ question_id: "additional_feedback",
+ simplified_answer: {
+ rating_numeric: 4,
+ comment: "The system was very helpful overall",
+ },
+ },
+ ],
+ },
+};
\ No newline at end of file
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/additionalFeedback/AdditionalFeedbackQuestion.test.tsx b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/additionalFeedback/AdditionalFeedbackQuestion.test.tsx
new file mode 100644
index 000000000..6dddf0fb7
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/additionalFeedback/AdditionalFeedbackQuestion.test.tsx
@@ -0,0 +1,137 @@
+// mute the console
+import "src/_test_utilities/consoleMock";
+import React from "react";
+import { render, screen } from "src/_test_utilities/test-utils";
+import AdditionalFeedbackQuestion, {
+ DATA_TEST_ID,
+} from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/additionalFeedback/AdditionalFeedbackQuestion";
+import { FeedbackProvider } from "src/feedback/overallFeedback/feedbackContext/FeedbackContext";
+import { DATA_TEST_ID as QUESTION_RENDERER_DATA_TEST_ID } from "src/feedback/overallFeedback/overallFeedbackForm/components/questionsRenderer/QuestionRenderer";
+import { mockQuestionsConfig } from "src/feedback/overallFeedback/overallFeedbackForm/overallFeedbackForm.test.utils";
+import { ADDITIONAL_FEEDBACK_QUESTION_ID } from "./constants";
+
+jest.mock("src/feedback/overallFeedback/overallFeedbackForm/components/questionsRenderer/QuestionRenderer", () => {
+ const actual = jest.requireActual("src/feedback/overallFeedback/overallFeedbackForm/components/questionsRenderer/QuestionRenderer");
+ return {
+ ...actual,
+ __esModule: true,
+ default: jest.fn().mockImplementation(() => ),
+ };
+});
+
+describe("AdditionalFeedbackQuestion", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ const mockOnChange = jest.fn();
+
+ // Spy on the useFeedback hook to return our mockQuestionsConfig
+ jest.spyOn(require("src/feedback/overallFeedback/feedbackContext/FeedbackContext"), "useFeedback").mockReturnValue({
+ questionsConfig: mockQuestionsConfig,
+ setQuestionsConfig: jest.fn(),
+ isLoading: false,
+ error: null,
+ answers: [],
+ handleAnswerChange: jest.fn(),
+ clearAnswers: jest.fn(),
+ });
+
+ test("should render correctly with empty feedbackItems", () => {
+ // GIVEN a questions config and no feedback items
+ function Wrapper({ children }: React.PropsWithChildren<{}>) {
+ return {children};
+ }
+ // WHEN the component is rendered
+ const { container } = render(
+ , { wrapper: Wrapper }
+ );
+ // THEN the question should be in the document
+ expect(screen.getByTestId(DATA_TEST_ID.ADDITIONAL_FEEDBACK)).toBeInTheDocument();
+ // AND the question renderer should be in the document
+ expect(screen.getByTestId(QUESTION_RENDERER_DATA_TEST_ID.QUESTION_RENDERER)).toBeInTheDocument();
+ // AND it should match the snapshot
+ expect(container).toMatchSnapshot();
+ });
+
+ test("should render correctly with existing feedbackItems", () => {
+ // GIVEN a questions config and a feedback item
+ const feedbackItems = [
+ {
+ question_id: ADDITIONAL_FEEDBACK_QUESTION_ID,
+ simplified_answer: {
+ rating_numeric: 4,
+ comment: "The system was very helpful overall",
+ },
+ },
+ ];
+ function Wrapper({ children }: React.PropsWithChildren<{}>) {
+ return {children};
+ }
+ // WHEN the component is rendered
+ const { container } = render(
+ , { wrapper: Wrapper }
+ );
+ // THEN the question should be in the document
+ expect(screen.getByTestId(DATA_TEST_ID.ADDITIONAL_FEEDBACK)).toBeInTheDocument();
+ // AND it should match the snapshot
+ expect(container).toMatchSnapshot();
+ });
+
+ test("should return null when questionsConfig is not available", () => {
+ // GIVEN the useFeedback hook returns null for questionsConfig
+ jest.spyOn(require("src/feedback/overallFeedback/feedbackContext/FeedbackContext"), "useFeedback").mockReturnValue({
+ questionsConfig: null,
+ setQuestionsConfig: jest.fn(),
+ isLoading: false,
+ error: null,
+ answers: [],
+ handleAnswerChange: jest.fn(),
+ clearAnswers: jest.fn(),
+ });
+
+ function Wrapper({ children }: React.PropsWithChildren<{}>) {
+ return {children};
+ }
+
+ // WHEN the component is rendered
+ const { container } = render(
+ , { wrapper: Wrapper }
+ );
+
+ // THEN expect the container to be empty
+ expect(container).toBeEmptyDOMElement();
+ // AND expect an error to be logged
+ expect(console.error).toHaveBeenCalledWith(expect.any(Error));
+ });
+
+ test("should return null when question ID is not in questionsConfig", () => {
+ // GIVEN the useFeedback hook returns a config without the required question ID
+ const configWithoutQuestion = { ...mockQuestionsConfig };
+ delete configWithoutQuestion[ADDITIONAL_FEEDBACK_QUESTION_ID];
+
+ jest.spyOn(require("src/feedback/overallFeedback/feedbackContext/FeedbackContext"), "useFeedback").mockReturnValue({
+ questionsConfig: configWithoutQuestion,
+ setQuestionsConfig: jest.fn(),
+ isLoading: false,
+ error: null,
+ answers: [],
+ handleAnswerChange: jest.fn(),
+ clearAnswers: jest.fn(),
+ });
+
+ function Wrapper({ children }: React.PropsWithChildren<{}>) {
+ return {children};
+ }
+
+ // WHEN the component is rendered
+ const { container } = render(
+ , { wrapper: Wrapper }
+ );
+
+ // THEN expect the container to be empty
+ expect(container).toBeEmptyDOMElement();
+ // AND expect an error to be logged
+ expect(console.error).toHaveBeenCalledWith(expect.any(Error));
+ });
+});
\ No newline at end of file
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/additionalFeedback/AdditionalFeedbackQuestion.tsx b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/additionalFeedback/AdditionalFeedbackQuestion.tsx
new file mode 100644
index 000000000..4636afd22
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/additionalFeedback/AdditionalFeedbackQuestion.tsx
@@ -0,0 +1,43 @@
+import React from "react";
+import { Box } from "@mui/material";
+import { useFeedback } from "src/feedback/overallFeedback/feedbackContext/FeedbackContext";
+import QuestionRenderer
+ from "src/feedback/overallFeedback/overallFeedbackForm/components/questionsRenderer/QuestionRenderer";
+import { FeedbackItem } from "src/feedback/overallFeedback/overallFeedbackService/OverallFeedback.service.types";
+import { FeedbackError } from "src/error/commonErrors";
+import { ADDITIONAL_FEEDBACK_QUESTION_ID } from "./constants";
+
+const uniqueId = "5d5def55-b805-4ef6-84cd-e0388d0e7cca";
+
+export const DATA_TEST_ID = {
+ ADDITIONAL_FEEDBACK: `additional-feedback-${uniqueId}`,
+};
+
+interface AdditionalFeedbackQuestionProps {
+ feedbackItems: FeedbackItem[];
+ onChange: (data: FeedbackItem) => void;
+}
+
+const AdditionalFeedbackQuestion: React.FC = ({ feedbackItems, onChange }) => {
+ const { questionsConfig, error } = useFeedback();
+
+ if (!questionsConfig) {
+ console.error(new FeedbackError("Questions configuration is not available", error));
+ return null;
+ }
+
+ const question = questionsConfig[ADDITIONAL_FEEDBACK_QUESTION_ID];
+
+ if (!question) {
+ console.error(new FeedbackError(`Questions configuration is not available for question: ${ADDITIONAL_FEEDBACK_QUESTION_ID}`, error));
+ return null;
+ }
+
+ return (
+
+
+
+ );
+};
+
+export default AdditionalFeedbackQuestion;
\ No newline at end of file
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/additionalFeedback/__snapshots__/AdditionalFeedbackQuestion.test.tsx.snap b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/additionalFeedback/__snapshots__/AdditionalFeedbackQuestion.test.tsx.snap
new file mode 100644
index 000000000..2a22f2ebe
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/additionalFeedback/__snapshots__/AdditionalFeedbackQuestion.test.tsx.snap
@@ -0,0 +1,27 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`AdditionalFeedbackQuestion should render correctly with empty feedbackItems 1`] = `
+
+
+
+
+
+`;
+
+exports[`AdditionalFeedbackQuestion should render correctly with existing feedbackItems 1`] = `
+
+
+
+
+
+`;
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/additionalFeedback/constants.ts b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/additionalFeedback/constants.ts
new file mode 100644
index 000000000..599968cb2
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/additionalFeedback/constants.ts
@@ -0,0 +1,2 @@
+// this is in a file of its own instead of in the AdditionalFeedbackQuestion.tsx to avoid circular dependencies
+export const ADDITIONAL_FEEDBACK_QUESTION_ID = "additional_feedback";
\ No newline at end of file
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/clarityOfSkills/ClarityOfSkillsQuestion.stories.tsx b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/clarityOfSkills/ClarityOfSkillsQuestion.stories.tsx
new file mode 100644
index 000000000..81bb0e215
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/clarityOfSkills/ClarityOfSkillsQuestion.stories.tsx
@@ -0,0 +1,138 @@
+import { Meta, StoryObj } from "@storybook/react";
+import ClarityOfSkillsQuestion from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/clarityOfSkills/ClarityOfSkillsQuestion";
+import { action } from "@storybook/addon-actions";
+import { FeedbackProvider } from "src/feedback/overallFeedback/feedbackContext/FeedbackContext";
+import { getBackendUrl } from "src/envService";
+import { QuestionsConfig, QuestionType, YesNoEnum } from "src/feedback/overallFeedback/overallFeedbackForm/overallFeedbackForm.types";
+import UserPreferencesStateService from "src/userPreferences/UserPreferencesStateService";
+import AuthenticationServiceFactory from "src/auth/services/Authentication.service.factory";
+import AuthenticationStateService from "src/auth/services/AuthenticationState.service";
+import { TabiyaUser } from "src/auth/auth.types";
+import AuthenticationService from "src/auth/services/Authentication.service";
+
+// Mock authentication service to provide token validation and user info for API calls
+class MockAuthenticationService extends AuthenticationService {
+ private static instance: MockAuthenticationService;
+
+ private constructor() {
+ super();
+ }
+
+ static getInstance(): MockAuthenticationService {
+ if (!this.instance) {
+ this.instance = new this();
+ }
+ return this.instance;
+ }
+
+ async refreshToken(): Promise {}
+ cleanup(): void {}
+ async logout(): Promise {}
+ getUser(token: string): TabiyaUser | null {
+ return { id: "1", name: "Test User", email: "test@example.com" } as TabiyaUser;
+ }
+ getToken(): string {
+ return "foo token";
+ }
+ isTokenValid(token: string): { isValid: boolean; decodedToken: any; failureCause?: string } {
+ return { isValid: true, decodedToken: { sub: "1", email: "test@example.com" } };
+ }
+}
+
+// Mock questions config that matches the backend response structure
+const mockQuestionsConfig: QuestionsConfig = {
+ clarity_of_skills: {
+ question_id: "clarity_of_skills",
+ question_text: "How clear was the skills assessment?",
+ description: "Please rate the clarity",
+ type: QuestionType.YesNo,
+ show_comments_on: YesNoEnum.No,
+ comment_placeholder: "Please share your thoughts",
+ },
+ // Add other required fields with dummy values
+ satisfaction_with_compass: {} as any,
+ perceived_bias: {} as any,
+ work_experience_accuracy: {} as any,
+ incorrect_skills: {} as any,
+ missing_skills: {} as any,
+ interaction_ease: {} as any,
+ recommendation: {} as any,
+ additional_feedback: {} as any,
+};
+
+const meta: Meta = {
+ title: "Feedback/OverallFeedback/QuestionComponents/ClarityOfSkillsQuestion",
+ component: ClarityOfSkillsQuestion,
+ tags: ["autodocs"],
+ args: {
+ feedbackItems: [],
+ onChange: (data) => {
+ action("onChange")(data);
+ },
+ },
+ parameters: {
+ mockData: [
+ // Mock the questions config endpoint that FeedbackProvider uses to fetch question data
+ {
+ url: getBackendUrl() + "/conversations/123/feedback/questions",
+ method: "GET",
+ status: 200,
+ response: mockQuestionsConfig,
+ },
+ // Mock the feedback submission endpoint that OverallFeedbackService uses to save ratings
+ {
+ url: getBackendUrl() + "/conversations/123/feedback",
+ method: "PATCH",
+ status: 200,
+ response: {
+ message: "Feedback submitted successfully",
+ },
+ },
+ ],
+ },
+ decorators: [
+ (Story) => {
+ // Mock session ID for API endpoint construction
+ const mockUserPrefsService = UserPreferencesStateService.getInstance();
+ mockUserPrefsService.getActiveSessionId = () => 123;
+
+ // Mock auth state for API authentication and user identification
+ const mockAuthStateService = AuthenticationStateService.getInstance();
+ mockAuthStateService.getToken = () => "foo token";
+ mockAuthStateService.getUser = () => ({ id: "1", name: "Test User", email: "test@example.com" } as TabiyaUser);
+
+ // Mock auth service factory to provide our mock auth service
+ AuthenticationServiceFactory.getCurrentAuthenticationService = () => MockAuthenticationService.getInstance();
+
+ return (
+
+
+
+ );
+ },
+ ],
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ feedbackItems: [],
+ },
+};
+
+export const WithExistingFeedback: Story = {
+ args: {
+ feedbackItems: [
+ {
+ question_id: "clarity_of_skills",
+ simplified_answer: {
+ rating_boolean: false,
+ comment: "The skills assessment was not clear enough",
+ },
+ },
+ ],
+ },
+};
\ No newline at end of file
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/clarityOfSkills/ClarityOfSkillsQuestion.test.tsx b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/clarityOfSkills/ClarityOfSkillsQuestion.test.tsx
new file mode 100644
index 000000000..6329ce0b7
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/clarityOfSkills/ClarityOfSkillsQuestion.test.tsx
@@ -0,0 +1,134 @@
+// mute the console
+import "src/_test_utilities/consoleMock";
+import React from "react";
+import { render, screen } from "src/_test_utilities/test-utils";
+import ClarityOfSkillsQuestion, {
+ DATA_TEST_ID,
+} from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/clarityOfSkills/ClarityOfSkillsQuestion";
+import { FeedbackProvider } from "src/feedback/overallFeedback/feedbackContext/FeedbackContext";
+import { mockQuestionsConfig } from "src/feedback/overallFeedback/overallFeedbackForm/overallFeedbackForm.test.utils";
+import { CLARITY_OF_SKILLS_QUESTION_ID } from "./constants";
+
+jest.mock("src/feedback/overallFeedback/overallFeedbackForm/components/questionsRenderer/QuestionRenderer", () => {
+ const actual = jest.requireActual("src/feedback/overallFeedback/overallFeedbackForm/components/questionsRenderer/QuestionRenderer");
+ return {
+ ...actual,
+ __esModule: true,
+ default: jest.fn().mockImplementation(() => ),
+ };
+})
+
+describe("ClarityOfSkillsQuestion", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ const mockOnChange = jest.fn();
+
+ // Spy on the useFeedback hook to return our mockQuestionsConfig
+ jest.spyOn(require("src/feedback/overallFeedback/feedbackContext/FeedbackContext"), "useFeedback").mockReturnValue({
+ questionsConfig: mockQuestionsConfig,
+ setQuestionsConfig: jest.fn(),
+ isLoading: false,
+ error: null,
+ answers: [],
+ handleAnswerChange: jest.fn(),
+ clearAnswers: jest.fn(),
+ });
+
+ test("should render correctly with empty feedbackItems", () => {
+ // GIVEN a questions config and no feedback items
+ function Wrapper({ children }: React.PropsWithChildren<{}>) {
+ return {children};
+ }
+ // WHEN the component is rendered
+ const { container } = render(
+ , { wrapper: Wrapper }
+ );
+ // THEN the question should be in the document
+ expect(screen.getByTestId(DATA_TEST_ID.CLARITY_OF_SKILLS)).toBeInTheDocument();
+ // AND it should match the snapshot
+ expect(container).toMatchSnapshot();
+ });
+
+ test("should render correctly with existing feedbackItems", () => {
+ // GIVEN a questions config and a feedback item
+ const feedbackItems = [
+ {
+ question_id: CLARITY_OF_SKILLS_QUESTION_ID,
+ simplified_answer: {
+ rating_boolean: true,
+ comment: "The skills are clear.",
+ },
+ },
+ ];
+ function Wrapper({ children }: React.PropsWithChildren<{}>) {
+ return {children};
+ }
+ // WHEN the component is rendered
+ const { container } = render(
+ , { wrapper: Wrapper }
+ );
+ // THEN the question should be in the document
+ expect(screen.getByTestId(DATA_TEST_ID.CLARITY_OF_SKILLS)).toBeInTheDocument();
+ // AND it should match the snapshot
+ expect(container).toMatchSnapshot();
+ });
+
+ test("should return null when questionsConfig is not available", () => {
+ // GIVEN the useFeedback hook returns null for questionsConfig
+ jest.spyOn(require("src/feedback/overallFeedback/feedbackContext/FeedbackContext"), "useFeedback").mockReturnValue({
+ questionsConfig: null,
+ setQuestionsConfig: jest.fn(),
+ isLoading: false,
+ error: null,
+ answers: [],
+ handleAnswerChange: jest.fn(),
+ clearAnswers: jest.fn(),
+ });
+
+ function Wrapper({ children }: React.PropsWithChildren<{}>) {
+ return {children};
+ }
+
+ // WHEN the component is rendered
+ const { container } = render(
+ , { wrapper: Wrapper }
+ );
+
+ // THEN expect the container to be empty
+ expect(container).toBeEmptyDOMElement();
+ // AND expect an error to be logged
+ expect(console.error).toHaveBeenCalledWith(expect.any(Error));
+ });
+
+ test("should return null when question ID is not in questionsConfig", () => {
+ // GIVEN the useFeedback hook returns a config without the required question ID
+ const configWithoutQuestion = { ...mockQuestionsConfig };
+ delete configWithoutQuestion[CLARITY_OF_SKILLS_QUESTION_ID];
+
+ jest.spyOn(require("src/feedback/overallFeedback/feedbackContext/FeedbackContext"), "useFeedback").mockReturnValue({
+ questionsConfig: configWithoutQuestion,
+ setQuestionsConfig: jest.fn(),
+ isLoading: false,
+ error: null,
+ answers: [],
+ handleAnswerChange: jest.fn(),
+ clearAnswers: jest.fn(),
+ });
+
+ function Wrapper({ children }: React.PropsWithChildren<{}>) {
+ return {children};
+ }
+
+ // WHEN the component is rendered
+ const { container } = render(
+ , { wrapper: Wrapper }
+ );
+
+ // THEN expect the container to be empty
+ expect(container).toBeEmptyDOMElement();
+ // AND expect an error to be logged
+ expect(console.error).toHaveBeenCalledWith(expect.any(Error));
+ });
+});
\ No newline at end of file
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/clarityOfSkills/ClarityOfSkillsQuestion.tsx b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/clarityOfSkills/ClarityOfSkillsQuestion.tsx
new file mode 100644
index 000000000..16dc5bc31
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/clarityOfSkills/ClarityOfSkillsQuestion.tsx
@@ -0,0 +1,45 @@
+import React from "react";
+import { Box } from "@mui/material";
+import { useFeedback } from "src/feedback/overallFeedback/feedbackContext/FeedbackContext";
+import QuestionRenderer
+ from "src/feedback/overallFeedback/overallFeedbackForm/components/questionsRenderer/QuestionRenderer";
+import { FeedbackItem } from "src/feedback/overallFeedback/overallFeedbackService/OverallFeedback.service.types";
+import { FeedbackError } from "src/error/commonErrors";
+import { CLARITY_OF_SKILLS_QUESTION_ID } from "./constants";
+
+;
+
+const uniqueId = "f52f579d-4cb6-4674-825d-37612e821e65";
+
+export const DATA_TEST_ID = {
+ CLARITY_OF_SKILLS: `clarity-of-skills-${uniqueId}`,
+};
+
+interface ClarityOfSkillsQuestionProps {
+ feedbackItems: FeedbackItem[];
+ onChange: (data: FeedbackItem) => void;
+}
+
+const ClarityOfSkillsQuestion: React.FC = ({ feedbackItems, onChange }) => {
+ const { questionsConfig, error } = useFeedback();
+
+ if (!questionsConfig || error) {
+ console.error(new FeedbackError("Questions configuration is not available", error));
+ return null;
+ }
+
+ const question = questionsConfig[CLARITY_OF_SKILLS_QUESTION_ID];
+
+ if (!question) {
+ console.error(new FeedbackError(`Questions configuration is not available for question: ${CLARITY_OF_SKILLS_QUESTION_ID}`, error));
+ return null;
+ }
+
+ return (
+
+
+
+ );
+};
+
+export default ClarityOfSkillsQuestion;
\ No newline at end of file
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/clarityOfSkills/__snapshots__/ClarityOfSkillsQuestion.test.tsx.snap b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/clarityOfSkills/__snapshots__/ClarityOfSkillsQuestion.test.tsx.snap
new file mode 100644
index 000000000..3807c14b3
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/clarityOfSkills/__snapshots__/ClarityOfSkillsQuestion.test.tsx.snap
@@ -0,0 +1,27 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ClarityOfSkillsQuestion should render correctly with empty feedbackItems 1`] = `
+
+
+
+
+
+`;
+
+exports[`ClarityOfSkillsQuestion should render correctly with existing feedbackItems 1`] = `
+
+
+
+
+
+`;
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/clarityOfSkills/constants.ts b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/clarityOfSkills/constants.ts
new file mode 100644
index 000000000..b0c1579a1
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/clarityOfSkills/constants.ts
@@ -0,0 +1,2 @@
+// this is in a file of its own instead of in the ClarityOfSkillsQuestion.tsx to avoid circular dependencies
+export const CLARITY_OF_SKILLS_QUESTION_ID = "clarity_of_skills";
\ No newline at end of file
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/customerSatisfactionRating/CustomerSatisfaction.stories.tsx b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/customerSatisfactionRating/CustomerSatisfaction.stories.tsx
new file mode 100644
index 000000000..64edbc0c6
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/customerSatisfactionRating/CustomerSatisfaction.stories.tsx
@@ -0,0 +1,119 @@
+import { Meta, type StoryObj } from "@storybook/react";
+import { action } from "@storybook/addon-actions";
+import CustomerSatisfactionRating from "./CustomerSatisfaction";
+import { FeedbackProvider } from "src/feedback/overallFeedback/feedbackContext/FeedbackContext";
+import { getBackendUrl } from "src/envService";
+import { QuestionType } from "src/feedback/overallFeedback/overallFeedbackForm/overallFeedbackForm.types";
+import UserPreferencesStateService from "src/userPreferences/UserPreferencesStateService";
+import AuthenticationServiceFactory from "src/auth/services/Authentication.service.factory";
+import AuthenticationStateService from "src/auth/services/AuthenticationState.service";
+import { TabiyaUser } from "src/auth/auth.types";
+import AuthenticationService from "src/auth/services/Authentication.service";
+
+// Mock authentication service to provide token validation and user info for API calls
+class MockAuthenticationService extends AuthenticationService {
+ private static instance: MockAuthenticationService;
+
+ private constructor() {
+ super();
+ }
+
+ static getInstance(): MockAuthenticationService {
+ if (!this.instance) {
+ this.instance = new this();
+ }
+ return this.instance;
+ }
+
+ async refreshToken(): Promise {}
+ cleanup(): void {}
+ async logout(): Promise {}
+ getUser(token: string): TabiyaUser | null {
+ return { id: "1", name: "Test User", email: "test@example.com" } as TabiyaUser;
+ }
+ getToken(): string {
+ return "foo token";
+ }
+ isTokenValid(token: string): { isValid: boolean; decodedToken: any; failureCause?: string } {
+ return { isValid: true, decodedToken: { sub: "1", email: "test@example.com" } };
+ }
+}
+
+// Mock questions config that matches the backend response structure
+const mockQuestionsConfig = {
+ satisfaction_with_compass: {
+ questionId: "satisfaction_with_compass",
+ question_text: "How satisfied are you with your experience?",
+ description: "Please rate your overall satisfaction",
+ type: QuestionType.Rating,
+ low_rating_label: "Not satisfied",
+ high_rating_label: "Very satisfied",
+ max_rating: 5,
+ display_rating: true,
+ comment_placeholder: "Please share your thoughts (optional)",
+ },
+ // Add other required fields with dummy values
+ perceived_bias: {} as any,
+ work_experience_accuracy: {} as any,
+ clarity_of_skills: {} as any,
+ incorrect_skills: {} as any,
+ missing_skills: {} as any,
+ interaction_ease: {} as any,
+ recommendation: {} as any,
+};
+
+const meta: Meta = {
+ title: "Feedback/OverallFeedback/CustomerSatisfactionRating",
+ component: CustomerSatisfactionRating,
+ tags: ["autodocs"],
+ args: {
+ notifyOnCustomerSatisfactionRatingSubmitted: action("notifyOnCustomerSatisfactionRatingSubmitted"),
+ },
+ parameters: {
+ mockData: [
+ // Mock the questions config endpoint that FeedbackProvider uses to fetch question data
+ {
+ url: getBackendUrl() + "/conversations/123/feedback/questions",
+ method: "GET",
+ status: 200,
+ response: mockQuestionsConfig,
+ },
+ // Mock the feedback submission endpoint that OverallFeedbackService uses to save ratings
+ {
+ url: getBackendUrl() + "/conversations/123/feedback",
+ method: "PATCH",
+ status: 200,
+ response: {
+ message: "Feedback submitted successfully",
+ },
+ },
+ ],
+ },
+ decorators: [
+ (Story) => {
+ // Mock session ID for API endpoint construction
+ const mockUserPrefsService = UserPreferencesStateService.getInstance();
+ mockUserPrefsService.getActiveSessionId = () => 123;
+
+ // Mock auth state for API authentication and user identification
+ const mockAuthStateService = AuthenticationStateService.getInstance();
+ mockAuthStateService.getToken = () => "foo token";
+ mockAuthStateService.getUser = () => ({ id: "1", name: "Test User", email: "test@example.com" } as TabiyaUser);
+
+ // Mock auth service factory to provide our mock auth service
+ AuthenticationServiceFactory.getCurrentAuthenticationService = () => MockAuthenticationService.getInstance();
+
+ return (
+
+
+
+ );
+ },
+ ],
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
\ No newline at end of file
diff --git a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/customerSatisfactionRating/CustomerSatisfaction.test.tsx b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/customerSatisfactionRating/CustomerSatisfaction.test.tsx
similarity index 73%
rename from frontend-new/src/feedback/overallFeedback/feedbackForm/components/customerSatisfactionRating/CustomerSatisfaction.test.tsx
rename to frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/customerSatisfactionRating/CustomerSatisfaction.test.tsx
index a28496cc5..171f577d8 100644
--- a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/customerSatisfactionRating/CustomerSatisfaction.test.tsx
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/customerSatisfactionRating/CustomerSatisfaction.test.tsx
@@ -1,10 +1,9 @@
// mute the console
import "src/_test_utilities/consoleMock";
-
import React from "react";
import { render, screen, act, waitFor} from "src/_test_utilities/test-utils";
import CustomerSatisfactionRating, { UI_TEXT, DATA_TEST_ID } from "./CustomerSatisfaction";
-import CustomRating, { DATA_TEST_ID as CUSTOM_RATING_DATA_TEST_ID } from "src/feedback/overallFeedback/feedbackForm/components/customRating/CustomRating";
+import CustomRating, { DATA_TEST_ID as CUSTOM_RATING_DATA_TEST_ID } from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/customRating/CustomRating";
import { DATA_TEST_ID as BACKDROP_DATA_TEST_ID } from "src/theme/Backdrop/Backdrop";
import UserPreferencesStateService from "src/userPreferences/UserPreferencesStateService";
import OverallFeedbackService from "src/feedback/overallFeedback/overallFeedbackService/OverallFeedback.service";
@@ -16,8 +15,11 @@ import {
} from "src/userPreferences/UserPreferencesService/userPreferences.types";
import { mockBrowserIsOnLine } from "src/_test_utilities/mockBrowserIsOnline";
import { resetAllMethodMocks } from "src/_test_utilities/resetAllMethodMocks";
-import { FeedbackResponse, QUESTION_KEYS } from "src/feedback/overallFeedback/overallFeedbackService/OverallFeedback.service.types";
-import { QuestionType } from "src/feedback/overallFeedback/feedbackForm/feedbackForm.types";
+import { FeedbackResponse } from "src/feedback/overallFeedback/overallFeedbackService/OverallFeedback.service.types";
+import { QuestionType } from "src/feedback/overallFeedback/overallFeedbackForm/overallFeedbackForm.types";
+import { FeedbackProvider } from "src/feedback/overallFeedback/feedbackContext/FeedbackContext";
+import { mockQuestionsConfig } from "src/feedback/overallFeedback/overallFeedbackForm/overallFeedbackForm.test.utils";
+import { CUSTOMER_SATISFACTION_QUESTION_KEY } from "./constants";
// mock the snackbar provider
jest.mock("src/theme/SnackbarProvider/SnackbarProvider", () => {
@@ -33,8 +35,8 @@ jest.mock("src/theme/SnackbarProvider/SnackbarProvider", () => {
});
// mock the custom rating component
-jest.mock("src/feedback/overallFeedback/feedbackForm/components/customRating/CustomRating", () => {
- const actual = jest.requireActual("src/feedback/overallFeedback/feedbackForm/components/customRating/CustomRating");
+jest.mock("src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/customRating/CustomRating", () => {
+ const actual = jest.requireActual("src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/customRating/CustomRating");
return {
...actual,
__esModule: true,
@@ -68,17 +70,43 @@ const mockFeedbackResponse: FeedbackResponse = {
created_at: new Date().toISOString(),
};
-describe("CustomerSatisfactionRating", () => {
+// Helper function to wrap components with FeedbackProvider
+const renderWithFeedbackProvider = (ui: React.ReactElement) => {
+ return render(
+
+ {ui}
+
+ );
+};
+describe("CustomerSatisfactionRating", () => {
beforeEach(() => {
jest.clearAllMocks();
mockBrowserIsOnLine(true);
UserPreferencesStateService.getInstance().clearUserPreferences();
// Reset all method mocks on the singletons that may have been mocked
- // As a good practice, we should the mock*Once() methods to avoid side effects between tests
- // As a precaution, we reset all method mocks to ensure that no side effects are carried over between tests
resetAllMethodMocks(OverallFeedbackService.getInstance());
+
+ // Reset all method mocks on the user preferences service
+ resetAllMethodMocks(UserPreferencesStateService.getInstance());
+
+ // Mock the useFeedback hook to return our mockQuestionsConfig
+ jest.spyOn(require("src/feedback/overallFeedback/feedbackContext/FeedbackContext"), "useFeedback").mockReturnValue({
+ questionsConfig: mockQuestionsConfig,
+ setQuestionsConfig: jest.fn(),
+ isLoading: false,
+ error: null,
+ answers: [],
+ handleAnswerChange: jest.fn(),
+ clearAnswers: jest.fn(),
+ });
+
+ // Mock the active session ID
+ jest.spyOn(UserPreferencesStateService.getInstance(), "getActiveSessionId").mockReturnValue(1);
+
+ // Mock the getQuestionsConfig to return our mock config
+ jest.spyOn(OverallFeedbackService.getInstance(), "getQuestionsConfig").mockResolvedValue(mockQuestionsConfig);
});
test("should render component successfully", () => {
@@ -88,30 +116,30 @@ describe("CustomerSatisfactionRating", () => {
);
// WHEN the component is rendered
- render(givenCustomerSatisfactionRating);
+ renderWithFeedbackProvider(givenCustomerSatisfactionRating);
// THEN the customer satisfaction rating container should be in the document
expect(screen.getByTestId(DATA_TEST_ID.CUSTOMER_SATISFACTION_RATING_CONTAINER)).toBeInTheDocument();
// AND the custom rating component should be called with the correct props
expect(CustomRating).toHaveBeenCalledWith({
- questionId: QUESTION_KEYS.CUSTOMER_SATISFACTION,
- questionText: UI_TEXT.CUSTOMER_SATISFACTION_QUESTION_TEXT,
+ question_id: CUSTOMER_SATISFACTION_QUESTION_KEY,
+ question_text: expect.stringContaining("Finally, we'd love to hear your thoughts"),
ratingValue: null,
notifyChange: expect.any(Function),
lowRatingLabel: UI_TEXT.RATING_LABEL_LOW,
highRatingLabel: UI_TEXT.RATING_LABEL_HIGH,
maxRating: 5,
disabled: false,
- type: QuestionType.Rating
+ type: QuestionType.Rating,
+ description: expect.any(String),
+ comment_placeholder: expect.any(String),
}, {});
// AND the custom rating container to be in the document
expect(screen.getByTestId(CUSTOM_RATING_DATA_TEST_ID.CUSTOM_RATING_CONTAINER)).toBeInTheDocument();
// AND expect no errors or warning to have occurred
expect(console.error).not.toHaveBeenCalled();
expect(console.warn).not.toHaveBeenCalled();
- // AND the component to match the snapshot
- expect(givenCustomerSatisfactionRating).toMatchSnapshot();
});
test("should submit rating successfully", async () => {
@@ -121,7 +149,7 @@ describe("CustomerSatisfactionRating", () => {
jest.spyOn(OverallFeedbackService.getInstance(), "sendFeedback").mockResolvedValueOnce(mockFeedbackResponse);
// AND component is rendered
const givenNotifyOnSubmitted = jest.fn();
- render();
+ renderWithFeedbackProvider();
// WHEN a rating is selected
const ratingChangeCallback = (CustomRating as jest.Mock).mock.calls.at(-1)[0].notifyChange;
@@ -153,7 +181,7 @@ describe("CustomerSatisfactionRating", () => {
jest.spyOn(OverallFeedbackService.getInstance(), "sendFeedback").mockRejectedValueOnce(givenError);
// AND the component is rendered
const givenNotifyOnSubmitted = jest.fn();
- render();
+ renderWithFeedbackProvider();
// WHEN a rating is selected
const ratingChangeCallback = (CustomRating as jest.Mock).mock.calls.at(-1)[0].notifyChange;
@@ -177,13 +205,10 @@ describe("CustomerSatisfactionRating", () => {
test("should handle missing user session", async () => {
// GIVEN the user has no valid sessions
- UserPreferencesStateService.getInstance().setUserPreferences({
- ...mockUserPreferences,
- sessions: [],
- });
+ jest.spyOn(UserPreferencesStateService.getInstance(), "getActiveSessionId").mockReturnValue(null);
// AND the component is rendered
const givenNotifyOnSubmitted = jest.fn();
- render();
+ renderWithFeedbackProvider();
// WHEN a rating is selected
const ratingChangeCallback = (CustomRating as jest.Mock).mock.calls.at(-1)[0].notifyChange;
@@ -205,7 +230,6 @@ describe("CustomerSatisfactionRating", () => {
expect(console.warn).not.toHaveBeenCalled();
});
- // test should handle offline
test("should handle offline", async () => {
// GIVEN the user has a valid session
UserPreferencesStateService.getInstance().setUserPreferences(mockUserPreferences);
@@ -216,9 +240,9 @@ describe("CustomerSatisfactionRating", () => {
// WHEN the component is rendered
jest.spyOn(OverallFeedbackService.getInstance(), "sendFeedback")
const givenNotifyOnSubmitted = jest.fn();
- render();
+ renderWithFeedbackProvider();
// THEN expect the rating to be disabled
expect((CustomRating as jest.Mock).mock.calls.at(-1)[0].disabled).toBe(true);
});
-});
\ No newline at end of file
+});
\ No newline at end of file
diff --git a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/customerSatisfactionRating/CustomerSatisfaction.tsx b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/customerSatisfactionRating/CustomerSatisfaction.tsx
similarity index 65%
rename from frontend-new/src/feedback/overallFeedback/feedbackForm/components/customerSatisfactionRating/CustomerSatisfaction.tsx
rename to frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/customerSatisfactionRating/CustomerSatisfaction.tsx
index 4e9e14e60..52a08fba3 100644
--- a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/customerSatisfactionRating/CustomerSatisfaction.tsx
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/customerSatisfactionRating/CustomerSatisfaction.tsx
@@ -1,17 +1,19 @@
import { useContext, useState } from "react";
import {
- QUESTION_KEYS,
FeedbackItem,
SimplifiedAnswer,
} from "src/feedback/overallFeedback/overallFeedbackService/OverallFeedback.service.types";
-import { QuestionType } from "src/feedback/overallFeedback/feedbackForm/feedbackForm.types";
+import { QuestionType } from "src/feedback/overallFeedback/overallFeedbackForm/overallFeedbackForm.types";
import { useSnackbar } from "src/theme/SnackbarProvider/SnackbarProvider";
import { Backdrop } from "src/theme/Backdrop/Backdrop";
-import CustomRating from "src/feedback/overallFeedback/feedbackForm/components/customRating/CustomRating";
+import CustomRating
+ from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/customRating/CustomRating";
import { IsOnlineContext } from "src/app/isOnlineProvider/IsOnlineProvider";
-import questions from "src/feedback/overallFeedback/feedbackForm/questions-en.json";
import OverallFeedbackService from "src/feedback/overallFeedback/overallFeedbackService/OverallFeedback.service";
import UserPreferencesStateService from "src/userPreferences/UserPreferencesStateService";
+import { useFeedback } from "src/feedback/overallFeedback/feedbackContext/FeedbackContext";
+import { FeedbackError } from "src/error/commonErrors";
+import { CUSTOMER_SATISFACTION_QUESTION_KEY } from "./constants";
interface CustomerSatisfactionRatingProps {
notifyOnCustomerSatisfactionRatingSubmitted: () => void;
@@ -24,19 +26,34 @@ export const DATA_TEST_ID = {
};
export const UI_TEXT = {
- CUSTOMER_SATISFACTION_QUESTION_TEXT: "Finally, we'd love to hear your thoughts on your experience so far! " + questions[QUESTION_KEYS.CUSTOMER_SATISFACTION].question_text,
+ CUSTOMER_SATISFACTION_QUESTION_TEXT_PREFIX: "Finally, we'd love to hear your thoughts on your experience so far! ",
RATING_LABEL_LOW: "Unsatisfied",
RATING_LABEL_HIGH: "Satisfied",
};
+
const CustomerSatisfactionRating: React.FC = ({
- notifyOnCustomerSatisfactionRatingSubmitted,
- }) => {
+ notifyOnCustomerSatisfactionRatingSubmitted,
+}) => {
const { enqueueSnackbar } = useSnackbar();
const isOnline = useContext(IsOnlineContext);
+ const { questionsConfig, error } = useFeedback();
const [selectedRating, setSelectedRating] = useState(null);
const [isSubmittingRating, setIsSubmittingRating] = useState(false);
+ if (!questionsConfig || error) {
+ console.error(new FeedbackError("Questions configuration is not available", error));
+ return null;
+ }
+
+ const question = questionsConfig[CUSTOMER_SATISFACTION_QUESTION_KEY];
+
+ if (!question) {
+ console.error(new FeedbackError(`Questions configuration is not available for question: ${CUSTOMER_SATISFACTION_QUESTION_KEY}`, error));
+ return null;
+ }
+ const questionText = UI_TEXT.CUSTOMER_SATISFACTION_QUESTION_TEXT_PREFIX + question.question_text;
+
const handleInputChange = async (questionId: string, value: SimplifiedAnswer) => {
const formattedData: FeedbackItem = {
question_id: questionId,
@@ -70,16 +87,18 @@ const CustomerSatisfactionRating: React.FC = ({
- handleInputChange(QUESTION_KEYS.CUSTOMER_SATISFACTION, { rating_numeric: value, comment: comments })
+ handleInputChange(CUSTOMER_SATISFACTION_QUESTION_KEY, { rating_numeric: value, comment: comments })
}
lowRatingLabel={UI_TEXT.RATING_LABEL_LOW}
highRatingLabel={UI_TEXT.RATING_LABEL_HIGH}
maxRating={5}
disabled={!isOnline || isSubmittingRating}
+ comment_placeholder={question.comment_placeholder}
/>
);
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/customerSatisfactionRating/constants.ts b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/customerSatisfactionRating/constants.ts
new file mode 100644
index 000000000..84eb39bcd
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/customerSatisfactionRating/constants.ts
@@ -0,0 +1,4 @@
+// this is in a file of its own instead of in the CustomerSatisfaction.tsx to avoid circular dependencies
+// since the question id is used in multiple places, including perhaps in the userPreferencesStateService
+// which children of this component might import...
+export const CUSTOMER_SATISFACTION_QUESTION_KEY = "satisfaction_with_compass";
\ No newline at end of file
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/incorrectSkills/IncorrectSkillsQuestion.stories.tsx b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/incorrectSkills/IncorrectSkillsQuestion.stories.tsx
new file mode 100644
index 000000000..ffd0b788f
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/incorrectSkills/IncorrectSkillsQuestion.stories.tsx
@@ -0,0 +1,138 @@
+import { Meta, StoryObj } from "@storybook/react";
+import IncorrectSkillsQuestion from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/incorrectSkills/IncorrectSkillsQuestion";
+import { action } from "@storybook/addon-actions";
+import { FeedbackProvider } from "src/feedback/overallFeedback/feedbackContext/FeedbackContext";
+import { getBackendUrl } from "src/envService";
+import { QuestionsConfig, QuestionType, YesNoEnum } from "src/feedback/overallFeedback/overallFeedbackForm/overallFeedbackForm.types";
+import UserPreferencesStateService from "src/userPreferences/UserPreferencesStateService";
+import AuthenticationServiceFactory from "src/auth/services/Authentication.service.factory";
+import AuthenticationStateService from "src/auth/services/AuthenticationState.service";
+import { TabiyaUser } from "src/auth/auth.types";
+import AuthenticationService from "src/auth/services/Authentication.service";
+
+// Mock authentication service to provide token validation and user info for API calls
+class MockAuthenticationService extends AuthenticationService {
+ private static instance: MockAuthenticationService;
+
+ private constructor() {
+ super();
+ }
+
+ static getInstance(): MockAuthenticationService {
+ if (!this.instance) {
+ this.instance = new this();
+ }
+ return this.instance;
+ }
+
+ async refreshToken(): Promise {}
+ cleanup(): void {}
+ async logout(): Promise {}
+ getUser(token: string): TabiyaUser | null {
+ return { id: "1", name: "Test User", email: "test@example.com" } as TabiyaUser;
+ }
+ getToken(): string {
+ return "foo token";
+ }
+ isTokenValid(token: string): { isValid: boolean; decodedToken: any; failureCause?: string } {
+ return { isValid: true, decodedToken: { sub: "1", email: "test@example.com" } };
+ }
+}
+
+// Mock questions config that matches the backend response structure
+const mockQuestionsConfig: QuestionsConfig = {
+ incorrect_skills: {
+ question_id: "incorrect_skills",
+ question_text: "Were there any skills incorrectly identified?",
+ description: "Please let us know about any incorrect skills",
+ type: QuestionType.YesNo,
+ show_comments_on: YesNoEnum.Yes,
+ comment_placeholder: "Please specify which skills were incorrect",
+ },
+ // Add other required fields with dummy values
+ satisfaction_with_compass: {} as any,
+ perceived_bias: {} as any,
+ work_experience_accuracy: {} as any,
+ clarity_of_skills: {} as any,
+ missing_skills: {} as any,
+ interaction_ease: {} as any,
+ recommendation: {} as any,
+ additional_feedback: {} as any,
+};
+
+const meta: Meta = {
+ title: "Feedback/OverallFeedback/QuestionComponents/IncorrectSkillsQuestion",
+ component: IncorrectSkillsQuestion,
+ tags: ["autodocs"],
+ args: {
+ feedbackItems: [],
+ onChange: (data) => {
+ action("onChange")(data);
+ },
+ },
+ parameters: {
+ mockData: [
+ // Mock the questions config endpoint that FeedbackProvider uses to fetch question data
+ {
+ url: getBackendUrl() + "/conversations/123/feedback/questions",
+ method: "GET",
+ status: 200,
+ response: mockQuestionsConfig,
+ },
+ // Mock the feedback submission endpoint that OverallFeedbackService uses to save ratings
+ {
+ url: getBackendUrl() + "/conversations/123/feedback",
+ method: "PATCH",
+ status: 200,
+ response: {
+ message: "Feedback submitted successfully",
+ },
+ },
+ ],
+ },
+ decorators: [
+ (Story) => {
+ // Mock session ID for API endpoint construction
+ const mockUserPrefsService = UserPreferencesStateService.getInstance();
+ mockUserPrefsService.getActiveSessionId = () => 123;
+
+ // Mock auth state for API authentication and user identification
+ const mockAuthStateService = AuthenticationStateService.getInstance();
+ mockAuthStateService.getToken = () => "foo token";
+ mockAuthStateService.getUser = () => ({ id: "1", name: "Test User", email: "test@example.com" } as TabiyaUser);
+
+ // Mock auth service factory to provide our mock auth service
+ AuthenticationServiceFactory.getCurrentAuthenticationService = () => MockAuthenticationService.getInstance();
+
+ return (
+
+
+
+ );
+ },
+ ],
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ feedbackItems: [],
+ },
+};
+
+export const WithExistingFeedback: Story = {
+ args: {
+ feedbackItems: [
+ {
+ question_id: "incorrect_skills",
+ simplified_answer: {
+ rating_boolean: true,
+ comment: "The system incorrectly identified me as having Python experience",
+ },
+ },
+ ],
+ },
+};
\ No newline at end of file
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/incorrectSkills/IncorrectSkillsQuestion.test.tsx b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/incorrectSkills/IncorrectSkillsQuestion.test.tsx
new file mode 100644
index 000000000..7e1a0da5a
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/incorrectSkills/IncorrectSkillsQuestion.test.tsx
@@ -0,0 +1,82 @@
+// mute the console
+import "src/_test_utilities/consoleMock";
+import React from "react";
+import { render, screen } from "src/_test_utilities/test-utils";
+import IncorrectSkillsQuestion, {
+ DATA_TEST_ID,
+
+} from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/incorrectSkills/IncorrectSkillsQuestion";
+import { FeedbackProvider } from "src/feedback/overallFeedback/feedbackContext/FeedbackContext"
+import { DATA_TEST_ID as QUESTION_RENDERER_DATA_TEST_ID } from "src/feedback/overallFeedback/overallFeedbackForm/components/questionsRenderer/QuestionRenderer";
+import { mockQuestionsConfig } from "src/feedback/overallFeedback/overallFeedbackForm/overallFeedbackForm.test.utils";
+import { INCORRECT_SKILLS_QUESTION_ID } from "./constants";
+
+jest.mock("src/feedback/overallFeedback/overallFeedbackForm/components/questionsRenderer/QuestionRenderer", () => {
+ const actual = jest.requireActual("src/feedback/overallFeedback/overallFeedbackForm/components/questionsRenderer/QuestionRenderer");
+ return {
+ ...actual,
+ __esModule: true,
+ default: jest.fn().mockImplementation(() => ),
+ };
+})
+
+describe("IncorrectSkillsQuestion", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ const mockOnChange = jest.fn();
+
+ // Spy on the useFeedback hook to return our mockQuestionsConfig
+ jest.spyOn(require("src/feedback/overallFeedback/feedbackContext/FeedbackContext"), "useFeedback").mockReturnValue({
+ questionsConfig: mockQuestionsConfig,
+ setQuestionsConfig: jest.fn(),
+ isLoading: false,
+ error: null,
+ answers: [],
+ handleAnswerChange: jest.fn(),
+ clearAnswers: jest.fn(),
+ });
+
+ test("should render correctly with empty feedbackItems", () => {
+ // GIVEN a questions config and no feedback items
+ function Wrapper({ children }: React.PropsWithChildren<{}>) {
+ return {children};
+ }
+ // WHEN the component is rendered
+ const { container } = render(
+ , { wrapper: Wrapper }
+ );
+ // THEN the question should be in the document
+ expect(screen.getByTestId(DATA_TEST_ID.INCORRECT_SKILLS)).toBeInTheDocument();
+
+ // AND it should call the QuestionRenderer with the correct props
+ expect(screen.getByTestId(QUESTION_RENDERER_DATA_TEST_ID.QUESTION_RENDERER)).toBeInTheDocument();
+ // AND it should match the snapshot
+ expect(container).toMatchSnapshot();
+ });
+
+ test("should render correctly with existing feedbackItems", () => {
+ // GIVEN a questions config and a feedback item
+ const feedbackItems = [
+ {
+ question_id: INCORRECT_SKILLS_QUESTION_ID,
+ simplified_answer: {
+ rating_boolean: true,
+ comment: "There are some incorrect skills.",
+ },
+ },
+ ];
+ function Wrapper({ children }: React.PropsWithChildren<{}>) {
+ return {children};
+ }
+ // WHEN the component is rendered
+ const { container } = render(
+ , { wrapper: Wrapper }
+ );
+ // THEN the question should be in the document
+ expect(screen.getByTestId(DATA_TEST_ID.INCORRECT_SKILLS)).toBeInTheDocument();
+ // AND it should match the snapshot
+ expect(container).toMatchSnapshot();
+ });
+});
\ No newline at end of file
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/incorrectSkills/IncorrectSkillsQuestion.tsx b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/incorrectSkills/IncorrectSkillsQuestion.tsx
new file mode 100644
index 000000000..763559db1
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/incorrectSkills/IncorrectSkillsQuestion.tsx
@@ -0,0 +1,43 @@
+import React from "react";
+import { Box } from "@mui/material";
+import { useFeedback } from "src/feedback/overallFeedback/feedbackContext/FeedbackContext";
+import QuestionRenderer
+ from "src/feedback/overallFeedback/overallFeedbackForm/components/questionsRenderer/QuestionRenderer";
+import { FeedbackItem } from "src/feedback/overallFeedback/overallFeedbackService/OverallFeedback.service.types";
+import { FeedbackError } from "src/error/commonErrors";
+import { INCORRECT_SKILLS_QUESTION_ID } from "./constants";
+
+const uniqueId = "dcf60100-613f-4b8e-84a1-42b7ba8b806c";
+
+export const DATA_TEST_ID = {
+ INCORRECT_SKILLS: `incorrect-skills-${uniqueId}`,
+};
+
+interface IncorrectSkillsQuestionProps {
+ feedbackItems: FeedbackItem[];
+ onChange: (data: FeedbackItem) => void;
+}
+
+const IncorrectSkillsQuestion: React.FC = ({ feedbackItems, onChange }) => {
+ const { questionsConfig, error } = useFeedback();
+
+ if (!questionsConfig) {
+ console.error(new FeedbackError("Questions configuration is not available", error));
+ return null;
+ }
+
+ const question = questionsConfig[INCORRECT_SKILLS_QUESTION_ID];
+
+ if (!question) {
+ console.error(new FeedbackError(`Questions configuration is not available for question: ${INCORRECT_SKILLS_QUESTION_ID}`, error));
+ return null;
+ }
+
+ return (
+
+
+
+ );
+};
+
+export default IncorrectSkillsQuestion;
\ No newline at end of file
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/incorrectSkills/__snapshots__/IncorrectSkillsQuestion.test.tsx.snap b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/incorrectSkills/__snapshots__/IncorrectSkillsQuestion.test.tsx.snap
new file mode 100644
index 000000000..29c5bbe5a
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/incorrectSkills/__snapshots__/IncorrectSkillsQuestion.test.tsx.snap
@@ -0,0 +1,27 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`IncorrectSkillsQuestion should render correctly with empty feedbackItems 1`] = `
+
+
+
+
+
+`;
+
+exports[`IncorrectSkillsQuestion should render correctly with existing feedbackItems 1`] = `
+
+
+
+
+
+`;
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/incorrectSkills/constants.ts b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/incorrectSkills/constants.ts
new file mode 100644
index 000000000..1bb2d34f2
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/incorrectSkills/constants.ts
@@ -0,0 +1,2 @@
+// this is in a file of its own instead of in the IncorrectSkillsQuestion.tsx to avoid circular dependencies
+export const INCORRECT_SKILLS_QUESTION_ID = "incorrect_skills";
\ No newline at end of file
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/interactionEase/InteractionEaseQuestion.stories.tsx b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/interactionEase/InteractionEaseQuestion.stories.tsx
new file mode 100644
index 000000000..78b2f80f6
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/interactionEase/InteractionEaseQuestion.stories.tsx
@@ -0,0 +1,141 @@
+import { Meta, StoryObj } from "@storybook/react";
+import InteractionEaseQuestion from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/interactionEase/InteractionEaseQuestion";
+import { action } from "@storybook/addon-actions";
+import { FeedbackProvider } from "src/feedback/overallFeedback/feedbackContext/FeedbackContext";
+import { getBackendUrl } from "src/envService";
+import { QuestionsConfig, QuestionType } from "src/feedback/overallFeedback/overallFeedbackForm/overallFeedbackForm.types";
+import UserPreferencesStateService from "src/userPreferences/UserPreferencesStateService";
+import AuthenticationServiceFactory from "src/auth/services/Authentication.service.factory";
+import AuthenticationStateService from "src/auth/services/AuthenticationState.service";
+import { TabiyaUser } from "src/auth/auth.types";
+import AuthenticationService from "src/auth/services/Authentication.service";
+
+// Mock authentication service to provide token validation and user info for API calls
+class MockAuthenticationService extends AuthenticationService {
+ private static instance: MockAuthenticationService;
+
+ private constructor() {
+ super();
+ }
+
+ static getInstance(): MockAuthenticationService {
+ if (!this.instance) {
+ this.instance = new this();
+ }
+ return this.instance;
+ }
+
+ async refreshToken(): Promise {}
+ cleanup(): void {}
+ async logout(): Promise {}
+ getUser(token: string): TabiyaUser | null {
+ return { id: "1", name: "Test User", email: "test@example.com" } as TabiyaUser;
+ }
+ getToken(): string {
+ return "foo token";
+ }
+ isTokenValid(token: string): { isValid: boolean; decodedToken: any; failureCause?: string } {
+ return { isValid: true, decodedToken: { sub: "1", email: "test@example.com" } };
+ }
+}
+
+// Mock questions config that matches the backend response structure
+const mockQuestionsConfig: QuestionsConfig = {
+ interaction_ease: {
+ question_id: "interaction_ease",
+ question_text: "How easy was it to interact with the system?",
+ description: "Please rate the ease of interaction",
+ type: QuestionType.Rating,
+ low_rating_label: "Not easy",
+ high_rating_label: "Very easy",
+ max_rating: 5,
+ display_rating: true,
+ comment_placeholder: "Please share your thoughts",
+ },
+ // Add other required fields with dummy values
+ satisfaction_with_compass: {} as any,
+ perceived_bias: {} as any,
+ work_experience_accuracy: {} as any,
+ clarity_of_skills: {} as any,
+ incorrect_skills: {} as any,
+ missing_skills: {} as any,
+ recommendation: {} as any,
+ additional_feedback: {} as any,
+};
+
+const meta: Meta = {
+ title: "Feedback/OverallFeedback/QuestionComponents/InteractionEaseQuestion",
+ component: InteractionEaseQuestion,
+ tags: ["autodocs"],
+ args: {
+ feedbackItems: [],
+ onChange: (data) => {
+ action("onChange")(data);
+ },
+ },
+ parameters: {
+ mockData: [
+ // Mock the questions config endpoint that FeedbackProvider uses to fetch question data
+ {
+ url: getBackendUrl() + "/conversations/123/feedback/questions",
+ method: "GET",
+ status: 200,
+ response: mockQuestionsConfig,
+ },
+ // Mock the feedback submission endpoint that OverallFeedbackService uses to save ratings
+ {
+ url: getBackendUrl() + "/conversations/123/feedback",
+ method: "PATCH",
+ status: 200,
+ response: {
+ message: "Feedback submitted successfully",
+ },
+ },
+ ],
+ },
+ decorators: [
+ (Story) => {
+ // Mock session ID for API endpoint construction
+ const mockUserPrefsService = UserPreferencesStateService.getInstance();
+ mockUserPrefsService.getActiveSessionId = () => 123;
+
+ // Mock auth state for API authentication and user identification
+ const mockAuthStateService = AuthenticationStateService.getInstance();
+ mockAuthStateService.getToken = () => "foo token";
+ mockAuthStateService.getUser = () => ({ id: "1", name: "Test User", email: "test@example.com" } as TabiyaUser);
+
+ // Mock auth service factory to provide our mock auth service
+ AuthenticationServiceFactory.getCurrentAuthenticationService = () => MockAuthenticationService.getInstance();
+
+ return (
+
+
+
+ );
+ },
+ ],
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ feedbackItems: [],
+ },
+};
+
+export const WithExistingFeedback: Story = {
+ args: {
+ feedbackItems: [
+ {
+ question_id: "interaction_ease",
+ simplified_answer: {
+ rating_numeric: 4,
+ comment: "The interface was very intuitive and easy to use",
+ },
+ },
+ ],
+ },
+};
\ No newline at end of file
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/interactionEase/InteractionEaseQuestion.test.tsx b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/interactionEase/InteractionEaseQuestion.test.tsx
new file mode 100644
index 000000000..84d33e028
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/interactionEase/InteractionEaseQuestion.test.tsx
@@ -0,0 +1,134 @@
+// mute the console
+import "src/_test_utilities/consoleMock";
+import React from "react";
+import { render, screen } from "src/_test_utilities/test-utils";
+import InteractionEaseQuestion, {
+ DATA_TEST_ID,
+} from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/interactionEase/InteractionEaseQuestion";
+import { FeedbackProvider } from "src/feedback/overallFeedback/feedbackContext/FeedbackContext";
+import { mockQuestionsConfig } from "src/feedback/overallFeedback/overallFeedbackForm/overallFeedbackForm.test.utils";
+import { INTERACTION_EASE_QUESTION_ID } from "./constants";
+
+jest.mock("src/feedback/overallFeedback/overallFeedbackForm/components/questionsRenderer/QuestionRenderer", () => {
+ const actual = jest.requireActual("src/feedback/overallFeedback/overallFeedbackForm/components/questionsRenderer/QuestionRenderer");
+ return {
+ ...actual,
+ __esModule: true,
+ default: jest.fn().mockImplementation(() => ),
+ };
+})
+
+describe("InteractionEaseQuestion", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ const mockOnChange = jest.fn();
+
+ // Spy on the useFeedback hook to return our mockQuestionsConfig
+ jest.spyOn(require("src/feedback/overallFeedback/feedbackContext/FeedbackContext"), "useFeedback").mockReturnValue({
+ questionsConfig: mockQuestionsConfig,
+ setQuestionsConfig: jest.fn(),
+ isLoading: false,
+ error: null,
+ answers: [],
+ handleAnswerChange: jest.fn(),
+ clearAnswers: jest.fn(),
+ });
+
+ test("should render correctly with empty feedbackItems", () => {
+ // GIVEN a questions config and no feedback items
+ function Wrapper({ children }: React.PropsWithChildren<{}>) {
+ return {children};
+ }
+ // WHEN the component is rendered
+ const { container } = render(
+ , { wrapper: Wrapper }
+ );
+ // THEN the question should be in the document
+ expect(screen.getByTestId(DATA_TEST_ID.INTERACTION_EASE)).toBeInTheDocument();
+ // AND it should match the snapshot
+ expect(container).toMatchSnapshot();
+ });
+
+ test("should render correctly with existing feedbackItems", () => {
+ // GIVEN a questions config and a feedback item
+ const feedbackItems = [
+ {
+ question_id: INTERACTION_EASE_QUESTION_ID,
+ simplified_answer: {
+ rating_numeric: 4,
+ comment: "The interaction was easy.",
+ },
+ },
+ ];
+ function Wrapper({ children }: React.PropsWithChildren<{}>) {
+ return {children};
+ }
+ // WHEN the component is rendered
+ const { container } = render(
+ , { wrapper: Wrapper }
+ );
+ // THEN the question should be in the document
+ expect(screen.getByTestId(DATA_TEST_ID.INTERACTION_EASE)).toBeInTheDocument();
+ // AND it should match the snapshot
+ expect(container).toMatchSnapshot();
+ });
+
+ test("should return null when questionsConfig is not available", () => {
+ // GIVEN the useFeedback hook returns null for questionsConfig
+ jest.spyOn(require("src/feedback/overallFeedback/feedbackContext/FeedbackContext"), "useFeedback").mockReturnValue({
+ questionsConfig: null,
+ setQuestionsConfig: jest.fn(),
+ isLoading: false,
+ error: null,
+ answers: [],
+ handleAnswerChange: jest.fn(),
+ clearAnswers: jest.fn(),
+ });
+
+ function Wrapper({ children }: React.PropsWithChildren<{}>) {
+ return {children};
+ }
+
+ // WHEN the component is rendered
+ const { container } = render(
+ , { wrapper: Wrapper }
+ );
+
+ // THEN expect the container to be empty
+ expect(container).toBeEmptyDOMElement();
+ // AND expect an error to be logged
+ expect(console.error).toHaveBeenCalledWith(expect.any(Error));
+ });
+
+ test("should return null when question ID is not in questionsConfig", () => {
+ // GIVEN the useFeedback hook returns a config without the required question ID
+ const configWithoutQuestion = { ...mockQuestionsConfig };
+ delete configWithoutQuestion[INTERACTION_EASE_QUESTION_ID];
+
+ jest.spyOn(require("src/feedback/overallFeedback/feedbackContext/FeedbackContext"), "useFeedback").mockReturnValue({
+ questionsConfig: configWithoutQuestion,
+ setQuestionsConfig: jest.fn(),
+ isLoading: false,
+ error: null,
+ answers: [],
+ handleAnswerChange: jest.fn(),
+ clearAnswers: jest.fn(),
+ });
+
+ function Wrapper({ children }: React.PropsWithChildren<{}>) {
+ return {children};
+ }
+
+ // WHEN the component is rendered
+ const { container } = render(
+ , { wrapper: Wrapper }
+ );
+
+ // THEN expect the container to be empty
+ expect(container).toBeEmptyDOMElement();
+ // AND expect an error to be logged
+ expect(console.error).toHaveBeenCalledWith(expect.any(Error));
+ });
+});
\ No newline at end of file
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/interactionEase/InteractionEaseQuestion.tsx b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/interactionEase/InteractionEaseQuestion.tsx
new file mode 100644
index 000000000..114d39270
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/interactionEase/InteractionEaseQuestion.tsx
@@ -0,0 +1,43 @@
+import React from "react";
+import { Box } from "@mui/material";
+import { useFeedback } from "src/feedback/overallFeedback/feedbackContext/FeedbackContext";
+import QuestionRenderer
+ from "src/feedback/overallFeedback/overallFeedbackForm/components/questionsRenderer/QuestionRenderer";
+import { FeedbackItem } from "src/feedback/overallFeedback/overallFeedbackService/OverallFeedback.service.types";
+import { FeedbackError } from "src/error/commonErrors";
+import { INTERACTION_EASE_QUESTION_ID } from "./constants";
+
+const uniqueId = "de1e4fe0-25ea-49bc-ad32-8d1d3af27161";
+
+export const DATA_TEST_ID = {
+ INTERACTION_EASE: `interaction-ease-${uniqueId}`,
+};
+
+interface InteractionEaseQuestionProps {
+ feedbackItems: FeedbackItem[];
+ onChange: (data: FeedbackItem) => void;
+}
+
+const InteractionEaseQuestion: React.FC = ({ feedbackItems, onChange }) => {
+ const { questionsConfig, error } = useFeedback();
+
+ if (!questionsConfig) {
+ console.error(new FeedbackError("Questions configuration is not available", error));
+ return null;
+ }
+
+ const question = questionsConfig[INTERACTION_EASE_QUESTION_ID];
+
+ if (!question) {
+ console.error(new FeedbackError(`Questions configuration is not available for question: ${INTERACTION_EASE_QUESTION_ID}`, error));
+ return null;
+ }
+
+ return (
+
+
+
+ );
+};
+
+export default InteractionEaseQuestion;
\ No newline at end of file
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/interactionEase/__snapshots__/InteractionEaseQuestion.test.tsx.snap b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/interactionEase/__snapshots__/InteractionEaseQuestion.test.tsx.snap
new file mode 100644
index 000000000..50af10577
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/interactionEase/__snapshots__/InteractionEaseQuestion.test.tsx.snap
@@ -0,0 +1,27 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`InteractionEaseQuestion should render correctly with empty feedbackItems 1`] = `
+
+
+
+
+
+`;
+
+exports[`InteractionEaseQuestion should render correctly with existing feedbackItems 1`] = `
+
+
+
+
+
+`;
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/interactionEase/constants.ts b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/interactionEase/constants.ts
new file mode 100644
index 000000000..7ab6280aa
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/interactionEase/constants.ts
@@ -0,0 +1,2 @@
+// this is in a file of its own instead of in the InteractionEaseQuestion.tsx to avoid circular dependencies
+export const INTERACTION_EASE_QUESTION_ID = "interaction_ease";
\ No newline at end of file
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/missingSkills/MissingSkillsQuestion.stories.tsx b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/missingSkills/MissingSkillsQuestion.stories.tsx
new file mode 100644
index 000000000..9ddb118a0
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/missingSkills/MissingSkillsQuestion.stories.tsx
@@ -0,0 +1,139 @@
+import { Meta, StoryObj } from "@storybook/react";
+import MissingSkillsQuestion from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/missingSkills/MissingSkillsQuestion";
+import { action } from "@storybook/addon-actions";
+import { FeedbackProvider } from "src/feedback/overallFeedback/feedbackContext/FeedbackContext";
+import { getBackendUrl } from "src/envService";
+import { QuestionsConfig, QuestionType } from "src/feedback/overallFeedback/overallFeedbackForm/overallFeedbackForm.types";
+import UserPreferencesStateService from "src/userPreferences/UserPreferencesStateService";
+import AuthenticationServiceFactory from "src/auth/services/Authentication.service.factory";
+import AuthenticationStateService from "src/auth/services/AuthenticationState.service";
+import { TabiyaUser } from "src/auth/auth.types";
+import AuthenticationService from "src/auth/services/Authentication.service";
+import { YesNoEnum } from "src/feedback/overallFeedback/overallFeedbackForm/overallFeedbackForm.types";
+
+// Mock authentication service to provide token validation and user info for API calls
+class MockAuthenticationService extends AuthenticationService {
+ private static instance: MockAuthenticationService;
+
+ private constructor() {
+ super();
+ }
+
+ static getInstance(): MockAuthenticationService {
+ if (!this.instance) {
+ this.instance = new this();
+ }
+ return this.instance;
+ }
+
+ async refreshToken(): Promise {}
+ cleanup(): void {}
+ async logout(): Promise {}
+ getUser(token: string): TabiyaUser | null {
+ return { id: "1", name: "Test User", email: "test@example.com" } as TabiyaUser;
+ }
+ getToken(): string {
+ return "foo token";
+ }
+ isTokenValid(token: string): { isValid: boolean; decodedToken: any; failureCause?: string } {
+ return { isValid: true, decodedToken: { sub: "1", email: "test@example.com" } };
+ }
+}
+
+// Mock questions config that matches the backend response structure
+const mockQuestionsConfig: QuestionsConfig = {
+ missing_skills: {
+ question_id: "missing_skills",
+ question_text: "Were there any skills missing from the assessment?",
+ description: "Please let us know if any important skills were not included",
+ type: QuestionType.YesNo,
+ show_comments_on: YesNoEnum.Yes,
+ comment_placeholder: "Please specify which skills were missing",
+ },
+ // Add other required fields with dummy values
+ satisfaction_with_compass: {} as any,
+ perceived_bias: {} as any,
+ work_experience_accuracy: {} as any,
+ clarity_of_skills: {} as any,
+ incorrect_skills: {} as any,
+ interaction_ease: {} as any,
+ recommendation: {} as any,
+ additional_feedback: {} as any,
+};
+
+const meta: Meta = {
+ title: "Feedback/OverallFeedback/QuestionComponents/MissingSkillsQuestion",
+ component: MissingSkillsQuestion,
+ tags: ["autodocs"],
+ args: {
+ feedbackItems: [],
+ onChange: (data) => {
+ action("onChange")(data);
+ },
+ },
+ parameters: {
+ mockData: [
+ // Mock the questions config endpoint that FeedbackProvider uses to fetch question data
+ {
+ url: getBackendUrl() + "/conversations/123/feedback/questions",
+ method: "GET",
+ status: 200,
+ response: mockQuestionsConfig,
+ },
+ // Mock the feedback submission endpoint that OverallFeedbackService uses to save ratings
+ {
+ url: getBackendUrl() + "/conversations/123/feedback",
+ method: "PATCH",
+ status: 200,
+ response: {
+ message: "Feedback submitted successfully",
+ },
+ },
+ ],
+ },
+ decorators: [
+ (Story) => {
+ // Mock session ID for API endpoint construction
+ const mockUserPrefsService = UserPreferencesStateService.getInstance();
+ mockUserPrefsService.getActiveSessionId = () => 123;
+
+ // Mock auth state for API authentication and user identification
+ const mockAuthStateService = AuthenticationStateService.getInstance();
+ mockAuthStateService.getToken = () => "foo token";
+ mockAuthStateService.getUser = () => ({ id: "1", name: "Test User", email: "test@example.com" } as TabiyaUser);
+
+ // Mock auth service factory to provide our mock auth service
+ AuthenticationServiceFactory.getCurrentAuthenticationService = () => MockAuthenticationService.getInstance();
+
+ return (
+
+
+
+ );
+ },
+ ],
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ feedbackItems: [],
+ },
+};
+
+export const WithExistingFeedback: Story = {
+ args: {
+ feedbackItems: [
+ {
+ question_id: "missing_skills",
+ simplified_answer: {
+ rating_boolean: true,
+ comment: "Project management and leadership skills were not assessed",
+ },
+ },
+ ],
+ },
+};
\ No newline at end of file
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/missingSkills/MissingSkillsQuestion.test.tsx b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/missingSkills/MissingSkillsQuestion.test.tsx
new file mode 100644
index 000000000..47c8e697c
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/missingSkills/MissingSkillsQuestion.test.tsx
@@ -0,0 +1,134 @@
+// mute the console
+import "src/_test_utilities/consoleMock";
+import React from "react";
+import { render, screen } from "src/_test_utilities/test-utils";
+import MissingSkillsQuestion, {
+ DATA_TEST_ID,
+} from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/missingSkills/MissingSkillsQuestion";
+import { FeedbackProvider } from "src/feedback/overallFeedback/feedbackContext/FeedbackContext";
+import { mockQuestionsConfig } from "src/feedback/overallFeedback/overallFeedbackForm/overallFeedbackForm.test.utils";
+import { MISSING_SKILLS_QUESTION_ID } from "./constants";
+
+jest.mock("src/feedback/overallFeedback/overallFeedbackForm/components/questionsRenderer/QuestionRenderer", () => {
+ const actual = jest.requireActual("src/feedback/overallFeedback/overallFeedbackForm/components/questionsRenderer/QuestionRenderer");
+ return {
+ ...actual,
+ __esModule: true,
+ default: jest.fn().mockImplementation(() => ),
+ };
+})
+
+describe("MissingSkillsQuestion", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ const mockOnChange = jest.fn();
+
+ // Spy on the useFeedback hook to return our mockQuestionsConfig
+ jest.spyOn(require("src/feedback/overallFeedback/feedbackContext/FeedbackContext"), "useFeedback").mockReturnValue({
+ questionsConfig: mockQuestionsConfig,
+ setQuestionsConfig: jest.fn(),
+ isLoading: false,
+ error: null,
+ answers: [],
+ handleAnswerChange: jest.fn(),
+ clearAnswers: jest.fn(),
+ });
+
+ test("should render correctly with empty feedbackItems", () => {
+ // GIVEN a questions config and no feedback items
+ function Wrapper({ children }: React.PropsWithChildren<{}>) {
+ return {children};
+ }
+ // WHEN the component is rendered
+ const { container } = render(
+ , { wrapper: Wrapper }
+ );
+ // THEN the question should be in the document
+ expect(screen.getByTestId(DATA_TEST_ID.MISSING_SKILLS)).toBeInTheDocument();
+ // AND it should match the snapshot
+ expect(container).toMatchSnapshot();
+ });
+
+ test("should render correctly with existing feedbackItems", () => {
+ // GIVEN a questions config and a feedback item
+ const feedbackItems = [
+ {
+ question_id: MISSING_SKILLS_QUESTION_ID,
+ simplified_answer: {
+ rating_boolean: true,
+ comment: "Some skills were missing.",
+ },
+ },
+ ];
+ function Wrapper({ children }: React.PropsWithChildren<{}>) {
+ return {children};
+ }
+ // WHEN the component is rendered
+ const { container } = render(
+ , { wrapper: Wrapper }
+ );
+ // THEN the question should be in the document
+ expect(screen.getByTestId(DATA_TEST_ID.MISSING_SKILLS)).toBeInTheDocument();
+ // AND it should match the snapshot
+ expect(container).toMatchSnapshot();
+ });
+
+ test("should return null when questionsConfig is not available", () => {
+ // GIVEN the useFeedback hook returns null for questionsConfig
+ jest.spyOn(require("src/feedback/overallFeedback/feedbackContext/FeedbackContext"), "useFeedback").mockReturnValue({
+ questionsConfig: null,
+ setQuestionsConfig: jest.fn(),
+ isLoading: false,
+ error: null,
+ answers: [],
+ handleAnswerChange: jest.fn(),
+ clearAnswers: jest.fn(),
+ });
+
+ function Wrapper({ children }: React.PropsWithChildren<{}>) {
+ return {children};
+ }
+
+ // WHEN the component is rendered
+ const { container } = render(
+ , { wrapper: Wrapper }
+ );
+
+ // THEN expect the container to be empty
+ expect(container).toBeEmptyDOMElement();
+ // AND expect an error to be logged
+ expect(console.error).toHaveBeenCalledWith(expect.any(Error));
+ });
+
+ test("should return null when question ID is not in questionsConfig", () => {
+ // GIVEN the useFeedback hook returns a config without the required question ID
+ const configWithoutQuestion = { ...mockQuestionsConfig };
+ delete configWithoutQuestion[MISSING_SKILLS_QUESTION_ID];
+
+ jest.spyOn(require("src/feedback/overallFeedback/feedbackContext/FeedbackContext"), "useFeedback").mockReturnValue({
+ questionsConfig: configWithoutQuestion,
+ setQuestionsConfig: jest.fn(),
+ isLoading: false,
+ error: null,
+ answers: [],
+ handleAnswerChange: jest.fn(),
+ clearAnswers: jest.fn(),
+ });
+
+ function Wrapper({ children }: React.PropsWithChildren<{}>) {
+ return {children};
+ }
+
+ // WHEN the component is rendered
+ const { container } = render(
+ , { wrapper: Wrapper }
+ );
+
+ // THEN expect the container to be empty
+ expect(container).toBeEmptyDOMElement();
+ // AND expect an error to be logged
+ expect(console.error).toHaveBeenCalledWith(expect.any(Error));
+ });
+});
\ No newline at end of file
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/missingSkills/MissingSkillsQuestion.tsx b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/missingSkills/MissingSkillsQuestion.tsx
new file mode 100644
index 000000000..82a74175e
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/missingSkills/MissingSkillsQuestion.tsx
@@ -0,0 +1,43 @@
+import React from "react";
+import { Box } from "@mui/material";
+import { useFeedback } from "src/feedback/overallFeedback/feedbackContext/FeedbackContext";
+import QuestionRenderer
+ from "src/feedback/overallFeedback/overallFeedbackForm/components/questionsRenderer/QuestionRenderer";
+import { FeedbackItem } from "src/feedback/overallFeedback/overallFeedbackService/OverallFeedback.service.types";
+import { FeedbackError } from "src/error/commonErrors";
+import { MISSING_SKILLS_QUESTION_ID } from "./constants";
+
+const uniqueId = "95ccb7e6-350e-4fd1-81d0-96c5b6094ab4";
+
+export const DATA_TEST_ID = {
+ MISSING_SKILLS: `missing-skills-${uniqueId}`,
+};
+
+interface MissingSkillsQuestionProps {
+ feedbackItems: FeedbackItem[];
+ onChange: (data: FeedbackItem) => void;
+}
+
+const MissingSkillsQuestion: React.FC = ({ feedbackItems, onChange }) => {
+ const { questionsConfig,error } = useFeedback();
+
+ if (!questionsConfig) {
+ console.error(new FeedbackError("Questions configuration is not available", error));
+ return null;
+ }
+
+ const question = questionsConfig[MISSING_SKILLS_QUESTION_ID];
+
+ if (!question) {
+ console.error(new FeedbackError(`Questions configuration is not available for question: ${MISSING_SKILLS_QUESTION_ID}`, error));
+ return null;
+ }
+
+ return (
+
+
+
+ );
+};
+
+export default MissingSkillsQuestion;
\ No newline at end of file
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/missingSkills/__snapshots__/MissingSkillsQuestion.test.tsx.snap b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/missingSkills/__snapshots__/MissingSkillsQuestion.test.tsx.snap
new file mode 100644
index 000000000..fee9af047
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/missingSkills/__snapshots__/MissingSkillsQuestion.test.tsx.snap
@@ -0,0 +1,27 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`MissingSkillsQuestion should render correctly with empty feedbackItems 1`] = `
+
+
+
+
+
+`;
+
+exports[`MissingSkillsQuestion should render correctly with existing feedbackItems 1`] = `
+
+
+
+
+
+`;
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/missingSkills/constants.ts b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/missingSkills/constants.ts
new file mode 100644
index 000000000..0db2a9a7a
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/missingSkills/constants.ts
@@ -0,0 +1,2 @@
+// this is in a file of its own instead of in the MissingSkillsQuestion.tsx to avoid circular dependencies
+export const MISSING_SKILLS_QUESTION_ID = "missing_skills";
\ No newline at end of file
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/perceivedBias/PerceivedBiasQuestion.stories.tsx b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/perceivedBias/PerceivedBiasQuestion.stories.tsx
new file mode 100644
index 000000000..91c11993b
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/perceivedBias/PerceivedBiasQuestion.stories.tsx
@@ -0,0 +1,139 @@
+import { Meta, StoryObj } from "@storybook/react";
+import PerceivedBiasQuestion from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/perceivedBias/PerceivedBiasQuestion";
+import { action } from "@storybook/addon-actions";
+import { FeedbackProvider } from "src/feedback/overallFeedback/feedbackContext/FeedbackContext";
+import { getBackendUrl } from "src/envService";
+import { QuestionsConfig, QuestionType } from "src/feedback/overallFeedback/overallFeedbackForm/overallFeedbackForm.types";
+import UserPreferencesStateService from "src/userPreferences/UserPreferencesStateService";
+import AuthenticationServiceFactory from "src/auth/services/Authentication.service.factory";
+import AuthenticationStateService from "src/auth/services/AuthenticationState.service";
+import { TabiyaUser } from "src/auth/auth.types";
+import AuthenticationService from "src/auth/services/Authentication.service";
+import { YesNoEnum } from "src/feedback/overallFeedback/overallFeedbackForm/overallFeedbackForm.types";
+
+// Mock authentication service to provide token validation and user info for API calls
+class MockAuthenticationService extends AuthenticationService {
+ private static instance: MockAuthenticationService;
+
+ private constructor() {
+ super();
+ }
+
+ static getInstance(): MockAuthenticationService {
+ if (!this.instance) {
+ this.instance = new this();
+ }
+ return this.instance;
+ }
+
+ async refreshToken(): Promise {}
+ cleanup(): void {}
+ async logout(): Promise {}
+ getUser(token: string): TabiyaUser | null {
+ return { id: "1", name: "Test User", email: "test@example.com" } as TabiyaUser;
+ }
+ getToken(): string {
+ return "foo token";
+ }
+ isTokenValid(token: string): { isValid: boolean; decodedToken: any; failureCause?: string } {
+ return { isValid: true, decodedToken: { sub: "1", email: "test@example.com" } };
+ }
+}
+
+// Mock questions config that matches the backend response structure
+const mockQuestionsConfig: QuestionsConfig = {
+ perceived_bias: {
+ question_id: "perceived_bias",
+ question_text: "Did you notice any bias in the skills assessment?",
+ description: "Please let us know if you observed any bias in the assessment process",
+ type: QuestionType.YesNo,
+ show_comments_on: YesNoEnum.Yes,
+ comment_placeholder: "Please describe any bias you noticed",
+ },
+ // Add other required fields with dummy values
+ satisfaction_with_compass: {} as any,
+ work_experience_accuracy: {} as any,
+ clarity_of_skills: {} as any,
+ incorrect_skills: {} as any,
+ missing_skills: {} as any,
+ interaction_ease: {} as any,
+ recommendation: {} as any,
+ additional_feedback: {} as any,
+};
+
+const meta: Meta = {
+ title: "Feedback/OverallFeedback/QuestionComponents/PerceivedBiasQuestion",
+ component: PerceivedBiasQuestion,
+ tags: ["autodocs"],
+ args: {
+ feedbackItems: [],
+ onChange: (data) => {
+ action("onChange")(data);
+ },
+ },
+ parameters: {
+ mockData: [
+ // Mock the questions config endpoint that FeedbackProvider uses to fetch question data
+ {
+ url: getBackendUrl() + "/conversations/123/feedback/questions",
+ method: "GET",
+ status: 200,
+ response: mockQuestionsConfig,
+ },
+ // Mock the feedback submission endpoint that OverallFeedbackService uses to save ratings
+ {
+ url: getBackendUrl() + "/conversations/123/feedback",
+ method: "PATCH",
+ status: 200,
+ response: {
+ message: "Feedback submitted successfully",
+ },
+ },
+ ],
+ },
+ decorators: [
+ (Story) => {
+ // Mock session ID for API endpoint construction
+ const mockUserPrefsService = UserPreferencesStateService.getInstance();
+ mockUserPrefsService.getActiveSessionId = () => 123;
+
+ // Mock auth state for API authentication and user identification
+ const mockAuthStateService = AuthenticationStateService.getInstance();
+ mockAuthStateService.getToken = () => "foo token";
+ mockAuthStateService.getUser = () => ({ id: "1", name: "Test User", email: "test@example.com" } as TabiyaUser);
+
+ // Mock auth service factory to provide our mock auth service
+ AuthenticationServiceFactory.getCurrentAuthenticationService = () => MockAuthenticationService.getInstance();
+
+ return (
+
+
+
+ );
+ },
+ ],
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ feedbackItems: [],
+ },
+};
+
+export const WithExistingFeedback: Story = {
+ args: {
+ feedbackItems: [
+ {
+ question_id: "perceived_bias",
+ simplified_answer: {
+ rating_boolean: true,
+ comment: "The assessment seemed to favor certain industries over others",
+ },
+ },
+ ],
+ },
+};
\ No newline at end of file
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/perceivedBias/PerceivedBiasQuestion.test.tsx b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/perceivedBias/PerceivedBiasQuestion.test.tsx
new file mode 100644
index 000000000..256a10d11
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/perceivedBias/PerceivedBiasQuestion.test.tsx
@@ -0,0 +1,134 @@
+// mute the console
+import "src/_test_utilities/consoleMock";
+import React from "react";
+import { render, screen } from "src/_test_utilities/test-utils";
+import PerceivedBiasQuestion, {
+ DATA_TEST_ID,
+} from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/perceivedBias/PerceivedBiasQuestion";
+import { FeedbackProvider } from "src/feedback/overallFeedback/feedbackContext/FeedbackContext";
+import { mockQuestionsConfig } from "src/feedback/overallFeedback/overallFeedbackForm/overallFeedbackForm.test.utils";
+import { PERCEIVED_BIAS_QUESTION_ID } from "./constants";
+
+jest.mock("src/feedback/overallFeedback/overallFeedbackForm/components/questionsRenderer/QuestionRenderer", () => {
+ const actual = jest.requireActual("src/feedback/overallFeedback/overallFeedbackForm/components/questionsRenderer/QuestionRenderer");
+ return {
+ ...actual,
+ __esModule: true,
+ default: jest.fn().mockImplementation(() => ),
+ };
+})
+
+describe("PerceivedBiasQuestion", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ const mockOnChange = jest.fn();
+
+ // Spy on the useFeedback hook to return our mockQuestionsConfig
+ jest.spyOn(require("src/feedback/overallFeedback/feedbackContext/FeedbackContext"), "useFeedback").mockReturnValue({
+ questionsConfig: mockQuestionsConfig,
+ setQuestionsConfig: jest.fn(),
+ isLoading: false,
+ error: null,
+ answers: [],
+ handleAnswerChange: jest.fn(),
+ clearAnswers: jest.fn(),
+ });
+
+ test("should render correctly with empty feedbackItems", () => {
+ // GIVEN a questions config and no feedback items
+ function Wrapper({ children }: React.PropsWithChildren<{}>) {
+ return {children};
+ }
+ // WHEN the component is rendered
+ const { container } = render(
+ , { wrapper: Wrapper }
+ );
+ // THEN the question should be in the document
+ expect(screen.getByTestId(DATA_TEST_ID.PERCEIVED_BIAS)).toBeInTheDocument();
+ // AND it should match the snapshot
+ expect(container).toMatchSnapshot();
+ });
+
+ test("should render correctly with existing feedbackItems", () => {
+ // GIVEN a questions config and a feedback item
+ const feedbackItems = [
+ {
+ question_id: PERCEIVED_BIAS_QUESTION_ID,
+ simplified_answer: {
+ rating_boolean: true,
+ comment: "I noticed some bias.",
+ },
+ },
+ ];
+ function Wrapper({ children }: React.PropsWithChildren<{}>) {
+ return {children};
+ }
+ // WHEN the component is rendered
+ const { container } = render(
+ , { wrapper: Wrapper }
+ );
+ // THEN the question should be in the document
+ expect(screen.getByTestId(DATA_TEST_ID.PERCEIVED_BIAS)).toBeInTheDocument();
+ // AND it should match the snapshot
+ expect(container).toMatchSnapshot();
+ });
+
+ test("should return null when questionsConfig is not available", () => {
+ // GIVEN the useFeedback hook returns null for questionsConfig
+ jest.spyOn(require("src/feedback/overallFeedback/feedbackContext/FeedbackContext"), "useFeedback").mockReturnValue({
+ questionsConfig: null,
+ setQuestionsConfig: jest.fn(),
+ isLoading: false,
+ error: null,
+ answers: [],
+ handleAnswerChange: jest.fn(),
+ clearAnswers: jest.fn(),
+ });
+
+ function Wrapper({ children }: React.PropsWithChildren<{}>) {
+ return {children};
+ }
+
+ // WHEN the component is rendered
+ const { container } = render(
+ , { wrapper: Wrapper }
+ );
+
+ // THEN expect the container to be empty
+ expect(container).toBeEmptyDOMElement();
+ // AND expect an error to be logged
+ expect(console.error).toHaveBeenCalledWith(expect.any(Error));
+ });
+
+ test("should return null when question ID is not in questionsConfig", () => {
+ // GIVEN the useFeedback hook returns a config without the required question ID
+ const configWithoutQuestion = { ...mockQuestionsConfig };
+ delete configWithoutQuestion[PERCEIVED_BIAS_QUESTION_ID];
+
+ jest.spyOn(require("src/feedback/overallFeedback/feedbackContext/FeedbackContext"), "useFeedback").mockReturnValue({
+ questionsConfig: configWithoutQuestion,
+ setQuestionsConfig: jest.fn(),
+ isLoading: false,
+ error: null,
+ answers: [],
+ handleAnswerChange: jest.fn(),
+ clearAnswers: jest.fn(),
+ });
+
+ function Wrapper({ children }: React.PropsWithChildren<{}>) {
+ return {children};
+ }
+
+ // WHEN the component is rendered
+ const { container } = render(
+ , { wrapper: Wrapper }
+ );
+
+ // THEN expect the container to be empty
+ expect(container).toBeEmptyDOMElement();
+ // AND expect an error to be logged
+ expect(console.error).toHaveBeenCalledWith(expect.any(Error));
+ });
+});
\ No newline at end of file
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/perceivedBias/PerceivedBiasQuestion.tsx b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/perceivedBias/PerceivedBiasQuestion.tsx
new file mode 100644
index 000000000..01d56ebe0
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/perceivedBias/PerceivedBiasQuestion.tsx
@@ -0,0 +1,43 @@
+import React from "react";
+import { Box } from "@mui/material";
+import { useFeedback } from "src/feedback/overallFeedback/feedbackContext/FeedbackContext";
+import QuestionRenderer
+ from "src/feedback/overallFeedback/overallFeedbackForm/components/questionsRenderer/QuestionRenderer";
+import { FeedbackItem } from "src/feedback/overallFeedback/overallFeedbackService/OverallFeedback.service.types";
+import { FeedbackError } from "src/error/commonErrors";
+import { PERCEIVED_BIAS_QUESTION_ID } from "./constants";
+
+const uniqueId = "f60211ff-b133-41e5-9457-bb55d8d6b350";
+
+export const DATA_TEST_ID = {
+ PERCEIVED_BIAS: `perceived-bias-${uniqueId}`,
+};
+
+interface PerceivedBiasQuestionProps {
+ feedbackItems: FeedbackItem[];
+ onChange: (data: FeedbackItem) => void;
+}
+
+const PerceivedBiasQuestion: React.FC = ({ feedbackItems, onChange }) => {
+ const { questionsConfig, error } = useFeedback();
+
+ if (!questionsConfig) {
+ console.error(new FeedbackError("Questions configuration is not available", error));
+ return null;
+ }
+
+ const question = questionsConfig[PERCEIVED_BIAS_QUESTION_ID];
+
+ if (!question) {
+ console.error(new FeedbackError(`Questions configuration is not available for question: ${PERCEIVED_BIAS_QUESTION_ID}`, error));
+ return null;
+ }
+
+ return (
+
+
+
+ );
+};
+
+export default PerceivedBiasQuestion;
\ No newline at end of file
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/perceivedBias/__snapshots__/PerceivedBiasQuestion.test.tsx.snap b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/perceivedBias/__snapshots__/PerceivedBiasQuestion.test.tsx.snap
new file mode 100644
index 000000000..08e464f29
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/perceivedBias/__snapshots__/PerceivedBiasQuestion.test.tsx.snap
@@ -0,0 +1,27 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`PerceivedBiasQuestion should render correctly with empty feedbackItems 1`] = `
+
+
+
+
+
+`;
+
+exports[`PerceivedBiasQuestion should render correctly with existing feedbackItems 1`] = `
+
+
+
+
+
+`;
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/perceivedBias/constants.ts b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/perceivedBias/constants.ts
new file mode 100644
index 000000000..a47d09d67
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/perceivedBias/constants.ts
@@ -0,0 +1,2 @@
+// this is in a file of its own instead of in the PerceivedBiasQuestion.tsx to avoid circular dependencies
+export const PERCEIVED_BIAS_QUESTION_ID = "perceived_bias";
\ No newline at end of file
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/recommendation/RecommendationQuestion.stories.tsx b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/recommendation/RecommendationQuestion.stories.tsx
new file mode 100644
index 000000000..cfc5b45f8
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/recommendation/RecommendationQuestion.stories.tsx
@@ -0,0 +1,139 @@
+import { Meta, StoryObj } from "@storybook/react";
+import RecommendationQuestion from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/recommendation/RecommendationQuestion";
+import { action } from "@storybook/addon-actions";
+import { FeedbackProvider } from "src/feedback/overallFeedback/feedbackContext/FeedbackContext";
+import { getBackendUrl } from "src/envService";
+import { QuestionsConfig, QuestionType } from "src/feedback/overallFeedback/overallFeedbackForm/overallFeedbackForm.types";
+import UserPreferencesStateService from "src/userPreferences/UserPreferencesStateService";
+import AuthenticationServiceFactory from "src/auth/services/Authentication.service.factory";
+import AuthenticationStateService from "src/auth/services/AuthenticationState.service";
+import { TabiyaUser } from "src/auth/auth.types";
+import AuthenticationService from "src/auth/services/Authentication.service";
+import { YesNoEnum } from "src/feedback/overallFeedback/overallFeedbackForm/overallFeedbackForm.types";
+
+// Mock authentication service to provide token validation and user info for API calls
+class MockAuthenticationService extends AuthenticationService {
+ private static instance: MockAuthenticationService;
+
+ private constructor() {
+ super();
+ }
+
+ static getInstance(): MockAuthenticationService {
+ if (!this.instance) {
+ this.instance = new this();
+ }
+ return this.instance;
+ }
+
+ async refreshToken(): Promise {}
+ cleanup(): void {}
+ async logout(): Promise {}
+ getUser(token: string): TabiyaUser | null {
+ return { id: "1", name: "Test User", email: "test@example.com" } as TabiyaUser;
+ }
+ getToken(): string {
+ return "foo token";
+ }
+ isTokenValid(token: string): { isValid: boolean; decodedToken: any; failureCause?: string } {
+ return { isValid: true, decodedToken: { sub: "1", email: "test@example.com" } };
+ }
+}
+
+// Mock questions config that matches the backend response structure
+const mockQuestionsConfig: QuestionsConfig = {
+ recommendation: {
+ question_id: "recommendation",
+ question_text: "Would you recommend this skills assessment to others?",
+ description: "Please let us know if you would recommend this tool",
+ type: QuestionType.YesNo,
+ show_comments_on: YesNoEnum.Yes,
+ comment_placeholder: "Please share why you would or would not recommend it",
+ },
+ // Add other required fields with dummy values
+ satisfaction_with_compass: {} as any,
+ perceived_bias: {} as any,
+ work_experience_accuracy: {} as any,
+ clarity_of_skills: {} as any,
+ incorrect_skills: {} as any,
+ missing_skills: {} as any,
+ interaction_ease: {} as any,
+ additional_feedback: {} as any,
+};
+
+const meta: Meta = {
+ title: "Feedback/OverallFeedback/QuestionComponents/RecommendationQuestion",
+ component: RecommendationQuestion,
+ tags: ["autodocs"],
+ args: {
+ feedbackItems: [],
+ onChange: (data) => {
+ action("onChange")(data);
+ },
+ },
+ parameters: {
+ mockData: [
+ // Mock the questions config endpoint that FeedbackProvider uses to fetch question data
+ {
+ url: getBackendUrl() + "/conversations/123/feedback/questions",
+ method: "GET",
+ status: 200,
+ response: mockQuestionsConfig,
+ },
+ // Mock the feedback submission endpoint that OverallFeedbackService uses to save ratings
+ {
+ url: getBackendUrl() + "/conversations/123/feedback",
+ method: "PATCH",
+ status: 200,
+ response: {
+ message: "Feedback submitted successfully",
+ },
+ },
+ ],
+ },
+ decorators: [
+ (Story) => {
+ // Mock session ID for API endpoint construction
+ const mockUserPrefsService = UserPreferencesStateService.getInstance();
+ mockUserPrefsService.getActiveSessionId = () => 123;
+
+ // Mock auth state for API authentication and user identification
+ const mockAuthStateService = AuthenticationStateService.getInstance();
+ mockAuthStateService.getToken = () => "foo token";
+ mockAuthStateService.getUser = () => ({ id: "1", name: "Test User", email: "test@example.com" } as TabiyaUser);
+
+ // Mock auth service factory to provide our mock auth service
+ AuthenticationServiceFactory.getCurrentAuthenticationService = () => MockAuthenticationService.getInstance();
+
+ return (
+
+
+
+ );
+ },
+ ],
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ feedbackItems: [],
+ },
+};
+
+export const WithExistingFeedback: Story = {
+ args: {
+ feedbackItems: [
+ {
+ question_id: "recommendation",
+ simplified_answer: {
+ rating_boolean: true,
+ comment: "The tool provides valuable insights and is easy to use",
+ },
+ },
+ ],
+ },
+};
\ No newline at end of file
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/recommendation/RecommendationQuestion.test.tsx b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/recommendation/RecommendationQuestion.test.tsx
new file mode 100644
index 000000000..ad90df4a3
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/recommendation/RecommendationQuestion.test.tsx
@@ -0,0 +1,134 @@
+// mute the console
+import "src/_test_utilities/consoleMock";
+import React from "react";
+import { render, screen } from "src/_test_utilities/test-utils";
+import RecommendationQuestion, {
+ DATA_TEST_ID,
+} from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/recommendation/RecommendationQuestion";
+import { FeedbackProvider } from "src/feedback/overallFeedback/feedbackContext/FeedbackContext";
+import { mockQuestionsConfig } from "src/feedback/overallFeedback/overallFeedbackForm/overallFeedbackForm.test.utils";
+import { RECOMMENDATION_QUESTION_ID } from "./constants";
+
+jest.mock("src/feedback/overallFeedback/overallFeedbackForm/components/questionsRenderer/QuestionRenderer", () => {
+ const actual = jest.requireActual("src/feedback/overallFeedback/overallFeedbackForm/components/questionsRenderer/QuestionRenderer");
+ return {
+ ...actual,
+ __esModule: true,
+ default: jest.fn().mockImplementation(() => ),
+ };
+})
+
+describe("RecommendationQuestion", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ const mockOnChange = jest.fn();
+
+ // Spy on the useFeedback hook to return our mockQuestionsConfig
+ jest.spyOn(require("src/feedback/overallFeedback/feedbackContext/FeedbackContext"), "useFeedback").mockReturnValue({
+ questionsConfig: mockQuestionsConfig,
+ setQuestionsConfig: jest.fn(),
+ isLoading: false,
+ error: null,
+ answers: [],
+ handleAnswerChange: jest.fn(),
+ clearAnswers: jest.fn(),
+ });
+
+ test("should render correctly with empty feedbackItems", () => {
+ // GIVEN a questions config and no feedback items
+ function Wrapper({ children }: React.PropsWithChildren<{}>) {
+ return {children};
+ }
+ // WHEN the component is rendered
+ const { container } = render(
+ , { wrapper: Wrapper }
+ );
+ // THEN the question should be in the document
+ expect(screen.getByTestId(DATA_TEST_ID.RECOMMENDATION)).toBeInTheDocument();
+ // AND it should match the snapshot
+ expect(container).toMatchSnapshot();
+ });
+
+ test("should render correctly with existing feedbackItems", () => {
+ // GIVEN a questions config and a feedback item
+ const feedbackItems = [
+ {
+ question_id: RECOMMENDATION_QUESTION_ID,
+ simplified_answer: {
+ rating_boolean: true,
+ comment: "I would recommend it.",
+ },
+ },
+ ];
+ function Wrapper({ children }: React.PropsWithChildren<{}>) {
+ return {children};
+ }
+ // WHEN the component is rendered
+ const { container } = render(
+ , { wrapper: Wrapper }
+ );
+ // THEN the question should be in the document
+ expect(screen.getByTestId(DATA_TEST_ID.RECOMMENDATION)).toBeInTheDocument();
+ // AND it should match the snapshot
+ expect(container).toMatchSnapshot();
+ });
+
+ test("should return null when questionsConfig is not available", () => {
+ // GIVEN the useFeedback hook returns null for questionsConfig
+ jest.spyOn(require("src/feedback/overallFeedback/feedbackContext/FeedbackContext"), "useFeedback").mockReturnValue({
+ questionsConfig: null,
+ setQuestionsConfig: jest.fn(),
+ isLoading: false,
+ error: null,
+ answers: [],
+ handleAnswerChange: jest.fn(),
+ clearAnswers: jest.fn(),
+ });
+
+ function Wrapper({ children }: React.PropsWithChildren<{}>) {
+ return {children};
+ }
+
+ // WHEN the component is rendered
+ const { container } = render(
+ , { wrapper: Wrapper }
+ );
+
+ // THEN expect the container to be empty
+ expect(container).toBeEmptyDOMElement();
+ // AND expect an error to be logged
+ expect(console.error).toHaveBeenCalledWith(expect.any(Error));
+ });
+
+ test("should return null when question ID is not in questionsConfig", () => {
+ // GIVEN the useFeedback hook returns a config without the required question ID
+ const configWithoutQuestion = { ...mockQuestionsConfig };
+ delete configWithoutQuestion[RECOMMENDATION_QUESTION_ID];
+
+ jest.spyOn(require("src/feedback/overallFeedback/feedbackContext/FeedbackContext"), "useFeedback").mockReturnValue({
+ questionsConfig: configWithoutQuestion,
+ setQuestionsConfig: jest.fn(),
+ isLoading: false,
+ error: null,
+ answers: [],
+ handleAnswerChange: jest.fn(),
+ clearAnswers: jest.fn(),
+ });
+
+ function Wrapper({ children }: React.PropsWithChildren<{}>) {
+ return {children};
+ }
+
+ // WHEN the component is rendered
+ const { container } = render(
+ , { wrapper: Wrapper }
+ );
+
+ // THEN expect the container to be empty
+ expect(container).toBeEmptyDOMElement();
+ // AND expect an error to be logged
+ expect(console.error).toHaveBeenCalledWith(expect.any(Error));
+ });
+});
\ No newline at end of file
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/recommendation/RecommendationQuestion.tsx b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/recommendation/RecommendationQuestion.tsx
new file mode 100644
index 000000000..8b98c87f4
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/recommendation/RecommendationQuestion.tsx
@@ -0,0 +1,45 @@
+import React from "react";
+import { Box } from "@mui/material";
+import { useFeedback } from "src/feedback/overallFeedback/feedbackContext/FeedbackContext";
+import QuestionRenderer
+ from "src/feedback/overallFeedback/overallFeedbackForm/components/questionsRenderer/QuestionRenderer";
+import { FeedbackItem } from "src/feedback/overallFeedback/overallFeedbackService/OverallFeedback.service.types";
+import { FeedbackError } from "src/error/commonErrors";
+import { RECOMMENDATION_QUESTION_ID } from "./constants";
+
+;
+
+const uniqueId = "3f2595f0-a373-42d7-b410-94d608e7a455";
+
+export const DATA_TEST_ID = {
+ RECOMMENDATION: `recommendation-${uniqueId}`,
+};
+
+interface RecommendationQuestionProps {
+ feedbackItems: FeedbackItem[];
+ onChange: (data: FeedbackItem) => void;
+}
+
+const RecommendationQuestion: React.FC = ({ feedbackItems, onChange }) => {
+ const { questionsConfig, error } = useFeedback();
+
+ if (!questionsConfig) {
+ console.error(new FeedbackError("Questions configuration is not available", error));
+ return null;
+ }
+
+ const question = questionsConfig[RECOMMENDATION_QUESTION_ID];
+
+ if (!question) {
+ console.error(new FeedbackError(`Questions configuration is not available for question: ${RECOMMENDATION_QUESTION_ID}`, error));
+ return null;
+ }
+
+ return (
+
+
+
+ );
+};
+
+export default RecommendationQuestion;
\ No newline at end of file
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/recommendation/__snapshots__/RecommendationQuestion.test.tsx.snap b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/recommendation/__snapshots__/RecommendationQuestion.test.tsx.snap
new file mode 100644
index 000000000..0b1a39a9a
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/recommendation/__snapshots__/RecommendationQuestion.test.tsx.snap
@@ -0,0 +1,27 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`RecommendationQuestion should render correctly with empty feedbackItems 1`] = `
+
+
+
+
+
+`;
+
+exports[`RecommendationQuestion should render correctly with existing feedbackItems 1`] = `
+
+
+
+
+
+`;
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/recommendation/constants.ts b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/recommendation/constants.ts
new file mode 100644
index 000000000..4fe1fdaf0
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/recommendation/constants.ts
@@ -0,0 +1,2 @@
+// this is in a file of its own instead of in the RecommendationQuestion.tsx to avoid circular dependencies
+export const RECOMMENDATION_QUESTION_ID = "recommendation";
\ No newline at end of file
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/workExperienceAccuracy/WorkExperienceAccuracyQuestion.stories.tsx b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/workExperienceAccuracy/WorkExperienceAccuracyQuestion.stories.tsx
new file mode 100644
index 000000000..8db06a5f6
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/workExperienceAccuracy/WorkExperienceAccuracyQuestion.stories.tsx
@@ -0,0 +1,142 @@
+import { Meta, StoryObj } from "@storybook/react";
+import WorkExperienceAccuracyQuestion from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/workExperienceAccuracy/WorkExperienceAccuracyQuestion";
+import { action } from "@storybook/addon-actions";
+import { FeedbackProvider } from "src/feedback/overallFeedback/feedbackContext/FeedbackContext";
+import { getBackendUrl } from "src/envService";
+import { QuestionsConfig, QuestionType } from "src/feedback/overallFeedback/overallFeedbackForm/overallFeedbackForm.types";
+import UserPreferencesStateService from "src/userPreferences/UserPreferencesStateService";
+import AuthenticationServiceFactory from "src/auth/services/Authentication.service.factory";
+import AuthenticationStateService from "src/auth/services/AuthenticationState.service";
+import { TabiyaUser } from "src/auth/auth.types";
+import AuthenticationService from "src/auth/services/Authentication.service";
+
+// Mock authentication service to provide token validation and user info for API calls
+class MockAuthenticationService extends AuthenticationService {
+ private static instance: MockAuthenticationService;
+
+ private constructor() {
+ super();
+ }
+
+ static getInstance(): MockAuthenticationService {
+ if (!this.instance) {
+ this.instance = new this();
+ }
+ return this.instance;
+ }
+
+ async refreshToken(): Promise {}
+ cleanup(): void {}
+ async logout(): Promise {}
+ getUser(token: string): TabiyaUser | null {
+ return { id: "1", name: "Test User", email: "test@example.com" } as TabiyaUser;
+ }
+ getToken(): string {
+ return "foo token";
+ }
+ isTokenValid(token: string): { isValid: boolean; decodedToken: any; failureCause?: string } {
+ return { isValid: true, decodedToken: { sub: "1", email: "test@example.com" } };
+ }
+}
+
+// Mock questions config that matches the backend response structure
+const mockQuestionsConfig: QuestionsConfig = {
+ work_experience_accuracy: {
+ question_id: "work_experience_accuracy",
+ question_text: "How accurate was the assessment of your work experience?",
+ description: "Please rate how well the system captured your work experience",
+ type: QuestionType.Checkbox,
+ options: {
+ "very_accurate": "Very accurate",
+ "somewhat_accurate": "Somewhat accurate",
+ "not_accurate": "Not accurate"
+ },
+ comment_placeholder: "Please share your thoughts on the accuracy",
+ },
+ // Add other required fields with dummy values
+ satisfaction_with_compass: {} as any,
+ perceived_bias: {} as any,
+ clarity_of_skills: {} as any,
+ incorrect_skills: {} as any,
+ missing_skills: {} as any,
+ interaction_ease: {} as any,
+ recommendation: {} as any,
+ additional_feedback: {} as any,
+};
+
+const meta: Meta = {
+ title: "Feedback/OverallFeedback/QuestionComponents/WorkExperienceAccuracyQuestion",
+ component: WorkExperienceAccuracyQuestion,
+ tags: ["autodocs"],
+ args: {
+ feedbackItems: [],
+ onChange: (data) => {
+ action("onChange")(data);
+ },
+ },
+ parameters: {
+ mockData: [
+ // Mock the questions config endpoint that FeedbackProvider uses to fetch question data
+ {
+ url: getBackendUrl() + "/conversations/123/feedback/questions",
+ method: "GET",
+ status: 200,
+ response: mockQuestionsConfig,
+ },
+ // Mock the feedback submission endpoint that OverallFeedbackService uses to save ratings
+ {
+ url: getBackendUrl() + "/conversations/123/feedback",
+ method: "PATCH",
+ status: 200,
+ response: {
+ message: "Feedback submitted successfully",
+ },
+ },
+ ],
+ },
+ decorators: [
+ (Story) => {
+ // Mock session ID for API endpoint construction
+ const mockUserPrefsService = UserPreferencesStateService.getInstance();
+ mockUserPrefsService.getActiveSessionId = () => 123;
+
+ // Mock auth state for API authentication and user identification
+ const mockAuthStateService = AuthenticationStateService.getInstance();
+ mockAuthStateService.getToken = () => "foo token";
+ mockAuthStateService.getUser = () => ({ id: "1", name: "Test User", email: "test@example.com" } as TabiyaUser);
+
+ // Mock auth service factory to provide our mock auth service
+ AuthenticationServiceFactory.getCurrentAuthenticationService = () => MockAuthenticationService.getInstance();
+
+ return (
+
+
+
+ );
+ },
+ ],
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ feedbackItems: [],
+ },
+};
+
+export const WithExistingFeedback: Story = {
+ args: {
+ feedbackItems: [
+ {
+ question_id: "work_experience_accuracy",
+ simplified_answer: {
+ selected_options_keys: ["somewhat_accurate"],
+ comment: "The system captured most of my experience accurately, but missed some recent roles",
+ },
+ },
+ ],
+ },
+};
\ No newline at end of file
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/workExperienceAccuracy/WorkExperienceAccuracyQuestion.test.tsx b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/workExperienceAccuracy/WorkExperienceAccuracyQuestion.test.tsx
new file mode 100644
index 000000000..f4da72bad
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/workExperienceAccuracy/WorkExperienceAccuracyQuestion.test.tsx
@@ -0,0 +1,134 @@
+// mute the console
+import "src/_test_utilities/consoleMock";
+import React from "react";
+import { render, screen } from "src/_test_utilities/test-utils";
+import WorkExperienceAccuracyQuestion, {
+ DATA_TEST_ID,
+} from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/workExperienceAccuracy/WorkExperienceAccuracyQuestion";
+import { FeedbackProvider } from "src/feedback/overallFeedback/feedbackContext/FeedbackContext";
+import { mockQuestionsConfig } from "src/feedback/overallFeedback/overallFeedbackForm/overallFeedbackForm.test.utils";
+import { WORK_EXPERIENCE_ACCURACY_QUESTION_ID } from "./constants";
+
+jest.mock("src/feedback/overallFeedback/overallFeedbackForm/components/questionsRenderer/QuestionRenderer", () => {
+ const actual = jest.requireActual("src/feedback/overallFeedback/overallFeedbackForm/components/questionsRenderer/QuestionRenderer");
+ return {
+ ...actual,
+ __esModule: true,
+ default: jest.fn().mockImplementation(() => ),
+ };
+})
+
+describe("WorkExperienceAccuracyQuestion", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ const mockOnChange = jest.fn();
+
+ // Spy on the useFeedback hook to return our mockQuestionsConfig
+ jest.spyOn(require("src/feedback/overallFeedback/feedbackContext/FeedbackContext"), "useFeedback").mockReturnValue({
+ questionsConfig: mockQuestionsConfig,
+ setQuestionsConfig: jest.fn(),
+ isLoading: false,
+ error: null,
+ answers: [],
+ handleAnswerChange: jest.fn(),
+ clearAnswers: jest.fn(),
+ });
+
+ test("should render correctly with empty feedbackItems", () => {
+ // GIVEN a questions config and no feedback items
+ function Wrapper({ children }: React.PropsWithChildren<{}>) {
+ return {children};
+ }
+ // WHEN the component is rendered
+ const { container } = render(
+ , { wrapper: Wrapper }
+ );
+ // THEN the question should be in the document
+ expect(screen.getByTestId(DATA_TEST_ID.WORK_EXPERIENCE_ACCURACY)).toBeInTheDocument();
+ // AND it should match the snapshot
+ expect(container).toMatchSnapshot();
+ });
+
+ test("should render correctly with existing feedbackItems", () => {
+ // GIVEN a questions config and a feedback item
+ const feedbackItems = [
+ {
+ question_id: WORK_EXPERIENCE_ACCURACY_QUESTION_ID,
+ simplified_answer: {
+ rating_boolean: true,
+ comment: "The work experience was accurate.",
+ },
+ },
+ ];
+ function Wrapper({ children }: React.PropsWithChildren<{}>) {
+ return {children};
+ }
+ // WHEN the component is rendered
+ const { container } = render(
+ , { wrapper: Wrapper }
+ );
+ // THEN the question should be in the document
+ expect(screen.getByTestId(DATA_TEST_ID.WORK_EXPERIENCE_ACCURACY)).toBeInTheDocument();
+ // AND it should match the snapshot
+ expect(container).toMatchSnapshot();
+ });
+
+ test("should return null when questionsConfig is not available", () => {
+ // GIVEN the useFeedback hook returns null for questionsConfig
+ jest.spyOn(require("src/feedback/overallFeedback/feedbackContext/FeedbackContext"), "useFeedback").mockReturnValue({
+ questionsConfig: null,
+ setQuestionsConfig: jest.fn(),
+ isLoading: false,
+ error: null,
+ answers: [],
+ handleAnswerChange: jest.fn(),
+ clearAnswers: jest.fn(),
+ });
+
+ function Wrapper({ children }: React.PropsWithChildren<{}>) {
+ return {children};
+ }
+
+ // WHEN the component is rendered
+ const { container } = render(
+ , { wrapper: Wrapper }
+ );
+
+ // THEN expect the container to be empty
+ expect(container).toBeEmptyDOMElement();
+ // AND expect an error to be logged
+ expect(console.error).toHaveBeenCalledWith(expect.any(Error));
+ });
+
+ test("should return null when question ID is not in questionsConfig", () => {
+ // GIVEN the useFeedback hook returns a config without the required question ID
+ const configWithoutQuestion = { ...mockQuestionsConfig };
+ delete configWithoutQuestion[WORK_EXPERIENCE_ACCURACY_QUESTION_ID];
+
+ jest.spyOn(require("src/feedback/overallFeedback/feedbackContext/FeedbackContext"), "useFeedback").mockReturnValue({
+ questionsConfig: configWithoutQuestion,
+ setQuestionsConfig: jest.fn(),
+ isLoading: false,
+ error: null,
+ answers: [],
+ handleAnswerChange: jest.fn(),
+ clearAnswers: jest.fn(),
+ });
+
+ function Wrapper({ children }: React.PropsWithChildren<{}>) {
+ return {children};
+ }
+
+ // WHEN the component is rendered
+ const { container } = render(
+ , { wrapper: Wrapper }
+ );
+
+ // THEN expect the container to be empty
+ expect(container).toBeEmptyDOMElement();
+ // AND expect an error to be logged
+ expect(console.error).toHaveBeenCalledWith(expect.any(Error));
+ });
+});
\ No newline at end of file
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/workExperienceAccuracy/WorkExperienceAccuracyQuestion.tsx b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/workExperienceAccuracy/WorkExperienceAccuracyQuestion.tsx
new file mode 100644
index 000000000..be36ac50a
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/workExperienceAccuracy/WorkExperienceAccuracyQuestion.tsx
@@ -0,0 +1,45 @@
+import React from "react";
+import { Box } from "@mui/material";
+import { useFeedback } from "src/feedback/overallFeedback/feedbackContext/FeedbackContext";
+import QuestionRenderer
+ from "src/feedback/overallFeedback/overallFeedbackForm/components/questionsRenderer/QuestionRenderer";
+import { FeedbackItem } from "src/feedback/overallFeedback/overallFeedbackService/OverallFeedback.service.types";
+import { FeedbackError } from "src/error/commonErrors";
+import { WORK_EXPERIENCE_ACCURACY_QUESTION_ID } from "./constants";
+
+;
+
+const uniqueId = "b8cad548-471b-4ec1-8a6f-bbe9028f87b9";
+
+export const DATA_TEST_ID = {
+ WORK_EXPERIENCE_ACCURACY: `work-experience-accuracy-${uniqueId}`,
+};
+
+interface WorkExperienceAccuracyQuestionProps {
+ feedbackItems: FeedbackItem[];
+ onChange: (data: FeedbackItem) => void;
+}
+
+const WorkExperienceAccuracyQuestion: React.FC = ({ feedbackItems, onChange }) => {
+ const { questionsConfig, error } = useFeedback();
+
+ if (!questionsConfig) {
+ console.error(new FeedbackError("Questions configuration is not available", error));
+ return null;
+ }
+
+ const question = questionsConfig[WORK_EXPERIENCE_ACCURACY_QUESTION_ID];
+
+ if (!question) {
+ console.error(new FeedbackError(`Questions configuration is not available for question: ${WORK_EXPERIENCE_ACCURACY_QUESTION_ID}`, error));
+ return null;
+ }
+
+ return (
+
+
+
+ );
+};
+
+export default WorkExperienceAccuracyQuestion;
\ No newline at end of file
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/workExperienceAccuracy/__snapshots__/WorkExperienceAccuracyQuestion.test.tsx.snap b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/workExperienceAccuracy/__snapshots__/WorkExperienceAccuracyQuestion.test.tsx.snap
new file mode 100644
index 000000000..0d1e20099
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/workExperienceAccuracy/__snapshots__/WorkExperienceAccuracyQuestion.test.tsx.snap
@@ -0,0 +1,27 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`WorkExperienceAccuracyQuestion should render correctly with empty feedbackItems 1`] = `
+
+
+
+
+
+`;
+
+exports[`WorkExperienceAccuracyQuestion should render correctly with existing feedbackItems 1`] = `
+
+
+
+
+
+`;
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/workExperienceAccuracy/constants.ts b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/workExperienceAccuracy/constants.ts
new file mode 100644
index 000000000..9d98a59f7
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionComponents/workExperienceAccuracy/constants.ts
@@ -0,0 +1,2 @@
+// this is in a file of its own instead of in the WorkExperienceAccuracyQuesiton.tsx to avoid circular dependencies
+export const WORK_EXPERIENCE_ACCURACY_QUESTION_ID = "work_experience_accuracy";
\ No newline at end of file
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/checkboxQuestion/CheckboxQuestion.stories.tsx b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/checkboxQuestion/CheckboxQuestion.stories.tsx
new file mode 100644
index 000000000..21e8366bd
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/checkboxQuestion/CheckboxQuestion.stories.tsx
@@ -0,0 +1,136 @@
+import { Meta, StoryObj } from "@storybook/react";
+import CheckboxQuestion from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/checkboxQuestion/CheckboxQuestion";
+import { action } from "@storybook/addon-actions";
+import { FeedbackProvider } from "src/feedback/overallFeedback/feedbackContext/FeedbackContext";
+import { getBackendUrl } from "src/envService";
+import { QuestionType } from "src/feedback/overallFeedback/overallFeedbackForm/overallFeedbackForm.types";
+import UserPreferencesStateService from "src/userPreferences/UserPreferencesStateService";
+import AuthenticationServiceFactory from "src/auth/services/Authentication.service.factory";
+import AuthenticationStateService from "src/auth/services/AuthenticationState.service";
+import { TabiyaUser } from "src/auth/auth.types";
+import AuthenticationService from "src/auth/services/Authentication.service";
+
+// Mock authentication service to provide token validation and user info for API calls
+class MockAuthenticationService extends AuthenticationService {
+ private static instance: MockAuthenticationService;
+
+ private constructor() {
+ super();
+ }
+
+ static getInstance(): MockAuthenticationService {
+ if (!this.instance) {
+ this.instance = new this();
+ }
+ return this.instance;
+ }
+
+ async refreshToken(): Promise {}
+ cleanup(): void {}
+ async logout(): Promise {}
+ getUser(token: string): TabiyaUser | null {
+ return { id: "1", name: "Test User", email: "test@example.com" } as TabiyaUser;
+ }
+ getToken(): string {
+ return "foo token";
+ }
+ isTokenValid(token: string): { isValid: boolean; decodedToken: any; failureCause?: string } {
+ return { isValid: true, decodedToken: { sub: "1", email: "test@example.com" } };
+ }
+}
+
+// Mock questions config that matches the backend response structure
+const mockQuestionsConfig = {
+ checkbox_question: {
+ questionId: "checkbox_question",
+ question_text: "How accurate and relevant was the information provided?",
+ description: "Please select all that apply",
+ type: QuestionType.Checkbox,
+ options: [
+ { key: "accurate", value: "Accurate" },
+ { key: "relevant", value: "Relevant" },
+ { key: "upToDate", value: "Up-to-date" },
+ { key: "easyToUnderstand", value: "Easy to understand" },
+ ],
+ comment_placeholder: "Please provide comments",
+ },
+ // Add other required fields with dummy values
+ satisfaction_with_compass: {} as any,
+ perceived_bias: {} as any,
+ work_experience_accuracy: {} as any,
+ clarity_of_skills: {} as any,
+ incorrect_skills: {} as any,
+ missing_skills: {} as any,
+ interaction_ease: {} as any,
+ recommendation: {} as any,
+};
+
+const meta: Meta = {
+ title: "Feedback/OverallFeedback/QuestionTypes/CheckboxQuestion",
+ component: CheckboxQuestion,
+ tags: ["autodocs"],
+ args: {
+ notifyChange: (selectedOptions, comments) => {
+ action("notifyChange")(selectedOptions, comments);
+ },
+ },
+ parameters: {
+ mockData: [
+ // Mock the questions config endpoint that FeedbackProvider uses to fetch question data
+ {
+ url: getBackendUrl() + "/conversations/123/feedback/questions",
+ method: "GET",
+ status: 200,
+ response: mockQuestionsConfig,
+ },
+ // Mock the feedback submission endpoint that OverallFeedbackService uses to save ratings
+ {
+ url: getBackendUrl() + "/conversations/123/feedback",
+ method: "PATCH",
+ status: 200,
+ response: {
+ message: "Feedback submitted successfully",
+ },
+ },
+ ],
+ },
+ decorators: [
+ (Story) => {
+ // Mock session ID for API endpoint construction
+ const mockUserPrefsService = UserPreferencesStateService.getInstance();
+ mockUserPrefsService.getActiveSessionId = () => 123;
+
+ // Mock auth state for API authentication and user identification
+ const mockAuthStateService = AuthenticationStateService.getInstance();
+ mockAuthStateService.getToken = () => "foo token";
+ mockAuthStateService.getUser = () => ({ id: "1", name: "Test User", email: "test@example.com" } as TabiyaUser);
+
+ // Mock auth service factory to provide our mock auth service
+ AuthenticationServiceFactory.getCurrentAuthenticationService = () => MockAuthenticationService.getInstance();
+
+ return (
+
+
+
+ );
+ },
+ ],
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Shown: Story = {
+ args: {
+ question_text: "How accurate and relevant was the information provided?",
+ options: {
+ accurate:"Accurate",
+ relevant: "Relevant",
+ upToDate: "Up-to-date",
+ easyToUnderstand: "Easy to understand"
+ },
+ selectedOptions: [],
+ comment_placeholder: "Please provide comments",
+ },
+};
diff --git a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/checkboxQuestion/CheckboxQuestion.test.tsx b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/checkboxQuestion/CheckboxQuestion.test.tsx
similarity index 83%
rename from frontend-new/src/feedback/overallFeedback/feedbackForm/components/checkboxQuestion/CheckboxQuestion.test.tsx
rename to frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/checkboxQuestion/CheckboxQuestion.test.tsx
index e1843c766..fc48c5836 100644
--- a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/checkboxQuestion/CheckboxQuestion.test.tsx
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/checkboxQuestion/CheckboxQuestion.test.tsx
@@ -2,28 +2,30 @@
import "src/_test_utilities/consoleMock";
import { render, screen } from "src/_test_utilities/test-utils";
import { fireEvent } from "@testing-library/react";
-import { QuestionType } from "src/feedback/overallFeedback/feedbackForm/feedbackForm.types";
+import { QuestionType } from "src/feedback/overallFeedback/overallFeedbackForm/overallFeedbackForm.types";
import CheckboxQuestion, {
CheckboxQuestionProps,
DATA_TEST_ID,
-} from "src/feedback/overallFeedback/feedbackForm/components/checkboxQuestion/CheckboxQuestion";
-import { DATA_TEST_ID as COMMENT_TEXT_FIELD_TEST_ID } from "src/feedback/overallFeedback/feedbackForm/components/commentTextField/CommentTextField";
+} from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/checkboxQuestion/CheckboxQuestion";
+import { DATA_TEST_ID as COMMENT_TEXT_FIELD_TEST_ID } from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/commentTextField/CommentTextField";
describe("CheckboxQuestion", () => {
// mock question
const mockQuestion: CheckboxQuestionProps = {
type: QuestionType.Checkbox,
- questionId: "skills_experience",
- questionText: "What are your skills and experiences?",
+ question_id: "skills_experience",
+ question_text: "What are your skills and experiences?",
+ description: "Test description",
+ comment_placeholder: "Please provide comments",
selectedOptions: ["javascript", "react"],
notifyChange: jest.fn(),
- options: [
- { key: "javascript", value: "JavaScript" },
- { key: "react", value: "React" },
- { key: "typescript", value: "TypeScript" },
- { key: "nodejs", value: "Node.js" },
- ],
- comments: "I have experience with JavaScript and React",
+ options: {
+ javascript: "JavaScript",
+ react: "React",
+ typescript: "TypeScript",
+ nodejs: "Node.js"
+ },
+ comments: "I have experience with JavaScript and React"
};
test("should render component successfully", () => {
diff --git a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/checkboxQuestion/CheckboxQuestion.tsx b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/checkboxQuestion/CheckboxQuestion.tsx
similarity index 78%
rename from frontend-new/src/feedback/overallFeedback/feedbackForm/components/checkboxQuestion/CheckboxQuestion.tsx
rename to frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/checkboxQuestion/CheckboxQuestion.tsx
index cc38f15be..29f906294 100644
--- a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/checkboxQuestion/CheckboxQuestion.tsx
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/checkboxQuestion/CheckboxQuestion.tsx
@@ -1,14 +1,15 @@
import React, { useState, useEffect, useRef } from "react";
import { Checkbox, FormControl, FormControlLabel, FormGroup, FormLabel, useTheme } from "@mui/material";
-import { BaseQuestion, Option } from "src/feedback/overallFeedback/feedbackForm/feedbackForm.types";
-import QuestionText from "src/feedback/overallFeedback/feedbackForm/components/questionText/QuestionText";
-import CommentTextField from "src/feedback/overallFeedback/feedbackForm/components/commentTextField/CommentTextField";
-import { focusAndScrollToField } from "src/feedback/overallFeedback/feedbackForm/util";
+import { BaseQuestion } from "src/feedback/overallFeedback/overallFeedbackForm/overallFeedbackForm.types";
+import QuestionText from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/questionText/QuestionText";
+import CommentTextField from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/commentTextField/CommentTextField";
+import { focusAndScrollToField } from "src/feedback/overallFeedback/overallFeedbackForm/util";
export interface CheckboxQuestionProps extends BaseQuestion {
+ question_id: string;
selectedOptions: string[];
notifyChange: (selectedOptions: string[], comments?: string) => void;
- options: Option[];
+ options: Record;
comments?: string;
}
@@ -22,12 +23,12 @@ export const DATA_TEST_ID = {
};
const CheckboxQuestion: React.FC = ({
- questionText,
+ question_text,
selectedOptions,
notifyChange,
options,
comments,
- placeholder,
+ comment_placeholder,
}) => {
const theme = useTheme();
const [checkedOptions, setCheckedOptions] = useState(selectedOptions);
@@ -69,7 +70,7 @@ const CheckboxQuestion: React.FC = ({
data-testid={DATA_TEST_ID.FORM_CONTROL}
>
-
+ = ({
gap: theme.tabiyaSpacing.sm,
}}
>
- {options?.map((option) => (
+ {Object.keys(options).map((key: string) => (
handleCheckboxChange(option.key)}
+ checked={checkedOptions.includes(key)}
+ onChange={() => handleCheckboxChange(key)}
data-testid={DATA_TEST_ID.CHECKBOX_OPTION}
sx={{ padding: 0, marginRight: theme.tabiyaSpacing.sm }}
/>
}
- label={option.value}
+ label={options[key]}
sx={{ margin: 0, width: "fit-content" }}
/>
))}
{checkedOptions.length > 0 && (
diff --git a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/commentTextField/CommentTextField.test.tsx b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/commentTextField/CommentTextField.test.tsx
similarity index 87%
rename from frontend-new/src/feedback/overallFeedback/feedbackForm/components/commentTextField/CommentTextField.test.tsx
rename to frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/commentTextField/CommentTextField.test.tsx
index 7accc4f46..201d077cc 100644
--- a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/commentTextField/CommentTextField.test.tsx
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/commentTextField/CommentTextField.test.tsx
@@ -3,7 +3,7 @@ import "src/_test_utilities/consoleMock";
import CommentTextField, {
DATA_TEST_ID,
-} from "src/feedback/overallFeedback/feedbackForm/components/commentTextField/CommentTextField";
+} from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/commentTextField/CommentTextField";
import { render, screen } from "src/_test_utilities/test-utils";
describe("CommentTextField", () => {
diff --git a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/commentTextField/CommentTextField.tsx b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/commentTextField/CommentTextField.tsx
similarity index 100%
rename from frontend-new/src/feedback/overallFeedback/feedbackForm/components/commentTextField/CommentTextField.tsx
rename to frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/commentTextField/CommentTextField.tsx
diff --git a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/commentTextField/__snapshots__/CommentTextField.test.tsx.snap b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/commentTextField/__snapshots__/CommentTextField.test.tsx.snap
similarity index 100%
rename from frontend-new/src/feedback/overallFeedback/feedbackForm/components/commentTextField/__snapshots__/CommentTextField.test.tsx.snap
rename to frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/commentTextField/__snapshots__/CommentTextField.test.tsx.snap
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/customRating/CustomRating.stories.tsx b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/customRating/CustomRating.stories.tsx
new file mode 100644
index 000000000..fbc5701da
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/customRating/CustomRating.stories.tsx
@@ -0,0 +1,142 @@
+import { Meta, StoryObj } from "@storybook/react";
+import CustomRating from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/customRating/CustomRating";
+import { action } from "@storybook/addon-actions";
+import { QuestionType } from "src/feedback/overallFeedback/overallFeedbackForm/overallFeedbackForm.types";
+import { FeedbackProvider } from "src/feedback/overallFeedback/feedbackContext/FeedbackContext";
+import { getBackendUrl } from "src/envService";
+import UserPreferencesStateService from "src/userPreferences/UserPreferencesStateService";
+import AuthenticationServiceFactory from "src/auth/services/Authentication.service.factory";
+import AuthenticationStateService from "src/auth/services/AuthenticationState.service";
+import { TabiyaUser } from "src/auth/auth.types";
+import AuthenticationService from "src/auth/services/Authentication.service";
+
+// Mock authentication service to provide token validation and user info for API calls
+class MockAuthenticationService extends AuthenticationService {
+ private static instance: MockAuthenticationService;
+
+ private constructor() {
+ super();
+ }
+
+ static getInstance(): MockAuthenticationService {
+ if (!this.instance) {
+ this.instance = new this();
+ }
+ return this.instance;
+ }
+
+ async refreshToken(): Promise {}
+ cleanup(): void {}
+ async logout(): Promise {}
+ getUser(token: string): TabiyaUser | null {
+ return { id: "1", name: "Test User", email: "test@example.com" } as TabiyaUser;
+ }
+ getToken(): string {
+ return "foo token";
+ }
+ isTokenValid(token: string): { isValid: boolean; decodedToken: any; failureCause?: string } {
+ return { isValid: true, decodedToken: { sub: "1", email: "test@example.com" } };
+ }
+}
+
+// Mock questions config that matches the backend response structure
+const mockQuestionsConfig = {
+ interaction_ease: {
+ questionId: "interaction_ease",
+ question_text: "How easy was it to interact with the system?",
+ description: "Please rate the ease of interaction",
+ type: QuestionType.Rating,
+ low_rating_label: "Not easy",
+ high_rating_label: "Very easy",
+ max_rating: 5,
+ display_rating: true,
+ comment_placeholder: "Please share your thoughts",
+ },
+ // Add other required fields with dummy values
+ satisfaction_with_compass: {} as any,
+ perceived_bias: {} as any,
+ work_experience_accuracy: {} as any,
+ clarity_of_skills: {} as any,
+ incorrect_skills: {} as any,
+ missing_skills: {} as any,
+ recommendation: {} as any,
+};
+
+const meta: Meta = {
+ title: "Feedback/OverallFeedback/QuestionTypes/CustomRating",
+ component: CustomRating,
+ tags: ["autodocs"],
+ args: {
+ notifyChange: (value, comments) => {
+ action("notifyChange")(value, comments);
+ },
+ },
+ parameters: {
+ mockData: [
+ // Mock the questions config endpoint that FeedbackProvider uses to fetch question data
+ {
+ url: getBackendUrl() + "/conversations/123/feedback/questions",
+ method: "GET",
+ status: 200,
+ response: mockQuestionsConfig,
+ },
+ // Mock the feedback submission endpoint that OverallFeedbackService uses to save ratings
+ {
+ url: getBackendUrl() + "/conversations/123/feedback",
+ method: "PATCH",
+ status: 200,
+ response: {
+ message: "Feedback submitted successfully",
+ },
+ },
+ ],
+ },
+ decorators: [
+ (Story) => {
+ // Mock session ID for API endpoint construction
+ const mockUserPrefsService = UserPreferencesStateService.getInstance();
+ mockUserPrefsService.getActiveSessionId = () => 123;
+
+ // Mock auth state for API authentication and user identification
+ const mockAuthStateService = AuthenticationStateService.getInstance();
+ mockAuthStateService.getToken = () => "foo token";
+ mockAuthStateService.getUser = () => ({ id: "1", name: "Test User", email: "test@example.com" } as TabiyaUser);
+
+ // Mock auth service factory to provide our mock auth service
+ AuthenticationServiceFactory.getCurrentAuthenticationService = () => MockAuthenticationService.getInstance();
+
+ return (
+
+
+
+ );
+ },
+ ],
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Shown: Story = {
+ args: {
+ question_text: "How easy was it to interact with the system?",
+ question_id: "interaction_ease",
+ lowRatingLabel: "Not easy",
+ highRatingLabel: "Very easy",
+ comment_placeholder: "Please provide comments",
+ description: "Test description",
+ type: QuestionType.Rating
+ },
+};
+
+export const ShownWithNoRating: Story = {
+ args: {
+ question_text: "How easy was it to interact with the system?",
+ question_id: "interaction_ease",
+ displayRating: false,
+ comment_placeholder: "Please provide comments",
+ description: "Test description",
+ type: QuestionType.Rating
+ },
+};
diff --git a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/customRating/CustomRating.test.tsx b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/customRating/CustomRating.test.tsx
similarity index 87%
rename from frontend-new/src/feedback/overallFeedback/feedbackForm/components/customRating/CustomRating.test.tsx
rename to frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/customRating/CustomRating.test.tsx
index 0a5a439ed..4d14c4ab3 100644
--- a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/customRating/CustomRating.test.tsx
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/customRating/CustomRating.test.tsx
@@ -4,26 +4,27 @@ import "src/_test_utilities/consoleMock";
import CustomRating, {
CustomRatingProps,
DATA_TEST_ID,
-} from "src/feedback/overallFeedback/feedbackForm/components/customRating/CustomRating";
+} from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/customRating/CustomRating";
import { render, screen } from "src/_test_utilities/test-utils";
import { fireEvent } from "@testing-library/react";
-import { QuestionType } from "src/feedback/overallFeedback/feedbackForm/feedbackForm.types";
-import { DATA_TEST_ID as QUESTION_TEXT_DATA_TEST_ID } from "src/feedback/overallFeedback/feedbackForm/components/questionText/QuestionText";
-import { DATA_TEST_ID as COMMENT_TEXT_FIELD_TEST_ID } from "src/feedback/overallFeedback/feedbackForm/components/commentTextField/CommentTextField";
+import { QuestionType } from "src/feedback/overallFeedback/overallFeedbackForm/overallFeedbackForm.types";
+import { DATA_TEST_ID as QUESTION_TEXT_DATA_TEST_ID } from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/questionText/QuestionText";
+import { DATA_TEST_ID as COMMENT_TEXT_FIELD_TEST_ID } from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/commentTextField/CommentTextField";
describe("CustomRating", () => {
// mock question
const mockQuestion: CustomRatingProps = {
type: QuestionType.Rating,
- questionText: "How would you rate the overall experience?",
- questionId: "overall_experience",
- placeholder: "Please provide your feedback here",
+ question_text: "How would you rate the overall experience?",
+ question_id: "overall_experience",
+ comment_placeholder: "Please provide your feedback here",
ratingValue: 6,
displayRating: true,
lowRatingLabel: "Very Difficult",
highRatingLabel: "Very Easy",
notifyChange: jest.fn(),
maxRating: 5,
+ description: "Test description"
};
test("should render component successfully", () => {
diff --git a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/customRating/CustomRating.tsx b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/customRating/CustomRating.tsx
similarity index 86%
rename from frontend-new/src/feedback/overallFeedback/feedbackForm/components/customRating/CustomRating.tsx
rename to frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/customRating/CustomRating.tsx
index 9833fae71..649cacec8 100644
--- a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/customRating/CustomRating.tsx
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/customRating/CustomRating.tsx
@@ -3,10 +3,10 @@ import { useMediaQuery, useTheme } from "@mui/material";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import Rating from "@mui/material/Rating";
-import { BaseQuestion } from "src/feedback/overallFeedback/feedbackForm/feedbackForm.types";
-import QuestionText from "src/feedback/overallFeedback/feedbackForm/components/questionText/QuestionText";
-import CommentTextField from "src/feedback/overallFeedback/feedbackForm/components/commentTextField/CommentTextField";
-import { focusAndScrollToField } from "src/feedback/overallFeedback/feedbackForm/util";
+import { BaseQuestion } from "src/feedback/overallFeedback/overallFeedbackForm/overallFeedbackForm.types";
+import QuestionText from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/questionText/QuestionText";
+import CommentTextField from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/commentTextField/CommentTextField";
+import { focusAndScrollToField } from "src/feedback/overallFeedback/overallFeedbackForm/util";
export interface CustomRatingProps extends BaseQuestion {
ratingValue: number | null;
@@ -30,17 +30,17 @@ export const DATA_TEST_ID = {
};
const CustomRating: React.FC = ({
- questionId,
+ question_id,
ratingValue,
notifyChange,
- questionText,
+ question_text,
lowRatingLabel,
highRatingLabel,
comments,
displayRating = true,
disabled,
maxRating,
- placeholder,
+ comment_placeholder,
}) => {
const theme = useTheme();
const [commentText, setCommentText] = useState(comments ?? "");
@@ -68,12 +68,12 @@ const CustomRating: React.FC = ({
data-testid={DATA_TEST_ID.CUSTOM_RATING_CONTAINER}
>
-
+
{displayRating && (
handleRatingChange(newValue)}
max={maxRating}
@@ -117,9 +117,9 @@ const CustomRating: React.FC = ({
)}
- {placeholder && (
+ {comment_placeholder && (
{
diff --git a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/questionText/QuestionText.tsx b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/questionText/QuestionText.tsx
similarity index 100%
rename from frontend-new/src/feedback/overallFeedback/feedbackForm/components/questionText/QuestionText.tsx
rename to frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/questionText/QuestionText.tsx
diff --git a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/questionText/__snapshots__/QuestionText.test.tsx.snap b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/questionText/__snapshots__/QuestionText.test.tsx.snap
similarity index 100%
rename from frontend-new/src/feedback/overallFeedback/feedbackForm/components/questionText/__snapshots__/QuestionText.test.tsx.snap
rename to frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/questionText/__snapshots__/QuestionText.test.tsx.snap
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/yesNoQuestion/YesNoQuestion.stories.tsx b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/yesNoQuestion/YesNoQuestion.stories.tsx
new file mode 100644
index 000000000..e2ba61ada
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/yesNoQuestion/YesNoQuestion.stories.tsx
@@ -0,0 +1,138 @@
+import { Meta, StoryObj } from "@storybook/react";
+import YesNoQuestion from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/yesNoQuestion/YesNoQuestion";
+import { YesNoEnum } from "src/feedback/overallFeedback/overallFeedbackForm/overallFeedbackForm.types";
+import { action } from "@storybook/addon-actions";
+import { FeedbackProvider } from "src/feedback/overallFeedback/feedbackContext/FeedbackContext";
+import { getBackendUrl } from "src/envService";
+import { QuestionType } from "src/feedback/overallFeedback/overallFeedbackForm/overallFeedbackForm.types";
+import UserPreferencesStateService from "src/userPreferences/UserPreferencesStateService";
+import AuthenticationServiceFactory from "src/auth/services/Authentication.service.factory";
+import AuthenticationStateService from "src/auth/services/AuthenticationState.service";
+import { TabiyaUser } from "src/auth/auth.types";
+import AuthenticationService from "src/auth/services/Authentication.service";
+
+// Mock authentication service to provide token validation and user info for API calls
+class MockAuthenticationService extends AuthenticationService {
+ private static instance: MockAuthenticationService;
+
+ private constructor() {
+ super();
+ }
+
+ static getInstance(): MockAuthenticationService {
+ if (!this.instance) {
+ this.instance = new this();
+ }
+ return this.instance;
+ }
+
+ async refreshToken(): Promise {}
+ cleanup(): void {}
+ async logout(): Promise {}
+ getUser(token: string): TabiyaUser | null {
+ return { id: "1", name: "Test User", email: "test@example.com" } as TabiyaUser;
+ }
+ getToken(): string {
+ return "foo token";
+ }
+ isTokenValid(token: string): { isValid: boolean; decodedToken: any; failureCause?: string } {
+ return { isValid: true, decodedToken: { sub: "1", email: "test@example.com" } };
+ }
+}
+
+// Mock questions config that matches the backend response structure
+const mockQuestionsConfig = {
+ yes_no_question: {
+ questionId: "yes_no_question",
+ question_text: "Is this a question?",
+ description: "Please answer yes or no",
+ type: QuestionType.YesNo,
+ show_comments_on: "No",
+ comment_placeholder: "Please provide comments",
+ },
+ // Add other required fields with dummy values
+ satisfaction_with_compass: {} as any,
+ perceived_bias: {} as any,
+ work_experience_accuracy: {} as any,
+ clarity_of_skills: {} as any,
+ incorrect_skills: {} as any,
+ missing_skills: {} as any,
+ interaction_ease: {} as any,
+ recommendation: {} as any,
+};
+
+const meta: Meta = {
+ title: "Feedback/OverallFeedback/QuestionTypes/YesNoQuestion",
+ component: YesNoQuestion,
+ tags: ["autodocs"],
+ args: {
+ notifyChange: (value, comments) => {
+ action("notifyChange")(value, comments);
+ },
+ },
+ parameters: {
+ mockData: [
+ // Mock the questions config endpoint that FeedbackProvider uses to fetch question data
+ {
+ url: getBackendUrl() + "/conversations/123/feedback/questions",
+ method: "GET",
+ status: 200,
+ response: mockQuestionsConfig,
+ },
+ // Mock the feedback submission endpoint that OverallFeedbackService uses to save ratings
+ {
+ url: getBackendUrl() + "/conversations/123/feedback",
+ method: "PATCH",
+ status: 200,
+ response: {
+ message: "Feedback submitted successfully",
+ },
+ },
+ ],
+ },
+ decorators: [
+ (Story) => {
+ // Mock session ID for API endpoint construction
+ const mockUserPrefsService = UserPreferencesStateService.getInstance();
+ mockUserPrefsService.getActiveSessionId = () => 123;
+
+ // Mock auth state for API authentication and user identification
+ const mockAuthStateService = AuthenticationStateService.getInstance();
+ mockAuthStateService.getToken = () => "foo token";
+ mockAuthStateService.getUser = () => ({ id: "1", name: "Test User", email: "test@example.com" } as TabiyaUser);
+
+ // Mock auth service factory to provide our mock auth service
+ AuthenticationServiceFactory.getCurrentAuthenticationService = () => MockAuthenticationService.getInstance();
+
+ return (
+
+
+
+ );
+ },
+ ],
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const ShowCommentsWhenYesSelected: Story = {
+ args: {
+ question_text: "Is this a question?",
+ question_id: "is_question",
+ showCommentsOn: YesNoEnum.Yes,
+ comment_placeholder: "Please provide comments",
+ description: "Test description"
+ },
+};
+
+export const ShowCommentsWhenNoSelected: Story = {
+ args: {
+ question_text: "Is this not a question?",
+ question_id: "is_not_question",
+ showCommentsOn: YesNoEnum.No,
+ comment_placeholder: "Please provide comments",
+ description: "Test description"
+ },
+};
diff --git a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/yesNoQuestion/YesNoQuestion.test.tsx b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/yesNoQuestion/YesNoQuestion.test.tsx
similarity index 87%
rename from frontend-new/src/feedback/overallFeedback/feedbackForm/components/yesNoQuestion/YesNoQuestion.test.tsx
rename to frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/yesNoQuestion/YesNoQuestion.test.tsx
index 4e838c7b1..7e25c690f 100644
--- a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/yesNoQuestion/YesNoQuestion.test.tsx
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/yesNoQuestion/YesNoQuestion.test.tsx
@@ -4,21 +4,23 @@ import "src/_test_utilities/consoleMock";
import YesNoQuestion, {
DATA_TEST_ID,
YesNoQuestionProps,
-} from "src/feedback/overallFeedback/feedbackForm/components/yesNoQuestion/YesNoQuestion";
+} from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/yesNoQuestion/YesNoQuestion";
import { render, screen } from "src/_test_utilities/test-utils";
import { fireEvent } from "@testing-library/react";
-import { QuestionType, YesNoEnum } from "src/feedback/overallFeedback/feedbackForm/feedbackForm.types";
-import { DATA_TEST_ID as COMMENT_TEXT_FIELD_TEST_ID } from "src/feedback/overallFeedback/feedbackForm/components/commentTextField/CommentTextField";
+import { QuestionType, YesNoEnum } from "src/feedback/overallFeedback/overallFeedbackForm/overallFeedbackForm.types";
+import { DATA_TEST_ID as COMMENT_TEXT_FIELD_TEST_ID } from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/commentTextField/CommentTextField";
describe("YesNoQuestion", () => {
// mock question
const mockQuestion: YesNoQuestionProps = {
type: QuestionType.YesNo,
- questionText: "Do you like the product?",
- questionId: "like_product",
+ question_text: "Do you like the product?",
+ question_id: "like_product",
ratingValue: true,
showCommentsOn: YesNoEnum.Yes,
notifyChange: jest.fn(),
+ description: "Test description",
+ comment_placeholder: "Test placeholder"
};
test("should render component successfully", () => {
diff --git a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/yesNoQuestion/YesNoQuestion.tsx b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/yesNoQuestion/YesNoQuestion.tsx
similarity index 85%
rename from frontend-new/src/feedback/overallFeedback/feedbackForm/components/yesNoQuestion/YesNoQuestion.tsx
rename to frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/yesNoQuestion/YesNoQuestion.tsx
index 42e7bc2a3..f9bf1051a 100644
--- a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/yesNoQuestion/YesNoQuestion.tsx
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/yesNoQuestion/YesNoQuestion.tsx
@@ -1,9 +1,9 @@
import React, { useState, useEffect, useCallback, useRef } from "react";
import { FormControl, FormControlLabel, FormLabel, Radio, RadioGroup, useTheme } from "@mui/material";
-import { BaseQuestion, YesNoEnum } from "src/feedback/overallFeedback/feedbackForm/feedbackForm.types";
-import QuestionText from "src/feedback/overallFeedback/feedbackForm/components/questionText/QuestionText";
-import CommentTextField from "src/feedback/overallFeedback/feedbackForm/components/commentTextField/CommentTextField";
-import { focusAndScrollToField } from "src/feedback/overallFeedback/feedbackForm/util";
+import { BaseQuestion, YesNoEnum } from "src/feedback/overallFeedback/overallFeedbackForm/overallFeedbackForm.types";
+import QuestionText from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/questionText/QuestionText";
+import CommentTextField from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/commentTextField/CommentTextField";
+import { focusAndScrollToField } from "src/feedback/overallFeedback/overallFeedbackForm/util";
export interface YesNoQuestionProps extends BaseQuestion {
ratingValue: boolean | null;
@@ -23,13 +23,13 @@ export const DATA_TEST_ID = {
};
const YesNoQuestion: React.FC = ({
- questionText,
- questionId,
+ question_text,
+ question_id,
ratingValue,
notifyChange,
comments,
showCommentsOn,
- placeholder,
+ comment_placeholder,
}) => {
// function to determine if comments should be shown
const shouldShowComments = useCallback(
@@ -75,11 +75,11 @@ const YesNoQuestion: React.FC = ({
data-testid={DATA_TEST_ID.FORM_CONTROL}
>
-
+ = ({
{showComments && (
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/questionsRenderer/QuestionRenderer.test.tsx b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/questionsRenderer/QuestionRenderer.test.tsx
new file mode 100644
index 000000000..a24f9ad15
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/questionsRenderer/QuestionRenderer.test.tsx
@@ -0,0 +1,132 @@
+// mute the console
+import "src/_test_utilities/consoleMock";
+
+import { fireEvent } from "@testing-library/react";
+import { render, screen } from "src/_test_utilities/test-utils";
+import QuestionRenderer, {
+ DATA_TEST_ID,
+} from "src/feedback/overallFeedback/overallFeedbackForm/components/questionsRenderer/QuestionRenderer";
+import { FeedbackItem } from "src/feedback/overallFeedback/overallFeedbackService/OverallFeedback.service.types";
+import { Question, QuestionType, RatingQuestion, CheckboxQuestionType, YesNoQuestion, YesNoEnum } from "src/feedback/overallFeedback/overallFeedbackForm/overallFeedbackForm.types";
+import {
+ DATA_TEST_ID as CUSTOM_RATING_DATA_TEST_ID,
+} from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/customRating/CustomRating";
+import {
+ DATA_TEST_ID as YES_NO_QUESTION_DATA_TEST_ID,
+} from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/yesNoQuestion/YesNoQuestion";
+import {
+ DATA_TEST_ID as CHECKBOX_QUESTION_DATA_TEST_ID,
+} from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/checkboxQuestion/CheckboxQuestion";
+
+describe("QuestionRenderer", () => {
+ const getMockQuestion = (questionType: T): Question => {
+ const baseQuestion = {
+ question_id: "q1",
+ type: questionType,
+ question_text: "Select options",
+ description: "Test description",
+ comment_placeholder: "Please provide comments",
+ };
+
+ switch (questionType) {
+ case QuestionType.Rating:
+ return {
+ ...baseQuestion,
+ type: QuestionType.Rating,
+ max_rating: 5,
+ low_rating_label: "Low",
+ high_rating_label: "High",
+ } as RatingQuestion;
+ case QuestionType.Checkbox:
+ return {
+ ...baseQuestion,
+ type: QuestionType.Checkbox,
+ options: {
+ option1: "option1",
+ option2: "option2"
+ }
+ } as CheckboxQuestionType;
+ case QuestionType.YesNo:
+ return {
+ ...baseQuestion,
+ type: QuestionType.YesNo,
+ show_comments_on: YesNoEnum.Yes,
+ } as YesNoQuestion;
+ default:
+ throw new Error(`Unsupported question type: ${questionType}`);
+ }
+ };
+
+ const mockAnswers: FeedbackItem[] = [
+ { question_id: "q1", simplified_answer: { selected_options_keys: ["option1"] } },
+ { question_id: "q2", simplified_answer: { rating_numeric: 3 } },
+ { question_id: "q3", simplified_answer: { rating_boolean: true, comment: "Yes comment" }},
+ ];
+
+ const mockOnChange = jest.fn();
+
+ test.each([
+ [QuestionType.Checkbox],
+ [QuestionType.Rating],
+ [QuestionType.YesNo],
+ ])("should render %s component successfully", (givenQuestionType) => {
+ // Given the component
+ const givenStepsComponent = (
+
+ );
+
+ // When the component is rendered
+ render(givenStepsComponent);
+
+ // THEN expect no errors or warning to have occurred
+ expect(console.error).not.toHaveBeenCalled();
+ expect(console.warn).not.toHaveBeenCalled();
+ // AND expect the component to be in the document
+ const stepsComponent = screen.getByTestId(DATA_TEST_ID.QUESTION_RENDERER);
+ expect(stepsComponent).toBeInTheDocument();
+ // AND the rating question to be in the document
+ const testIdMap = {
+ [QuestionType.YesNo]: YES_NO_QUESTION_DATA_TEST_ID.FORM_CONTROL,
+ [QuestionType.Checkbox]: CHECKBOX_QUESTION_DATA_TEST_ID.FORM_CONTROL,
+ [QuestionType.Rating]: CUSTOM_RATING_DATA_TEST_ID.CUSTOM_RATING_CONTAINER,
+ };
+ expect(screen.getByTestId(testIdMap[givenQuestionType])).toBeInTheDocument();
+ expect(stepsComponent).toMatchSnapshot();
+ });
+
+ test("should call onChange when checkbox question is answered", () => {
+ // Given the component is rendered
+ render();
+
+ // When the checkbox question is answered
+ const checkbox = screen.getAllByTestId(CHECKBOX_QUESTION_DATA_TEST_ID.CHECKBOX_OPTION)[0];
+ fireEvent.click(checkbox);
+
+ // Then expect onChange to be called
+ expect(mockOnChange).toHaveBeenCalled();
+ });
+
+ test("should call onChange when rating question is answered", () => {
+ // Given the component is rendered
+ render();
+
+ // When the rating question is answered
+ const starsIcon = screen.getAllByTestId(CUSTOM_RATING_DATA_TEST_ID.CUSTOM_RATING_ICON)[4];
+ fireEvent.click(starsIcon);
+
+ // Then expect onChange to be called
+ expect(mockOnChange).toHaveBeenCalled();
+ });
+
+ test("should call onChange when yes/no question is answered", () => {
+ // Given the component is rendered
+ render();
+
+ // When the yes/no question is answered
+ const radioNo = screen.getByTestId(YES_NO_QUESTION_DATA_TEST_ID.RADIO_NO);
+ fireEvent.click(radioNo);
+
+ // Then expect onChange to be called
+ expect(mockOnChange).toHaveBeenCalled();
+ });
+});
diff --git a/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/questionsRenderer/QuestionRenderer.tsx b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/questionsRenderer/QuestionRenderer.tsx
new file mode 100644
index 000000000..8b7830e78
--- /dev/null
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/questionsRenderer/QuestionRenderer.tsx
@@ -0,0 +1,103 @@
+import React from "react";
+import { Box } from "@mui/material";
+import { Question, QuestionType } from "src/feedback/overallFeedback/overallFeedbackForm/overallFeedbackForm.types";
+import {
+ SimplifiedAnswer,
+ FeedbackItem,
+} from "src/feedback/overallFeedback/overallFeedbackService/OverallFeedback.service.types";
+import CustomRating from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/customRating/CustomRating";
+import YesNoQuestion from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/yesNoQuestion/YesNoQuestion";
+import CheckboxQuestion from "src/feedback/overallFeedback/overallFeedbackForm/components/formContent/questionTypes/checkboxQuestion/CheckboxQuestion";
+import { useIsSmallOrShortScreen } from "src/feedback/overallFeedback/overallFeedbackForm/useIsSmallOrShortScreen";
+
+interface QuestionRendererProps {
+ question: Question;
+ feedbackItems: FeedbackItem[];
+ onChange: (data: FeedbackItem) => void;
+}
+
+const uniqueId = "3bf900a2-f9fa-4b28-8c20-8bc21570635c";
+
+export const DATA_TEST_ID = {
+ QUESTION_RENDERER: `question-renderer-${uniqueId}`,
+};
+
+const QuestionRenderer: React.FC = ({ question, feedbackItems, onChange }) => {
+ const isSmallOrShortScreen = useIsSmallOrShortScreen()
+
+ const getAnswerByQuestionId = (questionId: string): SimplifiedAnswer | undefined => {
+ return feedbackItems.find((item: FeedbackItem) => item.question_id === questionId)?.simplified_answer;
+ };
+
+ const handleInputChange = (questionId: string, value: SimplifiedAnswer) => {
+ const formattedData: FeedbackItem = {
+ question_id: questionId,
+ simplified_answer: value,
+ };
+ onChange(formattedData);
+ };
+
+ const answer = getAnswerByQuestionId(question.question_id) || {};
+
+ return (
+ (isSmallOrShortScreen ? theme.tabiyaSpacing.xl * 3 : theme.tabiyaSpacing.xl * 1.2)}
+ data-testid={DATA_TEST_ID.QUESTION_RENDERER}
+ >
+
+ {question.type === QuestionType.Checkbox && (
+
+ handleInputChange(question.question_id, { selected_options_keys: selectedOptions, comment: comments })
+ }
+ comments={answer.comment ?? ""}
+ comment_placeholder={question.comment_placeholder ?? null}
+ />
+ )}
+ {question.type === QuestionType.Rating && (
+
+ handleInputChange(question.question_id, { rating_numeric: value, comment: comments })
+ }
+ lowRatingLabel={question.low_rating_label ?? ""}
+ highRatingLabel={question.high_rating_label ?? ""}
+ comments={answer.comment ?? ""}
+ maxRating={question.max_rating ?? 5}
+ comment_placeholder={question.comment_placeholder ?? null}
+ />
+ )}
+ {question.type === QuestionType.YesNo && (
+
+ handleInputChange(question.question_id, { rating_boolean: value, comment: comments })
+ }
+ showCommentsOn={question.show_comments_on ?? undefined}
+ comments={answer.comment ?? ""}
+ comment_placeholder={question.comment_placeholder ?? null}
+ />
+ )}
+
+
+ );
+};
+
+export default QuestionRenderer;
diff --git a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/stepsComponent/__snapshots__/StepsComponent.test.tsx.snap b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/questionsRenderer/__snapshots__/QuestionRenderer.test.tsx.snap
similarity index 89%
rename from frontend-new/src/feedback/overallFeedback/feedbackForm/components/stepsComponent/__snapshots__/StepsComponent.test.tsx.snap
rename to frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/questionsRenderer/__snapshots__/QuestionRenderer.test.tsx.snap
index edf51e017..da49c44cd 100644
--- a/frontend-new/src/feedback/overallFeedback/feedbackForm/components/stepsComponent/__snapshots__/StepsComponent.test.tsx.snap
+++ b/frontend-new/src/feedback/overallFeedback/overallFeedbackForm/components/questionsRenderer/__snapshots__/QuestionRenderer.test.tsx.snap
@@ -1,9 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`StepsComponent should render component successfully 1`] = `
+exports[`QuestionRenderer should render checkbox component successfully 1`] = `