From b7c541cecc64642bfebf77f7678cccfc22a9d229 Mon Sep 17 00:00:00 2001 From: ColtenOuO Date: Fri, 2 Oct 2026 21:11:09 +0000 Subject: [PATCH] Keep a drawn problem the new filter still shows Ticking or unticking a difficulty redrew the recommended problem even when its own level stayed checked, so widening the filter to look at Hard threw away the Medium problem already on screen. Only a problem picked by hand survived that. Now any problem whose level is still checked stays, and a redraw happens only when the change hides it. A kept draw is not turned into a hand pick, which would stand for good. It is held against the reports it was drawn from, since the roll no longer draws it and returning from the preflight would otherwise swap it. Once refreshed reports differ, as when the interview just passed it, the lobby draws again. --- tests/browser/lobby.test.js | 76 ++++++++++++++++++++++++++++++++++--- web/app.js | 30 ++++++++++++--- 2 files changed, 95 insertions(+), 11 deletions(-) diff --git a/tests/browser/lobby.test.js b/tests/browser/lobby.test.js index 2f82c9e6..9c4bc31d 100644 --- a/tests/browser/lobby.test.js +++ b/tests/browser/lobby.test.js @@ -1050,20 +1050,84 @@ lobbyTest( ); lobbyTest( - "changing difficulty clears the previous random-pick exclusion", + "a drawn problem survives a difficulty change that still includes it", async (page) => { await page.addInitScript(() => { Math.random = () => 0; }); - const before = await lobby(page); + await lobby(page); await page.click("#random-problem"); - assert.notEqual((await snapshot(page)).card, before.card); - await setLevel(page, "Hard", true); - await setLevel(page, "Hard", false); - assert.equal((await snapshot(page)).card, before.card); + const drawn = await snapshot(page); + const level = (await cardInfo(page, drawn.card)).level; + + const others = ["Easy", "Medium", "Hard"].filter((name) => name !== level); + for (const other of others) { + await setLevel(page, other, true); + assert.equal( + (await snapshot(page)).card, + drawn.card, + `checking ${other} redrew a problem it did not hide`, + ); + } + // Returning from the media preflight re-runs the recommendation, and the + // roll that drew this problem names a different one under the new filter. + await restore(page); + await awaitReady(page); + assert.equal( + (await snapshot(page)).card, + drawn.card, + "a kept problem moved when the page was restored", + ); + await setLevel(page, others[0], false); + assert.equal( + (await snapshot(page)).card, + drawn.card, + `unchecking ${others[0]} redrew a problem it did not hide`, + ); + + await setLevel(page, level, false); + const redrawn = await snapshot(page); + assert.notEqual(redrawn.card, drawn.card, "a hidden problem stayed picked"); + const info = await cardInfo(page, redrawn.card); + assert.equal(info.level, others[1]); + assert.equal(info.hidden, false); }, ); +// Widening the filter keeps the card on screen, but it was drawn from the +// reports in hand, so refreshed ones that pass it decide again. The usual order +// widens before leaving for the interview; the other widens inside the reload, +// while the reports in hand still belong to the page before the candidate left. +for (const [when, widenFirst] of [ + ["before the interview", true], + ["while the history reloads", false], +]) { + lobbyTest( + `a drawn problem kept ${when} is redrawn from the history it comes back to`, + async (page) => { + const shown = (await lobby(page)).card; + const widen = async () => { + await setLevel(page, "Hard", true); + assert.equal((await snapshot(page)).card, shown); + }; + + if (widenFirst) await widen(); + const release = holdHistory(); + reports = [hired(shown)]; + await restore(page); + if (!widenFirst) await widen(); + + release(); + await awaitReady(page); + assert.notEqual( + (await snapshot(page)).card, + shown, + "a problem the refreshed history passed stayed recommended", + ); + }, + ); +} + lobbyTest( "Random problem waits for history on load and browser restore", async (page) => { diff --git a/web/app.js b/web/app.js index 7a90d634..49adca21 100644 --- a/web/app.js +++ b/web/app.js @@ -35,6 +35,11 @@ let reports = []; /// The practice focus the share box currently refers to. let sharedFocus = null; let manualProblem = false; +/// The reports a drawn problem kept across a difficulty change was drawn from, +/// serialized, or null when nothing is kept. Not `manualProblem`: a hand pick +/// stands for good, while a kept draw stands only until the history it was +/// drawn from changes. +let keptDraw = null; let manualDuration = false; let manualDifficulty = false; let historyReady = false; @@ -176,6 +181,7 @@ for (const card of cards) { nodes.randomProblem.addEventListener("click", () => { if (!historyReady || accountUpdatePending() || deletingReports) return; manualProblem = false; + keptDraw = null; roll = Math.random(); avoidedProblem = problem?.id; applyDifficulties(); @@ -192,11 +198,20 @@ for (const input of levels) { } manualDifficulty = true; applyDifficulties(); - // A pick made by hand survives a filter that still includes it. Only one - // that now hides it hands the choice back to the lobby. - if (manualProblem && selectedDifficulties().has(problem?.difficulty)) + // A problem survives a filter that still includes it, whether it was + // picked by hand or drawn: the checkbox answered which levels to offer, + // not whether to throw away the problem on screen. Only a filter that now + // hides it hands the choice back to the lobby. + if (selectedDifficulties().has(problem?.difficulty)) { + // Kept, a drawn problem is no longer what `roll` draws under this + // filter, so the next `settle` would swap it. Held against the reports + // in hand, which while the history is still loading are the ones about + // to be replaced, so `settle` redraws then. + if (!manualProblem) keptDraw = JSON.stringify(reports); return; + } manualProblem = false; + keptDraw = null; roll = Math.random(); avoidedProblem = undefined; // Not before the reports are in. Recommending from an empty history here @@ -535,6 +550,10 @@ function refreshHistory() { /// describing history the page had already replaced. function settle() { historyReady = true; + // Refreshed reports that differ from the ones a kept draw came from may have + // just recorded it as passed, so the lobby draws again. + if (keptDraw !== null && keptDraw !== JSON.stringify(reports)) + keptDraw = null; // The level suggestion only applies when the candidate has not already said // what they want. Moving their checkboxes would also hide the card they just // picked. @@ -880,8 +899,9 @@ function applyDifficulties() { function recommend(note = "") { // A candidate who picked a card has answered the question this line asks, so // it stays answered. Naming a different problem here contradicted the card - // they had just selected. - if (manualProblem) return; + // they had just selected. A kept draw stands the same way until `settle` + // drops it. + if (manualProblem || keptDraw !== null) return; const choice = pickProblem( cards, selectedDifficulties(),