diff --git a/tests/browser/candidate-cases.test.js b/tests/browser/candidate-cases.test.js index 3ff8b09b3..8a7f1997d 100644 --- a/tests/browser/candidate-cases.test.js +++ b/tests/browser/candidate-cases.test.js @@ -692,3 +692,186 @@ test("C stays unavailable until a function-style judge has loaded", async (t) => await new Promise((closed) => enabled.server.close(closed)); } }); + +const CASES_KEY = "codetrial.candidateCases.chargeback-pair-match"; + +async function addCase(page, input) { + const count = await page.locator("#candidate-case-list > li").count(); + await page.evaluate((value) => { + document.querySelector("#candidate-case-input").value = value; + document.querySelector("#candidate-case-add").click(); + }, input); + await page.waitForFunction( + (n) => document.querySelector("#candidate-case-list").children.length === n, + count + 1, + ); + const stored = await page.evaluate( + (k) => sessionStorage.getItem(k), + CASES_KEY, + ); + assert.ok(stored?.includes(input)); +} + +test("ending an interview clears candidate cases from session storage", async (t) => { + if (!browser) return t.skip("playwright chromium unavailable"); + const page = await browser.newPage(); + try { + await page.goto(`${base}/interview.html?problem=chargeback-pair-match`, { + waitUntil: "domcontentloaded", + }); + await candidateCasesReady(page); + await addCase(page, "[[2,7,11,15],9]"); + + await page.evaluate(() => document.querySelector("#end").click()); + await page.waitForFunction( + () => + sessionStorage.getItem( + "codetrial.candidateCases.chargeback-pair-match", + ) === null, + ); + } finally { + await page.close(); + } +}); + +test("rendering a report clears candidate cases from session storage", async (t) => { + if (!browser) return t.skip("playwright chromium unavailable"); + const page = await browser.newPage(); + try { + await page.goto(`${base}/interview.html?problem=chargeback-pair-match`, { + waitUntil: "domcontentloaded", + }); + await candidateCasesReady(page); + await addCase(page, "[[2,7,11,15],9]"); + + await page.evaluate(() => { + document.querySelector("#force-report").click(); + }); + await page.waitForFunction( + () => + sessionStorage.getItem( + "codetrial.candidateCases.chargeback-pair-match", + ) === null, + ); + } finally { + await page.close(); + } +}); + +test("in-flight candidate case additions are refused when interview leaves live", async (t) => { + if (!browser) return t.skip("playwright chromium unavailable"); + const page = await browser.newPage(); + let releaseJudge = () => {}; + try { + releaseJudge = await holdJudge(page); + await page.goto(`${base}/interview.html?problem=chargeback-pair-match`, { + waitUntil: "domcontentloaded", + }); + await page.waitForFunction( + () => !document.querySelector("#candidate-case-add").disabled, + ); + await page.evaluate(() => { + document.querySelector("#candidate-case-input").value = "[[2,7,11,15],9]"; + document.querySelector("#candidate-case-add").click(); + document.querySelector("#end").click(); + }); + releaseJudge(); + await page.waitForFunction( + () => !document.querySelector("#candidate-case-add").disabled, + ); + const stored = await page.evaluate(() => + sessionStorage.getItem("codetrial.candidateCases.chargeback-pair-match"), + ); + assert.equal(stored, null); + const casesCount = await page.evaluate( + () => document.querySelector("#candidate-case-list").children.length, + ); + assert.equal(casesCount, 0); + const status = await page.evaluate( + () => document.querySelector("#candidate-case-status").textContent, + ); + assert.equal(status, "The interview has ended."); + } finally { + releaseJudge(); + await page.close(); + } +}); + +test("ending an interview empties the candidate case list", async (t) => { + if (!browser) return t.skip("playwright chromium unavailable"); + const page = await browser.newPage(); + try { + await page.goto(`${base}/interview.html?problem=chargeback-pair-match`, { + waitUntil: "domcontentloaded", + }); + await candidateCasesReady(page); + await addCase(page, "[[2,7,11,15],9]"); + await addCase(page, "[[1,2,3],5]"); + + await page.evaluate(() => document.querySelector("#end").click()); + await page.waitForFunction( + () => + document.querySelector("#candidate-case-list").children.length === 0, + ); + const count = await page.locator("#candidate-case-list > li").count(); + assert.equal(count, 0); + } finally { + await page.close(); + } +}); + +test("candidate cases do not persist across new interview sessions in the same tab", async (t) => { + if (!browser) return t.skip("playwright chromium unavailable"); + const page = await browser.newPage(); + try { + await page.goto(`${base}/interview.html?problem=chargeback-pair-match`, { + waitUntil: "domcontentloaded", + }); + await candidateCasesReady(page); + await addCase(page, "[[2,7,11,15],9]"); + + await page.goto(`${base}/`, { waitUntil: "domcontentloaded" }); + await page.goto(`${base}/interview.html?problem=chargeback-pair-match`, { + waitUntil: "domcontentloaded", + }); + await candidateCasesReady(page); + + const count = await page.locator("#candidate-case-list > li").count(); + assert.equal(count, 0); + const stored = await page.evaluate( + (k) => sessionStorage.getItem(k), + CASES_KEY, + ); + assert.equal(stored, null); + } finally { + await page.close(); + } +}); + +test("candidate cases are preserved on page reload", async (t) => { + if (!browser) return t.skip("playwright chromium unavailable"); + const page = await browser.newPage(); + try { + await page.goto(`${base}/interview.html?problem=chargeback-pair-match`, { + waitUntil: "domcontentloaded", + }); + await candidateCasesReady(page); + await addCase(page, "[[2,7,11,15],9]"); + + await page.reload({ waitUntil: "domcontentloaded" }); + await page.waitForFunction( + () => + document.querySelector("#candidate-case-list").children.length === 1, + ); + + const count = await page.locator("#candidate-case-list > li").count(); + assert.equal(count, 1); + const stored = await page.evaluate( + (k) => sessionStorage.getItem(k), + CASES_KEY, + ); + assert.ok(stored?.includes("[[2,7,11,15],9]")); + } finally { + await page.close(); + } +}); diff --git a/web/interview.js b/web/interview.js index e21565cd1..68ff42d09 100644 --- a/web/interview.js +++ b/web/interview.js @@ -2623,10 +2623,20 @@ async function runTests() { } async function initializeCandidateCases() { + const isReload = + performance.getEntriesByType("navigation")[0]?.type === "reload"; + const activeSessionKey = "codetrial.activeSession"; + const wasActive = Boolean(readStored(activeSessionKey, tabStorage)); + writeStored(activeSessionKey, "1", tabStorage); + + if (!isReload && wasActive) { + writeStored(candidateCaseStorageKey, "", tabStorage); + } try { - const saved = JSON.parse( - readStored(candidateCaseStorageKey, tabStorage) || "[]", - ); + const saved = + !isReload && wasActive + ? [] + : JSON.parse(readStored(candidateCaseStorageKey, tabStorage) || "[]"); state.candidateCases = Array.isArray(saved) ? saved.slice(0, CANDIDATE_CASE_LIMIT) : []; @@ -2675,6 +2685,10 @@ async function addCandidateCaseNow() { } try { const spec = await loadJudge(problem.page); + if (state.phase !== "live") { + nodes.candidateCaseStatus.textContent = "The interview has ended."; + return "refused"; + } const input = parseCandidateCase(spec, nodes.candidateCaseInput.value); const expectedText = nodes.candidateCaseExpected.value.trim(); const expected = expectedText ? JSON.parse(expectedText) : undefined; @@ -2716,6 +2730,13 @@ function renderCandidateCases() { } } +function clearCandidateCases() { + writeStored(candidateCaseStorageKey, "", tabStorage); + state.candidateCases = []; + renderCandidateCases(); + nodes.candidateCaseStatus.textContent = "The interview has ended."; +} + /// Remove a saved case by position. The results panel is reset rather than /// kept: runBrowserTests labels candidate rows by the position they held at /// run time, and a removal renumbers every later case, so a kept panel would @@ -2920,6 +2941,7 @@ function endInterview(reason) { clearTimeout(codePublishTimer); codePublishTimer = null; pendingLanguagePublish = null; + clearCandidateCases(); nodes.end.disabled = true; nodes.ending.hidden = false; // Only the interviewer answers `end_interview`, so a report is coming only @@ -3113,6 +3135,7 @@ function renderReport() { stopAvatar(); stopLocalMedia(); stopEndingClock(); + clearCandidateCases(); nodes.ending.hidden = true; nodes.report.hidden = false; nodes.report.innerHTML = reportMarkup({