@faststore/core 4.4.1-dev.0 → 4.5.0-dev.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/.turbo/turbo-generate.log +4 -4
  2. package/.turbo/turbo-test.log +66 -52
  3. package/@generated/cached-operations.json +2 -0
  4. package/@generated/gql.ts +12 -0
  5. package/@generated/graphql.ts +73 -0
  6. package/@generated/persisted-documents.json +2 -0
  7. package/@generated/schema.graphql +59 -0
  8. package/CHANGELOG.md +6 -0
  9. package/package.json +6 -6
  10. package/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawer.tsx +5 -1
  11. package/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawerBody.tsx +16 -6
  12. package/src/components/account/Layout/Layout.tsx +14 -5
  13. package/src/components/account/quotes/MyAccountListQuotes/MyAccountListQuotes.tsx +170 -0
  14. package/src/components/account/quotes/MyAccountListQuotes/MyAccountListQuotesTable/MyAccountListQuotesTable.tsx +194 -0
  15. package/src/components/account/quotes/MyAccountListQuotes/MyAccountListQuotesTable/MyAccountQuoteStatusBadge.tsx +15 -0
  16. package/src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesFilterSlider/MyAccountQuotesFilterSlider.tsx +162 -0
  17. package/src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesFilterSlider/section.module.scss +49 -0
  18. package/src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesSelectedTags/MyAccountQuotesSelectedTags.tsx +125 -0
  19. package/src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesStatusSelector/MyAccountQuotesStatusSelector.tsx +107 -0
  20. package/src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesStatusSelector/styles.module.scss +144 -0
  21. package/src/components/account/quotes/MyAccountListQuotes/index.ts +2 -0
  22. package/src/components/account/quotes/MyAccountListQuotes/quoteFilters.ts +89 -0
  23. package/src/components/account/quotes/MyAccountListQuotes/styles.module.scss +174 -0
  24. package/src/customizations/src/myAccount/extensions/quotes/after.tsx +5 -0
  25. package/src/customizations/src/myAccount/extensions/quotes/before.tsx +5 -0
  26. package/src/pages/pvt/account/quotes/index.tsx +264 -0
  27. package/src/sdk/account/getMyAccountRoutes.ts +7 -0
  28. package/src/utils/quoteStatus.ts +27 -0
  29. package/test/components/account/Layout.test.tsx +62 -0
  30. package/test/components/account/OrganizationDrawerBody.test.tsx +52 -0
  31. package/test/components/quotes/MyAccountListQuotes.test.tsx +232 -0
  32. package/test/components/quotes/MyAccountListQuotesTable.test.tsx +240 -0
  33. package/test/components/quotes/MyAccountQuoteStatusBadge.test.tsx +28 -0
  34. package/test/components/quotes/MyAccountQuotesFilterSlider.test.tsx +152 -0
  35. package/test/components/quotes/MyAccountQuotesSelectedTags.test.tsx +122 -0
  36. package/test/components/quotes/MyAccountQuotesStatusSelector.test.tsx +85 -0
  37. package/test/components/quotes/quoteFilters.test.ts +185 -0
  38. package/test/sdk/account/getMyAccountRoutes.test.ts +90 -0
  39. package/test/server/index.test.ts +2 -0
@@ -0,0 +1,240 @@
1
+ /**
2
+ * @vitest-environment jsdom
3
+ */
4
+
5
+ import { render, screen } from '@testing-library/react'
6
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
7
+
8
+ const mockUseSession = vi.hoisted(() => vi.fn())
9
+ const mockUseScreenResize = vi.hoisted(() => vi.fn())
10
+ vi.mock('src/sdk/session', () => ({ useSession: mockUseSession }))
11
+ vi.mock('src/sdk/ui/useScreenResize', () => ({
12
+ default: mockUseScreenResize,
13
+ }))
14
+
15
+ import MyAccountListQuotesTable, {
16
+ Pagination,
17
+ } from '../../../src/components/account/quotes/MyAccountListQuotes/MyAccountListQuotesTable/MyAccountListQuotesTable'
18
+
19
+ const originalLocation = window.location
20
+
21
+ function stubLocation() {
22
+ Object.defineProperty(window, 'location', {
23
+ configurable: true,
24
+ value: { ...originalLocation, search: '', href: '' },
25
+ })
26
+ }
27
+
28
+ function baseFilters(page = 1) {
29
+ return {
30
+ page,
31
+ status: [],
32
+ createdAtFrom: '',
33
+ createdAtTo: '',
34
+ expiresAtFrom: '',
35
+ expiresAtTo: '',
36
+ label: '',
37
+ }
38
+ }
39
+
40
+ function baseQuote(overrides: Partial<Record<string, unknown>> = {}) {
41
+ return {
42
+ id: 'q-1',
43
+ status: 'Draft',
44
+ label: null,
45
+ createdAt: '2026-01-01T10:00:00Z',
46
+ expiresAt: '2099-01-01T00:00:00Z',
47
+ amount: 150.5,
48
+ createdBy: null,
49
+ ...overrides,
50
+ }
51
+ }
52
+
53
+ describe('MyAccountListQuotesTable', () => {
54
+ beforeEach(() => {
55
+ stubLocation()
56
+ mockUseSession.mockReturnValue({
57
+ locale: 'en-US',
58
+ currency: { code: 'USD' },
59
+ })
60
+ })
61
+
62
+ afterEach(() => {
63
+ vi.clearAllMocks()
64
+ Object.defineProperty(window, 'location', {
65
+ configurable: true,
66
+ value: originalLocation,
67
+ })
68
+ })
69
+
70
+ it('renders a row per quote with id, status and formatted amount', () => {
71
+ mockUseScreenResize.mockReturnValue({ isDesktop: true })
72
+
73
+ render(
74
+ <MyAccountListQuotesTable
75
+ listQuotes={{ list: [baseQuote()], paging: {} as any }}
76
+ total={1}
77
+ perPage={25}
78
+ filters={baseFilters()}
79
+ />
80
+ )
81
+
82
+ expect(screen.getByText('q-1')).toBeTruthy()
83
+ expect(screen.getByText('Draft')).toBeTruthy()
84
+ expect(screen.getByText('$150.50')).toBeTruthy()
85
+ })
86
+
87
+ it('renders the label when present and omits it when absent', () => {
88
+ mockUseScreenResize.mockReturnValue({ isDesktop: true })
89
+
90
+ const { rerender } = render(
91
+ <MyAccountListQuotesTable
92
+ listQuotes={{
93
+ list: [baseQuote({ label: 'My favorite quote' })],
94
+ paging: {} as any,
95
+ }}
96
+ total={1}
97
+ perPage={25}
98
+ filters={baseFilters()}
99
+ />
100
+ )
101
+ expect(screen.getByText('My favorite quote')).toBeTruthy()
102
+
103
+ rerender(
104
+ <MyAccountListQuotesTable
105
+ listQuotes={{ list: [baseQuote({ label: null })], paging: {} as any }}
106
+ total={1}
107
+ perPage={25}
108
+ filters={baseFilters()}
109
+ />
110
+ )
111
+ expect(screen.queryByText('My favorite quote')).toBeNull()
112
+ })
113
+
114
+ it('hides the dates column on mobile', () => {
115
+ mockUseScreenResize.mockReturnValue({ isDesktop: false })
116
+
117
+ render(
118
+ <MyAccountListQuotesTable
119
+ listQuotes={{ list: [baseQuote()], paging: {} as any }}
120
+ total={1}
121
+ perPage={25}
122
+ filters={baseFilters()}
123
+ />
124
+ )
125
+
126
+ expect(screen.queryByText('Creation date')).toBeNull()
127
+ })
128
+
129
+ it('shows "Created by" only when createdBy is set', () => {
130
+ mockUseScreenResize.mockReturnValue({ isDesktop: true })
131
+
132
+ render(
133
+ <MyAccountListQuotesTable
134
+ listQuotes={{
135
+ list: [baseQuote({ createdBy: 'Ada Lovelace' })],
136
+ paging: {} as any,
137
+ }}
138
+ total={1}
139
+ perPage={25}
140
+ filters={baseFilters()}
141
+ />
142
+ )
143
+
144
+ expect(screen.getByText('Created by')).toBeTruthy()
145
+ expect(screen.getByText('Ada Lovelace')).toBeTruthy()
146
+ })
147
+
148
+ it('shows a relative expiry hint for quotes expiring soon', () => {
149
+ mockUseScreenResize.mockReturnValue({ isDesktop: true })
150
+ const soon = new Date(Date.now() + 3 * 3_600_000).toISOString()
151
+
152
+ render(
153
+ <MyAccountListQuotesTable
154
+ listQuotes={{
155
+ list: [baseQuote({ expiresAt: soon })],
156
+ paging: {} as any,
157
+ }}
158
+ total={1}
159
+ perPage={25}
160
+ filters={baseFilters()}
161
+ />
162
+ )
163
+
164
+ expect(screen.getByText(/hours left/)).toBeTruthy()
165
+ })
166
+
167
+ it('omits the relative expiry hint for already-expired quotes', () => {
168
+ mockUseScreenResize.mockReturnValue({ isDesktop: true })
169
+ const past = new Date(Date.now() - 3_600_000).toISOString()
170
+
171
+ render(
172
+ <MyAccountListQuotesTable
173
+ listQuotes={{
174
+ list: [baseQuote({ expiresAt: past })],
175
+ paging: {} as any,
176
+ }}
177
+ total={1}
178
+ perPage={25}
179
+ filters={baseFilters()}
180
+ />
181
+ )
182
+
183
+ expect(screen.queryByText(/left/)).toBeNull()
184
+ })
185
+ })
186
+
187
+ describe('Pagination', () => {
188
+ beforeEach(() => {
189
+ stubLocation()
190
+ })
191
+
192
+ afterEach(() => {
193
+ Object.defineProperty(window, 'location', {
194
+ configurable: true,
195
+ value: originalLocation,
196
+ })
197
+ })
198
+
199
+ it('renders the current index range', () => {
200
+ render(<Pagination page={2} total={60} perPage={25} />)
201
+
202
+ expect(screen.getByText('26 — 50 of 60')).toBeTruthy()
203
+ })
204
+
205
+ it('caps the last index label at the total', () => {
206
+ render(<Pagination page={3} total={60} perPage={25} />)
207
+
208
+ expect(screen.getByText('51 — 60 of 60')).toBeTruthy()
209
+ })
210
+
211
+ it('disables the previous button on the first page', () => {
212
+ render(<Pagination page={1} total={60} perPage={25} />)
213
+
214
+ const button = screen.getByRole('button', { name: 'Previous Page' })
215
+ expect((button as HTMLButtonElement).disabled).toBe(true)
216
+ })
217
+
218
+ it('disables the next button on the last page', () => {
219
+ render(<Pagination page={3} total={60} perPage={25} />)
220
+
221
+ const button = screen.getByRole('button', { name: 'Next Page' })
222
+ expect((button as HTMLButtonElement).disabled).toBe(true)
223
+ })
224
+
225
+ it('navigates without a page param when going back to page 1', () => {
226
+ render(<Pagination page={2} total={60} perPage={25} />)
227
+
228
+ screen.getByRole('button', { name: 'Previous Page' }).click()
229
+
230
+ expect(window.location.href).toBe('/pvt/account/quotes')
231
+ })
232
+
233
+ it('navigates with the target page param otherwise', () => {
234
+ render(<Pagination page={1} total={60} perPage={25} />)
235
+
236
+ screen.getByRole('button', { name: 'Next Page' }).click()
237
+
238
+ expect(window.location.href).toBe('/pvt/account/quotes?page=2')
239
+ })
240
+ })
@@ -0,0 +1,28 @@
1
+ /**
2
+ * @vitest-environment jsdom
3
+ */
4
+
5
+ import { render, screen } from '@testing-library/react'
6
+ import { describe, expect, it } from 'vitest'
7
+
8
+ import MyAccountQuoteStatusBadge from '../../../src/components/account/quotes/MyAccountListQuotes/MyAccountListQuotesTable/MyAccountQuoteStatusBadge'
9
+
10
+ describe('MyAccountQuoteStatusBadge', () => {
11
+ it('renders the mapped label and variant for a known status', () => {
12
+ render(<MyAccountQuoteStatusBadge status="Approved" />)
13
+
14
+ const badge = screen.getByText('Approved')
15
+ expect(badge.getAttribute('data-fs-my-account-badge-variant')).toBe(
16
+ 'success'
17
+ )
18
+ })
19
+
20
+ it('falls back to the raw status and a neutral variant when unknown', () => {
21
+ render(<MyAccountQuoteStatusBadge status="SomethingElse" />)
22
+
23
+ const badge = screen.getByText('SomethingElse')
24
+ expect(badge.getAttribute('data-fs-my-account-badge-variant')).toBe(
25
+ 'neutral'
26
+ )
27
+ })
28
+ })
@@ -0,0 +1,152 @@
1
+ /**
2
+ * @vitest-environment jsdom
3
+ */
4
+
5
+ import { fireEvent, render, screen } from '@testing-library/react'
6
+ import React from 'react'
7
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
8
+
9
+ vi.mock('@faststore/ui', async () => {
10
+ const actual =
11
+ await vi.importActual<typeof import('@faststore/ui')>('@faststore/ui')
12
+ return {
13
+ ...actual,
14
+ FilterSlider: ({
15
+ children,
16
+ applyBtnProps,
17
+ clearBtnProps,
18
+ }: React.PropsWithChildren<{
19
+ applyBtnProps?: { onClick?: () => void; children?: React.ReactNode }
20
+ clearBtnProps?: { onClick?: () => void; children?: React.ReactNode }
21
+ }>) => (
22
+ <div>
23
+ {children}
24
+ <button onClick={applyBtnProps?.onClick}>
25
+ {applyBtnProps?.children}
26
+ </button>
27
+ <button onClick={clearBtnProps?.onClick}>
28
+ {clearBtnProps?.children}
29
+ </button>
30
+ </div>
31
+ ),
32
+ }
33
+ })
34
+
35
+ import MyAccountQuotesFilterSlider from '../../../src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesFilterSlider/MyAccountQuotesFilterSlider'
36
+ import { getAllFacets } from '../../../src/components/account/quotes/MyAccountListQuotes/quoteFilters'
37
+
38
+ const originalLocation = window.location
39
+
40
+ function stubLocation(search = '') {
41
+ Object.defineProperty(window, 'location', {
42
+ configurable: true,
43
+ value: { ...originalLocation, search, href: '' },
44
+ })
45
+ }
46
+
47
+ function baseProps(overrides: Record<string, unknown> = {}) {
48
+ return {
49
+ facets: getAllFacets({
50
+ page: 1,
51
+ status: [],
52
+ createdAtFrom: '',
53
+ createdAtTo: '',
54
+ expiresAtFrom: '',
55
+ expiresAtTo: '',
56
+ label: '',
57
+ }),
58
+ testId: 'test-filter-slider',
59
+ dispatch: vi.fn(),
60
+ expanded: false,
61
+ selected: [],
62
+ title: 'Filters',
63
+ clearButtonLabel: 'Clear All',
64
+ applyButtonLabel: 'View Results',
65
+ ...overrides,
66
+ }
67
+ }
68
+
69
+ describe('MyAccountQuotesFilterSlider', () => {
70
+ beforeEach(() => {
71
+ stubLocation()
72
+ })
73
+
74
+ afterEach(() => {
75
+ vi.clearAllMocks()
76
+ Object.defineProperty(window, 'location', {
77
+ configurable: true,
78
+ value: originalLocation,
79
+ })
80
+ })
81
+
82
+ it('navigates with the selected status params on apply', () => {
83
+ render(
84
+ <MyAccountQuotesFilterSlider
85
+ {...(baseProps({
86
+ selected: [{ key: 'status', value: 'Draft' }],
87
+ }) as any)}
88
+ />
89
+ )
90
+
91
+ fireEvent.click(screen.getByText('View Results'))
92
+
93
+ expect(window.location.href).toBe('/pvt/account/quotes?status=Draft')
94
+ })
95
+
96
+ it('navigates with the created date range params on apply', () => {
97
+ render(<MyAccountQuotesFilterSlider {...(baseProps() as any)} />)
98
+
99
+ fireEvent.change(screen.getAllByLabelText('From')[0], {
100
+ target: { value: '2026-01-01' },
101
+ })
102
+ fireEvent.change(screen.getAllByLabelText('To')[0], {
103
+ target: { value: '2026-01-31' },
104
+ })
105
+
106
+ fireEvent.click(screen.getByText('View Results'))
107
+
108
+ expect(window.location.href).toBe(
109
+ '/pvt/account/quotes?createdAtFrom=2026-01-01&createdAtTo=2026-01-31'
110
+ )
111
+ })
112
+
113
+ it('preserves an existing label param when applying filters', () => {
114
+ stubLocation('?label=hello')
115
+
116
+ render(
117
+ <MyAccountQuotesFilterSlider
118
+ {...(baseProps({
119
+ selected: [{ key: 'status', value: 'Draft' }],
120
+ }) as any)}
121
+ />
122
+ )
123
+
124
+ fireEvent.click(screen.getByText('View Results'))
125
+
126
+ expect(window.location.href).toBe(
127
+ '/pvt/account/quotes?label=hello&status=Draft'
128
+ )
129
+ })
130
+
131
+ it('navigates with no query string when no filters are applied', () => {
132
+ render(<MyAccountQuotesFilterSlider {...(baseProps() as any)} />)
133
+
134
+ fireEvent.click(screen.getByText('View Results'))
135
+
136
+ expect(window.location.href).toBe('/pvt/account/quotes')
137
+ })
138
+
139
+ it('clears the created/expiry date refs and dispatches an empty facet selection on clear', () => {
140
+ const dispatch = vi.fn()
141
+ render(
142
+ <MyAccountQuotesFilterSlider {...(baseProps({ dispatch }) as any)} />
143
+ )
144
+
145
+ fireEvent.click(screen.getByText('Clear All'))
146
+
147
+ expect(dispatch).toHaveBeenCalledWith({
148
+ type: 'selectFacets',
149
+ payload: [],
150
+ })
151
+ })
152
+ })
@@ -0,0 +1,122 @@
1
+ /**
2
+ * @vitest-environment jsdom
3
+ */
4
+
5
+ import { fireEvent, render, screen } from '@testing-library/react'
6
+ import { describe, expect, it, vi } from 'vitest'
7
+
8
+ const mockUseSession = vi.hoisted(() => vi.fn())
9
+ vi.mock('src/sdk/session', () => ({ useSession: mockUseSession }))
10
+
11
+ import MyAccountQuotesSelectedTags from '../../../src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesSelectedTags/MyAccountQuotesSelectedTags'
12
+ import { formatFilterDate } from '../../../src/components/account/quotes/MyAccountListQuotes/quoteFilters'
13
+
14
+ mockUseSession.mockReturnValue({ locale: 'en-US' })
15
+
16
+ describe('MyAccountQuotesSelectedTags', () => {
17
+ it('renders nothing when there are no active filters', () => {
18
+ const { container } = render(
19
+ <MyAccountQuotesSelectedTags
20
+ filters={{}}
21
+ onClearAll={vi.fn()}
22
+ onRemoveFilter={vi.fn()}
23
+ />
24
+ )
25
+
26
+ expect(container.firstChild).toBeNull()
27
+ })
28
+
29
+ it('renders a range tag when both from and to dates are set', () => {
30
+ render(
31
+ <MyAccountQuotesSelectedTags
32
+ filters={{ createdAtFrom: '2026-01-01', createdAtTo: '2026-01-31' }}
33
+ onClearAll={vi.fn()}
34
+ onRemoveFilter={vi.fn()}
35
+ />
36
+ )
37
+
38
+ const expected = `Created: ${formatFilterDate('2026-01-01', 'en-US')} to ${formatFilterDate('2026-01-31', 'en-US')}`
39
+ expect(screen.getByText(expected)).toBeTruthy()
40
+ })
41
+
42
+ it('renders an open-ended "from" tag when only the start date is set', () => {
43
+ render(
44
+ <MyAccountQuotesSelectedTags
45
+ filters={{ expiresAtFrom: '2026-02-01' }}
46
+ onClearAll={vi.fn()}
47
+ onRemoveFilter={vi.fn()}
48
+ />
49
+ )
50
+
51
+ const expected = `Expires: from ${formatFilterDate('2026-02-01', 'en-US')}`
52
+ expect(screen.getByText(expected)).toBeTruthy()
53
+ })
54
+
55
+ it('renders an open-ended "to" tag when only the end date is set', () => {
56
+ render(
57
+ <MyAccountQuotesSelectedTags
58
+ filters={{ expiresAtTo: '2026-02-28' }}
59
+ onClearAll={vi.fn()}
60
+ onRemoveFilter={vi.fn()}
61
+ />
62
+ )
63
+
64
+ const expected = `Expires: to ${formatFilterDate('2026-02-28', 'en-US')}`
65
+ expect(screen.getByText(expected)).toBeTruthy()
66
+ })
67
+
68
+ it('renders one tag per selected status using its mapped label', () => {
69
+ render(
70
+ <MyAccountQuotesSelectedTags
71
+ filters={{ status: ['Draft', 'Approved'] }}
72
+ onClearAll={vi.fn()}
73
+ onRemoveFilter={vi.fn()}
74
+ />
75
+ )
76
+
77
+ expect(screen.getByText('Draft')).toBeTruthy()
78
+ expect(screen.getByText('Approved')).toBeTruthy()
79
+ })
80
+
81
+ it('falls back to the raw value for an unmapped status', () => {
82
+ render(
83
+ <MyAccountQuotesSelectedTags
84
+ filters={{ status: ['NotAStatus'] }}
85
+ onClearAll={vi.fn()}
86
+ onRemoveFilter={vi.fn()}
87
+ />
88
+ )
89
+
90
+ expect(screen.getByText('NotAStatus')).toBeTruthy()
91
+ })
92
+
93
+ it('calls onRemoveFilter with the right key/value when a tag is cleared', () => {
94
+ const onRemoveFilter = vi.fn()
95
+ render(
96
+ <MyAccountQuotesSelectedTags
97
+ filters={{ status: ['Draft'] }}
98
+ onClearAll={vi.fn()}
99
+ onRemoveFilter={onRemoveFilter}
100
+ />
101
+ )
102
+
103
+ fireEvent.click(screen.getByText('×'))
104
+
105
+ expect(onRemoveFilter).toHaveBeenCalledWith('status', 'Draft')
106
+ })
107
+
108
+ it('calls onClearAll when the Clear All button is clicked', () => {
109
+ const onClearAll = vi.fn()
110
+ render(
111
+ <MyAccountQuotesSelectedTags
112
+ filters={{ status: ['Draft'] }}
113
+ onClearAll={onClearAll}
114
+ onRemoveFilter={vi.fn()}
115
+ />
116
+ )
117
+
118
+ fireEvent.click(screen.getByText('Clear All'))
119
+
120
+ expect(onClearAll).toHaveBeenCalledTimes(1)
121
+ })
122
+ })
@@ -0,0 +1,85 @@
1
+ /**
2
+ * @vitest-environment jsdom
3
+ */
4
+
5
+ import { fireEvent, render, screen } from '@testing-library/react'
6
+ import { describe, expect, it, vi } from 'vitest'
7
+
8
+ import MyAccountQuotesStatusSelector from '../../../src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesStatusSelector/MyAccountQuotesStatusSelector'
9
+
10
+ describe('MyAccountQuotesStatusSelector', () => {
11
+ it('renders a chip with the mapped label for each selected value', () => {
12
+ render(
13
+ <MyAccountQuotesStatusSelector value={['Draft']} onChange={vi.fn()} />
14
+ )
15
+
16
+ expect(screen.getByText('Draft')).toBeTruthy()
17
+ })
18
+
19
+ it('opens the dropdown when the trigger is clicked', () => {
20
+ const { container } = render(
21
+ <MyAccountQuotesStatusSelector value={[]} onChange={vi.fn()} />
22
+ )
23
+
24
+ expect(screen.queryByLabelText('Approved')).toBeNull()
25
+
26
+ fireEvent.click(
27
+ container.querySelector('[aria-controls="status-listbox"]')!
28
+ )
29
+
30
+ expect(screen.getByLabelText('Approved')).toBeTruthy()
31
+ })
32
+
33
+ it('calls onChange with the key added when a checkbox is checked', () => {
34
+ const onChange = vi.fn()
35
+ const { container } = render(
36
+ <MyAccountQuotesStatusSelector value={[]} onChange={onChange} />
37
+ )
38
+
39
+ fireEvent.click(
40
+ container.querySelector('[aria-controls="status-listbox"]')!
41
+ )
42
+ fireEvent.click(screen.getByLabelText('Draft'))
43
+
44
+ expect(onChange).toHaveBeenCalledWith(['Draft'])
45
+ })
46
+
47
+ it('calls onChange with the key removed when an already-selected checkbox is unchecked', () => {
48
+ const onChange = vi.fn()
49
+ const { container } = render(
50
+ <MyAccountQuotesStatusSelector value={['Draft']} onChange={onChange} />
51
+ )
52
+
53
+ fireEvent.click(
54
+ container.querySelector('[aria-controls="status-listbox"]')!
55
+ )
56
+ fireEvent.click(screen.getByLabelText('Draft'))
57
+
58
+ expect(onChange).toHaveBeenCalledWith([])
59
+ })
60
+
61
+ it('calls onChange with the key removed when a chip remove button is clicked', () => {
62
+ const onChange = vi.fn()
63
+ render(
64
+ <MyAccountQuotesStatusSelector
65
+ value={['Draft', 'Approved']}
66
+ onChange={onChange}
67
+ />
68
+ )
69
+
70
+ fireEvent.click(screen.getByLabelText('Remove Draft'))
71
+
72
+ expect(onChange).toHaveBeenCalledWith(['Approved'])
73
+ })
74
+
75
+ it('falls back to the raw key for a chip with no matching status entry', () => {
76
+ render(
77
+ <MyAccountQuotesStatusSelector
78
+ value={['NotAStatus']}
79
+ onChange={vi.fn()}
80
+ />
81
+ )
82
+
83
+ expect(screen.getByText('NotAStatus')).toBeTruthy()
84
+ })
85
+ })