382 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
		
		
			
		
	
	
			382 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
|  | /** | ||
|  |  * @license | ||
|  |  * Copyright Google Inc. All Rights Reserved. | ||
|  |  * | ||
|  |  * Use of this source code is governed by an MIT-style license that can be | ||
|  |  * found in the LICENSE file at https://angular.io/license
 | ||
|  |  */ | ||
|  | 
 | ||
|  | import {ifEnvSupports, ifEnvSupportsWithDone, supportPatchXHROnProperty, zoneSymbol} from '../test-util'; | ||
|  | declare const global: any; | ||
|  | const wtfMock = global.wtfMock; | ||
|  | 
 | ||
|  | describe('XMLHttpRequest', function() { | ||
|  |   let testZone: Zone; | ||
|  | 
 | ||
|  |   beforeEach(() => { testZone = Zone.current.fork({name: 'test'}); }); | ||
|  | 
 | ||
|  |   it('should intercept XHRs and treat them as MacroTasks', function(done) { | ||
|  |     let req: XMLHttpRequest; | ||
|  |     let onStable: any; | ||
|  |     const testZoneWithWtf = Zone.current.fork((Zone as any)['wtfZoneSpec']).fork({ | ||
|  |       name: 'TestZone', | ||
|  |       onHasTask: (delegate: ZoneDelegate, curr: Zone, target: Zone, hasTask: HasTaskState) => { | ||
|  |         if (!hasTask.macroTask) { | ||
|  |           onStable && onStable(); | ||
|  |         } | ||
|  |       } | ||
|  |     }); | ||
|  | 
 | ||
|  |     testZoneWithWtf.run(() => { | ||
|  |       req = new XMLHttpRequest(); | ||
|  |       const logs: string[] = []; | ||
|  |       req.onload = () => { logs.push('onload'); }; | ||
|  |       onStable = function() { | ||
|  |         expect(wtfMock.log[wtfMock.log.length - 2]) | ||
|  |             .toEqual('> Zone:invokeTask:XMLHttpRequest.send("<root>::ProxyZone::WTF::TestZone")'); | ||
|  |         expect(wtfMock.log[wtfMock.log.length - 1]) | ||
|  |             .toEqual('< Zone:invokeTask:XMLHttpRequest.send'); | ||
|  |         if (supportPatchXHROnProperty()) { | ||
|  |           expect(wtfMock.log[wtfMock.log.length - 3]) | ||
|  |               .toMatch(/\< Zone\:invokeTask.*addEventListener\:load/); | ||
|  |           expect(wtfMock.log[wtfMock.log.length - 4]) | ||
|  |               .toMatch(/\> Zone\:invokeTask.*addEventListener\:load/); | ||
|  |         } | ||
|  |         // if browser can patch onload
 | ||
|  |         if ((req as any)[zoneSymbol('loadfalse')]) { | ||
|  |           expect(logs).toEqual(['onload']); | ||
|  |         } | ||
|  |         done(); | ||
|  |       }; | ||
|  | 
 | ||
|  |       req.open('get', '/', true); | ||
|  |       req.send(); | ||
|  |       const lastScheduled = wtfMock.log[wtfMock.log.length - 1]; | ||
|  |       expect(lastScheduled).toMatch('# Zone:schedule:macroTask:XMLHttpRequest.send'); | ||
|  |     }, null, undefined, 'unit-test'); | ||
|  |   }); | ||
|  | 
 | ||
|  |   it('should not trigger Zone callback of internal onreadystatechange', function(done) { | ||
|  |     const scheduleSpy = jasmine.createSpy('schedule'); | ||
|  |     const xhrZone = Zone.current.fork({ | ||
|  |       name: 'xhr', | ||
|  |       onScheduleTask: (delegate: ZoneDelegate, currentZone: Zone, targetZone, task: Task) => { | ||
|  |         if (task.type === 'eventTask') { | ||
|  |           scheduleSpy(task.source); | ||
|  |         } | ||
|  |         return delegate.scheduleTask(targetZone, task); | ||
|  |       } | ||
|  |     }); | ||
|  | 
 | ||
|  |     xhrZone.run(() => { | ||
|  |       const req = new XMLHttpRequest(); | ||
|  |       req.onload = function() { | ||
|  |         expect(Zone.current.name).toEqual('xhr'); | ||
|  |         if (supportPatchXHROnProperty()) { | ||
|  |           expect(scheduleSpy).toHaveBeenCalled(); | ||
|  |         } | ||
|  |         done(); | ||
|  |       }; | ||
|  |       req.open('get', '/', true); | ||
|  |       req.send(); | ||
|  |     }); | ||
|  |   }); | ||
|  | 
 | ||
|  |   it('should work with onreadystatechange', function(done) { | ||
|  |     let req: XMLHttpRequest; | ||
|  | 
 | ||
|  |     testZone.run(function() { | ||
|  |       req = new XMLHttpRequest(); | ||
|  |       req.onreadystatechange = function() { | ||
|  |         // Make sure that the wrapCallback will only be called once
 | ||
|  |         req.onreadystatechange = null as any; | ||
|  |         expect(Zone.current).toBe(testZone); | ||
|  |         done(); | ||
|  |       }; | ||
|  |       req.open('get', '/', true); | ||
|  |     }); | ||
|  | 
 | ||
|  |     req !.send(); | ||
|  |   }); | ||
|  | 
 | ||
|  |   it('should return null when access ontimeout first time without error', function() { | ||
|  |     let req: XMLHttpRequest = new XMLHttpRequest(); | ||
|  |     expect(req.ontimeout).toBe(null); | ||
|  |   }); | ||
|  | 
 | ||
|  |   const supportsOnProgress = function() { return 'onprogress' in (new XMLHttpRequest()); }; | ||
|  | 
 | ||
|  |   (<any>supportsOnProgress).message = 'XMLHttpRequest.onprogress'; | ||
|  | 
 | ||
|  |   describe('onprogress', ifEnvSupports(supportsOnProgress, function() { | ||
|  |              it('should work with onprogress', function(done) { | ||
|  |                let req: XMLHttpRequest; | ||
|  |                testZone.run(function() { | ||
|  |                  req = new XMLHttpRequest(); | ||
|  |                  req.onprogress = function() { | ||
|  |                    // Make sure that the wrapCallback will only be called once
 | ||
|  |                    req.onprogress = null as any; | ||
|  |                    expect(Zone.current).toBe(testZone); | ||
|  |                    done(); | ||
|  |                  }; | ||
|  |                  req.open('get', '/', true); | ||
|  |                }); | ||
|  | 
 | ||
|  |                req !.send(); | ||
|  |              }); | ||
|  | 
 | ||
|  |              it('should allow canceling of an XMLHttpRequest', function(done) { | ||
|  |                const spy = jasmine.createSpy('spy'); | ||
|  |                let req: XMLHttpRequest; | ||
|  |                let pending = false; | ||
|  | 
 | ||
|  |                const trackingTestZone = Zone.current.fork({ | ||
|  |                  name: 'tracking test zone', | ||
|  |                  onHasTask: (delegate: ZoneDelegate, current: Zone, target: Zone, | ||
|  |                              hasTaskState: HasTaskState) => { | ||
|  |                    if (hasTaskState.change == 'macroTask') { | ||
|  |                      pending = hasTaskState.macroTask; | ||
|  |                    } | ||
|  |                    delegate.hasTask(target, hasTaskState); | ||
|  |                  } | ||
|  |                }); | ||
|  | 
 | ||
|  |                trackingTestZone.run(function() { | ||
|  |                  req = new XMLHttpRequest(); | ||
|  |                  req.onreadystatechange = function() { | ||
|  |                    if (req.readyState === XMLHttpRequest.DONE) { | ||
|  |                      if (req.status !== 0) { | ||
|  |                        spy(); | ||
|  |                      } | ||
|  |                    } | ||
|  |                  }; | ||
|  |                  req.open('get', '/', true); | ||
|  | 
 | ||
|  |                  req.send(); | ||
|  |                  req.abort(); | ||
|  |                }); | ||
|  | 
 | ||
|  |                setTimeout(function() { | ||
|  |                  expect(spy).not.toHaveBeenCalled(); | ||
|  |                  expect(pending).toEqual(false); | ||
|  |                  done(); | ||
|  |                }, 0); | ||
|  |              }); | ||
|  | 
 | ||
|  |              it('should allow aborting an XMLHttpRequest after its completed', function(done) { | ||
|  |                let req: XMLHttpRequest; | ||
|  | 
 | ||
|  |                testZone.run(function() { | ||
|  |                  req = new XMLHttpRequest(); | ||
|  |                  req.onreadystatechange = function() { | ||
|  |                    if (req.readyState === XMLHttpRequest.DONE) { | ||
|  |                      if (req.status !== 0) { | ||
|  |                        setTimeout(function() { | ||
|  |                          req.abort(); | ||
|  |                          done(); | ||
|  |                        }, 0); | ||
|  |                      } | ||
|  |                    } | ||
|  |                  }; | ||
|  |                  req.open('get', '/', true); | ||
|  | 
 | ||
|  |                  req.send(); | ||
|  |                }); | ||
|  |              }); | ||
|  |            })); | ||
|  | 
 | ||
|  |   it('should preserve other setters', function() { | ||
|  |     const req = new XMLHttpRequest(); | ||
|  |     req.open('get', '/', true); | ||
|  |     req.send(); | ||
|  |     try { | ||
|  |       req.responseType = 'document'; | ||
|  |       expect(req.responseType).toBe('document'); | ||
|  |     } catch (e) { | ||
|  |       // Android browser: using this setter throws, this should be preserved
 | ||
|  |       expect(e.message).toBe('INVALID_STATE_ERR: DOM Exception 11'); | ||
|  |     } | ||
|  |   }); | ||
|  | 
 | ||
|  |   it('should work with synchronous XMLHttpRequest', function() { | ||
|  |     const log: HasTaskState[] = []; | ||
|  |     Zone.current | ||
|  |         .fork({ | ||
|  |           name: 'sync-xhr-test', | ||
|  |           onHasTask: function( | ||
|  |               delegate: ZoneDelegate, current: Zone, target: Zone, hasTaskState: HasTaskState) { | ||
|  |             log.push(hasTaskState); | ||
|  |             delegate.hasTask(target, hasTaskState); | ||
|  |           } | ||
|  |         }) | ||
|  |         .run(() => { | ||
|  |           const req = new XMLHttpRequest(); | ||
|  |           req.open('get', '/', false); | ||
|  |           req.send(); | ||
|  |         }); | ||
|  |     expect(log).toEqual([]); | ||
|  |   }); | ||
|  | 
 | ||
|  |   it('should preserve static constants', function() { | ||
|  |     expect(XMLHttpRequest.UNSENT).toEqual(0); | ||
|  |     expect(XMLHttpRequest.OPENED).toEqual(1); | ||
|  |     expect(XMLHttpRequest.HEADERS_RECEIVED).toEqual(2); | ||
|  |     expect(XMLHttpRequest.LOADING).toEqual(3); | ||
|  |     expect(XMLHttpRequest.DONE).toEqual(4); | ||
|  |   }); | ||
|  | 
 | ||
|  |   it('should work properly when send request multiple times on single xmlRequest instance', | ||
|  |      function(done) { | ||
|  |        testZone.run(function() { | ||
|  |          const req = new XMLHttpRequest(); | ||
|  |          req.open('get', '/', true); | ||
|  |          req.send(); | ||
|  |          req.onload = function() { | ||
|  |            req.onload = null as any; | ||
|  |            req.open('get', '/', true); | ||
|  |            req.onload = function() { done(); }; | ||
|  |            expect(() => { req.send(); }).not.toThrow(); | ||
|  |          }; | ||
|  |        }); | ||
|  |      }); | ||
|  | 
 | ||
|  |   it('should keep taskcount correctly when abort was called multiple times before request is done', | ||
|  |      function(done) { | ||
|  |        testZone.run(function() { | ||
|  |          const req = new XMLHttpRequest(); | ||
|  |          req.open('get', '/', true); | ||
|  |          req.send(); | ||
|  |          req.addEventListener('readystatechange', function(ev) { | ||
|  |            if (req.readyState >= 2) { | ||
|  |              expect(() => { req.abort(); }).not.toThrow(); | ||
|  |              done(); | ||
|  |            } | ||
|  |          }); | ||
|  |        }); | ||
|  |      }); | ||
|  | 
 | ||
|  |   it('should trigger readystatechange if xhr request trigger cors error', (done) => { | ||
|  |     const req = new XMLHttpRequest(); | ||
|  |     let err: any = null; | ||
|  |     try { | ||
|  |       req.open('get', 'file:///test', true); | ||
|  |     } catch (err) { | ||
|  |       // in IE, open will throw Access is denied error
 | ||
|  |       done(); | ||
|  |       return; | ||
|  |     } | ||
|  |     req.addEventListener('readystatechange', function(ev) { | ||
|  |       if (req.readyState === 4) { | ||
|  |         const xhrScheduled = (req as any)[zoneSymbol('xhrScheduled')]; | ||
|  |         const task = (req as any)[zoneSymbol('xhrTask')]; | ||
|  |         if (xhrScheduled === false) { | ||
|  |           expect(task.state).toEqual('scheduling'); | ||
|  |           setTimeout(() => { | ||
|  |             if (err) { | ||
|  |               expect(task.state).toEqual('unknown'); | ||
|  |             } else { | ||
|  |               expect(task.state).toEqual('notScheduled'); | ||
|  |             } | ||
|  |             done(); | ||
|  |           }); | ||
|  |         } else { | ||
|  |           expect(task.state).toEqual('scheduled'); | ||
|  |           done(); | ||
|  |         } | ||
|  |       } | ||
|  |     }); | ||
|  |     try { | ||
|  |       req.send(); | ||
|  |     } catch (error) { | ||
|  |       err = error; | ||
|  |     } | ||
|  |   }); | ||
|  | 
 | ||
|  |   it('should invoke task if xhr request trigger cors error', (done) => { | ||
|  |     const logs: string[] = []; | ||
|  |     const zone = Zone.current.fork({ | ||
|  |       name: 'xhr', | ||
|  |       onHasTask: (delegate: ZoneDelegate, curr: Zone, target: Zone, hasTask: HasTaskState) => { | ||
|  |         logs.push(JSON.stringify(hasTask)); | ||
|  |       } | ||
|  |     }); | ||
|  |     const req = new XMLHttpRequest(); | ||
|  |     try { | ||
|  |       req.open('get', 'file:///test', true); | ||
|  |     } catch (err) { | ||
|  |       // in IE, open will throw Access is denied error
 | ||
|  |       done(); | ||
|  |       return; | ||
|  |     } | ||
|  |     zone.run(() => { | ||
|  |       let isError = false; | ||
|  |       let timerId = null; | ||
|  |       try { | ||
|  |         timerId = (window as any)[zoneSymbol('setTimeout')](() => { | ||
|  |           expect(logs).toEqual([ | ||
|  |             `{"microTask":false,"macroTask":true,"eventTask":false,"change":"macroTask"}`, | ||
|  |             `{"microTask":false,"macroTask":false,"eventTask":false,"change":"macroTask"}` | ||
|  |           ]); | ||
|  |           done(); | ||
|  |         }, 500); | ||
|  |         req.send(); | ||
|  |       } catch (error) { | ||
|  |         isError = true; | ||
|  |         (window as any)[zoneSymbol('clearTimeout')](timerId); | ||
|  |         done(); | ||
|  |       } | ||
|  |     }); | ||
|  |   }); | ||
|  | 
 | ||
|  |   it('should not throw error when get XMLHttpRequest.prototype.onreadystatechange the first time', | ||
|  |      function() { | ||
|  |        const func = function() { | ||
|  |          testZone.run(function() { | ||
|  |            const req = new XMLHttpRequest(); | ||
|  |            req.onreadystatechange; | ||
|  |          }); | ||
|  |        }; | ||
|  |        expect(func).not.toThrow(); | ||
|  |      }); | ||
|  | 
 | ||
|  |   it('should be in the zone when use XMLHttpRequest.addEventListener', function(done) { | ||
|  |     testZone.run(function() { | ||
|  |       // sometimes this case will cause timeout
 | ||
|  |       // so we set it longer
 | ||
|  |       const interval = (<any>jasmine).DEFAULT_TIMEOUT_INTERVAL; | ||
|  |       (<any>jasmine).DEFAULT_TIMEOUT_INTERVAL = 5000; | ||
|  |       const req = new XMLHttpRequest(); | ||
|  |       req.open('get', '/', true); | ||
|  |       req.addEventListener('readystatechange', function() { | ||
|  |         if (req.readyState === 4) { | ||
|  |           // expect(Zone.current.name).toEqual('test');
 | ||
|  |           (<any>jasmine).DEFAULT_TIMEOUT_INTERVAL = interval; | ||
|  |           done(); | ||
|  |         } | ||
|  |       }); | ||
|  |       req.send(); | ||
|  |     }); | ||
|  |   }); | ||
|  | 
 | ||
|  |   it('should return origin listener when call xhr.onreadystatechange', | ||
|  |      ifEnvSupportsWithDone(supportPatchXHROnProperty, function(done: Function) { | ||
|  |        testZone.run(function() { | ||
|  |          // sometimes this case will cause timeout
 | ||
|  |          // so we set it longer
 | ||
|  |          const req = new XMLHttpRequest(); | ||
|  |          req.open('get', '/', true); | ||
|  |          const interval = (<any>jasmine).DEFAULT_TIMEOUT_INTERVAL; | ||
|  |          (<any>jasmine).DEFAULT_TIMEOUT_INTERVAL = 5000; | ||
|  |          const listener = req.onreadystatechange = function() { | ||
|  |            if (req.readyState === 4) { | ||
|  |              (<any>jasmine).DEFAULT_TIMEOUT_INTERVAL = interval; | ||
|  |              done(); | ||
|  |            } | ||
|  |          }; | ||
|  |          expect(req.onreadystatechange).toBe(listener); | ||
|  |          req.onreadystatechange = function() { return listener.call(this); }; | ||
|  |          req.send(); | ||
|  |        }); | ||
|  |      })); | ||
|  | }); |