@faststore/core 4.4.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 +63 -50
  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 +10 -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,264 @@
1
+ import type { Locator } from '@vtex/client-cms'
2
+ import type { GetServerSideProps } from 'next'
3
+ import { NextSeo } from 'next-seo'
4
+ import type { ComponentType } from 'react'
5
+ import { Layout } from 'src/components/account'
6
+ import RenderSections from 'src/components/cms/RenderSections'
7
+ import GLOBAL_COMPONENTS from 'src/components/cms/global/Components'
8
+ import CUSTOM_COMPONENTS from 'src/customizations/src/components'
9
+
10
+ import { getGlobalSectionsData } from 'src/components/cms/GlobalSections'
11
+
12
+ import { gql } from '@generated/gql'
13
+ import type {
14
+ ServerAccountPageQueryQuery,
15
+ ServerAccountPageQueryQueryVariables,
16
+ ServerListQuotesQueryQuery,
17
+ ServerListQuotesQueryQueryVariables,
18
+ ServerQuotesOrganizationMemberQueryQuery,
19
+ ServerQuotesOrganizationMemberQueryQueryVariables,
20
+ } from '@generated/graphql'
21
+ import { ServerAccountPageQueryDocument } from '@generated/graphql'
22
+ import AfterSection from 'src/customizations/src/myAccount/extensions/quotes/after'
23
+ import BeforeSection from 'src/customizations/src/myAccount/extensions/quotes/before'
24
+ import type { MyAccountProps } from 'src/experimental/myAccountServerSideProps'
25
+ import { execute } from 'src/server'
26
+ import { injectGlobalSections } from 'src/server/cms/global'
27
+ import { withLocaleValidationSSR } from 'src/utils/localization/withLocaleValidation'
28
+ import { getMyAccountRedirect } from 'src/utils/myAccountRedirect'
29
+
30
+ import storeConfig from 'discovery.config'
31
+ import { MyAccountListQuotes } from 'src/components/account/quotes/MyAccountListQuotes'
32
+ import { getIsRepresentative } from 'src/sdk/account/getIsRepresentative'
33
+ import PageProvider from 'src/sdk/overrides/PageProvider'
34
+ import { extractStatusFromError } from 'src/utils/utilities'
35
+
36
+ const COMPONENTS: Record<string, ComponentType<any>> = {
37
+ ...GLOBAL_COMPONENTS,
38
+ ...CUSTOM_COMPONENTS,
39
+ }
40
+
41
+ type ListQuotesPageProps = {
42
+ listQuotes: ServerListQuotesQueryQuery['listUserQuotes']
43
+ total: number
44
+ perPage: number
45
+ filters: {
46
+ page: number
47
+ status: string[]
48
+ createdAtFrom: string
49
+ createdAtTo: string
50
+ expiresAtFrom: string
51
+ expiresAtTo: string
52
+ label: string
53
+ }
54
+ } & MyAccountProps
55
+
56
+ export default function ListQuotesPage({
57
+ globalSections: globalSectionsProp,
58
+ accountName,
59
+ listQuotes,
60
+ total,
61
+ perPage,
62
+ filters,
63
+ isRepresentative,
64
+ }: ListQuotesPageProps) {
65
+ const { sections: globalSections, settings: globalSettings } =
66
+ globalSectionsProp ?? {}
67
+
68
+ return (
69
+ <PageProvider context={{ globalSettings }}>
70
+ <RenderSections globalSections={globalSections} components={COMPONENTS}>
71
+ <NextSeo noindex nofollow />
72
+
73
+ <Layout isRepresentative={isRepresentative} accountName={accountName}>
74
+ <BeforeSection />
75
+ <MyAccountListQuotes
76
+ listQuotes={listQuotes}
77
+ filters={filters}
78
+ perPage={perPage}
79
+ total={total}
80
+ />
81
+ <AfterSection />
82
+ </Layout>
83
+ </RenderSections>
84
+ </PageProvider>
85
+ )
86
+ }
87
+
88
+ const query = gql(`
89
+ query ServerListQuotesQuery ($page: Int, $perPage: Int, $status: [String], $createdAtFrom: String, $createdAtTo: String, $expiresAtFrom: String, $expiresAtTo: String, $label: String) {
90
+ listUserQuotes (page: $page, perPage: $perPage, status: $status, createdAtFrom: $createdAtFrom, createdAtTo: $createdAtTo, expiresAtFrom: $expiresAtFrom, expiresAtTo: $expiresAtTo, label: $label) {
91
+ list {
92
+ id
93
+ status
94
+ label
95
+ createdAt
96
+ expiresAt
97
+ amount
98
+ createdBy
99
+ }
100
+ paging {
101
+ total
102
+ currentPage
103
+ perPage
104
+ }
105
+ }
106
+ }
107
+ `)
108
+
109
+ const organizationMemberQuery = gql(`
110
+ query ServerQuotesOrganizationMemberQuery {
111
+ isOrganizationMember
112
+ }
113
+ `)
114
+
115
+ const getServerSidePropsBase: GetServerSideProps<
116
+ MyAccountProps,
117
+ Record<string, string>,
118
+ Locator
119
+ > = async (context) => {
120
+ const isRepresentative = getIsRepresentative({
121
+ headers: context.req.headers as Record<string, string>,
122
+ account: storeConfig.api.storeId,
123
+ })
124
+
125
+ const contentContext = {
126
+ previewData: context.previewData,
127
+ locale: context.locale,
128
+ }
129
+
130
+ const { isFaststoreMyAccountEnabled, redirect } = getMyAccountRedirect({
131
+ query: context.query,
132
+ })
133
+
134
+ if (!isFaststoreMyAccountEnabled) {
135
+ return { redirect }
136
+ }
137
+
138
+ const [
139
+ globalSectionsPromise,
140
+ globalSectionsHeaderPromise,
141
+ globalSectionsFooterPromise,
142
+ ] = getGlobalSectionsData(contentContext)
143
+
144
+ const page = Number(context.query.page as string | undefined) || 1
145
+ const perPage = 25
146
+ const status = (
147
+ Array.isArray(context.query.status)
148
+ ? context.query.status
149
+ : [context.query.status]
150
+ ).filter(Boolean) as string[]
151
+ const createdAtFrom =
152
+ (context.query.createdAtFrom as string | undefined) || ''
153
+ const createdAtTo = (context.query.createdAtTo as string | undefined) || ''
154
+ const expiresAtFrom =
155
+ (context.query.expiresAtFrom as string | undefined) || ''
156
+ const expiresAtTo = (context.query.expiresAtTo as string | undefined) || ''
157
+ const label = (context.query.label as string | undefined) || ''
158
+
159
+ const [
160
+ listQuotesResult,
161
+ accountProfileResult,
162
+ organizationMemberResult,
163
+ globalSections,
164
+ globalSectionsHeader,
165
+ globalSectionsFooter,
166
+ ] = await Promise.all([
167
+ execute<ServerListQuotesQueryQueryVariables, ServerListQuotesQueryQuery>(
168
+ {
169
+ variables: {
170
+ page,
171
+ perPage,
172
+ status,
173
+ createdAtFrom: createdAtFrom || undefined,
174
+ createdAtTo: createdAtTo || undefined,
175
+ expiresAtFrom: expiresAtFrom || undefined,
176
+ expiresAtTo: expiresAtTo || undefined,
177
+ label: label || undefined,
178
+ },
179
+ operation: query,
180
+ },
181
+ { headers: { ...context.req.headers } }
182
+ ),
183
+ execute<ServerAccountPageQueryQueryVariables, ServerAccountPageQueryQuery>(
184
+ { variables: {}, operation: ServerAccountPageQueryDocument },
185
+ { headers: { ...context.req.headers } }
186
+ ).catch(() => null),
187
+ execute<
188
+ ServerQuotesOrganizationMemberQueryQueryVariables,
189
+ ServerQuotesOrganizationMemberQueryQuery
190
+ >(
191
+ { variables: {}, operation: organizationMemberQuery },
192
+ { headers: { ...context.req.headers } }
193
+ ).catch(() => null),
194
+ globalSectionsPromise,
195
+ globalSectionsHeaderPromise,
196
+ globalSectionsFooterPromise,
197
+ ])
198
+
199
+ if (!organizationMemberResult?.data?.isOrganizationMember) {
200
+ return {
201
+ redirect: {
202
+ destination: `/pvt/account/403?from=${encodeURIComponent('/pvt/account/quotes')}`,
203
+ permanent: false,
204
+ },
205
+ }
206
+ }
207
+
208
+ if (listQuotesResult.errors) {
209
+ console.error(...listQuotesResult.errors)
210
+
211
+ const errorStatus = extractStatusFromError(listQuotesResult.errors[0])
212
+
213
+ const destination =
214
+ errorStatus === 403 || errorStatus === 401
215
+ ? `/pvt/account/403?from=${encodeURIComponent('/pvt/account/quotes')}`
216
+ : '/pvt/account/404'
217
+
218
+ return {
219
+ redirect: {
220
+ destination,
221
+ permanent: false,
222
+ },
223
+ }
224
+ }
225
+
226
+ if (!listQuotesResult.data.listUserQuotes) {
227
+ return {
228
+ redirect: {
229
+ destination: '/pvt/account/404',
230
+ permanent: false,
231
+ },
232
+ }
233
+ }
234
+
235
+ const globalSectionsResult = injectGlobalSections({
236
+ globalSections,
237
+ globalSectionsHeader,
238
+ globalSectionsFooter,
239
+ })
240
+
241
+ return {
242
+ props: {
243
+ globalSections: globalSectionsResult,
244
+ accountName: accountProfileResult?.data?.accountProfile?.name ?? '',
245
+ listQuotes: listQuotesResult.data.listUserQuotes,
246
+ total: listQuotesResult.data.listUserQuotes.paging.total,
247
+ perPage: listQuotesResult.data.listUserQuotes.paging.perPage,
248
+ filters: {
249
+ page: listQuotesResult.data.listUserQuotes.paging.currentPage ?? page,
250
+ status,
251
+ createdAtFrom,
252
+ createdAtTo,
253
+ expiresAtFrom,
254
+ expiresAtTo,
255
+ label,
256
+ },
257
+ isRepresentative,
258
+ },
259
+ }
260
+ }
261
+
262
+ export const getServerSideProps = withLocaleValidationSSR(
263
+ getServerSidePropsBase
264
+ )
@@ -27,6 +27,9 @@ export const PROFILE_ROUTE = '/pvt/account/profile'
27
27
  export const ORDERS_ROUTE = '/pvt/account/orders'
28
28
  export const USER_DETAILS_ROUTE = '/pvt/account/user-details'
29
29
  export const SECURITY_ROUTE = '/pvt/account/security'
30
+ export const QUOTES_ROUTE = '/pvt/account/quotes'
31
+
32
+ export const ROUTES_ONLY_FOR_B2B_MEMBERS = [QUOTES_ROUTE]
30
33
 
31
34
  const ROUTE_LABEL_KEYS: Record<string, keyof AccountNavigationLabels> = {
32
35
  [PROFILE_ROUTE]: 'profileLabel',
@@ -45,6 +48,10 @@ const DEFAULT_ROUTES: Route[] = [
45
48
  title: 'Orders',
46
49
  route: ORDERS_ROUTE,
47
50
  },
51
+ {
52
+ title: 'Quotes',
53
+ route: QUOTES_ROUTE,
54
+ },
48
55
  {
49
56
  title: 'User Details',
50
57
  route: USER_DETAILS_ROUTE,
@@ -0,0 +1,27 @@
1
+ export type QuoteStatusKey =
2
+ | 'Draft'
3
+ | 'Requested'
4
+ | 'InReview'
5
+ | 'Reviewed'
6
+ | 'Approved'
7
+ | 'Declined'
8
+ | 'Expired'
9
+ | 'ConvertedToCart'
10
+ | 'ConvertedToOrder'
11
+
12
+ export type QuoteStatusMapValue = {
13
+ variant: 'neutral' | 'warning' | 'info' | 'success' | 'danger'
14
+ label: string
15
+ }
16
+
17
+ export const quoteStatusMap: Record<QuoteStatusKey, QuoteStatusMapValue> = {
18
+ Draft: { variant: 'neutral', label: 'Draft' },
19
+ Requested: { variant: 'warning', label: 'Requested' },
20
+ InReview: { variant: 'warning', label: 'In Review' },
21
+ Reviewed: { variant: 'info', label: 'Revised' },
22
+ Approved: { variant: 'success', label: 'Approved' },
23
+ Declined: { variant: 'danger', label: 'Declined' },
24
+ Expired: { variant: 'neutral', label: 'Expired' },
25
+ ConvertedToCart: { variant: 'success', label: 'Converted' },
26
+ ConvertedToOrder: { variant: 'success', label: 'Converted to Order' },
27
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * @vitest-environment jsdom
3
+ */
4
+
5
+ import { 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
+ vi.mock('../../../src/components/account/Menu', () => ({
12
+ __esModule: true,
13
+ default: ({ items }: { items: Array<{ title: string; route: string }> }) => (
14
+ <ul>
15
+ {items.map((item) => (
16
+ <li key={item.route}>{item.title}</li>
17
+ ))}
18
+ </ul>
19
+ ),
20
+ }))
21
+
22
+ import Layout from '../../../src/components/account/Layout/Layout'
23
+
24
+ describe('Layout', () => {
25
+ it('hides the Quotes route when the user is not an org member', () => {
26
+ mockUseSession.mockReturnValue({ b2b: null })
27
+
28
+ render(<Layout accountName="Jane Doe">content</Layout>)
29
+
30
+ expect(screen.queryByText('Quotes')).toBeNull()
31
+ expect(screen.getByText('Profile')).toBeTruthy()
32
+ })
33
+
34
+ it('shows the Quotes route when the user belongs to an org unit', () => {
35
+ mockUseSession.mockReturnValue({ b2b: { unitId: 'unit-1' } })
36
+
37
+ render(<Layout accountName="Jane Doe">content</Layout>)
38
+
39
+ expect(screen.getByText('Quotes')).toBeTruthy()
40
+ })
41
+
42
+ it('hides User Details when not a representative, regardless of org membership', () => {
43
+ mockUseSession.mockReturnValue({ b2b: { unitId: 'unit-1' } })
44
+
45
+ render(
46
+ <Layout accountName="Jane Doe" isRepresentative={false}>
47
+ content
48
+ </Layout>
49
+ )
50
+
51
+ expect(screen.queryByText('User Details')).toBeNull()
52
+ expect(screen.getByText('Quotes')).toBeTruthy()
53
+ })
54
+
55
+ it('renders children inside the layout content area', () => {
56
+ mockUseSession.mockReturnValue({ b2b: null })
57
+
58
+ render(<Layout accountName="Jane Doe">child content</Layout>)
59
+
60
+ expect(screen.getByText('child content')).toBeTruthy()
61
+ })
62
+ })
@@ -0,0 +1,52 @@
1
+ /**
2
+ * @vitest-environment jsdom
3
+ */
4
+
5
+ import { render, screen } from '@testing-library/react'
6
+ import { describe, expect, it, vi } from 'vitest'
7
+
8
+ const mockUseAccountNavigationLabels = vi.hoisted(() => vi.fn())
9
+ vi.mock('src/sdk/account/accountPageContext', () => ({
10
+ useAccountNavigationLabels: mockUseAccountNavigationLabels,
11
+ }))
12
+
13
+ vi.mock('src/components/ui/Link', () => ({
14
+ __esModule: true,
15
+ default: ({
16
+ href,
17
+ children,
18
+ }: {
19
+ href: string
20
+ children: React.ReactNode
21
+ }) => <a href={href}>{children}</a>,
22
+ }))
23
+
24
+ import { OrganizationDrawerBody } from '../../../src/components/account/Drawer/OrganizationDrawer/OrganizationDrawerBody'
25
+
26
+ describe('OrganizationDrawerBody', () => {
27
+ it('hides the Quotes route when the user is not an org member', () => {
28
+ mockUseAccountNavigationLabels.mockReturnValue(undefined)
29
+
30
+ render(<OrganizationDrawerBody isRepresentative isOrgMember={false} />)
31
+
32
+ expect(screen.queryByText('Quotes')).toBeNull()
33
+ expect(screen.getByText('Profile')).toBeTruthy()
34
+ })
35
+
36
+ it('shows the Quotes route when the user belongs to an org unit', () => {
37
+ mockUseAccountNavigationLabels.mockReturnValue(undefined)
38
+
39
+ render(<OrganizationDrawerBody isRepresentative isOrgMember />)
40
+
41
+ expect(screen.getByText('Quotes')).toBeTruthy()
42
+ })
43
+
44
+ it('hides User Details when not a representative, regardless of org membership', () => {
45
+ mockUseAccountNavigationLabels.mockReturnValue(undefined)
46
+
47
+ render(<OrganizationDrawerBody isRepresentative={false} isOrgMember />)
48
+
49
+ expect(screen.queryByText('User Details')).toBeNull()
50
+ expect(screen.getByText('Quotes')).toBeTruthy()
51
+ })
52
+ })
@@ -0,0 +1,232 @@
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
+ const mockUseScreenResize = vi.hoisted(() => vi.fn())
10
+ vi.mock('src/sdk/ui/useScreenResize', () => ({
11
+ default: mockUseScreenResize,
12
+ }))
13
+
14
+ vi.mock(
15
+ '../../../src/components/account/quotes/MyAccountListQuotes/MyAccountListQuotesTable/MyAccountListQuotesTable',
16
+ () => ({
17
+ __esModule: true,
18
+ default: () => <div data-testid="quotes-table" />,
19
+ Pagination: ({ page }: { page: number }) => (
20
+ <div data-testid="pagination">page {page}</div>
21
+ ),
22
+ })
23
+ )
24
+
25
+ vi.mock(
26
+ '../../../src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesFilterSlider/MyAccountQuotesFilterSlider',
27
+ () => ({
28
+ __esModule: true,
29
+ default: () => <div data-testid="filter-slider" />,
30
+ })
31
+ )
32
+
33
+ const mockUseUI = vi.hoisted(() => vi.fn())
34
+ vi.mock('@faststore/ui', async () => {
35
+ const actual =
36
+ await vi.importActual<typeof import('@faststore/ui')>('@faststore/ui')
37
+ return {
38
+ ...actual,
39
+ useUI: mockUseUI,
40
+ SearchInputField: React.forwardRef(function MockSearchInputField(
41
+ { onBlur, onKeyDown }: any,
42
+ ref: any
43
+ ) {
44
+ const inputRef = React.useRef<HTMLInputElement>(null)
45
+ React.useImperativeHandle(ref, () => ({ inputRef: inputRef.current }))
46
+ return (
47
+ <input
48
+ ref={inputRef}
49
+ placeholder="Search"
50
+ onBlur={onBlur}
51
+ onKeyDown={onKeyDown}
52
+ />
53
+ )
54
+ }),
55
+ }
56
+ })
57
+
58
+ import MyAccountListQuotes from '../../../src/components/account/quotes/MyAccountListQuotes/MyAccountListQuotes'
59
+
60
+ const originalLocation = window.location
61
+
62
+ function stubLocation(search = '') {
63
+ Object.defineProperty(window, 'location', {
64
+ configurable: true,
65
+ value: { ...originalLocation, search, href: '' },
66
+ })
67
+ }
68
+
69
+ function baseFilters(overrides: Record<string, unknown> = {}) {
70
+ return {
71
+ page: 1,
72
+ status: [],
73
+ createdAtFrom: '',
74
+ createdAtTo: '',
75
+ expiresAtFrom: '',
76
+ expiresAtTo: '',
77
+ label: '',
78
+ ...overrides,
79
+ }
80
+ }
81
+
82
+ describe('MyAccountListQuotes', () => {
83
+ const openFilter = vi.fn()
84
+
85
+ beforeEach(() => {
86
+ stubLocation()
87
+ mockUseScreenResize.mockReturnValue({ isDesktop: true })
88
+ mockUseUI.mockReturnValue({ openFilter, filter: false })
89
+ })
90
+
91
+ afterEach(() => {
92
+ vi.clearAllMocks()
93
+ Object.defineProperty(window, 'location', {
94
+ configurable: true,
95
+ value: originalLocation,
96
+ })
97
+ })
98
+
99
+ it('renders the quotes table when there are results', () => {
100
+ render(
101
+ <MyAccountListQuotes
102
+ listQuotes={{ list: [{ id: 'q-1' }] } as any}
103
+ total={1}
104
+ perPage={25}
105
+ filters={baseFilters()}
106
+ />
107
+ )
108
+
109
+ expect(screen.getByTestId('quotes-table')).toBeTruthy()
110
+ })
111
+
112
+ it('renders the "no quotes" empty state when there are no results and no filters', () => {
113
+ render(
114
+ <MyAccountListQuotes
115
+ listQuotes={{ list: [] } as any}
116
+ total={0}
117
+ perPage={25}
118
+ filters={baseFilters()}
119
+ />
120
+ )
121
+
122
+ expect(screen.getByText("You don't have any quotes")).toBeTruthy()
123
+ expect(screen.queryByTestId('quotes-table')).toBeNull()
124
+ })
125
+
126
+ it('renders the "no results" empty state when filters are active but nothing matches', () => {
127
+ render(
128
+ <MyAccountListQuotes
129
+ listQuotes={{ list: [] } as any}
130
+ total={0}
131
+ perPage={25}
132
+ filters={baseFilters({ status: ['Draft'] })}
133
+ />
134
+ )
135
+
136
+ expect(screen.getByText('No results found')).toBeTruthy()
137
+ })
138
+
139
+ it('opens the filter slider and dispatches the current selected facets when Filters is clicked', () => {
140
+ render(
141
+ <MyAccountListQuotes
142
+ listQuotes={{ list: [] } as any}
143
+ total={0}
144
+ perPage={25}
145
+ filters={baseFilters({ status: ['Draft'] })}
146
+ />
147
+ )
148
+
149
+ fireEvent.click(screen.getByText('Filters'))
150
+
151
+ expect(openFilter).toHaveBeenCalledTimes(1)
152
+ })
153
+
154
+ it('shows the active filter count badge', () => {
155
+ render(
156
+ <MyAccountListQuotes
157
+ listQuotes={{ list: [] } as any}
158
+ total={0}
159
+ perPage={25}
160
+ filters={baseFilters({ status: ['Draft'] })}
161
+ />
162
+ )
163
+
164
+ expect(screen.getByText('1')).toBeTruthy()
165
+ })
166
+
167
+ it('renders the filter slider when the UI filter drawer is open', () => {
168
+ mockUseUI.mockReturnValue({ openFilter, filter: true })
169
+
170
+ render(
171
+ <MyAccountListQuotes
172
+ listQuotes={{ list: [] } as any}
173
+ total={0}
174
+ perPage={25}
175
+ filters={baseFilters()}
176
+ />
177
+ )
178
+
179
+ expect(screen.getByTestId('filter-slider')).toBeTruthy()
180
+ })
181
+
182
+ it('shows pagination on desktop when there are results', () => {
183
+ mockUseScreenResize.mockReturnValue({ isDesktop: true })
184
+
185
+ render(
186
+ <MyAccountListQuotes
187
+ listQuotes={{ list: [{ id: 'q-1' }] } as any}
188
+ total={30}
189
+ perPage={25}
190
+ filters={baseFilters({ page: 2 })}
191
+ />
192
+ )
193
+
194
+ expect(screen.getAllByTestId('pagination').length).toBeGreaterThan(0)
195
+ })
196
+
197
+ it('navigates with the search term on blur', async () => {
198
+ render(
199
+ <MyAccountListQuotes
200
+ listQuotes={{ list: [] } as any}
201
+ total={0}
202
+ perPage={25}
203
+ filters={baseFilters()}
204
+ />
205
+ )
206
+
207
+ const input = screen.getByPlaceholderText('Search')
208
+ fireEvent.change(input, { target: { value: 'ACME' } })
209
+ fireEvent.blur(input)
210
+ await new Promise((resolve) => setTimeout(resolve, 350))
211
+
212
+ expect(window.location.href).toBe('/pvt/account/quotes?label=ACME')
213
+ })
214
+
215
+ it('navigates without a label param when the search is cleared', async () => {
216
+ render(
217
+ <MyAccountListQuotes
218
+ listQuotes={{ list: [] } as any}
219
+ total={0}
220
+ perPage={25}
221
+ filters={baseFilters({ label: 'ACME' })}
222
+ />
223
+ )
224
+
225
+ const input = screen.getByPlaceholderText('Search')
226
+ fireEvent.change(input, { target: { value: '' } })
227
+ fireEvent.blur(input)
228
+ await new Promise((resolve) => setTimeout(resolve, 350))
229
+
230
+ expect(window.location.href).toBe('/pvt/account/quotes?')
231
+ })
232
+ })