From d1620965186c5b632c4ebecfcb1a8c68519d03f5 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Thu, 24 Sep 2026 11:45:34 -0700 Subject: [PATCH 1/2] refactor(ui): share deploy chat field error treatment --- .../components/chat/chat-field-error.test.tsx | 86 +++++++++++++++++++ .../deploy-modal/components/chat/chat.tsx | 29 ++++--- 2 files changed, 103 insertions(+), 12 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat-field-error.test.tsx diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat-field-error.test.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat-field-error.test.tsx new file mode 100644 index 00000000000..039844dfa4d --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat-field-error.test.tsx @@ -0,0 +1,86 @@ +import type { ComponentProps, PropsWithChildren } from 'react' +import { JSDOM } from 'jsdom' +import { renderToStaticMarkup } from 'react-dom/server' +import { describe, expect, it, vi } from 'vitest' + +const validation = vi.hoisted(() => ({ + current: { isChecking: false, error: 'Use lowercase letters', isValid: false } as { + isChecking: boolean + error: string | null + isValid: boolean + }, +})) + +vi.mock('@sim/emcn', () => ({ + Input: (props: ComponentProps<'input'>) => , + Label: (props: ComponentProps<'label'>) => ( + + ), + cn: (...values: unknown[]) => values.filter(Boolean).join(' '), + Tooltip: { + Root: ({ children }: PropsWithChildren) => <>{children}, + Trigger: ({ children }: PropsWithChildren) => <>{children}, + Content: ({ children }: PropsWithChildren) => <>{children}, + }, +})) +vi.mock('@sim/emcn/icons', () => ({ Check: () => null, TriangleAlert: () => null })) +vi.mock('@sim/logger', () => ({ createLogger: () => ({}) })) +vi.mock('@/components/ui', () => ({ GeneratedPasswordInput: () => null })) +vi.mock('@/lib/core/config/deployment-shape', () => ({ useDeploymentShape: () => ({}) })) +vi.mock('@/lib/core/utils/urls', () => ({ + getBaseUrl: () => 'https://sim.ai', + getEmailDomain: () => 'sim.ai', +})) +vi.mock('@/lib/messaging/email/validation', () => ({ validateAllowlistEntry: () => true })) +vi.mock('@/lib/workflows/streaming/output-selector', () => ({ + formatInternalOutputSelector: () => '', +})) +vi.mock( + '@/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select', + () => ({ + OutputSelect: () => null, + }) +) +vi.mock('@/hooks/queries/chats', () => ({ + useCreateChat: () => ({}), + useDeleteChat: () => ({}), + useRevealChatPassword: () => ({}), + useUpdateChat: () => ({}), +})) +vi.mock('@/hooks/use-permission-config', () => ({ usePermissionConfig: () => ({}) })) +vi.mock('./hooks', () => ({ useIdentifierValidation: () => validation.current })) + +import { IdentifierInput } from './chat' + +function renderIdentifier() { + return new JSDOM(renderToStaticMarkup()) + .window.document +} + +describe('deploy URL field error', () => { + it('announces and associates the URL validation error with its input', () => { + validation.current = { isChecking: false, error: 'Use lowercase letters', isValid: false } + const document = renderIdentifier() + const input = document.querySelector('#chat-url') + const alert = document.querySelector('[role="alert"]') + + expect(alert?.textContent).toBe('Use lowercase letters') + expect(alert?.className).toBe('mt-[6.5px] text-[var(--text-error)] text-caption') + expect(input?.getAttribute('aria-invalid')).toBe('true') + expect(input?.getAttribute('aria-describedby')).toBe(alert?.id) + expect(alert?.id).toBeTruthy() + expect(document.querySelector('label')?.htmlFor).toBe(input?.id) + }) + + it('omits the error relationship when the URL is valid', () => { + validation.current = { isChecking: false, error: null, isValid: true } + const document = renderIdentifier() + const input = document.querySelector('#chat-url') + + expect(document.querySelector('[role="alert"]')).toBeNull() + expect(input?.getAttribute('aria-invalid')).toBe('false') + expect(input?.hasAttribute('aria-describedby')).toBe(false) + }) +}) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx index b9dfb3271a2..bc6acdc0469 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx @@ -1,6 +1,6 @@ 'use client' -import { useEffect, useRef, useState } from 'react' +import { type ReactNode, useEffect, useId, useRef, useState } from 'react' import { ChipButtonGroup, ChipButtonGroupItem, @@ -49,6 +49,14 @@ const logger = createLogger('ChatDeploy') const IDENTIFIER_PATTERN = /^[a-z0-9-]+$/ +function DeployFieldError({ children, id }: { children: ReactNode; id?: string }) { + return ( + + ) +} + interface ChatDeployProps { workflowId: string deploymentInfo: { @@ -382,11 +390,7 @@ export function ChatDeploy({ className='w-full' disablePortal /> - {errors.outputBlocks && ( -

- {errors.outputBlocks} -

- )} + {errors.outputBlocks && {errors.outputBlocks}}
@@ -539,7 +543,7 @@ const getDomainPrefix = (() => { return () => prefix })() -function IdentifierInput({ +export function IdentifierInput({ value, onChange, originalIdentifier, @@ -547,6 +551,7 @@ function IdentifierInput({ onValidationChange, isEditingExisting = false, }: IdentifierInputProps) { + const errorId = useId() const { isChecking, error, isValid } = useIdentifierValidation( value, originalIdentifier, @@ -590,6 +595,8 @@ function IdentifierInput({ onChange={(e) => handleChange(e.target.value)} required disabled={disabled} + aria-invalid={Boolean(error)} + aria-describedby={error ? errorId : undefined} className={cn( 'rounded-none border-0 bg-transparent pl-0 shadow-none disabled:bg-transparent disabled:opacity-100', (isChecking || (isValid && value)) && 'pr-8' @@ -617,7 +624,7 @@ function IdentifierInput({ )}
- {error &&

{error}

} + {error && {error}}

{isEditingExisting && value ? ( <> @@ -745,9 +752,7 @@ function AuthSelector({ } /> {canRevealPassword && revealPasswordMutation.isError && ( -

- Failed to load the current password -

+ Failed to load the current password )}

{getPasswordHelperText(hasExistingPassword)} @@ -772,7 +777,7 @@ function AuthSelector({ )} - {error &&

{error}

} + {error && {error}} ) } From adb6ca1520ad5372ebddfffb01af93c40e74c383 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Thu, 24 Sep 2026 12:35:07 -0700 Subject: [PATCH 2/2] refactor(ui): name deploy error props --- .../components/deploy-modal/components/chat/chat.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx index bc6acdc0469..3caefaad692 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx @@ -49,7 +49,12 @@ const logger = createLogger('ChatDeploy') const IDENTIFIER_PATTERN = /^[a-z0-9-]+$/ -function DeployFieldError({ children, id }: { children: ReactNode; id?: string }) { +interface DeployFieldErrorProps { + children: ReactNode + id?: string +} + +function DeployFieldError({ children, id }: DeployFieldErrorProps) { return (