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`