Skip to content

Commit f056d58

Browse files
committed
Download selected interview reports
Let candidates save multiple attempts without repetitive downloads.
1 parent 2acb3d3 commit f056d58

4 files changed

Lines changed: 308 additions & 3 deletions

File tree

‎tests/browser/lobby.test.js‎

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3732,6 +3732,218 @@ for (const storage of ["account", "device"]) {
37323732
);
37333733
}
37343734

3735+
/// Names every report the page asks the browser to save, at the moment it
3736+
/// asks, so a test can say exactly which files one click produced rather than
3737+
/// waiting for download events that may still be on their way.
3738+
const recordDownloadNames = (page) =>
3739+
page.addInitScript(() => {
3740+
window.downloadNames = [];
3741+
const click = HTMLAnchorElement.prototype.click;
3742+
HTMLAnchorElement.prototype.click = function () {
3743+
if (this.download) window.downloadNames.push(this.download);
3744+
return click.call(this);
3745+
};
3746+
});
3747+
const takeDownloadNames = (page) =>
3748+
page.evaluate(() => window.downloadNames.splice(0).sort());
3749+
3750+
for (const storage of ["account", "device"]) {
3751+
lobbyTest(
3752+
`saved ${storage} reports download the ticked ones as their own markdown`,
3753+
async (page) => {
3754+
const reportsSaved = [
3755+
{ decision: "HIRE", codingScore: 81 },
3756+
{ decision: "NO_HIRE", codingScore: 42 },
3757+
{ incomplete: true, decision: "HIRE" },
3758+
];
3759+
const entries = reportsSaved.map((report, index) => ({
3760+
problemId: EASY[0],
3761+
problemTitle: "Saved report",
3762+
date: `2026-01-01T${17 + index}:30:00Z`,
3763+
language: "python",
3764+
report: { ...report, summary: `Stored summary ${index}` },
3765+
}));
3766+
if (storage === "account") {
3767+
reports = entries.map((payload) => ({ payload }));
3768+
} else {
3769+
session = { signedIn: false };
3770+
await page.addInitScript((entries) => {
3771+
localStorage.setItem("codetrial_history", JSON.stringify(entries));
3772+
}, entries);
3773+
}
3774+
await recordDownloadNames(page);
3775+
await lobby(page);
3776+
const history = page.locator("#attempt-history");
3777+
const boxes = history.getByRole("checkbox");
3778+
const toggle = page.locator("#attempt-select-all");
3779+
const downloadSelected = page.locator("#attempt-download-selected");
3780+
const note = page.locator("#attempt-download-note");
3781+
const when = (index) => `Jan 2, 2026, ${index + 1}:30 AM`;
3782+
const box = (index) =>
3783+
history.getByRole("checkbox", { name: `${when(index)} · ` });
3784+
const filename = (index) =>
3785+
`interview-report-${EASY[0]}-2026-01-02-0${index + 1}3000.md`;
3786+
const text = async (download) => {
3787+
const chunks = [];
3788+
for await (const chunk of await download.createReadStream())
3789+
chunks.push(chunk);
3790+
return Buffer.concat(chunks).toString("utf8");
3791+
};
3792+
3793+
assert.equal(await boxes.count(), reportsSaved.length);
3794+
for (const box of await boxes.all()) assert.ok(await box.isChecked());
3795+
assert.equal(await toggle.textContent(), "Deselect all");
3796+
assert.equal(
3797+
await downloadSelected.textContent(),
3798+
"Download selected (3)",
3799+
);
3800+
3801+
// The description is the checkbox's label, so clicking the text toggles.
3802+
await history.locator("label", { hasText: when(1) }).click();
3803+
assert.equal(await box(1).isChecked(), false);
3804+
assert.equal(await toggle.textContent(), "Select all");
3805+
assert.equal(
3806+
await downloadSelected.textContent(),
3807+
"Download selected (2)",
3808+
);
3809+
3810+
const batch = new Map();
3811+
let onDownload;
3812+
let timer;
3813+
const arrived = new Promise((resolve, reject) => {
3814+
onDownload = (download) => {
3815+
batch.set(download.suggestedFilename(), download);
3816+
if (batch.size === 2) resolve();
3817+
};
3818+
// node:test has no default timeout, so a download that never starts
3819+
// fails here with what did arrive instead of hanging the suite.
3820+
timer = setTimeout(() => {
3821+
const names = [...batch.keys()].join(", ") || "none";
3822+
reject(new Error(`only these downloads arrived: ${names}`));
3823+
}, 10_000);
3824+
});
3825+
page.on("download", onDownload);
3826+
try {
3827+
await downloadSelected.click();
3828+
assert.deepEqual(await takeDownloadNames(page), [
3829+
filename(0),
3830+
filename(2),
3831+
]);
3832+
assert.equal(
3833+
await note.textContent(),
3834+
"Your browser may ask to allow multiple downloads from this page. If only the first file arrived, allow automatic downloads for this site in the browser's site settings and try again.",
3835+
);
3836+
await arrived;
3837+
} finally {
3838+
clearTimeout(timer);
3839+
page.off("download", onDownload);
3840+
}
3841+
3842+
// The batch reuses the row's own download, so each file it writes is the
3843+
// one that row's button writes, an incomplete report included.
3844+
for (const index of [0, 2]) {
3845+
const downloading = page.waitForEvent("download");
3846+
await history
3847+
.getByRole("button", {
3848+
name: `Download report (.md) for Saved report, ${when(index)}`,
3849+
exact: true,
3850+
})
3851+
.click();
3852+
const single = await downloading;
3853+
assert.equal(single.suggestedFilename(), filename(index));
3854+
const markdown = await text(single);
3855+
assert.ok(markdown.includes(`Stored summary ${index}`));
3856+
assert.equal(await text(batch.get(filename(index))), markdown);
3857+
}
3858+
await takeDownloadNames(page);
3859+
3860+
await toggle.click();
3861+
for (const box of await boxes.all()) assert.ok(await box.isChecked());
3862+
assert.equal(await toggle.textContent(), "Deselect all");
3863+
assert.equal(
3864+
await downloadSelected.textContent(),
3865+
"Download selected (3)",
3866+
);
3867+
3868+
await toggle.click();
3869+
for (const box of await boxes.all())
3870+
assert.equal(await box.isChecked(), false);
3871+
assert.equal(await toggle.textContent(), "Select all");
3872+
assert.equal(
3873+
await downloadSelected.textContent(),
3874+
"Download selected (0)",
3875+
);
3876+
assert.equal(await downloadSelected.isDisabled(), true);
3877+
},
3878+
{ timezoneId: "Asia/Taipei", locale: "en-US", acceptDownloads: true },
3879+
);
3880+
}
3881+
3882+
lobbyTest(
3883+
"a filtered attempt list keeps its ticks and downloads only what it shows",
3884+
async (page) => {
3885+
const attempt = (id, language, day) => ({
3886+
id,
3887+
problemId: EASY[0],
3888+
payload: {
3889+
id,
3890+
problemId: EASY[0],
3891+
problemTitle: `Attempt ${id}`,
3892+
language,
3893+
date: `2026-01-0${day}T00:00:00Z`,
3894+
report: { decision: "HIRE" },
3895+
},
3896+
});
3897+
reports = [
3898+
attempt("a", "python", 1),
3899+
attempt("b", "cpp", 2),
3900+
attempt("c", "python", 3),
3901+
];
3902+
await recordDownloadNames(page);
3903+
await lobby(page);
3904+
const box = (id) =>
3905+
page.getByRole("checkbox", { name: ` · Attempt ${id} · ` });
3906+
const toggle = page.locator("#attempt-select-all");
3907+
const downloadSelected = page.locator("#attempt-download-selected");
3908+
const note = page.locator("#attempt-download-note");
3909+
const filename = (day) =>
3910+
`interview-report-${EASY[0]}-2026-01-0${day}-080000.md`;
3911+
3912+
await box("a").uncheck();
3913+
await page.selectOption("#progress-language", "python");
3914+
assert.equal(await box("a").isChecked(), false);
3915+
assert.equal(await box("c").isChecked(), true);
3916+
assert.equal(await toggle.textContent(), "Select all");
3917+
assert.equal(await downloadSelected.textContent(), "Download selected (1)");
3918+
await downloadSelected.click();
3919+
assert.deepEqual(await takeDownloadNames(page), [filename(3)]);
3920+
// One file is one download, which no browser asks about.
3921+
assert.equal(await note.textContent(), "");
3922+
3923+
await page.selectOption("#progress-language", "cpp");
3924+
assert.equal(await box("b").isChecked(), true);
3925+
assert.equal(await toggle.textContent(), "Deselect all");
3926+
await downloadSelected.click();
3927+
assert.deepEqual(await takeDownloadNames(page), [filename(2)]);
3928+
await toggle.click();
3929+
assert.equal(await box("b").isChecked(), false);
3930+
assert.equal(await downloadSelected.isDisabled(), true);
3931+
3932+
await page.selectOption("#progress-language", "all");
3933+
assert.equal(await box("a").isChecked(), false);
3934+
assert.equal(await box("b").isChecked(), false);
3935+
assert.equal(await box("c").isChecked(), true);
3936+
assert.equal(await toggle.textContent(), "Select all");
3937+
assert.equal(await downloadSelected.textContent(), "Download selected (1)");
3938+
// Some ticked is not all ticked, so the toggle ticks the rest.
3939+
await toggle.click();
3940+
for (const id of ["a", "b", "c"])
3941+
assert.equal(await box(id).isChecked(), true);
3942+
assert.equal(await downloadSelected.textContent(), "Download selected (3)");
3943+
},
3944+
{ timezoneId: "Asia/Taipei", locale: "en-US" },
3945+
);
3946+
37353947
lobbyTest(
37363948
"try again selects a problem without escaping an active title search",
37373949
async (page) => {

‎web/app.js‎

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,10 @@ const nodes = {
130130
),
131131
progressSummary: document.querySelector("#progress-summary"),
132132
attemptHistory: document.querySelector("#attempt-history"),
133+
attemptSelection: document.querySelector("#attempt-selection"),
134+
attemptSelectAll: document.querySelector("#attempt-select-all"),
135+
attemptDownloadSelected: document.querySelector("#attempt-download-selected"),
136+
attemptDownloadNote: document.querySelector("#attempt-download-note"),
133137
progressTrends: document.querySelector("#progress-trends"),
134138
progressWeaknesses: document.querySelector("#progress-weaknesses"),
135139
progressTopics: document.querySelector("#progress-topics"),
@@ -487,6 +491,12 @@ nodes.groundingClear.addEventListener("click", clearGrounding);
487491

488492
let progressNormalized = [];
489493
let progressSuffix = "saved";
494+
/// The rows of `progressNormalized` unticked for "Download selected". Keyed on
495+
/// the row objects: a filter change reuses them, so it keeps every choice, and
496+
/// a reload builds new ones that were never unticked, so it starts all ticked.
497+
const unselectedAttempts = new WeakSet();
498+
/// What the attempt list draws now: the only reports a batch download takes.
499+
let shownAttempts = [];
490500
const selectedProgressPhases = new Map();
491501
/// Whether the history on screen came from an account: null until /api/session
492502
/// answers, and never assumed false, because a browser that cannot ask is not a
@@ -501,6 +511,29 @@ for (const filter of [
501511
filter.addEventListener("change", renderProgress);
502512
}
503513
nodes.deleteReports.addEventListener("click", deleteSavedReports);
514+
nodes.attemptSelectAll.addEventListener("click", () => {
515+
const select = !allAttemptsSelected();
516+
for (const attempt of shownAttempts) {
517+
if (select) unselectedAttempts.delete(attempt);
518+
else unselectedAttempts.add(attempt);
519+
}
520+
for (const box of nodes.attemptHistory.querySelectorAll(
521+
"[data-attempt-select]",
522+
))
523+
box.checked = select;
524+
syncAttemptSelection();
525+
});
526+
nodes.attemptDownloadSelected.addEventListener("click", () => {
527+
const selected = selectedAttempts();
528+
for (const attempt of selected) downloadSavedReport(attempt);
529+
// Chrome saves the first file of one click and asks before the rest, or
530+
// drops them silently once the site is blocked. Neither reaches the page, so
531+
// the note says how to recover rather than what happened.
532+
nodes.attemptDownloadNote.textContent =
533+
selected.length > 1
534+
? "Your browser may ask to allow multiple downloads from this page. If only the first file arrived, allow automatic downloads for this site in the browser's site settings and try again."
535+
: "";
536+
});
504537

505538
const durations = [...document.querySelectorAll("[data-duration]")];
506539
for (const button of durations) {
@@ -1528,6 +1561,7 @@ function showProgressError(message) {
15281561
// history it had last managed to load.
15291562
reports = [];
15301563
progressNormalized = [];
1564+
shownAttempts = [];
15311565
renderPracticeFocus();
15321566
hideRecentPerformance();
15331567
nodes.historyHeader.hidden = false;
@@ -1537,6 +1571,7 @@ function showProgressError(message) {
15371571
nodes.progressWeaknesses.replaceChildren();
15381572
nodes.progressTopics.replaceChildren();
15391573
nodes.attemptHistory.replaceChildren();
1574+
syncAttemptSelection();
15401575
}
15411576

15421577
function showProgress(entries, suffix) {
@@ -1681,6 +1716,7 @@ function renderRecentPerformance() {
16811716
/// and it cost one more sanitizing pass over every stored report.
16821717
function renderAttemptHistory(attempts) {
16831718
nodes.attemptHistory.replaceChildren();
1719+
shownAttempts = attempts;
16841720
for (const attempt of [...attempts].reverse()) {
16851721
const item = document.createElement("li");
16861722
const when = new Date(attempt.at).toLocaleString(undefined, {
@@ -1690,8 +1726,22 @@ function renderAttemptHistory(attempts) {
16901726
const verdict = attempt.report.incomplete
16911727
? "INCOMPLETE"
16921728
: (attempt.report.decision ?? "UNSCORED");
1693-
const label = document.createElement("p");
1694-
label.textContent = `${when} · ${attempt.problemTitle} · ${attempt.difficulty ?? "Difficulty not recorded"} · ${languageLabel(attempt.language ?? "not recorded")} · ${verdict}`;
1729+
const select = document.createElement("input");
1730+
select.type = "checkbox";
1731+
select.dataset.attemptSelect = "";
1732+
select.checked = !unselectedAttempts.has(attempt);
1733+
select.addEventListener("change", () => {
1734+
if (select.checked) unselectedAttempts.delete(attempt);
1735+
else unselectedAttempts.add(attempt);
1736+
syncAttemptSelection();
1737+
});
1738+
const label = document.createElement("label");
1739+
label.append(
1740+
select,
1741+
`${when} · ${attempt.problemTitle} · ${attempt.difficulty ?? "Difficulty not recorded"} · ${languageLabel(attempt.language ?? "not recorded")} · ${verdict}`,
1742+
);
1743+
const line = document.createElement("p");
1744+
line.append(label);
16951745
const open = document.createElement("button");
16961746
open.type = "button";
16971747
open.textContent = "Open report";
@@ -1756,7 +1806,7 @@ function renderAttemptHistory(attempts) {
17561806
: "";
17571807
nodes.recommendation.textContent = `Selected problem: ${title(card)}.${adjustment}`;
17581808
});
1759-
item.append(label, open, download, retry);
1809+
item.append(line, open, download, retry);
17601810
// A row with no id has nothing both stores and the account agree on, and
17611811
// deleting by anything weaker could take a different report than this one.
17621812
if (attempt.id) {
@@ -1777,6 +1827,29 @@ function renderAttemptHistory(attempts) {
17771827
item.append(report);
17781828
nodes.attemptHistory.append(item);
17791829
}
1830+
syncAttemptSelection();
1831+
}
1832+
1833+
function selectedAttempts() {
1834+
return shownAttempts.filter((attempt) => !unselectedAttempts.has(attempt));
1835+
}
1836+
1837+
function allAttemptsSelected() {
1838+
return selectedAttempts().length === shownAttempts.length;
1839+
}
1840+
1841+
function syncAttemptSelection() {
1842+
nodes.attemptDownloadNote.textContent = "";
1843+
nodes.attemptSelection.hidden = shownAttempts.length === 0;
1844+
// With nothing listed, a count here would be the lobby speaking before any
1845+
// history arrived; the labels are written when there is something to count.
1846+
if (nodes.attemptSelection.hidden) return;
1847+
const selected = selectedAttempts().length;
1848+
nodes.attemptSelectAll.textContent = allAttemptsSelected()
1849+
? "Deselect all"
1850+
: "Select all";
1851+
nodes.attemptDownloadSelected.textContent = `Download selected (${selected})`;
1852+
nodes.attemptDownloadSelected.disabled = selected === 0;
17801853
}
17811854

17821855
function downloadSavedReport(attempt) {

‎web/index.html‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1990,6 +1990,13 @@ <h2 id="progress-title">Interview progress</h2>
19901990
<p id="progress-summary" aria-live="polite"></p>
19911991
<section aria-labelledby="attempt-history-title">
19921992
<h3 id="attempt-history-title">Past attempts</h3>
1993+
<div id="attempt-selection" class="attempt-selection" hidden>
1994+
<button id="attempt-select-all" type="button">Deselect all</button>
1995+
<button id="attempt-download-selected" type="button">
1996+
Download selected (0)
1997+
</button>
1998+
</div>
1999+
<p id="attempt-download-note" class="small" role="status"></p>
19932000
<ol id="attempt-history"></ol>
19942001
</section>
19952002
<div id="progress-trends" class="progress-trends"></div>

0 commit comments

Comments
 (0)