@faststore/core 4.8.1-dev.0 → 4.8.1-dev.2

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 (89) hide show
  1. package/.turbo/turbo-generate.log +3 -3
  2. package/.turbo/turbo-test.log +146 -105
  3. package/@generated/gql.ts +3 -3
  4. package/@generated/graphql.ts +4 -2
  5. package/@generated/persisted-documents.json +1 -1
  6. package/@generated/schema.graphql +4 -0
  7. package/CHANGELOG.md +16 -0
  8. package/cms/faststore/my-account/components/cms_component__accountnavigation.jsonc +10 -0
  9. package/cms/faststore/my-account/components/cms_component__accountorderdelivery.jsonc +80 -0
  10. package/cms/faststore/my-account/components/cms_component__accountorderdetails.jsonc +25 -0
  11. package/cms/faststore/my-account/components/cms_component__accountorderslist.jsonc +10 -0
  12. package/cms/faststore/my-account/components/cms_component__accountorderstatus.jsonc +90 -0
  13. package/cms/faststore/my-account/components/cms_component__accountordersummary.jsonc +25 -0
  14. package/package.json +6 -6
  15. package/src/components/account/Drawer/OrganizationDrawer/ContractSwitcher/ContractSwitcher.tsx +4 -6
  16. package/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawer.tsx +18 -3
  17. package/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawerFooter.tsx +13 -2
  18. package/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawerHeader.tsx +1 -0
  19. package/src/components/account/Drawer/OrganizationDrawer/section.module.scss +7 -0
  20. package/src/components/account/Drawer/OrganizationSignInButton/OrganizationSignInButton.tsx +7 -9
  21. package/src/components/account/Menu/Menu.tsx +24 -7
  22. package/src/components/account/orders/ListOrders/FilterSlider/FilterFacetPendingApproval/FilterFacetPendingApproval.tsx +8 -3
  23. package/src/components/account/orders/ListOrders/FilterSlider/FilterSlider.tsx +1 -0
  24. package/src/components/account/orders/ListOrders/ListOrders.tsx +2 -2
  25. package/src/components/account/orders/ListOrders/listOrdersLabels.ts +4 -0
  26. package/src/components/account/orders/OrderDetails/BuyingPolicyAlert/BuyingPolicyAlert.tsx +45 -9
  27. package/src/components/account/orders/OrderDetails/DeliveryCard/DeliveryCard.tsx +14 -3
  28. package/src/components/account/orders/OrderDetails/DeliveryOptionAccordion/DeliveryOptionAccordion.tsx +4 -1
  29. package/src/components/account/orders/OrderDetails/DeliveryOptionAccordion/DeliveryOptionAccordionDeliveryInfo.tsx +14 -7
  30. package/src/components/account/orders/OrderDetails/DeliveryOptionAccordion/DeliveryOptionAccordionProducts.tsx +8 -5
  31. package/src/components/account/orders/OrderDetails/OrderDetailsHeader.tsx +5 -0
  32. package/src/components/account/orders/OrderDetails/StatusCard/StatusCard.tsx +26 -89
  33. package/src/components/account/orders/OrderDetails/StatusCard/statusCardLocalization.ts +114 -0
  34. package/src/components/account/orders/OrderDetails/SummaryCard/SummaryCard.tsx +2 -1
  35. package/src/components/account/orders/OrderDetails/SummaryCard/getOrderTotalLabel.ts +19 -0
  36. package/src/components/account/orders/OrderDetails/getDeliveryOptionLabel.ts +135 -0
  37. package/src/components/account/orders/OrderDetails/orderDetailsLabels.ts +73 -0
  38. package/src/components/navigation/Navbar/Navbar.tsx +17 -19
  39. package/src/components/navigation/Navbar/NavbarAccountArea.tsx +57 -0
  40. package/src/components/navigation/NavbarSlider/NavbarSlider.tsx +16 -18
  41. package/src/components/sections/Account/AccountOrderDelivery.tsx +3 -1
  42. package/src/components/sections/Account/AccountOrderDetails.tsx +16 -0
  43. package/src/components/sections/Account/AccountOrderStatus.tsx +27 -3
  44. package/src/components/sections/Account/AccountOrdersList.tsx +4 -0
  45. package/src/pages/api/fs/switch-contract.ts +194 -0
  46. package/src/pages/api/graphql.ts +108 -69
  47. package/src/sdk/account/accountPageContext.ts +14 -3
  48. package/src/sdk/account/changeContractToken.ts +19 -63
  49. package/src/sdk/account/clearPersistedSessionState.ts +13 -7
  50. package/src/sdk/account/getMyAccountRoutes.ts +2 -0
  51. package/src/sdk/account/useAvailableContracts.ts +1 -0
  52. package/src/sdk/account/useSwitchContract.ts +11 -3
  53. package/src/sdk/overrides/PageProvider.tsx +1 -1
  54. package/src/sdk/session/index.ts +2 -1
  55. package/src/sdk/session/isSignInAreaResolved.ts +12 -0
  56. package/test/components/account/ContractSwitcher.browser.test.tsx +42 -0
  57. package/test/components/account/Menu.browser.test.tsx +123 -0
  58. package/test/components/account/OrganizationDrawer.browser.test.tsx +148 -0
  59. package/test/components/account/OrganizationDrawerFooter.browser.test.tsx +95 -1
  60. package/test/components/account/OrganizationDrawerHeader.browser.test.tsx +2 -0
  61. package/test/components/account/OrganizationSignInButton.browser.test.tsx +49 -0
  62. package/test/components/account/orders/BuyingPolicyAlert.browser.test.tsx +342 -0
  63. package/test/components/account/orders/DeliveryCard.browser.test.tsx +36 -0
  64. package/test/components/account/orders/DeliveryOptionAccordion.browser.test.tsx +76 -0
  65. package/test/components/account/orders/DeliveryOptionAccordionDeliveryInfo.browser.test.tsx +41 -0
  66. package/test/components/account/orders/DeliveryOptionAccordionProducts.browser.test.tsx +35 -0
  67. package/test/components/account/orders/FilterFacetPendingApproval.browser.test.tsx +25 -0
  68. package/test/components/account/orders/StatusCard.browser.test.tsx +37 -0
  69. package/test/components/account/orders/StatusCard.test.ts +101 -0
  70. package/test/components/account/orders/SummaryCard.browser.test.tsx +59 -0
  71. package/test/components/account/orders/getDeliveryOptionLabel.test.ts +129 -0
  72. package/test/components/account/orders/getOrderTotalLabel.test.ts +35 -0
  73. package/test/components/account/orders/listOrdersLabels.test.ts +23 -0
  74. package/test/components/account/orders/orderDetailsLabels.test.ts +13 -0
  75. package/test/components/navigation/Navbar.browser.test.tsx +183 -0
  76. package/test/components/navigation/NavbarAccountArea.browser.test.tsx +115 -0
  77. package/test/components/navigation/NavbarSlider.browser.test.tsx +143 -0
  78. package/test/components/sections/Account/AccountOrderDelivery.browser.test.tsx +72 -0
  79. package/test/components/sections/Account/AccountOrderDetails.browser.test.tsx +151 -0
  80. package/test/components/sections/Account/AccountOrderStatus.browser.test.tsx +59 -0
  81. package/test/components/sections/Account/AccountOrdersList.browser.test.tsx +67 -0
  82. package/test/pages/api/graphql.test.ts +27 -1
  83. package/test/pages/api/switch-contract.test.ts +385 -0
  84. package/test/sdk/account/changeContractToken.browser.test.ts +23 -71
  85. package/test/sdk/account/clearPersistedSessionState.browser.test.ts +24 -0
  86. package/test/sdk/account/getMyAccountRoutes.test.ts +11 -1
  87. package/test/sdk/account/useSwitchContract.browser.test.ts +21 -0
  88. package/test/sdk/session/isSignInAreaResolved.test.ts +22 -0
  89. package/test/utils/userOrderStatus.test.ts +22 -0
@@ -0,0 +1,385 @@
1
+ import type { NextApiRequest, NextApiResponse } from 'next'
2
+ import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest'
3
+
4
+ import type { SwitchContractResponse } from '../../../src/pages/api/fs/switch-contract'
5
+
6
+ vi.mock('discovery.config', () => ({
7
+ __esModule: true,
8
+ default: { api: { storeId: 'test-store' } },
9
+ }))
10
+
11
+ const CONTRACT_ID = '69b5d80e-8f61-44e0-a5cb-db54c326699c'
12
+ const ORDER_FORM_COOKIES =
13
+ 'checkout.vtex.com=__ofid=abc123; CheckoutOrderFormOwnership=owner-1'
14
+ const AUTH_COOKIE = 'VtexIdclientAutCookie_test-store=old-token'
15
+
16
+ const successPayload = {
17
+ authStatus: 'Success',
18
+ expiresIn: 3600,
19
+ authCookie: {
20
+ Name: 'VtexIdclientAutCookie_test-store',
21
+ Value: 'new-contract-token',
22
+ },
23
+ accountAuthCookie: null,
24
+ }
25
+
26
+ function createReqRes(overrides: Partial<NextApiRequest> = {}): {
27
+ req: NextApiRequest
28
+ res: NextApiResponse<SwitchContractResponse> & {
29
+ status: Mock
30
+ json: Mock
31
+ end: Mock
32
+ setHeader: Mock
33
+ }
34
+ } {
35
+ const res: Record<string, unknown> = {
36
+ status: vi.fn().mockImplementation(function (this: unknown) {
37
+ return this
38
+ }),
39
+ json: vi.fn(),
40
+ end: vi.fn(),
41
+ setHeader: vi.fn(),
42
+ }
43
+
44
+ return {
45
+ req: {
46
+ method: 'POST',
47
+ body: { contractId: CONTRACT_ID },
48
+ query: {},
49
+ ...overrides,
50
+ headers: {
51
+ host: 'store.example.com',
52
+ cookie: `${AUTH_COOKIE}; ${ORDER_FORM_COOKIES}`,
53
+ 'x-forwarded-proto': 'https',
54
+ ...overrides.headers,
55
+ },
56
+ } as unknown as NextApiRequest,
57
+ res: res as unknown as ReturnType<typeof createReqRes>['res'],
58
+ }
59
+ }
60
+
61
+ async function getHandler() {
62
+ const mod = await import('../../../src/pages/api/fs/switch-contract')
63
+ return mod.default
64
+ }
65
+
66
+ const setCookiesFrom = (res: { setHeader: Mock }): string[] => {
67
+ const call = res.setHeader.mock.calls.find(
68
+ ([name]) => String(name).toLowerCase() === 'set-cookie'
69
+ )
70
+
71
+ return (call?.[1] as string[] | undefined) ?? []
72
+ }
73
+
74
+ describe('/api/fs/switch-contract', () => {
75
+ beforeEach(() => {
76
+ vi.resetModules()
77
+ global.fetch = vi.fn().mockResolvedValue({
78
+ ok: true,
79
+ status: 200,
80
+ json: async () => successPayload,
81
+ })
82
+ vi.spyOn(console, 'error').mockImplementation(() => {})
83
+ })
84
+
85
+ it('returns 405 for non-POST requests', async () => {
86
+ const handler = await getHandler()
87
+ const { req, res } = createReqRes({ method: 'GET' })
88
+
89
+ await handler(req, res)
90
+
91
+ expect(res.status).toHaveBeenCalledWith(405)
92
+ expect(res.end).toHaveBeenCalled()
93
+ expect(global.fetch).not.toHaveBeenCalled()
94
+ })
95
+
96
+ it('returns 400 when contractId is missing', async () => {
97
+ const handler = await getHandler()
98
+ const { req, res } = createReqRes({ body: {} })
99
+
100
+ await handler(req, res)
101
+
102
+ expect(res.status).toHaveBeenCalledWith(400)
103
+ expect(res.json).toHaveBeenCalledWith(
104
+ expect.objectContaining({
105
+ success: false,
106
+ error: 'contractId is required',
107
+ })
108
+ )
109
+ })
110
+
111
+ it('returns 400 when the body is missing', async () => {
112
+ const handler = await getHandler()
113
+ const { req, res } = createReqRes({ body: undefined })
114
+
115
+ await handler(req, res)
116
+
117
+ expect(res.status).toHaveBeenCalledWith(400)
118
+ })
119
+
120
+ it('returns 400 when contractId is not a string', async () => {
121
+ const handler = await getHandler()
122
+ const { req, res } = createReqRes({ body: { contractId: 42 } })
123
+
124
+ await handler(req, res)
125
+
126
+ expect(res.status).toHaveBeenCalledWith(400)
127
+ })
128
+
129
+ it('relays to switch-properties forwarding the buyer cookie', async () => {
130
+ const handler = await getHandler()
131
+ const { req, res } = createReqRes()
132
+
133
+ await handler(req, res)
134
+
135
+ expect(global.fetch).toHaveBeenCalledTimes(1)
136
+ const [url, init] = (global.fetch as Mock).mock.calls[0]
137
+
138
+ expect(url).toBe(
139
+ 'https://test-store.myvtex.com/api/authenticator/storefront/credential/switch-properties?an=test-store'
140
+ )
141
+ expect(init.method).toBe('POST')
142
+ expect(init.headers.cookie).toBe(`${AUTH_COOKIE}; ${ORDER_FORM_COOKIES}`)
143
+ expect(init.signal).toBeDefined()
144
+ expect(JSON.parse(init.body as string)).toEqual({
145
+ properties: { customerId: CONTRACT_ID },
146
+ })
147
+ })
148
+
149
+ it('refuses to follow redirects so the cookie cannot leak cross-origin', async () => {
150
+ const handler = await getHandler()
151
+ const { req, res } = createReqRes()
152
+
153
+ await handler(req, res)
154
+
155
+ const [, init] = (global.fetch as Mock).mock.calls[0]
156
+
157
+ expect(init.redirect).toBe('error')
158
+ })
159
+
160
+ it('emits the new auth cookie and the orderForm expiry in one batch', async () => {
161
+ const handler = await getHandler()
162
+ const { req, res } = createReqRes()
163
+
164
+ await handler(req, res)
165
+
166
+ const cookies = setCookiesFrom(res)
167
+
168
+ expect(cookies).toEqual(
169
+ expect.arrayContaining([
170
+ expect.stringContaining(
171
+ 'VtexIdclientAutCookie_test-store=new-contract-token'
172
+ ),
173
+ ])
174
+ )
175
+ expect(
176
+ cookies.filter((cookie) => cookie.startsWith('checkout.vtex.com=;'))
177
+ .length
178
+ ).toBeGreaterThan(0)
179
+ expect(
180
+ cookies.filter((cookie) =>
181
+ cookie.startsWith('CheckoutOrderFormOwnership=;')
182
+ ).length
183
+ ).toBeGreaterThan(0)
184
+ expect(res.status).toHaveBeenCalledWith(200)
185
+ expect(res.json).toHaveBeenCalledWith({ success: true })
186
+ })
187
+
188
+ it('carries Max-Age from expiresIn and Secure behind https', async () => {
189
+ const handler = await getHandler()
190
+ const { req, res } = createReqRes()
191
+
192
+ await handler(req, res)
193
+
194
+ const authCookie = setCookiesFrom(res).find((cookie) =>
195
+ cookie.startsWith('VtexIdclientAutCookie_test-store=')
196
+ )
197
+
198
+ expect(authCookie).toContain('Max-Age=3600')
199
+ expect(authCookie).toContain('Path=/')
200
+ expect(authCookie).toContain('SameSite=Lax')
201
+ expect(authCookie).toContain('Secure')
202
+ })
203
+
204
+ it.each([
205
+ ['zero', 0],
206
+ ['negative', -1],
207
+ ['fractional', 1.5],
208
+ ['a numeric string', '3600'],
209
+ ['not a number', 'soon'],
210
+ ['missing', undefined],
211
+ ])(
212
+ 'falls back to the default Max-Age when expiresIn is %s',
213
+ async (_label, expiresIn) => {
214
+ ;(global.fetch as Mock).mockResolvedValue({
215
+ ok: true,
216
+ status: 200,
217
+ json: async () => ({ ...successPayload, expiresIn }),
218
+ })
219
+ const handler = await getHandler()
220
+ const { req, res } = createReqRes()
221
+
222
+ await handler(req, res)
223
+
224
+ const authCookie = setCookiesFrom(res).find((cookie) =>
225
+ cookie.startsWith('VtexIdclientAutCookie_test-store=')
226
+ )
227
+
228
+ // Max-Age=0 would delete the very credential this response installs.
229
+ expect(authCookie).toContain('Max-Age=86400')
230
+ expect(res.status).toHaveBeenCalledWith(200)
231
+ }
232
+ )
233
+
234
+ it('omits Secure on plain http', async () => {
235
+ const handler = await getHandler()
236
+ const { req, res } = createReqRes({
237
+ headers: { 'x-forwarded-proto': 'http' },
238
+ })
239
+
240
+ await handler(req, res)
241
+
242
+ const authCookie = setCookiesFrom(res).find((cookie) =>
243
+ cookie.startsWith('VtexIdclientAutCookie_test-store=')
244
+ )
245
+
246
+ expect(authCookie).not.toContain('Secure')
247
+ })
248
+
249
+ it('also emits the accountAuthCookie when present', async () => {
250
+ ;(global.fetch as Mock).mockResolvedValue({
251
+ ok: true,
252
+ status: 200,
253
+ json: async () => ({
254
+ ...successPayload,
255
+ accountAuthCookie: {
256
+ Name: 'VtexIdclientAutCookie_account',
257
+ Value: 'account-token',
258
+ },
259
+ }),
260
+ })
261
+ const handler = await getHandler()
262
+ const { req, res } = createReqRes()
263
+
264
+ await handler(req, res)
265
+
266
+ expect(setCookiesFrom(res)).toEqual(
267
+ expect.arrayContaining([
268
+ expect.stringContaining('VtexIdclientAutCookie_account=account-token'),
269
+ ])
270
+ )
271
+ })
272
+
273
+ it('does not expire orderForm cookies that the request does not carry', async () => {
274
+ const handler = await getHandler()
275
+ const { req, res } = createReqRes({ headers: { cookie: AUTH_COOKIE } })
276
+
277
+ await handler(req, res)
278
+
279
+ const cookies = setCookiesFrom(res)
280
+
281
+ expect(
282
+ cookies.some((cookie) => cookie.startsWith('checkout.vtex.com=;'))
283
+ ).toBe(false)
284
+ expect(res.status).toHaveBeenCalledWith(200)
285
+ })
286
+
287
+ it('returns 401 and sets no cookies when the upstream rejects the token', async () => {
288
+ ;(global.fetch as Mock).mockResolvedValue({ ok: false, status: 401 })
289
+ const handler = await getHandler()
290
+ const { req, res } = createReqRes()
291
+
292
+ await handler(req, res)
293
+
294
+ expect(res.status).toHaveBeenCalledWith(401)
295
+ expect(res.setHeader).not.toHaveBeenCalled()
296
+ })
297
+
298
+ it('maps upstream 403 to 401', async () => {
299
+ ;(global.fetch as Mock).mockResolvedValue({ ok: false, status: 403 })
300
+ const handler = await getHandler()
301
+ const { req, res } = createReqRes()
302
+
303
+ await handler(req, res)
304
+
305
+ expect(res.status).toHaveBeenCalledWith(401)
306
+ })
307
+
308
+ it('returns 502 and sets no cookies on other upstream failures', async () => {
309
+ ;(global.fetch as Mock).mockResolvedValue({ ok: false, status: 500 })
310
+ const handler = await getHandler()
311
+ const { req, res } = createReqRes()
312
+
313
+ await handler(req, res)
314
+
315
+ expect(res.status).toHaveBeenCalledWith(502)
316
+ expect(res.setHeader).not.toHaveBeenCalled()
317
+ })
318
+
319
+ it('returns 502 and sets no cookies when authStatus is not success', async () => {
320
+ ;(global.fetch as Mock).mockResolvedValue({
321
+ ok: true,
322
+ status: 200,
323
+ json: async () => ({ ...successPayload, authStatus: 'WrongCredentials' }),
324
+ })
325
+ const handler = await getHandler()
326
+ const { req, res } = createReqRes()
327
+
328
+ await handler(req, res)
329
+
330
+ expect(res.status).toHaveBeenCalledWith(502)
331
+ expect(res.setHeader).not.toHaveBeenCalled()
332
+ })
333
+
334
+ it('returns 502 and sets no cookies when no auth cookie comes back', async () => {
335
+ ;(global.fetch as Mock).mockResolvedValue({
336
+ ok: true,
337
+ status: 200,
338
+ json: async () => ({ ...successPayload, authCookie: null }),
339
+ })
340
+ const handler = await getHandler()
341
+ const { req, res } = createReqRes()
342
+
343
+ await handler(req, res)
344
+
345
+ expect(res.status).toHaveBeenCalledWith(502)
346
+ expect(res.setHeader).not.toHaveBeenCalled()
347
+ })
348
+
349
+ it('returns 504 when the upstream call times out', async () => {
350
+ const timeout = new Error('aborted')
351
+ timeout.name = 'TimeoutError'
352
+ ;(global.fetch as Mock).mockRejectedValue(timeout)
353
+ const handler = await getHandler()
354
+ const { req, res } = createReqRes()
355
+
356
+ await handler(req, res)
357
+
358
+ expect(res.status).toHaveBeenCalledWith(504)
359
+ expect(res.setHeader).not.toHaveBeenCalled()
360
+ })
361
+
362
+ it('returns 502 when the upstream call throws', async () => {
363
+ ;(global.fetch as Mock).mockRejectedValue(new Error('network failure'))
364
+ const handler = await getHandler()
365
+ const { req, res } = createReqRes()
366
+
367
+ await handler(req, res)
368
+
369
+ expect(res.status).toHaveBeenCalledWith(502)
370
+ expect(res.setHeader).not.toHaveBeenCalled()
371
+ })
372
+
373
+ it('does not log the error object, only its name', async () => {
374
+ ;(global.fetch as Mock).mockRejectedValue(new Error('token=super-secret'))
375
+ const handler = await getHandler()
376
+ const { req, res } = createReqRes()
377
+
378
+ await handler(req, res)
379
+
380
+ const logged = (console.error as Mock).mock.calls.flat().join(' ')
381
+
382
+ expect(logged).not.toContain('super-secret')
383
+ expect(logged).toContain('Error')
384
+ })
385
+ })
@@ -4,44 +4,25 @@
4
4
 
5
5
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
6
6
 
7
- vi.mock('discovery.config', () => ({
8
- default: {
9
- api: { storeId: 'b2bfaststoredev' },
10
- },
11
- }))
12
-
13
7
  const mockFetch = vi.hoisted(() => vi.fn())
14
8
  vi.mock('isomorphic-unfetch', () => ({ __esModule: true, default: mockFetch }))
15
9
 
16
10
  import {
17
11
  ContractSwitchError,
18
- applyVtexAuthCookieFromSwitchResponse,
19
12
  changeContractToken,
20
13
  isContractSwitchEnabled,
21
14
  } from '../../../src/sdk/account/changeContractToken'
22
15
 
23
- const successPayload = {
24
- authStatus: 'Success',
25
- expiresIn: 86_399,
26
- authCookie: {
27
- Name: 'VtexIdclientAutCookie_b2bfaststoredev',
28
- Value: 'new-contract-token',
29
- },
30
- accountAuthCookie: null,
31
- }
32
-
33
16
  describe('changeContractToken', () => {
34
17
  beforeEach(() => {
35
- document.cookie = ''
36
18
  mockFetch.mockResolvedValue({
37
19
  ok: true,
38
20
  status: 200,
39
- json: async () => successPayload,
21
+ json: async () => ({ success: true }),
40
22
  })
41
23
  })
42
24
 
43
25
  afterEach(() => {
44
- document.cookie = ''
45
26
  vi.clearAllMocks()
46
27
  })
47
28
 
@@ -51,35 +32,30 @@ describe('changeContractToken', () => {
51
32
 
52
33
  it('rejects when contractId is missing', async () => {
53
34
  await expect(changeContractToken('')).rejects.toThrow(ContractSwitchError)
35
+ expect(mockFetch).not.toHaveBeenCalled()
54
36
  })
55
37
 
56
- it('posts to switch-properties with the contract customerId', async () => {
38
+ it('posts the contractId to the switch-contract route', async () => {
57
39
  const contractId = '69b5d80e-8f61-44e0-a5cb-db54c326699c'
58
40
 
59
41
  await expect(changeContractToken(contractId)).resolves.toBe(true)
60
42
 
61
43
  expect(mockFetch).toHaveBeenCalledTimes(1)
62
44
  const [url, init] = mockFetch.mock.calls[0]
63
- expect(url).toBe(
64
- '/api/authenticator/storefront/credential/switch-properties?an=b2bfaststoredev'
65
- )
45
+ expect(url).toBe('/api/fs/switch-contract')
66
46
  expect(init.method).toBe('POST')
67
47
  expect(init.credentials).toBe('include')
68
48
  expect(init.signal).toBeDefined()
69
- expect(JSON.parse(init.body as string)).toEqual({
70
- properties: { customerId: contractId },
71
- })
49
+ expect(JSON.parse(init.body as string)).toEqual({ contractId })
72
50
  })
73
51
 
74
- it('applies the auth cookie returned by switch-properties', async () => {
52
+ it('never writes cookies from the client', async () => {
75
53
  await changeContractToken('contract-1')
76
54
 
77
- expect(document.cookie).toContain(
78
- 'VtexIdclientAutCookie_b2bfaststoredev=new-contract-token'
79
- )
55
+ expect(document.cookie).not.toContain('VtexIdclientAutCookie')
80
56
  })
81
57
 
82
- it('throws when the switch request fails', async () => {
58
+ it('throws when the route responds with a failure status', async () => {
83
59
  mockFetch.mockResolvedValueOnce({ ok: false, status: 401 })
84
60
 
85
61
  await expect(changeContractToken('contract-1')).rejects.toThrow(
@@ -87,7 +63,7 @@ describe('changeContractToken', () => {
87
63
  )
88
64
  })
89
65
 
90
- it('throws when the switch request times out', async () => {
66
+ it('throws when the request times out', async () => {
91
67
  const timeoutError = new Error('The operation was aborted')
92
68
  timeoutError.name = 'TimeoutError'
93
69
  mockFetch.mockRejectedValueOnce(timeoutError)
@@ -97,61 +73,37 @@ describe('changeContractToken', () => {
97
73
  )
98
74
  })
99
75
 
100
- it('throws when authStatus is not success', async () => {
101
- mockFetch.mockResolvedValueOnce({
102
- ok: true,
103
- status: 200,
104
- json: async () => ({ ...successPayload, authStatus: 'Failed' }),
105
- })
76
+ it('throws when the network call fails', async () => {
77
+ mockFetch.mockRejectedValueOnce(new Error('offline'))
106
78
 
107
79
  await expect(changeContractToken('contract-1')).rejects.toThrow(
108
- /Contract switch was not successful/
80
+ /Failed to switch contract: offline/
109
81
  )
110
82
  })
111
83
 
112
- it('throws when authCookie is missing from a successful response', async () => {
84
+ it('throws when the route reports success: false', async () => {
113
85
  mockFetch.mockResolvedValueOnce({
114
86
  ok: true,
115
87
  status: 200,
116
- json: async () => ({
117
- authStatus: 'Success',
118
- authCookie: null,
119
- }),
88
+ json: async () => ({ success: false, error: 'nope' }),
120
89
  })
121
90
 
122
91
  await expect(changeContractToken('contract-1')).rejects.toThrow(
123
- /no auth cookie was returned/
92
+ /Contract switch was not successful/
124
93
  )
125
94
  })
126
- })
127
95
 
128
- describe('applyVtexAuthCookieFromSwitchResponse', () => {
129
- beforeEach(() => {
130
- document.cookie = ''
131
- })
132
-
133
- afterEach(() => {
134
- document.cookie = ''
135
- })
136
-
137
- it('sets auth and account auth cookies when present', () => {
138
- applyVtexAuthCookieFromSwitchResponse({
139
- expiresIn: 3600,
140
- authCookie: {
141
- Name: 'VtexIdclientAutCookie_b2bfaststoredev',
142
- Value: 'store-token',
143
- },
144
- accountAuthCookie: {
145
- Name: 'VtexIdclientAutCookie_account',
146
- Value: 'account-token',
96
+ it('throws when the response body is not valid JSON', async () => {
97
+ mockFetch.mockResolvedValueOnce({
98
+ ok: true,
99
+ status: 200,
100
+ json: async () => {
101
+ throw new Error('invalid json')
147
102
  },
148
103
  })
149
104
 
150
- expect(document.cookie).toContain(
151
- 'VtexIdclientAutCookie_b2bfaststoredev=store-token'
152
- )
153
- expect(document.cookie).toContain(
154
- 'VtexIdclientAutCookie_account=account-token'
105
+ await expect(changeContractToken('contract-1')).rejects.toThrow(
106
+ /Contract switch was not successful/
155
107
  )
156
108
  })
157
109
  })
@@ -38,4 +38,28 @@ describe('clearPersistedSessionState', () => {
38
38
  expect(sessionStorage.getItem(STORAGE_KEY_PERSON_ID)).toBeNull()
39
39
  expect(sessionStorage.getItem(STORAGE_KEY_CACHE_BUST_LAST_VALUE)).toBeNull()
40
40
  })
41
+
42
+ it('also clears the persisted cart (fs::cart)', async () => {
43
+ await clearPersistedSessionState()
44
+
45
+ expect(mockDel).toHaveBeenCalledWith('fs::cart')
46
+ })
47
+
48
+ it('leaves cookies alone: the HttpOnly orderForm cookies are the route\u2019s job', async () => {
49
+ document.cookie = 'checkout.vtex.com=__ofid=abc123; path=/'
50
+
51
+ await clearPersistedSessionState()
52
+
53
+ expect(document.cookie).toContain('checkout.vtex.com')
54
+ })
55
+
56
+ it('still clears sessionStorage when IndexedDB deletion rejects', async () => {
57
+ mockDel.mockRejectedValue(new Error('blocked'))
58
+
59
+ await expect(clearPersistedSessionState()).resolves.toBeUndefined()
60
+
61
+ expect(sessionStorage.getItem(SESSION_READY_KEY)).toBeNull()
62
+ expect(sessionStorage.getItem(STORAGE_KEY_PERSON_ID)).toBeNull()
63
+ expect(sessionStorage.getItem(STORAGE_KEY_CACHE_BUST_LAST_VALUE)).toBeNull()
64
+ })
41
65
  })
@@ -74,7 +74,7 @@ describe('getMyAccountRoutes', () => {
74
74
  expect(result.find((r) => r.route === PROFILE_ROUTE)?.title).toBe('Profile')
75
75
  })
76
76
 
77
- it('does not relabel routes with no matching label key (e.g. Quotes)', () => {
77
+ it('does not relabel routes when their label key has no CMS value (e.g. Quotes)', () => {
78
78
  const result = getMyAccountRoutes({
79
79
  routes: [],
80
80
  labels: { profileLabel: 'Meu Perfil' },
@@ -83,6 +83,16 @@ describe('getMyAccountRoutes', () => {
83
83
  expect(result.find((r) => r.route === QUOTES_ROUTE)?.title).toBe('Quotes')
84
84
  })
85
85
 
86
+ it('overrides the Cards and Quotes menu titles with matching CMS labels', () => {
87
+ const result = getMyAccountRoutes({
88
+ routes: [],
89
+ labels: { cardsLabel: 'Cartões', quotesLabel: 'Cotações' },
90
+ })
91
+
92
+ expect(result.find((r) => r.route === CARDS_ROUTE)?.title).toBe('Cartões')
93
+ expect(result.find((r) => r.route === QUOTES_ROUTE)?.title).toBe('Cotações')
94
+ })
95
+
86
96
  it('preserves contentType through getMyAccountRoutes', () => {
87
97
  const result = getMyAccountRoutes({
88
98
  routes: [
@@ -69,6 +69,27 @@ describe('useSwitchContract', () => {
69
69
  expect(result.current.error).toBeNull()
70
70
  })
71
71
 
72
+ it('still reloads when clearing the persisted state fails', async () => {
73
+ vi.spyOn(
74
+ changeContractTokenModule,
75
+ 'changeContractToken'
76
+ ).mockResolvedValueOnce(true)
77
+ mockClearPersistedSessionState.mockRejectedValueOnce(new Error('blocked'))
78
+
79
+ const { result } = renderHook(() => useSwitchContract())
80
+
81
+ let ok: boolean | undefined
82
+ await act(async () => {
83
+ ok = await result.current.switchContract('contract-2')
84
+ })
85
+
86
+ // The route already swapped the cookies, so stopping here would leave the
87
+ // UI on the previous contract while the browser is on the new one.
88
+ expect(ok).toBe(true)
89
+ expect(mockReload).toHaveBeenCalledTimes(1)
90
+ expect(result.current.error).toBeNull()
91
+ })
92
+
72
93
  it('surfaces an error when switch-properties fails', async () => {
73
94
  vi.spyOn(
74
95
  changeContractTokenModule,
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { isSignInAreaResolved } from '../../../src/sdk/session/isSignInAreaResolved'
3
+
4
+ describe('isSignInAreaResolved', () => {
5
+ it('waits for the first validation of this page load', () => {
6
+ expect(
7
+ isSignInAreaResolved({ isSessionReady: true, hasValidated: false })
8
+ ).toBe(false)
9
+ })
10
+
11
+ it('resolves once the session is ready and validated', () => {
12
+ expect(
13
+ isSignInAreaResolved({ isSessionReady: true, hasValidated: true })
14
+ ).toBe(true)
15
+ })
16
+
17
+ it('never resolves before the session is ready', () => {
18
+ expect(
19
+ isSignInAreaResolved({ isSessionReady: false, hasValidated: true })
20
+ ).toBe(false)
21
+ })
22
+ })
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, it } from 'vitest'
2
2
 
3
3
  import {
4
+ extractOrderStatusLabelsFromSections,
4
5
  getLocalizedOrderStatusMap,
5
6
  getOrderStatusLabel,
6
7
  orderStatusMap,
@@ -33,4 +34,25 @@ describe('getLocalizedOrderStatusMap', () => {
33
34
  })
34
35
  ).toBe('Custom fallback')
35
36
  })
37
+
38
+ it('extracts order-details status translations from CMS sections', () => {
39
+ expect(
40
+ extractOrderStatusLabelsFromSections([
41
+ {
42
+ data: {
43
+ statusTitle: 'Situação',
44
+ paymentApprovedStatus: 'Pagamento aprovado',
45
+ },
46
+ },
47
+ {
48
+ data: {
49
+ canceledStatus: 'Cancelado',
50
+ },
51
+ },
52
+ ])
53
+ ).toEqual({
54
+ paymentApprovedStatus: 'Pagamento aprovado',
55
+ canceledStatus: 'Cancelado',
56
+ })
57
+ })
36
58
  })