Skip to content

Commit ea26024

Browse files
committed
Let the candidate resize the test results panel
The fixed drawer can hide long compiler output during an interview. Add an accessible splitter that preserves the selected height while keeping the editor, controls, and collapsed drawer usable.
1 parent 2f1907b commit ea26024

5 files changed

Lines changed: 376 additions & 6 deletions

File tree

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
// Run with: node --test tests/browser/test-drawer-resizer.test.js
2+
3+
import { after, before, test } from "node:test";
4+
import assert from "node:assert/strict";
5+
6+
import {
7+
DEFAULT_RUNTIME_CONFIG,
8+
launchChromium,
9+
startStaticServer,
10+
} from "./source.js";
11+
12+
const STORAGE_KEY = "codetrial:testDrawerHeight";
13+
14+
let browser = null;
15+
let server = null;
16+
let base = "";
17+
18+
before(async () => {
19+
browser = await launchChromium();
20+
if (!browser) return;
21+
({ server, base } = await startStaticServer({
22+
runtimeConfig: DEFAULT_RUNTIME_CONFIG,
23+
}));
24+
});
25+
26+
after(async () => {
27+
await browser?.close();
28+
await new Promise((closed) => (server ? server.close(closed) : closed()));
29+
});
30+
31+
async function openInterview(page) {
32+
await page.goto(`${base}/interview.html`, {
33+
waitUntil: "domcontentloaded",
34+
});
35+
await page.waitForFunction(
36+
() => document.querySelector("#test-drawer-resizer").ariaValueNow !== null,
37+
);
38+
await page.evaluate(() => {
39+
document.querySelector("#audio-check").hidden = true;
40+
});
41+
}
42+
43+
const layout = (page) =>
44+
page.evaluate((key) => {
45+
const rect = (selector) =>
46+
document.querySelector(selector).getBoundingClientRect();
47+
const handle = document.querySelector("#test-drawer-resizer");
48+
const drawerElement = document.querySelector("#test-drawer");
49+
const drawer = drawerElement.getBoundingClientRect();
50+
const editorElement = document.querySelector(".editor-stack");
51+
return {
52+
editor: editorElement.getBoundingClientRect().height,
53+
editorMin: Number.parseFloat(getComputedStyle(editorElement).minHeight),
54+
drawer: drawer.height,
55+
drawerTop: drawer.top,
56+
drawerClientHeight: drawerElement.clientHeight,
57+
drawerScrollHeight: drawerElement.scrollHeight,
58+
addCaseBottom: rect("#candidate-case").bottom,
59+
resultsToggleTop: rect("#results-toggle").top,
60+
resultsToggleBottom: rect("#results-toggle").bottom,
61+
resultsHidden: document.querySelector("#results-body").hidden,
62+
drawerBottom: drawer.bottom,
63+
min: Number(handle.ariaValueMin),
64+
max: Number(handle.ariaValueMax),
65+
now: Number(handle.ariaValueNow),
66+
stored: localStorage.getItem(key),
67+
};
68+
}, STORAGE_KEY);
69+
70+
async function carry(page, dy) {
71+
const box = await page.locator("#test-drawer-resizer").boundingBox();
72+
const x = box.x + box.width / 2;
73+
const y = box.y + box.height / 2;
74+
await page.mouse.move(x, y);
75+
await page.mouse.down();
76+
await page.mouse.move(x, y + dy, { steps: 4 });
77+
await page.evaluate(() => new Promise(requestAnimationFrame));
78+
await page.mouse.up();
79+
}
80+
81+
test("the test drawer resizes vertically and remembers the chosen height", async (t) => {
82+
if (!browser) return t.skip("playwright chromium unavailable");
83+
const page = await browser.newPage({
84+
viewport: { width: 1280, height: 720 },
85+
});
86+
try {
87+
await openInterview(page);
88+
const initial = await layout(page);
89+
assert.equal(initial.stored, null);
90+
91+
await carry(page, -48);
92+
const taller = await layout(page);
93+
assert.equal(taller.drawer, Math.round(initial.drawer) + 48);
94+
assert.equal(taller.editor, initial.editor - 48);
95+
assert.equal(taller.now, taller.drawer);
96+
assert.equal(taller.stored, String(taller.drawer));
97+
98+
await page.focus("#test-drawer-resizer");
99+
await page.keyboard.press("End");
100+
const chosen = await layout(page);
101+
assert.ok(chosen.drawer > taller.drawer);
102+
assert.equal(Math.round(Number(chosen.stored)), chosen.drawer);
103+
104+
await page.click("#results-toggle");
105+
await page.waitForFunction(
106+
() =>
107+
Number(document.querySelector("#test-drawer-resizer").ariaValueNow) ===
108+
Math.round(
109+
document.querySelector("#test-drawer").getBoundingClientRect().height,
110+
),
111+
);
112+
const collapsed = await layout(page);
113+
assert.equal(collapsed.resultsHidden, true);
114+
assert.equal(await page.locator("#test-drawer-resizer").isHidden(), true);
115+
assert.ok(collapsed.drawer < chosen.drawer);
116+
assert.equal(collapsed.now, Math.round(collapsed.drawer));
117+
assert.equal(collapsed.stored, chosen.stored);
118+
119+
await page.click("#results-toggle");
120+
await page.waitForFunction(
121+
(height) =>
122+
document.querySelector("#test-drawer").getBoundingClientRect()
123+
.height === height &&
124+
Number(document.querySelector("#test-drawer-resizer").ariaValueNow) ===
125+
height,
126+
chosen.drawer,
127+
);
128+
const reopened = await layout(page);
129+
assert.equal(reopened.resultsHidden, false);
130+
assert.equal(await page.locator("#test-drawer-resizer").isVisible(), true);
131+
assert.equal(reopened.now, chosen.drawer);
132+
133+
await page.locator(".editor-toolbar").evaluate((toolbar) => {
134+
toolbar.style.minHeight = "10rem";
135+
});
136+
await page.waitForFunction(
137+
(height) =>
138+
document.querySelector("#test-drawer").getBoundingClientRect().height <
139+
height,
140+
chosen.drawer,
141+
);
142+
const toolbarClamped = await layout(page);
143+
assert.ok(toolbarClamped.drawer < chosen.drawer);
144+
assert.ok(toolbarClamped.editor >= toolbarClamped.editorMin - 1);
145+
assert.equal(toolbarClamped.stored, chosen.stored);
146+
147+
await page.locator(".editor-toolbar").evaluate((toolbar) => {
148+
toolbar.style.removeProperty("min-height");
149+
});
150+
await page.waitForFunction(
151+
(height) =>
152+
document.querySelector("#test-drawer").getBoundingClientRect()
153+
.height === height,
154+
chosen.drawer,
155+
);
156+
157+
await page.setViewportSize({ width: 1280, height: 640 });
158+
await page.waitForFunction(
159+
(height) =>
160+
document.querySelector("#test-drawer").getBoundingClientRect().height <
161+
height,
162+
chosen.drawer,
163+
);
164+
const constrained = await layout(page);
165+
assert.ok(constrained.drawer < chosen.drawer);
166+
assert.equal(constrained.stored, chosen.stored);
167+
168+
await page.setViewportSize({ width: 1280, height: 720 });
169+
await page.waitForFunction(
170+
(height) =>
171+
document.querySelector("#test-drawer").getBoundingClientRect()
172+
.height === height,
173+
chosen.drawer,
174+
);
175+
const restored = await layout(page);
176+
assert.equal(restored.drawer, chosen.drawer);
177+
assert.equal(restored.stored, chosen.stored);
178+
179+
await openInterview(page);
180+
const reloaded = await layout(page);
181+
assert.equal(reloaded.drawer, chosen.drawer);
182+
assert.equal(reloaded.now, chosen.drawer);
183+
} finally {
184+
await page.close();
185+
}
186+
});
187+
188+
test("the horizontal divider supports keys and keeps both panels usable", async (t) => {
189+
if (!browser) return t.skip("playwright chromium unavailable");
190+
const page = await browser.newPage({
191+
viewport: { width: 1280, height: 720 },
192+
});
193+
try {
194+
await openInterview(page);
195+
const initial = await layout(page);
196+
await page.focus("#test-drawer-resizer");
197+
198+
await page.keyboard.press("ArrowUp");
199+
assert.equal((await layout(page)).drawer, Math.round(initial.drawer) + 16);
200+
await page.keyboard.press("ArrowDown");
201+
assert.equal((await layout(page)).drawer, Math.round(initial.drawer));
202+
203+
await page.locator("#candidate-case > summary").click();
204+
await page.focus("#test-drawer-resizer");
205+
await page.keyboard.press("Home");
206+
const smallest = await layout(page);
207+
assert.equal(smallest.drawer, smallest.min);
208+
assert.ok(smallest.addCaseBottom <= smallest.resultsToggleTop);
209+
assert.ok(smallest.resultsToggleBottom <= smallest.drawerBottom);
210+
211+
await page.locator("#candidate-case-list").evaluate((list) => {
212+
list.innerHTML = Array.from(
213+
{ length: 8 },
214+
(_, index) => `<li>Candidate case ${index + 1}</li>`,
215+
).join("");
216+
});
217+
const overflowing = await layout(page);
218+
assert.ok(overflowing.drawerScrollHeight > overflowing.drawerClientHeight);
219+
await page.locator("#results-toggle").scrollIntoViewIfNeeded();
220+
const scrolled = await layout(page);
221+
assert.ok(scrolled.resultsToggleTop >= scrolled.drawerTop - 1);
222+
assert.ok(scrolled.resultsToggleBottom <= scrolled.drawerBottom + 1);
223+
await page.locator("#candidate-case-list").evaluate((list) => {
224+
list.replaceChildren();
225+
document.querySelector("#test-drawer").scrollTop = 0;
226+
});
227+
await page.focus("#test-drawer-resizer");
228+
await page.keyboard.press("End");
229+
const largest = await layout(page);
230+
assert.equal(largest.drawer, largest.max);
231+
const shared = largest.editor + largest.drawer;
232+
assert.ok(
233+
largest.drawer >= shared - 97,
234+
`drawer leaves more than 6rem of the ${shared}px shared room unused`,
235+
);
236+
assert.ok(largest.editor >= 95, `editor is only ${largest.editor}px tall`);
237+
238+
await page.locator("#candidate-case > summary").click();
239+
await page.focus("#test-drawer-resizer");
240+
await page.keyboard.press("Enter");
241+
const reset = await layout(page);
242+
assert.equal(reset.drawer, initial.drawer);
243+
assert.equal(reset.stored, null);
244+
} finally {
245+
await page.close();
246+
}
247+
});
248+
249+
test("automatic drawer sizing keeps a usable editor", async (t) => {
250+
if (!browser) return t.skip("playwright chromium unavailable");
251+
const page = await browser.newPage({
252+
viewport: { width: 1280, height: 600 },
253+
});
254+
try {
255+
await openInterview(page);
256+
await page.locator("#candidate-case > summary").click();
257+
await page.evaluate(() => {
258+
document.querySelector("#candidate-case-list").innerHTML = Array.from(
259+
{ length: 8 },
260+
(_, index) => `<li>Candidate case ${index + 1}</li>`,
261+
).join("");
262+
document.querySelector("#results-body").innerHTML =
263+
`<pre>${"compiler output\n".repeat(80)}</pre>`;
264+
});
265+
const crowded = await layout(page);
266+
assert.equal(crowded.editorMin, 96);
267+
assert.ok(crowded.editor >= crowded.editorMin - 1);
268+
} finally {
269+
await page.close();
270+
}
271+
});
272+
273+
test("disabled execution hides the test drawer divider", async (t) => {
274+
if (!browser) return t.skip("playwright chromium unavailable");
275+
const page = await browser.newPage();
276+
try {
277+
await page.goto(`${base}/interview.html?editorExecution=0`, {
278+
waitUntil: "domcontentloaded",
279+
});
280+
const divider = page.locator("#test-drawer-resizer");
281+
assert.equal(await divider.isHidden(), true);
282+
assert.equal(await page.locator("#test-drawer").isHidden(), true);
283+
} finally {
284+
await page.close();
285+
}
286+
});

‎web/interview.html‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,16 @@ <h1 id="problem-title">Loading interview...</h1>
253253
aria-label="Code editor"
254254
></textarea>
255255
</div>
256-
<section class="test-drawer">
256+
<div
257+
id="test-drawer-resizer"
258+
class="splitter test-drawer-resizer"
259+
role="separator"
260+
aria-orientation="horizontal"
261+
aria-label="Resize test panel"
262+
aria-controls="test-drawer"
263+
tabindex="0"
264+
></div>
265+
<section id="test-drawer" class="test-drawer">
257266
<details id="candidate-case">
258267
<summary>Add a case</summary>
259268
<label

‎web/interview.js‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ const HIDE_EXAMPLES_KEY = "codetrial:hideExamples";
209209
const EDITOR_FONT_SIZE_KEY = "codetrial:editorFontSize";
210210
const EDITOR_THEME_KEY = "codetrial:editorTheme";
211211
const SIDEBAR_WIDTH_KEY = "codetrial:sidebarWidth";
212+
const TEST_DRAWER_HEIGHT_KEY = "codetrial:testDrawerHeight";
212213
// The sidebar's narrowest is the width it had before it could be resized. The
213214
// panel beside it keeps more than its widest toolbar item, the row of language
214215
// tabs, needs, so the toolbar wraps but nothing in it is cut off.
@@ -511,6 +512,8 @@ const nodes = {
511512
compileDisclosure: document.querySelector(".compile-disclosure"),
512513
run: document.querySelector("#run-tests"),
513514
testDrawer: document.querySelector(".test-drawer"),
515+
testDrawerResizer: document.querySelector("#test-drawer-resizer"),
516+
candidateCase: document.querySelector("#candidate-case"),
514517
candidateCaseInput: document.querySelector("#candidate-case-input"),
515518
candidateCaseExpected: document.querySelector("#candidate-case-expected"),
516519
candidateCaseAdd: document.querySelector("#candidate-case-add"),
@@ -628,6 +631,17 @@ async function init() {
628631
storageKey: SIDEBAR_WIDTH_KEY,
629632
bounds: sidebarBounds,
630633
});
634+
attachSplitter({
635+
handle: nodes.testDrawerResizer,
636+
pane: nodes.testDrawer,
637+
axis: "y",
638+
side: "after",
639+
target: nodes.editorPanel,
640+
property: "--test-drawer-height",
641+
storageKey: TEST_DRAWER_HEIGHT_KEY,
642+
bounds: testDrawerBounds,
643+
resizeTargets: [nodes.editorStack, nodes.editorToolbar],
644+
});
631645
compactJimStageBesideNarrowPanel(
632646
whiteboard ? nodes.boardPanel : nodes.editorPanel,
633647
);
@@ -676,6 +690,7 @@ async function init() {
676690

677691
function renderRuntimeConfig() {
678692
nodes.run.hidden = !editorOptions.execution;
693+
nodes.testDrawerResizer.hidden = !editorOptions.execution;
679694
nodes.testDrawer.hidden = !editorOptions.execution;
680695
// No origin literal here: /runtime-config.js supplies it, and the server is
681696
// the only place that may name it, because the same value builds the CSP.
@@ -3543,6 +3558,30 @@ function sidebarBounds() {
35433558
};
35443559
}
35453560

3561+
/// How tall the test drawer may be. Its two toggles remain visible at the
3562+
/// minimum. At the maximum only a short, still usable editor remains, so long
3563+
/// output can fill almost all of the coding area. Their heights are summed
3564+
/// because that is the room the divider trades between them.
3565+
function testDrawerBounds() {
3566+
const drawerTop = nodes.testDrawer.getBoundingClientRect().top;
3567+
const min =
3568+
nodes.candidateCase.getBoundingClientRect().bottom -
3569+
drawerTop +
3570+
nodes.resultsToggle.getBoundingClientRect().height;
3571+
const room =
3572+
nodes.editorPanel.clientHeight -
3573+
nodes.editorToolbar.offsetHeight -
3574+
nodes.testDrawerResizer.offsetHeight;
3575+
const rootFontSize = Number.parseFloat(
3576+
getComputedStyle(document.documentElement).fontSize,
3577+
);
3578+
const editorMin = rootFontSize * 6;
3579+
return {
3580+
min,
3581+
max: Math.max(min, room - editorMin),
3582+
};
3583+
}
3584+
35463585
/// Takes the avatar off a panel too narrow to carry it, leaving the captions,
35473586
/// as the stylesheet does for a narrow window. The window's width stopped
35483587
/// being enough to tell once the sidebar could be widened: at 1280px the

0 commit comments

Comments
 (0)