mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
This commit updates the `fetch` patch for zone.js. Currently, we're attaching an `abort` event listener on the signal (when it's provided) and never removing it. We should be good citizens and remove event listeners whenever objects need to be properly collected. In Firefox, when saving a heap snapshot and running it through `fxsnapshot`, querying `AbortSignal` will print a so-called "CaptureMap" with a list of "lambdas," indicating that the signal is not garbage collected because of the event listener lambda function. PR Close #57882
310 lines
12 KiB
TypeScript
310 lines
12 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google LLC 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.dev/license
|
|
*/
|
|
import {isNode} from '../../lib/common/utils';
|
|
import {ifEnvSupports, ifEnvSupportsWithDone, isFirefox, isSafari} from '../test-util';
|
|
|
|
declare const global: any;
|
|
|
|
describe(
|
|
'fetch',
|
|
isNode
|
|
? () => {
|
|
it('is untested for node as the fetch implementation is experimental', () => {});
|
|
}
|
|
: ifEnvSupports(
|
|
'fetch',
|
|
function () {
|
|
let testZone: Zone;
|
|
beforeEach(() => {
|
|
testZone = Zone.current.fork({name: 'TestZone'});
|
|
});
|
|
it('should work for text response', function (done) {
|
|
testZone.run(function () {
|
|
global['fetch']('/base/angular/packages/zone.js/test/assets/sample.json').then(
|
|
function (response: any) {
|
|
const fetchZone = Zone.current;
|
|
expect(fetchZone.name).toBe(testZone.name);
|
|
|
|
response.text().then(function (text: string) {
|
|
expect(Zone.current.name).toBe(fetchZone.name);
|
|
expect(text.trim()).toEqual('{"hello": "world"}');
|
|
done();
|
|
});
|
|
},
|
|
);
|
|
});
|
|
});
|
|
|
|
it('should work for json response', function (done) {
|
|
testZone.run(function () {
|
|
global['fetch']('/base/angular/packages/zone.js/test/assets/sample.json').then(
|
|
function (response: any) {
|
|
const fetchZone = Zone.current;
|
|
expect(fetchZone.name).toBe(testZone.name);
|
|
|
|
response.json().then(function (obj: any) {
|
|
expect(Zone.current.name).toBe(fetchZone.name);
|
|
expect(obj.hello).toEqual('world');
|
|
done();
|
|
});
|
|
},
|
|
);
|
|
});
|
|
});
|
|
|
|
it('should work for blob response', function (done) {
|
|
testZone.run(function () {
|
|
global['fetch']('/base/angular/packages/zone.js/test/assets/sample.json').then(
|
|
function (response: any) {
|
|
const fetchZone = Zone.current;
|
|
expect(fetchZone.name).toBe(testZone.name);
|
|
|
|
// Android 4.3- doesn't support response.blob()
|
|
if (response.blob) {
|
|
response.blob().then(function (blob: any) {
|
|
expect(Zone.current.name).toBe(fetchZone.name);
|
|
expect(blob instanceof Blob).toEqual(true);
|
|
done();
|
|
});
|
|
} else {
|
|
done();
|
|
}
|
|
},
|
|
);
|
|
});
|
|
});
|
|
|
|
it('should work for arrayBuffer response', function (done) {
|
|
testZone.run(function () {
|
|
global['fetch']('/base/angular/packages/zone.js/test/assets/sample.json').then(
|
|
function (response: any) {
|
|
const fetchZone = Zone.current;
|
|
expect(fetchZone.name).toBe(testZone.name);
|
|
|
|
// Android 4.3- doesn't support response.arrayBuffer()
|
|
if (response.arrayBuffer) {
|
|
response.arrayBuffer().then(function (blob: any) {
|
|
expect(Zone.current).toBe(fetchZone);
|
|
expect(blob instanceof ArrayBuffer).toEqual(true);
|
|
done();
|
|
});
|
|
} else {
|
|
done();
|
|
}
|
|
},
|
|
);
|
|
});
|
|
});
|
|
|
|
it(
|
|
'should throw error when send crendential',
|
|
ifEnvSupportsWithDone(isFirefox, function (done: DoneFn) {
|
|
testZone.run(function () {
|
|
global['fetch']('http://user:password@example.com').then(
|
|
function (response: any) {
|
|
fail('should not success');
|
|
},
|
|
(error: any) => {
|
|
expect(Zone.current.name).toEqual(testZone.name);
|
|
expect(error.constructor.name).toEqual('TypeError');
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
}),
|
|
);
|
|
|
|
describe('macroTask', () => {
|
|
const logs: string[] = [];
|
|
let fetchZone: Zone;
|
|
let fetchTask: any = null;
|
|
beforeEach(() => {
|
|
logs.splice(0);
|
|
fetchZone = Zone.current.fork({
|
|
name: 'fetch',
|
|
onScheduleTask: (delegate: ZoneDelegate, curr: Zone, target: Zone, task: Task) => {
|
|
if (task.type !== 'eventTask') {
|
|
logs.push(`scheduleTask:${task.source}:${task.type}`);
|
|
}
|
|
if (task.source === 'fetch') {
|
|
fetchTask = task;
|
|
}
|
|
return delegate.scheduleTask(target, task);
|
|
},
|
|
onInvokeTask: (
|
|
delegate: ZoneDelegate,
|
|
curr: Zone,
|
|
target: Zone,
|
|
task: Task,
|
|
applyThis: any,
|
|
applyArgs: any,
|
|
) => {
|
|
if (task.type !== 'eventTask') {
|
|
logs.push(`invokeTask:${task.source}:${task.type}`);
|
|
}
|
|
return delegate.invokeTask(target, task, applyThis, applyArgs);
|
|
},
|
|
onCancelTask: (delegate: ZoneDelegate, curr: Zone, target: Zone, task: Task) => {
|
|
if (task.type !== 'eventTask') {
|
|
logs.push(`cancelTask:${task.source}:${task.type}`);
|
|
}
|
|
return delegate.cancelTask(target, task);
|
|
},
|
|
});
|
|
});
|
|
it('fetch should be considered as macroTask', (done: DoneFn) => {
|
|
fetchZone.run(() => {
|
|
global['fetch']('/base/angular/packages/zone.js/test/assets/sample.json').then(
|
|
function (response: any) {
|
|
expect(Zone.current.name).toBe(fetchZone.name);
|
|
expect(logs).toEqual([
|
|
'scheduleTask:fetch:macroTask',
|
|
'scheduleTask:Promise.then:microTask',
|
|
'invokeTask:Promise.then:microTask',
|
|
'invokeTask:fetch:macroTask',
|
|
'scheduleTask:Promise.then:microTask',
|
|
'invokeTask:Promise.then:microTask',
|
|
|
|
// This is the `finally` task, which is used for cleanup.
|
|
'scheduleTask:Promise.then:microTask',
|
|
'invokeTask:Promise.then:microTask',
|
|
]);
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
});
|
|
|
|
// https://github.com/angular/angular/issues/50327
|
|
it('Response.json() should be considered as macroTask', (done) => {
|
|
fetchZone.run(() => {
|
|
global['fetch']('/base/angular/packages/zone.js/test/assets/sample.json')
|
|
.then((response: any) => {
|
|
const promise = response.json();
|
|
// Ensure it's a `ZoneAwarePromise`.
|
|
expect(promise).toBeInstanceOf(global.Promise);
|
|
return promise;
|
|
})
|
|
.then(() => {
|
|
expect(logs).toEqual([
|
|
'scheduleTask:fetch:macroTask',
|
|
'scheduleTask:Promise.then:microTask',
|
|
'invokeTask:Promise.then:microTask',
|
|
'invokeTask:fetch:macroTask',
|
|
'scheduleTask:Promise.then:microTask',
|
|
'invokeTask:Promise.then:microTask',
|
|
|
|
// This is the `finally` task, which is used for cleanup.
|
|
'scheduleTask:Promise.then:microTask',
|
|
'invokeTask:Promise.then:microTask',
|
|
|
|
// Please refer to the issue link above. Previously, `Response` methods were not
|
|
// patched by zone.js, and their return values were considered only as
|
|
// microtasks (not macrotasks). The Angular zone stabilized prematurely,
|
|
// occurring before the resolution of the `response.json()` promise due to the
|
|
// falsy value of `zone.hasPendingMacrotasks`. We are now ensuring that
|
|
// `Response` methods are treated as macrotasks, similar to the behavior of
|
|
// `fetch`.
|
|
'scheduleTask:Response.json:macroTask',
|
|
'scheduleTask:Promise.then:microTask',
|
|
'invokeTask:Promise.then:microTask',
|
|
'invokeTask:Response.json:macroTask',
|
|
'scheduleTask:Promise.then:microTask',
|
|
'invokeTask:Promise.then:microTask',
|
|
'scheduleTask:Promise.then:microTask',
|
|
'invokeTask:Promise.then:microTask',
|
|
]);
|
|
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it(
|
|
'cancel fetch should invoke onCancelTask',
|
|
ifEnvSupportsWithDone('AbortController', (done: DoneFn) => {
|
|
if (isSafari()) {
|
|
// safari not work with AbortController
|
|
done();
|
|
return;
|
|
}
|
|
fetchZone.run(() => {
|
|
const AbortController = global['AbortController'];
|
|
const abort = new AbortController();
|
|
const signal = abort.signal;
|
|
global['fetch']('/base/angular/packages/zone.js/test/assets/sample.json', {
|
|
signal,
|
|
})
|
|
.then(function (response: any) {
|
|
fail('should not get response');
|
|
})
|
|
.catch(function (error: any) {
|
|
expect(error.name).toEqual('AbortError');
|
|
expect(logs).toEqual([
|
|
'scheduleTask:fetch:macroTask',
|
|
'cancelTask:fetch:macroTask',
|
|
'scheduleTask:Promise.then:microTask',
|
|
'invokeTask:Promise.then:microTask',
|
|
'scheduleTask:Promise.then:microTask',
|
|
'invokeTask:Promise.then:microTask',
|
|
'scheduleTask:Promise.then:microTask',
|
|
'invokeTask:Promise.then:microTask',
|
|
]);
|
|
done();
|
|
});
|
|
abort.abort();
|
|
});
|
|
}),
|
|
);
|
|
|
|
it(
|
|
'cancel fetchTask should trigger abort',
|
|
ifEnvSupportsWithDone('AbortController', (done: DoneFn) => {
|
|
if (isSafari()) {
|
|
// safari not work with AbortController
|
|
done();
|
|
return;
|
|
}
|
|
fetchZone.run(() => {
|
|
const AbortController = global['AbortController'];
|
|
const abort = new AbortController();
|
|
const signal = abort.signal;
|
|
global['fetch']('/base/angular/packages/zone.js/test/assets/sample.json', {
|
|
signal,
|
|
})
|
|
.then(function (response: any) {
|
|
fail('should not get response');
|
|
})
|
|
.catch(function (error: any) {
|
|
expect(error.name).toEqual('AbortError');
|
|
expect(logs).toEqual([
|
|
'scheduleTask:fetch:macroTask',
|
|
'cancelTask:fetch:macroTask',
|
|
'scheduleTask:Promise.then:microTask',
|
|
'invokeTask:Promise.then:microTask',
|
|
'scheduleTask:Promise.then:microTask',
|
|
'invokeTask:Promise.then:microTask',
|
|
'scheduleTask:Promise.then:microTask',
|
|
'invokeTask:Promise.then:microTask',
|
|
]);
|
|
done();
|
|
});
|
|
fetchTask.zone.cancelTask(fetchTask);
|
|
});
|
|
}),
|
|
);
|
|
});
|
|
},
|
|
emptyRun,
|
|
),
|
|
);
|
|
|
|
function emptyRun() {
|
|
// Jasmine will throw if there are no tests.
|
|
it('should pass', () => {});
|
|
}
|