@faststore/core 4.9.0-dev.2 → 4.9.0-dev.4
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.
- package/.turbo/turbo-generate.log +3 -3
- package/.turbo/turbo-test.log +130 -125
- package/CHANGELOG.md +10 -0
- package/cms/faststore/my-account/components/cms_component__accountlistquotes.jsonc +240 -0
- package/cms/faststore/my-account/components/cms_component__accountnavigation.jsonc +76 -0
- package/cms/faststore/my-account/pages/cms_content_type__myaccountquotes.jsonc +18 -0
- package/package.json +6 -6
- package/src/components/account/Drawer/OrganizationDrawer/ContractSwitcher/ContractSwitcher.tsx +129 -26
- package/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawerHeader.tsx +7 -1
- package/src/components/account/quotes/MyAccountListQuotes/MyAccountListQuotes.tsx +42 -11
- package/src/components/account/quotes/MyAccountListQuotes/MyAccountListQuotesTable/MyAccountListQuotesTable.tsx +45 -15
- package/src/components/account/quotes/MyAccountListQuotes/MyAccountListQuotesTable/MyAccountQuoteStatusBadge.tsx +11 -3
- package/src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesFilterSlider/MyAccountQuotesFilterSlider.tsx +23 -0
- package/src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesSelectedTags/MyAccountQuotesSelectedTags.tsx +35 -11
- package/src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesStatusSelector/MyAccountQuotesStatusSelector.tsx +27 -10
- package/src/components/account/quotes/MyAccountListQuotes/index.ts +2 -0
- package/src/components/account/quotes/MyAccountListQuotes/quoteFilters.ts +16 -6
- package/src/components/account/quotes/MyAccountListQuotes/quotesLabels.ts +163 -0
- package/src/components/cms/account/Components.ts +2 -0
- package/src/components/sections/Account/AccountListQuotes.tsx +141 -0
- package/src/pages/pvt/account/quotes/index.tsx +59 -40
- package/src/sdk/account/accountPageContext.ts +17 -0
- package/src/sdk/account/getMyAccountRoutes.ts +17 -0
- package/src/sdk/account/resolveNonBlankLabels.ts +36 -0
- package/src/server/cms/myAccountDefaultSections.ts +2 -0
- package/src/utils/quoteStatus.ts +81 -0
- package/test/components/account/ContractSwitcher.browser.test.tsx +163 -0
- package/test/components/account/OrganizationDrawer.browser.test.tsx +4 -0
- package/test/components/account/OrganizationDrawerHeader.browser.test.tsx +44 -1
- package/test/components/quotes/MyAccountListQuotes.test.tsx +36 -2
- package/test/components/quotes/MyAccountListQuotesTable.test.tsx +68 -0
- package/test/components/quotes/MyAccountQuoteStatusBadge.test.tsx +25 -0
- package/test/components/quotes/MyAccountQuotesFilterSlider.test.tsx +52 -11
- package/test/components/quotes/MyAccountQuotesSelectedTags.test.tsx +32 -0
- package/test/components/quotes/MyAccountQuotesStatusSelector.test.tsx +100 -0
- package/test/components/quotes/quoteFilters.test.ts +34 -0
- package/test/components/sections/Account/AccountListQuotes.browser.test.tsx +122 -0
- package/test/server/cms/myAccountDefaultSections.test.ts +21 -0
package/src/utils/quoteStatus.ts
CHANGED
|
@@ -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=
|
|
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
|
-
|
|
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:
|
|
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
|
-
{...
|
|
92
|
+
{...baseProps({
|
|
86
93
|
selected: [{ key: 'status', value: 'Draft' }],
|
|
87
|
-
})
|
|
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 {...
|
|
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
|
-
{...
|
|
125
|
+
{...baseProps({
|
|
119
126
|
selected: [{ key: 'status', value: 'Draft' }],
|
|
120
|
-
})
|
|
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 {...
|
|
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
|
})
|