diff --git a/packages/apollo-react/src/canvas/components/StageNode/StageNode.stories.tsx b/packages/apollo-react/src/canvas/components/StageNode/StageNode.stories.tsx index 7985c2868..6bd102b0c 100644 --- a/packages/apollo-react/src/canvas/components/StageNode/StageNode.stories.tsx +++ b/packages/apollo-react/src/canvas/components/StageNode/StageNode.stories.tsx @@ -953,6 +953,7 @@ export const MaximumTaskAdornments: Story = { label: 'Longest running required task', icon: , isRequired: true, + isMocked: true, hasEntryCondition: true, }, ], diff --git a/packages/apollo-react/src/canvas/components/StageNode/StageNode.types.ts b/packages/apollo-react/src/canvas/components/StageNode/StageNode.types.ts index 2cc312296..300b594d5 100644 --- a/packages/apollo-react/src/canvas/components/StageNode/StageNode.types.ts +++ b/packages/apollo-react/src/canvas/components/StageNode/StageNode.types.ts @@ -28,6 +28,7 @@ export interface StageTaskItem { isRequired?: boolean; isAdhoc?: boolean; isPlaceholder?: boolean; + isMocked?: boolean; taskGroupType?: 'sequential' | 'event-driven' | 'adhoc'; hasEntryCondition?: boolean; } diff --git a/packages/apollo-react/src/canvas/components/StageNode/tasks/AdhocTask.tsx b/packages/apollo-react/src/canvas/components/StageNode/tasks/AdhocTask.tsx index bb9d31b84..fabee019d 100644 --- a/packages/apollo-react/src/canvas/components/StageNode/tasks/AdhocTask.tsx +++ b/packages/apollo-react/src/canvas/components/StageNode/tasks/AdhocTask.tsx @@ -5,6 +5,7 @@ import type { StageTaskExecution, StageTaskItem } from '../StageNode.types'; import { TaskBreakpointDot } from './TaskBreakpointDot'; import { TaskContent } from './TaskContent'; import { TaskMenu, type TaskMenuHandle } from './TaskMenu'; +import { TaskMockedChip } from './TaskMockedChip'; interface AdhocTaskItemProps { task: StageTaskItem; @@ -63,6 +64,7 @@ const AdhocTaskItemComponent = ({ paused={taskExecution?.status === 'Paused'} onToggle={onToggleBreakpoint} /> + {getContextMenuItems && ( { expect(screen.getByTestId('stage-task-breakpoint-task-1')).toHaveClass('pointer-events-none'); }); }); + + describe('Mocked marker', () => { + it('renders the mocked marker on the task card when the task is mocked', () => { + render(); + + const chip = screen.getByTestId('stage-task-mocked-task-1'); + expect(screen.getByTestId('stage-task-card-task-1')).toContainElement(chip); + }); + + it('keeps the mocked marker out of the trailing actions group', () => { + render(); + + const chip = screen.getByTestId('stage-task-mocked-task-1'); + expect(screen.getByTestId('stage-task-actions-task-1')).not.toContainElement(chip); + }); + + it('does not render the mocked marker when the task is not mocked', () => { + render(); + + expect(screen.queryByTestId('stage-task-mocked-task-1')).not.toBeInTheDocument(); + }); + }); }); diff --git a/packages/apollo-react/src/canvas/components/StageNode/tasks/DraggableTask.tsx b/packages/apollo-react/src/canvas/components/StageNode/tasks/DraggableTask.tsx index 7605d3e12..cdbb25949 100644 --- a/packages/apollo-react/src/canvas/components/StageNode/tasks/DraggableTask.tsx +++ b/packages/apollo-react/src/canvas/components/StageNode/tasks/DraggableTask.tsx @@ -13,6 +13,7 @@ import type { DraggableTaskProps } from './DraggableTask.types'; import { TaskBreakpointDot } from './TaskBreakpointDot'; import { TaskContent } from './TaskContent'; import { TaskMenu, type TaskMenuHandle } from './TaskMenu'; +import { TaskMockedChip } from './TaskMockedChip'; const DraggableTaskComponent = ({ task, @@ -100,6 +101,7 @@ const DraggableTaskComponent = ({ paused={taskExecution?.status === 'Paused'} onToggle={onToggleBreakpoint} /> + + {getContextMenuItems && ( diff --git a/packages/apollo-react/src/canvas/components/StageNode/tasks/TaskMockedChip.test.tsx b/packages/apollo-react/src/canvas/components/StageNode/tasks/TaskMockedChip.test.tsx new file mode 100644 index 000000000..12d35324f --- /dev/null +++ b/packages/apollo-react/src/canvas/components/StageNode/tasks/TaskMockedChip.test.tsx @@ -0,0 +1,56 @@ +import { describe, expect, it, vi } from 'vitest'; +import { render, screen } from '../../../utils/testing'; +import { TaskMockedChip } from './TaskMockedChip'; + +// Surface CanvasTooltip content into the DOM so assertions don't depend on +// Radix's pointer-event-driven open/close logic (unreliable in jsdom). +vi.mock('../../CanvasTooltip', () => ({ + CanvasTooltip: ({ + content, + children, + }: { + content: React.ReactNode; + children: React.ReactNode; + }) => ( + + {children} + + ), +})); + +describe('TaskMockedChip', () => { + it('renders nothing when the task is not mocked', () => { + render(); + + expect(screen.queryByTestId('stage-task-mocked-t1')).not.toBeInTheDocument(); + }); + + it('renders a corner marker with an outputs-mocked tooltip when the task is mocked', () => { + render(); + + const chip = screen.getByTestId('stage-task-mocked-t1'); + expect(chip.closest('[data-testid="canvas-tooltip"]')).toHaveAttribute( + 'data-tooltip-content', + "This task's outputs are mocked" + ); + }); + + it('sits at the top-right corner of the task card, mirroring the breakpoint dot', () => { + render(); + + expect(screen.getByTestId('stage-task-mocked-t1')).toHaveClass( + 'absolute', + '-top-1.5', + '-right-1.5' + ); + }); + + it('labels the marker for screen readers', () => { + render(); + + expect(screen.getByRole('img', { name: "This task's outputs are mocked" })).toBeInTheDocument(); + }); +}); diff --git a/packages/apollo-react/src/canvas/components/StageNode/tasks/TaskMockedChip.tsx b/packages/apollo-react/src/canvas/components/StageNode/tasks/TaskMockedChip.tsx new file mode 100644 index 000000000..fa24d4cb0 --- /dev/null +++ b/packages/apollo-react/src/canvas/components/StageNode/tasks/TaskMockedChip.tsx @@ -0,0 +1,38 @@ +import { Badge } from '@uipath/apollo-wind'; +import { FlaskConical } from 'lucide-react'; +import { memo } from 'react'; +import { useSafeLingui } from '../../../../i18n'; +import { CanvasTooltip } from '../../CanvasTooltip'; + +export interface TaskMockedChipProps { + /** Whether this task's outputs are mocked. */ + mocked: boolean; + /** Task id, used for a stable test id. */ + taskId: string; +} + +function TaskMockedChipInner({ mocked, taskId }: TaskMockedChipProps) { + const { _ } = useSafeLingui(); + + if (!mocked) { + return null; + } + + const label = _({ id: 'stage-node.task-mocked', message: "This task's outputs are mocked" }); + + return ( + + + + + + ); +} + +export const TaskMockedChip = memo(TaskMockedChipInner); diff --git a/packages/apollo-react/src/canvas/locales/en.json b/packages/apollo-react/src/canvas/locales/en.json index e9a915ac0..7d764f5ef 100644 --- a/packages/apollo-react/src/canvas/locales/en.json +++ b/packages/apollo-react/src/canvas/locales/en.json @@ -34,6 +34,7 @@ "stage-node.status.early-exit": "Early exit", "stage-node.task-badge.ran-n-times": "{count, plural, one {Ran # time} other {Ran # times}}", "stage-node.task-badge.running-again": "Running again", + "stage-node.task-mocked": "This task's outputs are mocked", "stage-node.ungroup-parallel-tasks": "Ungroup parallel tasks", "stage-node.untitled-stage": "Untitled stage", "sticky-note.formatting.bold": "Bold ({boldShortcut})",