Skip to content

Commit 7e16db9

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 b183ee2 commit 7e16db9

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

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)