Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions frontend/src/features/informationTabs/InformationTabs.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { render, screen } from "@testing-library/react";
import type { ReactNode } from "react";
import InformationTabs from "./InformationTabs";

jest.mock("./questionTab/QuestionTab", () => ({
__esModule: true,
default: () => <div data-testid="question-tab" />,
}));

describe("InformationTabs", () => {
const defaultProps = {
submissionResults: null,
questionSelected: true,
questionIndex: 1,
setQuestionIndex: jest.fn(),
questionType: "experiment" as const,
setQuestionType: jest.fn(),
questionView: "root" as any,
setQuestionView: jest.fn(),
onSubmit: jest.fn(async () => true),
onSubmitAtLine: jest.fn(async () => true),
setSubmissionResults: jest.fn(),
onClearCanvas: jest.fn(),
onRestoreCanvas: jest.fn(),
currentCanvasState: { elements: [], ids: [], classes: [] },
masterErrorList: {} as any,
elements: [],
setElements: jest.fn(),
onOpenEditor: jest.fn(),
isSandboxMode: false,
onQuestionDataChange: jest.fn(),
tabScrollPositions: { question: 0 },
setTabScrollPositions: jest.fn(),
fontScale: 1,
};

it("renders the question section and feedback section in one panel", () => {
const { container } = render(<InformationTabs {...defaultProps} />);

expect(screen.getByTestId("question-tab")).toBeInTheDocument();
expect(screen.getByRole("heading", { name: /feedback/i })).toBeInTheDocument();
expect(container.querySelector("[role='tablist']")).not.toBeInTheDocument();

const questionNode = screen.getByTestId("question-tab");
const feedbackHeading = screen.getByRole("heading", { name: /feedback/i });
expect(
questionNode.compareDocumentPosition(feedbackHeading) & Node.DOCUMENT_POSITION_FOLLOWING
).toBeTruthy();
});
});
121 changes: 35 additions & 86 deletions frontend/src/features/informationTabs/InformationTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import styles from "./InformationTabs.module.css";

interface InformationTabsProps {
submissionResults: SubmissionResult | null;
activeTab: Tab;
setActive: (tab: Tab) => void;
questionSelected: boolean;
questionIndex: number | null;
setQuestionIndex: (index: number | null) => void;
Expand All @@ -35,8 +33,6 @@ interface InformationTabsProps {

export default function InformationTabs({
submissionResults,
activeTab,
setActive,
questionSelected,
questionIndex,
setQuestionIndex,
Expand Down Expand Up @@ -66,60 +62,30 @@ export default function InformationTabs({
const lastSubmitLineRef = useRef<LastLineCtx>(null);
const [lastSubmitLine, setLastSubmitLine] = useState<LastLineCtx>(null);

const saveCurrentScroll = () => {
if (tabBodyRef.current) {
setTabScrollPositions((prev) => ({
...prev,
[activeTab]: tabBodyRef.current!.scrollTop,
}));
}
};

// Restore scroll position when active tab changes or on mount
// Restore scroll position on mount
useEffect(() => {
if (tabBodyRef.current) {
tabBodyRef.current.scrollTop = tabScrollPositions[activeTab];
tabBodyRef.current.scrollTop = tabScrollPositions["question"];
}
}, [activeTab, tabScrollPositions]);
}, []);

// Save scroll position on unmount (panel close)
const activeTabRef = useRef(activeTab);
activeTabRef.current = activeTab;
useEffect(() => {
const el = tabBodyRef.current;
return () => {
if (el) {
setTabScrollPositions((prev) => ({
...prev,
[activeTabRef.current]: el.scrollTop,
["question"]: el.scrollTop,
}));
}
};
}, [setTabScrollPositions]);

const renderTabButton = (tab: Tab, label: string) => (
<button
key={tab}
type="button"
className={`${styles.tabBtn} ${activeTab === tab ? styles.active : ""}`}
onClick={() => {
saveCurrentScroll();
setActive(tab);
}}
aria-pressed={activeTab === tab}
>
{label}
</button>
);

const handleSubmit = async () => {
lastSubmitLineRef.current = null;
setLastSubmitLine(null);
const success = await onSubmit();
if (success) {
saveCurrentScroll();
setActive("feedback");
}
return success;
};

Expand All @@ -141,56 +107,39 @@ export default function InformationTabs({
return (
<div className={styles.containerWrapper}>
<div className={styles.container}>
<nav className={styles.tabHeaders} role="tablist">
{renderTabButton("question", "Question")}
{renderTabButton("feedback", "Feedback")}
</nav>

<div className={styles.tabBody} ref={tabBodyRef}>
<div
className={activeTab === "question" ? "" : styles.hidden}
role="tabpanel"
aria-hidden={activeTab !== "question"}
>
<QuestionTab
questionIndex={questionIndex}
setQuestionIndex={setQuestionIndex}
questionType={questionType}
setQuestionType={setQuestionType}
questionView={questionView}
setQuestionView={setQuestionView}
onSubmit={handleSubmit}
onSubmitAtLine={handleSubmitAtLine}
setSubmissionResults={setSubmissionResults}
onClearCanvas={onClearCanvas}
onRestoreCanvas={onRestoreCanvas}
currentCanvasState={currentCanvasState}
onQuestionDataChange={onQuestionDataChange}
isSandboxMode={isSandboxMode}
fontScale={fontScale}
/>
</div>
<QuestionTab
questionIndex={questionIndex}
setQuestionIndex={setQuestionIndex}
questionType={questionType}
setQuestionType={setQuestionType}
questionView={questionView}
setQuestionView={setQuestionView}
onSubmit={handleSubmit}
onSubmitAtLine={handleSubmitAtLine}
setSubmissionResults={setSubmissionResults}
onClearCanvas={onClearCanvas}
onRestoreCanvas={onRestoreCanvas}
currentCanvasState={currentCanvasState}
onQuestionDataChange={onQuestionDataChange}
isSandboxMode={isSandboxMode}
fontScale={fontScale}
/>

<div
className={activeTab === "feedback" ? "" : styles.hidden}
role="tabpanel"
aria-hidden={activeTab !== "feedback"}
>
<FeedbackTab
submissionResults={submissionResults}
masterErrorList={masterErrorList}
elements={elements}
setElements={setElements}
onOpenEditor={onOpenEditor}
questionSelected={questionSelected}
questionIndex={questionIndex}
questionType={questionType}
isSandboxMode={!isSandboxMode}
onResubmit={handleResubmit}
resubmitLine={lastSubmitLine}
fontScale={fontScale}
/>
</div>
<FeedbackTab
submissionResults={submissionResults}
masterErrorList={masterErrorList}
elements={elements}
setElements={setElements}
onOpenEditor={onOpenEditor}
questionSelected={questionSelected}
questionIndex={questionIndex}
questionType={questionType}
isSandboxMode={!isSandboxMode}
onResubmit={handleResubmit}
resubmitLine={lastSubmitLine}
fontScale={fontScale}
/>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
font-size: 1rem;
font-weight: 700;
color: var(--text-tertiary);
margin: 0 0 18px 0;
margin: 32px 0 18px 0;
letter-spacing: 0.08em;
text-transform: uppercase;
border-bottom: none;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { render, screen } from "@testing-library/react";
import FeedbackTab from "./FeedbackTab";

describe("FeedbackTab", () => {
const defaultProps = {
submissionResults: null,
questionSelected: true,
questionIndex: 1,
questionType: "experiment" as const,
masterErrorList: {} as any,
elements: [],
setElements: jest.fn(),
onOpenEditor: jest.fn(),
isSandboxMode: false,
onResubmit: jest.fn(async () => true),
resubmitLine: null,
fontScale: 1,
};

it("renders the experiment feedback heading with question number", () => {
render(<FeedbackTab {...defaultProps} />);

expect(
screen.getByRole("heading", {
name: /feedback - experiment question 1/i,
})
).toBeInTheDocument();
});
});
3 changes: 0 additions & 3 deletions frontend/src/features/memoryModelEditor/MemoryModelEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,6 @@ export default function MemoryModelEditor({
elements: state.elements,
setElements: state.setElements,
setSubmissionResults: state.setSubmissionResults,
setActiveInfoTab: state.setActiveInfoTab,
});

useCanvasLocalStorage({
Expand Down Expand Up @@ -581,8 +580,6 @@ export default function MemoryModelEditor({
>
<InformationTabs
submissionResults={state.submissionResults}
activeTab={state.activeInfoTab}
setActive={state.setActiveInfoTab}
questionSelected={state.selectedQuestionIndex !== null}
questionIndex={state.selectedQuestionIndex}
setQuestionIndex={state.setSelectedQuestionIndex}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useRef } from "react";
import { CanvasElement, SubmissionResult, Tab } from "../../shared/types";
import { CanvasElement, SubmissionResult } from "../../shared/types";
import { submitCanvas, submitCanvasAtLine } from "../../validationServices/questionValidationService";
import { applyFeedbackErrors, clearFeedbackErrors } from "../utils/feedbackErrorMapper";

Expand All @@ -9,7 +9,6 @@ interface UseCanvasSubmissionParams {
elements: CanvasElement[];
setElements: React.Dispatch<React.SetStateAction<CanvasElement[]>>;
setSubmissionResults: (results: SubmissionResult | null) => void;
setActiveInfoTab: (tab: Tab) => void;
}

interface UseCanvasSubmissionReturn {
Expand All @@ -28,14 +27,12 @@ export function useCanvasSubmission({
elements,
setElements,
setSubmissionResults,
setActiveInfoTab,
}: UseCanvasSubmissionParams): UseCanvasSubmissionReturn {
const idxRef = useRef(selectedQuestionIndex);
const typeRef = useRef(selectedQuestionType);
const elsRef = useRef(elements);
const setElsRef = useRef(setElements);
const setResultsRef = useRef(setSubmissionResults);
const setTabRef = useRef(setActiveInfoTab);

useEffect(() => {
idxRef.current = selectedQuestionIndex;
Expand All @@ -52,9 +49,6 @@ export function useCanvasSubmission({
useEffect(() => {
setResultsRef.current = setSubmissionResults;
}, [setSubmissionResults]);
useEffect(() => {
setTabRef.current = setActiveInfoTab;
}, [setActiveInfoTab]);

const handleCanvasSubmit = useCallback(async (): Promise<boolean> => {
const index = idxRef.current;
Expand All @@ -64,7 +58,6 @@ export function useCanvasSubmission({
if (index === null || qtype === null) {
console.warn("Cannot submit: question index or type is null");
setResultsRef.current(null);
setTabRef.current("feedback");
return false;
}

Expand All @@ -75,7 +68,6 @@ export function useCanvasSubmission({
if (validElements.length === 0) {
console.warn("No valid elements to submit");
setResultsRef.current(null);
setTabRef.current("feedback");
return false;
}

Expand All @@ -101,20 +93,15 @@ export function useCanvasSubmission({
// Determine if submission was correct based on result
const isCorrect = determineIfCorrect(result);

// Switch to feedback tab to show submission results
setTabRef.current("feedback");

return isCorrect;
} else {
console.warn("Submission returned undefined result");
setResultsRef.current(null);
setTabRef.current("feedback");
return false;
}
} catch (error) {
console.error("Canvas submission failed:", error);
setResultsRef.current(null);
setTabRef.current("feedback");
return false;
}
}, []);
Expand All @@ -127,7 +114,6 @@ export function useCanvasSubmission({
if (index === null || qtype === null) {
console.warn("Cannot submit at line: question index or type is null");
setResultsRef.current(null);
setTabRef.current("feedback");
return false;
}

Expand All @@ -137,7 +123,6 @@ export function useCanvasSubmission({
if (validElements.length === 0) {
console.warn("No valid elements to submit at line");
setResultsRef.current(null);
setTabRef.current("feedback");
return false;
}

Expand All @@ -157,18 +142,15 @@ export function useCanvasSubmission({
setResultsRef.current(result);

const isCorrect = determineIfCorrect(result);
setTabRef.current("feedback");
return isCorrect;
} else {
console.warn("submitAtLine returned undefined result");
setResultsRef.current(null);
setTabRef.current("feedback");
return false;
}
} catch (error) {
console.error("Canvas submitAtLine failed:", error);
setResultsRef.current(null);
setTabRef.current("feedback");
return false;
}
}, []);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ export function useMemoryModelEditorState(sandbox: boolean) {
// Scroll positions for info panel tabs (persists across panel close/open)
const [tabScrollPositions, setTabScrollPositions] = useState<Record<Tab, number>>({
question: 0,
feedback: 0,
});

// Panel state
Expand Down
Loading