mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-23 10:55:31 -06:00
90526f3a9c
* fix(ui): stop composer mic hover shift and stray red hover The device-picker chevron collapsed to width 0 while idle and grew to 20px on hover, shoving the right-aligned mic button 20px left under the cursor on every mouse-over. Detach the picker into its own fixed-width ghost button beside the mic instead of morphing the mic's shape into a split pill; hover only fades the picker's opacity, so the reserved layout never shifts. Also fixes the mic showing the send button's loud accent-red hover in dark/openknot themes: those themes overrode .chat-send-btn:hover directly, which outranked every variant's own hover rule (including the intentional neutral mic hover and the stop button's danger hover). Theme overrides now set a --chat-send-hover-background variable that each variant's hover rule can still take precedence over. * chore(scripts): register composer mic hover proof script Missing package.json entry made the deadcode full-tree scan flag the new capture script as an unused file, same as the existing ui:proof:workboard sibling.
106 lines
3.5 KiB
TypeScript
106 lines
3.5 KiB
TypeScript
#!/usr/bin/env node
|
|
// Captures composer mic-hover proof shots: idle vs hovered talk control, plus
|
|
// the mic button's x position in both states (the hover-shift regression).
|
|
import { mkdir } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { chromium } from "playwright";
|
|
import {
|
|
canRunPlaywrightChromium,
|
|
installMockGateway,
|
|
resolvePlaywrightChromiumExecutablePath,
|
|
startControlUiE2eServer,
|
|
} from "../ui/src/test-helpers/control-ui-e2e.ts";
|
|
|
|
function readOption(name: string): string | undefined {
|
|
const prefix = `--${name}=`;
|
|
const inline = process.argv.slice(2).find((arg) => arg.startsWith(prefix));
|
|
if (inline) {
|
|
return inline.slice(prefix.length);
|
|
}
|
|
const index = process.argv.indexOf(`--${name}`);
|
|
return index >= 0 ? process.argv[index + 1] : undefined;
|
|
}
|
|
|
|
const outputDir = path.resolve(
|
|
readOption("output-dir") ?? ".artifacts/control-ui-e2e/composer-mic-hover-proof",
|
|
);
|
|
const label = readOption("label") ?? "after";
|
|
const executablePath = resolvePlaywrightChromiumExecutablePath(chromium.executablePath());
|
|
if (!canRunPlaywrightChromium(executablePath)) {
|
|
throw new Error(`Playwright Chromium is unavailable at ${executablePath}`);
|
|
}
|
|
|
|
await mkdir(outputDir, { recursive: true });
|
|
const server = await startControlUiE2eServer(undefined, { source: true });
|
|
const browser = await chromium.launch({ executablePath });
|
|
const context = await browser.newContext({
|
|
colorScheme: "dark",
|
|
viewport: { width: 1280, height: 800 },
|
|
});
|
|
const page = await context.newPage();
|
|
page.setDefaultTimeout(30_000);
|
|
|
|
try {
|
|
await installMockGateway(page);
|
|
await page.goto(`${server.baseUrl}chat`);
|
|
const composer = page.locator(".agent-chat__input");
|
|
await composer.waitFor({ state: "visible" });
|
|
const voice = page.getByRole("button", { name: "Start voice input" });
|
|
await voice.waitFor({ state: "visible" });
|
|
|
|
await page.mouse.move(10, 10);
|
|
await page.waitForTimeout(400);
|
|
const composerBox = await composer.boundingBox();
|
|
if (!composerBox) {
|
|
throw new Error("composer has no layout box");
|
|
}
|
|
const clip = {
|
|
x: Math.max(0, composerBox.x - 12),
|
|
y: Math.max(0, composerBox.y - 12),
|
|
width: Math.min(1280, composerBox.width + 24),
|
|
height: composerBox.height + 24,
|
|
};
|
|
const idleVoiceBox = await voice.boundingBox();
|
|
await page.screenshot({ clip, path: path.join(outputDir, `${label}-idle.png`) });
|
|
|
|
await voice.hover();
|
|
await page.waitForTimeout(400);
|
|
const hoverVoiceBox = await voice.boundingBox();
|
|
await page.screenshot({ clip, path: path.join(outputDir, `${label}-hover.png`) });
|
|
|
|
const picker = page.getByRole("button", { name: "Microphone input" });
|
|
if (await picker.count()) {
|
|
await picker.hover();
|
|
await page.waitForTimeout(400);
|
|
await page.screenshot({ clip, path: path.join(outputDir, `${label}-hover-chevron.png`) });
|
|
await picker.click();
|
|
await page.waitForTimeout(500);
|
|
await page.screenshot({
|
|
path: path.join(outputDir, `${label}-picker-open.png`),
|
|
clip: { ...clip, y: Math.max(0, clip.y - 260), height: clip.height + 260 },
|
|
});
|
|
await page.keyboard.press("Escape");
|
|
}
|
|
|
|
if (!idleVoiceBox || !hoverVoiceBox) {
|
|
throw new Error("voice button has no layout box");
|
|
}
|
|
console.log(
|
|
JSON.stringify(
|
|
{
|
|
label,
|
|
idleVoiceX: idleVoiceBox.x,
|
|
hoverVoiceX: hoverVoiceBox.x,
|
|
hoverShiftPx: hoverVoiceBox.x - idleVoiceBox.x,
|
|
outputDir,
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
} finally {
|
|
await context.close();
|
|
await browser.close();
|
|
await server.close();
|
|
}
|