11import { beforeEach , describe , expect , test , vi } from "vitest" ;
22
33import { loadCoreSdkScript } from "./index" ;
4- import { insertScriptElement , type ScriptElement } from "../utils" ;
5-
6- vi . mock ( "../utils" , async ( ) => {
7- const actual = await vi . importActual < typeof import ( "../utils" ) > ( "../utils" ) ;
8- return {
9- ...actual ,
10- // default mock for insertScriptElement
11- insertScriptElement : vi
12- . fn ( )
13- . mockImplementation ( ( { onSuccess } : ScriptElement ) => {
14- vi . stubGlobal ( "paypal" , { version : "6" } ) ;
15- process . nextTick ( ( ) => onSuccess ( ) ) ;
16- } ) ,
17- } ;
18- } ) ;
19-
20- const mockedInsertScriptElement = vi . mocked ( insertScriptElement ) ;
214
225describe ( "loadCoreSdkScript()" , ( ) => {
6+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
7+ let scriptAppendChildSpy : any ;
8+
239 beforeEach ( ( ) => {
2410 document . head . innerHTML = "" ;
2511 vi . clearAllMocks ( ) ;
12+ vi . unstubAllGlobals ( ) ;
13+
14+ scriptAppendChildSpy = vi
15+ . spyOn ( document . head , "appendChild" )
16+ . mockImplementation ( ( node ) => {
17+ if ( node instanceof HTMLScriptElement ) {
18+ const namespace =
19+ node . getAttribute ( "data-namespace" ) ?? "paypal" ;
20+ vi . stubGlobal ( namespace , { version : "6" } ) ;
21+ process . nextTick ( ( ) =>
22+ node . dispatchEvent ( new Event ( "load" ) ) ,
23+ ) ;
24+ }
25+ return node ;
26+ } ) ;
2627 } ) ;
2728
2829 test ( "should default to using the sandbox environment" , async ( ) => {
29- await loadCoreSdkScript ( ) ;
30- expect ( mockedInsertScriptElement . mock . calls [ 0 ] [ 0 ] . url ) . toEqual (
30+ const result = await loadCoreSdkScript ( ) ;
31+ expect ( scriptAppendChildSpy ) . toHaveBeenCalledTimes ( 1 ) ;
32+ const scriptElement = scriptAppendChildSpy . mock . calls [ 0 ] [ 0 ] ;
33+ expect ( scriptElement . src ) . toBe (
3134 "https://www.sandbox.paypal.com/web-sdk/v6/core" ,
3235 ) ;
33- expect ( mockedInsertScriptElement ) . toHaveBeenCalledTimes ( 1 ) ;
36+ expect ( scriptElement . getAttribute ( "data-loading-state" ) ) . toBe (
37+ "resolved" ,
38+ ) ;
39+ expect ( result ) . toBeDefined ( ) ;
40+ expect ( window . paypal ) . toBeDefined ( ) ;
3441 } ) ;
3542
3643 test ( "should support options for using production environment" , async ( ) => {
37- await loadCoreSdkScript ( { environment : "production" } ) ;
38- expect ( mockedInsertScriptElement . mock . calls [ 0 ] [ 0 ] . url ) . toEqual (
44+ const result = await loadCoreSdkScript ( { environment : "production" } ) ;
45+ expect ( scriptAppendChildSpy ) . toHaveBeenCalledTimes ( 1 ) ;
46+ const scriptElement = scriptAppendChildSpy . mock . calls [ 0 ] [ 0 ] ;
47+ expect ( scriptElement . src ) . toBe (
3948 "https://www.paypal.com/web-sdk/v6/core" ,
4049 ) ;
41- expect ( mockedInsertScriptElement ) . toHaveBeenCalledTimes ( 1 ) ;
50+ expect ( scriptElement . getAttribute ( "data-loading-state" ) ) . toBe (
51+ "resolved" ,
52+ ) ;
53+ expect ( result ) . toBeDefined ( ) ;
54+ expect ( window . paypal ) . toBeDefined ( ) ;
4255 } ) ;
4356
4457 test ( "should support enabling debugging" , async ( ) => {
45- await loadCoreSdkScript ( { debug : true } ) ;
46- expect ( mockedInsertScriptElement . mock . calls [ 0 ] [ 0 ] . url ) . toEqual (
58+ const result = await loadCoreSdkScript ( { debug : true } ) ;
59+ expect ( scriptAppendChildSpy ) . toHaveBeenCalledTimes ( 1 ) ;
60+ const scriptElement = scriptAppendChildSpy . mock . calls [ 0 ] [ 0 ] ;
61+ expect ( scriptElement . src ) . toBe (
4762 "https://www.sandbox.paypal.com/web-sdk/v6/core?debug=true" ,
4863 ) ;
49- expect ( mockedInsertScriptElement ) . toHaveBeenCalledTimes ( 1 ) ;
64+ expect ( scriptElement . getAttribute ( "data-loading-state" ) ) . toBe (
65+ "resolved" ,
66+ ) ;
67+ expect ( result ) . toBeDefined ( ) ;
68+ expect ( window . paypal ) . toBeDefined ( ) ;
69+ } ) ;
70+
71+ test ( "should avoid inserting two script elements when called twice sequentially" , async ( ) => {
72+ const result1 = await loadCoreSdkScript ( ) ;
73+ const result2 = await loadCoreSdkScript ( ) ;
74+ // should only insert the script once
75+ // the existing loaded window.paypal reference is returned on the second call
76+ expect ( scriptAppendChildSpy ) . toHaveBeenCalledTimes ( 1 ) ;
77+ const scriptElement = scriptAppendChildSpy . mock . calls [ 0 ] [ 0 ] ;
78+ expect ( scriptElement . src ) . toBe (
79+ "https://www.sandbox.paypal.com/web-sdk/v6/core" ,
80+ ) ;
81+ expect ( scriptElement . getAttribute ( "data-loading-state" ) ) . toBe (
82+ "resolved" ,
83+ ) ;
84+ expect ( result1 ) . toBeDefined ( ) ;
85+ expect ( result2 ) . toBeDefined ( ) ;
86+ expect ( result1 ) . toBe ( result2 ) ;
87+ expect ( window . paypal ) . toBeDefined ( ) ;
88+ } ) ;
89+
90+ test ( "should avoid inserting two script elements when called twice in parallel" , async ( ) => {
91+ const [ result1 , result2 ] = await Promise . all ( [
92+ loadCoreSdkScript ( ) ,
93+ loadCoreSdkScript ( ) ,
94+ ] ) ;
95+ // should only insert the script once
96+ expect ( scriptAppendChildSpy ) . toHaveBeenCalledTimes ( 1 ) ;
97+ const scriptElement = scriptAppendChildSpy . mock . calls [ 0 ] [ 0 ] ;
98+ expect ( scriptElement . src ) . toBe (
99+ "https://www.sandbox.paypal.com/web-sdk/v6/core" ,
100+ ) ;
101+ expect ( scriptElement . getAttribute ( "data-loading-state" ) ) . toBe (
102+ "resolved" ,
103+ ) ;
104+ expect ( result1 ) . toBeDefined ( ) ;
105+ expect ( result2 ) . toBeDefined ( ) ;
106+ expect ( result1 ) . toBe ( result2 ) ;
107+ expect ( window . paypal ) . toBeDefined ( ) ;
108+ } ) ;
109+
110+ test ( "should return reference to existing script when loading state is pending" , async ( ) => {
111+ document . head . innerHTML = `<script src="https://www.sandbox.paypal.com/web-sdk/v6/core" data-loading-state="pending"></script>` ;
112+ const loadCoreSdkScriptReference = loadCoreSdkScript ( ) ;
113+
114+ process . nextTick ( ( ) => {
115+ vi . stubGlobal ( "paypal" , { version : "6" } ) ;
116+ document
117+ . querySelector ( 'script[src*="/web-sdk/v6/core"]' ) !
118+ . dispatchEvent ( new Event ( "load" ) ) ;
119+ } ) ;
120+
121+ const result = await loadCoreSdkScriptReference ;
122+
123+ // should NOT insert the script since it already exists in the DOM in pending state
124+ expect ( scriptAppendChildSpy ) . toHaveBeenCalledTimes ( 0 ) ;
125+ expect (
126+ document
127+ . querySelector ( 'script[src*="/web-sdk/v6/core"]' ) !
128+ . getAttribute ( "data-loading-state" ) ,
129+ ) . toBe ( "resolved" ) ;
130+ expect ( result ) . toBeDefined ( ) ;
131+ expect ( window . paypal ) . toBeDefined ( ) ;
132+ } ) ;
133+
134+ test ( "should reject when the script fails to load" , async ( ) => {
135+ vi . spyOn ( document . head , "appendChild" ) . mockImplementationOnce (
136+ ( node ) => {
137+ process . nextTick ( ( ) => node . dispatchEvent ( new Event ( "error" ) ) ) ;
138+ return node ;
139+ } ,
140+ ) ;
141+
142+ expect ( async ( ) => {
143+ await loadCoreSdkScript ( ) ;
144+ } ) . rejects . toThrowError (
145+ 'The script "https://www.sandbox.paypal.com/web-sdk/v6/core" failed to load. Check the HTTP status code and response body in DevTools to learn more.' ,
146+ ) ;
147+ } ) ;
148+
149+ test ( "should error due to unvalid input" , async ( ) => {
150+ expect ( async ( ) => {
151+ // @ts -expect-error invalid arguments
152+ await loadCoreSdkScript ( 123 ) ;
153+ } ) . rejects . toThrowError ( "Expected an options object" ) ;
154+
155+ expect ( async ( ) => {
156+ // @ts -expect-error invalid arguments
157+ await loadCoreSdkScript ( { environment : "bad_value" } ) ;
158+ } ) . rejects . toThrowError (
159+ 'The "environment" option must be either "production" or "sandbox"' ,
160+ ) ;
50161 } ) ;
51162
52163 describe ( "dataNamespace option" , ( ) => {
53164 test ( "should support custom data-namespace attribute" , async ( ) => {
54165 const customNamespace = "myCustomNamespace" ;
55166
56- // Update mock to set the custom namespace instead of window.paypal
57- mockedInsertScriptElement . mockImplementationOnce (
58- ( { onSuccess } : ScriptElement ) => {
59- vi . stubGlobal ( customNamespace , { version : "6" } ) ;
60- process . nextTick ( ( ) => onSuccess ( ) ) ;
61- } ,
62- ) ;
63-
64167 const result = await loadCoreSdkScript ( {
65168 dataNamespace : customNamespace ,
66169 } ) ;
67170
68- expect ( mockedInsertScriptElement . mock . calls [ 0 ] [ 0 ] . url ) . toEqual (
171+ expect ( scriptAppendChildSpy ) . toHaveBeenCalledTimes ( 1 ) ;
172+ const scriptElement = scriptAppendChildSpy . mock . calls [ 0 ] [ 0 ] ;
173+ expect ( scriptElement . src ) . toBe (
69174 "https://www.sandbox.paypal.com/web-sdk/v6/core" ,
70175 ) ;
71- expect (
72- mockedInsertScriptElement . mock . calls [ 0 ] [ 0 ] . attributes ,
73- ) . toEqual ( {
74- "data-namespace" : customNamespace ,
75- } ) ;
76- expect ( mockedInsertScriptElement ) . toHaveBeenCalledTimes ( 1 ) ;
176+
177+ expect ( scriptElement . getAttribute ( "data-namespace" ) ) . toBe (
178+ customNamespace ,
179+ ) ;
180+
77181 expect ( result ) . toBeDefined ( ) ;
182+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
183+ expect ( window [ customNamespace as any ] ) . toBeDefined ( ) ;
78184 } ) ;
79185
80186 test ( "should error when dataNamespace is an empty string" , async ( ) => {
@@ -102,16 +208,18 @@ describe("loadCoreSdkScript()", () => {
102208 dataSdkIntegrationSource : integrationSource ,
103209 } ) ;
104210
105- expect ( mockedInsertScriptElement . mock . calls [ 0 ] [ 0 ] . url ) . toEqual (
211+ expect ( scriptAppendChildSpy ) . toHaveBeenCalledTimes ( 1 ) ;
212+ const scriptElement = scriptAppendChildSpy . mock . calls [ 0 ] [ 0 ] ;
213+ expect ( scriptElement . src ) . toBe (
106214 "https://www.sandbox.paypal.com/web-sdk/v6/core" ,
107215 ) ;
216+
108217 expect (
109- mockedInsertScriptElement . mock . calls [ 0 ] [ 0 ] . attributes ,
110- ) . toEqual ( {
111- "data-sdk-integration-source" : integrationSource ,
112- } ) ;
113- expect ( mockedInsertScriptElement ) . toHaveBeenCalledTimes ( 1 ) ;
218+ scriptElement . getAttribute ( "data-sdk-integration-source" ) ,
219+ ) . toBe ( integrationSource ) ;
220+
114221 expect ( result ) . toBeDefined ( ) ;
222+ expect ( window . paypal ) . toBeDefined ( ) ;
115223 } ) ;
116224
117225 test ( "should error when dataSdkIntegrationSource is an empty string" , async ( ) => {
@@ -130,26 +238,4 @@ describe("loadCoreSdkScript()", () => {
130238 ) ;
131239 } ) ;
132240 } ) ;
133-
134- test ( "should return PayPal namespace with version property" , async ( ) => {
135- const result = await loadCoreSdkScript ( ) ;
136- expect ( result ) . toBeDefined ( ) ;
137- expect ( result ?. version ) . toBeDefined ( ) ;
138- expect ( result ?. version ) . toBe ( "6" ) ;
139- expect ( typeof result ?. version ) . toBe ( "string" ) ;
140- } ) ;
141-
142- test ( "should error due to unvalid input" , async ( ) => {
143- expect ( async ( ) => {
144- // @ts -expect-error invalid arguments
145- await loadCoreSdkScript ( 123 ) ;
146- } ) . rejects . toThrowError ( "Expected an options object" ) ;
147-
148- expect ( async ( ) => {
149- // @ts -expect-error invalid arguments
150- await loadCoreSdkScript ( { environment : "bad_value" } ) ;
151- } ) . rejects . toThrowError (
152- 'The "environment" option must be either "production" or "sandbox"' ,
153- ) ;
154- } ) ;
155241} ) ;
0 commit comments