diff --git a/app/src/components/settings/panels/cron/CoreJobList.test.tsx b/app/src/components/settings/panels/cron/CoreJobList.test.tsx new file mode 100644 index 000000000..b5ec087e5 --- /dev/null +++ b/app/src/components/settings/panels/cron/CoreJobList.test.tsx @@ -0,0 +1,78 @@ +import { render, screen } from '@testing-library/react'; +import { describe, expect, test, vi } from 'vitest'; + +import type { CoreCronJob, CoreCronRun } from '../../../../utils/tauriCommands'; +import CoreJobList from './CoreJobList'; + +vi.mock('../../../../lib/i18n/I18nContext', () => ({ + useT: () => ({ + t: (key: string) => + ({ + 'common.enabled': 'Enabled', + 'common.remove': 'Remove', + 'settings.cron.jobs.desc': 'Manage cron jobs', + 'settings.cron.jobs.lastStatus': 'Last status', + 'settings.cron.jobs.nextRun': 'Next run', + 'settings.cron.jobs.pause': 'Pause', + 'settings.cron.jobs.recentRuns': 'Recent runs', + 'settings.cron.jobs.schedule': 'Schedule', + 'settings.cron.jobs.title': 'Scheduled Jobs', + 'settings.cron.jobs.viewRuns': 'View Runs', + 'subconscious.runNow': 'Run Now', + })[key] ?? key, + }), +})); + +const job: CoreCronJob = { + id: 'morning_briefing', + expression: '0 9 * * *', + schedule: { kind: 'cron', expr: '0 9 * * *' }, + command: 'brief', + name: 'Morning Briefing', + job_type: 'agent', + session_target: 'isolated', + enabled: true, + delivery: { mode: 'origin', best_effort: true }, + delete_after_run: false, + created_at: '2026-05-18T00:00:00.000Z', + next_run: '2026-05-18T09:00:00.000Z', + last_status: 'ok', +}; + +const run: CoreCronRun = { + id: 42, + job_id: job.id, + started_at: '2026-05-18T09:00:00.000Z', + finished_at: '2026-05-18T09:00:05.000Z', + status: 'success', +}; + +function renderList() { + return render( + + ); +} + +describe('CoreJobList stable test hooks', () => { + test('renders deterministic data-testid hooks for the row and row actions', () => { + renderList(); + + expect(screen.getByTestId('cron-job-row-morning_briefing')).toBeInTheDocument(); + expect(screen.getByTestId('cron-job-toggle-morning_briefing')).toHaveTextContent('Pause'); + expect(screen.getByTestId('cron-job-run-morning_briefing')).toHaveTextContent('Run Now'); + expect(screen.getByTestId('cron-job-view-runs-morning_briefing')).toHaveTextContent( + 'View Runs' + ); + expect(screen.getByTestId('cron-job-remove-morning_briefing')).toHaveTextContent('Remove'); + expect(screen.getByTestId('cron-job-runs-morning_briefing')).toHaveTextContent('success'); + }); +}); diff --git a/app/src/components/settings/panels/cron/CoreJobList.tsx b/app/src/components/settings/panels/cron/CoreJobList.tsx index 14f1a71ab..af6e916dd 100644 --- a/app/src/components/settings/panels/cron/CoreJobList.tsx +++ b/app/src/components/settings/panels/cron/CoreJobList.tsx @@ -52,6 +52,7 @@ const CoreJobList = ({ return (
@@ -100,6 +101,7 @@ const CoreJobList = ({
{runs.length > 0 && ( -
+
{t('settings.cron.jobs.recentRuns')}
diff --git a/app/test/e2e/specs/cron-jobs-flow.spec.ts b/app/test/e2e/specs/cron-jobs-flow.spec.ts index 0215a620d..c47ff3f0f 100644 --- a/app/test/e2e/specs/cron-jobs-flow.spec.ts +++ b/app/test/e2e/specs/cron-jobs-flow.spec.ts @@ -57,50 +57,41 @@ async function waitForAnyText(candidates: string[], timeoutMs = 10_000): Promise return null; } -/** Click the action button (Pause | Resume | Remove | …) inside the morning_briefing row. */ -async function clickActionForJob(jobName: string, action: string): Promise { +/** Click the action button (Pause | Resume | Remove | …) inside the seeded cron row. */ +async function clickActionForJob( + jobId: string, + action: 'toggle' | 'run' | 'view-runs' | 'remove' +): Promise { return Boolean( await browser.execute( - (name: string, label: string) => { - const rows = Array.from(document.querySelectorAll('div')) - .filter(div => /text-sm font-semibold text-stone-900/.test(div.className)) - .filter(div => (div.textContent ?? '').trim() === name); - if (rows.length === 0) return false; - // Walk up to the panel row container (sibling-of-sibling structure in CoreJobList). - const container = rows[0]?.closest('div.p-4'); - if (!container) return false; - const buttons = Array.from(container.querySelectorAll('button')); - const btn = buttons.find(b => (b.textContent ?? '').trim() === label); + (id: string, actionName: string) => { + const btn = document.querySelector( + `[data-testid="cron-job-${actionName}-${id}"]` + ); if (!btn) return false; btn.click(); return true; }, - jobName, + jobId, action ) ); } -/** Poll for the in-row action button label to settle (e.g. "Pause" → "Resume"). */ +/** Poll for the in-row toggle action button label to settle (e.g. "Pause" → "Resume"). */ async function waitForRowActionLabel( - jobName: string, + jobId: string, expected: string, timeoutMs = 10_000 ): Promise { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { - const current = await browser.execute((name: string) => { - const rows = Array.from(document.querySelectorAll('div')) - .filter(div => /text-sm font-semibold text-stone-900/.test(div.className)) - .filter(div => (div.textContent ?? '').trim() === name); - const container = rows[0]?.closest('div.p-4'); - if (!container) return null; - const labels = Array.from(container.querySelectorAll('button')).map(b => - (b.textContent ?? '').trim() + const current = await browser.execute((id: string) => { + const btn = document.querySelector( + `[data-testid="cron-job-toggle-${id}"]` ); - // We care about the toggle button (first one in the row). - return labels[0] ?? null; - }, jobName); + return (btn?.textContent ?? '').trim() || null; + }, jobId); if (current === expected) return true; await browser.pause(400); } @@ -156,7 +147,7 @@ describe('Cron jobs settings panel (real UI flow)', () => { const startLabel = await waitForRowActionLabel(MORNING_BRIEFING, 'Pause', 5_000); expect(startLabel).toBe(true); - const clicked = await clickActionForJob(MORNING_BRIEFING, 'Pause'); + const clicked = await clickActionForJob(MORNING_BRIEFING, 'toggle'); expect(clicked).toBe(true); const flipped = await waitForRowActionLabel(MORNING_BRIEFING, 'Resume', 10_000); @@ -170,14 +161,14 @@ describe('Cron jobs settings panel (real UI flow)', () => { expect(stillResumed).toBe(true); // Restore so the next test starts from the enabled state. - const restored = await clickActionForJob(MORNING_BRIEFING, 'Resume'); + const restored = await clickActionForJob(MORNING_BRIEFING, 'toggle'); expect(restored).toBe(true); const back = await waitForRowActionLabel(MORNING_BRIEFING, 'Pause', 10_000); expect(back).toBe(true); }); it('clicking Remove deletes the job from both the UI and the sidecar', async () => { - const clicked = await clickActionForJob(MORNING_BRIEFING, 'Remove'); + const clicked = await clickActionForJob(MORNING_BRIEFING, 'remove'); expect(clicked).toBe(true); // UI assertion first — the row should disappear and the empty state appear.