@faststore/core 4.9.0-dev.3 → 4.9.0-dev.5

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 +3 -3
  2. package/.turbo/turbo-test.log +132 -138
  3. package/CHANGELOG.md +12 -0
  4. package/cms/faststore/my-account/components/cms_component__accountlistquotes.jsonc +240 -0
  5. package/cms/faststore/my-account/components/cms_component__accountnavigation.jsonc +76 -0
  6. package/cms/faststore/my-account/pages/cms_content_type__myaccountquotes.jsonc +18 -0
  7. package/package.json +6 -6
  8. package/src/components/account/Drawer/OrganizationDrawer/ContractSwitcher/ContractSwitcher.tsx +129 -26
  9. package/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawerHeader.tsx +7 -1
  10. package/src/components/account/quotes/MyAccountListQuotes/MyAccountListQuotes.tsx +42 -11
  11. package/src/components/account/quotes/MyAccountListQuotes/MyAccountListQuotesTable/MyAccountListQuotesTable.tsx +45 -15
  12. package/src/components/account/quotes/MyAccountListQuotes/MyAccountListQuotesTable/MyAccountQuoteStatusBadge.tsx +11 -3
  13. package/src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesFilterSlider/MyAccountQuotesFilterSlider.tsx +23 -0
  14. package/src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesSelectedTags/MyAccountQuotesSelectedTags.tsx +35 -11
  15. package/src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesStatusSelector/MyAccountQuotesStatusSelector.tsx +27 -10
  16. package/src/components/account/quotes/MyAccountListQuotes/index.ts +2 -0
  17. package/src/components/account/quotes/MyAccountListQuotes/quoteFilters.ts +16 -6
  18. package/src/components/account/quotes/MyAccountListQuotes/quotesLabels.ts +163 -0
  19. package/src/components/cms/account/Components.ts +2 -0
  20. package/src/components/sections/Account/AccountListQuotes.tsx +141 -0
  21. package/src/pages/pvt/account/quotes/index.tsx +59 -40
  22. package/src/pages/{s.tsx → s/index.tsx} +11 -0
  23. package/src/sdk/account/accountPageContext.ts +17 -0
  24. package/src/sdk/account/getMyAccountRoutes.ts +17 -0
  25. package/src/sdk/account/resolveNonBlankLabels.ts +36 -0
  26. package/src/server/cms/myAccountDefaultSections.ts +2 -0
  27. package/src/utils/quoteStatus.ts +81 -0
  28. package/test/components/account/ContractSwitcher.browser.test.tsx +163 -0
  29. package/test/components/account/OrganizationDrawer.browser.test.tsx +4 -0
  30. package/test/components/account/OrganizationDrawerHeader.browser.test.tsx +44 -1
  31. package/test/components/quotes/MyAccountListQuotes.test.tsx +36 -2
  32. package/test/components/quotes/MyAccountListQuotesTable.test.tsx +68 -0
  33. package/test/components/quotes/MyAccountQuoteStatusBadge.test.tsx +25 -0
  34. package/test/components/quotes/MyAccountQuotesFilterSlider.test.tsx +52 -11
  35. package/test/components/quotes/MyAccountQuotesSelectedTags.test.tsx +32 -0
  36. package/test/components/quotes/MyAccountQuotesStatusSelector.test.tsx +100 -0
  37. package/test/components/quotes/quoteFilters.test.ts +34 -0
  38. package/test/components/sections/Account/AccountListQuotes.browser.test.tsx +122 -0
  39. package/test/server/cms/myAccountDefaultSections.test.ts +21 -0
@@ -25,3 +25,84 @@ export const quoteStatusMap: Record<QuoteStatusKey, QuoteStatusMapValue> = {
25
25
  ConvertedToCart: { variant: 'success', label: 'Converted' },
26
26
  ConvertedToOrder: { variant: 'success', label: 'Converted to Order' },
27
27
  }
28
+
29
+ /** CMS-configurable overrides for each quote status label, keyed like the other My Account CMS label sets. */
30
+ export type QuoteStatusCmsLabels = Partial<{
31
+ draftStatus: string
32
+ requestedStatus: string
33
+ inReviewStatus: string
34
+ revisedStatus: string
35
+ approvedStatus: string
36
+ declinedStatus: string
37
+ expiredStatus: string
38
+ convertedStatus: string
39
+ convertedToOrderStatus: string
40
+ }>
41
+
42
+ const QUOTE_STATUS_CMS_LABEL_KEYS = [
43
+ 'draftStatus',
44
+ 'requestedStatus',
45
+ 'inReviewStatus',
46
+ 'revisedStatus',
47
+ 'approvedStatus',
48
+ 'declinedStatus',
49
+ 'expiredStatus',
50
+ 'convertedStatus',
51
+ 'convertedToOrderStatus',
52
+ ] as const satisfies readonly (keyof QuoteStatusCmsLabels)[]
53
+
54
+ const CMS_LABEL_TO_STATUS_KEY: Record<
55
+ keyof Required<QuoteStatusCmsLabels>,
56
+ QuoteStatusKey
57
+ > = {
58
+ draftStatus: 'Draft',
59
+ requestedStatus: 'Requested',
60
+ inReviewStatus: 'InReview',
61
+ revisedStatus: 'Reviewed',
62
+ approvedStatus: 'Approved',
63
+ declinedStatus: 'Declined',
64
+ expiredStatus: 'Expired',
65
+ convertedStatus: 'ConvertedToCart',
66
+ convertedToOrderStatus: 'ConvertedToOrder',
67
+ }
68
+
69
+ export function pickQuoteStatusCmsLabels(
70
+ data?: Record<string, unknown>
71
+ ): QuoteStatusCmsLabels | undefined {
72
+ if (!data) {
73
+ return undefined
74
+ }
75
+
76
+ const labels: QuoteStatusCmsLabels = {}
77
+ let hasAny = false
78
+
79
+ for (const key of QUOTE_STATUS_CMS_LABEL_KEYS) {
80
+ const value = data[key]
81
+ if (typeof value === 'string' && value.trim().length > 0) {
82
+ labels[key] = value
83
+ hasAny = true
84
+ }
85
+ }
86
+
87
+ return hasAny ? labels : undefined
88
+ }
89
+
90
+ export function getLocalizedQuoteStatusMap(
91
+ cmsLabels?: QuoteStatusCmsLabels
92
+ ): Record<QuoteStatusKey, QuoteStatusMapValue> {
93
+ if (!cmsLabels) {
94
+ return quoteStatusMap
95
+ }
96
+
97
+ const localizedMap = { ...quoteStatusMap }
98
+
99
+ for (const key of QUOTE_STATUS_CMS_LABEL_KEYS) {
100
+ const label = cmsLabels[key]
101
+ if (typeof label === 'string' && label.trim().length > 0) {
102
+ const statusKey = CMS_LABEL_TO_STATUS_KEY[key]
103
+ localizedMap[statusKey] = { ...localizedMap[statusKey], label }
104
+ }
105
+ }
106
+
107
+ return localizedMap
108
+ }
@@ -9,6 +9,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
9
9
  const mockUseAvailableContracts = vi.hoisted(() => vi.fn())
10
10
  const mockSwitchContract = vi.hoisted(() => vi.fn())
11
11
  const mockUseSwitchContract = vi.hoisted(() => vi.fn())
12
+ const mockUseAccountNavigationLabels = vi.hoisted(() => vi.fn())
12
13
 
13
14
  vi.mock('src/sdk/account/useAvailableContracts', () => ({
14
15
  useAvailableContracts: mockUseAvailableContracts,
@@ -18,6 +19,10 @@ vi.mock('src/sdk/account/useSwitchContract', () => ({
18
19
  useSwitchContract: mockUseSwitchContract,
19
20
  }))
20
21
 
22
+ vi.mock('src/sdk/account/accountPageContext', () => ({
23
+ useAccountNavigationLabels: mockUseAccountNavigationLabels,
24
+ }))
25
+
21
26
  vi.mock('src/sdk/session', () => ({
22
27
  useSession: () => ({
23
28
  b2b: { customerId: 'a', unitId: 'unit-1' },
@@ -46,6 +51,7 @@ describe('ContractSwitcher', () => {
46
51
  loading: false,
47
52
  error: null,
48
53
  })
54
+ mockUseAccountNavigationLabels.mockReturnValue(undefined)
49
55
  })
50
56
 
51
57
  afterEach(() => {
@@ -275,4 +281,161 @@ describe('ContractSwitcher', () => {
275
281
 
276
282
  await waitFor(() => expect(mockSwitchContract).toHaveBeenCalledWith('b'))
277
283
  })
284
+
285
+ it('honors CMS-provided labels for the header, current session, search, footer, and available count', () => {
286
+ mockUseAccountNavigationLabels.mockReturnValue({
287
+ contractSwitcherTitleLabel: 'Trocar contrato',
288
+ contractSwitcherBackLabel: 'Voltar ao menu da conta',
289
+ contractSwitcherCloseLabel: 'Fechar',
290
+ contractSwitcherCurrentSessionLabel: 'Sessão atual',
291
+ contractSwitcherSearchPlaceholder: 'Buscar',
292
+ contractSwitcherSearchAriaLabel: 'Buscar contratos',
293
+ contractSwitcherAvailableCountLabel:
294
+ 'Selecione um dos {count} contratos disponíveis:',
295
+ contractSwitcherCancelLabel: 'Cancelar',
296
+ contractSwitcherConfirmLabel: 'Confirmar',
297
+ })
298
+
299
+ const { container } = render(
300
+ <ContractSwitcher onBack={vi.fn()} onClose={vi.fn()} />
301
+ )
302
+
303
+ expect(screen.getByText('Trocar contrato')).toBeTruthy()
304
+ expect(screen.getByLabelText('Voltar ao menu da conta')).toBeTruthy()
305
+ expect(screen.getByLabelText('Fechar')).toBeTruthy()
306
+ expect(screen.getByText('Sessão atual')).toBeTruthy()
307
+ expect(screen.getByPlaceholderText('Buscar')).toBeTruthy()
308
+ expect(screen.getByLabelText('Buscar contratos')).toBeTruthy()
309
+ expect(
310
+ container.querySelector('[data-fs-contract-switcher-count]')?.textContent
311
+ ).toBe('Selecione um dos 2 contratos disponíveis:')
312
+ expect(screen.getByRole('button', { name: 'Cancelar' })).toBeTruthy()
313
+ expect(screen.getByRole('button', { name: 'Confirmar' })).toBeTruthy()
314
+ })
315
+
316
+ it('keeps text after a repeated {count} token', () => {
317
+ mockUseAccountNavigationLabels.mockReturnValue({
318
+ contractSwitcherAvailableCountLabel: '{count} of {count} contracts',
319
+ })
320
+
321
+ const { container } = render(
322
+ <ContractSwitcher onBack={vi.fn()} onClose={vi.fn()} />
323
+ )
324
+
325
+ expect(
326
+ container.querySelector('[data-fs-contract-switcher-count]')?.textContent
327
+ ).toBe('2 of {count} contracts')
328
+ })
329
+
330
+ it('renders a count template without {count} unchanged', () => {
331
+ mockUseAccountNavigationLabels.mockReturnValue({
332
+ contractSwitcherAvailableCountLabel: 'Contratos disponíveis',
333
+ })
334
+
335
+ const { container } = render(
336
+ <ContractSwitcher onBack={vi.fn()} onClose={vi.fn()} />
337
+ )
338
+
339
+ expect(
340
+ container.querySelector('[data-fs-contract-switcher-count]')?.textContent
341
+ ).toBe('Contratos disponíveis')
342
+ })
343
+
344
+ it('honors CMS-provided labels for the empty, no-match, load-error, and switch-error states', () => {
345
+ mockUseAccountNavigationLabels.mockReturnValue({
346
+ contractSwitcherEmptyLabel: 'Nenhum outro contrato disponível.',
347
+ contractSwitcherNoMatchLabel: 'Nenhum contrato encontrado.',
348
+ contractSwitcherLoadErrorLabel: 'Não foi possível carregar contratos.',
349
+ contractSwitcherSwitchErrorLabel: 'Não foi possível trocar de contrato.',
350
+ contractSwitcherClearSearchLabel: 'Limpar busca',
351
+ })
352
+
353
+ mockUseAvailableContracts.mockReturnValue({
354
+ contracts: [{ id: 'a', corporateName: 'Stellar Global', isActive: true }],
355
+ loading: false,
356
+ error: null,
357
+ })
358
+
359
+ render(<ContractSwitcher onBack={vi.fn()} onClose={vi.fn()} />)
360
+
361
+ expect(screen.getByText('Nenhum outro contrato disponível.')).toBeTruthy()
362
+ })
363
+
364
+ it('honors the CMS-provided load error label', () => {
365
+ mockUseAccountNavigationLabels.mockReturnValue({
366
+ contractSwitcherLoadErrorLabel: 'Não foi possível carregar contratos.',
367
+ })
368
+ mockUseAvailableContracts.mockReturnValue({
369
+ contracts: [],
370
+ loading: false,
371
+ error: new Error('boom'),
372
+ })
373
+
374
+ render(<ContractSwitcher onBack={vi.fn()} onClose={vi.fn()} />)
375
+
376
+ expect(
377
+ screen.getByText('Não foi possível carregar contratos.')
378
+ ).toBeTruthy()
379
+ })
380
+
381
+ it('honors the CMS-provided no-match and clear-search labels', () => {
382
+ mockUseAccountNavigationLabels.mockReturnValue({
383
+ contractSwitcherNoMatchLabel: 'Nenhum contrato encontrado.',
384
+ contractSwitcherClearSearchLabel: 'Limpar busca',
385
+ })
386
+
387
+ render(<ContractSwitcher onBack={vi.fn()} onClose={vi.fn()} />)
388
+
389
+ fireEvent.change(screen.getByLabelText('Search contracts'), {
390
+ target: { value: 'zzzz-no-match' },
391
+ })
392
+ expect(screen.getByText('Nenhum contrato encontrado.')).toBeTruthy()
393
+
394
+ fireEvent.change(screen.getByLabelText('Search contracts'), {
395
+ target: { value: 'acme' },
396
+ })
397
+ expect(screen.getByLabelText('Limpar busca')).toBeTruthy()
398
+ })
399
+
400
+ it('honors the CMS-provided switch-error label', () => {
401
+ mockUseAccountNavigationLabels.mockReturnValue({
402
+ contractSwitcherSwitchErrorLabel: 'Não foi possível trocar de contrato.',
403
+ })
404
+ mockUseSwitchContract.mockReturnValue({
405
+ switchContract: mockSwitchContract,
406
+ loading: false,
407
+ error: new Error('switch failed'),
408
+ enabled: true,
409
+ })
410
+
411
+ render(<ContractSwitcher onBack={vi.fn()} onClose={vi.fn()} />)
412
+
413
+ expect(
414
+ screen.getByText('Não foi possível trocar de contrato.')
415
+ ).toBeTruthy()
416
+ })
417
+
418
+ it('falls back to English defaults when CMS labels are blank', () => {
419
+ mockUseAccountNavigationLabels.mockReturnValue({
420
+ contractSwitcherTitleLabel: ' ',
421
+ contractSwitcherBackLabel: '',
422
+ contractSwitcherCloseLabel: ' ',
423
+ contractSwitcherSearchAriaLabel: '',
424
+ contractSwitcherClearSearchLabel: ' ',
425
+ contractSwitcherConfirmLabel: '',
426
+ })
427
+
428
+ render(<ContractSwitcher onBack={vi.fn()} onClose={vi.fn()} />)
429
+
430
+ expect(screen.getByText('Change contract')).toBeTruthy()
431
+ expect(screen.getByLabelText('Back to account menu')).toBeTruthy()
432
+ expect(screen.getByLabelText('Close')).toBeTruthy()
433
+ expect(screen.getByLabelText('Search contracts')).toBeTruthy()
434
+ expect(screen.getByRole('button', { name: 'Confirm' })).toBeTruthy()
435
+
436
+ fireEvent.change(screen.getByLabelText('Search contracts'), {
437
+ target: { value: 'acme' },
438
+ })
439
+ expect(screen.getByLabelText('Clear search')).toBeTruthy()
440
+ })
278
441
  })
@@ -44,6 +44,10 @@ vi.mock('src/sdk/account/useAvailableContracts', () => ({
44
44
  useAvailableContracts: mockUseAvailableContracts,
45
45
  }))
46
46
 
47
+ vi.mock('src/sdk/account/accountPageContext', () => ({
48
+ useAccountNavigationLabels: () => undefined,
49
+ }))
50
+
47
51
  vi.mock(
48
52
  '../../../src/components/account/Drawer/OrganizationDrawer/ContractSwitcher',
49
53
  () => ({
@@ -3,11 +3,20 @@
3
3
  */
4
4
 
5
5
  import { fireEvent, render, screen } from '@testing-library/react'
6
- import { describe, expect, it, vi } from 'vitest'
6
+ import { beforeEach, 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
+ }))
7
12
 
8
13
  import { OrganizationDrawerHeader } from '../../../src/components/account/Drawer/OrganizationDrawer/OrganizationDrawerHeader'
9
14
 
10
15
  describe('OrganizationDrawerHeader', () => {
16
+ beforeEach(() => {
17
+ mockUseAccountNavigationLabels.mockReturnValue(undefined)
18
+ })
19
+
11
20
  it('renders contract name fallback initial and links when contractUrl is set', () => {
12
21
  render(
13
22
  <OrganizationDrawerHeader
@@ -54,4 +63,38 @@ describe('OrganizationDrawerHeader', () => {
54
63
  expect(screen.getByAltText('Logo')).toBeTruthy()
55
64
  expect(screen.queryByRole('link', { name: /acme corp/i })).toBeNull()
56
65
  })
66
+
67
+ it('honors the CMS-provided change contract label', () => {
68
+ mockUseAccountNavigationLabels.mockReturnValue({
69
+ changeContractLabel: 'Trocar',
70
+ })
71
+ const onChangeContract = vi.fn()
72
+
73
+ render(
74
+ <OrganizationDrawerHeader
75
+ contractName="Acme Corp"
76
+ contractUrl={null}
77
+ onChangeContract={onChangeContract}
78
+ />
79
+ )
80
+
81
+ fireEvent.click(screen.getByRole('button', { name: 'Trocar' }))
82
+ expect(onChangeContract).toHaveBeenCalledTimes(1)
83
+ })
84
+
85
+ it('falls back to Change when the CMS label is blank', () => {
86
+ mockUseAccountNavigationLabels.mockReturnValue({
87
+ changeContractLabel: ' ',
88
+ })
89
+
90
+ render(
91
+ <OrganizationDrawerHeader
92
+ contractName="Acme Corp"
93
+ contractUrl={null}
94
+ onChangeContract={vi.fn()}
95
+ />
96
+ )
97
+
98
+ expect(screen.getByRole('button', { name: 'Change' })).toBeTruthy()
99
+ })
57
100
  })
@@ -38,7 +38,7 @@ vi.mock('@faststore/ui', async () => {
38
38
  ...actual,
39
39
  useUI: mockUseUI,
40
40
  SearchInputField: React.forwardRef(function MockSearchInputField(
41
- { onBlur, onKeyDown }: any,
41
+ { onBlur, onKeyDown, placeholder }: any,
42
42
  ref: any
43
43
  ) {
44
44
  const inputRef = React.useRef<HTMLInputElement>(null)
@@ -46,7 +46,7 @@ vi.mock('@faststore/ui', async () => {
46
46
  return (
47
47
  <input
48
48
  ref={inputRef}
49
- placeholder="Search"
49
+ placeholder={placeholder ?? 'Search'}
50
50
  onBlur={onBlur}
51
51
  onKeyDown={onKeyDown}
52
52
  />
@@ -229,4 +229,38 @@ describe('MyAccountListQuotes', () => {
229
229
 
230
230
  expect(window.location.href).toBe('/pvt/account/quotes?')
231
231
  })
232
+
233
+ it('honors CMS-provided labels for the page title, search and filters button', () => {
234
+ render(
235
+ <MyAccountListQuotes
236
+ listQuotes={{ list: [{ id: 'q-1' }] } as any}
237
+ total={1}
238
+ perPage={25}
239
+ filters={baseFilters()}
240
+ labels={{
241
+ pageTitle: 'Cotações',
242
+ searchPlaceholder: 'Buscar',
243
+ filtersLabel: 'Filtros',
244
+ }}
245
+ />
246
+ )
247
+
248
+ expect(screen.getByText('Cotações')).toBeTruthy()
249
+ expect(screen.getByPlaceholderText('Buscar')).toBeTruthy()
250
+ expect(screen.getByText('Filtros')).toBeTruthy()
251
+ })
252
+
253
+ it('honors CMS-provided empty state labels', () => {
254
+ render(
255
+ <MyAccountListQuotes
256
+ listQuotes={{ list: [] } as any}
257
+ total={0}
258
+ perPage={25}
259
+ filters={baseFilters()}
260
+ labels={{ noQuotesLabel: 'Você não tem cotações' }}
261
+ />
262
+ )
263
+
264
+ expect(screen.getByText('Você não tem cotações')).toBeTruthy()
265
+ })
232
266
  })
@@ -182,6 +182,55 @@ describe('MyAccountListQuotesTable', () => {
182
182
 
183
183
  expect(screen.queryByText(/left/)).toBeNull()
184
184
  })
185
+
186
+ it('honors CMS-provided labels for column headers, total and status', () => {
187
+ mockUseScreenResize.mockReturnValue({ isDesktop: true })
188
+
189
+ render(
190
+ <MyAccountListQuotesTable
191
+ listQuotes={{
192
+ list: [baseQuote({ createdBy: 'Ada Lovelace', status: 'Approved' })],
193
+ paging: {} as any,
194
+ }}
195
+ total={1}
196
+ perPage={25}
197
+ filters={baseFilters()}
198
+ labels={{
199
+ createdByLabel: 'Criado por',
200
+ creationDateLabel: 'Data de criação',
201
+ expiresOnLabel: 'Expira em',
202
+ totalLabel: 'Valor total',
203
+ approvedStatus: 'Aprovado',
204
+ }}
205
+ />
206
+ )
207
+
208
+ expect(screen.getByText('Criado por')).toBeTruthy()
209
+ expect(screen.getByText('Data de criação')).toBeTruthy()
210
+ expect(screen.getByText('Expira em')).toBeTruthy()
211
+ expect(screen.getByText('Valor total')).toBeTruthy()
212
+ expect(screen.getByText('Aprovado')).toBeTruthy()
213
+ })
214
+
215
+ it('honors a CMS-provided relative expiry unit and suffix', () => {
216
+ mockUseScreenResize.mockReturnValue({ isDesktop: true })
217
+ const soon = new Date(Date.now() + 3 * 3_600_000).toISOString()
218
+
219
+ render(
220
+ <MyAccountListQuotesTable
221
+ listQuotes={{
222
+ list: [baseQuote({ expiresAt: soon })],
223
+ paging: {} as any,
224
+ }}
225
+ total={1}
226
+ perPage={25}
227
+ filters={baseFilters()}
228
+ labels={{ hoursLabel: 'horas', leftSuffixLabel: 'restantes' }}
229
+ />
230
+ )
231
+
232
+ expect(screen.getByText(/horas restantes/)).toBeTruthy()
233
+ })
185
234
  })
186
235
 
187
236
  describe('Pagination', () => {
@@ -237,4 +286,23 @@ describe('Pagination', () => {
237
286
 
238
287
  expect(window.location.href).toBe('/pvt/account/quotes?page=2')
239
288
  })
289
+
290
+ it('honors CMS-provided pagination labels', () => {
291
+ render(
292
+ <Pagination
293
+ page={2}
294
+ total={60}
295
+ perPage={25}
296
+ labels={{
297
+ paginationOfLabel: 'de',
298
+ previousPageLabel: 'Página anterior',
299
+ nextPageLabel: 'Próxima página',
300
+ }}
301
+ />
302
+ )
303
+
304
+ expect(screen.getByText('26 — 50 de 60')).toBeTruthy()
305
+ expect(screen.getByRole('button', { name: 'Página anterior' })).toBeTruthy()
306
+ expect(screen.getByRole('button', { name: 'Próxima página' })).toBeTruthy()
307
+ })
240
308
  })
@@ -25,4 +25,29 @@ describe('MyAccountQuoteStatusBadge', () => {
25
25
  'neutral'
26
26
  )
27
27
  })
28
+
29
+ it('honors a CMS-provided label override, keeping the mapped variant', () => {
30
+ render(
31
+ <MyAccountQuoteStatusBadge
32
+ status="Approved"
33
+ statusCmsLabels={{ approvedStatus: 'Aprovado' }}
34
+ />
35
+ )
36
+
37
+ const badge = screen.getByText('Aprovado')
38
+ expect(badge.getAttribute('data-fs-my-account-badge-variant')).toBe(
39
+ 'success'
40
+ )
41
+ })
42
+
43
+ it('leaves statuses without a CMS override on their default label', () => {
44
+ render(
45
+ <MyAccountQuoteStatusBadge
46
+ status="Declined"
47
+ statusCmsLabels={{ approvedStatus: 'Aprovado' }}
48
+ />
49
+ )
50
+
51
+ expect(screen.getByText('Declined')).toBeTruthy()
52
+ })
28
53
  })
@@ -33,7 +33,9 @@ vi.mock('@faststore/ui', async () => {
33
33
  })
34
34
 
35
35
  import MyAccountQuotesFilterSlider from '../../../src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesFilterSlider/MyAccountQuotesFilterSlider'
36
+ import type { MyAccountQuotesFilterSliderProps } from '../../../src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesFilterSlider/MyAccountQuotesFilterSlider'
36
37
  import { getAllFacets } from '../../../src/components/account/quotes/MyAccountListQuotes/quoteFilters'
38
+ import type { useMyAccountFilter } from '../../../src/sdk/search/useMyAccountFilter'
37
39
 
38
40
  const originalLocation = window.location
39
41
 
@@ -44,7 +46,12 @@ function stubLocation(search = '') {
44
46
  })
45
47
  }
46
48
 
47
- function baseProps(overrides: Record<string, unknown> = {}) {
49
+ type FilterSliderTestProps = MyAccountQuotesFilterSliderProps &
50
+ ReturnType<typeof useMyAccountFilter>
51
+
52
+ function baseProps(
53
+ overrides: Partial<FilterSliderTestProps> = {}
54
+ ): FilterSliderTestProps {
48
55
  return {
49
56
  facets: getAllFacets({
50
57
  page: 1,
@@ -57,7 +64,7 @@ function baseProps(overrides: Record<string, unknown> = {}) {
57
64
  }),
58
65
  testId: 'test-filter-slider',
59
66
  dispatch: vi.fn(),
60
- expanded: false,
67
+ expanded: new Set<number>(),
61
68
  selected: [],
62
69
  title: 'Filters',
63
70
  clearButtonLabel: 'Clear All',
@@ -82,9 +89,9 @@ describe('MyAccountQuotesFilterSlider', () => {
82
89
  it('navigates with the selected status params on apply', () => {
83
90
  render(
84
91
  <MyAccountQuotesFilterSlider
85
- {...(baseProps({
92
+ {...baseProps({
86
93
  selected: [{ key: 'status', value: 'Draft' }],
87
- }) as any)}
94
+ })}
88
95
  />
89
96
  )
90
97
 
@@ -94,7 +101,7 @@ describe('MyAccountQuotesFilterSlider', () => {
94
101
  })
95
102
 
96
103
  it('navigates with the created date range params on apply', () => {
97
- render(<MyAccountQuotesFilterSlider {...(baseProps() as any)} />)
104
+ render(<MyAccountQuotesFilterSlider {...baseProps()} />)
98
105
 
99
106
  fireEvent.change(screen.getAllByLabelText('From')[0], {
100
107
  target: { value: '2026-01-01' },
@@ -115,9 +122,9 @@ describe('MyAccountQuotesFilterSlider', () => {
115
122
 
116
123
  render(
117
124
  <MyAccountQuotesFilterSlider
118
- {...(baseProps({
125
+ {...baseProps({
119
126
  selected: [{ key: 'status', value: 'Draft' }],
120
- }) as any)}
127
+ })}
121
128
  />
122
129
  )
123
130
 
@@ -129,7 +136,7 @@ describe('MyAccountQuotesFilterSlider', () => {
129
136
  })
130
137
 
131
138
  it('navigates with no query string when no filters are applied', () => {
132
- render(<MyAccountQuotesFilterSlider {...(baseProps() as any)} />)
139
+ render(<MyAccountQuotesFilterSlider {...baseProps()} />)
133
140
 
134
141
  fireEvent.click(screen.getByText('View Results'))
135
142
 
@@ -138,9 +145,7 @@ describe('MyAccountQuotesFilterSlider', () => {
138
145
 
139
146
  it('clears the created/expiry date refs and dispatches an empty facet selection on clear', () => {
140
147
  const dispatch = vi.fn()
141
- render(
142
- <MyAccountQuotesFilterSlider {...(baseProps({ dispatch }) as any)} />
143
- )
148
+ render(<MyAccountQuotesFilterSlider {...baseProps({ dispatch })} />)
144
149
 
145
150
  fireEvent.click(screen.getByText('Clear All'))
146
151
 
@@ -149,4 +154,40 @@ describe('MyAccountQuotesFilterSlider', () => {
149
154
  payload: [],
150
155
  })
151
156
  })
157
+
158
+ it('honors CMS-provided date range labels', () => {
159
+ render(
160
+ <MyAccountQuotesFilterSlider
161
+ {...baseProps({
162
+ fromLabel: 'De',
163
+ toLabel: 'Até',
164
+ invalidDateRangeLabel: 'Intervalo inválido',
165
+ })}
166
+ />
167
+ )
168
+
169
+ expect(screen.getAllByLabelText('De').length).toBeGreaterThan(0)
170
+ expect(screen.getAllByLabelText('Até').length).toBeGreaterThan(0)
171
+ })
172
+
173
+ it('honors a CMS-provided status field label', () => {
174
+ render(
175
+ <MyAccountQuotesFilterSlider {...baseProps({ statusLabel: 'Estado' })} />
176
+ )
177
+
178
+ expect(screen.getByText('Estado')).toBeTruthy()
179
+ })
180
+
181
+ it('forwards a CMS-provided remove-status aria-label template to the status chip', () => {
182
+ render(
183
+ <MyAccountQuotesFilterSlider
184
+ {...baseProps({
185
+ selected: [{ key: 'status', value: 'Draft' }],
186
+ removeStatusAriaLabel: 'Remover {status}',
187
+ })}
188
+ />
189
+ )
190
+
191
+ expect(screen.getByLabelText('Remover Draft')).toBeTruthy()
192
+ })
152
193
  })
@@ -119,4 +119,36 @@ describe('MyAccountQuotesSelectedTags', () => {
119
119
 
120
120
  expect(onClearAll).toHaveBeenCalledTimes(1)
121
121
  })
122
+
123
+ it('honors CMS-provided labels for the clear-all button, tag prefixes and date connectors', () => {
124
+ render(
125
+ <MyAccountQuotesSelectedTags
126
+ filters={{ createdAtFrom: '2026-01-01', createdAtTo: '2026-01-31' }}
127
+ onClearAll={vi.fn()}
128
+ onRemoveFilter={vi.fn()}
129
+ labels={{
130
+ clearAllLabel: 'Limpar tudo',
131
+ createdTagLabel: 'Criado em',
132
+ dateRangeSeparatorLabel: 'até',
133
+ }}
134
+ />
135
+ )
136
+
137
+ const expected = `Criado em: ${formatFilterDate('2026-01-01', 'en-US')} até ${formatFilterDate('2026-01-31', 'en-US')}`
138
+ expect(screen.getByText(expected)).toBeTruthy()
139
+ expect(screen.getByText('Limpar tudo')).toBeTruthy()
140
+ })
141
+
142
+ it('honors a CMS-provided status option label in the tag', () => {
143
+ render(
144
+ <MyAccountQuotesSelectedTags
145
+ filters={{ status: ['Draft'] }}
146
+ onClearAll={vi.fn()}
147
+ onRemoveFilter={vi.fn()}
148
+ labels={{ draftStatus: 'Rascunho' }}
149
+ />
150
+ )
151
+
152
+ expect(screen.getByText('Rascunho')).toBeTruthy()
153
+ })
122
154
  })