@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.
- package/package.json +13 -5
- package/eslint.config.js +0 -34
- package/src/__tests__/checkout-confirmation.test.tsx +0 -177
- package/src/__tests__/connection-state.test.tsx +0 -84
- package/src/__tests__/context.test.tsx +0 -88
- package/src/__tests__/controlled-provider.test.tsx +0 -116
- package/src/__tests__/credential-confirm.test.tsx +0 -193
- package/src/__tests__/initial-provider.test.tsx +0 -81
- package/src/__tests__/provider-priority-list.test.tsx +0 -159
- package/src/__tests__/provider-status-bar.test.tsx +0 -152
- package/src/__tests__/slugged-provider.test.tsx +0 -108
- package/src/__tests__/verification-slot.test.tsx +0 -125
- package/src/card/tokenize.test.ts +0 -194
- package/src/components/checkout/__tests__/card-3ds-handover.test.tsx +0 -147
- package/src/components/checkout/__tests__/chain-instruments.test.tsx +0 -244
- package/src/components/checkout/__tests__/clear-cart-on-paid.test.tsx +0 -64
- package/src/components/checkout/__tests__/hosted-return.test.ts +0 -109
- package/src/components/checkout/__tests__/method-capability.test.tsx +0 -120
- package/src/components/checkout/__tests__/mint-deadline.test.ts +0 -99
- package/src/components/checkout/__tests__/payments-unavailable.test.tsx +0 -53
- package/src/components/checkout/__tests__/provider-chain.test.ts +0 -161
- package/src/components/checkout/__tests__/save-on-continue.test.tsx +0 -165
- package/src/components/checkout/__tests__/second-host.test.tsx +0 -86
- package/src/components/checkout/__tests__/unresolved-charge.test.tsx +0 -155
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
import { cleanup, render, screen, waitFor } from '@testing-library/react';
|
|
3
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
-
|
|
5
|
-
import type { MerchantSettingsView } from '@12-apps/payments-backend';
|
|
6
|
-
|
|
7
|
-
import type { PaymentsSettingsClient } from '../client';
|
|
8
|
-
import { PaymentProviderSettings } from '../components/PaymentProviderSettings';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* The activation step's slot.
|
|
12
|
-
*
|
|
13
|
-
* "Connected" and "can charge" are different facts, and only the HOST can prove
|
|
14
|
-
* the second — it owns the endpoint that puts a real cent through the store's
|
|
15
|
-
* account. This package's job is narrower and worth pinning: place that step
|
|
16
|
-
* where an owner will actually find it, and tell the host which provider is
|
|
17
|
-
* open and whether it is already live.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
const PROVIDERS = [
|
|
21
|
-
{ name: 'pagbank', displayName: 'PagBank', authMode: 'oauth', credentialSchema: [] },
|
|
22
|
-
];
|
|
23
|
-
|
|
24
|
-
function viewWith(config: Record<string, unknown> | null): MerchantSettingsView {
|
|
25
|
-
return {
|
|
26
|
-
providers: PROVIDERS,
|
|
27
|
-
configs: config ? [config] : [],
|
|
28
|
-
activeProvider: null,
|
|
29
|
-
} as unknown as MerchantSettingsView;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function fakeClient(view: MerchantSettingsView): PaymentsSettingsClient {
|
|
33
|
-
return {
|
|
34
|
-
getSettings: vi.fn().mockResolvedValue(view),
|
|
35
|
-
getSetupGuide: vi.fn().mockResolvedValue(null),
|
|
36
|
-
setEnabled: vi.fn(),
|
|
37
|
-
saveCredentials: vi.fn(),
|
|
38
|
-
} as unknown as PaymentsSettingsClient;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
afterEach(cleanup);
|
|
42
|
-
|
|
43
|
-
describe('PaymentProviderSettings — the verification slot', () => {
|
|
44
|
-
it('is not rendered while the owner is still on the provider list', async () => {
|
|
45
|
-
const renderVerification = vi.fn(() => <div data-testid="slot" />);
|
|
46
|
-
render(
|
|
47
|
-
<PaymentProviderSettings
|
|
48
|
-
client={fakeClient(viewWith(null))}
|
|
49
|
-
renderVerification={renderVerification}
|
|
50
|
-
/>,
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
await screen.findByTestId('payments-provider-picker');
|
|
54
|
-
expect(renderVerification).not.toHaveBeenCalled();
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('hands the host the open provider and its connection state', async () => {
|
|
58
|
-
const renderVerification = vi.fn(() => <div data-testid="slot" />);
|
|
59
|
-
render(
|
|
60
|
-
<PaymentProviderSettings
|
|
61
|
-
client={fakeClient(
|
|
62
|
-
viewWith({ provider: 'pagbank', status: 'VERIFIED', enabled: true, environments: {} }),
|
|
63
|
-
)}
|
|
64
|
-
initialProvider="pagbank"
|
|
65
|
-
renderVerification={renderVerification}
|
|
66
|
-
/>,
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
await screen.findByTestId('slot');
|
|
70
|
-
expect(renderVerification).toHaveBeenCalledWith(
|
|
71
|
-
expect.objectContaining({ provider: 'pagbank', connected: true }),
|
|
72
|
-
);
|
|
73
|
-
/**
|
|
74
|
-
* `enabled` is deliberately withheld. The step used it to render a
|
|
75
|
-
* standing "Provedor ativo" banner, which went on reassuring a store whose
|
|
76
|
-
* every charge was refused — the banner and the refusal were on screen
|
|
77
|
-
* together. The step reports what it observes, nothing else.
|
|
78
|
-
*/
|
|
79
|
-
expect(renderVerification).not.toHaveBeenCalledWith(
|
|
80
|
-
expect.objectContaining({ enabled: expect.anything() }),
|
|
81
|
-
);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* A provider with no stored row yet still renders the slot — the host decides
|
|
86
|
-
* to show nothing. Reporting `connected: false` is what lets it.
|
|
87
|
-
*/
|
|
88
|
-
it('reports a provider with no stored connection as not connected', async () => {
|
|
89
|
-
const renderVerification = vi.fn(() => <div data-testid="slot" />);
|
|
90
|
-
render(
|
|
91
|
-
<PaymentProviderSettings
|
|
92
|
-
client={fakeClient(viewWith(null))}
|
|
93
|
-
initialProvider="pagbank"
|
|
94
|
-
renderVerification={renderVerification}
|
|
95
|
-
/>,
|
|
96
|
-
);
|
|
97
|
-
|
|
98
|
-
await screen.findByTestId('slot');
|
|
99
|
-
expect(renderVerification).toHaveBeenCalledWith(
|
|
100
|
-
expect.objectContaining({ connected: false }),
|
|
101
|
-
);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Placement, not just presence: the step must sit OUTSIDE the manual
|
|
106
|
-
* credentials disclosure. An owner who connected by OAuth is told no key
|
|
107
|
-
* needs copying, so they have no reason to open it — and the step that
|
|
108
|
-
* actually turns the store on would be hidden in there.
|
|
109
|
-
*/
|
|
110
|
-
it('places the step outside the manual-credentials disclosure', async () => {
|
|
111
|
-
render(
|
|
112
|
-
<PaymentProviderSettings
|
|
113
|
-
client={fakeClient(
|
|
114
|
-
viewWith({ provider: 'pagbank', status: 'VERIFIED', enabled: false, environments: {} }),
|
|
115
|
-
)}
|
|
116
|
-
initialProvider="pagbank"
|
|
117
|
-
renderVerification={() => <div data-testid="slot" />}
|
|
118
|
-
/>,
|
|
119
|
-
);
|
|
120
|
-
|
|
121
|
-
const slot = await screen.findByTestId('slot');
|
|
122
|
-
await waitFor(() => expect(screen.queryByTestId('payments-manual-fallback')).toBeNull());
|
|
123
|
-
expect(slot.closest('[data-testid="payments-manual-fallback"]')).toBeNull();
|
|
124
|
-
});
|
|
125
|
-
});
|
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
|
-
|
|
4
|
-
import { tokenizeForCheckout, tokenizerFor } from "./tokenize";
|
|
5
|
-
import type { CardDetails } from "./types";
|
|
6
|
-
|
|
7
|
-
/** A Luhn-valid Visa test PAN (the mock path validates before minting). */
|
|
8
|
-
const VALID_CARD: CardDetails = {
|
|
9
|
-
number: "4111 1111 1111 1111",
|
|
10
|
-
holder: "Ana Compradora",
|
|
11
|
-
expiry: "12/39",
|
|
12
|
-
cvv: "123",
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
/** The always-declines test PAN the mock encodes as `tok_declined…`. */
|
|
16
|
-
const DECLINE_CARD: CardDetails = { ...VALID_CARD, number: "4000 0000 0000 0002" };
|
|
17
|
-
|
|
18
|
-
describe("tokenizeForCheckout — the mock gate (FUT-697)", () => {
|
|
19
|
-
it("NEVER mints a mock token without server-granted stub permission", async () => {
|
|
20
|
-
// The exact production shape of the bug: a Stone/Stripe store resolved to
|
|
21
|
-
// a null PagBank key, and the old fallback minted a fake `tok_…` that
|
|
22
|
-
// entered a real charge. With no `mockTokenization` grant, the answer is
|
|
23
|
-
// a clear bail BEFORE any money moves.
|
|
24
|
-
const result = await tokenizeForCheckout(VALID_CARD, {
|
|
25
|
-
provider: "stripe",
|
|
26
|
-
publicKey: null,
|
|
27
|
-
mockTokenization: false,
|
|
28
|
-
});
|
|
29
|
-
expect(result.ok).toBe(false);
|
|
30
|
-
if (!result.ok) expect(result.error).toContain("indisponível");
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("bails clearly for a provider with no browser scheme, even WITH a key", async () => {
|
|
34
|
-
const result = await tokenizeForCheckout(VALID_CARD, {
|
|
35
|
-
provider: "infinitepay",
|
|
36
|
-
publicKey: "pk_live_123",
|
|
37
|
-
mockTokenization: false,
|
|
38
|
-
});
|
|
39
|
-
expect(result.ok).toBe(false);
|
|
40
|
-
if (!result.ok) expect(result.error).toContain("indisponível");
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("bails clearly when the store has no provider at all", async () => {
|
|
44
|
-
const result = await tokenizeForCheckout(VALID_CARD, {
|
|
45
|
-
provider: null,
|
|
46
|
-
publicKey: null,
|
|
47
|
-
mockTokenization: false,
|
|
48
|
-
});
|
|
49
|
-
expect(result.ok).toBe(false);
|
|
50
|
-
if (!result.ok) expect(result.error).toContain("indisponível");
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("mints the mock token under stub permission (dev / e2e path)", async () => {
|
|
54
|
-
const result = await tokenizeForCheckout(VALID_CARD, {
|
|
55
|
-
provider: "pagbank",
|
|
56
|
-
publicKey: null,
|
|
57
|
-
mockTokenization: true,
|
|
58
|
-
});
|
|
59
|
-
expect(result.ok).toBe(true);
|
|
60
|
-
if (result.ok) {
|
|
61
|
-
expect(result.data.token).toMatch(/^tok/);
|
|
62
|
-
expect(result.data.token).not.toMatch(/^tok_declined/);
|
|
63
|
-
expect(result.data.last4).toBe("1111");
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it("encodes the decline scenario in the mock token", async () => {
|
|
68
|
-
const result = await tokenizeForCheckout(DECLINE_CARD, {
|
|
69
|
-
provider: "pagbank",
|
|
70
|
-
publicKey: null,
|
|
71
|
-
mockTokenization: true,
|
|
72
|
-
});
|
|
73
|
-
expect(result.ok).toBe(true);
|
|
74
|
-
if (result.ok) expect(result.data.token).toMatch(/^tok_declined/);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("still validates the card before minting a mock", async () => {
|
|
78
|
-
const result = await tokenizeForCheckout(
|
|
79
|
-
{ ...VALID_CARD, number: "1234" },
|
|
80
|
-
{ provider: "pagbank", publicKey: null, mockTokenization: true },
|
|
81
|
-
);
|
|
82
|
-
expect(result.ok).toBe(false);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it("routes a keyed PagBank store to the REAL tokenizer — a key beats stub permission", async () => {
|
|
86
|
-
// Stub the SDK so `ensurePagBankSdk` short-circuits (no script injection in
|
|
87
|
-
// the test env). The encrypted blob coming back — never a `tok_…` mock —
|
|
88
|
-
// is the proof that a present key routes to REAL tokenization even when
|
|
89
|
-
// stub permission was granted.
|
|
90
|
-
const pagSeguro = {
|
|
91
|
-
encryptCard: () => ({ hasErrors: false, encryptedCard: "enc-blob-123" }),
|
|
92
|
-
};
|
|
93
|
-
(window as typeof window & { PagSeguro?: typeof pagSeguro }).PagSeguro = pagSeguro;
|
|
94
|
-
try {
|
|
95
|
-
const result = await tokenizeForCheckout(VALID_CARD, {
|
|
96
|
-
provider: "pagbank",
|
|
97
|
-
publicKey: "PUB-KEY",
|
|
98
|
-
mockTokenization: true, // even with stub permission, a key means REAL tokenization
|
|
99
|
-
});
|
|
100
|
-
expect(result.ok).toBe(true);
|
|
101
|
-
if (result.ok) {
|
|
102
|
-
expect(result.data.token).toBe("enc-blob-123");
|
|
103
|
-
expect(result.data.token).not.toMatch(/^tok/);
|
|
104
|
-
}
|
|
105
|
-
} finally {
|
|
106
|
-
delete (window as typeof window & { PagSeguro?: typeof pagSeguro }).PagSeguro;
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
describe("tokenizerFor — scheme is chosen by provider, never by capability", () => {
|
|
112
|
-
it("names each implemented provider's own scheme", () => {
|
|
113
|
-
expect(tokenizerFor("pagbank")).toBe("pagbank-sdk");
|
|
114
|
-
expect(tokenizerFor("stone")).toBe("pagarme-token");
|
|
115
|
-
expect(tokenizerFor("stripe")).toBe("stripe-pm");
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it("answers null for providers with no browser card path", () => {
|
|
119
|
-
expect(tokenizerFor("infinitepay")).toBeNull();
|
|
120
|
-
expect(tokenizerFor("unknown-vendor")).toBeNull();
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
describe("tokenizeForCheckout — the stripe-pm scheme (FUT-698)", () => {
|
|
125
|
-
afterEach(() => {
|
|
126
|
-
vi.unstubAllGlobals();
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
/** Stub fetch once and capture the single call the tokenizer makes. */
|
|
130
|
-
function stubStripe(status: number, body: unknown): { url?: string; init?: RequestInit }[] {
|
|
131
|
-
const calls: { url?: string; init?: RequestInit }[] = [];
|
|
132
|
-
vi.stubGlobal(
|
|
133
|
-
"fetch",
|
|
134
|
-
vi.fn(async (url: RequestInfo | URL, init?: RequestInit) => {
|
|
135
|
-
calls.push({ url: String(url), init });
|
|
136
|
-
return new Response(JSON.stringify(body), { status });
|
|
137
|
-
}),
|
|
138
|
-
);
|
|
139
|
-
return calls;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
it("mints a PaymentMethod at Stripe with the publishable key — the PAN never reaches us", async () => {
|
|
143
|
-
const calls = stubStripe(200, { id: "pm_123", card: { brand: "visa", last4: "1111" } });
|
|
144
|
-
|
|
145
|
-
const result = await tokenizeForCheckout(VALID_CARD, {
|
|
146
|
-
provider: "stripe",
|
|
147
|
-
publicKey: "pk_test_1",
|
|
148
|
-
mockTokenization: false,
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
expect(result.ok).toBe(true);
|
|
152
|
-
if (result.ok) {
|
|
153
|
-
expect(result.data.token).toBe("pm_123");
|
|
154
|
-
expect(result.data.last4).toBe("1111");
|
|
155
|
-
}
|
|
156
|
-
expect(calls[0]?.url).toBe("https://api.stripe.com/v1/payment_methods");
|
|
157
|
-
const headers = calls[0]?.init?.headers as Record<string, string>;
|
|
158
|
-
expect(headers["Authorization"]).toBe("Bearer pk_test_1");
|
|
159
|
-
const form = new URLSearchParams(String(calls[0]?.init?.body));
|
|
160
|
-
expect(form.get("type")).toBe("card");
|
|
161
|
-
expect(form.get("card[number]")).toBe("4111111111111111");
|
|
162
|
-
expect(form.get("card[exp_month]")).toBe("12");
|
|
163
|
-
expect(form.get("card[exp_year]")).toBe("2039");
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it("carries Stripe's refusal verbatim — the provider's answer must reach a human", async () => {
|
|
167
|
-
stubStripe(402, { error: { message: "Your card number is incorrect." } });
|
|
168
|
-
|
|
169
|
-
const result = await tokenizeForCheckout(VALID_CARD, {
|
|
170
|
-
provider: "stripe",
|
|
171
|
-
publicKey: "pk_test_1",
|
|
172
|
-
mockTokenization: false,
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
expect(result.ok).toBe(false);
|
|
176
|
-
if (!result.ok) {
|
|
177
|
-
expect(result.error).toContain("HTTP 402");
|
|
178
|
-
expect(result.error).toContain("Your card number is incorrect.");
|
|
179
|
-
}
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it("a keyed Stripe store routes to the REAL tokenizer — a key beats stub permission", async () => {
|
|
183
|
-
stubStripe(200, { id: "pm_real" });
|
|
184
|
-
|
|
185
|
-
const result = await tokenizeForCheckout(VALID_CARD, {
|
|
186
|
-
provider: "stripe",
|
|
187
|
-
publicKey: "pk_test_1",
|
|
188
|
-
mockTokenization: true, // even with stub permission, a key means REAL tokenization
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
expect(result.ok).toBe(true);
|
|
192
|
-
if (result.ok) expect(result.data.token).toBe("pm_real");
|
|
193
|
-
});
|
|
194
|
-
});
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
/**
|
|
3
|
-
* FUT-698 — "Dado uma loja Stripe, quando o comprador paga com um cartão que
|
|
4
|
-
* exige 3DS, então ele é levado ao desafio" — the browser-unreachable half of
|
|
5
|
-
* that scenario (the stub adapter settles cards inline, so no e2e journey can
|
|
6
|
-
* produce a real challenge). The return half — "e volta com o pedido pago
|
|
7
|
-
* exatamente uma vez" — is the hosted-return machinery, pinned in
|
|
8
|
-
* hosted-return.test.ts and settled server-side by the idempotent webhook path.
|
|
9
|
-
*
|
|
10
|
-
* The charge answers `hostedCheckoutUrl` and the card hook must hand the buyer
|
|
11
|
-
* over exactly as a redirect provider's link does (FUT-556): park the order,
|
|
12
|
-
* navigate, and start NO poll in a tab that is being torn down.
|
|
13
|
-
*/
|
|
14
|
-
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
15
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
16
|
-
|
|
17
|
-
import type { JSX } from "react";
|
|
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 type { CardTokenizationConfig } from "../../../card";
|
|
30
|
-
import type { CheckoutOrder, OrderStatus } from "../types";
|
|
31
|
-
import { useCardCheckout } from "../use-card-checkout";
|
|
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 STRIPE_CONFIG: CardTokenizationConfig = {
|
|
45
|
-
provider: "stripe",
|
|
46
|
-
publicKey: "pk_test_1",
|
|
47
|
-
mockTokenization: false,
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
/** A saved card, so paying skips tokenization and goes straight to the charge. */
|
|
51
|
-
const SAVED_CARD = {
|
|
52
|
-
id: "card-1",
|
|
53
|
-
brand: "visa",
|
|
54
|
-
last4: "4242",
|
|
55
|
-
expMonth: 12,
|
|
56
|
-
expYear: 2033,
|
|
57
|
-
holder: "OLGA STONE",
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
function Harness({ onResolved }: { onResolved: (status: OrderStatus) => void }): JSX.Element {
|
|
61
|
-
const cc = useCardCheckout(ORDER, {}, STRIPE_CONFIG, onResolved, 10, "acme");
|
|
62
|
-
return (
|
|
63
|
-
<div>
|
|
64
|
-
<span data-testid="submitted">{String(cc.submitted)}</span>
|
|
65
|
-
<span data-testid="selection">{cc.selection}</span>
|
|
66
|
-
<button type="button" data-testid="pay" onClick={() => void cc.handlePay()}>
|
|
67
|
-
Pagar
|
|
68
|
-
</button>
|
|
69
|
-
</div>
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** Render, wait for the saved card to be SELECTED, and tap Pagar. */
|
|
74
|
-
async function pay(onResolved = vi.fn()): Promise<void> {
|
|
75
|
-
render(<Harness onResolved={onResolved} />);
|
|
76
|
-
// Selection — not merely the fetch — is what routes handlePay past the
|
|
77
|
-
// empty new-card form; clicking earlier validates that form and stops.
|
|
78
|
-
await waitFor(() => {
|
|
79
|
-
expect(screen.getByTestId("selection").textContent).toBe("card-1");
|
|
80
|
-
});
|
|
81
|
-
fireEvent.click(screen.getByTestId("pay"));
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const assign = vi.fn();
|
|
85
|
-
|
|
86
|
-
beforeEach(() => {
|
|
87
|
-
vi.clearAllMocks();
|
|
88
|
-
window.sessionStorage.clear();
|
|
89
|
-
client.listSavedCards.mockResolvedValue([SAVED_CARD]);
|
|
90
|
-
client.pollOrderStatus.mockResolvedValue({ ok: true, data: "AWAITING_PAYMENT" });
|
|
91
|
-
vi.stubGlobal("location", { search: "", assign });
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
afterEach(() => {
|
|
95
|
-
cleanup();
|
|
96
|
-
vi.unstubAllGlobals();
|
|
97
|
-
window.sessionStorage.clear();
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
describe("card charge → 3DS handover (FUT-698)", () => {
|
|
101
|
-
it("hands the buyer to the challenge page with the order parked for the return trip", async () => {
|
|
102
|
-
client.chargeCard.mockResolvedValue({
|
|
103
|
-
ok: true,
|
|
104
|
-
data: { status: "AWAITING_PAYMENT", hostedCheckoutUrl: "https://hooks.stripe.com/3ds/x" },
|
|
105
|
-
});
|
|
106
|
-
const onResolved = vi.fn();
|
|
107
|
-
|
|
108
|
-
await pay(onResolved);
|
|
109
|
-
|
|
110
|
-
await waitFor(() => {
|
|
111
|
-
expect(assign).toHaveBeenCalledWith("https://hooks.stripe.com/3ds/x");
|
|
112
|
-
});
|
|
113
|
-
// Parked exactly as the redirect-provider handoff parks (FUT-556), so the
|
|
114
|
-
// return trip resumes THIS order's confirmation screen.
|
|
115
|
-
const parked = window.sessionStorage.getItem("futurepay.checkout.hostedOrder");
|
|
116
|
-
expect(parked).not.toBeNull();
|
|
117
|
-
expect((JSON.parse(parked as string) as { orderId: string }).orderId).toBe("o1");
|
|
118
|
-
// The tab is navigating away: nothing resolved, no poll started here.
|
|
119
|
-
expect(onResolved).not.toHaveBeenCalled();
|
|
120
|
-
expect(client.pollOrderStatus).not.toHaveBeenCalled();
|
|
121
|
-
expect(screen.getByTestId("submitted").textContent).toBe("false");
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it("an accepted charge WITHOUT a challenge begins polling instead of navigating", async () => {
|
|
125
|
-
client.chargeCard.mockResolvedValue({ ok: true, data: { status: "AWAITING_PAYMENT" } });
|
|
126
|
-
|
|
127
|
-
await pay();
|
|
128
|
-
|
|
129
|
-
await waitFor(() => {
|
|
130
|
-
expect(screen.getByTestId("submitted").textContent).toBe("true");
|
|
131
|
-
});
|
|
132
|
-
expect(assign).not.toHaveBeenCalled();
|
|
133
|
-
expect(window.sessionStorage.getItem("futurepay.checkout.hostedOrder")).toBeNull();
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it("a decline resolves to the status screen, never a handover", async () => {
|
|
137
|
-
client.chargeCard.mockResolvedValue({ ok: true, data: { status: "FAILED" } });
|
|
138
|
-
const onResolved = vi.fn();
|
|
139
|
-
|
|
140
|
-
await pay(onResolved);
|
|
141
|
-
|
|
142
|
-
await waitFor(() => {
|
|
143
|
-
expect(onResolved).toHaveBeenCalledWith("FAILED");
|
|
144
|
-
});
|
|
145
|
-
expect(assign).not.toHaveBeenCalled();
|
|
146
|
-
});
|
|
147
|
-
});
|