Skip to content
Closed
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
157 changes: 156 additions & 1 deletion v2/src/pages/MissionWorkspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
loadDraft,
saveDraft,
} from "@/services/draft-store";
import { runMissionCode } from "@/services/runner";
import { runManualCode, runMissionCode } from "@/services/runner";
import {
runMissionAdmin,
type MissionSolution,
Expand All @@ -62,6 +62,7 @@
type Attempt,
type AttemptKind,
type Language,
type ManualRunResult,
type RunResult,
} from "@/types";

Expand All @@ -70,6 +71,7 @@
type MobilePane = "brief" | "code" | "results";
type BriefTab = "problem" | "hints" | "history" | "solution";
type SaveState = "loading" | "saving" | "synced" | "local" | "error";
type ManualRunState = "idle" | "running";

const RESULTS_WIDTH_KEY = "tomatin.v3.workspace-results-width";
const MIN_RESULTS_WIDTH = 290;
Expand Down Expand Up @@ -432,6 +434,10 @@
const [saveState, setSaveState] = useState<SaveState>("loading");
const [saveMessage, setSaveMessage] = useState("");
const [result, setResult] = useState<RunResult | null>(null);
const [manualOpen, setManualOpen] = useState(false);
const [manualExpression, setManualExpression] = useState("");
const [manualResult, setManualResult] = useState<ManualRunResult | null>(null);
const [manualRunning, setManualRunning] = useState<ManualRunState>("idle");
const [running, setRunning] = useState<AttemptKind | null>(null);
const [briefTab, setBriefTab] = useState<BriefTab>(
linkedAttemptId || linkedReviewId ? "history" : "problem",
Expand Down Expand Up @@ -817,6 +823,19 @@
setRunning(null);
}

async function executeManualTest() {
if (language === "cpp" || manualRunning === "running") return;
setManualRunning("running");
setManualResult(null);
const manual = await runManualCode({
language,
code: currentCode,
expression: manualExpression,
});
setManualResult(manual);
setManualRunning("idle");
}

function clampResultsWidth(nextWidth: number) {
const workbenchWidth = workbenchRef.current?.clientWidth ?? 0;
const maxWidth = Math.max(
Expand Down Expand Up @@ -1428,6 +1447,23 @@
? "Pyodide Worker"
: "Web Worker"}
</span>
<button
className="button secondary"
type="button"
disabled={Boolean(running) || isStudentPreview || language === "cpp"}
title={
language === "cpp"
? "El probador manual está disponible solo para JavaScript y Python"
: undefined
}
onClick={() => {
setManualOpen(true);
setManualResult(null);
}}
>
<TerminalSquare aria-hidden="true" />
Test Manual
</button>
<button
className="button secondary"
type="button"
Expand Down Expand Up @@ -1478,6 +1514,125 @@
</div>
</section>

{manualOpen ? (
<div className="manual-test-backdrop" role="presentation">
<section
className="manual-test-dialog"
aria-labelledby="manual-test-title"
role="dialog"
aria-modal="true"
>
<header>
<div>
<span className="eyebrow">PRUEBA MANUAL</span>
<h2 id="manual-test-title">
Ejecutar expresión en {LANGUAGE_META[language].label}
</h2>
<p className="manual-test-desc">
Prueba aquí tus funciones con valores personalizados
</p>
</div>
<button
className="icon-button"
type="button"
aria-label="Cerrar prueba manual"
title="Cerrar"
onClick={() => setManualOpen(false)}
>
<X aria-hidden="true" />
</button>
</header>
<label className="manual-test-field">
<span>Expresión</span>
<input
type="text"
value={manualExpression}
placeholder={
language === "python" ? "mi_funcion(42)" : "miFuncion(42)"
}
onChange={(event) => setManualExpression(event.target.value)}
onKeyDown={(event) => {
if (event.key === "Enter") {
event.preventDefault();
void executeManualTest();
}
}}
autoFocus
/>
</label>
{manualSignatureHints.length > 0 ? (

Check failure on line 1563 in v2/src/pages/MissionWorkspace.tsx

View workflow job for this annotation

GitHub Actions / test

Cannot find name 'manualSignatureHints'.
<p className="manual-test-hint">
Funciones evaluadas en este ejercicio:{" "}
{manualSignatureHints.map((signature, index) => (

Check failure on line 1566 in v2/src/pages/MissionWorkspace.tsx

View workflow job for this annotation

GitHub Actions / test

Parameter 'index' implicitly has an 'any' type.

Check failure on line 1566 in v2/src/pages/MissionWorkspace.tsx

View workflow job for this annotation

GitHub Actions / test

Parameter 'signature' implicitly has an 'any' type.

Check failure on line 1566 in v2/src/pages/MissionWorkspace.tsx

View workflow job for this annotation

GitHub Actions / test

Cannot find name 'manualSignatureHints'.
<span key={signature}>
{index > 0 ? ", " : ""}
<code>{signature}</code>
</span>
))}
</p>
) : null}
<div className="manual-test-actions">
<button
className="button secondary"
type="button"
onClick={() => setManualOpen(false)}
>
Cancelar
</button>
<button
className="button primary"
type="button"
disabled={manualRunning === "running"}
onClick={() => void executeManualTest()}
>
{manualRunning === "running" ? (
<LoaderCircle className="spin" aria-hidden="true" />
) : (
<Play aria-hidden="true" />
)}
Ejecutar expresión
</button>
</div>
{manualResult ? (
<section
className={`manual-test-result result-${manualResult.status}`}
aria-live="polite"
>
<strong>
{manualResult.status === "passed"
? "Resultado"
: "No se pudo ejecutar"}
</strong>
<dl>
<div>
<dt>Expresión</dt>
<dd>
<code>{manualResult.expression}</code>
</dd>
</div>
{manualResult.value !== undefined ? (
<div>
<dt>Valor</dt>
<dd>
<code>{manualResult.value}</code>
</dd>
</div>
) : null}
</dl>
{manualResult.stdout || manualResult.stderr ? (
<pre>
{manualResult.stdout ? `$ ${manualResult.stdout}\n` : ""}
{manualResult.stderr
? `[stderr]\n${manualResult.stderr}`
: ""}
</pre>
) : null}
</section>
) : null}
</section>
</div>
) : null}

<button
className="workspace-resizer"
type="button"
Expand Down
114 changes: 114 additions & 0 deletions v2/src/services/runner.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type {
Language,
ManualRunResult,
MissionTest,
RunnerRequest,
RunResult,
Expand All @@ -14,6 +16,7 @@
ok: boolean;
logs: string[];
tests: TestResult[];
manual?: ManualRunResult | { expression: string; value?: string } | null;
error?: string;
stack?: string;
}
Expand Down Expand Up @@ -84,6 +87,117 @@
});
}

function runManualInWorker(
worker: Worker,
request: ManualRunnerRequest,

Check failure on line 92 in v2/src/services/runner.ts

View workflow job for this annotation

GitHub Actions / test

Cannot find name 'ManualRunnerRequest'. Did you mean 'RunnerRequest'?
timeoutMs: number,
): Promise<ManualRunResult> {
const startedAt = performance.now();

return new Promise((resolve) => {
const timeout = window.setTimeout(() => {
worker.terminate();
resolve({
id: crypto.randomUUID(),
status: "timeout",
expression: request.expression,
stdout: "",
stderr: `La ejecución superó ${timeoutMs} ms.`,
diagnostics: [],
durationMs: timeoutMs,
createdAt: new Date().toISOString(),
});
}, timeoutMs);

worker.onmessage = ({ data }: MessageEvent<WorkerResponse>) => {
window.clearTimeout(timeout);
worker.terminate();
const manual = data.manual && "expression" in data.manual ? data.manual : null;
resolve({
id: crypto.randomUUID(),
status: data.ok ? "passed" : "runtime_error",
expression: request.expression,
value: manual?.value,
stdout: data.logs.join("\n").slice(0, 32_768),
stderr: data.error ?? "",
diagnostics: data.error
? [{ severity: "error", message: data.error }]
: [],
durationMs: Math.round(performance.now() - startedAt),
createdAt: new Date().toISOString(),
});
};

worker.onerror = (event) => {
window.clearTimeout(timeout);
worker.terminate();
resolve({
id: crypto.randomUUID(),
status: "runtime_error",
expression: request.expression,
stdout: "",
stderr: event.message,
diagnostics: [{ severity: "error", message: event.message }],
durationMs: Math.round(performance.now() - startedAt),
createdAt: new Date().toISOString(),
});
};

worker.postMessage({
code: request.code,
tests: [],
manualExpression: request.expression,
});
});
}

export async function runManualCode(
request: ManualRunnerRequest,

Check failure on line 155 in v2/src/services/runner.ts

View workflow job for this annotation

GitHub Actions / test

Cannot find name 'ManualRunnerRequest'. Did you mean 'RunnerRequest'?
): Promise<ManualRunResult> {
if (request.code.length > 65_536) {
return {
id: crypto.randomUUID(),
status: "failed",
expression: request.expression,
stdout: "",
stderr: "El código supera el límite de 64 KB.",
diagnostics: [],
createdAt: new Date().toISOString(),
};
}

if (!request.expression.trim()) {
return {
id: crypto.randomUUID(),
status: "failed",
expression: request.expression,
stdout: "",
stderr: "Escribe una expresión para probar.",
diagnostics: [],
createdAt: new Date().toISOString(),
};
}

if (request.language === "javascript") {
return runManualInWorker(
new Worker(
new URL("../workers/javascript-runner.worker.ts", import.meta.url),
{ type: "module" },
),
request,
1_500,
);
}

return runManualInWorker(
new Worker(new URL("../workers/python-runner.worker.ts", import.meta.url), {
type: "module",
}),
request,
30_000,
);
}

export async function runMissionCode(request: RunnerRequest): Promise<RunResult> {
const variant = request.mission.variants[request.language];
const tests = variant.publicTests;
Expand Down
Loading
Loading