mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
test(cron): add stable job row selectors (#2062)
Co-authored-by: honor2030 <9779460+honor2030@users.noreply.github.com>
This commit is contained in:
@@ -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(
|
||||
<CoreJobList
|
||||
loading={false}
|
||||
coreJobs={[job]}
|
||||
coreRunsByJob={{ [job.id]: [run] }}
|
||||
coreBusyKey={null}
|
||||
onToggleCoreJob={vi.fn()}
|
||||
onRunCoreJob={vi.fn()}
|
||||
onLoadCoreRuns={vi.fn()}
|
||||
onRemoveCoreJob={vi.fn()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
@@ -52,6 +52,7 @@ const CoreJobList = ({
|
||||
return (
|
||||
<div
|
||||
key={job.id}
|
||||
data-testid={`cron-job-row-${job.id}`}
|
||||
className={`p-4 ${index === 0 ? '' : 'border-t border-stone-200 dark:border-neutral-800'} space-y-3`}>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
@@ -100,6 +101,7 @@ const CoreJobList = ({
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
data-testid={`cron-job-toggle-${job.id}`}
|
||||
className="btn btn-sm btn-outline"
|
||||
disabled={coreBusyKey === `core-toggle:${job.id}`}
|
||||
onClick={() => onToggleCoreJob(job)}>
|
||||
@@ -111,6 +113,7 @@ const CoreJobList = ({
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid={`cron-job-run-${job.id}`}
|
||||
className="btn btn-sm btn-outline"
|
||||
disabled={coreBusyKey === `core-run:${job.id}`}
|
||||
onClick={() => onRunCoreJob(job.id)}>
|
||||
@@ -120,6 +123,7 @@ const CoreJobList = ({
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid={`cron-job-view-runs-${job.id}`}
|
||||
className="btn btn-sm btn-outline"
|
||||
disabled={coreBusyKey === `core-runs:${job.id}`}
|
||||
onClick={() => onLoadCoreRuns(job.id)}>
|
||||
@@ -129,6 +133,7 @@ const CoreJobList = ({
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid={`cron-job-remove-${job.id}`}
|
||||
className="btn btn-sm btn-error"
|
||||
disabled={coreBusyKey === `core-remove:${job.id}`}
|
||||
onClick={() => onRemoveCoreJob(job.id)}>
|
||||
@@ -139,7 +144,9 @@ const CoreJobList = ({
|
||||
</div>
|
||||
|
||||
{runs.length > 0 && (
|
||||
<div className="rounded-lg border border-stone-200 dark:border-neutral-800 bg-stone-50 dark:bg-neutral-800/60 p-3 space-y-1">
|
||||
<div
|
||||
data-testid={`cron-job-runs-${job.id}`}
|
||||
className="rounded-lg border border-stone-200 dark:border-neutral-800 bg-stone-50 dark:bg-neutral-800/60 p-3 space-y-1">
|
||||
<div className="text-[11px] uppercase tracking-wide text-stone-400 dark:text-neutral-500">
|
||||
{t('settings.cron.jobs.recentRuns')}
|
||||
</div>
|
||||
|
||||
@@ -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<boolean> {
|
||||
/** Click the action button (Pause | Resume | Remove | …) inside the seeded cron row. */
|
||||
async function clickActionForJob(
|
||||
jobId: string,
|
||||
action: 'toggle' | 'run' | 'view-runs' | 'remove'
|
||||
): Promise<boolean> {
|
||||
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<HTMLButtonElement>('button'));
|
||||
const btn = buttons.find(b => (b.textContent ?? '').trim() === label);
|
||||
(id: string, actionName: string) => {
|
||||
const btn = document.querySelector<HTMLButtonElement>(
|
||||
`[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<boolean> {
|
||||
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<HTMLButtonElement>('button')).map(b =>
|
||||
(b.textContent ?? '').trim()
|
||||
const current = await browser.execute((id: string) => {
|
||||
const btn = document.querySelector<HTMLButtonElement>(
|
||||
`[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.
|
||||
|
||||
Reference in New Issue
Block a user