Skip to content

Commit 032f429

Browse files
committed
Restore the saved language on retry
Retrying a coding attempt currently resets its language to Python, which can hide the mismatch until starter code or tests appear. Preserve the saved language only when the problem offers it, retaining Python as the fallback.
1 parent 6b6cdfe commit 032f429

5 files changed

Lines changed: 88 additions & 11 deletions

File tree

‎tests/browser/candidate-cases.test.js‎

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -656,9 +656,10 @@ test("C stays unavailable until a function-style judge has loaded", async (t) =>
656656
let releaseJudge = () => {};
657657
try {
658658
releaseJudge = await holdJudge(page);
659-
await page.goto(`${enabled.base}/interview.html?problem=${problem}`, {
660-
waitUntil: "domcontentloaded",
661-
});
659+
await page.goto(
660+
`${enabled.base}/interview.html?problem=${problem}&language=c`,
661+
{ waitUntil: "domcontentloaded" },
662+
);
662663
const c = page.locator('[data-language="c"]');
663664
await page.waitForFunction(
664665
() => document.querySelector('[data-language="c"]').disabled,
@@ -671,12 +672,20 @@ test("C stays unavailable until a function-style judge has loaded", async (t) =>
671672
releaseJudge();
672673
await candidateCasesReady(page);
673674
assert.equal(await c.isDisabled(), classStyle);
674-
if (classStyle)
675+
if (classStyle) {
675676
assert.match(
676677
await c.getAttribute("title"),
677678
/only builds function judges/,
678679
);
679-
else assert.equal(await clickSelects(c), true);
680+
assert.match(
681+
await page
682+
.locator('[data-language="python"]')
683+
.getAttribute("class"),
684+
/\bselected\b/,
685+
);
686+
} else {
687+
assert.match(await c.getAttribute("class"), /\bselected\b/);
688+
}
680689
for (const language of ["python", "javascript", "cpp", "java"]) {
681690
assert.equal(
682691
await page.locator(`[data-language="${language}"]`).isDisabled(),

‎tests/browser/editor-pipeline.test.js‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,36 @@ const setCode = (page, value, start = value.length, end = start) =>
9595
{ value, start, end },
9696
);
9797

98+
test("the interview restores an offered language from the URL", async (t) => {
99+
const ctx = await editorPage(t, { params: { language: "javascript" } });
100+
if (!ctx) return;
101+
const { page, errors } = ctx;
102+
try {
103+
assert.match(
104+
await page.locator('[data-language="javascript"]').getAttribute("class"),
105+
/\bselected\b/,
106+
);
107+
assert.deepEqual(errors, []);
108+
} finally {
109+
await page.close();
110+
}
111+
});
112+
113+
test("the interview keeps Python for an unknown saved language", async (t) => {
114+
const ctx = await editorPage(t, { params: { language: "unknown" } });
115+
if (!ctx) return;
116+
const { page, errors } = ctx;
117+
try {
118+
assert.match(
119+
await page.locator('[data-language="python"]').getAttribute("class"),
120+
/\bselected\b/,
121+
);
122+
assert.deepEqual(errors, []);
123+
} finally {
124+
await page.close();
125+
}
126+
});
127+
98128
test("typing an opening bracket auto-closes it, in a real browser", async (t) => {
99129
const ctx = await editorPage(t);
100130
if (!ctx) return;

‎tests/browser/lobby.test.js‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2044,6 +2044,22 @@ lobbyTest("try again selects the problem", async (page) => {
20442044
);
20452045
});
20462046

2047+
lobbyTest("try again restores the saved language", async (page) => {
2048+
reports = [
2049+
{
2050+
payload: {
2051+
...savedAttempt(EASY[0]).payload,
2052+
language: "javascript",
2053+
},
2054+
},
2055+
];
2056+
await lobby(page);
2057+
await page.getByRole("button", { name: "Try again" }).click();
2058+
await page.click("#start");
2059+
await page.waitForURL("**/interview?**");
2060+
assert.equal(new URL(page.url()).searchParams.get("language"), "javascript");
2061+
});
2062+
20472063
lobbyTest(
20482064
"an unmappable history entry fetches the page map at most once",
20492065
async (page) => {

‎web/app.js‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ let duration;
5353
let interviewLoop = "coding_behavioral";
5454
/// Which surface the interview runs on; see the format row in index.html.
5555
let mode = "coding";
56+
/// The saved language attached to a problem selected through Try again.
57+
let retryLanguage = null;
5658
let reports = [];
5759
/// The practice focus the share box currently refers to.
5860
let sharedFocus = null;
@@ -393,6 +395,8 @@ start.addEventListener("click", async () => {
393395
destination.searchParams.set("duration", String(duration));
394396
destination.searchParams.set("loop", interviewLoop);
395397
destination.searchParams.set("mode", mode);
398+
if (mode === "coding" && retryLanguage)
399+
destination.searchParams.set("language", retryLanguage);
396400
setEditorOptions(destination.searchParams, selectedEditorOptions());
397401
const profile = {
398402
role: nodes.profileRole.value.trim(),
@@ -1113,12 +1117,13 @@ function applySuggestedLevel() {
11131117
/// Clearing a filtered-out card must not leave Start pointing at it, and
11141118
/// picking another card must not bypass an account request already in flight.
11151119
/// Callers own the explanation beside the picker; this only knows which card.
1116-
function setProblem(card) {
1120+
function setProblem(card, language = null) {
11171121
// Two buttons, not a sweep of all 150. `problem` still holds the outgoing
11181122
// card here, which is what makes that possible.
11191123
mark(problem?.button, false);
11201124
mark(card?.button, true);
11211125
problem = card;
1126+
retryLanguage = language;
11221127
syncLobbyControls();
11231128
}
11241129

@@ -1391,7 +1396,7 @@ function renderAttemptHistory(attempts) {
13911396
if (!card) return;
13921397
manualProblem = true;
13931398
card.button.hidden = false;
1394-
setProblem(card);
1399+
setProblem(card, attempt.language);
13951400
const savedDuration = attempt.durationMin;
13961401
const offered = durations.map((button) =>
13971402
Number(button.dataset.duration),

‎web/interview.js‎

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,9 @@ const judgePromise = loadJudge(problem.page).catch(() => null);
278278
const candidateCaseStorageKey = `codetrial.candidateCases.${problem.page}`;
279279
/// Filled by `applyLanguages`, which `init` calls before anything reads it.
280280
let languages = [];
281+
// A delayed judge may make the requested C tab available after the first
282+
// paint. Do not switch to it after the candidate has chosen a tab or edited.
283+
let languageChoiceCommitted = false;
281284
/// What the lobby asked for, until `/api/token` says what it got. The range
282285
/// here mirrors the server's own and is the fallback for a URL that arrives
283286
/// without passing through the lobby; the answer below is what the interview
@@ -295,6 +298,7 @@ const interviewLoop = codingLoop(params.get("loop"));
295298
/// the answer would leave the editor bound and the starter code published in
296299
/// an interview that has neither.
297300
const whiteboard = modeIsWhiteboard(params.get("mode"));
301+
const requestedLanguage = whiteboard ? null : params.get("language");
298302
/// The same answer as the wire spelling, which the token request and the
299303
/// checklist both need. Derived from the boolean rather than from the URL a
300304
/// second time, so an unrecognized value cannot reach the server as itself.
@@ -616,7 +620,11 @@ async function init() {
616620
applyEditorTheme(readStored(EDITOR_THEME_KEY));
617621
renderProblem();
618622
applyLanguages(null);
619-
setLanguage("python");
623+
setLanguage(
624+
requestedLanguage && languages.includes(requestedLanguage)
625+
? requestedLanguage
626+
: "python",
627+
);
620628
bindEvents();
621629
if (whiteboard) initWhiteboard();
622630
placeJimStageBelowToolbar(
@@ -651,6 +659,13 @@ async function init() {
651659
// microtask.
652660
void judgePromise.then((spec) => {
653661
applyLanguages(spec);
662+
if (
663+
!languageChoiceCommitted &&
664+
requestedLanguage &&
665+
state.language !== requestedLanguage &&
666+
languages.includes(requestedLanguage)
667+
)
668+
setLanguage(requestedLanguage);
654669
});
655670
// No enumeration here. Before the preflight grant the browser reports only a
656671
// blank placeholder, so the list is discarded and repainted a few lines down;
@@ -850,9 +865,10 @@ function bindEvents() {
850865
nodes.resultsChevron.textContent = nodes.resultsBody.hidden ? "^" : "v";
851866
});
852867
for (const button of document.querySelectorAll("[data-language]")) {
853-
button.addEventListener("click", () =>
854-
setLanguage(button.dataset.language),
855-
);
868+
button.addEventListener("click", () => {
869+
languageChoiceCommitted = true;
870+
setLanguage(button.dataset.language);
871+
});
856872
}
857873
for (const [button, step] of [
858874
[nodes.editorFontSmaller, -1],
@@ -988,6 +1004,7 @@ function bindEvents() {
9881004
}
9891005
});
9901006
nodes.editor.addEventListener("input", () => {
1007+
languageChoiceCommitted = true;
9911008
// Before the buffer below is taken, because this sends the tab as it
9921009
// arrived. Typing inside the switch's own debounce window cancels it
9931010
// otherwise, and the single packet that survives carries the switch and the

0 commit comments

Comments
 (0)