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
59 changes: 59 additions & 0 deletions frontend/src/features/canvas/components/CallStack.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React from "react";
import { fireEvent, render, screen } from "@testing-library/react";
import CallStack from "./CallStack";

function renderCallStack(
overrides: Partial<React.ComponentProps<typeof CallStack>> = {}
) {
return render(
<svg>
<CallStack
frames={[]}
selected={null}
onSelect={jest.fn()}
onReorder={jest.fn()}
{...overrides}
/>
</svg>
);
}

describe("CallStack help icon", () => {
it("shows a help icon labelled for the MemoryViz 'Call Stack' title", () => {
renderCallStack({ visualStyle: "memoryviz" });

expect(
screen.getByRole("button", { name: "Help: Call Stack" })
).toBeInTheDocument();
});

it("shows a help icon labelled for the Python Tutor 'Frames' title", () => {
renderCallStack({ visualStyle: "pythonTutor" });

expect(
screen.getByRole("button", { name: "Help: Frames" })
).toBeInTheDocument();
});

it("opens a dialog explaining the call stack on click", () => {
renderCallStack({ visualStyle: "memoryviz" });

fireEvent.click(screen.getByRole("button", { name: "Help: Call Stack" }));

expect(screen.getByRole("dialog")).toBeInTheDocument();
expect(screen.getByText("Call Stack", { selector: "h4" })).toBeInTheDocument();
expect(
screen.getByText(/currently active function calls/i)
).toBeInTheDocument();
});

it("closes the dialog when the close button is clicked", () => {
renderCallStack({ visualStyle: "memoryviz" });

fireEvent.click(screen.getByRole("button", { name: "Help: Call Stack" }));
expect(screen.getByRole("dialog")).toBeInTheDocument();

fireEvent.click(screen.getByRole("button", { name: "Close" }));
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
});
14 changes: 14 additions & 0 deletions frontend/src/features/canvas/components/CallStack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from "../../shared/types";
import { BoxDimensions } from "../utils/box.types";
import CanvasBox from "./CanvasBox";
import HelpIcon from "../../shared/components/HelpIcon";
import styles from "./CallStack.module.css";
import {
DEFAULT_BOX_WIDTH,
Expand Down Expand Up @@ -389,6 +390,19 @@ const CallStack: React.FC<CallStackProps> = ({
{visualStyle === "pythonTutor" ? "Frames" : "Call Stack"}
</text>

<foreignObject
x={x + columnWidth - 26}
y={yPosition + (HEADER_HEIGHT - 18) / 2}
width={18}
height={18}
style={{ overflow: "visible" }}
>
<HelpIcon
title={visualStyle === "pythonTutor" ? "Frames" : "Call Stack"}
text="Shows the currently active function calls, most recent on top. Each frame lists that function's local variables and their values. Reorder frames by dragging to change which one is on top."
/>
</foreignObject>

<clipPath id={clipPathId}>
<rect
x={x - horizontalPadding}
Expand Down
27 changes: 27 additions & 0 deletions frontend/src/features/canvasControls/CanvasControls.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@
}

.title {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
text-align: center;
font-size: 0.85rem;
font-weight: 700;
Expand Down Expand Up @@ -148,6 +152,13 @@
transition: color 200ms ease;
}

.labelGroup {
display: inline-flex;
align-items: center;
gap: 6px;
min-width: 0;
}

.scaleValue {
font-size: 0.9rem;
font-weight: 600;
Expand All @@ -170,6 +181,22 @@
margin-bottom: 0;
}

.buttonWrapperRow {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}

.buttonWrapperRow:last-child {
margin-bottom: 0;
}

.buttonWrapperRow > *:first-child {
flex: 1;
min-width: 0;
}

.feedbackWrapper {
margin-top: 16px;
}
Expand Down
109 changes: 109 additions & 0 deletions frontend/src/features/canvasControls/CanvasControls.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ function openSettingsTab() {
fireEvent.click(screen.getByRole("button", { name: /settings/i }));
}

function openViewTab() {
fireEvent.click(screen.getByRole("button", { name: /^view$/i }));
}

describe("CanvasControls", () => {
beforeEach(() => {
localStorage.clear();
Expand Down Expand Up @@ -93,3 +97,108 @@ describe("CanvasControls", () => {
expect(handleReferenceArrowChange).toHaveBeenCalledWith(true);
});
});

describe("CanvasControls help icons", () => {
beforeEach(() => {
localStorage.clear();
});

it("shows a help icon for the panel title explaining the tabs", () => {
renderControls();

fireEvent.click(
screen.getByRole("button", { name: "Help: Canvas Controls" })
);

expect(screen.getByRole("dialog")).toBeInTheDocument();
expect(screen.getByText(/Actions to undo/i)).toBeInTheDocument();
});

it("shows help icons for Clear and Download without triggering their actions", () => {
const handleClear = jest.fn();
renderControls({ onClear: handleClear, elements: [] });

fireEvent.click(screen.getByRole("button", { name: "Help: Clear" }));
expect(screen.getByRole("dialog")).toBeInTheDocument();
expect(handleClear).not.toHaveBeenCalled();

fireEvent.click(screen.getByRole("button", { name: "Close" }));

fireEvent.click(screen.getByRole("button", { name: "Help: Download" }));
expect(screen.getByRole("dialog")).toBeInTheDocument();
});

it("shows help icons for all three zoom controls in the View tab", () => {
renderControls({
onScaleChange: jest.fn(),
onEditorScaleChange: jest.fn(),
onFontScaleChange: jest.fn(),
});
openViewTab();

expect(
screen.getByRole("button", { name: "Help: Canvas Zoom" })
).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Help: Editor Zoom" })
).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Help: Question Zoom" })
).toBeInTheDocument();
});

it("shows a help icon for the practice/test mode toggle without flipping it", () => {
const handleModeToggle = jest.fn();
renderControls({ onModeToggle: handleModeToggle, isSandboxMode: true });
openSettingsTab();

fireEvent.click(
screen.getByRole("button", { name: "Help: Practice / Test Mode" })
);

expect(screen.getByRole("dialog")).toBeInTheDocument();
expect(handleModeToggle).not.toHaveBeenCalled();
});

it("shows a help icon for the Python Tutor Style toggle", () => {
renderControls();
openSettingsTab();

fireEvent.click(
screen.getByRole("button", { name: "Help: Python Tutor Style" })
);

expect(screen.getByRole("dialog")).toBeInTheDocument();
expect(
screen.getByText(/look like PythonTutor's visualizer/i)
).toBeInTheDocument();
});

it("shows help icons for Standalone Primitives and Reference Arrows in Python Tutor mode", () => {
renderControls({ visualStyle: "pythonTutor" });
openSettingsTab();

expect(
screen.getByRole("button", { name: "Help: Standalone Primitives" })
).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Help: Reference Arrows" })
).toBeInTheDocument();
});

it("shows a help icon for Dark Mode without flipping the toggle", () => {
renderControls();
openSettingsTab();

const darkModeSwitch = screen.getByRole("switch", { name: /dark mode/i });
expect(darkModeSwitch).toHaveAttribute("aria-checked", "false");

fireEvent.click(screen.getByRole("button", { name: "Help: Dark Mode" }));

expect(screen.getByRole("dialog")).toBeInTheDocument();
expect(
screen.getByText(/exported on a white background/i)
).toBeInTheDocument();
expect(darkModeSwitch).toHaveAttribute("aria-checked", "false");
});
});
Loading