diff --git a/package-lock.json b/package-lock.json index 3ef4561..19bc6dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "@types/react-router-dom": "^5.3.3", "axios": "^1.4.0", "bootstrap": "^5.3.3", - "chart.js": "^3.7.0", + "chart.js": "^4.1.1", "formik": "^2.2.9", "jquery": "^3.7.1", "jwt-decode": "^3.1.2", @@ -38,7 +38,7 @@ "react-redux": "^8.0.5", "react-router-dom": "^6.11.1", "react-scripts": "^5.0.1", - "recharts": "^2.12.3", + "recharts": "^2.0.0", "redux-persist": "^6.0.0", "sass": "^1.62.1", "save": "^2.9.0", @@ -3045,6 +3045,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", + "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==", + "license": "MIT" + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -5760,9 +5766,16 @@ } }, "node_modules/chart.js": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.7.0.tgz", - "integrity": "sha512-31gVuqqKp3lDIFmzpKIrBeum4OpZsQjSIAqlOpgjosHDJZlULtvwLEZKtEhIAZc7JMPaHlYMys40Qy9Mf+1AAg==" + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.5.tgz", + "integrity": "sha512-CVVjg1RYTJV9OCC8WeJPMx8gsV8K6WIyIEQUE3ui4AR9Hfgls9URri6Ja3hyMVBbTF8Q2KFa19PE815gWcWhng==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } }, "node_modules/check-types": { "version": "11.2.3", @@ -16603,6 +16616,28 @@ "node": ">= 0.8" } }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/void-elements": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", diff --git a/src/pages/EditQuestionnaire/Questionnaire.test.tsx b/src/pages/EditQuestionnaire/Questionnaire.test.tsx new file mode 100644 index 0000000..b665b0e --- /dev/null +++ b/src/pages/EditQuestionnaire/Questionnaire.test.tsx @@ -0,0 +1,197 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import Questionnaire from './Questionnaire'; +import '@testing-library/jest-dom'; + +describe('Questionnaire Component', () => { + test('renders questionnaire title', () => { + render(); + expect(screen.getByText('Edit Teammate Review')).toBeInTheDocument(); + }); + + test('displays min and max item score inputs with default values', () => { + render(); + + // Adjusting selectors to ensure they match the rendered input fields in Questionnaire component + const minScoreInput = screen.getByDisplayValue('0'); // Default minScore value is 0 + const maxScoreInput = screen.getByDisplayValue('5'); // Default maxScore value is 5 + + expect(minScoreInput).toBeInTheDocument(); + expect(maxScoreInput).toBeInTheDocument(); + }); + + test('toggles privacy setting', () => { + render(); + const privacyCheckbox = screen.getByLabelText(/Is this Teammate review private:/i); + + expect(privacyCheckbox).not.toBeChecked(); // Confirm initial state is unchecked + fireEvent.click(privacyCheckbox); + expect(privacyCheckbox).toBeChecked(); // After click, checkbox should be checked + }); + + test('updates min and max item scores', () => { + render(); + + // Query by placeholder text instead if labels are not accessible + const minInput = screen.getByDisplayValue('0'); + const maxInput = screen.getByDisplayValue('5'); + + // Change min and max score values + fireEvent.change(minInput, { target: { value: '1' } }); + fireEvent.change(maxInput, { target: { value: '10' } }); + + expect(minInput).toHaveValue(1); // New min score should be 1 + expect(maxInput).toHaveValue(10); // New max score should be 10 + }); + + test('renders questionnaire items with default data', () => { + render(); + + const questionElements = screen.getAllByRole('textbox'); + expect(questionElements.length).toBeGreaterThan(0); // Check if at least one question is rendered + expect(questionElements[0]).toHaveValue('How many times was this person late to meetings?'); + }); + + test('allows editing of question fields', () => { + render(); + const questionInput = screen.getAllByRole('textbox')[0]; + + // Edit the first question text + fireEvent.change(questionInput, { target: { value: 'Updated Question Text' } }); + expect(questionInput).toHaveValue('Updated Question Text'); + }); + + test('adds a new question', () => { + render(); + const addButton = screen.getByRole('button', { name: /Add Question/i }); + + // Simulate adding a question + fireEvent.click(addButton); + + const questionInputs = screen.getAllByRole('textbox'); + expect(questionInputs.length).toBeGreaterThan(1); // Check for an additional question input + }); + + test('exports questionnaire data as JSON', () => { + render(); + const exportLink = screen.getByText(/Export Questionnaire/i); + + // Mock the URL.createObjectURL function + const createObjectURL = jest.fn(); + global.URL.createObjectURL = createObjectURL; + + fireEvent.click(exportLink); + + expect(createObjectURL).toHaveBeenCalled(); // Verify that createObjectURL was triggered for export + }); + + test('opens import modal and handles import data', () => { + render(); + const importLink = screen.getByText(/Import Questionnaire/i); + fireEvent.click(importLink); + + // Verify if import modal opens + expect(screen.getByRole('dialog')).toBeInTheDocument(); + }); + + test("renders title and main controls", () => { + render(); + + expect(screen.getByText("Edit Teammate Review")).toBeInTheDocument(); + expect(screen.getByLabelText(/Min item score:/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/Max item score:/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/Is this Teammate review private:/i)).toBeInTheDocument(); + }); + + test("renders initial items", () => { + render(); + + // Check for specific question text + expect(screen.getByText("How many times was this person late to meetings?")).toBeInTheDocument(); + expect(screen.getAllByRole("textbox").length).toBeGreaterThan(0); // Ensure textboxes for questions are rendered + }); + + test("removes a question", () => { + render(); + const removeButton = screen.getAllByRole("button", { name: /Remove/i })[0]; + + fireEvent.click(removeButton); + + const questions = screen.getAllByRole("textbox"); + expect(questions.length).toBeLessThan(11); // Assuming initially 11 questions + }); + + test("opens and closes import modal", () => { + render(); + const importButton = screen.getByRole("button", { name: /Import Questionnaire/i }); + + fireEvent.click(importButton); + expect(screen.getByRole("dialog")).toBeInTheDocument(); // Check modal visibility + + const closeButton = screen.getByRole("button", { name: /Close/i }); + fireEvent.click(closeButton); + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); // Modal should disappear + }); + + test("opens and closes export modal", () => { + render(); + const exportButton = screen.getByRole("button", { name: /Export Questionnaire/i }); + + fireEvent.click(exportButton); + expect(screen.getByRole("dialog")).toBeInTheDocument(); // Check modal visibility + + const closeButton = screen.getByRole("button", { name: /Close/i }); + fireEvent.click(closeButton); + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); // Modal should disappear + }); + + test("updates question details", () => { + render(); + const questionTextarea = screen.getAllByRole("textbox")[0]; + + fireEvent.change(questionTextarea, { target: { value: "Updated Question Text" } }); + expect(questionTextarea).toHaveValue("Updated Question Text"); + }); + + test("updates item type dropdown", () => { + render(); + const dropdown = screen.getAllByRole("combobox")[0]; + + fireEvent.change(dropdown, { target: { value: "Dropdown" } }); + expect(dropdown).toHaveValue("Dropdown"); + }); + + test("renders correctly with empty questionnaire data", () => { + render(); + // Mock an empty questionnaire + const emptyData = { title: "", data: [] }; + fireEvent.change(screen.getByRole("textbox", { name: /Title/i }), { target: { value: emptyData.title } }); + + expect(screen.queryAllByRole("textbox").length).toBe(1); // Only title input should be visible + }); + + test("removes all questions and displays appropriate message", () => { + render(); + + const removeButtons = screen.getAllByRole("button", { name: /Remove/i }); + removeButtons.forEach((btn) => fireEvent.click(btn)); + + expect(screen.getByText(/No questions available/i)).toBeInTheDocument(); // Assuming a placeholder is displayed for empty state + }); + + test("detects duplicate sequence numbers when adding a question", () => { + render(); + + const addButton = screen.getByRole("button", { name: /Add Question/i }); + fireEvent.click(addButton); + + const sequenceInput = screen.getAllByRole("textbox")[0]; // Assume the sequence field + fireEvent.change(sequenceInput, { target: { value: "1.0" } }); // Duplicate sequence + + fireEvent.click(addButton); + + expect(screen.getByText(/Duplicate sequence number detected/i)).toBeInTheDocument(); // Error message for duplicates + }); + + +}); diff --git a/src/pages/EditQuestionnaire/Questionnaire.tsx b/src/pages/EditQuestionnaire/Questionnaire.tsx index 29a9d6c..aca934c 100644 --- a/src/pages/EditQuestionnaire/Questionnaire.tsx +++ b/src/pages/EditQuestionnaire/Questionnaire.tsx @@ -5,22 +5,28 @@ import ExportModal from "./ExportModal"; interface ImportedData { title: string; data: Array<{ - seq: number; - question: string; - type: string; - weight: number; - text_area_size: string; - max_label: string; - min_label: string; + id: number, + sequence: number; // defines the order of questions + question: string; // the question itself + type: string; // type of item which can be Criterion, DropDown, multiple choice etc.. + weight: number; // defines the weight of the question + text_area_size: string; // size of the text area in rows and columns + max_label: string; // the maximum value, differs according to the type + min_label: string; // the minimum value, differs according to the type }>; } +// Various Types of items available +const itemTypeArray = ['Criterion','Scale','Cake','Dropdown','Checkbox','TextArea','TextField','UploadFile','SectionHeader','TableHeader','ColumnHeader']; + const Questionnaire = () => { - const sample_questionnaire = { +// Sample data for initial questionnaire + const initialQuestionnaire = { title: "Edit Teammate Review", data: [ { - seq: 1.0, + id:1, + sequence: 1.0, question: "How many times was this person late to meetings?", type: "Criterion", weight: 1, @@ -29,7 +35,8 @@ const Questionnaire = () => { min_label: "almost always", }, { - seq: 2.0, + id:2, + sequence: 2.0, question: "How many times did this person not show up?", type: "Criterion", weight: 1, @@ -38,7 +45,8 @@ const Questionnaire = () => { min_label: "almost always", }, { - seq: 3.0, + id:3, + sequence: 3.0, question: "How much did this person offer to do in this project?", type: "Criterion", weight: 1, @@ -47,7 +55,8 @@ const Questionnaire = () => { min_label: "20%-0%", }, { - seq: 4.0, + id:4, + sequence: 4.5, question: "What fraction of the work assigned to this person did s(he) do?", type: "Criterion", weight: 1, @@ -56,16 +65,8 @@ const Questionnaire = () => { min_label: "20%-0%", }, { - seq: 4.5, - question: "Did this person do assigned work on time?", - type: "Criterion", - weight: 1, - text_area_size: "50, 30", - max_label: "always", - min_label: "never", - }, - { - seq: 5.0, + id:5, + sequence: 5.0, question: "How much initiative did this person take on this project?", type: "Criterion", weight: 1, @@ -74,7 +75,8 @@ const Questionnaire = () => { min_label: "total deadbeat", }, { - seq: 6.0, + id:6, + sequence: 6.0, question: "Did this person try to avoid doing any task that was necessary?", type: "Criterion", weight: 1, @@ -83,7 +85,8 @@ const Questionnaire = () => { min_label: "absolutely", }, { - seq: 7.0, + id:7, + sequence: 7.0, question: "How many of the useful ideas did this person come up with?", type: "Criterion", weight: 1, @@ -92,7 +95,8 @@ const Questionnaire = () => { min_label: "20%-0%", }, { - seq: 8.0, + id:8, + sequence: 8.0, question: "What fraction of the coding did this person do?", type: "Criterion", weight: 1, @@ -101,7 +105,8 @@ const Questionnaire = () => { min_label: "20%-0%", }, { - seq: 9.0, + id:9, + sequence: 9.0, question: "What fraction of the documentation did this person write?", type: "Criterion", weight: 1, @@ -110,7 +115,8 @@ const Questionnaire = () => { min_label: "20%-0%", }, { - seq: 11.0, + id:10, + sequence: 10.0, question: "How important is this person to the team?", type: "Criterion", weight: 1, @@ -118,75 +124,108 @@ const Questionnaire = () => { max_label: "indispensable", min_label: "redundant", }, + // ... additional questions omitted for brevity ], }; + // State hooks for questionnaire settings + const [selectedItemType, setSelectedItemType] = useState(''); // State to track selected value + const [itemQuantity,setItemQuantity] = useState("1"); const [minScore, setMinScore] = useState(0); const [maxScore, setMaxScore] = useState(5); - const [isPrivate, setIsPrivate] = useState(false); + const [isReviewPrivate, setIsReviewPrivate] = useState(false); - const [questionnaireData, setQuestionnaireData] = useState(sample_questionnaire); - const [showImportModal, setShowImportModal] = useState(false); - const [showExportModal, setShowExportModal] = useState(false); + // State hooks for questionnaire data and modals + const [questionnaireData, setQuestionnaireData] = useState(initialQuestionnaire); + const [isImportModalVisible, setImportModalVisible] = useState(false); + const [isExportModalVisible, setExportModalVisible] = useState(false); // Function to export questionnaire data const exportQuestionnaire = () => { const dataToExport = JSON.stringify(questionnaireData); const blob = new Blob([dataToExport], { type: "application/json" }); const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = "questionnaire.json"; - a.click(); + const downloadLink = document.createElement("a"); + downloadLink.href = url; + downloadLink.download = "questionnaire.json"; + downloadLink.click(); URL.revokeObjectURL(url); }; - // Function to handle imported data - const handleFileChange = (importedData: ImportedData) => { + // Function to handle imported data to update questionnaire + const handleImportData = (importedData: ImportedData) => { setQuestionnaireData(importedData); }; + // Function to add a new item to the questionnaire + const handleAddItem = ()=> { + let updatedData = questionnaireData.data; + for (let i=0; i< parseInt(itemQuantity); i++) { + const newQuestion = { + id: questionnaireData.data.slice(-1)[0].id + 1, + sequence: questionnaireData.data.slice(-1)[0].id + 1, + question: "Type your question", + type: selectedItemType, + weight: 1, + text_area_size: "50, 30", + max_label: "max value", + min_label: "min value" + }; + updatedData.push(newQuestion); + } + setQuestionnaireData({...questionnaireData, data: updatedData }); + } + + // function to handle the dropdown change event + const handleDropdownChange = (event: React.ChangeEvent) => { + setSelectedItemType(event.target.value); // Safely access the value + }; + // function for removing the item + const handleRemoveItem = (seq: number)=>{ + let updatedData = questionnaireData.data.filter(q=> +q.id!== seq); + setQuestionnaireData({...questionnaireData, data: [...updatedData] }); + } return ( -
-
-

{sample_questionnaire.title}

-
-
+
+
+

{questionnaireData.title}

+
+ {/* Min Score Input */} +
Min item score: setMinScore(parseInt(e.target.value, 10))} + min={0} // Using parseInt to convert the input value to a number >
-
-
-
+ {/* Max Score Input */} +
Max item score: setMaxScore(parseInt(e.target.value, 10))} + min={0} // Using parseInt to convert the input value to a number > -
-
-
+ {/* Privacy Toggle */} +
Is this Teammate review private:{' '} setIsPrivate(!isPrivate)} + checked={isReviewPrivate} + onChange={() => setIsReviewPrivate(!isReviewPrivate)} /> -
-
-
+ {/* Update Parameters Button */} +
-
+

- -
-
Seq
-
Question
-
Type
-
Weight
-
Text_area_size
-
Max_label
-
Min_label
-
Action
+ + {/* Display questionnaire items */} +
+
Sequence
+
Question
+
Type
+
Weight
+
Text Area Size
+
Max Label
+
Min Label
+
Action
- {sample_questionnaire.data.map((item) => { + {/* Iterate over questions */} + {questionnaireData.data.map((item) => { return ( -
-
+
+ {/* Sequence number */} +
-
- + {/* The Question Text field */} +
+
-
+ {/* The Item type dropdown */} +
+ {/* The weight of the item chosen */}
+ {/* The text-area size of the item being added */}
+ {/* The maximum label you want to attach to that item */}
+ {/* The minimum label you want to attach to that item */}
-
+
+ {/* Remove item button */}
); })}
+ {/* Add new item inputs */}

- + setItemQuantity(e.target.value)}>

@@ -305,18 +351,14 @@ const Questionnaire = () => {

- + {/* Iterating item array for getting all the various type of items as options to select from dropdown */} + {itemTypeArray.map((itemType) => + + ) + }
@@ -324,69 +366,74 @@ const Questionnaire = () => { question(s)

+ {/* Add a new question button */}
- +

-
- -
-
-
-
- -
+ {/* Save all questions button */} + {/*
+ +
*/} + {/* Edit/View Advice button */} + {/*
+ +
*/}

-
-
- setShowImportModal(true)} - > - Import Questionnaire - {" "} - | - setShowExportModal(true)} + {/* Import/Export Section */} +
+ {/* button for Importing Questionnaire*/} +
+ {" "} +
+ {/* button for Importing Questionnaire*/} +
{/* Render import and export modals conditionally */} - {showImportModal && ( + {isImportModalVisible && ( setShowImportModal(false)} - onImport={handleFileChange} + onClose={() => setImportModalVisible(false)} + onImport={handleImportData} /> )} - {showExportModal && ( + {isExportModalVisible && ( setShowExportModal(false)} + onClose={() => setExportModalVisible(false)} onExport={exportQuestionnaire} /> )}