Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 20 additions & 8 deletions apps/web/src/pages/routine-detail-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -365,14 +365,26 @@ export function RoutineDetailPage({
subtitle={row.tenantName}
actions={
<>
<RunNowButton variant="outline" size="sm" onRun={onRunNow} />
<Button
type="button"
size="sm"
onClick={() => onToggleEnabled(!row.routine.enabled)}
>
{row.routine.enabled ? "Pause" : "Resume"}
</Button>
{!row.routine.enabled || health.state === "paused" ? (
<Button
type="button"
size="sm"
onClick={() => onToggleEnabled(true)}
>
Resume
</Button>
) : (
<>
<RunNowButton variant="outline" size="sm" onRun={onRunNow} />
<Button
type="button"
size="sm"
onClick={() => onToggleEnabled(false)}
>
Pause
</Button>
</>
)}
<Button type="button" variant="outline" size="sm" onClick={onEdit}>
Edit
</Button>
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/pages/routines-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ export function GlobalRoutinesList({
<TableCell>
<Switch
checked={row.routine.enabled}
label={`${row.routine.enabled ? "Pause" : "Resume"} ${row.routine.name}`}
label={`${row.routine.enabled ? "On" : "Off"} ${row.routine.name}`}
onCheckedChange={(enabled) => onToggleEnabled(row, enabled)}
/>
</TableCell>
Expand Down
72 changes: 72 additions & 0 deletions apps/web/test/routine-detail-page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,32 @@ describe("RoutineDetailPage", () => {
expect(actions).toContain("Run now");
expect(actions).toContain("Pause");
});

function topBarActions(markup: string): string {
const start = markup.indexOf('data-testid="stage-top-bar-actions"');
const end = markup.indexOf("</header>", start);
return markup.slice(start, end === -1 ? undefined : end);
}

test("hides Run now when the routine is off, and offers Resume only", () => {
const actions = topBarActions(
renderPage({ routine: { ...routine, enabled: false } }),
);
expect(actions).toContain("Resume");
expect(actions).not.toContain("Run now");
expect(actions).not.toContain("Pause");
});

test("hides Run now when the routine is dead-lettered, and offers Resume only", () => {
const actions = topBarActions(
renderPage({
routine: { ...routine, deadLetteredAt: "2026-01-02T00:00:00.000Z" },
}),
);
expect(actions).toContain("Resume");
expect(actions).not.toContain("Run now");
expect(actions).not.toContain("Pause");
});
});

describe("RoutineDetailPage lifecycle actions", () => {
Expand Down Expand Up @@ -237,6 +263,21 @@ describe("RoutineDetailPage lifecycle actions", () => {
}
});

test("Resume on a dead-lettered routine asks for it to be enabled again", () => {
const calls: boolean[] = [];
const { container, root } = mount(
{ onToggleEnabled: (enabled: boolean) => calls.push(enabled) },
{ routine: { ...routine, deadLetteredAt: "2026-01-02T00:00:00.000Z" } },
);
try {
clickButton(container, "Resume");
expect(calls).toEqual([true]);
} finally {
act(() => root.unmount());
container.remove();
}
});

test("Run now triggers the run-now mutation", () => {
let runs = 0;
const { container, root } = mount({
Expand All @@ -254,6 +295,37 @@ describe("RoutineDetailPage lifecycle actions", () => {
}
});

test("Run now reads as starting while the request is in flight", async () => {
let resolveRun: () => void = () => undefined;
const pending = new Promise<void>((resolve) => {
resolveRun = resolve;
});
const { container, root } = mount({
onRunNow: () => pending,
});
try {
clickButton(container, "Run now");
const button = [...container.querySelectorAll("button")].find(
(candidate) => candidate.textContent?.includes("Starting") === true,
);
expect(button).not.toBeUndefined();
expect(button?.disabled).toBe(true);

await act(async () => {
resolveRun();
await pending;
});
expect(
[...container.querySelectorAll("button")].some(
(candidate) => candidate.textContent?.includes("Starting") === true,
),
).toBe(false);
} finally {
act(() => root.unmount());
container.remove();
}
});

test("Edit opens the routine's editor", () => {
let edits = 0;
const { container, root } = mount({
Expand Down
24 changes: 24 additions & 0 deletions apps/web/test/routines-page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,20 @@ describe("GlobalRoutinesList", () => {
expect(markup).toContain("—");
});

test("the On switch is named On, not Pause, while the routine is enabled", () => {
const markup = renderList([row()]);
expect(markup).toContain('aria-label="On Morning brief"');
expect(markup).not.toContain('aria-label="Pause Morning brief"');
});

test("the On switch is named Off when the routine is disabled", () => {
const markup = renderList([
row({ routine: { ...routine, enabled: false } }),
]);
expect(markup).toContain('aria-label="Off Morning brief"');
expect(markup).not.toContain('aria-label="Resume Morning brief"');
});

test("Run now calls onRunNow with the row", async () => {
const calls: GlobalRoutineRow[] = [];
const container = document.createElement("div");
Expand Down Expand Up @@ -371,6 +385,16 @@ describe("GlobalRoutinesList", () => {
}
});

test("a dead-lettered routine's action reads Resume, not Run now", () => {
const markup = renderList([
row({
routine: { ...routine, deadLetteredAt: "2026-01-02T00:00:00.000Z" },
}),
]);
expect(markup).toContain("Resume");
expect(markup).not.toContain("Run now");
});

test("Edit calls onEditRoutine with the row", () => {
const calls: GlobalRoutineRow[] = [];
const container = document.createElement("div");
Expand Down
Loading