Skip to content
Merged
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
15 changes: 15 additions & 0 deletions docs/reference/endpoint-inventory.json
Original file line number Diff line number Diff line change
Expand Up @@ -1315,6 +1315,17 @@
"companyUuid",
"timeOffPolicyUuid"
]
},
"UNSTABLE_TimeOff.HolidaySelectionForm": {
"endpoints": [
{
"method": "POST",
"path": "/v1/companies/:companyUuid/holiday_pay_policy"
}
],
"variables": [
"companyUuid"
]
}
},
"flows": {
Expand Down Expand Up @@ -2084,6 +2095,10 @@
"UNSTABLE_TimeOff.ViewPolicyEmployees"
],
"endpoints": [
{
"method": "POST",
"path": "/v1/companies/:companyUuid/holiday_pay_policy"
},
{
"method": "GET",
"path": "/v1/companies/:companyUuid/time_off_policies"
Expand Down
1 change: 1 addition & 0 deletions docs/reference/endpoint-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ import inventory from '@gusto/embedded-react-sdk/endpoint-inventory.json'
| **UNSTABLE_TimeOff.PolicyList** | GET | `/v1/companies/:companyUuid/time_off_policies` |
| | PUT | `/v1/time_off_policies/:timeOffPolicyUuid/deactivate` |
| | GET | `/v1/companies/:companyId/employees` |
| **UNSTABLE_TimeOff.HolidaySelectionForm** | POST | `/v1/companies/:companyUuid/holiday_pay_policy` |

## Flows

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
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 { HolidaySelectionForm } from './HolidaySelectionForm'
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'

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

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

describe('rendering', () => {
it('renders the heading', async () => {
renderWithProviders(<HolidaySelectionForm {...defaultProps} />)

await waitFor(() => {
expect(screen.getByText('Choose your company holidays')).toBeInTheDocument()
})
})

it('renders all 11 federal holidays', async () => {
renderWithProviders(<HolidaySelectionForm {...defaultProps} />)

await waitFor(() => {
expect(screen.getByText("New Year's Day")).toBeInTheDocument()
})
expect(screen.getByText('Martin Luther King, Jr. Day')).toBeInTheDocument()
expect(screen.getByText("Presidents' Day")).toBeInTheDocument()
expect(screen.getByText('Memorial Day')).toBeInTheDocument()
expect(screen.getByText('Juneteenth')).toBeInTheDocument()
expect(screen.getByText('Independence Day')).toBeInTheDocument()
expect(screen.getByText('Labor Day')).toBeInTheDocument()
expect(screen.getByText("Columbus Day (Indigenous Peoples' Day)")).toBeInTheDocument()
expect(screen.getByText('Veterans Day')).toBeInTheDocument()
expect(screen.getByText('Thanksgiving')).toBeInTheDocument()
expect(screen.getByText('Christmas Day')).toBeInTheDocument()
})

it('renders observed dates', async () => {
renderWithProviders(<HolidaySelectionForm {...defaultProps} />)

await waitFor(() => {
expect(screen.getByText('January 1')).toBeInTheDocument()
})
expect(screen.getByText('Third Monday in January')).toBeInTheDocument()
expect(screen.getByText('December 25')).toBeInTheDocument()
})

it('renders next observation dates with year', async () => {
renderWithProviders(<HolidaySelectionForm {...defaultProps} />)

await waitFor(() => {
expect(screen.getByText("New Year's Day")).toBeInTheDocument()
})

const container = screen.getByTestId('data-view')
expect(container.textContent).toMatch(/\d{4}/)
})

it('renders Back and Continue buttons', async () => {
renderWithProviders(<HolidaySelectionForm {...defaultProps} />)

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

it('renders all holidays as selected by default', async () => {
renderWithProviders(<HolidaySelectionForm {...defaultProps} />)

await waitFor(() => {
const checkboxes = screen.getAllByRole('checkbox')
expect(checkboxes.length).toBeGreaterThanOrEqual(11)
})
})
})

describe('selection', () => {
it('can deselect a holiday', async () => {
renderWithProviders(<HolidaySelectionForm {...defaultProps} />)

await waitFor(() => {
expect(screen.getAllByRole('checkbox').length).toBeGreaterThan(0)
})

const checkboxes = screen.getAllByRole('checkbox')
await user.click(checkboxes[1]!)

expect(checkboxes[1]).not.toBeChecked()
})
})

describe('actions', () => {
it('calls CANCEL event when Back button is clicked', async () => {
renderWithProviders(<HolidaySelectionForm {...defaultProps} />)

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

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

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

it('calls POST and emits HOLIDAY_SELECTION_DONE on Continue', async () => {
let postCalled = false
server.use(
http.post(`${API_BASE_URL}/v1/companies/:companyUuid/holiday_pay_policy`, () => {
postCalled = true
return HttpResponse.json({
version: 'abc123',
company_uuid: 'company-123',
federal_holidays: {},
employees: [],
})
}),
)

renderWithProviders(<HolidaySelectionForm {...defaultProps} />)

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

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

await waitFor(() => {
expect(onEvent).toHaveBeenCalledWith(componentEvents.TIME_OFF_HOLIDAY_SELECTION_DONE)
})

expect(postCalled).toBe(true)
})

it('emits HOLIDAY_SELECTION_DONE after deselecting a holiday and clicking Continue', async () => {
server.use(
http.post(`${API_BASE_URL}/v1/companies/:companyUuid/holiday_pay_policy`, () => {
return HttpResponse.json({
version: 'abc123',
company_uuid: 'company-123',
federal_holidays: {},
employees: [],
})
}),
)

renderWithProviders(<HolidaySelectionForm {...defaultProps} />)

await waitFor(() => {
expect(screen.getAllByRole('checkbox').length).toBeGreaterThan(0)
})

const checkboxes = screen.getAllByRole('checkbox')
await user.click(checkboxes[1]!)

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

await waitFor(() => {
expect(onEvent).toHaveBeenCalledWith(componentEvents.TIME_OFF_HOLIDAY_SELECTION_DONE)
})
})
})
})
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import { useCallback, useState } from 'react'
import { SelectHolidaysPresentation } from '../TimeOffManagement/SelectHolidays/SelectHolidaysPresentation'
import type { HolidayItem } from '../TimeOffManagement/SelectHolidays/SelectHolidaysTypes'
import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useHolidayPayPoliciesCreateMutation } from '@gusto/embedded-api/react-query/holidayPayPoliciesCreate'
import {
getDefaultHolidayItems,
buildFederalHolidaysPayload,
FEDERAL_HOLIDAY_KEYS,
} from '../shared/holidayHelpers'
import { HolidaySelectionFormPresentation } from './HolidaySelectionFormPresentation'
import type { HolidayItem } from './HolidaySelectionFormTypes'
import { BaseComponent, type BaseComponentInterface } from '@/components/Base'
import { useBase } from '@/components/Base/useBase'
import { componentEvents } from '@/shared/constants'
import { useI18n } from '@/i18n'

export interface HolidaySelectionFormProps extends BaseComponentInterface {
companyId: string
Expand All @@ -12,70 +20,21 @@ export interface HolidaySelectionFormProps extends BaseComponentInterface {
export function HolidaySelectionForm(props: HolidaySelectionFormProps) {
return (
<BaseComponent {...props}>
<Root />
<Root companyId={props.companyId} />
</BaseComponent>
)
}

const DEFAULT_HOLIDAYS: HolidayItem[] = [
{ uuid: 'new-years', name: "New Year's Day", observedDate: 'January 1', nextObservation: '' },
{
uuid: 'mlk',
name: 'Martin Luther King, Jr. Day',
observedDate: 'Third Monday in January',
nextObservation: '',
},
{
uuid: 'presidents',
name: "Presidents' Day",
observedDate: 'Third Monday in February',
nextObservation: '',
},
{
uuid: 'memorial',
name: 'Memorial Day',
observedDate: 'Last Monday in May',
nextObservation: '',
},
{ uuid: 'juneteenth', name: 'Juneteenth', observedDate: 'June 19', nextObservation: '' },
{
uuid: 'independence',
name: 'Independence Day',
observedDate: 'July 4',
nextObservation: '',
},
{
uuid: 'labor',
name: 'Labor Day',
observedDate: 'First Monday in September',
nextObservation: '',
},
{
uuid: 'columbus',
name: "Columbus Day (Indigenous Peoples' Day)",
observedDate: 'Second Monday in October',
nextObservation: '',
},
{ uuid: 'veterans', name: 'Veterans Day', observedDate: 'November 11', nextObservation: '' },
{
uuid: 'thanksgiving',
name: 'Thanksgiving',
observedDate: 'Fourth Thursday in November',
nextObservation: '',
},
{
uuid: 'christmas',
name: 'Christmas Day',
observedDate: 'December 25',
nextObservation: '',
},
]
function Root({ companyId }: { companyId: string }) {
useI18n('Company.TimeOff.HolidayPolicy')
const { t } = useTranslation('Company.TimeOff.HolidayPolicy')
const { onEvent, baseSubmitHandler } = useBase()

const ALL_HOLIDAY_UUIDS = new Set(DEFAULT_HOLIDAYS.map(h => h.uuid))
const holidays = useMemo(() => getDefaultHolidayItems(t), [t])
const allKeys = useMemo(() => new Set(FEDERAL_HOLIDAY_KEYS), [])
const [selectedUuids, setSelectedUuids] = useState(allKeys)

function Root() {
const { onEvent } = useBase()
const [selectedUuids, setSelectedUuids] = useState(ALL_HOLIDAY_UUIDS)
const { mutateAsync: createPolicy } = useHolidayPayPoliciesCreateMutation()

const handleSelectionChange = useCallback((item: HolidayItem, selected: boolean) => {
setSelectedUuids(prev => {
Expand All @@ -89,17 +48,27 @@ function Root() {
})
}, [])

const handleContinue = useCallback(() => {
onEvent(componentEvents.TIME_OFF_HOLIDAY_SELECTION_DONE)
}, [onEvent])
const handleContinue = useCallback(async () => {
await baseSubmitHandler({}, async () => {
await createPolicy({
request: {
companyUuid: companyId,
holidayPayPolicyRequest: {
federalHolidays: buildFederalHolidaysPayload(selectedUuids),
},
},
})
onEvent(componentEvents.TIME_OFF_HOLIDAY_SELECTION_DONE)
})
}, [baseSubmitHandler, createPolicy, companyId, selectedUuids, onEvent])

const handleBack = useCallback(() => {
onEvent(componentEvents.CANCEL)
}, [onEvent])

return (
<SelectHolidaysPresentation
holidays={DEFAULT_HOLIDAYS}
<HolidaySelectionFormPresentation
holidays={holidays}
selectedHolidayUuids={selectedUuids}
onSelectionChange={handleSelectionChange}
onContinue={handleContinue}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useState } from 'react'
import { fn } from 'storybook/test'
import { SelectHolidaysPresentation } from './SelectHolidaysPresentation'
import type { HolidayItem } from './SelectHolidaysTypes'
import { HolidaySelectionFormPresentation } from './HolidaySelectionFormPresentation'
import type { HolidayItem } from './HolidaySelectionFormTypes'
import { useI18n } from '@/i18n'

function I18nLoader({ children }: { children: React.ReactNode }) {
Expand All @@ -10,7 +10,7 @@ function I18nLoader({ children }: { children: React.ReactNode }) {
}

export default {
title: 'TimeOff/SelectHolidays',
title: 'TimeOff/HolidaySelectionForm',
decorators: [
(Story: React.ComponentType) => (
<I18nLoader>
Expand Down Expand Up @@ -111,7 +111,7 @@ function StoryWrapper({ initialSelected }: { initialSelected: Set<string> }) {
}

return (
<SelectHolidaysPresentation
<HolidaySelectionFormPresentation
holidays={holidays}
selectedHolidayUuids={selectedUuids}
onSelectionChange={handleSelectionChange}
Expand All @@ -127,4 +127,4 @@ export const PartialSelection = () => <StoryWrapper initialSelected={partialUuid

export const EditMode = () => <StoryWrapper initialSelected={new Set()} />

export const ViewMode = () => <SelectHolidaysPresentation mode="view" holidays={holidays} />
export const ViewMode = () => <HolidaySelectionFormPresentation mode="view" holidays={holidays} />
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import type { HolidayItem, SelectHolidaysPresentationProps } from './SelectHolidaysTypes'
import type {
HolidayItem,
HolidaySelectionFormPresentationProps,
} from './HolidaySelectionFormTypes'
import { DataView, Flex, ActionsLayout, useDataView } from '@/components/Common'
import { useComponentContext } from '@/contexts/ComponentAdapter/useComponentContext'
import { useI18n } from '@/i18n'

export function SelectHolidaysPresentation(props: SelectHolidaysPresentationProps) {
export function HolidaySelectionFormPresentation(props: HolidaySelectionFormPresentationProps) {
useI18n('Company.TimeOff.HolidayPolicy')
const { t } = useTranslation('Company.TimeOff.HolidayPolicy')
const { Heading, Text, Button } = useComponentContext()
Expand Down
Loading
Loading