

























1+import { chromium, type Browser, type Page } from "playwright";
2+import { afterAll, beforeAll, describe, expect, it } from "vitest";
3+import {
4+canRunPlaywrightChromium,
5+installMockGateway,
6+startControlUiE2eServer,
7+type ControlUiE2eServer,
8+type MockGatewayControls,
9+type MockGatewayRequest,
10+} from "../../test-helpers/control-ui-e2e.ts";
11+12+const chromiumExecutablePath = chromium.executablePath();
13+const chromiumAvailable = canRunPlaywrightChromium(chromiumExecutablePath);
14+const allowMissingChromium = process.env.OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM === "1";
15+const describeControlUiE2e = chromiumAvailable || !allowMissingChromium ? describe : describe.skip;
16+17+let browser: Browser;
18+let server: ControlUiE2eServer;
19+20+function cronJob(id: string, name: string, schedule: Record<string, unknown>, state = {}) {
21+return {
22+ id,
23+ name,
24+enabled: true,
25+createdAtMs: Date.parse("2026-05-29T08:00:00.000Z"),
26+updatedAtMs: Date.parse("2026-05-29T08:05:00.000Z"),
27+ schedule,
28+sessionTarget: "main",
29+wakeMode: "next-heartbeat",
30+payload: { kind: "systemEvent", text: `${name} fired` },
31+ state,
32+};
33+}
34+35+function cronListResponse(jobs: unknown[], total = jobs.length) {
36+return {
37+ jobs,
38+ total,
39+offset: 0,
40+limit: 50,
41+hasMore: false,
42+nextOffset: null,
43+};
44+}
45+46+function requireRecord(value: unknown): Record<string, unknown> {
47+if (!value || typeof value !== "object" || Array.isArray(value)) {
48+throw new Error("Expected object value");
49+}
50+return value as Record<string, unknown>;
51+}
52+53+function requestParams(request: MockGatewayRequest): Record<string, unknown> {
54+return requireRecord(request.params);
55+}
56+57+async function waitForCronListRequest(
58+gateway: MockGatewayControls,
59+predicate: (params: Record<string, unknown>) => boolean,
60+): Promise<MockGatewayRequest> {
61+const deadline = Date.now() + 10_000;
62+let requests: MockGatewayRequest[] = [];
63+while (Date.now() < deadline) {
64+requests = await gateway.getRequests("cron.list");
65+const match = requests.find((request) => predicate(requestParams(request)));
66+if (match) {
67+return match;
68+}
69+await new Promise((resolve) => setTimeout(resolve, 50));
70+}
71+throw new Error(`No matching cron.list request found: ${JSON.stringify(requests)}`);
72+}
73+74+function jobTitle(page: Page, name: string) {
75+return page.locator(".cron-job .list-title", { hasText: new RegExp(`^${name}$`, "u") });
76+}
77+78+describeControlUiE2e("Control UI cron mocked Gateway E2E", () => {
79+beforeAll(async () => {
80+if (!chromiumAvailable) {
81+throw new Error(
82+`Playwright Chromium is not installed at ${chromiumExecutablePath}. Run \`pnpm --dir ui exec playwright install chromium\`, or set OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM=1 only when intentionally skipping this lane.`,
83+);
84+}
85+server = await startControlUiE2eServer();
86+browser = await chromium.launch();
87+});
88+89+afterAll(async () => {
90+await browser?.close();
91+await server?.close();
92+});
93+94+it("sends cron job table filters through the Gateway and renders the filtered page", async () => {
95+const everyOk = cronJob(
96+"digest-every-ok",
97+"Digest every minute",
98+{ kind: "every", everyMs: 60_000 },
99+{ lastRunStatus: "ok", lastRunAtMs: Date.parse("2026-05-29T08:10:00.000Z") },
100+);
101+const cronUnknown = cronJob(
102+"nightly-cron-unknown",
103+"Nightly cron pending",
104+{ kind: "cron", expr: "0 1 * * *", tz: "UTC" },
105+{},
106+);
107+108+const context = await browser.newContext({
109+locale: "en-US",
110+serviceWorkers: "block",
111+viewport: { height: 900, width: 1280 },
112+});
113+const page = await context.newPage();
114+const gateway = await installMockGateway(page, {
115+methodResponses: {
116+"cron.list": {
117+cases: [
118+{
119+match: { scheduleKind: "cron", lastRunStatus: "unknown" },
120+response: cronListResponse([cronUnknown]),
121+},
122+{
123+match: {},
124+response: cronListResponse([everyOk, cronUnknown], 2),
125+},
126+],
127+},
128+"cron.runs": {
129+entries: [],
130+total: 0,
131+offset: 0,
132+limit: 50,
133+hasMore: false,
134+nextOffset: null,
135+},
136+"cron.status": {
137+enabled: true,
138+jobs: 2,
139+nextWakeAtMs: Date.parse("2026-05-29T09:00:00.000Z"),
140+storePath: "/tmp/openclaw-e2e/cron/jobs.json",
141+},
142+},
143+});
144+145+try {
146+await page.goto(`${server.baseUrl}cron`);
147+await jobTitle(page, "Digest every minute").waitFor({ timeout: 10_000 });
148+await jobTitle(page, "Nightly cron pending").waitFor({ timeout: 10_000 });
149+150+const initialRequest = await waitForCronListRequest(
151+gateway,
152+(params) => params.limit === 50 && params.scheduleKind === "all",
153+);
154+expect(requestParams(initialRequest)).toMatchObject({
155+enabled: "all",
156+includeDisabled: true,
157+lastRunStatus: "all",
158+limit: 50,
159+offset: 0,
160+scheduleKind: "all",
161+sortBy: "nextRunAtMs",
162+sortDir: "asc",
163+});
164+165+await page.locator("details.cron-filter-panel").first().locator("summary").click();
166+await page.locator('[data-test-id="cron-jobs-schedule-filter"]').selectOption("cron");
167+await page.locator('[data-test-id="cron-jobs-last-status-filter"]').selectOption("unknown");
168+169+const filteredRequest = await waitForCronListRequest(
170+gateway,
171+(params) => params.scheduleKind === "cron" && params.lastRunStatus === "unknown",
172+);
173+expect(requestParams(filteredRequest)).toMatchObject({
174+enabled: "all",
175+includeDisabled: true,
176+lastRunStatus: "unknown",
177+limit: 50,
178+offset: 0,
179+scheduleKind: "cron",
180+sortBy: "nextRunAtMs",
181+sortDir: "asc",
182+});
183+await jobTitle(page, "Nightly cron pending").waitFor({ timeout: 10_000 });
184+await expect.poll(async () => jobTitle(page, "Digest every minute").count()).toBe(0);
185+} finally {
186+await context.close();
187+}
188+});
189+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。