Skip to content

Commit 8394d54

Browse files
authored
Merge pull request #303 from domin67/resize-test-results-panel
Let the candidate resize the test results panel
2 parents 59bde8f + ea26024 commit 8394d54

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
@@ -210,6 +210,7 @@ const HIDE_EXAMPLES_KEY = "codetrial:hideExamples";
210210
const EDITOR_FONT_SIZE_KEY = "codetrial:editorFontSize";
211211
const EDITOR_THEME_KEY = "codetrial:editorTheme";
212212
const SIDEBAR_WIDTH_KEY = "codetrial:sidebarWidth";
213+
const TEST_DRAWER_HEIGHT_KEY = "codetrial:testDrawerHeight";
213214
// The sidebar's narrowest is the width it had before it could be resized. The
214215
// panel beside it keeps more than its widest toolbar item, the row of language
215216
// tabs, needs, so the toolbar wraps but nothing in it is cut off.
@@ -512,6 +513,8 @@ const nodes = {
512513
compileDisclosure: document.querySelector(".compile-disclosure"),
513514
run: document.querySelector("#run-tests"),
514515
testDrawer: document.querySelector(".test-drawer"),
516+
testDrawerResizer: document.querySelector("#test-drawer-resizer"),
517+
candidateCase: document.querySelector("#candidate-case"),
515518
candidateCaseInput: document.querySelector("#candidate-case-input"),
516519
candidateCaseExpected: document.querySelector("#candidate-case-expected"),
517520
candidateCaseAdd: document.querySelector("#candidate-case-add"),
@@ -629,6 +632,17 @@ async function init() {
629632
storageKey: SIDEBAR_WIDTH_KEY,
630633
bounds: sidebarBounds,
631634
});
635+
attachSplitter({
636+
handle: nodes.testDrawerResizer,
637+
pane: nodes.testDrawer,
638+
axis: "y",
639+
side: "after",
640+
target: nodes.editorPanel,
641+
property: "--test-drawer-height",
642+
storageKey: TEST_DRAWER_HEIGHT_KEY,
643+
bounds: testDrawerBounds,
644+
resizeTargets: [nodes.editorStack, nodes.editorToolbar],
645+
});
632646
compactJimStageBesideNarrowPanel(
633647
whiteboard ? nodes.boardPanel : nodes.editorPanel,
634648
);
@@ -677,6 +691,7 @@ async function init() {
677691

678692
function renderRuntimeConfig() {
679693
nodes.run.hidden = !editorOptions.execution;
694+
nodes.testDrawerResizer.hidden = !editorOptions.execution;
680695
nodes.testDrawer.hidden = !editorOptions.execution;
681696
// No origin literal here: /runtime-config.js supplies it, and the server is
682697
// the only place that may name it, because the same value builds the CSP.
@@ -3545,6 +3560,30 @@ function sidebarBounds() {
35453560
};
35463561
}
35473562

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

0 commit comments

Comments
 (0)