diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/entry-block-tile/entry-block-tile.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/entry-block-tile/entry-block-tile.tsx index 88ada367e9a..5c718c1be07 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/entry-block-tile/entry-block-tile.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/entry-block-tile/entry-block-tile.tsx @@ -15,6 +15,7 @@ export interface EntryBlockTileProps { export const EntryBlockTile = memo(function EntryBlockTile({ blockType }: EntryBlockTileProps) { return ( - Running - - ) + return Running }) /** diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/terminal-row-button.test.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/terminal-row-button.test.tsx new file mode 100644 index 00000000000..d6d5ab87610 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/terminal-row-button.test.tsx @@ -0,0 +1,110 @@ +/** + * @vitest-environment jsdom + */ +import { act } from 'react' +import { BlockTileView } from '@sim/workflow-renderer' +import { createRoot, type Root } from 'react-dom/client' +import { renderToStaticMarkup } from 'react-dom/server' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { + StatusDisplay, + TerminalRowButton, +} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components' +import { ROW_STYLES } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/types' + +let root: Root +let host: HTMLDivElement + +beforeEach(() => { + ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true + host = document.createElement('div') + document.body.appendChild(host) + root = createRoot(host) +}) + +afterEach(() => { + act(() => root.unmount()) + host.remove() +}) + +describe('TerminalRowButton', () => { + it('renders selected disclosure semantics and handles one native click locally', () => { + const onClick = vi.fn() + const onParentClick = vi.fn() + + act(() => { + root.render( +
+ + Workflow result + +
+ ) + }) + + const button = host.querySelector('button')! + expect(button.type).toBe('button') + expect(button.getAttribute('aria-expanded')).toBe('true') + expect(button.getAttribute('aria-current')).toBe('true') + expect(button.getAttribute('data-entry-id')).toBe('entry-1') + expect(button.className).toBe(ROW_STYLES.rowSelected) + act(() => button.focus()) + expect(document.activeElement).toBe(button) + act(() => button.click()) + expect(onClick).toHaveBeenCalledTimes(1) + expect(onParentClick).not.toHaveBeenCalled() + }) + + it('keeps the base chip row when selection and expansion are absent', () => { + act(() => { + root.render(Block output) + }) + const button = host.querySelector('button')! + expect(button.className).toBe(ROW_STYLES.row) + expect(button.hasAttribute('aria-expanded')).toBe(false) + expect(button.hasAttribute('aria-current')).toBe(false) + expect(button.textContent).toBe('Block output') + }) + + it('does not mark an unselected output row as current', () => { + act(() => { + root.render(Other output) + }) + expect(host.querySelector('button')?.hasAttribute('aria-current')).toBe(false) + }) + + it('keeps the running status inline inside a native button', () => { + const html = renderToStaticMarkup( + + + + ) + expect(html).toMatch(/^Running') + expect(html).not.toContain(' { + const Icon = ({ className }: { className?: string }) => + const html = renderToStaticMarkup( + + + + Agent + + + + + + + ) + const document = new DOMParser().parseFromString(html, 'text/html') + const button = document.querySelector('button') + expect(button?.getAttribute('aria-expanded')).toBe('false') + expect(button?.textContent).toContain('Agent') + expect(button?.textContent).toContain('Running') + expect(button?.querySelector('[data-workflow-type-icon="agent"]')).not.toBeNull() + expect(button?.querySelectorAll('button, a, div')).toHaveLength(0) + expect(document.body.children).toHaveLength(1) + }) +}) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/terminal-row-button.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/terminal-row-button.tsx new file mode 100644 index 00000000000..fb590115baf --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/terminal-row-button.tsx @@ -0,0 +1,31 @@ +import type { ButtonHTMLAttributes } from 'react' +import { cn } from '@sim/emcn' +import { ROW_STYLES } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/types' + +export interface TerminalRowButtonProps + extends Omit, 'aria-current'> { + /** Use the selected chip surface for the active output row. */ + selected?: boolean +} + +/** Native terminal row action with the established EMCN chip surface. */ +export function TerminalRowButton({ + selected, + className, + onClick, + type, + ...props +}: TerminalRowButtonProps) { + return ( +