From 229ce32e3272e92b08cfe7ed73b62ab68ecdbd6c Mon Sep 17 00:00:00 2001 From: ClawSweeper Date: Fri, 21 Aug 2026 13:12:10 -0700 Subject: [PATCH] fix(ui): align run history dropdown text (#127443) Co-authored-by: RoboClaw <309084314+roboclaw-bot@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> --- config/assertion-safety-baseline.txt | 2 +- .../cron-run-filter-typography.e2e.test.ts | 77 +++++++++++++++++++ ui/src/e2e/cron-select-values.e2e.test.ts | 15 ++-- ui/src/pages/cron/view-runs.ts | 55 ++++++++----- ui/src/pages/cron/view.test.ts | 8 +- ui/src/styles/cron.css | 19 ----- 6 files changed, 129 insertions(+), 47 deletions(-) create mode 100644 ui/src/e2e/cron-run-filter-typography.e2e.test.ts diff --git a/config/assertion-safety-baseline.txt b/config/assertion-safety-baseline.txt index be6875c89154..c52b4bc7dfc2 100644 --- a/config/assertion-safety-baseline.txt +++ b/config/assertion-safety-baseline.txt @@ -4233,7 +4233,7 @@ ui/src/pages/config/view-schema.ts 2 ui/src/pages/config/view.ts 5 ui/src/pages/connection/connection-page.ts 1 ui/src/pages/connection/view.ts 3 -ui/src/pages/cron/view-runs.ts 5 +ui/src/pages/cron/view-runs.ts 3 ui/src/pages/cron/view.ts 9 ui/src/pages/custodian/custodian-session-store.ts 1 ui/src/pages/custodian/custodian-surface.ts 2 diff --git a/ui/src/e2e/cron-run-filter-typography.e2e.test.ts b/ui/src/e2e/cron-run-filter-typography.e2e.test.ts new file mode 100644 index 000000000000..a92eb1a039a7 --- /dev/null +++ b/ui/src/e2e/cron-run-filter-typography.e2e.test.ts @@ -0,0 +1,77 @@ +import { expect, it } from "vitest"; +import { installMockGateway } from "../test-helpers/control-ui-e2e.ts"; +import { createControlUiE2eSuite } from "./control-ui-e2e-suite.test-support.ts"; + +const suite = createControlUiE2eSuite({ + name: "Control UI cron run filter typography E2E", + startServerBeforeBrowser: true, + unavailableMessage: (executablePath) => + `Playwright Chromium is not installed or cannot start at ${executablePath}. 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.`, +}); + +suite.define(() => { + it("keeps dropdown text aligned at mobile and desktop widths", async () => { + for (const viewport of [ + { height: 844, width: 390 }, + { height: 900, width: 1_280 }, + ]) { + await suite.withPage( + { + locale: "en-US", + serviceWorkers: "block", + viewport, + }, + async ({ page }) => { + const gateway = await installMockGateway(page, { + methodResponses: { + "cron.list": { + jobs: [], + snapshotRevision: "cron-run-filter-typography", + total: 0, + offset: 0, + limit: 50, + hasMore: false, + nextOffset: null, + }, + "cron.runs": { + entries: [], + total: 0, + offset: 0, + limit: 50, + hasMore: false, + nextOffset: null, + }, + "cron.status": { enabled: true, jobs: 0, nextWakeAtMs: null }, + }, + }); + + await page.goto(`${suite.server.baseUrl}cron`); + await page.getByRole("tab", { name: "Run history", exact: true }).click(); + const labels = page.locator(".cron-run-filters .cron-filter-dropdown__trigger span"); + await expect.poll(() => labels.count()).toBe(3); + const fontSizes = await labels.evaluateAll((elements) => + elements.map((element) => getComputedStyle(element).fontSize), + ); + expect(new Set(fontSizes).size).toBe(1); + + await page.getByRole("button", { name: "Sort Newest first", exact: true }).click(); + await page.locator('wa-dropdown-item[value="asc"]').click(); + await page.getByRole("button", { name: "Sort Oldest first", exact: true }).waitFor(); + await expect + .poll(async () => + (await gateway.getRequests("cron.runs")).some((request) => { + const params = request.params; + return ( + typeof params === "object" && + params !== null && + "sortDir" in params && + params.sortDir === "asc" + ); + }), + ) + .toBe(true); + }, + ); + } + }); +}); diff --git a/ui/src/e2e/cron-select-values.e2e.test.ts b/ui/src/e2e/cron-select-values.e2e.test.ts index 1374d1f4c674..65cd178078af 100644 --- a/ui/src/e2e/cron-select-values.e2e.test.ts +++ b/ui/src/e2e/cron-select-values.e2e.test.ts @@ -45,14 +45,19 @@ suite.define(() => { const response = await page.goto(`${suite.server.baseUrl}cron`); expect(response?.status()).toBe(200); await page.locator('[data-test-id="cron-list-tab-activity"]').click(); - const sort = page.locator("select.cron-run-sort"); + const sortMenu = page.locator("wa-dropdown", { has: page.locator(".cron-run-sort") }); + const sort = page.getByRole("button", { name: "Sort Newest first", exact: true }); await sort.waitFor({ state: "visible" }); - await sort.selectOption("asc"); - expect(await sort.inputValue()).toBe("asc"); - // Switching tabs recreates the select with the persisted non-first value. + await sort.click(); + await sortMenu.locator('wa-dropdown-item[value="asc"]').click(); + await page.getByRole("button", { name: "Sort Oldest first", exact: true }).waitFor(); + // Switching tabs recreates the dropdown with the persisted non-first value. await page.locator('[data-test-id="cron-list-tab-tasks"]').click(); await page.locator('[data-test-id="cron-list-tab-activity"]').click(); - expect(await page.locator("select.cron-run-sort").inputValue()).toBe("asc"); + await page.getByRole("button", { name: "Sort Oldest first", exact: true }).waitFor(); + expect( + await sortMenu.locator('wa-dropdown-item[value="asc"]').getAttribute("aria-current"), + ).toBe("true"); await page.locator('[data-test-id="cron-new-task"]').click(); const pickerValue = (selector: string) => diff --git a/ui/src/pages/cron/view-runs.ts b/ui/src/pages/cron/view-runs.ts index ed8c46d073ae..814d9f002254 100644 --- a/ui/src/pages/cron/view-runs.ts +++ b/ui/src/pages/cron/view-runs.ts @@ -223,8 +223,8 @@ export function renderRunsSection(props: CronRunsSectionProps) { .map((option) => option.label); const statusSummary = summarizeSelection(selectedStatusLabels, t("cron.runs.allStatuses")); const deliverySummary = summarizeSelection(selectedDeliveryLabels, t("cron.runs.allDelivery")); - // The sort select's .value binding commits before its options exist; - // selected attributes preserve non-first values. + const sortLabel = + props.runsSortDir === "asc" ? t("cron.runs.oldestFirst") : t("cron.runs.newestFirst"); return html`
${props.conditionActivity ? renderConditionActivity(props.conditionActivity) : nothing} @@ -273,23 +273,40 @@ export function renderRunsSection(props: CronRunsSectionProps) { void props.onRunsFiltersChange({ cronRunsDeliveryStatuses: [] }); }, })} - +
+ ) => { + const value = event.detail.item.value; + if (value === "asc" || value === "desc") { + void props.onRunsFiltersChange({ cronRunsSortDir: value }); + } + }} + > + + + ${t("cron.runs.newestFirst")} + + + + ${t("cron.runs.oldestFirst")} + + + +
${runs.length === 0 ? hasRunFilters diff --git a/ui/src/pages/cron/view.test.ts b/ui/src/pages/cron/view.test.ts index 2323d70f4e04..00cdb56ad1ab 100644 --- a/ui/src/pages/cron/view.test.ts +++ b/ui/src/pages/cron/view.test.ts @@ -355,9 +355,11 @@ describe("cron view selects", () => { it("shows persisted non-first values in jobs filters and runs sort", () => { const activity = renderView({ listTab: "activity", runsSortDir: "asc" }); - const sort = getElement(activity, "select.cron-run-sort", HTMLSelectElement); - expect(sort.value).toBe("asc"); - expect(sort.querySelector('option[value="asc"]')?.hasAttribute("selected")).toBe(true); + const sort = getElement(activity, ".cron-run-sort", HTMLButtonElement); + expect(sort.textContent).toContain("Oldest first"); + expect( + activity.querySelector('wa-dropdown-item[value="asc"]')?.getAttribute("aria-current"), + ).toBe("true"); const tasks = renderView({ jobsLastStatusFilter: "error" }); const lastStatus = getElement( tasks, diff --git a/ui/src/styles/cron.css b/ui/src/styles/cron.css index 083dc729944e..5eed30abdbd6 100644 --- a/ui/src/styles/cron.css +++ b/ui/src/styles/cron.css @@ -1033,25 +1033,6 @@ } } -/* Match the .btn--sm dropdown triggers beside it instead of the native look. */ -.cron-run-sort { - flex: 0 0 auto; - width: auto; - padding: 6px 10px; - font-size: 12px; - font-weight: 500; - color: var(--text); - border: 1px solid var(--border); - border-radius: var(--radius-md); - background: var(--bg-elevated); - cursor: var(--cursor-action); -} - -.cron-run-sort:hover { - background: var(--bg-hover); - border-color: var(--border-strong); -} - .cron-runs__empty { font-size: 13px; }