angular/packages/common/http/test/xhr_mock.ts
Paul Gschwendtner 489cf42cd0 fix(common): incorrect error type for XHR errors in TestRequest (#36082)
Currently the `HttpClient` always wraps errors from XHR requests, but
the underlying errors are always of type `ProgressEvent`, or don't have
a native error if the status code is just indicating failure (e.g. 404).

This behavior does not match in the `TestRequest` class provided by
`@angular/common/http/testing` where errors are considered being
of type `ErrorEvent`. This is incorrect because `ErrorEvent`s provide
information for errors in scripts or files which are evaluated. Since
the `HttpClient` never evaluates scripts/files, and also since XHR requests
clearly are documented to emit `ProgressEvent`'s, we should change the
`TestSupport` to retrieve such `ProgressEvent`'s instead of incompatible
objects of type `ErrorEvent`.

In favor of having a deprecation period, we keep supporting `ErrorEvent`
in the `TestRequest.error` signature. Eventually, we can remove this
signature in the future.

Resources:
  * https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/error_event
  * https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent
  * https://xhr.spec.whatwg.org/#event-xhr-errpr

Related to: https://github.com/angular/angular/issues/34748.

DEPRECATED: `TestRequest` from `@angular/common/http/testing` no longer
accepts `ErrorEvent` when simulating XHR errors. Instead instances of
`ProgressEvent` should be passed, matching with the native browser behavior.

PR Close #36082
2021-11-19 21:26:52 +00:00

151 lines
3.6 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 {XhrFactory} from '@angular/common';
import {HttpHeaders} from '@angular/common/http/src/headers';
export class MockXhrFactory implements XhrFactory {
// TODO(issue/24571): remove '!'.
mock!: MockXMLHttpRequest;
build(): XMLHttpRequest {
return (this.mock = new MockXMLHttpRequest()) as any;
}
}
export class MockXMLHttpRequestUpload {
constructor(private mock: MockXMLHttpRequest) {}
addEventListener(event: 'progress', handler: Function) {
this.mock.addEventListener('uploadProgress', handler);
}
removeEventListener(event: 'progress', handler: Function) {
this.mock.removeEventListener('uploadProgress');
}
}
export class MockXMLHttpRequest {
// Set by method calls.
body: any;
// TODO(issue/24571): remove '!'.
method!: string;
// TODO(issue/24571): remove '!'.
url!: string;
mockHeaders: {[key: string]: string} = {};
mockAborted: boolean = false;
// Directly settable interface.
withCredentials: boolean = false;
responseType: string = 'text';
// Mocked response interface.
response: any|undefined = undefined;
responseText: string|undefined = undefined;
responseURL: string|null = null;
status: number = 0;
statusText: string = '';
mockResponseHeaders: string = '';
listeners: {
error?: (event: ProgressEvent) => void,
timeout?: (event: ProgressEvent) => void,
abort?: () => void,
load?: () => void,
progress?: (event: ProgressEvent) => void,
uploadProgress?: (event: ProgressEvent) => void,
} = {};
upload = new MockXMLHttpRequestUpload(this);
open(method: string, url: string): void {
this.method = method;
this.url = url;
}
send(body: any): void {
this.body = body;
}
addEventListener(
event: 'error'|'timeout'|'load'|'progress'|'uploadProgress'|'abort',
handler: Function): void {
this.listeners[event] = handler as any;
}
removeEventListener(event: 'error'|'timeout'|'load'|'progress'|'uploadProgress'|'abort'): void {
delete this.listeners[event];
}
setRequestHeader(name: string, value: string): void {
this.mockHeaders[name] = value;
}
getAllResponseHeaders(): string {
return this.mockResponseHeaders;
}
getResponseHeader(header: string): string|null {
return new HttpHeaders(this.mockResponseHeaders).get(header);
}
mockFlush(status: number, statusText: string, body?: string) {
if (typeof body === 'string') {
this.responseText = body;
} else {
this.response = body;
}
this.status = status;
this.statusText = statusText;
this.mockLoadEvent();
}
mockDownloadProgressEvent(loaded: number, total?: number): void {
if (this.listeners.progress) {
this.listeners.progress({lengthComputable: total !== undefined, loaded, total} as any);
}
}
mockUploadProgressEvent(loaded: number, total?: number) {
if (this.listeners.uploadProgress) {
this.listeners.uploadProgress({
lengthComputable: total !== undefined,
loaded,
total,
} as any);
}
}
mockLoadEvent(): void {
if (this.listeners.load) {
this.listeners.load();
}
}
mockErrorEvent(error: any): void {
if (this.listeners.error) {
this.listeners.error(error);
}
}
mockTimeoutEvent(error: any): void {
if (this.listeners.timeout) {
this.listeners.timeout(error);
}
}
mockAbortEvent(): void {
if (this.listeners.abort) {
this.listeners.abort();
}
}
abort() {
this.mockAborted = true;
}
}