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
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,87 @@
border-color: #2d4a6a;
}

/* ── Divider between selector groups ── */
.selectorDivider {
height: 1px;
background: var(--border-primary);
margin: 0.25rem 0;
}

/* ── Step-by-step indicator ── */
.stepIndicator {
display: flex;
align-items: center;
gap: 0.6rem;
margin-bottom: 1rem;
padding: 0.6rem 0.85rem;
background: var(--bg-secondary);
border: 1px solid var(--border-primary);
border-radius: 10px;
transition: background-color 200ms ease, border-color 200ms ease;
}

.stepDots {
display: flex;
align-items: center;
gap: 0.35rem;
flex-shrink: 0;
}

.stepDot {
width: 0.55rem;
height: 0.55rem;
border-radius: 50%;
flex-shrink: 0;
transition: background 200ms ease, box-shadow 200ms ease;
}

.stepDotDone {
background: #10b981;
}

.stepDotActive {
background: #3b82f6;
box-shadow: 0 0 0 2.5px rgba(59, 130, 246, 0.25);
}

.stepDotPending {
background: var(--border-secondary);
}

.stepText {
font-size: calc(0.82rem * var(--font-scale, 1));
font-weight: 600;
color: var(--text-secondary);
transition: color 200ms ease;
}

.stepConsistencyError {
margin-top: 0.65rem;
padding: 0.55rem 0.85rem;
background: #fef2f2;
border: 1px solid #fecaca;
border-radius: 8px;
color: #dc2626;
font-size: calc(0.82rem * var(--font-scale, 1));
font-weight: 500;
line-height: 1.45;
}

:root[data-theme="dark"] .stepConsistencyError {
background: #3a1c1c;
border-color: #7f1d1d;
color: #f87171;
}

.stepHint {
margin: 0.4rem 0 0 0;
font-size: calc(0.75rem * var(--font-scale, 1));
color: var(--text-muted);
font-style: italic;
transition: color 200ms ease;
}

/* Auto-advance toolbar */
.autoToolbar {
display: flex;
Expand Down
171 changes: 171 additions & 0 deletions frontend/src/features/informationTabs/questionTab/QuestionTab.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ jest.mock("react-markdown", () => ({
}));
import {
buildLineIterations,
checkStepConsistency,
extractStepAssignments,
getCheckableLines,
getNextCheckableLine,
sortCheckableLines,
QuestionData,
StepAssignments,
} from "./QuestionTab";

describe("QuestionTab helper functions", () => {
Expand Down Expand Up @@ -50,3 +53,171 @@ describe("QuestionTab helper functions", () => {
expect(getNextCheckableLine([1, 2, 4], null)).toBeNull();
});
});

// ─── Helper canvas element factories ────────────────────────────────────────

function makeFrame(params: { name: string; targetId: number | null }[]) {
return {
boxId: 0,
id: "_",
x: 0,
y: 0,
kind: {
name: "function",
type: "function",
value: null,
functionName: "__main__",
params,
},
};
}

function makePrimitive(id: number, type: string, value: string) {
return { boxId: id, id, x: 0, y: 0, kind: { name: "primitive", type, value } };
}

function makeClass(classVariables: { name: string; targetId: number | null }[]) {
return {
boxId: 99,
id: "_",
x: 0,
y: 0,
kind: {
name: "class",
type: "class",
value: null,
className: "MyClass",
classVariables,
},
};
}

// ─── extractStepAssignments ──────────────────────────────────────────────────

describe("extractStepAssignments", () => {
it("returns empty maps for an empty element list", () => {
expect(extractStepAssignments([])).toEqual({ variableToId: {}, idToType: {} });
});

it("extracts variable→id mappings from a function frame's params", () => {
const result = extractStepAssignments([
makeFrame([{ name: "a", targetId: 1 }, { name: "b", targetId: 2 }]),
]);
expect(result.variableToId).toEqual({ a: 1, b: 2 });
expect(result.idToType).toEqual({});
});

it("skips params whose targetId is null", () => {
const result = extractStepAssignments([
makeFrame([{ name: "a", targetId: null }, { name: "b", targetId: 3 }]),
]);
expect(result.variableToId).toEqual({ b: 3 });
});

it("extracts idToType from primitive elements with numeric ids", () => {
const result = extractStepAssignments([
makePrimitive(1, "int", "5"),
makePrimitive(2, "str", "hello"),
]);
expect(result.idToType).toEqual({ 1: "int", 2: "str" });
expect(result.variableToId).toEqual({});
});

it("ignores non-frame elements whose id is not a number", () => {
const result = extractStepAssignments([
{ boxId: 0, id: "_", kind: { name: "primitive", type: "int", value: "5" } },
]);
expect(result.idToType).toEqual({});
});

it("combines frame variable mappings and primitive type mappings", () => {
const result = extractStepAssignments([
makeFrame([{ name: "x", targetId: 7 }]),
makePrimitive(7, "float", "3.14"),
]);
expect(result.variableToId).toEqual({ x: 7 });
expect(result.idToType).toEqual({ 7: "float" });
});

it("extracts classVariables from class elements", () => {
const result = extractStepAssignments([
makeClass([{ name: "attr", targetId: 5 }]),
]);
expect(result.variableToId).toEqual({ attr: 5 });
});

it("skips class classVariables with null targetId", () => {
const result = extractStepAssignments([
makeClass([{ name: "attr", targetId: null }, { name: "other", targetId: 9 }]),
]);
expect(result.variableToId).toEqual({ other: 9 });
});
});

// ─── checkStepConsistency ────────────────────────────────────────────────────

describe("checkStepConsistency", () => {
const empty: StepAssignments = { variableToId: {}, idToType: {} };

it("returns null when committed state is empty", () => {
const current: StepAssignments = { variableToId: { a: 1 }, idToType: { 1: "int" } };
expect(checkStepConsistency(empty, current)).toBeNull();
});

it("returns null when variable still maps to the same id", () => {
const committed: StepAssignments = { variableToId: { a: 1 }, idToType: {} };
const current: StepAssignments = { variableToId: { a: 1, b: 2 }, idToType: {} };
expect(checkStepConsistency(committed, current)).toBeNull();
});

it("returns an error naming the variable and both ids when variable→id changes", () => {
const committed: StepAssignments = { variableToId: { a: 1 }, idToType: {} };
const current: StepAssignments = { variableToId: { a: 2 }, idToType: {} };
const error = checkStepConsistency(committed, current);
expect(error).not.toBeNull();
expect(error).toMatch(/variable "a"/i);
expect(error).toMatch(/id 1/);
expect(error).toMatch(/id 2/);
});

it("returns null when an id still has the same type", () => {
const committed: StepAssignments = { variableToId: {}, idToType: { 1: "int" } };
const current: StepAssignments = { variableToId: {}, idToType: { 1: "int", 2: "str" } };
expect(checkStepConsistency(committed, current)).toBeNull();
});

it("returns an error naming the id and both types when type changes", () => {
const committed: StepAssignments = { variableToId: {}, idToType: { 1: "int" } };
const current: StepAssignments = { variableToId: {}, idToType: { 1: "str" } };
const error = checkStepConsistency(committed, current);
expect(error).not.toBeNull();
expect(error).toMatch(/id 1/i);
expect(error).toMatch(/int/);
expect(error).toMatch(/str/);
});

it("returns null when a committed variable is absent from the current state", () => {
// Absence is a backend concern (missing element), not a consistency violation
const committed: StepAssignments = { variableToId: { a: 1 }, idToType: {} };
expect(checkStepConsistency(committed, empty)).toBeNull();
});

it("returns null when a committed id is absent from the current state", () => {
const committed: StepAssignments = { variableToId: {}, idToType: { 1: "int" } };
expect(checkStepConsistency(committed, empty)).toBeNull();
});

it("checks variable mappings before id types", () => {
// Both a variable AND a type are wrong; the variable error should be reported first
const committed: StepAssignments = { variableToId: { a: 1 }, idToType: { 1: "int" } };
const current: StepAssignments = { variableToId: { a: 2 }, idToType: { 1: "str" } };
const error = checkStepConsistency(committed, current);
expect(error).not.toBeNull();
expect(error).toMatch(/variable "a"/i);
});

it("returns null for two steps where nothing changed", () => {
const state: StepAssignments = { variableToId: { a: 1, b: 2 }, idToType: { 1: "int", 2: "int" } };
expect(checkStepConsistency(state, state)).toBeNull();
});
});
Loading