Skip to content
Draft
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
378 changes: 334 additions & 44 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"./CHANGELOG.md"
],
"scripts": {
"postinstall": "patch-package",
"build": "npm run build:clean && npm run i18n:generate && vite build",
"build:ci": "npm run build && npm run lint:check && npm run format:check && npm run test:ci",
"build:clean": "rm -rf ./dist && mkdir ./dist",
Expand All @@ -42,7 +43,7 @@
"watch:vite": "vite build --watch --mode development",
"watch:translations": "node ./build/translationWatcher.js",
"dev": "node ./build/prompt.js && npm run i18n:generate && npm-run-all --parallel watch:vite watch:translations",
"dev:setup": "npm link ../gws-flows/node_modules/react && (cd ../gws-flows && yarn link -r ../embedded-react-sdk)",
"dev:setup": "npm link ../gws-flows/node_modules/react ../gws-flows/node_modules/react-dom && (cd ../gws-flows && yarn link -r ../embedded-react-sdk)",
"docs:events": "npx tsx ./build/eventTypeDocsEmitter.ts",
"docs:sync": "npx tsx .docs/src/sync/syncManager.ts",
"docs": "npx tsx .docs/src/preview/previewGenerator.ts",
Expand Down Expand Up @@ -94,6 +95,7 @@
"lint-staged": "^16.2.7",
"msw": "^2.12.7",
"npm-run-all": "^4.1.5",
"patch-package": "^8.0.1",
"prettier": "^3.7.4",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
Expand Down
26 changes: 26 additions & 0 deletions patches/@gusto+embedded-api+0.11.4.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
diff --git a/node_modules/@gusto/embedded-api/esm/models/components/payrollemployeecompensationstype.js b/node_modules/@gusto/embedded-api/esm/models/components/payrollemployeecompensationstype.js
index 5655c44..27f2bc2 100644
--- a/node_modules/@gusto/embedded-api/esm/models/components/payrollemployeecompensationstype.js
+++ b/node_modules/@gusto/embedded-api/esm/models/components/payrollemployeecompensationstype.js
@@ -57,7 +57,7 @@ export function hourlyCompensationsFromJSON(jsonString) {
export const PayrollEmployeeCompensationsTypePaidTimeOff$inboundSchema = z.object({
name: z.string().optional(),
hours: z.string().optional(),
- final_payout_unused_hours_input: z.string().optional(),
+ final_payout_unused_hours_input: z.nullable(z.string()).optional(),
}).transform((v) => {
return remap$(v, {
"final_payout_unused_hours_input": "finalPayoutUnusedHoursInput",
diff --git a/node_modules/@gusto/embedded-api/src/models/components/payrollemployeecompensationstype.ts b/node_modules/@gusto/embedded-api/src/models/components/payrollemployeecompensationstype.ts
index f96feb5..d1e539a 100644
--- a/node_modules/@gusto/embedded-api/src/models/components/payrollemployeecompensationstype.ts
+++ b/node_modules/@gusto/embedded-api/src/models/components/payrollemployeecompensationstype.ts
@@ -270,7 +270,7 @@ export const PayrollEmployeeCompensationsTypePaidTimeOff$inboundSchema:
> = z.object({
name: z.string().optional(),
hours: z.string().optional(),
- final_payout_unused_hours_input: z.string().optional(),
+ final_payout_unused_hours_input: z.nullable(z.string()).optional(),
}).transform((v) => {
return remap$(v, {
"final_payout_unused_hours_input": "finalPayoutUnusedHoursInput",
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
.root {
width: 100%;

dt {
font-weight: var(--g-fontWeightMedium);
color: var(--g-colorBodyContent);
}

dd {
font-weight: var(--g-fontWeightRegular);
color: var(--g-colorBodySubContent);
}

.item {
&:not(:last-child) {
padding-bottom: toRem(20);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { http, HttpResponse } from 'msw'
import { TerminateEmployee } from './TerminateEmployee'
import { server } from '@/test/mocks/server'
import { componentEvents } from '@/shared/constants'
import { setupApiTestMocks } from '@/test/mocks/apiServer'
import { renderWithProviders } from '@/test-utils/renderWithProviders'
import { API_BASE_URL } from '@/test/constants'

const mockEmployee = {
uuid: 'employee-123',
first_name: 'John',
last_name: 'Doe',
email: '[email protected]',
company_uuid: 'company-123',
terminated: false,
onboarded: true,
}

const mockTermination = {
uuid: 'termination-123',
employee_uuid: 'employee-123',
effective_date: '2025-01-15',
run_termination_payroll: true,
active: false,
cancelable: true,
}

const mockTerminationPayPeriods = [
{
employee_uuid: 'employee-123',
employee_name: 'John Doe',
start_date: '2025-01-01',
end_date: '2025-01-15',
check_date: '2025-01-20',
pay_schedule_uuid: 'pay-schedule-123',
},
]

const mockPayrollPrepared = {
payroll_uuid: 'payroll-123',
company_uuid: 'company-123',
off_cycle: true,
off_cycle_reason: 'Dismissed employee',
}

describe('TerminateEmployee', () => {
const onEvent = vi.fn()
const user = userEvent.setup()
const defaultProps = {
employeeId: 'employee-123',
companyId: 'company-123',
onEvent,
}

beforeEach(() => {
setupApiTestMocks()
onEvent.mockClear()

server.use(
http.get(`${API_BASE_URL}/v1/employees/:employee_id`, () => {
return HttpResponse.json(mockEmployee)
}),
http.post(`${API_BASE_URL}/v1/employees/:employee_id/terminations`, () => {
return HttpResponse.json(mockTermination)
}),
http.get(
`${API_BASE_URL}/v1/companies/:company_id/pay_periods/unprocessed_termination_pay_periods`,
() => {
return HttpResponse.json(mockTerminationPayPeriods)
},
),
http.post(`${API_BASE_URL}/v1/companies/:company_id/payrolls`, () => {
return HttpResponse.json(mockPayrollPrepared)
}),
)
})

describe('rendering', () => {
it('renders the termination form with employee name', async () => {
renderWithProviders(<TerminateEmployee {...defaultProps} />)

await waitFor(() => {
expect(screen.getByRole('heading', { name: 'Terminate John Doe' })).toBeInTheDocument()
})

expect(
screen.getByText(/Set their last day of work and choose how to handle their final payroll/),
).toBeInTheDocument()
})

it('renders the date picker for last day of work', async () => {
renderWithProviders(<TerminateEmployee {...defaultProps} />)

await waitFor(() => {
expect(screen.getByText('Last day of work')).toBeInTheDocument()
})
})

it('renders all payroll options', async () => {
renderWithProviders(<TerminateEmployee {...defaultProps} />)

await waitFor(() => {
expect(screen.getByText('Dismissal payroll')).toBeInTheDocument()
})

expect(screen.getByText('Regular payroll')).toBeInTheDocument()
expect(screen.getByText('Another way')).toBeInTheDocument()
})

it('renders submit and cancel buttons', async () => {
renderWithProviders(<TerminateEmployee {...defaultProps} />)

await waitFor(() => {
expect(screen.getByRole('button', { name: 'Terminate employee' })).toBeInTheDocument()
})

expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument()
})
})

describe('form validation', () => {
it('shows validation error when submitting without date', async () => {
renderWithProviders(<TerminateEmployee {...defaultProps} />)

await waitFor(() => {
expect(screen.getByRole('button', { name: 'Terminate employee' })).toBeInTheDocument()
})

await user.click(screen.getByRole('button', { name: 'Terminate employee' }))

await waitFor(() => {
expect(screen.getByText('Last day of work is required')).toBeInTheDocument()
})
})

it('has dismissal payroll selected by default', async () => {
renderWithProviders(<TerminateEmployee {...defaultProps} />)

await waitFor(() => {
expect(screen.getByLabelText('Dismissal payroll')).toBeChecked()
})
})
})

describe('cancel action', () => {
it('emits CANCEL event when cancel button is clicked', async () => {
renderWithProviders(<TerminateEmployee {...defaultProps} />)

await waitFor(() => {
expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument()
})

await user.click(screen.getByRole('button', { name: 'Cancel' }))

expect(onEvent).toHaveBeenCalledWith(componentEvents.CANCEL)
})
})

describe('payroll option selection', () => {
it('allows selecting dismissal payroll option', async () => {
renderWithProviders(<TerminateEmployee {...defaultProps} />)

await waitFor(() => {
expect(screen.getByText('Dismissal payroll')).toBeInTheDocument()
})

const dismissalRadio = screen.getByLabelText('Dismissal payroll')
await user.click(dismissalRadio)

expect(dismissalRadio).toBeChecked()
})

it('allows selecting regular payroll option', async () => {
renderWithProviders(<TerminateEmployee {...defaultProps} />)

await waitFor(() => {
expect(screen.getByText('Regular payroll')).toBeInTheDocument()
})

const regularRadio = screen.getByLabelText('Regular payroll')
await user.click(regularRadio)

expect(regularRadio).toBeChecked()
})

it('allows selecting another way option', async () => {
renderWithProviders(<TerminateEmployee {...defaultProps} />)

await waitFor(() => {
expect(screen.getByText('Another way')).toBeInTheDocument()
})

const anotherWayRadio = screen.getByLabelText('Another way')
await user.click(anotherWayRadio)

expect(anotherWayRadio).toBeChecked()
})

it('shows option descriptions', async () => {
renderWithProviders(<TerminateEmployee {...defaultProps} />)

await waitFor(() => {
expect(
screen.getByText(/Runs a final payroll that automatically pays out unused PTO/),
).toBeInTheDocument()
})

expect(
screen.getByText(/Same as dismissal payrolls, except there won.t be a separate record/),
).toBeInTheDocument()

expect(
screen.getByText(/You can run an off-cycle payroll to manually calculate/),
).toBeInTheDocument()
})
})
})
Loading