You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: exercises/04.debugging/03.solution.breakpoints/README.mdx
+107-102
Original file line number
Diff line number
Diff line change
@@ -34,15 +34,15 @@ Because I still want to be able to debug those steps, I will add _breakpoints_ t
34
34
35
35
## Conditional breakpoints
36
36
37
-
Speaking of more powers, it's time to put them to the test! But not before I finish the new test case for the `<MainMenu />` component.
37
+
Speaking of more powers, it's time to put them to good use! But not before I finish the new test case for the `<MainMenu />` component.
38
38
39
-
As the name suggests, the menu component renders the main navigation menu for our application. To do that, it recursively iterates over the `menuItems` array to render those items and their possible children. This is, effectively, a loop with a base case and a recursive case:
39
+
As the name suggests, the menu component renders the main navigation for our application. To do that, it recursively iterates over the `menuItems` array to render those items and their possible children. This is, effectively, a loop with a base case and a recursive case:
Since this test checks the currently active page, I need to somehow simulate the route that we are currently on in the application. I can do that by providing the path I want to the `initialEntries` prop of `<MemoryRouter />`:
Now, I need to check that the corresponding menu item is actually marked as the current page. Because the current page is indicated using the `aria-current` attribute set to `'page'`, I cannot directly locate that element. Instead, I will get all the rendered links and then find the one with that attribute set to the right value:
> 🦉 `page.getByRole('link')` returns a locator you can resolve in different ways. By default, it points to a single element matching the locator, but you can call its `.elements()` method to resolve it to a _list_ of matching HTML elements. This roughly translates to `.findAllByRole('link')` from RTL.
102
-
103
-
And I'm going to conclude this test by adding the following assertion:
This single assertion allows me to check multiple things at once:
110
-
111
-
- The `currentPageLink` element could be located (i.e. is rendered by the menu);
112
-
- The `currentPageLink` element has accessible name of `'Analytics'`, which in case of `role="link"` elements is derived from their text content. This way, I'm checking that the link is both visible to the screen-readers and has the correct text content.
113
-
114
-
But running this test presents me with an error:
115
-
116
-
```txt highlight=12-15 nonumber
56
+
But for some unknown reason, when I run the automated test, the link I expect to be active isn't the only one active!
117
57
58
+
```shell nonumber highlight=11-14
118
59
❯ src/main-menu.browser.test.tsx:22:39
119
60
20| )!
120
61
21|
@@ -131,66 +72,130 @@ Received:
131
72
Dashboard
132
73
```
133
74
134
-
Looks like the current page link that gets rendered is the "Dashboard" link and not "Analytics". This demands some debugging.
75
+
Something is definitely off here, and I can use breakpoints to help me find the root cause.
76
+
77
+
There's just a slight problem. Since the `<MainMenu />` component is recursive, if I put a breakpoint where I think is a good place to start debugging, it will trigger _for all rendered menu items_. That's not nice because I will be spending my time jumping between breakpoints instead of actually solving the issue.
78
+
79
+
This is where _conditional breakpoints_ can be a huge time-savior. They act just like the regulat breakpoints but they only trigger when their condition resolves to true. So I can say if `item.title === 'Dashboard'`, stop the execution. That's neat!
80
+
81
+
But not so fast. Conditional breakpoints can only access variables present in the _current scope_ (i.e. the scope where you are placing a breakpoint), and the `item` variable comes from the parental scope, which will make it impossible to use in the condition.
Now, there are many ways you can approach debugging this. You can go through the source code or maybe inspect the DOM via the `debug()` method from `render()` or print all the active link elements. Any technique is good if it helps you get to the bottom of the issue faster.
129
+
> This change won't affect the class name condition but instead will expose the `item` variable to this closure.
137
130
138
-
To teach you more tools for debugging, I will approach this issue differently. If I take a look above the printed error in the terminal, I see this line:
131
+
Now, I can add a conditional breakpoint in Visual Studio Code by right-clicking on the gutter next to the line I need and choosing the "Add Conditional Breakpoint..." option from the context menu:

144
134
145
-
Vitest takes screenshots of the page for every failed test case to give you a visual clue about what is going on. Let's take a look at that screenshot:
135
+

146
136
147
-

137
+
Let's run the `main-menu.browser.test.tsx` test suite with the debugger attached and arrive at this breakpoint just when I mean to:
148
138
149
-
Aha! Both "Dashboard" and "Analytics" are treated as currently active navigation links.
139
+

150
140
151
-
> 🦉 While it's not forbidden to have multiple `aria-current="page"` elements at the same time, it is generally advised by the [WAI-ARIA specification](https://w3c.github.io/aria/#aria-current)to "only mark **one** element as current".
141
+
As usual, I can look around to see the `item` and `isActive` values in this scope. Just like I suspected, the `isActive` prop is set to `true` for the Dashboard menu item. But why?
152
142
153
-
The fact that the "Analytics" menu item is marked as current is correct, that's what I expect. It's the "Dashboard" that's likely wrong here. It would be great to stop the time and look around when that menu item is getting rendered.
143
+
The answer to that lies deeper down the stack trace. If I click on the `NavLinkWithRef` frame in the stack trace, the debugger will bring me to the rendering part of `NavLink` from my menu. Here, I can inspect all the props that the `NavLink` gets but also _gain access to everything in its scope_.
154
144
155
-
That's a great use case for a breakpoint!
145
+
I am particularly interested in this line, where the `isActive` variable gets assigned:
156
146
157
-
But if I add a breakpoint in `<MenuItemsList />`, it will trigger on _every menu item_ that gets rendered. This will force me to step through irrelevant items before I get to the one I want, wasting my time.
I can still see that it's `false`, but having access to all the data React Router uses to compute this value, I can play around with it and, hopefully, find the root cause.
158
156
159
-
Instead, I will use a _conditional breakpoint_. It's like a regular breakpoint but it will only "activate" when a certain condition is met. What's great about these kind of breakpoints is that I can use expressions, accessing anything within the scope to write my condition.
157
+
I will start from inspecting the `locationPathname` and `toPathname` values in the Debug console:
160
158
161
-

159
+

162
160
163
-
> I am putting the breakpoint on the line with `render` to trigger before the problematic item is rendered.
161
+
<callout-success>Debug console is your go-to choice for evaluating expressions inside the breakpoints. You can reference everything available in the current scope, call functions, declare new variables, etc. This is your _debugging sandbox_!</callout-success>
164
162
165
-
As the condition, I will tell this breakpoint to become active only when the rendered item's `title` equals to `'Dashboard'`:
163
+
Since `locationPathname` (document location) is `/dashboard/analytics` just like I set it in tests, it certainly doesn't equal the link's `to` path, which is `/dashboard`. The condition follows on, and now it branches based on the `end` prop. Once I take a peek at its value...
166
164
167
-

165
+

168
166
169
-
If I run this test suite using the "Debug Vitest Browser" task we've prepared earlier, I will see the code execution stop right before the Dashboard menu item is about to get rendered:
167
+
It's `false`! This means that the first logical branch will resolve (the one with `!end`), and return `true` as the `isActive` value because _current location starts from the link's path_:
170
168
171
-

169
+
```ts nonumber
170
+
'/dashboard/analytics'.startsWith('/dashboard')
171
+
// true
172
+
```
172
173
173
-
There are three key figures to observe in the "Variables" panel:
174
+
🤦 I forgot to pass `end={true}` to the `NavLink` in my menu. Luckily, that's an easy fix:
0 commit comments