@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,244 +0,0 @@
1
- // @vitest-environment jsdom
2
- /**
3
- * FUT-563 — "o comprador digita o cartão UMA vez e a compra sobrevive ao
4
- * primeiro provedor falhar": the browser half of it.
5
- *
6
- * A card instrument is bound to the provider that minted it, so a chain the
7
- * checkout tokenized for only ONCE is a chain the gateway can attempt only
8
- * once — it skips the rest rather than send a blob they cannot read. What is
9
- * pinned here is that the same validated card produces one instrument per
10
- * chain entry and they all travel on ONE charge, with nothing re-typed.
11
- *
12
- * The browser journey (the `cadeia-de-provedores` Gherkin feature)
13
- * covers the happy path end to end; these are the halves it cannot stage — a
14
- * tail provider whose tokenization fails while the head's works, and a store
15
- * whose host published no chain at all.
16
- */
17
- import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
18
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
19
-
20
- import type { JSX } from "react";
21
-
22
- const client = vi.hoisted(() => ({
23
- chargeCard: vi.fn(),
24
- listSavedCards: vi.fn(),
25
- refreshCardPublicKey: vi.fn(),
26
- pollOrderStatus: vi.fn(),
27
- fetchCheckoutConfig: vi.fn(),
28
- }));
29
-
30
- vi.mock("../client", () => client);
31
-
32
- import type { CardTokenizationConfig } from "../../../card";
33
- import type { CardChainLink } from "../method-capability";
34
- import type { CheckoutOrder } from "../types";
35
- import { useCardCheckout } from "../use-card-checkout";
36
-
37
- const ORDER: CheckoutOrder = {
38
- orderId: "o1",
39
- status: "AWAITING_PAYMENT",
40
- method: "CARD",
41
- totalCents: 1250,
42
- subtotalCents: 1250,
43
- discountTotalCents: 0,
44
- appliedDiscounts: [],
45
- totalLabel: "R$ 12,50",
46
- };
47
-
48
- /** Stub-mode entries: the mock tokenizer mints without a real key or network. */
49
- const HEAD: CardChainLink = {
50
- provider: "pagbank",
51
- publicKey: null,
52
- mockTokenization: true,
53
- mintable: true,
54
- };
55
- const TAIL: CardChainLink = {
56
- provider: "stone",
57
- publicKey: null,
58
- mockTokenization: true,
59
- mintable: true,
60
- };
61
- /**
62
- * Declares an in-browser scheme, but this browser has none for it: no
63
- * tokenizer, no key, no mock grant. The mint FAILS — a different thing from
64
- * an entry that asks for no instrument.
65
- */
66
- const UNMINTABLE: CardChainLink = {
67
- provider: "unknown-vendor",
68
- publicKey: null,
69
- mockTokenization: false,
70
- mintable: true,
71
- };
72
- /** A hosted page: its own site takes the card, so nothing is minted for it. */
73
- const HOSTED: CardChainLink = {
74
- provider: "infinitepay",
75
- publicKey: null,
76
- mockTokenization: false,
77
- mintable: false,
78
- };
79
- /**
80
- * The ACTIVE provider of a hosted-headed store, as the config's head triple:
81
- * no scheme, no key, no stub grant. Nothing this browser can mint against.
82
- */
83
- const HOSTED_HEAD: CardTokenizationConfig = {
84
- provider: "infinitepay",
85
- publicKey: null,
86
- mockTokenization: false,
87
- };
88
-
89
- const CARD = { number: "4111111111111111", holder: "VERA CADEIA", expiry: "12/34", cvv: "123" };
90
-
91
- /**
92
- * Filling and paying are two BUTTONS, not one handler: `handlePay` closes over
93
- * the card in state, so a submit fired in the same tick as `setCard` validates
94
- * the empty form and never reaches the tokenizer.
95
- */
96
- function Harness({
97
- chain,
98
- config = HEAD,
99
- }: {
100
- chain: CardChainLink[];
101
- config?: CardTokenizationConfig;
102
- }): JSX.Element {
103
- const cc = useCardCheckout(ORDER, {}, config, vi.fn(), 10, undefined, chain);
104
- return (
105
- <div>
106
- <button type="button" data-testid="fill" onClick={() => cc.setCard(CARD)}>
107
- Preencher
108
- </button>
109
- <button type="button" data-testid="pay" onClick={() => void cc.handlePay()}>
110
- Pagar
111
- </button>
112
- {cc.error ? <p data-testid="pay-error">{cc.error}</p> : null}
113
- </div>
114
- );
115
- }
116
-
117
- /** The `tokensByProvider` of the single charge submitted. */
118
- function sentInstruments(): Record<string, string> | undefined {
119
- const input = client.chargeCard.mock.calls[0]?.[0] as
120
- | { tokensByProvider?: Record<string, string> }
121
- | undefined;
122
- return input?.tokensByProvider;
123
- }
124
-
125
- async function payWith(chain: CardChainLink[], config?: CardTokenizationConfig): Promise<void> {
126
- render(<Harness chain={chain} config={config} />);
127
- await waitFor(() => {
128
- expect(client.listSavedCards).toHaveBeenCalled();
129
- });
130
- fireEvent.click(screen.getByTestId("fill"));
131
- fireEvent.click(screen.getByTestId("pay"));
132
- await waitFor(() => {
133
- expect(client.chargeCard).toHaveBeenCalled();
134
- });
135
- }
136
-
137
- /** Fill + tap Pagar, then prove the submit never reached the charge route. */
138
- async function expectNoCharge(
139
- chain: CardChainLink[],
140
- config?: CardTokenizationConfig,
141
- ): Promise<void> {
142
- render(<Harness chain={chain} config={config} />);
143
- await waitFor(() => {
144
- expect(client.listSavedCards).toHaveBeenCalled();
145
- });
146
- fireEvent.click(screen.getByTestId("fill"));
147
- fireEvent.click(screen.getByTestId("pay"));
148
- // The refusal is what ends the submit, so waiting for it is what proves the
149
- // charge was skipped rather than merely still in flight.
150
- await waitFor(() => {
151
- expect(screen.getByTestId("pay-error").textContent).toMatch(/indisponível nesta loja/i);
152
- });
153
- expect(client.chargeCard).not.toHaveBeenCalled();
154
- }
155
-
156
- beforeEach(() => {
157
- vi.clearAllMocks();
158
- client.listSavedCards.mockResolvedValue([]);
159
- // The head is PagBank with no key, so the hook's on-demand key refresh runs
160
- // on mount (FUT-174). It finds none here — stub mode is what mints the token.
161
- client.refreshCardPublicKey.mockResolvedValue({ ok: true, data: { publicKey: null } });
162
- client.pollOrderStatus.mockResolvedValue({ ok: true, data: "AWAITING_PAYMENT" });
163
- client.chargeCard.mockResolvedValue({ ok: true, data: { status: "AWAITING_PAYMENT" } });
164
- });
165
-
166
- afterEach(() => {
167
- cleanup();
168
- });
169
-
170
- describe("one instrument per provider (FUT-563)", () => {
171
- it("mints for EVERY entry of the chain from one typed card", async () => {
172
- await payWith([HEAD, TAIL]);
173
-
174
- const minted = sentInstruments();
175
- expect(Object.keys(minted ?? {}).sort()).toEqual(["pagbank", "stone"]);
176
- // Distinct instruments: one blob reused across providers is the exact
177
- // thing the gateway refuses to send.
178
- expect(minted?.["pagbank"]).not.toBe(minted?.["stone"]);
179
- // The buyer typed once.
180
- expect(client.chargeCard).toHaveBeenCalledTimes(1);
181
- });
182
-
183
- it("keeps the head's own token as the bare `token`", async () => {
184
- await payWith([HEAD, TAIL]);
185
-
186
- const input = client.chargeCard.mock.calls[0]?.[0] as { token: string };
187
- expect(sentInstruments()?.["pagbank"]).toBe(input.token);
188
- });
189
-
190
- it("still pays when a TAIL provider cannot be tokenized for", async () => {
191
- // That provider is one the walk will skip — strictly better than failing
192
- // the whole payment because the second acquirer's key was missing. The map
193
- // still travels, naming who DID mint: the server then skips the tail by
194
- // name instead of reading the bare token as the head's and skipping
195
- // everyone else.
196
- await payWith([HEAD, UNMINTABLE]);
197
-
198
- expect(sentInstruments()).toEqual({ pagbank: expect.any(String) });
199
- expect(client.chargeCard).toHaveBeenCalledTimes(1);
200
- });
201
-
202
- it("sends the map when the TAIL is a provider that needs NO instrument", async () => {
203
- // The two-provider shape this feature exists for: a card acquirer plus a
204
- // hosted-page fallback. Nothing is minted for the hosted entry — its own
205
- // site takes the card — so a map counted by minted keys would be dropped,
206
- // the bare token read as the head's, and the fallback refused for
207
- // "holding someone else's instrument". The chain has TWO entries, so the
208
- // map goes.
209
- await payWith([HEAD, HOSTED]);
210
-
211
- expect(sentInstruments()).toEqual({ pagbank: expect.any(String) });
212
- // And never a mocked one for the hosted provider.
213
- expect(sentInstruments()).not.toHaveProperty("infinitepay");
214
- });
215
-
216
- it("mints against the first MINTABLE entry when the HEAD is a hosted page", async () => {
217
- // The store this feature creates by simply enabling a second provider:
218
- // InfinitePay (REDIRECT) first, an acquirer behind it. The server asks the
219
- // WHOLE chain whether anybody tokenizes in the browser, so it shows our
220
- // card form — and the submit must mint against the entry the form is being
221
- // shown FOR. Minting against the head refused the payment outright, with a
222
- // full PAN already typed and no provider ever asked.
223
- await payWith([HOSTED, HEAD], HOSTED_HEAD);
224
-
225
- expect(client.chargeCard).toHaveBeenCalledTimes(1);
226
- expect(sentInstruments()).toEqual({ pagbank: expect.any(String) });
227
- // The bare token is the one that was actually minted, not the head's.
228
- const input = client.chargeCard.mock.calls[0]?.[0] as { token: string };
229
- expect(input.token).toBe(sentInstruments()?.["pagbank"]);
230
- });
231
-
232
- it("still refuses when NO entry of the chain can be minted for", async () => {
233
- // Nothing to fall back to — the buyer is told so before any charge, which
234
- // is the honest answer and the one FUT-697 wrote.
235
- await expectNoCharge([HOSTED, UNMINTABLE], HOSTED_HEAD);
236
- });
237
-
238
- it("sends no map at all for a single-provider store", async () => {
239
- // The pre-FUT-563 wire shape, unchanged: one token, no map.
240
- await payWith([HEAD]);
241
-
242
- expect(sentInstruments()).toBeUndefined();
243
- });
244
- });
@@ -1,64 +0,0 @@
1
- // @vitest-environment jsdom
2
- /**
3
- * A buyer who paid must not be handed their cart back.
4
- *
5
- * FUT-601 fixed the SERVER half: the cart is emptied inside the confirmation
6
- * transaction, for the delivery that won the claim. Nothing told the SPA — its
7
- * cart provider survives every checkout route change and kept the snapshot it
8
- * seeded on mount, so the buyer pressed "Voltar ao cardápio" and found the
9
- * badge still counting the items they had just bought.
10
- *
11
- * Since FUT-564 the flow lives in this package and the cart lives in the host,
12
- * so the client half of that fix is a PORT: `onPaid` fires when — and only
13
- * when — the order settles PAID. The host re-reads its server-emptied cart
14
- * there; a payment that did not go through fires nothing, because that shopper
15
- * still has a basket to retry with.
16
- */
17
- import { act, renderHook, waitFor } from "@testing-library/react";
18
- import { afterEach, describe, expect, it, vi } from "vitest";
19
-
20
- import { useCheckoutController, type CheckoutHostPorts } from "../use-checkout-controller";
21
-
22
- /** One test's own port set — every callback a fresh spy. */
23
- function makePorts(): CheckoutHostPorts & { onPaid: ReturnType<typeof vi.fn> } {
24
- return {
25
- createOrder: vi.fn(),
26
- saveBuyerContact: vi.fn(),
27
- onExitToMenu: vi.fn(),
28
- onPaid: vi.fn(),
29
- };
30
- }
31
-
32
- afterEach(() => {
33
- vi.restoreAllMocks();
34
- });
35
-
36
- describe("the host's onPaid port after a checkout settles", () => {
37
- it("fires once the order is PAID", async () => {
38
- const ports = makePorts();
39
- const { result } = renderHook(() => useCheckoutController(ports));
40
-
41
- act(() => {
42
- result.current.handleResolved("PAID");
43
- });
44
-
45
- await waitFor(() => {
46
- expect(ports.onPaid).toHaveBeenCalledTimes(1);
47
- });
48
- });
49
-
50
- it("stays silent when the payment failed", async () => {
51
- const ports = makePorts();
52
- const { result } = renderHook(() => useCheckoutController(ports));
53
-
54
- act(() => {
55
- result.current.handleResolved("FAILED");
56
- });
57
-
58
- // The step advanced, so the effect has had its chance to run.
59
- await waitFor(() => {
60
- expect(result.current.finalStatus).toBe("FAILED");
61
- });
62
- expect(ports.onPaid).not.toHaveBeenCalled();
63
- });
64
- });
@@ -1,109 +0,0 @@
1
- // @vitest-environment jsdom
2
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
3
-
4
- import { rememberHostedOrder, takeHostedOrder } from "../hosted-return";
5
- import type { CheckoutOrder } from "../types";
6
-
7
- /**
8
- * FUT-556 — surviving the trip to a hosted checkout.
9
- *
10
- * A redirect provider tears the SPA down. These pin the two rules that keep the
11
- * return from landing on a blank payment step, and keep an ABANDONED payment
12
- * from resurfacing as if it had happened: the parked order is handed back only
13
- * on a return trip, and only once.
14
- */
15
-
16
- const ORDER: CheckoutOrder = {
17
- orderId: "o1",
18
- status: "AWAITING_PAYMENT",
19
- method: "PIX",
20
- totalCents: 550,
21
- subtotalCents: 550,
22
- discountTotalCents: 0,
23
- appliedDiscounts: [],
24
- totalLabel: "R$ 5,50",
25
- };
26
-
27
- /** Put the tab on `search`, the way the provider's redirect leaves it. */
28
- function land(search: string): void {
29
- window.history.replaceState({}, "", `/future-drink/menu/checkout${search}`);
30
- }
31
-
32
- describe("hosted-return", () => {
33
- beforeEach(() => {
34
- window.sessionStorage.clear();
35
- land("");
36
- });
37
- afterEach(() => {
38
- window.sessionStorage.clear();
39
- });
40
-
41
- it("hands the order back when the provider sent the buyer home", () => {
42
- rememberHostedOrder(ORDER);
43
- land("?transaction_nsu=123&slug=abc");
44
-
45
- expect(takeHostedOrder()).toEqual(ORDER);
46
- });
47
-
48
- it("recognises a return that carries only one of the markers", () => {
49
- rememberHostedOrder(ORDER);
50
- land("?slug=abc");
51
-
52
- expect(takeHostedOrder()?.orderId).toBe("o1");
53
- });
54
-
55
- it("recognises a buyer coming back from a Stripe 3DS challenge (FUT-698)", () => {
56
- // What Stripe appends to the card confirm's `return_url` after the issuer
57
- // page: the intent id, its client secret and the redirect verdict.
58
- rememberHostedOrder(ORDER);
59
- land("?payment_intent=pi_1&payment_intent_client_secret=pi_1_secret&redirect_status=succeeded");
60
-
61
- expect(takeHostedOrder()?.orderId).toBe("o1");
62
- });
63
-
64
- it("still recognises the hosted-store return exactly as before (FUT-556)", () => {
65
- // The InfinitePay handoff must keep working with the Stripe markers added:
66
- // "loja hospedada continua com o hand-off e retorno de hoje".
67
- rememberHostedOrder(ORDER);
68
- land("?transaction_nsu=123&slug=abc&order_nsu=o1");
69
-
70
- expect(takeHostedOrder()?.orderId).toBe("o1");
71
- });
72
-
73
- it("ignores a parked order when this is not a return trip", () => {
74
- rememberHostedOrder(ORDER);
75
- land("");
76
-
77
- // A buyer who abandoned the provider's page and came back to checkout later
78
- // must start a fresh order, not resume one they never paid.
79
- expect(takeHostedOrder()).toBeNull();
80
- });
81
-
82
- it("hands the order back exactly once", () => {
83
- rememberHostedOrder(ORDER);
84
- land("?transaction_nsu=123");
85
-
86
- expect(takeHostedOrder()).not.toBeNull();
87
- expect(takeHostedOrder()).toBeNull();
88
- });
89
-
90
- it("answers null for a value that is not an order", () => {
91
- window.sessionStorage.setItem("futurepay.checkout.hostedOrder", '{"nonsense":true}');
92
- land("?transaction_nsu=123");
93
-
94
- expect(takeHostedOrder()).toBeNull();
95
- });
96
-
97
- it("answers null for unparseable storage rather than throwing", () => {
98
- window.sessionStorage.setItem("futurepay.checkout.hostedOrder", "{not json");
99
- land("?transaction_nsu=123");
100
-
101
- expect(takeHostedOrder()).toBeNull();
102
- });
103
-
104
- it("answers null when nothing was parked", () => {
105
- land("?transaction_nsu=123");
106
-
107
- expect(takeHostedOrder()).toBeNull();
108
- });
109
- });
@@ -1,120 +0,0 @@
1
- // @vitest-environment jsdom
2
- /**
3
- * FUT-698 — the method picker derives its options from the chain's declared
4
- * capabilities (`capabilities.methods`, surfaced by `GET /api/checkout/config`).
5
- *
6
- * These are the browser-unreachable halves of the ticket's "método sem suporte
7
- * na cadeia não aparece no picker" scenario: every adapter shipped today
8
- * declares PIX and CARD, so the missing-method case only exists synthetically.
9
- * Test names keep the scenario's Given/When/Then. Rendered with NO slot
10
- * provider on purpose (raw-MUI defaults carry the load-bearing test ids);
11
- * jest-dom is not a dependency here — DOM properties are asserted directly.
12
- */
13
- import { cleanup, render, screen } from "@testing-library/react";
14
- import { afterEach, describe, expect, it, vi } from "vitest";
15
-
16
- import { PaymentStep } from "../checkout-steps";
17
- import { MethodPicker } from "../method-picker";
18
- import type { CheckoutProviderConfig } from "../types";
19
-
20
- afterEach(() => {
21
- cleanup();
22
- });
23
-
24
- function config(methods: CheckoutProviderConfig["methods"]): CheckoutProviderConfig {
25
- return {
26
- provider: "stone",
27
- tokenization: "PUBLIC_KEY",
28
- publicKey: "pk_stone",
29
- mockTokenization: false,
30
- methods,
31
- };
32
- }
33
-
34
- /** Render the Pagamento step with no order raised — just the picker. */
35
- function renderPaymentStep(providerConfig: CheckoutProviderConfig | null): void {
36
- render(
37
- <PaymentStep
38
- method={null}
39
- onMethodChange={vi.fn()}
40
- order={null}
41
- buyer={{}}
42
- creating={false}
43
- createError={null}
44
- errorField={null}
45
- onGenerate={vi.fn()}
46
- onUseEmail={vi.fn()}
47
- providerConfig={providerConfig}
48
- onResolved={vi.fn()}
49
- />,
50
- );
51
- }
52
-
53
- /** The offered methods, read off the rendered radiogroup's testids. */
54
- function offeredTestIds(): string[] {
55
- return Array.from(
56
- screen.getByTestId("checkout-method").querySelectorAll("[data-testid^='checkout-method-']"),
57
- ).map((option) => option.getAttribute("data-testid") ?? "");
58
- }
59
-
60
- describe("MethodPicker — method by capability (FUT-698)", () => {
61
- it("given the chain cannot PIX, when the picker renders, then PIX is not offered", () => {
62
- render(<MethodPicker value={null} onChange={vi.fn()} offered={["CARD"]} />);
63
-
64
- expect(offeredTestIds()).toEqual(["checkout-method-CARD"]);
65
- });
66
-
67
- it("given the chain cannot CARD, when the picker renders, then CARD is not offered", () => {
68
- render(<MethodPicker value={null} onChange={vi.fn()} offered={["PIX"]} />);
69
-
70
- expect(offeredTestIds()).toEqual(["checkout-method-PIX"]);
71
- });
72
-
73
- it("given the config has not answered yet, then every method renders (fail open)", () => {
74
- render(<MethodPicker value={null} onChange={vi.fn()} offered={null} />);
75
-
76
- expect(offeredTestIds()).toEqual(["checkout-method-PIX", "checkout-method-CARD"]);
77
- });
78
-
79
- it("card-path gating still wins over a chain that declares CARD", () => {
80
- // The chain CAN charge cards, but this browser has no way to mint one
81
- // (no scheme/key/stub) — the tile stays visible but DISABLED with a caption
82
- // (FUT-697 review): a removed tile would leave an unexplained one-option
83
- // "choice", while a disabled one says why.
84
- render(
85
- <MethodPicker value={null} onChange={vi.fn()} offered={["PIX", "CARD"]} cardUnavailable />,
86
- );
87
-
88
- expect(offeredTestIds()).toEqual(["checkout-method-PIX", "checkout-method-CARD"]);
89
- const cardTile = screen.getByTestId("checkout-method-CARD") as HTMLButtonElement;
90
- expect(cardTile.disabled).toBe(true);
91
- expect(screen.getByText("Indisponível nesta loja")).toBeTruthy();
92
- });
93
- });
94
-
95
- describe("PaymentStep — the config's methods reach the picker (FUT-698)", () => {
96
- it("given a chain that only PIXes, when Pagamento renders, then card is never offered", () => {
97
- renderPaymentStep(config(["PIX"]));
98
-
99
- expect(offeredTestIds()).toEqual(["checkout-method-PIX"]);
100
- });
101
-
102
- it("given a chain that declares BOLETO, then only methods the checkout can drive appear", () => {
103
- renderPaymentStep(config(["PIX", "CARD", "BOLETO"]));
104
-
105
- // Stone/Stripe declare BOLETO server-side (true), but this checkout has no
106
- // boleto UI yet (phase 2) — the picker offers only what it can honour.
107
- expect(screen.getByTestId("checkout-method-PIX")).toBeTruthy();
108
- expect(screen.getByTestId("checkout-method-CARD")).toBeTruthy();
109
- expect(screen.getByTestId("checkout-method").querySelectorAll("[role='radio']")).toHaveLength(
110
- 2,
111
- );
112
- });
113
-
114
- it("given no config yet, then Pagamento offers both methods as before", () => {
115
- renderPaymentStep(null);
116
-
117
- expect(screen.getByTestId("checkout-method-PIX")).toBeTruthy();
118
- expect(screen.getByTestId("checkout-method-CARD")).toBeTruthy();
119
- });
120
- });
@@ -1,99 +0,0 @@
1
- // @vitest-environment jsdom
2
- /**
3
- * FUT-563 — a BACKUP acquirer nobody can reach must not hold the payment.
4
- *
5
- * Minting one instrument per chain entry puts the buyer's Pagar button behind
6
- * every acquirer in the chain, and a tokenizer is a cross-origin POST with no
7
- * deadline of its own — browser `fetch` has none either. A middlebox that
8
- * accepts the socket and never answers therefore left "Pagar R$ …" spinning
9
- * and disabled for as long as the OS kept the connection: the failover feature
10
- * blocking on the provider it exists to fall back TO, with the head's own
11
- * token already minted and nothing wrong with it.
12
- *
13
- * These are the halves the browser journey cannot stage — it has no way to
14
- * make an acquirer stop answering.
15
- */
16
- import { afterEach, describe, expect, it, vi } from "vitest";
17
-
18
- import { resolveNewCardToken } from "../card-instruments";
19
- import type { CardChainLink } from "../method-capability";
20
-
21
- vi.mock("../client", () => ({ refreshCardPublicKey: vi.fn() }));
22
-
23
- const CARD = { number: "4111111111111111", holder: "VERA CADEIA", expiry: "12/34", cvv: "123" };
24
-
25
- /** Stub mode: the head mints locally, with no network and no key. */
26
- const HEAD: CardChainLink = {
27
- provider: "pagbank",
28
- publicKey: null,
29
- mockTokenization: true,
30
- mintable: true,
31
- };
32
- /** A real Pagar.me tokenizer — this one goes to the network. */
33
- const STONE: CardChainLink = {
34
- provider: "stone",
35
- publicKey: "pk_stone",
36
- mockTokenization: false,
37
- mintable: true,
38
- };
39
- /** A real Stripe tokenizer — likewise. */
40
- const STRIPE: CardChainLink = {
41
- provider: "stripe",
42
- publicKey: "pk_stripe",
43
- mockTokenization: false,
44
- mintable: true,
45
- };
46
-
47
- /** A tokenizer endpoint that accepts the request and never answers. */
48
- function blackHole(): ReturnType<typeof vi.fn> {
49
- const fetchMock = vi.fn(() => new Promise<Response>(() => undefined));
50
- vi.stubGlobal("fetch", fetchMock);
51
- return fetchMock;
52
- }
53
-
54
- afterEach(() => {
55
- vi.unstubAllGlobals();
56
- });
57
-
58
- describe("minting the chain is bounded and concurrent", () => {
59
- it("charges on the head when a TAIL acquirer never answers", async () => {
60
- const fetchMock = blackHole();
61
-
62
- const resolved = await resolveNewCardToken(CARD, HEAD, "o1", vi.fn(), false, [HEAD, STONE], 20);
63
-
64
- // The degradation this module already documents: the provider we could not
65
- // mint for is one the walk will skip. The payment still goes out.
66
- expect(resolved.ok).toBe(true);
67
- const instruments = resolved.ok ? resolved.data : null;
68
- expect(instruments?.tokensByProvider).toEqual({ pagbank: expect.any(String) });
69
- // And the abandoned request is ABORTED, not merely ignored — a token that
70
- // arrives after we gave up on it is one nothing will ever charge.
71
- const init = fetchMock.mock.calls[0]?.[1] as RequestInit | undefined;
72
- expect(init?.signal?.aborted).toBe(true);
73
- });
74
-
75
- it("asks every tail at ONCE, not one after the other", async () => {
76
- // Sequentially, two unreachable backups cost the buyer two full deadlines
77
- // before a charge they could have had immediately.
78
- const fetchMock = blackHole();
79
- const deadlineMs = 400;
80
-
81
- const minting = resolveNewCardToken(
82
- CARD,
83
- HEAD,
84
- "o1",
85
- vi.fn(),
86
- false,
87
- [HEAD, STONE, STRIPE],
88
- deadlineMs,
89
- );
90
- // Both requests are in flight well inside ONE deadline — which they could
91
- // not be if the second waited for the first to time out.
92
- await vi.waitFor(() => {
93
- expect(fetchMock).toHaveBeenCalledTimes(2);
94
- });
95
-
96
- const resolved = await minting;
97
- expect(resolved.ok).toBe(true);
98
- });
99
- });
@@ -1,53 +0,0 @@
1
- // @vitest-environment jsdom
2
- import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
3
- import { afterEach, describe, expect, it, vi } from "vitest";
4
-
5
- import { PaymentsUnavailable } from "../payments-unavailable";
6
-
7
- /**
8
- * A store with no active payment provider used to render the full payment step
9
- * — PIX and Cartão — and fail only at order creation, telling the shopper
10
- * "tente novamente em instantes": retry advice for a condition retrying never
11
- * fixes. These pin the two honest answers instead.
12
- *
13
- * Rendered with NO slot provider on purpose: outside a
14
- * `CheckoutComponentsProvider` the component falls back to the raw-MUI
15
- * defaults, so this suite also pins that the default slots carry the
16
- * load-bearing test ids. jest-dom is not a dependency here — DOM properties
17
- * are asserted directly.
18
- */
19
-
20
- afterEach(cleanup);
21
-
22
- describe("checkout — store cannot charge online", () => {
23
- it("offers the waiter when the shopper has a mesa", async () => {
24
- const onCallWaiter = vi.fn();
25
- render(<PaymentsUnavailable waiterAvailable onCallWaiter={onCallWaiter} />);
26
-
27
- expect(screen.getByTestId("checkout-call-waiter")).toBeTruthy();
28
- fireEvent.click(screen.getByTestId("checkout-call-waiter-button"));
29
- expect(onCallWaiter).toHaveBeenCalledTimes(1);
30
-
31
- // Never the dead end: the shopper is not told to retry.
32
- await waitFor(() => expect(screen.queryByText(/tente novamente/i)).toBeNull());
33
- });
34
-
35
- it("says plainly that the store does not charge online with no waiter to call", async () => {
36
- render(<PaymentsUnavailable waiterAvailable={false} />);
37
-
38
- expect(screen.getByTestId("checkout-payments-disabled")).toBeTruthy();
39
- // No waiter reachable — offering one would be a dead button.
40
- await waitFor(() => expect(screen.queryByTestId("checkout-call-waiter-button")).toBeNull());
41
- });
42
-
43
- it("renders no waiter button when the host supplies no handler", async () => {
44
- render(<PaymentsUnavailable waiterAvailable />);
45
- await waitFor(() => expect(screen.queryByTestId("checkout-call-waiter-button")).toBeNull());
46
- });
47
-
48
- it("disables the waiter button while the request is in flight", () => {
49
- render(<PaymentsUnavailable waiterAvailable onCallWaiter={vi.fn()} calling />);
50
- const button = screen.getByTestId("checkout-call-waiter-button") as HTMLButtonElement;
51
- expect(button.disabled).toBe(true);
52
- });
53
- });