Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit cdba1c6

Browse files
authored
Introduce PayrollLanding component as first step in RunPayrollFlow (#586)
* feat: add PayrollLanding component with tab navigation - Add PayrollLanding component with RunPayroll and PayrollHistory tabs - Fix TypeScript interface to include resource key generic - Add comprehensive unit tests focused on component orchestration - Add translation support with Payroll.PayrollLanding namespace - Fix ESLint import order in PayrollHistory.stories.tsx - Fix failing test in PayrollHistoryPresentation with waitFor - Add demo story for PayrollLanding (uses presentation components directly) - Update PayrollLanding to use proper BaseComponentInterface typing * fix: pr feedback * fix: pr feedback
1 parent 867f0f3 commit cdba1c6

File tree

14 files changed

+361
-125
lines changed

14 files changed

+361
-125
lines changed

.ladle/components.tsx

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import type { GlobalProvider } from '@ladle/react'
22
import '../src/styles/sdk.scss'
33
import { useState } from 'react'
4+
import { I18nextProvider } from 'react-i18next'
45
import { PlainComponentAdapter } from './adapters/PlainComponentAdapter'
56
import { defaultComponents } from '@/contexts/ComponentAdapter/adapters/defaultComponentAdapter'
67
import { GustoProviderCustomUIAdapter } from '@/contexts'
8+
import { SDKI18next } from '@/contexts/GustoProvider/SDKI18next'
9+
import { LocaleProvider } from '@/contexts/LocaleProvider'
10+
import { ThemeProvider } from '@/contexts/ThemeProvider'
711

812
const AdapterToggle = ({
913
mode,
@@ -42,12 +46,18 @@ const AdapterToggle = ({
4246
export const Provider: GlobalProvider = ({ children }: { children: React.ReactNode }) => {
4347
const [mode, setMode] = useState<'default' | 'plain'>('default')
4448
return (
45-
<GustoProviderCustomUIAdapter
46-
config={{ baseUrl: '' }}
47-
components={mode === 'plain' ? PlainComponentAdapter : defaultComponents}
48-
>
49-
{children}
50-
<AdapterToggle mode={mode} setMode={setMode} />
51-
</GustoProviderCustomUIAdapter>
49+
<I18nextProvider i18n={SDKI18next}>
50+
<LocaleProvider locale="en-US" currency="USD">
51+
<ThemeProvider>
52+
<GustoProviderCustomUIAdapter
53+
config={{ baseUrl: '' }}
54+
components={mode === 'plain' ? PlainComponentAdapter : defaultComponents}
55+
>
56+
{children}
57+
<AdapterToggle mode={mode} setMode={setMode} />
58+
</GustoProviderCustomUIAdapter>
59+
</ThemeProvider>
60+
</LocaleProvider>
61+
</I18nextProvider>
5262
)
5363
}

.ladle/helpers/I18nWrapper.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { I18nextProvider } from 'react-i18next'
2+
import { SDKI18next } from '@/contexts/GustoProvider/SDKI18next'
3+
import { LocaleProvider } from '@/contexts/LocaleProvider'
4+
import { ThemeProvider } from '@/contexts/ThemeProvider'
5+
6+
interface I18nWrapperProps {
7+
children: React.ReactNode
8+
}
9+
10+
export const I18nWrapper = ({ children }: I18nWrapperProps) => {
11+
return (
12+
<I18nextProvider i18n={SDKI18next}>
13+
<LocaleProvider locale="en-US" currency="USD">
14+
<ThemeProvider>{children}</ThemeProvider>
15+
</LocaleProvider>
16+
</I18nextProvider>
17+
)
18+
}

src/components/Payroll/PayrollHistory/PayrollHistory.stories.tsx

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { action } from '@ladle/react'
22
import type { Payroll } from '@gusto/embedded-api/models/components/payroll'
3+
import { I18nWrapper } from '../../../../.ladle/helpers/I18nWrapper'
34
import { PayrollHistoryPresentation } from './PayrollHistoryPresentation'
45
import type { PayrollHistoryItem } from './PayrollHistory'
56

@@ -75,26 +76,30 @@ const mockPayrollHistory: PayrollHistoryItem[] = [
7576

7677
export const PayrollHistoryStory = () => {
7778
return (
78-
<PayrollHistoryPresentation
79-
payrollHistory={mockPayrollHistory}
80-
selectedTimeFilter="3months"
81-
onTimeFilterChange={action('onTimeFilterChange')}
82-
onViewSummary={action('onViewSummary')}
83-
onViewReceipt={action('onViewReceipt')}
84-
onCancelPayroll={action('onCancelPayroll')}
85-
/>
79+
<I18nWrapper>
80+
<PayrollHistoryPresentation
81+
payrollHistory={mockPayrollHistory}
82+
selectedTimeFilter="3months"
83+
onTimeFilterChange={action('onTimeFilterChange')}
84+
onViewSummary={action('onViewSummary')}
85+
onViewReceipt={action('onViewReceipt')}
86+
onCancelPayroll={action('onCancelPayroll')}
87+
/>
88+
</I18nWrapper>
8689
)
8790
}
8891

8992
export const EmptyState = () => {
9093
return (
91-
<PayrollHistoryPresentation
92-
payrollHistory={[]}
93-
selectedTimeFilter="3months"
94-
onTimeFilterChange={action('onTimeFilterChange')}
95-
onViewSummary={action('onViewSummary')}
96-
onViewReceipt={action('onViewReceipt')}
97-
onCancelPayroll={action('onCancelPayroll')}
98-
/>
94+
<I18nWrapper>
95+
<PayrollHistoryPresentation
96+
payrollHistory={[]}
97+
selectedTimeFilter="3months"
98+
onTimeFilterChange={action('onTimeFilterChange')}
99+
onViewSummary={action('onViewSummary')}
100+
onViewReceipt={action('onViewReceipt')}
101+
onCancelPayroll={action('onCancelPayroll')}
102+
/>
103+
</I18nWrapper>
99104
)
100105
}

src/components/Payroll/PayrollHistory/PayrollHistoryPresentation.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,10 +148,12 @@ export const PayrollHistoryPresentation = ({
148148
<Flex
149149
flexDirection={{ base: 'column', medium: 'row' }}
150150
justifyContent="space-between"
151-
alignItems={{ base: 'flex-start', medium: 'center' }}
151+
alignItems="flex-start"
152152
gap={{ base: 12, medium: 24 }}
153153
>
154-
<Heading as="h2">{t('title')}</Heading>
154+
<Flex>
155+
<Heading as="h2">{t('title')}</Heading>
156+
</Flex>
155157
<div className={styles.timeFilterContainer}>
156158
<Select
157159
value={selectedTimeFilter}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest'
2+
import { screen, waitFor } from '@testing-library/react'
3+
import userEvent from '@testing-library/user-event'
4+
import { PayrollLanding } from './PayrollLanding'
5+
import { renderWithProviders } from '@/test-utils/renderWithProviders'
6+
import { setupApiTestMocks } from '@/test/mocks/apiServer'
7+
8+
describe('PayrollLanding', () => {
9+
const defaultProps = {
10+
companyId: 'test-company-123',
11+
onEvent: vi.fn(),
12+
}
13+
14+
beforeEach(() => {
15+
vi.clearAllMocks()
16+
setupApiTestMocks()
17+
})
18+
19+
describe('rendering', () => {
20+
it('renders the payroll list in run payroll tab by default', async () => {
21+
renderWithProviders(<PayrollLanding {...defaultProps} />)
22+
23+
// Wait for the tabs to load
24+
await waitFor(() => {
25+
expect(screen.getByRole('tab', { name: 'Run payroll' })).toBeInTheDocument()
26+
})
27+
28+
// Verify the Run payroll tab is selected
29+
expect(screen.getByRole('tab', { name: 'Run payroll' })).toHaveAttribute(
30+
'aria-selected',
31+
'true',
32+
)
33+
34+
// Verify PayrollList component content is rendered (look for the heading)
35+
await waitFor(() => {
36+
expect(screen.getByRole('heading', { name: 'Upcoming payroll' })).toBeInTheDocument()
37+
})
38+
})
39+
40+
it('does not render payroll history component by default', async () => {
41+
renderWithProviders(<PayrollLanding {...defaultProps} />)
42+
43+
await waitFor(() => {
44+
expect(screen.getByRole('tab', { name: 'Run payroll' })).toBeInTheDocument()
45+
})
46+
47+
// Verify payroll history tab exists but is not selected
48+
expect(screen.getByRole('tab', { name: 'Payroll history' })).toHaveAttribute(
49+
'aria-selected',
50+
'false',
51+
)
52+
53+
// Verify PayrollHistory content is not visible (should not see its heading)
54+
expect(screen.queryByRole('heading', { name: 'Payroll history' })).not.toBeInTheDocument()
55+
})
56+
})
57+
58+
describe('tab navigation', () => {
59+
it('switches between tabs correctly', async () => {
60+
const user = userEvent.setup()
61+
renderWithProviders(<PayrollLanding {...defaultProps} />)
62+
63+
await waitFor(() => {
64+
expect(screen.getByRole('tab', { name: 'Run payroll' })).toBeInTheDocument()
65+
})
66+
67+
// Switch to payroll history tab
68+
const payrollHistoryTab = screen.getByRole('tab', { name: 'Payroll history' })
69+
await user.click(payrollHistoryTab)
70+
71+
await waitFor(() => {
72+
expect(screen.getByRole('tab', { name: 'Payroll history' })).toHaveAttribute(
73+
'aria-selected',
74+
'true',
75+
)
76+
})
77+
expect(screen.getByRole('tab', { name: 'Run payroll' })).toHaveAttribute(
78+
'aria-selected',
79+
'false',
80+
)
81+
await waitFor(() => {
82+
expect(screen.getByRole('heading', { name: 'Payroll history' })).toBeInTheDocument()
83+
})
84+
85+
// Switch back to run payroll tab
86+
const runPayrollTab = screen.getByRole('tab', { name: 'Run payroll' })
87+
await user.click(runPayrollTab)
88+
89+
await waitFor(() => {
90+
expect(screen.getByRole('tab', { name: 'Run payroll' })).toHaveAttribute(
91+
'aria-selected',
92+
'true',
93+
)
94+
})
95+
expect(screen.getByRole('tab', { name: 'Payroll history' })).toHaveAttribute(
96+
'aria-selected',
97+
'false',
98+
)
99+
await waitFor(() => {
100+
expect(screen.getByRole('heading', { name: 'Upcoming payroll' })).toBeInTheDocument()
101+
})
102+
})
103+
})
104+
105+
describe('accessibility', () => {
106+
it('renders with proper accessibility structure', async () => {
107+
renderWithProviders(<PayrollLanding {...defaultProps} />)
108+
109+
await waitFor(() => {
110+
expect(screen.getByRole('tab', { name: 'Run payroll' })).toBeInTheDocument()
111+
})
112+
113+
// Verify the component renders with proper tab structure
114+
expect(screen.getByRole('tab', { name: 'Run payroll' })).toBeInTheDocument()
115+
expect(screen.getByRole('tab', { name: 'Payroll history' })).toBeInTheDocument()
116+
})
117+
})
118+
})
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { useState } from 'react'
2+
import { useTranslation } from 'react-i18next'
3+
import { PayrollHistory } from '../PayrollHistory/PayrollHistory'
4+
import { PayrollList } from '../PayrollList/PayrollList'
5+
import type { BaseComponentInterface } from '@/components/Base/Base'
6+
import { BaseComponent } from '@/components/Base/Base'
7+
import { useI18n } from '@/i18n'
8+
import { useComponentContext } from '@/contexts/ComponentAdapter/useComponentContext'
9+
10+
interface PayrollLandingProps extends BaseComponentInterface<'Payroll.PayrollLanding'> {
11+
companyId: string
12+
}
13+
14+
export function PayrollLanding(props: PayrollLandingProps) {
15+
return (
16+
<BaseComponent {...props}>
17+
<Root {...props} />
18+
</BaseComponent>
19+
)
20+
}
21+
22+
export const Root = ({ onEvent, companyId }: PayrollLandingProps) => {
23+
const [selectedTab, setSelectedTab] = useState('run-payroll')
24+
const { Tabs } = useComponentContext()
25+
26+
useI18n('Payroll.PayrollLanding')
27+
const { t } = useTranslation('Payroll.PayrollLanding')
28+
29+
const tabs = [
30+
{
31+
id: 'run-payroll',
32+
label: t('tabs.runPayroll'),
33+
content: <PayrollList companyId={companyId} onEvent={onEvent} />,
34+
},
35+
{
36+
id: 'payroll-history',
37+
label: t('tabs.payrollHistory'),
38+
content: <PayrollHistory companyId={companyId} onEvent={onEvent} />,
39+
},
40+
]
41+
42+
return (
43+
<Tabs
44+
tabs={tabs}
45+
selectedId={selectedTab}
46+
onSelectionChange={setSelectedTab}
47+
aria-label={t('aria.tabNavigation')}
48+
/>
49+
)
50+
}

0 commit comments

Comments
 (0)