@paypal/checkout-components 5.0.405-alpha-c03e09a.0 → 5.0.405

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.
@@ -2,7 +2,7 @@
2
2
  import { describe, expect, it, beforeEach, vi } from "vitest";
3
3
  import { FUNDING } from "@paypal/sdk-constants";
4
4
 
5
- import { BUTTON_COLOR, BUTTON_FLOW } from "../../constants";
5
+ import { BUTTON_COLOR } from "../../constants";
6
6
 
7
7
  import {
8
8
  getButtonColor,
@@ -14,7 +14,6 @@ import {
14
14
  hasInvalidScriptOptionsForFullRedesign,
15
15
  determineRandomButtonColor,
16
16
  getColorABTestFromStorage,
17
- getCobrandedBNPLLabelFlags,
18
17
  } from "./props";
19
18
 
20
19
  describe("getColorABTestFromStorage", () => {
@@ -865,98 +864,3 @@ describe("HideSubmitButtonProps type validation", () => {
865
864
  expect(validButtonProps.hideSubmitButtonForCardForm).toBe(false);
866
865
  });
867
866
  });
868
-
869
- describe("getCobrandedBNPLLabelFlags", () => {
870
- // $FlowFixMe - test object intentionally omits non-relevant ButtonPropsInputs fields
871
- const eligibleProps = {
872
- fundingSource: FUNDING.PAYPAL,
873
- fundingEligibility: {
874
- paylater: { eligible: true },
875
- },
876
- experiment: { isPaylaterCobrandedLabelEnabled: true },
877
- locale: { lang: "en", country: "US" },
878
- style: {},
879
- flow: BUTTON_FLOW.PURCHASE,
880
- };
881
-
882
- it("should return true when all conditions are met", () => {
883
- const { isPayNowOrLaterLabelEligible, shouldApplyPayNowOrLaterLabel } =
884
- // $FlowFixMe
885
- getCobrandedBNPLLabelFlags(eligibleProps);
886
-
887
- expect(isPayNowOrLaterLabelEligible).toBe(true);
888
- expect(shouldApplyPayNowOrLaterLabel).toBe(true);
889
- });
890
-
891
- it("should return false when experiment flag is disabled", () => {
892
- const { isPayNowOrLaterLabelEligible } =
893
- // $FlowFixMe
894
- getCobrandedBNPLLabelFlags({
895
- ...eligibleProps,
896
- experiment: { isPaylaterCobrandedLabelEnabled: false },
897
- });
898
-
899
- expect(isPayNowOrLaterLabelEligible).toBe(false);
900
- });
901
-
902
- it("should return false when paylater is not eligible", () => {
903
- const { isPayNowOrLaterLabelEligible } =
904
- // $FlowFixMe
905
- getCobrandedBNPLLabelFlags({
906
- ...eligibleProps,
907
- fundingEligibility: { paylater: { eligible: false } },
908
- });
909
-
910
- expect(isPayNowOrLaterLabelEligible).toBe(false);
911
- });
912
-
913
- it("should return false when fundingSource is not PAYPAL or undefined", () => {
914
- const { isPayNowOrLaterLabelEligible } =
915
- // $FlowFixMe
916
- getCobrandedBNPLLabelFlags({
917
- ...eligibleProps,
918
- fundingSource: FUNDING.VENMO,
919
- });
920
-
921
- expect(isPayNowOrLaterLabelEligible).toBe(false);
922
- });
923
-
924
- it("should return false when a non-paypal label is set", () => {
925
- const { isPayNowOrLaterLabelEligible } =
926
- // $FlowFixMe
927
- getCobrandedBNPLLabelFlags({
928
- ...eligibleProps,
929
- style: { label: "checkout" },
930
- });
931
-
932
- expect(isPayNowOrLaterLabelEligible).toBe(false);
933
- });
934
-
935
- it("should return true when label is explicitly set to paypal", () => {
936
- const { isPayNowOrLaterLabelEligible } =
937
- // $FlowFixMe
938
- getCobrandedBNPLLabelFlags({
939
- ...eligibleProps,
940
- style: { label: "paypal" },
941
- });
942
-
943
- expect(isPayNowOrLaterLabelEligible).toBe(true);
944
- });
945
-
946
- it("should return false when locale does not have PayNowOrLater content", () => {
947
- const { isPayNowOrLaterLabelEligible } =
948
- // $FlowFixMe
949
- getCobrandedBNPLLabelFlags({
950
- ...eligibleProps,
951
- locale: { lang: "fr", country: "FR" },
952
- });
953
-
954
- expect(isPayNowOrLaterLabelEligible).toBe(false);
955
- });
956
-
957
- it("should return false when props is null", () => {
958
- const { isPayNowOrLaterLabelEligible } = getCobrandedBNPLLabelFlags(null);
959
-
960
- expect(isPayNowOrLaterLabelEligible).toBe(false);
961
- });
962
- });
@@ -0,0 +1,46 @@
1
+ /* @flow */
2
+ /**
3
+ * Unit tests for QR Code component creation and configuration.
4
+ * Verifies component initialization, memoization, and proper setup of SDK dependencies.
5
+ */
6
+
7
+ import { describe, expect, test, vi } from "vitest";
8
+
9
+ import { getQRCodeComponent } from "./component";
10
+
11
+ vi.mock("@paypal/sdk-client/src", () => ({
12
+ getLogger: vi.fn(() => ({
13
+ metric: vi.fn().mockReturnThis(),
14
+ error: vi.fn().mockReturnThis(),
15
+ track: vi.fn().mockReturnThis(),
16
+ flush: vi.fn().mockReturnThis(),
17
+ metricCounter: vi.fn().mockReturnThis(),
18
+ })),
19
+ getPayPalDomainRegex: vi.fn(() => /paypal\.com/),
20
+ getPayPalDomain: vi.fn(() => "https://www.paypal.com"),
21
+ getCSPNonce: vi.fn(() => "mock-nonce"),
22
+ getSDKMeta: vi.fn(() => "mock-sdk-meta"),
23
+ getDebug: vi.fn(() => false),
24
+ getEnv: vi.fn(() => "test"),
25
+ getSessionID: vi.fn(() => "mock-session-id"),
26
+ getLocale: vi.fn(() => ({ country: "US", lang: "en" })),
27
+ getClientID: vi.fn(() => "mock-client-id"),
28
+ getCorrelationID: vi.fn(() => "mock-correlation-id"),
29
+ getBuyerCountry: vi.fn(() => "US"),
30
+ }));
31
+
32
+ describe("getQRCodeComponent", () => {
33
+ test("should create Zoid component", () => {
34
+ const component = getQRCodeComponent();
35
+
36
+ expect(component).toBeDefined();
37
+ expect(typeof component).toBe("function");
38
+ });
39
+
40
+ test("should memoize component instance", () => {
41
+ const component1 = getQRCodeComponent();
42
+ const component2 = getQRCodeComponent();
43
+
44
+ expect(component1).toBe(component2);
45
+ });
46
+ });
@@ -4,6 +4,7 @@
4
4
  import { destroyElement, type EventEmitterType } from "@krakenjs/belter/src";
5
5
  import { EVENT, type RenderOptionsType } from "@krakenjs/zoid/src";
6
6
  import { node, dom, type ChildType } from "@krakenjs/jsx-pragmatic/src";
7
+ import { getCSPNonce } from "@paypal/sdk-client/src";
7
8
 
8
9
  import { type QRCodeProps } from "./types";
9
10
 
@@ -113,7 +114,6 @@ export function QRCodeContainer({
113
114
  export function containerTemplate({
114
115
  frame,
115
116
  prerenderFrame,
116
- props,
117
117
  doc,
118
118
  uid,
119
119
  event,
@@ -122,7 +122,7 @@ export function containerTemplate({
122
122
  return;
123
123
  }
124
124
 
125
- const { cspNonce } = props;
125
+ const cspNonce = __WEB__ ? getCSPNonce() : undefined;
126
126
 
127
127
  return (
128
128
  <QRCodeContainer
@@ -0,0 +1,187 @@
1
+ /* @flow */
2
+ /** @jsx node */
3
+ /**
4
+ * Unit tests for QR Code container template and component.
5
+ * Tests CSP nonce handling, frame validation, and visibility state management
6
+ * for both web and non-web environments.
7
+ */
8
+
9
+ import { describe, expect, test, vi, beforeEach, afterEach } from "vitest";
10
+ import { dom } from "@krakenjs/jsx-pragmatic/src";
11
+ import { getCSPNonce } from "@paypal/sdk-client/src";
12
+
13
+ import { containerTemplate, QRCodeContainer } from "./container";
14
+
15
+ const TEST_UID = "test-uid-12345";
16
+ const TEST_NONCE = "test-csp-nonce-xyz";
17
+
18
+ vi.mock("@paypal/sdk-client/src", () => ({
19
+ getCSPNonce: vi.fn(),
20
+ }));
21
+
22
+ const createMocks = () => ({
23
+ frame: document.createElement("iframe"),
24
+ prerenderFrame: document.createElement("iframe"),
25
+ // $FlowFixMe - mock event emitter for tests
26
+ event: {
27
+ on: vi.fn(),
28
+ trigger: vi.fn(),
29
+ once: vi.fn(),
30
+ reset: vi.fn(),
31
+ triggerOnce: vi.fn(),
32
+ },
33
+ });
34
+
35
+ const originalWebValue = global.__WEB__;
36
+
37
+ const setupTest = () => {
38
+ vi.clearAllMocks();
39
+ global.__WEB__ = true;
40
+ };
41
+
42
+ const teardownTest = () => {
43
+ global.__WEB__ = originalWebValue;
44
+ };
45
+
46
+ describe("containerTemplate", () => {
47
+ beforeEach(setupTest);
48
+ afterEach(teardownTest);
49
+
50
+ test.each([[true], [false]])(
51
+ "when __WEB__ is %s, should call getCSPNonce conditionally",
52
+ (webValue) => {
53
+ global.__WEB__ = webValue;
54
+ // $FlowIssue - mock return value
55
+ getCSPNonce.mockReturnValue(TEST_NONCE);
56
+
57
+ const { frame, prerenderFrame, event } = createMocks();
58
+ // $FlowIssue - test mock parameters
59
+ const result = containerTemplate({
60
+ frame,
61
+ prerenderFrame,
62
+ doc: document,
63
+ uid: TEST_UID,
64
+ event,
65
+ // $FlowIssue - test mock parameters
66
+ props: {},
67
+ });
68
+
69
+ if (webValue) {
70
+ expect(getCSPNonce).toHaveBeenCalled();
71
+ // Verify nonce value is available for use
72
+ expect(getCSPNonce).toHaveReturnedWith(TEST_NONCE);
73
+ expect(result).toBeDefined();
74
+ } else {
75
+ expect(getCSPNonce).not.toHaveBeenCalled();
76
+ }
77
+ }
78
+ );
79
+
80
+ test.each([
81
+ [
82
+ "frame",
83
+ { frame: null, prerenderFrame: document.createElement("iframe") },
84
+ ],
85
+ [
86
+ "prerenderFrame",
87
+ { frame: document.createElement("iframe"), prerenderFrame: null },
88
+ ],
89
+ ])("should return undefined when %s is missing", (_paramName, frames) => {
90
+ // $FlowIssue - test props
91
+ const result = containerTemplate({
92
+ ...frames,
93
+ doc: document,
94
+ uid: TEST_UID,
95
+ event: createMocks().event,
96
+ props: {},
97
+ });
98
+ expect(result).toBeUndefined();
99
+ });
100
+
101
+ test("should apply cspNonce to style element in rendered output", () => {
102
+ // $FlowIssue - mock return value
103
+ getCSPNonce.mockReturnValue(TEST_NONCE);
104
+
105
+ const { frame, prerenderFrame, event } = createMocks();
106
+ // $FlowIssue - test mock parameters
107
+ const result = containerTemplate({
108
+ frame,
109
+ prerenderFrame,
110
+ doc: document,
111
+ uid: TEST_UID,
112
+ event,
113
+ // $FlowIssue - test mock parameters
114
+ props: {},
115
+ });
116
+
117
+ // $FlowFixMe - result is HTMLElement in test context
118
+ const styleElement = result.querySelector("style");
119
+ expect(styleElement?.getAttribute("nonce")).toBe(TEST_NONCE);
120
+ });
121
+ });
122
+
123
+ describe("QRCodeContainer", () => {
124
+ beforeEach(setupTest);
125
+ afterEach(teardownTest);
126
+
127
+ test.each([
128
+ ["frame", (mocks) => ({ ...mocks, frame: null })],
129
+ ["prerenderFrame", (mocks) => ({ ...mocks, prerenderFrame: null })],
130
+ ])("should throw error when %s is missing", (_paramName, modifyMocks) => {
131
+ const mocks = createMocks();
132
+ // $FlowIssue - intentionally passing null for error test
133
+ expect(() =>
134
+ QRCodeContainer({ uid: TEST_UID, ...modifyMocks(mocks) })
135
+ ).toThrow("Expected frame and prerenderframe");
136
+ });
137
+
138
+ test("should set up visibility classes for prerender transition", () => {
139
+ // Initial state: prerenderFrame visible, component frame invisible
140
+ const { frame, prerenderFrame, event } = createMocks();
141
+ QRCodeContainer({ uid: TEST_UID, frame, prerenderFrame, event });
142
+
143
+ // Verify component frame is initially hidden
144
+ expect(frame.classList.contains("component-frame")).toBe(true);
145
+ expect(frame.classList.contains("invisible")).toBe(true);
146
+
147
+ // Verify prerender frame is initially visible
148
+ expect(prerenderFrame.classList.contains("prerender-frame")).toBe(true);
149
+ expect(prerenderFrame.classList.contains("visible")).toBe(true);
150
+ });
151
+
152
+ test("should toggle frame visibility when EVENT.RENDERED fires", () => {
153
+ const mocks = createMocks();
154
+ QRCodeContainer({ uid: TEST_UID, ...mocks });
155
+
156
+ // Extract and invoke the registered handler
157
+ // $FlowFixMe - accessing vitest mock properties
158
+ const handler = mocks.event.on.mock.calls[0][1];
159
+ handler();
160
+
161
+ // Verify visibility toggle: prerenderFrame becomes invisible
162
+ expect(mocks.prerenderFrame.classList.contains("invisible")).toBe(true);
163
+ expect(mocks.prerenderFrame.classList.contains("visible")).toBe(false);
164
+
165
+ // Verify visibility toggle: component frame becomes visible
166
+ expect(mocks.frame.classList.contains("visible")).toBe(true);
167
+ expect(mocks.frame.classList.contains("invisible")).toBe(false);
168
+ });
169
+
170
+ test("should pass cspNonce to QRCodeContainer and apply to rendered style", () => {
171
+ const { frame, prerenderFrame, event } = createMocks();
172
+ const result = QRCodeContainer({
173
+ uid: TEST_UID,
174
+ frame,
175
+ prerenderFrame,
176
+ event,
177
+ cspNonce: TEST_NONCE,
178
+ });
179
+
180
+ // Render to DOM to access style element
181
+ // $FlowFixMe - result is ChildType with render method
182
+ const rendered = result.render(dom({ doc: document }));
183
+ const styleElement = rendered.querySelector("style");
184
+
185
+ expect(styleElement?.getAttribute("nonce")).toBe(TEST_NONCE);
186
+ });
187
+ });
@@ -4,14 +4,16 @@
4
4
  import { type RenderOptionsType } from "@krakenjs/zoid/src";
5
5
  import { node, dom } from "@krakenjs/jsx-pragmatic/src";
6
6
  import { SpinnerPage } from "@paypal/common-components/src";
7
+ import { getCSPNonce } from "@paypal/sdk-client/src";
7
8
 
8
9
  import { type QRCodeProps } from "./types";
9
10
 
10
11
  export function prerenderTemplate({
11
12
  doc,
12
- props,
13
13
  close,
14
14
  }: RenderOptionsType<QRCodeProps>): ?HTMLElement {
15
+ const cspNonce = __WEB__ ? getCSPNonce() : undefined;
16
+
15
17
  const style = `
16
18
  #close {
17
19
  position: absolute;
@@ -42,11 +44,9 @@ export function prerenderTemplate({
42
44
  `;
43
45
 
44
46
  const children = [
45
- <style nonce={props.cspNonce} innerHTML={style} />,
47
+ <style nonce={cspNonce} innerHTML={style} />,
46
48
  <a href="#" id="close" aria-label="close" role="button" onClick={close} />,
47
49
  ];
48
50
 
49
- return new SpinnerPage({ nonce: props.cspNonce }, children).render(
50
- dom({ doc })
51
- );
51
+ return new SpinnerPage({ nonce: cspNonce }, children).render(dom({ doc }));
52
52
  }
@@ -0,0 +1,108 @@
1
+ /* @flow */
2
+ /** @jsx node */
3
+ /**
4
+ * Unit tests for QR Code prerender template.
5
+ * Tests CSP nonce propagation to SpinnerPage and style elements
6
+ * in both web and non-web environments.
7
+ */
8
+
9
+ import { describe, expect, test, vi, beforeEach, afterEach } from "vitest";
10
+ import { getCSPNonce } from "@paypal/sdk-client/src";
11
+
12
+ import { prerenderTemplate } from "./prerender";
13
+
14
+ const TEST_NONCE = "prerender-nonce-abc123";
15
+
16
+ vi.mock("@paypal/sdk-client/src", () => ({
17
+ getCSPNonce: vi.fn(),
18
+ }));
19
+
20
+ let capturedSpinnerPageProps = null;
21
+ let capturedChildren = null;
22
+
23
+ vi.mock("@paypal/common-components/src", () => ({
24
+ SpinnerPage: vi.fn().mockImplementation((props, children) => {
25
+ capturedSpinnerPageProps = props;
26
+ capturedChildren = children;
27
+ return {
28
+ render: vi.fn(() => document.createElement("div")),
29
+ };
30
+ }),
31
+ }));
32
+
33
+ const originalWebValue = global.__WEB__;
34
+
35
+ const setupTest = () => {
36
+ vi.clearAllMocks();
37
+ capturedSpinnerPageProps = null;
38
+ capturedChildren = null;
39
+ global.__WEB__ = true;
40
+ };
41
+
42
+ const teardownTest = () => {
43
+ global.__WEB__ = originalWebValue;
44
+ };
45
+
46
+ describe("prerenderTemplate", () => {
47
+ beforeEach(setupTest);
48
+ afterEach(teardownTest);
49
+
50
+ test.each([
51
+ [true, TEST_NONCE],
52
+ [false, undefined],
53
+ ])(
54
+ "when __WEB__ is %s, should pass nonce %s to SpinnerPage",
55
+ (webValue, expectedNonce) => {
56
+ global.__WEB__ = webValue;
57
+ // $FlowIssue - mock return value
58
+ getCSPNonce.mockReturnValue(TEST_NONCE);
59
+
60
+ // $FlowIssue - test mock parameters
61
+ const result = prerenderTemplate({
62
+ doc: document,
63
+ close: vi.fn(),
64
+ // $FlowIssue - test mock parameters
65
+ props: {},
66
+ });
67
+
68
+ if (webValue) {
69
+ // Verify getCSPNonce is called in web environment
70
+ expect(getCSPNonce).toHaveBeenCalled();
71
+ expect(getCSPNonce).toHaveReturnedWith(TEST_NONCE);
72
+ } else {
73
+ // Verify getCSPNonce is NOT called in non-web environment
74
+ expect(getCSPNonce).not.toHaveBeenCalled();
75
+ }
76
+
77
+ // Verify SpinnerPage receives correct nonce prop
78
+ expect(result).toBeDefined();
79
+ expect(capturedSpinnerPageProps).toEqual({ nonce: expectedNonce });
80
+ }
81
+ );
82
+
83
+ test("should apply cspNonce to style element in children", () => {
84
+ // $FlowIssue - mock return value
85
+ getCSPNonce.mockReturnValue(TEST_NONCE);
86
+
87
+ // $FlowIssue - test mock parameters
88
+ prerenderTemplate({ doc: document, close: vi.fn(), props: {} });
89
+
90
+ expect(capturedChildren?.length).toBe(2);
91
+ // $FlowFixMe - capturedChildren is set by mock
92
+ expect(capturedChildren[0]?.props?.nonce).toBe(TEST_NONCE);
93
+ });
94
+
95
+ test("should wire up close handler to close button", () => {
96
+ const mockClose = vi.fn();
97
+ // $FlowIssue - test mock parameters
98
+ prerenderTemplate({ doc: document, close: mockClose, props: {} });
99
+
100
+ // Verify second child is close button with correct props
101
+ expect(capturedChildren?.length).toBe(2);
102
+ // $FlowFixMe - capturedChildren is set by mock
103
+ const closeButton = capturedChildren[1];
104
+ expect(closeButton?.props?.onClick).toBe(mockClose);
105
+ expect(closeButton?.props?.id).toBe("close");
106
+ expect(closeButton?.props["aria-label"]).toBe("close");
107
+ });
108
+ });