Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,130 @@ describe('usePayScheduleForm', () => {
expect(submitResult).toEqual(expect.objectContaining({ mode: 'update' }))
})
})

describe('clearing server-side errors on edit (SDK-923)', () => {
it('clears a custom (server-side) error on a date field when the user edits it', async () => {
const { result } = renderHook(() => usePayScheduleForm({ companyId: 'company-1' }), {
wrapper: GustoTestProvider,
})

await waitFor(() => {
expect(result.current.isLoading).toBe(false)
})
assertReady(result.current)
const { formMethods } = result.current.form.hookFormInternals

act(() => {
formMethods.setError('anchorPayDate', {
type: 'custom',
message: 'Pay date must be after start date',
})
})

await waitFor(() => {
expect(formMethods.getFieldState('anchorPayDate').error).toMatchObject({
type: 'custom',
})
})

act(() => {
formMethods.setValue('anchorPayDate', '2026-07-15', { shouldDirty: true })
})

await waitFor(() => {
expect(formMethods.getFieldState('anchorPayDate').error).toBeUndefined()
})
})

it('clears a custom error on the anchorEndOfPayPeriod date field when the user edits it', async () => {
const { result } = renderHook(() => usePayScheduleForm({ companyId: 'company-1' }), {
wrapper: GustoTestProvider,
})

await waitFor(() => {
expect(result.current.isLoading).toBe(false)
})
assertReady(result.current)
const { formMethods } = result.current.form.hookFormInternals

act(() => {
formMethods.setError('anchorEndOfPayPeriod', {
type: 'custom',
message: 'End of pay period invalid',
})
})

await waitFor(() => {
expect(formMethods.getFieldState('anchorEndOfPayPeriod').error).toMatchObject({
type: 'custom',
})
})

act(() => {
formMethods.setValue('anchorEndOfPayPeriod', '2026-07-22', { shouldDirty: true })
})

await waitFor(() => {
expect(formMethods.getFieldState('anchorEndOfPayPeriod').error).toBeUndefined()
})
})

it('does not clear non-custom (client-side) validation errors on edit', async () => {
const { result } = renderHook(() => usePayScheduleForm({ companyId: 'company-1' }), {
wrapper: GustoTestProvider,
})

await waitFor(() => {
expect(result.current.isLoading).toBe(false)
})
assertReady(result.current)
const { formMethods } = result.current.form.hookFormInternals

act(() => {
formMethods.setError('anchorPayDate', { type: 'required', message: 'Required' })
})

await waitFor(() => {
expect(formMethods.getFieldState('anchorPayDate').error?.type).toBe('required')
})

act(() => {
formMethods.setValue('anchorPayDate', '2026-07-15', { shouldDirty: true })
})

// Subscribe handler is scoped to type: 'custom' — RHF's normal validation
// loop owns client-side errors.
expect(formMethods.getFieldState('anchorPayDate').error?.type).toBe('required')
})

it('does not clear errors on non-date fields when they change', async () => {
const { result } = renderHook(() => usePayScheduleForm({ companyId: 'company-1' }), {
wrapper: GustoTestProvider,
})

await waitFor(() => {
expect(result.current.isLoading).toBe(false)
})
assertReady(result.current)
const { formMethods } = result.current.form.hookFormInternals

act(() => {
formMethods.setError('customName', { type: 'custom', message: 'Name taken' })
})

await waitFor(() => {
expect(formMethods.getFieldState('customName').error?.type).toBe('custom')
})

act(() => {
formMethods.setValue('customName', 'Updated', { shouldDirty: true })
})

// Subscribe is scoped to the two date fields; editing customName must
// not clear its custom error.
expect(formMethods.getFieldState('customName').error?.type).toBe('custom')
})
})
})

// ── Schema-level tests ──────────────────────────────────────────────────
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@
formMethods.setValue('day1', 15)
formMethods.setValue('day2', 31)
}
}, [watchedFrequency, watchedCustomTwicePerMonth, formMethods.setValue])

Check warning on line 310 in src/components/Company/PaySchedule/shared/usePayScheduleForm/usePayScheduleForm.tsx

View workflow job for this annotation

GitHub Actions / lint

React Hook useEffect has a missing dependency: 'formMethods'. Either include it or remove the dependency array

const formattedAnchorPayDate = formatWatchedDate(watchedAnchorPayDate)
const formattedAnchorEndOfPayPeriod = formatWatchedDate(watchedAnchorEndOfPayPeriod)
Expand Down Expand Up @@ -346,6 +346,27 @@
const queries = payScheduleId ? [payScheduleQuery, paymentConfigsQuery] : [paymentConfigsQuery]
const errorHandling = composeErrorHandler(queries, { submitError, setSubmitError })

// SDK-923: server-side validation errors (mirrored by SDKFormProvider as
// `type: 'custom'`) on the date fields block resubmission until refresh.
// Subscribe ONLY to the two date fields that can receive server errors and
// clear them on edit. Clearing the upstream submitError too stops the
// provider's sync effect from re-applying the error on the next render.
useEffect(() => {
const dateFields = ['anchorPayDate', 'anchorEndOfPayPeriod'] as const
const unsubscribe = formMethods.subscribe({
name: dateFields,
formState: { values: true },
callback: ({ name }) => {
if (!name || !(dateFields as readonly string[]).includes(name)) return
const fieldName = name as (typeof dateFields)[number]
if (formMethods.getFieldState(fieldName).error?.type !== 'custom') return
formMethods.clearErrors(fieldName)
setSubmitError(null)
},
})
return unsubscribe
}, [formMethods, setSubmitError])

const showCustomTwicePerMonth = watchedFrequency === 'Twice per month'
const showDay1 =
watchedFrequency === 'Monthly' ||
Expand Down
Loading