mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
Fix some typos such as `boostrap`, `propery` and more, both in documentation and in code (comments, identifiers). PR Close #52249
488 lines
17 KiB
TypeScript
488 lines
17 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.io/license
|
|
*/
|
|
|
|
import {HttpEvent, HttpEventType, HttpRequest, HttpResponse} from '@angular/common/http';
|
|
import {TestBed} from '@angular/core/testing';
|
|
import {Observable, of, Subject} from 'rxjs';
|
|
import {catchError, retry, scan, skip, take, toArray} from 'rxjs/operators';
|
|
|
|
import {HttpDownloadProgressEvent, HttpErrorResponse, HttpHeaderResponse, HttpParams, HttpStatusCode} from '../public_api';
|
|
import {FetchBackend, FetchFactory} from '../src/fetch';
|
|
|
|
function trackEvents(obs: Observable<any>): Promise<any[]> {
|
|
return obs
|
|
.pipe(
|
|
// We don't want the promise to fail on HttpErrorResponse
|
|
catchError((e) => of(e)),
|
|
scan(
|
|
(acc, event) => {
|
|
acc.push(event);
|
|
return acc;
|
|
},
|
|
[] as any[]),
|
|
)
|
|
.toPromise();
|
|
}
|
|
|
|
const TEST_POST = new HttpRequest('POST', '/test', 'some body', {
|
|
responseType: 'text',
|
|
});
|
|
|
|
const TEST_POST_WITH_JSON_BODY = new HttpRequest('POST', '/test', {'some': 'body'}, {
|
|
responseType: 'text',
|
|
});
|
|
|
|
const XSSI_PREFIX = ')]}\'\n';
|
|
|
|
describe('FetchBackend', async () => {
|
|
let fetchMock: MockFetchFactory = null!;
|
|
let backend: FetchBackend = null!;
|
|
let fetchSpy: jasmine.Spy<typeof fetch>;
|
|
|
|
function callFetchAndFlush(req: HttpRequest<any>): void {
|
|
backend.handle(req).pipe(take(1)).subscribe();
|
|
fetchMock.mockFlush(HttpStatusCode.Ok, 'OK', 'some response');
|
|
}
|
|
|
|
beforeEach(() => {
|
|
TestBed.configureTestingModule({
|
|
providers: [
|
|
{provide: FetchFactory, useClass: MockFetchFactory},
|
|
FetchBackend,
|
|
]
|
|
});
|
|
|
|
fetchMock = TestBed.inject(FetchFactory) as MockFetchFactory;
|
|
fetchSpy = spyOn(fetchMock, 'fetch').and.callThrough();
|
|
backend = TestBed.inject(FetchBackend);
|
|
});
|
|
|
|
it('emits status immediately', () => {
|
|
let event!: HttpEvent<any>;
|
|
// subscribe is sync
|
|
backend.handle(TEST_POST).pipe(take(1)).subscribe((e) => event = e);
|
|
fetchMock.mockFlush(HttpStatusCode.Ok, 'OK', 'some response');
|
|
expect(event.type).toBe(HttpEventType.Sent);
|
|
});
|
|
|
|
it('should not call fetch without a subscribe', () => {
|
|
const handle = backend.handle(TEST_POST);
|
|
expect(fetchSpy).not.toHaveBeenCalled();
|
|
handle.subscribe();
|
|
fetchMock.mockFlush(HttpStatusCode.Ok, 'OK', 'some response');
|
|
expect(fetchSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should be able to retry', ((done) => {
|
|
const handle = backend.handle(TEST_POST);
|
|
// Skipping both HttpSentEvent (from the 1st subscription + retry)
|
|
handle.pipe(retry(1), skip(2)).subscribe((response) => {
|
|
expect(response.type).toBe(HttpEventType.Response);
|
|
expect((response as HttpResponse<any>).body).toBe('some response');
|
|
done();
|
|
});
|
|
fetchMock.mockErrorEvent('Error 1');
|
|
fetchMock.resetFetchPromise();
|
|
|
|
fetchMock.mockFlush(HttpStatusCode.Ok, 'OK', 'some response');
|
|
}));
|
|
|
|
it('sets method, url, and responseType correctly', () => {
|
|
callFetchAndFlush(TEST_POST);
|
|
expect(fetchMock.request.method).toBe('POST');
|
|
expect(fetchMock.request.url).toBe('/test');
|
|
});
|
|
|
|
it('use query params from request', () => {
|
|
const requestWithQuery = new HttpRequest('GET', '/test', 'some body', {
|
|
params: new HttpParams({fromObject: {query: 'foobar'}}),
|
|
responseType: 'text',
|
|
});
|
|
callFetchAndFlush(requestWithQuery);
|
|
expect(fetchMock.request.method).toBe('GET');
|
|
expect(fetchMock.request.url).toBe('/test?query=foobar');
|
|
});
|
|
|
|
it('sets outgoing body correctly', () => {
|
|
callFetchAndFlush(TEST_POST);
|
|
expect(fetchMock.request.body).toBe('some body');
|
|
});
|
|
|
|
it('sets outgoing body correctly when request payload is json', () => {
|
|
callFetchAndFlush(TEST_POST_WITH_JSON_BODY);
|
|
expect(fetchMock.request.body).toBe('{"some":"body"}');
|
|
});
|
|
|
|
it('sets outgoing headers, including default headers', () => {
|
|
const post = TEST_POST.clone({
|
|
setHeaders: {
|
|
'Test': 'Test header',
|
|
},
|
|
});
|
|
callFetchAndFlush(post);
|
|
expect(fetchMock.request.headers).toEqual({
|
|
'Test': 'Test header',
|
|
'Accept': 'application/json, text/plain, */*',
|
|
'Content-Type': 'text/plain',
|
|
});
|
|
});
|
|
|
|
it('sets outgoing headers, including overriding defaults', () => {
|
|
const setHeaders = {
|
|
'Test': 'Test header',
|
|
'Accept': 'text/html',
|
|
'Content-Type': 'text/css',
|
|
};
|
|
callFetchAndFlush(TEST_POST.clone({setHeaders}));
|
|
expect(fetchMock.request.headers).toEqual(setHeaders);
|
|
});
|
|
|
|
it('passes withCredentials through', () => {
|
|
callFetchAndFlush(TEST_POST.clone({withCredentials: true}));
|
|
expect(fetchMock.request.credentials).toBe('include');
|
|
});
|
|
|
|
it('handles a text response', (async () => {
|
|
const promise = trackEvents(backend.handle(TEST_POST));
|
|
fetchMock.mockFlush(HttpStatusCode.Ok, 'OK', 'some response');
|
|
const events = await promise;
|
|
expect(events.length).toBe(2);
|
|
expect(events[1].type).toBe(HttpEventType.Response);
|
|
expect(events[1] instanceof HttpResponse).toBeTruthy();
|
|
const res = events[1] as HttpResponse<string>;
|
|
expect(res.body).toBe('some response');
|
|
expect(res.status).toBe(HttpStatusCode.Ok);
|
|
expect(res.statusText).toBe('OK');
|
|
}));
|
|
|
|
it('handles a json response', async () => {
|
|
const promise = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'})));
|
|
fetchMock.mockFlush(HttpStatusCode.Ok, 'OK', JSON.stringify({data: 'some data'}));
|
|
const events = await promise;
|
|
expect(events.length).toBe(2);
|
|
const res = events[1] as HttpResponse<{data: string}>;
|
|
expect(res.body!.data).toBe('some data');
|
|
});
|
|
|
|
it('handles a blank json response', async () => {
|
|
const promise = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'})));
|
|
fetchMock.mockFlush(HttpStatusCode.Ok, 'OK', '');
|
|
const events = await promise;
|
|
expect(events.length).toBe(2);
|
|
const res = events[1] as HttpResponse<{data: string}>;
|
|
expect(res.body).toBeNull();
|
|
});
|
|
|
|
it('handles a json error response', async () => {
|
|
const promise = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'})));
|
|
fetchMock.mockFlush(
|
|
HttpStatusCode.InternalServerError, 'Error', JSON.stringify({data: 'some data'}));
|
|
const events = await promise;
|
|
expect(events.length).toBe(2);
|
|
const res = events[1] as any as HttpErrorResponse;
|
|
expect(res.error.data).toBe('some data');
|
|
});
|
|
|
|
it('handles a json error response with XSSI prefix', async () => {
|
|
const promise = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'})));
|
|
fetchMock.mockFlush(
|
|
HttpStatusCode.InternalServerError, 'Error',
|
|
XSSI_PREFIX + JSON.stringify({data: 'some data'}));
|
|
const events = await promise;
|
|
expect(events.length).toBe(2);
|
|
const res = events[1] as any as HttpErrorResponse;
|
|
expect(res.error.data).toBe('some data');
|
|
});
|
|
|
|
it('handles a json string response', async () => {
|
|
const promise = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'})));
|
|
fetchMock.mockFlush(HttpStatusCode.Ok, 'OK', JSON.stringify('this is a string'));
|
|
const events = await promise;
|
|
expect(events.length).toBe(2);
|
|
const res = events[1] as HttpResponse<string>;
|
|
expect(res.body).toEqual('this is a string');
|
|
});
|
|
|
|
it('handles a json response with an XSSI prefix', async () => {
|
|
const promise = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'})));
|
|
fetchMock.mockFlush(HttpStatusCode.Ok, 'OK', XSSI_PREFIX + JSON.stringify({data: 'some data'}));
|
|
const events = await promise;
|
|
expect(events.length).toBe(2);
|
|
const res = events[1] as HttpResponse<{data: string}>;
|
|
expect(res.body!.data).toBe('some data');
|
|
});
|
|
|
|
it('emits unsuccessful responses via the error path', done => {
|
|
backend.handle(TEST_POST).subscribe({
|
|
error: (err: HttpErrorResponse) => {
|
|
expect(err instanceof HttpErrorResponse).toBe(true);
|
|
expect(err.error).toBe('this is the error');
|
|
done();
|
|
}
|
|
});
|
|
fetchMock.mockFlush(HttpStatusCode.BadRequest, 'Bad Request', 'this is the error');
|
|
});
|
|
|
|
it('emits real errors via the error path', done => {
|
|
// Skipping the HttpEventType.Sent that is sent first
|
|
backend.handle(TEST_POST).pipe(skip(1)).subscribe({
|
|
error: (err: HttpErrorResponse) => {
|
|
expect(err instanceof HttpErrorResponse).toBe(true);
|
|
expect(err.error instanceof Error).toBeTrue();
|
|
expect(err.url).toBe('/test');
|
|
done();
|
|
}
|
|
});
|
|
fetchMock.mockErrorEvent(new Error('blah'));
|
|
});
|
|
|
|
it('emits an error when browser cancels a request', done => {
|
|
backend.handle(TEST_POST).subscribe({
|
|
error: (err: HttpErrorResponse) => {
|
|
expect(err instanceof HttpErrorResponse).toBe(true);
|
|
expect(err.error instanceof DOMException).toBeTruthy();
|
|
done();
|
|
}
|
|
});
|
|
fetchMock.mockAbortEvent();
|
|
});
|
|
|
|
describe('progress events', () => {
|
|
it('are emitted for download progress', done => {
|
|
backend.handle(TEST_POST.clone({reportProgress: true})).pipe(toArray()).subscribe(events => {
|
|
expect(events.map(event => event.type)).toEqual([
|
|
HttpEventType.Sent,
|
|
HttpEventType.ResponseHeader,
|
|
HttpEventType.DownloadProgress,
|
|
HttpEventType.DownloadProgress,
|
|
HttpEventType.DownloadProgress,
|
|
HttpEventType.Response,
|
|
]);
|
|
const [progress1, progress2, response] = [
|
|
events[2] as HttpDownloadProgressEvent, events[3] as HttpDownloadProgressEvent,
|
|
events[5] as HttpResponse<string>
|
|
];
|
|
expect(progress1.partialText).toBe('down');
|
|
expect(progress1.loaded).toBe(4);
|
|
expect(progress1.total).toBe(10);
|
|
expect(progress2.partialText).toBe('download');
|
|
expect(progress2.loaded).toBe(8);
|
|
expect(progress2.total).toBe(10);
|
|
expect(response.body).toBe('downloaded');
|
|
done();
|
|
});
|
|
fetchMock.mockProgressEvent(4);
|
|
fetchMock.mockProgressEvent(8);
|
|
fetchMock.mockFlush(HttpStatusCode.Ok, 'OK', 'downloaded');
|
|
});
|
|
|
|
it('include ResponseHeader with headers and status', done => {
|
|
backend.handle(TEST_POST.clone({reportProgress: true})).pipe(toArray()).subscribe(events => {
|
|
expect(events.map(event => event.type)).toEqual([
|
|
HttpEventType.Sent,
|
|
HttpEventType.ResponseHeader,
|
|
HttpEventType.DownloadProgress,
|
|
HttpEventType.DownloadProgress,
|
|
HttpEventType.Response,
|
|
]);
|
|
const partial = events[1] as HttpHeaderResponse;
|
|
expect(partial.type).toEqual(HttpEventType.ResponseHeader);
|
|
expect(partial.headers.get('Content-Type')).toEqual('text/plain');
|
|
expect(partial.headers.get('Test')).toEqual('Test header');
|
|
done();
|
|
});
|
|
fetchMock.response.headers = {'Test': 'Test header', 'Content-Type': 'text/plain'};
|
|
fetchMock.mockProgressEvent(200);
|
|
fetchMock.mockFlush(HttpStatusCode.Ok, 'OK', 'Done');
|
|
});
|
|
});
|
|
describe('gets response URL', async () => {
|
|
it('from the response URL', done => {
|
|
backend.handle(TEST_POST).pipe(toArray()).subscribe(events => {
|
|
expect(events.length).toBe(2);
|
|
expect(events[1].type).toBe(HttpEventType.Response);
|
|
const response = events[1] as HttpResponse<string>;
|
|
expect(response.url).toBe('/response/url');
|
|
done();
|
|
});
|
|
fetchMock.response.url = '/response/url';
|
|
fetchMock.mockFlush(HttpStatusCode.Ok, 'OK', 'Test');
|
|
});
|
|
|
|
it('from X-Request-URL header if the response URL is not present', done => {
|
|
backend.handle(TEST_POST).pipe(toArray()).subscribe(events => {
|
|
expect(events.length).toBe(2);
|
|
expect(events[1].type).toBe(HttpEventType.Response);
|
|
const response = events[1] as HttpResponse<string>;
|
|
expect(response.url).toBe('/response/url');
|
|
done();
|
|
});
|
|
fetchMock.response.headers = {'X-Request-URL': '/response/url'};
|
|
fetchMock.mockFlush(HttpStatusCode.Ok, 'OK', 'Test');
|
|
});
|
|
|
|
it('falls back on Request.url if neither are available', done => {
|
|
backend.handle(TEST_POST).pipe(toArray()).subscribe(events => {
|
|
expect(events.length).toBe(2);
|
|
expect(events[1].type).toBe(HttpEventType.Response);
|
|
const response = events[1] as HttpResponse<string>;
|
|
expect(response.url).toBe('/test');
|
|
done();
|
|
});
|
|
fetchMock.mockFlush(HttpStatusCode.Ok, 'OK', 'Test');
|
|
});
|
|
});
|
|
describe('corrects for quirks', async () => {
|
|
it('by normalizing 0 status to 200 if a body is present', done => {
|
|
backend.handle(TEST_POST).pipe(toArray()).subscribe(events => {
|
|
expect(events.length).toBe(2);
|
|
expect(events[1].type).toBe(HttpEventType.Response);
|
|
const response = events[1] as HttpResponse<string>;
|
|
expect(response.status).toBe(HttpStatusCode.Ok);
|
|
done();
|
|
});
|
|
fetchMock.mockFlush(0, 'CORS 0 status', 'Test');
|
|
});
|
|
it('by leaving 0 status as 0 if a body is not present', done => {
|
|
backend.handle(TEST_POST).pipe(toArray()).subscribe({
|
|
error: (error: HttpErrorResponse) => {
|
|
expect(error.status).toBe(0);
|
|
done();
|
|
}
|
|
});
|
|
fetchMock.mockFlush(0, 'CORS 0 status');
|
|
});
|
|
});
|
|
});
|
|
|
|
export class MockFetchFactory extends FetchFactory {
|
|
public readonly response = new MockFetchResponse();
|
|
public readonly request = new MockFetchRequest();
|
|
private resolve!: Function;
|
|
private reject!: Function;
|
|
|
|
private clearWarningTimeout?: VoidFunction;
|
|
|
|
private promise = new Promise<Response>((resolve, reject) => {
|
|
this.resolve = resolve;
|
|
this.reject = reject;
|
|
});
|
|
|
|
override fetch = (input: RequestInfo|URL, init?: RequestInit):
|
|
Promise<Response> => {
|
|
this.request.method = init?.method;
|
|
this.request.url = input;
|
|
this.request.body = init?.body;
|
|
this.request.headers = init?.headers;
|
|
this.request.credentials = init?.credentials;
|
|
|
|
if (init?.signal) {
|
|
init?.signal.addEventListener('abort', () => {
|
|
this.reject();
|
|
this.clearWarningTimeout?.();
|
|
});
|
|
}
|
|
|
|
// Fetch uses a Macrotask to keep the NgZone unstable during the fetch
|
|
// If the promise is not resolved/rejected the unit will succeed but the test suite will
|
|
// fail with a timeout
|
|
const timeoutId = setTimeout(() => {
|
|
console.error('********* You forgot to resolve/reject the promise ********* ');
|
|
this.reject();
|
|
}, 5000);
|
|
this.clearWarningTimeout = () => clearTimeout(timeoutId);
|
|
|
|
return this.promise;
|
|
}
|
|
|
|
mockFlush(status: number, statusText: string, body?: string): void {
|
|
this.clearWarningTimeout?.();
|
|
this.response.setupBodyStream(body);
|
|
|
|
const response =
|
|
new Response(this.response.stream, {statusText, headers: this.response.headers});
|
|
|
|
// Have to be set outside the constructor because it might throw
|
|
// RangeError: init["status"] must be in the range of 200 to 599, inclusive
|
|
Object.defineProperty(response, 'status', {value: status});
|
|
|
|
if (this.response.url) {
|
|
// url is readonly
|
|
Object.defineProperty(response, 'url', {value: this.response.url});
|
|
}
|
|
this.resolve(response);
|
|
}
|
|
|
|
mockProgressEvent(loaded: number): void {
|
|
this.response.progress.push(loaded);
|
|
}
|
|
|
|
mockErrorEvent(error: any) {
|
|
this.reject(error);
|
|
}
|
|
|
|
mockAbortEvent() {
|
|
// When `abort()` is called, the fetch() promise rejects with an Error of type DOMException,
|
|
// with name AbortError. see
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/AbortController/abort
|
|
this.reject(new DOMException('', 'AbortError'));
|
|
}
|
|
|
|
resetFetchPromise() {
|
|
this.promise = new Promise<Response>((resolve, reject) => {
|
|
this.resolve = resolve;
|
|
this.reject = reject;
|
|
});
|
|
}
|
|
}
|
|
|
|
class MockFetchRequest {
|
|
public url!: RequestInfo|URL;
|
|
public method?: string;
|
|
public body: any;
|
|
public credentials?: RequestCredentials;
|
|
public headers?: HeadersInit;
|
|
}
|
|
|
|
class MockFetchResponse {
|
|
public url?: string;
|
|
public headers: Record<string, string> = {};
|
|
|
|
public progress: number[] = [];
|
|
|
|
private sub$ = new Subject<any>();
|
|
public stream = new ReadableStream({
|
|
|
|
start: (controller) => {
|
|
this.sub$.subscribe({
|
|
next: (val) => {
|
|
controller.enqueue(new TextEncoder().encode(val));
|
|
},
|
|
complete: () => {
|
|
controller.close();
|
|
}
|
|
});
|
|
},
|
|
});
|
|
|
|
public setupBodyStream(body?: string) {
|
|
if (body && this.progress.length) {
|
|
this.headers['content-length'] = `${body.length}`;
|
|
let shift = 0;
|
|
this.progress.forEach((loaded) => {
|
|
this.sub$.next(body.substring(shift, loaded));
|
|
shift = loaded;
|
|
});
|
|
this.sub$.next(body.substring(shift, body.length));
|
|
} else {
|
|
this.sub$.next(body);
|
|
}
|
|
|
|
this.sub$.complete();
|
|
}
|
|
}
|