All files / src/utils/components/submission-review BlankAnswerQuestion.jsx

98% Statements 49/50
93.75% Branches 45/48
100% Functions 5/5
100% Lines 48/48

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124                                    5x 27x 27x 27x 27x 2x   25x                       5x 39x 39x 39x 39x             39x   39x 39x 39x 39x   39x 27x 27x     3x 3x 3x 24x 18x 18x 18x   6x 6x 6x       39x   39x                                     5x 40x 40x 40x     40x 40x 40x   40x 40x 39x 39x 39x 39x 39x 39x     40x                
/**
 * Modified by Allan Ninal — 2026-08-12
 *
 * Simplified the isCorrect expression: two of its four conditions could not affect
 * the outcome. Behaviour unchanged (verified exhaustively).
 */
 
import React from "react";
import { Box } from "@mui/material";
import { useTheme } from "@mui/material/styles";
import TinyMCETextEditorReadOnly from "@/utils/components/TinyMCETextEditorReadOnly";
import { questionCardSurfaceBg } from "@/utils/components/question-bank/question-display/utils/questionCardStyles";
import { extractContent, stripHtml } from "./submissionAnswerUtils";
 
// Same "unwrap the paragraph wrapper" step DropDownMenuViewOnly / DragDropViewOnly
// (src/utils/components/question-bank/question-display/{drop-down-menu,drag-and-drop}/)
// use — TinyMCE wraps a choice's HTML in a single <p>, which would otherwise force
// a line break inside the inline blank span.
const unwrapBlock = (html) => {
    Iif (typeof document === "undefined" || !html) return html || "";
    const el = document.createElement("div");
    el.innerHTML = html;
    if (el.children.length === 1 && el.children[0].tagName === "P") {
        return el.children[0].innerHTML;
    }
    return html;
};
 
/**
 * Inline blank span for a drag-and-drop / drop-down-menu question, styled the
 * same way a highlighted multiple-choice option is: neutral/dashed when
 * unanswered, green when what the student put there matches the correct
 * answer, red when it doesn't. `studentRaw` / `correctRaw` are HTML strings
 * (already unwrapped via extractContent) — comparison uses the plain-text form
 * so markup differences don't cause false negatives, but the rendered content
 * keeps the original markup so math/formula content still renders.
 */
export const buildBlankSpan = (studentRaw, correctRaw, theme, hasGradingData) => {
    const dark = theme.palette.mode === "dark";
    const studentPlain = stripHtml(studentRaw);
    const correctPlain = stripHtml(correctRaw);
    const hasAnswer = studentPlain !== "";
    // `hasGradingData` and `correctPlain !== ""` were also in this expression and
    // neither could change the outcome: the colour block below short-circuits on
    // `!hasGradingData` before consulting isCorrect, and isCorrect already requires
    // a non-empty studentPlain, so an empty correctPlain fails the === anyway.
    // Verified over all 12 combinations of (hasGradingData, student, correct) —
    // identical colouring — and mutations removing either killed no test.
    const isCorrect = hasAnswer && studentPlain === correctPlain;
 
    let borderColor = dark ? "#5c6370" : "#c0c8d4";
    let bgColor = dark ? "#2c2f34" : "#f5f7fa";
    let textColor = dark ? "#9aa1ab" : "#9e9e9e";
    let borderStyle = "dashed";
 
    if (hasAnswer) {
        borderStyle = "solid";
        if (!hasGradingData) {
            // Backend withheld correctAnswer (grading hidden) — show the answer
            // as "submitted" without implying it's right or wrong.
            borderColor = theme.palette.info.main;
            bgColor = dark ? "#1e2a38" : "#eef4fb";
            textColor = dark ? theme.palette.info.light : theme.palette.info.dark;
        } else if (isCorrect) {
            borderColor = theme.palette.success.main;
            bgColor = dark ? "#1a352c" : "#edf7f0";
            textColor = dark ? theme.palette.success.light : theme.palette.success.dark;
        } else {
            borderColor = theme.palette.error.main;
            bgColor = dark ? "#3a1f1f" : "#fdecea";
            textColor = dark ? theme.palette.error.light : theme.palette.error.dark;
        }
    }
 
    const displayContent = hasAnswer ? unwrapBlock(studentRaw) : "No answer";
 
    return (
        `<span style="display:inline-block;padding:2px 12px;margin:0 4px;border-radius:6px;` +
        `border:2px ${borderStyle} ${borderColor};background-color:${bgColor};` +
        `vertical-align:middle;white-space:nowrap;line-height:normal;font-weight:${hasAnswer ? 500 : 400};` +
        `color:${textColor};">${displayContent}</span>`
    );
};
 
/**
 * Renders a drag-and-drop / drop-down-menu question with its `__blank__`
 * markers replaced by the student's answer at each blank (colored by
 * correctness) — instead of leaving the literal "__blank__" text in the
 * question stem, which is what a plain <ReadOnlyMathEditor content={question.question}>
 * would otherwise show.
 *
 * Blank identity differs by type (mirrors how the take-assignment inputs save
 * answers): drag-and-drop keys each blank by its 0-based position; drop-down-menu
 * keys each blank by that position's choice `id`.
 */
const BlankAnswerQuestion = ({ question, rawAnswer }) => {
    const theme = useTheme();
    const cardBg = questionCardSurfaceBg(theme);
    const correctAnswers = question?.correctAnswer?.content;
    // Backend omits correctAnswer entirely when grading is hidden from the
    // student — treat that as "ungraded", not "wrong" (see SubmissionQuestionCard).
    const hasGradingData = question?.correctAnswer !== undefined && question?.correctAnswer !== null;
    const answerList = Array.isArray(rawAnswer) ? rawAnswer : [];
    const isDropDown = question?.questionType === "drop-down-menu";
 
    let blankIndex = 0;
    const content = (question?.question || "").replace(/__blank__/g, () => {
        const idx = blankIndex++;
        const blankId = isDropDown ? question?.choices?.[idx]?.id : idx;
        const studentEntry = answerList.find((b) => b?.id === blankId);
        const studentRaw = extractContent(studentEntry?.answer);
        const correctRaw = extractContent(correctAnswers?.[idx]);
        return buildBlankSpan(studentRaw, correctRaw, theme, hasGradingData);
    });
 
    return (
        <Box>
            <TinyMCETextEditorReadOnly content={content} backgroundColor={cardBg} />
        </Box>
    );
};
 
export default BlankAnswerQuestion;