mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-22 03:31:46 +00:00
fix(ui): match settings controls to scene styling (#107109)
* fix(ui): match settings controls to scene styling * chore: leave release notes to release automation
This commit is contained in:
committed by
GitHub
parent
80399ed19b
commit
d45dd28df6
142
ui/src/e2e/config-controls-visual.e2e.test.ts
Normal file
142
ui/src/e2e/config-controls-visual.e2e.test.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
// Control UI tests cover shared Settings control styling through the mocked Gateway.
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { chromium, type Browser, type Page } from "playwright";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import {
|
||||
canRunPlaywrightChromium,
|
||||
installMockGateway,
|
||||
resolvePlaywrightChromiumExecutablePath,
|
||||
startControlUiE2eServer,
|
||||
type ControlUiE2eServer,
|
||||
} from "../test-helpers/control-ui-e2e.ts";
|
||||
|
||||
const chromiumExecutablePath = resolvePlaywrightChromiumExecutablePath(chromium.executablePath());
|
||||
const chromiumAvailable = canRunPlaywrightChromium(chromiumExecutablePath);
|
||||
const allowMissingChromium = process.env.OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM === "1";
|
||||
const describeControlUiE2e = chromiumAvailable || !allowMissingChromium ? describe : describe.skip;
|
||||
|
||||
let browser: Browser;
|
||||
let server: ControlUiE2eServer;
|
||||
const captureUiProofEnabled = process.env.OPENCLAW_CAPTURE_UI_PROOF === "1";
|
||||
const uiProofArtifactDir = path.join(
|
||||
process.cwd(),
|
||||
".artifacts",
|
||||
"control-ui-e2e",
|
||||
"settings-controls",
|
||||
);
|
||||
|
||||
async function resolvedBackground(page: Page, value: string): Promise<string> {
|
||||
return page.evaluate((background) => {
|
||||
const probe = document.createElement("div");
|
||||
probe.style.background = background;
|
||||
document.body.append(probe);
|
||||
const resolved = getComputedStyle(probe).backgroundColor;
|
||||
probe.remove();
|
||||
return resolved;
|
||||
}, value);
|
||||
}
|
||||
|
||||
describeControlUiE2e("Control UI Settings controls mocked Gateway E2E", () => {
|
||||
beforeAll(async () => {
|
||||
if (!chromiumAvailable) {
|
||||
throw new Error(
|
||||
`Playwright Chromium is not installed or cannot start at ${chromiumExecutablePath}. Run \`pnpm --dir ui exec playwright install --with-deps chromium\`, or set OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM=1 only when intentionally skipping this lane.`,
|
||||
);
|
||||
}
|
||||
server = await startControlUiE2eServer();
|
||||
browser = await chromium.launch({ executablePath: chromiumExecutablePath });
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await browser?.close();
|
||||
await server?.close();
|
||||
});
|
||||
|
||||
it("keeps the view selector compact and checked switches on the scene accent", async () => {
|
||||
const context = await browser.newContext({
|
||||
colorScheme: "dark",
|
||||
locale: "en-US",
|
||||
serviceWorkers: "block",
|
||||
viewport: { height: 900, width: 1280 },
|
||||
});
|
||||
const page = await context.newPage();
|
||||
const config = { browser: { enabled: true } };
|
||||
await installMockGateway(page, {
|
||||
methodResponses: {
|
||||
"config.get": {
|
||||
config,
|
||||
hash: "hash-1",
|
||||
issues: [],
|
||||
raw: JSON.stringify(config),
|
||||
valid: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await page.goto(`${server.baseUrl}config`);
|
||||
expect(response?.status()).toBe(200);
|
||||
|
||||
const viewSelector = page
|
||||
.locator(".content-header")
|
||||
.getByRole("radiogroup", { name: "Settings view", exact: true });
|
||||
await viewSelector.waitFor();
|
||||
const activeMode = page.locator('.content-header wa-radio[value="quick"]');
|
||||
expect(
|
||||
await activeMode.evaluate((element) => element.getBoundingClientRect().height),
|
||||
).toBeLessThanOrEqual(40);
|
||||
expect(
|
||||
await page.locator('.content-header [slot="label"]').evaluate((element) => {
|
||||
const box = element.getBoundingClientRect();
|
||||
return {
|
||||
height: box.height,
|
||||
overflow: getComputedStyle(element).overflow,
|
||||
width: box.width,
|
||||
};
|
||||
}),
|
||||
).toEqual({ height: 1, overflow: "hidden", width: 1 });
|
||||
expect(
|
||||
await activeMode.evaluate((element) => getComputedStyle(element).backgroundColor),
|
||||
).toBe(await resolvedBackground(page, "var(--bg-elevated)"));
|
||||
|
||||
const browserSwitchRole = page
|
||||
.locator("#settings-general-security")
|
||||
.getByRole("switch", { name: "Browser enabled", exact: true });
|
||||
await browserSwitchRole.waitFor();
|
||||
expect(await browserSwitchRole.getAttribute("aria-checked")).toBe("true");
|
||||
const browserSwitch = page.locator("#settings-general-security wa-switch.settings-toggle");
|
||||
expect(
|
||||
await browserSwitch.evaluate((element) => {
|
||||
const control = element.shadowRoot?.querySelector<HTMLElement>('[part="control"]');
|
||||
return control ? getComputedStyle(control).backgroundColor : null;
|
||||
}),
|
||||
).toBe(await resolvedBackground(page, "var(--accent)"));
|
||||
|
||||
if (captureUiProofEnabled) {
|
||||
await mkdir(uiProofArtifactDir, { recursive: true });
|
||||
await page.locator(".content-header").screenshot({
|
||||
animations: "disabled",
|
||||
path: path.join(uiProofArtifactDir, "01-settings-view.png"),
|
||||
});
|
||||
await page.locator("#settings-general-security").screenshot({
|
||||
animations: "disabled",
|
||||
path: path.join(uiProofArtifactDir, "02-security-controls.png"),
|
||||
});
|
||||
}
|
||||
|
||||
await browserSwitch.click();
|
||||
await expect.poll(() => browserSwitchRole.getAttribute("aria-checked")).toBe("false");
|
||||
await page.locator('.content-header wa-radio[value="advanced"]').click();
|
||||
await expect
|
||||
.poll(() =>
|
||||
page
|
||||
.locator('.content-header wa-radio[value="advanced"]')
|
||||
.evaluate((element) => (element as HTMLElement & { checked: boolean }).checked),
|
||||
)
|
||||
.toBe(true);
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -57,7 +57,7 @@ describe("supportsSystemInfo", () => {
|
||||
});
|
||||
|
||||
describe("ConfigPage settings mode control", () => {
|
||||
it("uses a Web Awesome radio group to switch modes", () => {
|
||||
it("uses the shared settings segmented control to switch modes", () => {
|
||||
const page = new ConfigPage();
|
||||
const state = page as unknown as {
|
||||
pageId: string;
|
||||
@@ -74,7 +74,9 @@ describe("ConfigPage settings mode control", () => {
|
||||
container.querySelectorAll<HTMLElement & { checked: boolean }>("wa-radio"),
|
||||
);
|
||||
|
||||
expect(group?.getAttribute("label")).toBe("Settings view");
|
||||
expect(group?.classList.contains("settings-segmented")).toBe(true);
|
||||
expect(group?.querySelector('[slot="label"]')?.textContent).toBe("Settings view");
|
||||
expect(quick?.classList.contains("settings-segmented__btn--active")).toBe(true);
|
||||
expect(quick?.checked).toBe(true);
|
||||
expect(advanced?.checked).toBe(false);
|
||||
if (group) {
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
} from "../../app/settings.ts";
|
||||
import { startThemeTransition } from "../../app/theme-transition.ts";
|
||||
import { resolveTheme, type ThemeMode, type ThemeName } from "../../app/theme.ts";
|
||||
import { renderSettingsSegmented } from "../../components/settings-ui.ts";
|
||||
import { renderSettingsWorkspace } from "../../components/settings-workspace.ts";
|
||||
import { i18n, isSupportedLocale, t, type Locale } from "../../i18n/index.ts";
|
||||
import { isMissingOperatorReadScopeError } from "../../lib/gateway-errors.ts";
|
||||
@@ -949,36 +950,20 @@ export class ConfigPage extends OpenClawLightDomElement {
|
||||
if (this.pageId !== "config") {
|
||||
return nothing;
|
||||
}
|
||||
const modes = [
|
||||
["quick", t("configPage.simple")],
|
||||
["advanced", t("configPage.advanced")],
|
||||
] as const;
|
||||
return html`
|
||||
<wa-radio-group
|
||||
class="config-view-toggle qs-segmented"
|
||||
label=${t("configPage.settingsView")}
|
||||
.value=${this.settingsMode}
|
||||
orientation="horizontal"
|
||||
@change=${(event: Event) => {
|
||||
const value = (event.currentTarget as HTMLElement & { value?: string }).value;
|
||||
if (value === "quick" || value === "advanced") {
|
||||
this.settingsMode = value;
|
||||
}
|
||||
}}
|
||||
>
|
||||
${modes.map(
|
||||
([mode, label]) => html`
|
||||
<wa-radio
|
||||
class="qs-segmented__btn"
|
||||
appearance="button"
|
||||
value=${mode}
|
||||
.checked=${this.settingsMode === mode}
|
||||
>
|
||||
${label}
|
||||
</wa-radio>
|
||||
`,
|
||||
)}
|
||||
</wa-radio-group>
|
||||
<div class="config-view-toggle">
|
||||
${renderSettingsSegmented({
|
||||
value: this.settingsMode,
|
||||
options: [
|
||||
{ value: "quick", label: t("configPage.simple") },
|
||||
{ value: "advanced", label: t("configPage.advanced") },
|
||||
],
|
||||
ariaLabel: t("configPage.settingsView"),
|
||||
onChange: (mode) => {
|
||||
this.settingsMode = mode;
|
||||
},
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -335,6 +335,9 @@ select.settings-select:not([multiple]) {
|
||||
}
|
||||
|
||||
wa-switch.settings-toggle {
|
||||
/* Web Awesome defaults to brand blue; settings controls follow the active scene. */
|
||||
--wa-form-control-activated-color: var(--accent);
|
||||
--wa-color-focus: var(--ring);
|
||||
flex: none;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user