Skip to content

Commit 5f3010d

Browse files
committed
Re-enable the red box to show available space
Way back in #187, I introduced a hacky tool to show the available space on a slide: it was a `mdbook` plugin which injected the necessary CSS on each slide. Crude, but it got the job done. The logic was moved from Python to a real CSS file with associated JavaScript in #1842. In #1917, the functionality was moved to a dedicated “instructor menu”, together with functionality for saving the state of the Playground on each slide. Unfortunately, the whole thing was disabled in #1935 since I found that the Playgrounds lost their code with the saving logic. I was also not 100% happy with dedicating space on each slide for a menu only used by instructors. However, I really think we need a tool to let slide editors know about the available space, so I would like to re-introduce the red box. This time via a keyboard shortcut to make it easy to toggle as needed. I’m suggesting enabling this for everyone, with the expectation that most people won’t find the shortcut and will quickly disable the box if they do (there is a dedicated button to hide it again). End-to-end tests have been added for the new functionality.
1 parent 0a734e9 commit 5f3010d

File tree

10 files changed

+316
-108
lines changed

10 files changed

+316
-108
lines changed

.github/workflows/build.sh

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,6 @@ export MDBOOK_OUTPUT__PANDOC__DISABLED=false
4343

4444
mdbook build -d "$dest_dir"
4545

46-
# Disable the redbox button in built versions of the course
47-
echo '// Disabled in published builds, see build.sh' > "${dest_dir}/html/theme/redbox.js"
48-
4946
mv "$dest_dir/pandoc/pdf/comprehensive-rust.pdf" "$dest_dir/html/"
5047
(cd "$dest_dir/exerciser" && zip --recurse-paths ../html/comprehensive-rust-exercises.zip comprehensive-rust-exercises/)
5148

GEMINI.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,3 +171,86 @@ its tasks correctly.
171171
snippet, treat it as a self-contained program. Do not assume it shares a scope
172172
or context with other snippets in the same file unless the surrounding text
173173
explicitly states otherwise.
174+
175+
## Interacting with the `mdbook` Theme
176+
177+
The `mdbook` theme has several interactive elements. Here's how to interact with
178+
them:
179+
180+
- **Sidebar Toggle:** The sidebar can be opened and closed by clicking the
181+
"hamburger" button in the top-left of the body text. This button has the ID
182+
`sidebar-toggle`. You can use the following JavaScript to toggle the sidebar:
183+
184+
```javascript
185+
const button = document.getElementById("sidebar-toggle");
186+
button.click();
187+
```
188+
189+
## WebdriverIO Testing
190+
191+
This project uses WebdriverIO for browser-based integration tests. Here are some
192+
key findings about the test environment:
193+
194+
### Test Environments
195+
196+
The `tests/` directory contains two primary configurations:
197+
198+
- `npm test` (runs `wdio.conf.ts`): This is the standard for self-contained
199+
integration tests. It uses `@wdio/static-server-service` to create a temporary
200+
web server on port 8080.
201+
- `npm run test-mdbook` (runs `wdio.conf-mdbook.ts`): This is for testing
202+
against a live `mdbook serve` instance, which typically runs on port 3000.
203+
204+
It is important to use the standard `npm test` command for most test development
205+
to ensure the tests are self-contained.
206+
207+
### Writing Stable Tests
208+
209+
Tests can be flaky if they don't correctly handle the asynchronous nature of the
210+
web browser and the test environment's state management.
211+
212+
- **State Leakage Between Tests:** Despite what the WebdriverIO documentation
213+
might suggest, `browser.url()` is not always sufficient to guarantee a clean
214+
slate between tests. Lingering state, such as inline CSS styles applied by
215+
JavaScript, can leak from one test into the next, causing unexpected failures.
216+
The most effective solution found for this project is to add
217+
`await browser.refresh();` to the `beforeEach` hook. This forces a full page
218+
reload that properly clears the old state.
219+
220+
- **Race Conditions with Dynamic Elements:** Many elements in this project are
221+
created dynamically by JavaScript after the initial page load. If a test tries
222+
to access an element immediately after navigation, it may fail because the
223+
script hasn't finished running and the element doesn't exist in the DOM yet.
224+
This creates a race condition. To prevent this, always use
225+
`await element.waitForExist()` to ensure the element is present before trying
226+
to interact with it or assert its state (e.g., `toBeDisplayed()`).
227+
228+
### Handling Redirects
229+
230+
`mdbook` uses a redirect map defined in `book.toml` under the
231+
`[output.html.redirect]` section. When writing tests, it is crucial to use the
232+
final, non-redirecting URL for navigation. Navigating to a URL that is a
233+
redirect will cause the browser to follow it, but this process can strip URL
234+
query parameters, leading to test failures for features that depend on them.
235+
236+
### Running and Debugging Tests
237+
238+
To run a single test file, use the `--spec` flag with the a string matching the
239+
file name:
240+
241+
```bash
242+
npm test -- --spec redbox
243+
```
244+
245+
To check for flakiness, you can repeat a test multiple times using the
246+
`--repeat` flag:
247+
248+
```bash
249+
npm test -- --spec redbox --repeat 100
250+
```
251+
252+
Use `--mochaOpts.grep` to run a single test within a file:
253+
254+
```bash
255+
npm test -- --spec redbox --mochaOpts.grep "should be hidden by default"
256+
```

book.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ urlcolor = "red"
6060
smart-punctuation = true
6161
additional-js = [
6262
"theme/speaker-notes.js",
63+
"theme/redbox.js",
6364
]
6465
additional-css = [
6566
"theme/css/svgbob.css",

src/running-the-course.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ better. Your students are also very welcome to [send us feedback][2]!
5555
[1]: https://github.com/google/comprehensive-rust/discussions/86
5656
[2]: https://github.com/google/comprehensive-rust/discussions/100
5757
[3]: https://github.com/google/comprehensive-rust#building
58+
[red-box]: ?show-red-box=true
5859

5960
<details>
6061

@@ -69,6 +70,11 @@ better. Your students are also very welcome to [send us feedback][2]!
6970
- **Familiarize yourself with `mdbook`:** The course is presented using
7071
`mdbook`. Knowing how to navigate, search, and use its features will make the
7172
presentation smoother.
73+
- **Slice size helper:** Press <kbd>Ctrl</kbd> + <kbd>Alt</kbd> + <kbd>B</kbd>
74+
to toggle a visual guide showing the amount of space available when
75+
presenting. Expect any content outside of the red box to be hidden initially.
76+
Use this as a guide when editing slides. You can also
77+
[enable it via this link][red-box].
7278

7379
### Creating a Good Learning Environment
7480

tests/src/redbox.test.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { describe, it } from "mocha";
2+
import { expect, browser } from "@wdio/globals";
3+
4+
describe("Red Box", () => {
5+
const redBox = () => $("#aspect-ratio-helper");
6+
const redBoxButton = () => $("#turn-off-red-box");
7+
8+
beforeEach(async () => {
9+
await browser.url("/hello-world.html");
10+
await browser.execute(() => sessionStorage.clear());
11+
// Clear any lingering state (like inline styles) from previous
12+
// tests. Reading https://webdriver.io/docs/api/browser/url,
13+
// this should not be necessary, but tests fail without it.
14+
await browser.refresh();
15+
});
16+
17+
it("should be hidden by default", async () => {
18+
await expect(redBox()).not.toBeDisplayed();
19+
});
20+
21+
describe("Keyboard Shortcut", () => {
22+
it("should show the red box when toggled on", async () => {
23+
await browser.toggleRedBox();
24+
await expect(redBox()).toBeDisplayed();
25+
await expect(redBoxButton()).toBeDisplayed();
26+
});
27+
28+
it("should hide the red box when toggled off", async () => {
29+
// Toggle on first
30+
await browser.toggleRedBox();
31+
await expect(redBox()).toBeDisplayed();
32+
33+
// Then toggle off
34+
await browser.toggleRedBox();
35+
await expect(redBox()).not.toBeDisplayed();
36+
});
37+
});
38+
39+
describe("URL Parameter", () => {
40+
it("should show red box", async () => {
41+
await browser.url("/hello-world.html?show-red-box=true");
42+
await expect(redBox()).toBeDisplayed();
43+
});
44+
45+
it("should override session storage", async () => {
46+
// Set session storage first to ensure the URL parameter takes precedence.
47+
await browser.execute(() => sessionStorage.setItem("showRedBox", "true"));
48+
await browser.url("/hello-world.html?show-red-box=false");
49+
await expect(redBox()).not.toBeDisplayed();
50+
});
51+
});
52+
53+
describe("Hide Button", () => {
54+
it("should hide the red box when clicked", async () => {
55+
await browser.toggleRedBox();
56+
await expect(redBox()).toBeDisplayed();
57+
58+
await (await redBoxButton()).click();
59+
await expect(redBox()).not.toBeDisplayed();
60+
});
61+
});
62+
63+
describe("Session Storage", () => {
64+
it("should persist being shown after a reload", async () => {
65+
await browser.toggleRedBox();
66+
await expect(redBox()).toBeDisplayed();
67+
68+
await browser.refresh();
69+
70+
await expect(redBox()).toBeDisplayed();
71+
});
72+
73+
it("should persist being hidden after a reload", async () => {
74+
await browser.toggleRedBox(); // turn on
75+
await browser.toggleRedBox(); // turn off
76+
await expect(redBox()).not.toBeDisplayed();
77+
78+
// Explicitly check that storage is cleared before reloading
79+
const storage = await browser.execute(() =>
80+
sessionStorage.getItem("showRedBox"),
81+
);
82+
expect(storage).toBeNull();
83+
84+
await browser.refresh();
85+
await expect(redBox()).not.toBeDisplayed();
86+
});
87+
});
88+
89+
describe("Interactions", () => {
90+
it("should be able to be hidden with the keyboard after being shown with the URL", async () => {
91+
await browser.url("/hello-world.html?show-red-box=true");
92+
await expect(redBox()).toBeDisplayed();
93+
94+
await browser.toggleRedBox();
95+
await expect(redBox()).not.toBeDisplayed();
96+
});
97+
});
98+
});

tests/src/speaker-notes.test.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,17 @@ import { $, expect, browser } from "@wdio/globals";
33

44
describe("speaker-notes", () => {
55
beforeEach(async () => {
6-
await browser.url("/");
6+
await browser.url("/welcome-day-1.html");
7+
await browser.refresh();
8+
});
9+
10+
afterEach(async () => {
11+
const handles = await browser.getWindowHandles();
12+
if (handles.length > 1) {
13+
await browser.switchToWindow(handles[1]);
14+
await browser.closeWindow();
15+
await browser.switchToWindow(handles[0]);
16+
}
717
});
818

919
it("contains summary with heading and button", async () => {
@@ -17,7 +27,7 @@ describe("speaker-notes", () => {
1727
const details$ = await $("details");
1828
const button$ = await $("details summary .pop-out");
1929
await expect(details$).toBeDisplayed();
20-
button$.scrollIntoView();
30+
await button$.scrollIntoView();
2131
await button$.click();
2232
await expect(details$).not.toBeDisplayed();
2333

@@ -28,4 +38,16 @@ describe("speaker-notes", () => {
2838
expect.stringContaining("#speaker-notes-open"),
2939
);
3040
});
41+
42+
it("should not show the red box in the speaker notes window", async () => {
43+
const button$ = await $("details summary .pop-out");
44+
await button$.scrollIntoView();
45+
await button$.click();
46+
47+
const handles = await browser.getWindowHandles();
48+
await browser.switchToWindow(handles[1]);
49+
50+
const redBox = await $("#aspect-ratio-helper");
51+
await expect(redBox).not.toExist();
52+
});
3153
});

tests/wdio.conf.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,8 +213,37 @@ export const config: WebdriverIO.Config = {
213213
* @param {Array.<String>} specs List of spec file paths that are to be run
214214
* @param {object} browser instance of created browser/device session
215215
*/
216-
before: function (capabilities, specs) {
217-
browser.setWindowSize(2560, 1440);
216+
before: async function (capabilities, specs) {
217+
await browser.setWindowSize(2560, 1440);
218+
219+
/**
220+
* Adds a custom `browser.toggleRedBox()` command.
221+
*
222+
* This command is necessary to reliably test the red box toggle
223+
* functionality. A direct `browser.keys()` call proved to be
224+
* flaky, causing intermittent test failures. This custom command
225+
* will wait for the UI to reflect the state change, thus
226+
* eliminating race conditions.
227+
*/
228+
browser.addCommand("toggleRedBox", async function () {
229+
const redBox = await $("#aspect-ratio-helper");
230+
const initialVisibility = await redBox.isDisplayed();
231+
232+
// Perform the toggle action.
233+
await browser.keys(["Control", "Alt", "b"]);
234+
235+
// Wait until the visibility state has changed.
236+
await browser.waitUntil(
237+
async function () {
238+
const currentVisibility = await redBox.isDisplayed();
239+
return currentVisibility !== initialVisibility;
240+
},
241+
{
242+
timeout: 5000,
243+
timeoutMsg: `Red box display state did not toggle after 5s. Initial state: ${initialVisibility}`,
244+
},
245+
);
246+
});
218247
},
219248
/**
220249
* Runs before a WebdriverIO command gets executed.

theme/css/redbox.css

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ div#aspect-ratio-helper {
88
}
99

1010
div#aspect-ratio-helper div {
11-
outline: 3px dashed red;
11+
position: relative;
12+
outline: 2em solid rgba(255, 0, 0, 0.2);
1213
margin: 0 auto;
1314
/* At this width, the theme fonts are readable in a 16
1415
person conference room. If the browser is wider, the
@@ -19,6 +20,16 @@ div#aspect-ratio-helper div {
1920
aspect-ratio: 16/8.5;
2021
}
2122

22-
#instructor-menu-list {
23-
margin-left: 55px;
23+
#turn-off-red-box {
24+
position: absolute;
25+
bottom: 10px;
26+
right: 10px;
27+
z-index: 10000;
28+
padding: 10px;
29+
background-color: #f44336;
30+
color: white;
31+
border: none;
32+
border-radius: 5px;
33+
cursor: pointer;
34+
pointer-events: auto;
2435
}

0 commit comments

Comments
 (0)