Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .github/workflows/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,6 @@ export MDBOOK_OUTPUT__PANDOC__DISABLED=false

mdbook build -d "$dest_dir"

# Disable the redbox button in built versions of the course
echo '// Disabled in published builds, see build.sh' > "${dest_dir}/html/theme/redbox.js"

mv "$dest_dir/pandoc/pdf/comprehensive-rust.pdf" "$dest_dir/html/"
(cd "$dest_dir/exerciser" && zip --recurse-paths ../html/comprehensive-rust-exercises.zip comprehensive-rust-exercises/)

Expand Down
83 changes: 83 additions & 0 deletions GEMINI.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,86 @@ its tasks correctly.
snippet, treat it as a self-contained program. Do not assume it shares a scope
or context with other snippets in the same file unless the surrounding text
explicitly states otherwise.

## Interacting with the `mdbook` Theme

The `mdbook` theme has several interactive elements. Here's how to interact with
them:

- **Sidebar Toggle:** The sidebar can be opened and closed by clicking the
"hamburger" button in the top-left of the body text. This button has the ID
`sidebar-toggle`. You can use the following JavaScript to toggle the sidebar:

```javascript
const button = document.getElementById("sidebar-toggle");
button.click();
```

## WebdriverIO Testing

This project uses WebdriverIO for browser-based integration tests. Here are some
key findings about the test environment:

### Test Environments

The `tests/` directory contains two primary configurations:

- `npm test` (runs `wdio.conf.ts`): This is the standard for self-contained
integration tests. It uses `@wdio/static-server-service` to create a temporary
web server on port 8080.
- `npm run test-mdbook` (runs `wdio.conf-mdbook.ts`): This is for testing
against a live `mdbook serve` instance, which typically runs on port 3000.

It is important to use the standard `npm test` command for most test development
to ensure the tests are self-contained.

### Writing Stable Tests

Tests can be flaky if they don't correctly handle the asynchronous nature of the
web browser and the test environment's state management.

- **State Leakage Between Tests:** Despite what the WebdriverIO documentation
might suggest, `browser.url()` is not always sufficient to guarantee a clean
slate between tests. Lingering state, such as inline CSS styles applied by
JavaScript, can leak from one test into the next, causing unexpected failures.
The most effective solution found for this project is to add
`await browser.refresh();` to the `beforeEach` hook. This forces a full page
reload that properly clears the old state.

- **Race Conditions with Dynamic Elements:** Many elements in this project are
created dynamically by JavaScript after the initial page load. If a test tries
to access an element immediately after navigation, it may fail because the
script hasn't finished running and the element doesn't exist in the DOM yet.
This creates a race condition. To prevent this, always use
`await element.waitForExist()` to ensure the element is present before trying
to interact with it or assert its state (e.g., `toBeDisplayed()`).

### Handling Redirects

`mdbook` uses a redirect map defined in `book.toml` under the
`[output.html.redirect]` section. When writing tests, it is crucial to use the
final, non-redirecting URL for navigation. Navigating to a URL that is a
redirect will cause the browser to follow it, but this process can strip URL
query parameters, leading to test failures for features that depend on them.

### Running and Debugging Tests

To run a single test file, use the `--spec` flag with the a string matching the
file name:

```bash
npm test -- --spec redbox
```

To check for flakiness, you can repeat a test multiple times using the
`--repeat` flag:

```bash
npm test -- --spec redbox --repeat 100
```

Use `--mochaOpts.grep` to run a single test within a file:

```bash
npm test -- --spec redbox --mochaOpts.grep "should be hidden by default"
```
1 change: 1 addition & 0 deletions book.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ urlcolor = "red"
smart-punctuation = true
additional-js = [
"theme/speaker-notes.js",
"theme/redbox.js",
]
additional-css = [
"theme/css/svgbob.css",
Expand Down
6 changes: 6 additions & 0 deletions src/running-the-course.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ better. Your students are also very welcome to [send us feedback][2]!
[1]: https://github.com/google/comprehensive-rust/discussions/86
[2]: https://github.com/google/comprehensive-rust/discussions/100
[3]: https://github.com/google/comprehensive-rust#building
[red-box]: ?show-red-box=true

<details>

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

### Creating a Good Learning Environment

Expand Down
98 changes: 98 additions & 0 deletions tests/src/redbox.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { describe, it } from "mocha";
import { expect, browser } from "@wdio/globals";

describe("Red Box", () => {
const redBox = () => $("#aspect-ratio-helper");
const redBoxButton = () => $("#turn-off-red-box");

beforeEach(async () => {
await browser.url("/hello-world.html");
await browser.execute(() => sessionStorage.clear());
// Clear any lingering state (like inline styles) from previous
// tests. Reading https://webdriver.io/docs/api/browser/url,
// this should not be necessary, but tests fail without it.
await browser.refresh();
});

it("should be hidden by default", async () => {
await expect(redBox()).not.toBeDisplayed();
});

describe("Keyboard Shortcut", () => {
it("should show the red box when toggled on", async () => {
await browser.toggleRedBox();
await expect(redBox()).toBeDisplayed();
await expect(redBoxButton()).toBeDisplayed();
});

it("should hide the red box when toggled off", async () => {
// Toggle on first
await browser.toggleRedBox();
await expect(redBox()).toBeDisplayed();

// Then toggle off
await browser.toggleRedBox();
await expect(redBox()).not.toBeDisplayed();
});
});

describe("URL Parameter", () => {
it("should show red box", async () => {
await browser.url("/hello-world.html?show-red-box=true");
await expect(redBox()).toBeDisplayed();
});

it("should override session storage", async () => {
// Set session storage first to ensure the URL parameter takes precedence.
await browser.execute(() => sessionStorage.setItem("showRedBox", "true"));
await browser.url("/hello-world.html?show-red-box=false");
await expect(redBox()).not.toBeDisplayed();
});
});

describe("Hide Button", () => {
it("should hide the red box when clicked", async () => {
await browser.toggleRedBox();
await expect(redBox()).toBeDisplayed();

await (await redBoxButton()).click();
await expect(redBox()).not.toBeDisplayed();
});
});

describe("Session Storage", () => {
it("should persist being shown after a reload", async () => {
await browser.toggleRedBox();
await expect(redBox()).toBeDisplayed();

await browser.refresh();

await expect(redBox()).toBeDisplayed();
});

it("should persist being hidden after a reload", async () => {
await browser.toggleRedBox(); // turn on
await browser.toggleRedBox(); // turn off
await expect(redBox()).not.toBeDisplayed();

// Explicitly check that storage is cleared before reloading
const storage = await browser.execute(() =>
sessionStorage.getItem("showRedBox"),
);
expect(storage).toBeNull();

await browser.refresh();
await expect(redBox()).not.toBeDisplayed();
});
});

describe("Interactions", () => {
it("should be able to be hidden with the keyboard after being shown with the URL", async () => {
await browser.url("/hello-world.html?show-red-box=true");
await expect(redBox()).toBeDisplayed();

await browser.toggleRedBox();
await expect(redBox()).not.toBeDisplayed();
});
});
});
26 changes: 24 additions & 2 deletions tests/src/speaker-notes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,17 @@ import { $, expect, browser } from "@wdio/globals";

describe("speaker-notes", () => {
beforeEach(async () => {
await browser.url("/");
await browser.url("/welcome-day-1.html");
await browser.refresh();
});

afterEach(async () => {
const handles = await browser.getWindowHandles();
if (handles.length > 1) {
await browser.switchToWindow(handles[1]);
await browser.closeWindow();
await browser.switchToWindow(handles[0]);
}
});

it("contains summary with heading and button", async () => {
Expand All @@ -17,7 +27,7 @@ describe("speaker-notes", () => {
const details$ = await $("details");
const button$ = await $("details summary .pop-out");
await expect(details$).toBeDisplayed();
button$.scrollIntoView();
await button$.scrollIntoView();
await button$.click();
await expect(details$).not.toBeDisplayed();

Expand All @@ -28,4 +38,16 @@ describe("speaker-notes", () => {
expect.stringContaining("#speaker-notes-open"),
);
});

it("should not show the red box in the speaker notes window", async () => {
const button$ = await $("details summary .pop-out");
await button$.scrollIntoView();
await button$.click();

const handles = await browser.getWindowHandles();
await browser.switchToWindow(handles[1]);

const redBox = await $("#aspect-ratio-helper");
await expect(redBox).not.toExist();
});
});
33 changes: 31 additions & 2 deletions tests/wdio.conf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,8 +213,37 @@ export const config: WebdriverIO.Config = {
* @param {Array.<String>} specs List of spec file paths that are to be run
* @param {object} browser instance of created browser/device session
*/
before: function (capabilities, specs) {
browser.setWindowSize(2560, 1440);
before: async function (capabilities, specs) {
await browser.setWindowSize(2560, 1440);

/**
* Adds a custom `browser.toggleRedBox()` command.
*
* This command is necessary to reliably test the red box toggle
* functionality. A direct `browser.keys()` call proved to be
* flaky, causing intermittent test failures. This custom command
* will wait for the UI to reflect the state change, thus
* eliminating race conditions.
*/
browser.addCommand("toggleRedBox", async function () {
const redBox = await $("#aspect-ratio-helper");
const initialVisibility = await redBox.isDisplayed();

// Perform the toggle action.
await browser.keys(["Control", "Alt", "b"]);

// Wait until the visibility state has changed.
await browser.waitUntil(
async function () {
const currentVisibility = await redBox.isDisplayed();
return currentVisibility !== initialVisibility;
},
{
timeout: 5000,
timeoutMsg: `Red box display state did not toggle after 5s. Initial state: ${initialVisibility}`,
},
);
});
},
/**
* Runs before a WebdriverIO command gets executed.
Expand Down
17 changes: 14 additions & 3 deletions theme/css/redbox.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ div#aspect-ratio-helper {
}

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

#instructor-menu-list {
margin-left: 55px;
#turn-off-red-box {
position: absolute;
bottom: 10px;
right: 10px;
z-index: 10000;
padding: 10px;
background-color: #f44336;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
pointer-events: auto;
}
Loading
Loading