learning-website-nextjs1-6 Exercise 3: Click Through It in a Real Browser ============================================================================== Reading the HTML shows that the links are there. It does not show what happens when they are clicked. A real headless Chrome, driven through the DevTools protocol, opened a lesson, set a marker on the page's window object, and clicked through the navigation. If the marker survives a click, the page was NOT reloaded: only what was new was loaded. Save as nav-test.mjs: // Click through the navigation in a real headless Chrome. Is each click a client-side navigation (the page is not // reloaded), and does the menu follow? // node nav-test.mjs import { spawn } from "node:child_process"; import { mkdtempSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; const base = process.argv[2] ?? "http://localhost:3001"; const chrome = spawn("C:/Program Files/Google/Chrome/Application/chrome.exe", [ "--headless=new", "--disable-gpu", "--remote-debugging-port=9335", `--user-data-dir=${mkdtempSync(join(tmpdir(), "lw-chrome-"))}`, "about:blank", ], { stdio: "ignore" }); const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); let targets; for (let i = 0; i < 40; i++) { try { targets = await (await fetch("http://127.0.0.1:9335/json")).json(); if (targets.length) break; } catch { /* not up yet */ } await sleep(250); } const ws = new WebSocket(targets.find((t) => t.type === "page").webSocketDebuggerUrl); await new Promise((r) => ws.addEventListener("open", r)); let id = 0; const pending = new Map(); ws.addEventListener("message", (e) => { const m = JSON.parse(e.data); if (m.id && pending.has(m.id)) { pending.get(m.id)(m); pending.delete(m.id); } }); const send = (method, params = {}) => new Promise((r) => { const n = ++id; pending.set(n, r); ws.send(JSON.stringify({ id: n, method, params })); }); const evaluate = async (expression) => (await send("Runtime.evaluate", { expression, returnByValue: true })).result?.result?.value; const state = () => evaluate(`JSON.stringify({ path: location.pathname, sameDocument: window.__marker === 1, current: [...document.querySelectorAll('nav[aria-label$="subjects"] a[aria-current="true"]')].map(a => a.textContent), h1: (document.querySelector("h1") || {}).textContent })`).then(JSON.parse); const click = (selector) => evaluate(`(() => { const e = document.querySelector(${JSON.stringify(selector)}); if (!e) return false; e.click(); return true; })()`); await send("Page.enable"); await send("Page.navigate", { url: base + "/hungary/hungarian-basic-3/hungarian_basic_conversation_3_5" }); await sleep(2500); await evaluate("window.__marker = 1"); const rows = []; const step = async (label, action) => { const ok = await action(); await sleep(1500); rows.push([label, ok, await state()]); }; rows.push(["start: opened directly", true, await state()]); await step("click Next chapter", () => click('a[rel="next"]')); await step("click Previous chapter", () => click('a[rel="prev"]')); await step("click the course in the breadcrumb", () => click('nav[aria-label="Breadcrumb"] li:nth-child(3) a')); await step("click Japan in the site menu", () => click('nav[aria-label$="subjects"] a[href="/japan"]')); await step("browser Back", () => evaluate("history.back(), true")); for (const [label, ok, s] of rows) { console.log(label.padEnd(38), ok ? "" : "(link not found) ", `${s.path}`.padEnd(66), `no reload: ${s.sameDocument}`.padEnd(16), `menu: ${s.current.join(",") || "-"}`.padEnd(14), `h1: ${String(s.h1).slice(0, 38)}`); } ws.close(); chrome.kill(); process.exit(0); FIRST RUN (this is what I found): start: opened directly /hungary/hungarian-basic-3/hungarian_basic_conversation_3_5 no reload: true menu: Hungary click Next chapter .../hungarian_basic_conversation_3_6 no reload: true menu: Hungary click Previous chapter .../hungarian_basic_conversation_3_5 no reload: true menu: Hungary click the course in the breadcrumb /hungary/hungarian-basic-3 no reload: true menu: Hungary click Japan in the site menu /japan no reload: true menu: - <-- bug browser Back /hungary/hungarian-basic-3 no reload: true menu: Hungary Every click was a client-side navigation (the marker survived), and Back worked. But after clicking Japan, NO menu item was marked current. What went wrong: the menu marks an item current when the path starts with the item's address. The items' addresses end in a slash ("/japan/") but Next serves "/japan", and "/japan" does not start with "/japan/". Pages INSIDE a subject worked ("/hungary/..." starts with "/hungary/"), which is why only a click on a subject's own page showed it, and why looking at lesson pages never would. How to avoid it: test the case at the edge of a rule (the subject's own page, not only pages below it), and compare addresses without depending on a trailing slash. THE FIX (in SiteMenu.tsx, shown in Exercise 2): compare with the trailing slash removed, and require a slash after it: const base = item.href.replace(/\/$/, ""); const current = pathname === base || pathname.startsWith(base + "/"); (The "/" after base also stops "/ai" from matching "/ai-tools".) SECOND RUN, after the fix: click Japan in the site menu /japan no reload: true menu: Japan (every other row unchanged) Links between sites come from one config. The bar of sites is built from the site map, so adding a site changes it everywhere; resolveLink (Exercise 1) turns a root-relative link into the right link from a given site (tested: a link into another site becomes http://systems.localhost:3004/linux/x/ in development and https://systems.osztromok.com/linux/x/ in production, and a path that no site owns throws an error). What was NOT done: resolveLink is written and tested but not yet applied to the links INSIDE the lesson pages (Chapter 11 covers rewriting links when migrating); the web development app has no content routes; keyboard navigation and a screen reader were not tried; and only Chrome was used. WHY THIS WORKS AS AN ANSWER --------------------------- It records the failure as well as the fix: a bug that only appears on one kind of page, found by clicking, in a place the unit tests and the HTML could not show.