Skip to content
Merged
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
53 changes: 47 additions & 6 deletions tests/browser/console.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,39 @@ test("a collection body disconnected after headers shows interruption and Retry
);
});

// Waits up to settleMs for the locator's element to hold focus. Returns what held focus at the
// first and the last check (role, accessible text, class, connected), so a failed check names
// the element that took focus instead of saying `false !== true` (finding 2016).
async function focusSettles(locator, settleMs = 1000) {
const deadline = Date.now() + settleMs;
let first;
for (;;) {
const sample = await locator.evaluate((node) => {
const active = globalThis.document.activeElement;
const describe = (element) => {
if (!element || element === globalThis.document.body) {
return element ? "<body>" : "nothing";
}
const role = element.getAttribute("role");
// getAttribute, not className: an SVG element's className is an object.
const className = element.getAttribute("class");
const name = (element.getAttribute("aria-label") ?? element.textContent ?? "").trim();
return (
`<${element.localName}${role ? ` role=${role}` : ""}` +
`${className ? ` class="${className}"` : ""}> "${name.slice(0, 60)}"` +
`${element.isConnected ? "" : " (detached)"}`
);
};
return { held: node === active, active: describe(active), target: describe(node) };
});
first ??= sample;
if (sample.held || Date.now() >= deadline) {
return { ...sample, first: first.active };
}
await new Promise((resolve) => setTimeout(resolve, 50));
}
}

async function openShellMenu(page) {
await page.getByRole("button", { name: /OpenClaw Enterprise/ }).click();
}
Expand Down Expand Up @@ -843,20 +876,28 @@ for (const focus of ["navigation link", "account menu"]) {
focused = page.getByRole("menuitem", { name: "Settings", exact: true });
}
await focused.waitFor();
assert.equal(
await focused.evaluate((node) => node === globalThis.document.activeElement),
true,
);
const opened = await focusSettles(focused);
if (opened.held && opened.first !== opened.active) {
t.diagnostic(`focus settled on the ${focus} after ${opened.first}`);
}
if (!opened.held) {
assert.fail(
`The opened drawer focuses its ${focus} ${opened.target}; focus is on ${opened.active}` +
` (first check: ${opened.first}; drawer ${await toggle.getAttribute("aria-expanded")})`,
);
}
await releaseHeldRoute(page, namespacesPattern, namespacesHold);
await page.getByRole("list", { name: "Namespaces", exact: true }).waitFor();
await page.locator('.content [aria-live="polite"][aria-busy="false"]').waitFor();

assert.equal(await toggle.getAttribute("aria-expanded"), "true", "The drawer stays open");
assert.equal(await page.locator("main").evaluate((node) => node.inert), true);
// No settling here: the re-render must not move focus at all.
const kept = await focusSettles(focused, 0);
assert.equal(
await focused.evaluate((node) => node === globalThis.document.activeElement),
kept.held,
true,
`The re-rendered drawer keeps focus on its ${focus}`,
`The re-rendered drawer keeps focus on its ${focus} ${kept.target}; focus is on ${kept.active}`,
);
await focused.click();
const destination = focus === "account menu" ? "Settings" : "Agents";
Expand Down
Loading