@12-apps/payments-frontend 1.4.0 → 1.4.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 (24) hide show
  1. package/package.json +13 -5
  2. package/eslint.config.js +0 -34
  3. package/src/__tests__/checkout-confirmation.test.tsx +0 -177
  4. package/src/__tests__/connection-state.test.tsx +0 -84
  5. package/src/__tests__/context.test.tsx +0 -88
  6. package/src/__tests__/controlled-provider.test.tsx +0 -116
  7. package/src/__tests__/credential-confirm.test.tsx +0 -193
  8. package/src/__tests__/initial-provider.test.tsx +0 -81
  9. package/src/__tests__/provider-priority-list.test.tsx +0 -159
  10. package/src/__tests__/provider-status-bar.test.tsx +0 -152
  11. package/src/__tests__/slugged-provider.test.tsx +0 -108
  12. package/src/__tests__/verification-slot.test.tsx +0 -125
  13. package/src/card/tokenize.test.ts +0 -194
  14. package/src/components/checkout/__tests__/card-3ds-handover.test.tsx +0 -147
  15. package/src/components/checkout/__tests__/chain-instruments.test.tsx +0 -244
  16. package/src/components/checkout/__tests__/clear-cart-on-paid.test.tsx +0 -64
  17. package/src/components/checkout/__tests__/hosted-return.test.ts +0 -109
  18. package/src/components/checkout/__tests__/method-capability.test.tsx +0 -120
  19. package/src/components/checkout/__tests__/mint-deadline.test.ts +0 -99
  20. package/src/components/checkout/__tests__/payments-unavailable.test.tsx +0 -53
  21. package/src/components/checkout/__tests__/provider-chain.test.ts +0 -161
  22. package/src/components/checkout/__tests__/save-on-continue.test.tsx +0 -165
  23. package/src/components/checkout/__tests__/second-host.test.tsx +0 -86
  24. package/src/components/checkout/__tests__/unresolved-charge.test.tsx +0 -155
@@ -1,161 +0,0 @@
1
- // @vitest-environment jsdom
2
- /**
3
- * FUT-563 — the browser mints ONE instrument PER PROVIDER, so a card charge
4
- * can walk the merchant's chain.
5
- *
6
- * A card token is bound to whoever minted it: the gateway refuses to hand
7
- * provider #2 provider #1's blob and skips it instead. So the checkout's whole
8
- * contribution to failover is this — take the card the buyer typed ONCE, and
9
- * produce an instrument for each entry of the chain the server published.
10
- *
11
- * The scenarios the browser journey cannot reach live here: a chain entry with
12
- * no in-browser scheme (a hosted page needs no instrument), a chain the host
13
- * never published (an older server, or a fetch blip), and an entry whose
14
- * tokenization fails while the others succeed.
15
- */
16
- import { describe, expect, it } from "vitest";
17
-
18
- import { cardChain, cardPathAvailable, cardTokenization } from "../method-capability";
19
- import type { CheckoutProviderConfig } from "../types";
20
-
21
- function link(
22
- provider: string,
23
- tokenization: CheckoutProviderConfig["tokenization"],
24
- publicKey: string | null = null,
25
- ) {
26
- return {
27
- provider,
28
- tokenization: tokenization as "NONE" | "PUBLIC_KEY" | "SDK" | "REDIRECT",
29
- publicKey,
30
- mockTokenization: false,
31
- methods: ["PIX" as const, "CARD" as const],
32
- };
33
- }
34
-
35
- function config(over: Partial<CheckoutProviderConfig> = {}): CheckoutProviderConfig {
36
- return {
37
- provider: "pagbank",
38
- tokenization: "PUBLIC_KEY",
39
- publicKey: "pk_pagbank",
40
- mockTokenization: false,
41
- methods: ["PIX", "CARD"],
42
- ...over,
43
- };
44
- }
45
-
46
- describe("cardChain", () => {
47
- it("gives one tokenization config per entry, in the merchant's order", async () => {
48
- const chain = cardChain(
49
- config({
50
- chain: [link("pagbank", "PUBLIC_KEY", "pk_pagbank"), link("stone", "PUBLIC_KEY", "pk_stone")],
51
- }),
52
- );
53
-
54
- expect(chain.map((entry) => entry.provider)).toEqual(["pagbank", "stone"]);
55
- expect(chain.map((entry) => entry.publicKey)).toEqual(["pk_pagbank", "pk_stone"]);
56
- });
57
-
58
- it("marks entries with no in-browser scheme unmintable instead of dropping them", async () => {
59
- // A REDIRECT provider's own page takes the card, and a NONE one asks for no
60
- // instrument. Minting for either would produce a fake token under stub mode
61
- // and an error everywhere else — and the gateway passes an unattributed
62
- // charge straight through to them anyway.
63
- //
64
- // They stay in the LIST because the charge walks them: the card path counts
65
- // this list to decide whether `tokensByProvider` has to travel at all, and
66
- // a chain counted by "who minted" hides the hosted page from the server.
67
- const chain = cardChain(
68
- config({
69
- chain: [
70
- link("pagbank", "PUBLIC_KEY", "pk_pagbank"),
71
- link("infinitepay", "REDIRECT"),
72
- link("nothing", "NONE"),
73
- ],
74
- }),
75
- );
76
-
77
- expect(chain.map((entry) => entry.provider)).toEqual(["pagbank", "infinitepay", "nothing"]);
78
- expect(chain.map((entry) => entry.mintable)).toEqual([true, false, false]);
79
- });
80
-
81
- it("falls back to the HEAD alone when the host published no chain", async () => {
82
- // Exactly the pre-FUT-563 behaviour: one instrument, for the active
83
- // provider. A server that does not answer a chain must not break checkout.
84
- expect(cardChain(config())).toEqual([{ ...cardTokenization(config()), mintable: true }]);
85
- });
86
-
87
- it("has nothing to mint for a store with no provider at all", async () => {
88
- expect(cardChain(null)).toEqual([]);
89
- });
90
-
91
- it("never re-heads or reorders the merchant's list", async () => {
92
- // The storefront is a READER of the priority list. Preferring the entry
93
- // with a key, or sorting by anything, would silently override the order the
94
- // owner set and the plan ceiling already truncated.
95
- const chain = cardChain(
96
- config({
97
- provider: "stone",
98
- chain: [link("stone", "PUBLIC_KEY", null), link("pagbank", "PUBLIC_KEY", "pk_pagbank")],
99
- }),
100
- );
101
-
102
- expect(chain.map((entry) => entry.provider)).toEqual(["stone", "pagbank"]);
103
- });
104
-
105
- it("counts the whole chain, hosted entries included", async () => {
106
- // What `tokensByProvider`'s presence is decided on. A card acquirer plus a
107
- // hosted-page fallback is TWO providers the walk may reach, even though
108
- // only one of them is ever minted for.
109
- const chain = cardChain(
110
- config({ chain: [link("pagbank", "PUBLIC_KEY", "pk_pagbank"), link("infinitepay", "REDIRECT")] }),
111
- );
112
-
113
- expect(chain).toHaveLength(2);
114
- expect(chain.filter((entry) => entry.mintable)).toHaveLength(1);
115
- });
116
- });
117
-
118
- /**
119
- * FUT-563 — the picker and the SERVER must answer the same question.
120
- *
121
- * `usesHostedCheckout` asks the WHOLE chain whether anybody tokenizes in the
122
- * browser, so a REDIRECT head no longer means "the buyer is handed over". A
123
- * client that still read the head alone offered a card the submit could not
124
- * mint, and a store whose only mintable entry has no key offered one nobody
125
- * could pay.
126
- */
127
- describe("cardPathAvailable", () => {
128
- it("offers CARD for a hosted-page store: its own site takes the card", async () => {
129
- expect(cardPathAvailable(config({ chain: [link("infinitepay", "REDIRECT")] }))).toBe(true);
130
- });
131
-
132
- it("offers CARD when a LATER entry can be minted for, not just the head", async () => {
133
- // The server shows our form for exactly this chain, so the picker must
134
- // agree — and the submit mints against the tail.
135
- const mixed = config({
136
- provider: "infinitepay",
137
- tokenization: "REDIRECT",
138
- publicKey: null,
139
- chain: [link("infinitepay", "REDIRECT"), link("pagbank", "PUBLIC_KEY", "pk_pagbank")],
140
- });
141
-
142
- expect(cardPathAvailable(mixed)).toBe(true);
143
- });
144
-
145
- it("refuses CARD when the only mintable entry has no key this browser can use", async () => {
146
- // Stone with no publishable key: the form would render and the submit
147
- // would die. Better a disabled tile that says so.
148
- const unusable = config({
149
- provider: "infinitepay",
150
- tokenization: "REDIRECT",
151
- publicKey: null,
152
- chain: [link("infinitepay", "REDIRECT"), link("stone", "PUBLIC_KEY", null)],
153
- });
154
-
155
- expect(cardPathAvailable(unusable)).toBe(false);
156
- });
157
-
158
- it("still fails OPEN while the config has not answered", async () => {
159
- expect(cardPathAvailable(null)).toBe(true);
160
- });
161
- });
@@ -1,165 +0,0 @@
1
- // @vitest-environment jsdom
2
- /**
3
- * The buyer's details must be persisted when they press "Continuar" on the
4
- * "Dados" step — NOT when a payment is raised.
5
- *
6
- * This is a regression test with a history: the save used to live only in the
7
- * order-creation path, which runs a whole step later, only once a payment
8
- * method is chosen, and which deliberately never writes the CPF. A buyer who
9
- * filled the form and then hit anything at all on the payment screen — no
10
- * provider configured, a declined card, an abandoned PIX, a closed tab — had
11
- * nothing stored.
12
- *
13
- * Since FUT-564 the write itself belongs to the host (`saveBuyerContact` port;
14
- * the storefront's implementation owns the wire shape and the blank-CPF-never-
15
- * clears rule, pinned in its own suite). What THIS suite pins is the flow's
16
- * side of the contract — TIMING, CONSENT and INDEPENDENCE: the port fires on
17
- * "Continuar", carries the CPF, respects the LGPD checkbox, and the step
18
- * advances no matter what the host does with the call.
19
- */
20
- import { act, renderHook, waitFor } from "@testing-library/react";
21
- import { afterEach, describe, expect, it, vi } from "vitest";
22
-
23
- import { useCheckoutController, type CheckoutHostPorts } from "../use-checkout-controller";
24
- import type { BuyerContact } from "../types";
25
-
26
- /** A valid CPF (passes the client-side check digits). */
27
- const VALID_CPF = "52998224725";
28
-
29
- /**
30
- * One test's own port set + recorder. Everything is a local — no module-level
31
- * mutable state, so the tests carry no order dependency between them
32
- * (`test-flakiness/no-test-isolation`). The stub only ever PUSHES onto its
33
- * container, never reassigns a closed-over binding (`no-global-state-mutation`).
34
- */
35
- function makePorts(): {
36
- ports: CheckoutHostPorts;
37
- savedContacts: () => BuyerContact[];
38
- orderRequested: () => boolean;
39
- } {
40
- const recorded: BuyerContact[] = [];
41
- const createOrder = vi.fn();
42
- return {
43
- ports: {
44
- createOrder,
45
- saveBuyerContact: (contact) => {
46
- recorded.push(contact);
47
- },
48
- onExitToMenu: vi.fn(),
49
- },
50
- savedContacts: () => recorded,
51
- orderRequested: () => createOrder.mock.calls.length > 0,
52
- };
53
- }
54
-
55
- afterEach(() => {
56
- vi.restoreAllMocks();
57
- });
58
-
59
- describe('"Dados" step persists the buyer on Continuar', () => {
60
- it("saves name, phone and CPF the moment Continuar is pressed", async () => {
61
- const { ports, savedContacts, orderRequested } = makePorts();
62
- const { result } = renderHook(() => useCheckoutController(ports));
63
-
64
- act(() => {
65
- result.current.setBuyer({ name: "Ana", phone: "11999999999", taxId: VALID_CPF });
66
- });
67
- act(() => {
68
- result.current.goToPayment();
69
- });
70
-
71
- await waitFor(() => expect(savedContacts()).toHaveLength(1));
72
- expect(savedContacts()[0]).toEqual({
73
- name: "Ana",
74
- phone: "11999999999",
75
- taxId: VALID_CPF,
76
- });
77
- // The whole point: no order was created, and none needed to be.
78
- expect(orderRequested()).toBe(false);
79
- });
80
-
81
- it("advances to Pagamento without waiting on the save", async () => {
82
- const { ports, savedContacts } = makePorts();
83
- // A host whose save blows up must not trap the buyer on the Dados step —
84
- // the port is fire-and-forget, so the flow never sees the failure at all.
85
- const { result } = renderHook(() => useCheckoutController(ports));
86
-
87
- act(() => {
88
- result.current.setBuyer({ name: "Ana", taxId: VALID_CPF });
89
- });
90
- act(() => {
91
- result.current.goToPayment();
92
- });
93
-
94
- await waitFor(() => expect(savedContacts()).toHaveLength(1));
95
- expect(result.current.step).toBe("payment");
96
- });
97
-
98
- it("does not save when the buyer declined the consent checkbox", async () => {
99
- const { ports, savedContacts } = makePorts();
100
- const { result } = renderHook(() => useCheckoutController(ports));
101
-
102
- act(() => {
103
- result.current.setBuyer({ name: "Ana", taxId: VALID_CPF });
104
- result.current.setSaveProfile(false);
105
- });
106
- act(() => {
107
- result.current.goToPayment();
108
- });
109
-
110
- await waitFor(() => expect(result.current.step).toBe("payment"));
111
- expect(savedContacts()).toHaveLength(0);
112
- });
113
-
114
- it("does not save — or advance — when the CPF is invalid", async () => {
115
- const { ports, savedContacts } = makePorts();
116
- const { result } = renderHook(() => useCheckoutController(ports));
117
-
118
- act(() => {
119
- result.current.setBuyer({ name: "Ana", taxId: "11111111111" });
120
- });
121
- act(() => {
122
- result.current.goToPayment();
123
- });
124
-
125
- await waitFor(() => expect(result.current.errorField).toBe("cpf"));
126
- expect(result.current.step).toBe("dados");
127
- expect(savedContacts()).toHaveLength(0);
128
- });
129
-
130
- it("lets a buyer with a CPF on file continue without retyping it", async () => {
131
- const { ports, savedContacts } = makePorts();
132
- // Third argument: the buyer already gave the store a CPF, so Dados was
133
- // skipped and they reopened it through "Alterar". The field starts empty
134
- // (the client never receives the saved value) — requiring one here stranded
135
- // them on a form they had no way to satisfy.
136
- const { result } = renderHook(() => useCheckoutController(ports, undefined, true));
137
-
138
- act(() => {
139
- result.current.setStep("dados");
140
- });
141
- act(() => {
142
- result.current.goToPayment();
143
- });
144
-
145
- await waitFor(() => expect(result.current.step).toBe("payment"));
146
- expect(result.current.errorField).toBeNull();
147
- // Nothing typed ⇒ the port carries no values at all. What the HOST must do
148
- // with a blank CPF (omit it so it can never clear the stored one) is the
149
- // host's contract, pinned beside its `saveBuyerContact` implementation.
150
- expect(savedContacts()[0]).toEqual({ name: undefined, phone: undefined, taxId: undefined });
151
- });
152
-
153
- it("still demands a CPF from a buyer who has none on file", async () => {
154
- const { ports, savedContacts } = makePorts();
155
- const { result } = renderHook(() => useCheckoutController(ports));
156
-
157
- act(() => {
158
- result.current.goToPayment();
159
- });
160
-
161
- await waitFor(() => expect(result.current.errorField).toBe("cpf"));
162
- expect(result.current.step).toBe("dados");
163
- expect(savedContacts()).toHaveLength(0);
164
- });
165
- });
@@ -1,86 +0,0 @@
1
- // @vitest-environment jsdom
2
- /**
3
- * The second-host proof (FUT-564 acceptance).
4
- *
5
- * This package's dependency graph contains NO `@12-apps/ui` — it is not even
6
- * resolvable from here — so a green run of this suite IS the proof that a host
7
- * without this repo's design system can mount the buyer checkout: every pixel
8
- * below renders through the raw-MUI default slots (`mui-defaults.tsx`),
9
- * because no `components` prop is passed and no provider sits above the flow.
10
- *
11
- * The walk is the buyer's own: Dados (CPF form, consent checkbox, pay bar) →
12
- * Pagamento (method picker) → the card form, with the order raised through the
13
- * host port. The load-bearing test ids must survive the default slots too —
14
- * they are the same hooks the storefront journeys click.
15
- */
16
- import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
17
- import { afterEach, describe, expect, it, vi } from "vitest";
18
-
19
- import { CheckoutFlow } from "../checkout-flow";
20
- import type { CheckoutOrder, CreateOrderRequest, CreateOrderResult } from "../types";
21
-
22
- afterEach(cleanup);
23
-
24
- const CARD_ORDER: CheckoutOrder = {
25
- orderId: "o-2nd-host",
26
- status: "AWAITING_PAYMENT",
27
- method: "CARD",
28
- totalCents: 1000,
29
- subtotalCents: 1000,
30
- discountTotalCents: 0,
31
- appliedDiscounts: [],
32
- totalLabel: "R$ 10,00",
33
- };
34
-
35
- /** A valid CPF (passes the client-side check digits). */
36
- const VALID_CPF = "52998224725";
37
-
38
- describe("CheckoutFlow with no design system present (raw-MUI default slots)", () => {
39
- it("walks Dados → Pagamento → card form through the default slots", async () => {
40
- const createOrder = vi.fn<(input: CreateOrderRequest) => Promise<CreateOrderResult>>(
41
- async () => ({ ok: true, data: CARD_ORDER }),
42
- );
43
-
44
- render(
45
- <CheckoutFlow
46
- cart={{ empty: false, totalLabel: "R$ 10,00", totalItems: 1 }}
47
- createOrder={createOrder}
48
- onExitToMenu={vi.fn()}
49
- />,
50
- );
51
-
52
- // Dados renders through the defaults: stepper, CPF field, consent, pay bar.
53
- expect(screen.getByTestId("checkout-stepper")).toBeTruthy();
54
- expect(screen.getByTestId("buyer-save-profile")).toBeTruthy();
55
- expect(screen.getByTestId("checkout-pay-bar")).toBeTruthy();
56
-
57
- fireEvent.change(screen.getByTestId("buyer-cpf"), { target: { value: VALID_CPF } });
58
- fireEvent.click(screen.getByTestId("checkout-continue"));
59
-
60
- // Pagamento: the method picker, with both in-browser methods on offer.
61
- await waitFor(() => expect(screen.getByTestId("checkout-method")).toBeTruthy());
62
- expect(screen.getByTestId("checkout-method-PIX")).toBeTruthy();
63
-
64
- // Choosing card auto-raises the order through the HOST port…
65
- fireEvent.click(screen.getByTestId("checkout-method-CARD"));
66
- await waitFor(() => expect(createOrder).toHaveBeenCalledTimes(1));
67
- expect(createOrder.mock.calls[0]?.[0]).toMatchObject({ method: "CARD", saveProfile: true });
68
-
69
- // …and the card form renders, again through the default slots.
70
- await waitFor(() => expect(screen.getByTestId("card-number")).toBeTruthy());
71
- expect(screen.getByTestId("card-holder")).toBeTruthy();
72
- expect(screen.getByTestId("card-pay")).toBeTruthy();
73
- });
74
-
75
- it("shows the empty-cart state for a cart with nothing to pay", () => {
76
- render(
77
- <CheckoutFlow
78
- cart={{ empty: true, totalLabel: "R$ 0,00", totalItems: 0 }}
79
- createOrder={vi.fn()}
80
- onExitToMenu={vi.fn()}
81
- />,
82
- );
83
-
84
- expect(screen.getByTestId("checkout-empty")).toBeTruthy();
85
- });
86
- });
@@ -1,155 +0,0 @@
1
- // @vitest-environment jsdom
2
- /**
3
- * FUT-563 — a charge NOBODY can confirm is not a charge that failed.
4
- *
5
- * `PAYMENT_UNRESOLVED` means some provider may be holding the buyer's money and
6
- * no probe could say. It is the one refusal where inviting a retry is actively
7
- * harmful, and both checkout surfaces used to do exactly that: the card view
8
- * put "não pague de novo" under a danger heading reading "Não foi possível
9
- * pagar", with the live "Pagar R$ …" bar directly beneath it; the Pagamento
10
- * step gave the same body a solid "Tentar novamente" that mints a SECOND order
11
- * at a new reference, outside the walk's re-probe of the first.
12
- *
13
- * What is pinned here is the presentation, not the words: the code decides it,
14
- * so a copy edit cannot silently turn the affordance back on.
15
- */
16
- import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
17
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
18
-
19
- const client = vi.hoisted(() => ({
20
- chargeCard: vi.fn(),
21
- listSavedCards: vi.fn(),
22
- refreshCardPublicKey: vi.fn(),
23
- pollOrderStatus: vi.fn(),
24
- fetchCheckoutConfig: vi.fn(),
25
- }));
26
-
27
- vi.mock("../client", () => client);
28
-
29
- import { CardView } from "../card-view";
30
- import { PaymentErrorPanel } from "../payment-error-panel";
31
- import type { CheckoutOrder } from "../types";
32
-
33
- const ORDER: CheckoutOrder = {
34
- orderId: "o1",
35
- status: "AWAITING_PAYMENT",
36
- method: "CARD",
37
- totalCents: 1250,
38
- subtotalCents: 1250,
39
- discountTotalCents: 0,
40
- appliedDiscounts: [],
41
- totalLabel: "R$ 12,50",
42
- };
43
-
44
- const UNRESOLVED =
45
- "Estamos confirmando o seu pagamento com o provedor. NÃO pague de novo — se a cobrança " +
46
- "foi feita, ela será confirmada sozinha.";
47
-
48
- /**
49
- * A card already in the vault. Paying with it needs no tokenizer and no typing,
50
- * which keeps these tests about the RENDERING of the answer.
51
- */
52
- const SAVED = {
53
- id: "card_1",
54
- brand: "visa",
55
- last4: "1111",
56
- expMonth: 12,
57
- expYear: 2034,
58
- holder: "VERA CADEIA",
59
- };
60
-
61
- beforeEach(() => {
62
- vi.clearAllMocks();
63
- client.listSavedCards.mockResolvedValue([SAVED]);
64
- client.refreshCardPublicKey.mockResolvedValue({ ok: true, data: { publicKey: null } });
65
- client.pollOrderStatus.mockResolvedValue({ ok: true, data: "AWAITING_PAYMENT" });
66
- });
67
-
68
- afterEach(() => {
69
- cleanup();
70
- });
71
-
72
- describe("the CARD view", () => {
73
- /** Render the card form and drive a submit whose charge comes back `code`d. */
74
- async function submitWith(failure: { error: string; code?: string }): Promise<void> {
75
- client.chargeCard.mockResolvedValue({ ok: false, ...failure });
76
- render(
77
- <CardView
78
- order={ORDER}
79
- providerConfig={{ provider: "pagbank", publicKey: null, mockTokenization: true }}
80
- providerChain={[
81
- { provider: "pagbank", publicKey: null, mockTokenization: true, mintable: true },
82
- ]}
83
- onResolved={vi.fn()}
84
- />,
85
- );
86
- // The saved card is preselected once the list lands, so the pay bar is
87
- // everything this submit needs.
88
- await waitFor(() => {
89
- expect(screen.getByTestId("saved-cards")).toBeTruthy();
90
- });
91
- fireEvent.click(screen.getByTestId("card-pay-bar").querySelector("button") as HTMLElement);
92
- await waitFor(() => {
93
- expect(client.chargeCard).toHaveBeenCalled();
94
- });
95
- }
96
-
97
- it("shows an UNRESOLVED charge as a warning, and takes the pay bar away", async () => {
98
- await submitWith({ error: UNRESOLVED, code: "PAYMENT_UNRESOLVED" });
99
-
100
- // Not "Não foi possível pagar": the buyer reads the bold title first, and
101
- // that one contradicts the body's own instruction. And the one action the
102
- // message forbids is not on screen at all.
103
- await waitFor(() => {
104
- expect(screen.getByTestId("card-unresolved")).toBeTruthy();
105
- expect(screen.queryByTestId("card-error")).toBeNull();
106
- expect(screen.queryByTestId("card-pay-bar")).toBeNull();
107
- });
108
- expect(screen.getByText("Estamos confirmando seu pagamento")).toBeTruthy();
109
- });
110
-
111
- it("still shows an ordinary failure as a failure, with the pay bar intact", async () => {
112
- await submitWith({ error: "Cartão recusado.", code: "PAYMENT_UNAVAILABLE" });
113
-
114
- await waitFor(() => {
115
- expect(screen.getByTestId("card-error")).toBeTruthy();
116
- });
117
- expect(screen.getByTestId("card-pay-bar")).toBeTruthy();
118
- });
119
- });
120
-
121
- describe("the Pagamento step's error panel", () => {
122
- it("offers NO retry for an unresolved charge", () => {
123
- const { container } = render(
124
- <PaymentErrorPanel
125
- message={UNRESOLVED}
126
- emailFlagged={false}
127
- code="PAYMENT_UNRESOLVED"
128
- onUseEmail={vi.fn()}
129
- onRetry={vi.fn()}
130
- />,
131
- );
132
-
133
- // "Tentar novamente" re-runs createOrder: a new order at a new reference,
134
- // which the walk's re-probe of the old one cannot protect.
135
- expect(screen.getByTestId("checkout-unresolved")).toBeTruthy();
136
- // Asserted on the rendered TEXT: nothing here is asynchronous, so this is
137
- // not an element that went away — the affordance was never offered.
138
- expect(container.textContent).not.toContain("Tentar novamente");
139
- });
140
-
141
- it("keeps the retry for every other refusal", () => {
142
- render(
143
- <PaymentErrorPanel
144
- message="Não foi possível criar o pedido."
145
- emailFlagged={false}
146
- code="EMPTY_CART"
147
- onUseEmail={vi.fn()}
148
- onRetry={vi.fn()}
149
- />,
150
- );
151
-
152
- expect(screen.getByTestId("checkout-error")).toBeTruthy();
153
- expect(screen.getByTestId("checkout-retry-payment")).toBeTruthy();
154
- });
155
- });