Files
openclaw/ui/src/styles/cursor-policy.node.test.ts
Peter Steinberger 41ed3c3460 fix(ui): buttons use the default cursor instead of the link hand (#126153)
* fix(ui): reserve pointer cursors for links

* test: align cursor and docs contracts
2026-08-18 21:40:52 -07:00

86 lines
3.0 KiB
TypeScript

// Control UI tests cover the cursor policy's source-level shape.
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
const stylesDir = path.dirname(fileURLToPath(import.meta.url));
function collectStyleSources(directory: string): string[] {
return fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
const entryPath = path.join(directory, entry.name);
if (entry.isDirectory()) {
return entry.name === "node_modules" ? [] : collectStyleSources(entryPath);
}
if (!entry.isFile() || entry.name.includes(".test.")) {
return [];
}
return entry.name.endsWith(".css") || entry.name.endsWith(".ts") ? [entryPath] : [];
});
}
/** Walks back from a declaration line to the selector list that owns it. */
function selectorFor(lines: readonly string[], declarationIndex: number): string {
const parts: string[] = [];
for (let index = declarationIndex - 1; index >= 0; index -= 1) {
const line = lines[index]?.trim() ?? "";
if (line.endsWith("{")) {
parts.unshift(line.slice(0, -1).trim());
for (let above = index - 1; above >= 0 && lines[above]?.trim().endsWith(","); above -= 1) {
parts.unshift(lines[above]!.trim());
}
break;
}
if (line === "" || line.endsWith("}")) {
break;
}
}
return parts.join(" ");
}
function selectorBranches(selector: string): string[] {
const branches: string[] = [];
let depth = 0;
let start = 0;
for (let index = 0; index < selector.length; index += 1) {
const character = selector[index];
if (character === "(" || character === "[") {
depth += 1;
} else if (character === ")" || character === "]") {
depth -= 1;
} else if (character === "," && depth === 0) {
branches.push(selector.slice(start, index).trim());
start = index + 1;
}
}
branches.push(selector.slice(start).trim());
return branches;
}
const ANCHOR_SELECTOR = /(^|[\s,(>~+])a([.#:[]|\b)/u;
const NEW_TAB_ACTION_SELECTOR = /\[data-new-tab-action\]/u;
describe("Control UI cursor policy", () => {
it("keeps the unconditional pointer on links and explicit new-tab controls only", () => {
const offenders = collectStyleSources(path.join(stylesDir, ".."))
.flatMap((filePath) => {
const lines = fs.readFileSync(filePath, "utf8").split("\n");
return lines.flatMap((line, index) =>
/^\s*cursor:\s*pointer;\s*$/u.test(line)
? selectorBranches(selectorFor(lines, index)).map((selector) => ({
file: path.relative(stylesDir, filePath),
selector,
}))
: [],
);
})
.filter(
(hit) => !ANCHOR_SELECTOR.test(hit.selector) && !NEW_TAB_ACTION_SELECTOR.test(hit.selector),
);
// State controls consume var(--cursor-action); only links and the explicit
// browser-tab/window hook may own an unconditional hand.
expect(offenders).toEqual([]);
});
});