Removed SyncTasks from the project. Massively alterered tests to handle the new async behavior (#62)

* Removed SyncTasks from the project.  Massively alterered tests to handle the new async behavior.

* PR feedback
This commit is contained in:
David de Regt 2020-02-20 15:37:26 -07:00 коммит произвёл GitHub
Родитель f0cd0355a3
Коммит c97f650f49
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
9 изменённых файлов: 535 добавлений и 396 удалений

Просмотреть файл

@ -14,7 +14,8 @@ npm install --save simplerestclients
### `SimpleWebRequest`
Wraps a single web request. Has lots of overrides for priorization, delays, retry logic, error handling, etc.
Wraps a single web request. Takes an options structure with overrides for priorization, delays, retry logic, error handling, etc. Has
an `abort()` method to cancel the request early (will result in a rejected promise from the `start()` method).
### `GenericRestClient`
@ -25,7 +26,6 @@ etc.
## GenericRestClient Sample Usage
```typescript
import * as SyncTasks from 'synctasks';
import { GenericRestClient, ApiCallOptions, Headers } from 'simplerestclients';
interface User {
@ -45,15 +45,15 @@ class MyRestClient extends GenericRestClient {
}
// Define public methods that expose the APIs provided through the REST service.
getAllUsers(): SyncTasks.Promise<User[]> {
getAllUsers(): Promise<User[]> {
return this.performApiGet<User[]>('users');
}
getUserById(id: string): SyncTasks.Promise<User> {
getUserById(id: string): Promise<User> {
return this.performApiGet<User>(`user/${ id }`);
}
setUser(user: User): SyncTasks.Promise<void> {
setUser(user: User): Promise<void> {
return this.performApiPut<void>(`user/${ user.id }`, user);
}
}

48
package-lock.json сгенерированный
Просмотреть файл

@ -1,6 +1,6 @@
{
"name": "simplerestclients",
"version": "0.2.12",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -5134,11 +5134,6 @@
"has-flag": "^3.0.0"
}
},
"synctasks": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/synctasks/-/synctasks-0.3.3.tgz",
"integrity": "sha512-8Gr8WHInZt5oU1q2N7ANqLSZ/TJn6bYlkqkwJJoGowFc5l81DRORgtHH9eJUpJGJsGJgYyWeVfKGVtUdTu4TEQ=="
},
"table": {
"version": "5.4.6",
"resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz",
@ -5723,8 +5718,7 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"aproba": {
"version": "1.2.0",
@ -5745,14 +5739,12 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -5767,20 +5759,17 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"core-util-is": {
"version": "1.0.2",
@ -5897,8 +5886,7 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"ini": {
"version": "1.3.5",
@ -5910,7 +5898,6 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -5925,7 +5912,6 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@ -5933,14 +5919,12 @@
"minimist": {
"version": "0.0.8",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"minipass": {
"version": "2.3.5",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@ -5959,7 +5943,6 @@
"version": "0.5.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@ -6040,8 +6023,7 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"object-assign": {
"version": "4.1.1",
@ -6053,7 +6035,6 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@ -6139,8 +6120,7 @@
"safe-buffer": {
"version": "5.1.2",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"safer-buffer": {
"version": "2.1.2",
@ -6176,7 +6156,6 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@ -6196,7 +6175,6 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@ -6240,14 +6218,12 @@
"wrappy": {
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"yallist": {
"version": "3.0.3",
"bundled": true,
"dev": true,
"optional": true
"dev": true
}
}
},

Просмотреть файл

@ -1,6 +1,6 @@
{
"name": "simplerestclients",
"version": "0.2.12",
"version": "1.0.0",
"description": "A library of components for accessing RESTful services with javascript/typescript.",
"author": "David de Regt <David.de.Regt@microsoft.com>",
"scripts": {
@ -13,9 +13,7 @@
"test:watch": "npm run clean && karma start",
"test:browser": "npm run clean && karma start --browsers=Chrome --single-run=false --auto-watch"
},
"dependencies": {
"synctasks": "^0.3.3"
},
"dependencies": {},
"devDependencies": {
"@types/faker": "4.1.8",
"@types/jasmine": "3.5.0",

Просмотреть файл

@ -6,8 +6,6 @@
* Base client type for accessing RESTful services
*/
import * as SyncTasks from 'synctasks';
import { isString } from './utils';
import { WebRequestOptions, SimpleWebRequest, WebResponse, Headers } from './SimpleWebRequest';
@ -30,7 +28,12 @@ export interface ETagResponse<T> {
eTag?: string;
}
export class GenericRestClient {
export interface ApiCallResponse<T, TCustomOptions extends {}> {
req: SimpleWebRequest<T, ApiCallOptions & Partial<TCustomOptions>>;
promise: Promise<WebResponse<T, ApiCallOptions & Partial<TCustomOptions>>>;
}
export class GenericRestClient<TCustomOptions extends {} = {}> {
protected _endpointUrl: string;
@ -47,9 +50,9 @@ export class GenericRestClient {
protected _performApiCall<T>(apiPath: string,
action: HttpAction,
objToPost: any,
givenOptions: ApiCallOptions = {}): SyncTasks.Promise<WebResponse<T, ApiCallOptions>> {
givenOptions: Partial<ApiCallOptions & TCustomOptions> = {}): ApiCallResponse<T, TCustomOptions> {
const options: ApiCallOptions = { ...this._defaultOptions, ...givenOptions };
const options: ApiCallOptions & Partial<TCustomOptions> = { ...this._defaultOptions, ...givenOptions };
if (objToPost) {
options.sendData = objToPost;
}
@ -67,90 +70,94 @@ export class GenericRestClient {
const finalUrl = options.excludeEndpointUrl ? apiPath : this._endpointUrl + apiPath;
return new SimpleWebRequest<T, ApiCallOptions>(
const req = new SimpleWebRequest<T, ApiCallOptions & Partial<TCustomOptions>>(
action,
finalUrl,
options,
() => this._getHeaders(options),
() => this._blockRequestUntil(options),
)
.start()
.then(response => {
this._processSuccessResponse<T>(response);
return response;
});
);
const promise = req.start().then(response => {
this._processSuccessResponse<T>(response);
return response;
});
return {
req,
promise,
};
}
protected _getHeaders(options: ApiCallOptions): Headers {
protected _getHeaders(options: ApiCallOptions & Partial<TCustomOptions>): Headers {
// Virtual function -- No-op by default
return {};
}
// Override (but make sure to call super and chain appropriately) this function if you want to add more blocking criteria.
// Also, this might be called multiple times to check if the conditions changed
protected _blockRequestUntil(options: ApiCallOptions): SyncTasks.Promise<void> | undefined {
protected _blockRequestUntil(options: ApiCallOptions & Partial<TCustomOptions>): Promise<void> | undefined {
// No-op by default
return undefined;
}
// Override this function to process any generic headers that come down with a successful response
protected _processSuccessResponse<T>(resp: WebResponse<T, ApiCallOptions>): void {
protected _processSuccessResponse<T>(resp: WebResponse<T, ApiCallOptions & Partial<TCustomOptions>>): void {
// No-op by default
}
performApiGet<T>(apiPath: string, options?: ApiCallOptions): SyncTasks.Promise<T> {
performApiGet<T>(apiPath: string, options?: ApiCallOptions & Partial<TCustomOptions>): Promise<T> {
return this
.performApiGetDetailed<T>(apiPath, options)
.then(resp => resp.body);
.promise.then(resp => resp.body);
}
performApiGetDetailed<T>(apiPath: string, options?: ApiCallOptions): SyncTasks.Promise<WebResponse<T, ApiCallOptions>> {
performApiGetDetailed<T>(apiPath: string, options?: ApiCallOptions & Partial<TCustomOptions>):
ApiCallResponse<T, ApiCallOptions & Partial<TCustomOptions>> {
return this._performApiCall<T>(apiPath, 'GET', undefined, options);
}
performApiPost<T>(apiPath: string, objToPost: any, options?: ApiCallOptions): SyncTasks.Promise<T> {
performApiPost<T>(apiPath: string, objToPost: any, options?: ApiCallOptions & Partial<TCustomOptions>): Promise<T> {
return this
.performApiPostDetailed<T>(apiPath, objToPost, options)
.then(resp => resp.body);
.promise.then(resp => resp.body);
}
performApiPostDetailed<T>(apiPath: string,
objToPost: any,
options?: ApiCallOptions): SyncTasks.Promise<WebResponse<T, ApiCallOptions>> {
performApiPostDetailed<T>(apiPath: string, objToPost: any, options?: ApiCallOptions & Partial<TCustomOptions>):
ApiCallResponse<T, ApiCallOptions & Partial<TCustomOptions>> {
return this._performApiCall<T>(apiPath, 'POST', objToPost, options);
}
performApiPatch<T>(apiPath: string, objToPatch: any, options?: ApiCallOptions): SyncTasks.Promise<T> {
performApiPatch<T>(apiPath: string, objToPatch: any, options?: ApiCallOptions & Partial<TCustomOptions>): Promise<T> {
return this
.performApiPatchDetailed<T>(apiPath, objToPatch, options)
.then(resp => resp.body);
.promise.then(resp => resp.body);
}
performApiPatchDetailed<T>(apiPath: string,
objToPatch: any,
options?: ApiCallOptions): SyncTasks.Promise<WebResponse<T, ApiCallOptions>> {
performApiPatchDetailed<T>(apiPath: string, objToPatch: any, options?: ApiCallOptions & Partial<TCustomOptions>):
ApiCallResponse<T, ApiCallOptions & Partial<TCustomOptions>> {
return this._performApiCall<T>(apiPath, 'PATCH', objToPatch, options);
}
performApiPut<T>(apiPath: string, objToPut: any, options?: ApiCallOptions): SyncTasks.Promise<T> {
performApiPut<T>(apiPath: string, objToPut: any, options?: ApiCallOptions & Partial<TCustomOptions>): Promise<T> {
return this
.performApiPutDetailed<T>(apiPath, objToPut, options)
.then(resp => resp.body);
.promise.then(resp => resp.body);
}
performApiPutDetailed<T>(apiPath: string, objToPut: any, options?: ApiCallOptions): SyncTasks.Promise<WebResponse<T, ApiCallOptions>> {
performApiPutDetailed<T>(apiPath: string, objToPut: any, options?: ApiCallOptions & Partial<TCustomOptions>):
ApiCallResponse<T, ApiCallOptions & Partial<TCustomOptions>> {
return this._performApiCall<T>(apiPath, 'PUT', objToPut, options);
}
performApiDelete<T>(apiPath: string, objToDelete?: any, options?: ApiCallOptions): SyncTasks.Promise<T> {
performApiDelete<T>(apiPath: string, objToDelete?: any, options?: ApiCallOptions & Partial<TCustomOptions>): Promise<T> {
return this
.performApiDeleteDetailed<T>(apiPath, objToDelete, options)
.then(resp => resp.body);
.promise.then(resp => resp.body);
}
performApiDeleteDetailed<T>(apiPath: string,
objToDelete: any,
options?: ApiCallOptions): SyncTasks.Promise<WebResponse<T, ApiCallOptions>> {
performApiDeleteDetailed<T>(apiPath: string, objToDelete: any, options?: ApiCallOptions & Partial<TCustomOptions>):
ApiCallResponse<T, ApiCallOptions & Partial<TCustomOptions>> {
return this._performApiCall<T>(apiPath, 'DELETE', objToDelete, options);
}
}

Просмотреть файл

@ -6,8 +6,6 @@
* Simple client for issuing web requests.
*/
import * as SyncTasks from 'synctasks';
import { attempt, isObject, isString, remove, assert, clone } from './utils';
import { ExponentialTime } from './ExponentialTime';
@ -218,7 +216,7 @@ export abstract class SimpleWebRequestBase<TOptions extends WebRequestOptions =
protected _url: string,
protected readonly options: TOptions,
protected readonly _getHeaders?: () => Headers,
protected readonly _blockRequestUntil?: () => SyncTasks.Promise<void> | undefined) {
protected readonly _blockRequestUntil?: () => Promise<void> | undefined) {
this._options = { ...DefaultOptions, ...options };
}
@ -230,13 +228,17 @@ export abstract class SimpleWebRequestBase<TOptions extends WebRequestOptions =
abstract abort(): void;
protected static checkQueueProcessing(): void {
while (requestQueue.length > 0 && executingList.length < SimpleWebRequestOptions.MaxSimultaneousRequests) {
const req = requestQueue.shift()!!!;
while (executingList.length < SimpleWebRequestOptions.MaxSimultaneousRequests) {
const req = requestQueue.shift();
if (!req) {
return;
}
blockedList.push(req);
const blockPromise = (req._blockRequestUntil && req._blockRequestUntil()) || SyncTasks.Resolved();
blockPromise.finally(() => {
const blockPromise = (req._blockRequestUntil && req._blockRequestUntil()) || Promise.resolve();
blockPromise.then(() => {
remove(blockedList, req);
}).then(() => {
if (executingList.length < SimpleWebRequestOptions.MaxSimultaneousRequests && !req._aborted) {
executingList.push(req);
SimpleWebRequest._scheduleHungRequestCleanupIfNeeded();
@ -245,6 +247,8 @@ export abstract class SimpleWebRequestBase<TOptions extends WebRequestOptions =
req._enqueue();
}
}, (err: any) => {
remove(blockedList, req);
// fail the request if the block promise is rejected
req._respond('_blockRequestUntil rejected: ' + err);
});
@ -664,14 +668,14 @@ export abstract class SimpleWebRequestBase<TOptions extends WebRequestOptions =
}
export class SimpleWebRequest<TBody, TOptions extends WebRequestOptions = WebRequestOptions> extends SimpleWebRequestBase<TOptions> {
private _deferred: SyncTasks.Deferred<WebResponse<TBody, TOptions>>;
private _resolve?: (resp: WebResponse<TBody, TOptions>) => void;
private _reject?: (resp?: any) => void;
constructor(action: string,
url: string,
options: TOptions,
getHeaders?: () => Headers,
blockRequestUntil?: () => SyncTasks.Promise<void> | undefined) {
blockRequestUntil?: () => Promise<void> | undefined) {
super(action, url, options, getHeaders, blockRequestUntil);
}
@ -693,7 +697,7 @@ export class SimpleWebRequest<TBody, TOptions extends WebRequestOptions = WebReq
this._requestTimeoutTimer = undefined;
}
if (!this._deferred) {
if (!this._resolve) {
assert(false, 'Haven\'t even fired start() yet -- can\'t abort');
return;
}
@ -707,21 +711,20 @@ export class SimpleWebRequest<TBody, TOptions extends WebRequestOptions = WebReq
}
}
start(): SyncTasks.Promise<WebResponse<TBody, TOptions>> {
if (this._deferred) {
start(): Promise<WebResponse<TBody, TOptions>> {
if (this._resolve) {
assert(false, 'WebRequest already started');
return SyncTasks.Rejected('WebRequest already started');
return Promise.reject('WebRequest already started');
}
this._deferred = SyncTasks.Defer<WebResponse<TBody, TOptions>>();
this._deferred.onCancel(() => {
// Abort the XHR -- this should chain through to the fail case on readystatechange
this.abort();
const promise = new Promise<WebResponse<TBody, TOptions>>((res, rej) => {
this._resolve = res;
this._reject = rej;
});
this._enqueue();
return this._deferred.promise();
return promise;
}
protected _respond(errorStatusText?: string): void {
@ -828,7 +831,7 @@ export class SimpleWebRequest<TBody, TOptions extends WebRequestOptions = WebReq
responseParsingException: responseParsingException,
};
this._deferred.resolve(resp);
this._resolve!!!(resp);
} else {
let errResp: WebErrorResponse<TOptions> = {
url: (this._xhr ? this._xhr.responseURL : undefined) || this._url,
@ -898,7 +901,7 @@ export class SimpleWebRequest<TBody, TOptions extends WebRequestOptions = WebReq
}
} else {
// No more retries -- fail.
this._deferred.reject(errResp);
this._reject!!!(errResp);
}
}

Просмотреть файл

@ -1,10 +1,9 @@
import * as faker from 'faker';
import * as SyncTasks from 'synctasks';
import { ErrorHandlingType, SimpleWebRequestBase, WebErrorResponse } from '../src/SimpleWebRequest';
import { GenericRestClient, ApiCallOptions } from '../src/GenericRestClient';
import { DETAILED_RESPONSE, REQUEST_OPTIONS } from './helpers';
import { DETAILED_RESPONSE, REQUEST_OPTIONS, asyncTick } from './helpers';
class RestClient extends GenericRestClient { }
const BASE_URL = faker.internet.url();
@ -13,6 +12,7 @@ const http = new RestClient(BASE_URL);
describe('GenericRestClient', () => {
beforeAll(() => {
jasmine.Ajax.install();
jasmine.clock().install();
// Run an initial request to finish feature detection - this is needed so we can directly call onLoad
const statusCode = 200;
const onSuccess = jasmine.createSpy('onSuccess');
@ -21,16 +21,27 @@ describe('GenericRestClient', () => {
http.performApiGet(path)
.then(onSuccess);
const request = jasmine.Ajax.requests.mostRecent();
request.respondWith({ status: statusCode });
expect(onSuccess).toHaveBeenCalled();
jasmine.Ajax.uninstall();
return asyncTick().then(() => {
const request = jasmine.Ajax.requests.mostRecent();
request.respondWith({ status: statusCode });
return asyncTick();
}).then(() => {
expect(onSuccess).toHaveBeenCalled();
jasmine.Ajax.uninstall();
jasmine.clock().uninstall();
});
});
beforeEach(() => jasmine.Ajax.install());
afterEach(() => jasmine.Ajax.uninstall());
beforeEach(() => {
jasmine.Ajax.install();
jasmine.clock().install();
});
afterEach(() => {
jasmine.Ajax.uninstall();
jasmine.clock().uninstall();
});
it('performs GET request with performApiGet', () => {
it('performs GET request with performApiGet ', () => {
const id = faker.random.uuid();
const statusCode = 200;
const onSuccess = jasmine.createSpy('onSuccess');
@ -43,19 +54,22 @@ describe('GenericRestClient', () => {
const path = `/get/${id}`;
const url = BASE_URL + path;
http.performApiGet(path)
const p1 = http.performApiGet(path)
.then(onSuccess);
const request = jasmine.Ajax.requests.mostRecent();
request.respondWith({
responseText: JSON.stringify(body),
status: statusCode,
return asyncTick().then(() => {
const request = jasmine.Ajax.requests.mostRecent();
expect(request.url).toEqual(url);
expect(request.method).toEqual(method);
request.respondWith({
responseText: JSON.stringify(body),
status: statusCode,
});
expect(request.status).toEqual(statusCode);
return p1;
}).then(() => {
expect(onSuccess).toHaveBeenCalledWith(body);
});
expect(request.url).toEqual(url);
expect(request.method).toEqual(method);
expect(request.status).toEqual(statusCode);
expect(onSuccess).toHaveBeenCalledWith(body);
});
it('performs GET request with performApiGetDetailed', () => {
@ -81,21 +95,25 @@ describe('GenericRestClient', () => {
responseParsingException,
};
http.performApiGetDetailed(path, { contentType: 'json' })
.then(onSuccess);
const p1 = http.performApiGetDetailed(path, { contentType: 'json' })
.promise.then(onSuccess);
const request = jasmine.Ajax.requests.mostRecent();
request.respondWith({
responseText: JSON.stringify(body),
status: statusCode,
return asyncTick().then(() => {
const request = jasmine.Ajax.requests.mostRecent();
expect(request.url).toEqual(url);
expect(request.method).toEqual(method);
request.respondWith({
responseText: JSON.stringify(body),
status: statusCode,
});
return p1;
}).then(() => {
expect(onSuccess).toHaveBeenCalledWith(response);
});
expect(request.url).toEqual(url);
expect(request.method).toEqual(method);
expect(onSuccess).toHaveBeenCalledWith(response);
});
it('performs POST request with performApiPost', () => {
it('performs POST request with performApiPost ', () => {
const statusCode = 201;
const onSuccess = jasmine.createSpy('onSuccess');
const method = 'POST';
@ -107,20 +125,24 @@ describe('GenericRestClient', () => {
const path = '/post';
const url = BASE_URL + path;
http.performApiPost(path, sendData, { contentType: 'json' })
const p1 = http.performApiPost(path, sendData, { contentType: 'json' })
.then(onSuccess);
const request = jasmine.Ajax.requests.mostRecent();
request.respondWith({
responseText: JSON.stringify(body),
status: statusCode,
});
return asyncTick().then(() => {
const request = jasmine.Ajax.requests.mostRecent();
expect(request.url).toEqual(url);
expect(request.method).toEqual(method);
expect(request.url).toEqual(url);
expect(request.method).toEqual(method);
expect(request.status).toEqual(statusCode);
expect(request.data() as any).toEqual(sendData);
expect(onSuccess).toHaveBeenCalledWith(body);
request.respondWith({
responseText: JSON.stringify(body),
status: statusCode,
});
expect(request.status).toEqual(statusCode);
expect(request.data() as any).toEqual(sendData);
return p1;
}).then(() => {
expect(onSuccess).toHaveBeenCalledWith(body);
});
});
it('performs POST request with performApiPostDetailed', () => {
@ -145,19 +167,23 @@ describe('GenericRestClient', () => {
responseParsingException,
};
http.performApiPostDetailed(path, sendData, { contentType: 'json' })
.then(onSuccess);
const p1 = http.performApiPostDetailed(path, sendData, { contentType: 'json' })
.promise.then(onSuccess);
const request = jasmine.Ajax.requests.mostRecent();
request.respondWith({
responseText: JSON.stringify(body),
status: statusCode,
return asyncTick().then(() => {
const request = jasmine.Ajax.requests.mostRecent();
expect(request.url).toEqual(url);
expect(request.method).toEqual(method);
request.respondWith({
responseText: JSON.stringify(body),
status: statusCode,
});
expect(request.status).toEqual(statusCode);
return p1;
}).then(() => {
expect(onSuccess).toHaveBeenCalledWith(response);
});
expect(request.url).toEqual(url);
expect(request.method).toEqual(method);
expect(request.status).toEqual(statusCode);
expect(onSuccess).toHaveBeenCalledWith(response);
});
it('performs PUT request with performApiPut', () => {
@ -170,20 +196,24 @@ describe('GenericRestClient', () => {
const path = '/put/' + id;
const url = BASE_URL + path;
http.performApiPut(path, sendData, { contentType: 'json' })
const p1 = http.performApiPut(path, sendData, { contentType: 'json' })
.then(onSuccess);
const request = jasmine.Ajax.requests.mostRecent();
request.respondWith({
responseText: JSON.stringify(body),
status: statusCode,
});
return asyncTick().then(() => {
const request = jasmine.Ajax.requests.mostRecent();
expect(request.url).toEqual(url);
expect(request.method).toEqual(method);
expect(request.url).toEqual(url);
expect(request.method).toEqual(method);
expect(request.status).toEqual(statusCode);
expect(request.data() as any).toEqual(sendData);
expect(onSuccess).toHaveBeenCalledWith(body);
request.respondWith({
responseText: JSON.stringify(body),
status: statusCode,
});
expect(request.status).toEqual(statusCode);
expect(request.data() as any).toEqual(sendData);
return p1;
}).then(() => {
expect(onSuccess).toHaveBeenCalledWith(body);
});
});
it('performs PUT request with performApiPutDetailed', () => {
@ -206,20 +236,24 @@ describe('GenericRestClient', () => {
responseParsingException,
};
http.performApiPutDetailed(path, sendData, { contentType: 'json' })
.then(onSuccess);
const p1 = http.performApiPutDetailed(path, sendData, { contentType: 'json' })
.promise.then(onSuccess);
const request = jasmine.Ajax.requests.mostRecent();
request.respondWith({
responseText: JSON.stringify(body),
status: statusCode,
return asyncTick().then(() => {
const request = jasmine.Ajax.requests.mostRecent();
expect(request.url).toEqual(url);
expect(request.method).toEqual(method);
request.respondWith({
responseText: JSON.stringify(body),
status: statusCode,
});
expect(request.status).toEqual(statusCode);
expect(request.data() as any).toEqual(sendData);
return p1;
}).then(() => {
expect(onSuccess).toHaveBeenCalledWith(response);
});
expect(request.url).toEqual(url);
expect(request.method).toEqual(method);
expect(request.status).toEqual(statusCode);
expect(request.data() as any).toEqual(sendData);
expect(onSuccess).toHaveBeenCalledWith(response);
});
it('performs PATCH request with performApiPatch', () => {
@ -235,20 +269,24 @@ describe('GenericRestClient', () => {
const path = '/patch' + id;
const url = BASE_URL + path;
http.performApiPatch(path, sendData, { contentType: 'json' })
const p1 = http.performApiPatch(path, sendData, { contentType: 'json' })
.then(onSuccess);
const request = jasmine.Ajax.requests.mostRecent();
request.respondWith({
responseText: JSON.stringify(body),
status: statusCode,
});
return asyncTick().then(() => {
const request = jasmine.Ajax.requests.mostRecent();
expect(request.url).toEqual(url);
expect(request.method).toEqual(method);
expect(request.url).toEqual(url);
expect(request.method).toEqual(method);
expect(request.status).toEqual(statusCode);
expect(request.data() as any).toEqual(sendData);
expect(onSuccess).toHaveBeenCalledWith(body);
request.respondWith({
responseText: JSON.stringify(body),
status: statusCode,
});
expect(request.status).toEqual(statusCode);
expect(request.data() as any).toEqual(sendData);
return p1;
}).then(() => {
expect(onSuccess).toHaveBeenCalledWith(body);
});
});
it('performs PATCH request with performApiPatchDetailed', () => {
@ -274,20 +312,24 @@ describe('GenericRestClient', () => {
responseParsingException,
};
http.performApiPatchDetailed(path, sendData, { contentType: 'json' })
.then(onSuccess);
const p1 = http.performApiPatchDetailed(path, sendData, { contentType: 'json' })
.promise.then(onSuccess);
const request = jasmine.Ajax.requests.mostRecent();
request.respondWith({
responseText: JSON.stringify(body),
status: statusCode,
return asyncTick().then(() => {
const request = jasmine.Ajax.requests.mostRecent();
expect(request.url).toEqual(url);
expect(request.method).toEqual(method);
request.respondWith({
responseText: JSON.stringify(body),
status: statusCode,
});
expect(request.status).toEqual(statusCode);
expect(request.data() as any).toEqual(sendData);
return p1;
}).then(() => {
expect(onSuccess).toHaveBeenCalledWith(response);
});
expect(request.url).toEqual(url);
expect(request.method).toEqual(method);
expect(request.status).toEqual(statusCode);
expect(request.data() as any).toEqual(sendData);
expect(onSuccess).toHaveBeenCalledWith(response);
});
it('performs DELETE request with performApiDelete', () => {
@ -298,18 +340,21 @@ describe('GenericRestClient', () => {
const path = `/delete/${faker.random.uuid()}`;
const url = BASE_URL + path;
http.performApiDelete(path)
const p1 = http.performApiDelete(path)
.then(onSuccess);
const request = jasmine.Ajax.requests.mostRecent();
request.respondWith({
responseText: JSON.stringify(body),
status: statusCode,
return asyncTick().then(() => {
const request = jasmine.Ajax.requests.mostRecent();
expect(request.url).toEqual(url);
expect(request.method).toEqual(method);
request.respondWith({
responseText: JSON.stringify(body),
status: statusCode,
});
return p1;
}).then(() => {
expect(onSuccess).toHaveBeenCalledWith(body);
});
expect(request.url).toEqual(url);
expect(request.method).toEqual(method);
expect(onSuccess).toHaveBeenCalledWith(body);
});
it('performs DELETE request with performApiDeleteDetailed', () => {
@ -332,19 +377,23 @@ describe('GenericRestClient', () => {
responseParsingException,
};
http.performApiDeleteDetailed(path, sendData, { contentType: 'json' })
.then(onSuccess);
const p1 = http.performApiDeleteDetailed(path, sendData, { contentType: 'json' })
.promise.then(onSuccess);
const request = jasmine.Ajax.requests.mostRecent();
request.respondWith({
responseText: JSON.stringify(body),
status: statusCode,
return asyncTick().then(() => {
const request = jasmine.Ajax.requests.mostRecent();
expect(request.url).toEqual(url);
expect(request.method).toEqual(method);
request.respondWith({
responseText: JSON.stringify(body),
status: statusCode,
});
expect(request.data() as any).toEqual(sendData);
return p1;
}).then(() => {
expect(onSuccess).toHaveBeenCalledWith(response);
});
expect(request.url).toEqual(url);
expect(request.method).toEqual(method);
expect(request.data() as any).toEqual(sendData);
expect(onSuccess).toHaveBeenCalledWith(response);
});
it('performs request with custom headers', () => {
@ -362,14 +411,17 @@ describe('GenericRestClient', () => {
const http = new Http(BASE_URL);
const path = '/auth';
http.performApiGet(path)
const p1 = http.performApiGet(path)
.then(onSuccess);
const request = jasmine.Ajax.requests.mostRecent();
request.respondWith({ status: statusCode });
expect(request.requestHeaders['Authorization']).toEqual(headers['Authorization']);
expect(onSuccess).toHaveBeenCalled();
return asyncTick().then(() => {
const request = jasmine.Ajax.requests.mostRecent();
expect(request.requestHeaders['Authorization']).toEqual(headers['Authorization']);
request.respondWith({ status: statusCode });
return p1;
}).then(() => {
expect(onSuccess).toHaveBeenCalled();
});
});
it('overrides response', () => {
@ -386,27 +438,33 @@ describe('GenericRestClient', () => {
const body = [' x ', ' y ', ' z '];
const url = BASE_URL + path;
http.performApiGet<string[]>(path)
const p1 = http.performApiGet<string[]>(path)
.then(onSuccess);
const request = jasmine.Ajax.requests.mostRecent();
request.respondWith({
responseText: JSON.stringify(body),
status: statusCode,
});
return asyncTick().then(() => {
const request = jasmine.Ajax.requests.mostRecent();
expect(request.url).toEqual(url);
expect(request.method).toEqual(method);
expect(request.url).toEqual(url);
expect(request.method).toEqual(method);
expect(request.status).toEqual(statusCode);
expect(onSuccess).toHaveBeenCalledWith(body.map((str: string) => str.trim()));
request.respondWith({
responseText: JSON.stringify(body),
status: statusCode,
});
expect(request.status).toEqual(statusCode);
return p1;
}).then(() => {
expect(onSuccess).toHaveBeenCalledWith(body.map((str: string) => str.trim()));
});
});
it('blocks the request with custom method', () => {
const blockDefer = SyncTasks.Defer<void>();
let blockResolver: () => void = () => undefined;
const blockPromise = new Promise<void>((res, rej) => { blockResolver = res; });
class Http extends GenericRestClient {
protected _blockRequestUntil(): SyncTasks.Promise<void> {
return blockDefer.promise();
protected _blockRequestUntil(): Promise<void> {
return blockPromise;
}
}
@ -415,21 +473,29 @@ describe('GenericRestClient', () => {
const http = new Http(BASE_URL);
const path = '/auth';
http.performApiGet(path)
const p1 = http.performApiGet(path)
.then(onSuccess);
let request = jasmine.Ajax.requests.mostRecent();
let request: any;
return asyncTick().then(() => {
request = jasmine.Ajax.requests.mostRecent();
expect(request).toBeUndefined();
blockDefer.resolve(void 0);
expect(request).toBeUndefined();
blockResolver();
request = jasmine.Ajax.requests.mostRecent();
request.respondWith({ status: statusCode });
expect(onSuccess).toHaveBeenCalled();
return asyncTick();
}).then(() => {
request = jasmine.Ajax.requests.mostRecent();
request.respondWith({ status: statusCode });
return p1;
}).then(() => {
expect(onSuccess).toHaveBeenCalled();
});
});
it('aborting request after failure w/retry', () => {
let blockDefer = SyncTasks.Defer<void>();
let blockResolver: () => void = () => undefined;
let blockPromise = new Promise<void>((res, rej) => { blockResolver = res; });
class Http extends GenericRestClient {
constructor(endpointUrl: string) {
@ -437,8 +503,8 @@ describe('GenericRestClient', () => {
this._defaultOptions.customErrorHandler = this._customErrorHandler;
this._defaultOptions.timeout = 1;
}
protected _blockRequestUntil(): SyncTasks.Promise<void> {
return blockDefer.promise();
protected _blockRequestUntil(): Promise<void> {
return blockPromise;
}
protected _customErrorHandler = (webRequest: SimpleWebRequestBase, errorResponse: WebErrorResponse): ErrorHandlingType => {
@ -455,38 +521,38 @@ describe('GenericRestClient', () => {
const http = new Http(BASE_URL);
const path = '/auth';
const req = http.performApiGet(path)
const resp = http.performApiGetDetailed(path);
const p1 = resp.promise
.then(onSuccess)
.catch(onFailure);
blockDefer.resolve(void 0);
const request1 = jasmine.Ajax.requests.mostRecent();
return asyncTick().then(() => {
blockResolver();
return asyncTick();
}).then(() => {
const request1 = jasmine.Ajax.requests.mostRecent();
// Reset blockuntil so retries may block
blockDefer = SyncTasks.Defer<void>();
// Reset blockuntil so retries may block
blockPromise = new Promise<void>((res, rej) => { blockResolver = res; });
request1.respondWith({ status: statusCode });
expect(onSuccess).not.toHaveBeenCalled();
expect(onFailure).not.toHaveBeenCalled();
request1.respondWith({ status: statusCode });
return asyncTick();
}).then(() => {
expect(onSuccess).not.toHaveBeenCalled();
expect(onFailure).not.toHaveBeenCalled();
// Calls abort function
req.cancel();
expect(onSuccess).not.toHaveBeenCalled();
expect(onFailure).toHaveBeenCalled();
resp.req.abort();
return p1;
}).then(() => {
expect(onSuccess).not.toHaveBeenCalled();
expect(onFailure).toHaveBeenCalled();
});
});
describe('Timing related tests' , () => {
beforeEach(() => {
jasmine.clock().install();
});
afterEach(() => {
jasmine.clock().uninstall();
});
it('failed request with retry handles multiple _respond calls', () => {
let blockDefer = SyncTasks.Defer<void>();
let blockResolver: () => void = () => undefined;
let blockPromise = new Promise<void>((res, rej) => { blockResolver = res; });
class Http extends GenericRestClient {
constructor(endpointUrl: string) {
@ -494,8 +560,8 @@ describe('GenericRestClient', () => {
this._defaultOptions.customErrorHandler = this._customErrorHandler;
this._defaultOptions.timeout = 1;
}
protected _blockRequestUntil(): SyncTasks.Promise<void> {
return blockDefer.promise();
protected _blockRequestUntil(): Promise<void> {
return blockPromise;
}
protected _customErrorHandler = (): ErrorHandlingType => {
@ -508,27 +574,37 @@ describe('GenericRestClient', () => {
const http = new Http(BASE_URL);
const path = '/auth';
http.performApiGet(path)
const p1 = http.performApiGet(path)
.then(onSuccess);
blockDefer.resolve(void 0);
const request1 = jasmine.Ajax.requests.mostRecent();
return asyncTick().then(() => {
blockResolver();
return asyncTick();
}).then(() => {
const request1 = jasmine.Ajax.requests.mostRecent();
// Reset blockuntil so retries may block
blockDefer = SyncTasks.Defer<void>();
// Reset blockuntil so retries may block
blockPromise = new Promise<void>((res, rej) => { blockResolver = res; });
// Store this so we're able to emulate double-request callbacks
const onloadToCall = request1.onload as any;
request1.respondWith({ status: statusCode });
onloadToCall(undefined);
expect(onSuccess).not.toHaveBeenCalled();
blockDefer.resolve(void 0);
// Store this so we're able to emulate double-request callbacks
const onloadToCall = request1.onload as any;
request1.respondWith({ status: statusCode });
onloadToCall(undefined);
jasmine.clock().tick(100);
return asyncTick();
}).then(() => {
expect(onSuccess).not.toHaveBeenCalled();
blockResolver();
const request2 = jasmine.Ajax.requests.mostRecent();
request2.respondWith({ status: 200 });
expect(onSuccess).toHaveBeenCalled();
jasmine.clock().tick(100);
return asyncTick();
}).then(() => {
const request2 = jasmine.Ajax.requests.mostRecent();
request2.respondWith({ status: 200 });
return p1;
}).then(() => {
expect(onSuccess).toHaveBeenCalled();
});
});
});
});

Просмотреть файл

@ -1,5 +1,4 @@
import * as faker from 'faker';
import * as SyncTasks from 'synctasks';
import {
ErrorHandlingType,
@ -9,19 +8,13 @@ import {
WebRequestPriority,
} from '../src/SimpleWebRequest';
import { DETAILED_RESPONSE } from './helpers';
import { asyncTick, DETAILED_RESPONSE } from './helpers';
describe('SimpleWebRequest', () => {
let catchExceptions = false;
const status = 200;
beforeEach(() => {
catchExceptions = SyncTasks.config.catchExceptions;
SyncTasks.config.catchExceptions = false;
jasmine.Ajax.install();
});
afterEach(() => {
SyncTasks.config.catchExceptions = catchExceptions;
jasmine.Ajax.uninstall();
});
@ -42,17 +35,21 @@ describe('SimpleWebRequest', () => {
responseParsingException,
};
new SimpleWebRequest<string>(method, url, requestOptions)
let request: any;
setTimeout(() => {
request = jasmine.Ajax.requests.mostRecent();
request.respondWith({ responseText: JSON.stringify(''), status: statusCode });
}, 0);
return new SimpleWebRequest<string>(method, url, requestOptions)
.start()
.then(onSuccess);
const request = jasmine.Ajax.requests.mostRecent();
request.respondWith({ responseText: JSON.stringify(''), status: statusCode });
expect(request.url).toEqual(url);
expect(request.method).toEqual(method);
expect(request.status).toEqual(statusCode);
expect(onSuccess).toHaveBeenCalledWith(response);
.then(onSuccess)
.then(() => {
expect(request.url).toEqual(url);
expect(request.method).toEqual(method);
expect(request.status).toEqual(statusCode);
expect(onSuccess).toHaveBeenCalledWith(response);
});
});
it('sends json POST request', () => {
@ -77,17 +74,21 @@ describe('SimpleWebRequest', () => {
responseParsingException,
};
new SimpleWebRequest<string>(method, url, requestOptions)
let request: any;
setTimeout(() => {
request = jasmine.Ajax.requests.mostRecent();
request.respondWith({ responseText: JSON.stringify(body), status: statusCode });
}, 0);
return new SimpleWebRequest<string>(method, url, requestOptions)
.start()
.then(onSuccess);
const request = jasmine.Ajax.requests.mostRecent();
request.respondWith({ responseText: JSON.stringify(body), status: statusCode });
expect(request.url).toEqual(url);
expect(request.method).toEqual(method);
expect(request.status).toEqual(statusCode);
expect(onSuccess).toHaveBeenCalledWith(response);
.then(onSuccess)
.then(() => {
expect(request.url).toEqual(url);
expect(request.method).toEqual(method);
expect(request.status).toEqual(statusCode);
expect(onSuccess).toHaveBeenCalledWith(response);
});
});
it('allows to set request headers', () => {
@ -98,14 +99,16 @@ describe('SimpleWebRequest', () => {
const method = 'POST';
const url = faker.internet.url();
new SimpleWebRequest<string>(url, method, {}, () => headers).start();
let request: any;
setTimeout(() => {
request = jasmine.Ajax.requests.mostRecent();
request.respondWith({ status: 200 });
}, 0);
const request = jasmine.Ajax.requests.mostRecent();
expect(request.requestHeaders['X-Requested-With']).toEqual(headers['X-Requested-With']);
expect(request.requestHeaders['Max-Forwards']).toEqual(headers['Max-Forwards']);
request.respondWith({ status });
return new SimpleWebRequest<string>(method, url, {}, () => headers).start().then(() => {
expect(request.requestHeaders['X-Requested-With']).toEqual(headers['X-Requested-With']);
expect(request.requestHeaders['Max-Forwards']).toEqual(headers['Max-Forwards']);
});
});
it('forbids to set Accept header', () => {
@ -117,9 +120,9 @@ describe('SimpleWebRequest', () => {
const method = 'GET';
const url = faker.internet.url();
const error = `Don't set Accept with options.headers -- use it with the options.acceptType property`;
const request = new SimpleWebRequest<string>(url, method, {}, () => headers);
const request = new SimpleWebRequest<string>(method, url, {}, () => headers);
expect(() => request.start()).toThrowError(error);
expect(() => (request as any)._fire()).toThrowError(error);
expect(console.error).toHaveBeenCalledWith(error);
});
@ -132,9 +135,9 @@ describe('SimpleWebRequest', () => {
const method = 'GET';
const url = faker.internet.url();
const error = `Don't set Content-Type with options.headers -- use it with the options.contentType property`;
const request = new SimpleWebRequest<string>(url, method, {}, () => headers);
const request = new SimpleWebRequest<string>(method, url, {}, () => headers);
expect(() => request.start()).toThrowError(error);
expect(() => (request as any)._fire()).toThrowError(error);
expect(console.error).toHaveBeenCalledWith(error);
});
@ -161,132 +164,186 @@ describe('SimpleWebRequest', () => {
const onSuccessCritical2 = jasmine.createSpy('onSuccessCritical2');
const status = 200;
new SimpleWebRequest<string>(url, method, { priority: WebRequestPriority.Low }).start().then(onSuccessLow1);
const p1 = new SimpleWebRequest<string>(method, url, { priority: WebRequestPriority.Low })
.start().then(onSuccessLow1);
jasmine.clock().tick(10);
new SimpleWebRequest<string>(url, method, { priority: WebRequestPriority.Critical }).start().then(onSuccessCritical1);
const p2 = new SimpleWebRequest<string>(method, url, { priority: WebRequestPriority.Critical })
.start().then(onSuccessCritical1);
jasmine.clock().tick(10);
new SimpleWebRequest<string>(url, method, { priority: WebRequestPriority.Low }).start().then(onSuccessLow2);
const p3 = new SimpleWebRequest<string>(method, url, { priority: WebRequestPriority.Low })
.start().then(onSuccessLow2);
jasmine.clock().tick(10);
SimpleWebRequestOptions.MaxSimultaneousRequests = 1;
// add a new request to kick the queue
new SimpleWebRequest<string>(url, method, { priority: WebRequestPriority.Critical }).start().then(onSuccessCritical2);
const p4 = new SimpleWebRequest<string>(method, url, { priority: WebRequestPriority.Critical })
.start().then(onSuccessCritical2);
// only one is executed
expect(jasmine.Ajax.requests.count()).toBe(1);
jasmine.Ajax.requests.mostRecent().respondWith({status});
// they're executed in correct order
expect(onSuccessCritical1).toHaveBeenCalled();
asyncTick().then(() => {
// only one is executed
expect(jasmine.Ajax.requests.count()).toBe(1);
jasmine.Ajax.requests.mostRecent().respondWith({status});
});
jasmine.Ajax.requests.mostRecent().respondWith({status});
expect(onSuccessCritical2).toHaveBeenCalled();
return p2.then(() => {
// they're executed in correct order
expect(onSuccessCritical1).toHaveBeenCalled();
expect(jasmine.Ajax.requests.count()).toBe(2);
jasmine.Ajax.requests.mostRecent().respondWith({status});
jasmine.Ajax.requests.mostRecent().respondWith({status});
expect(onSuccessLow1).toHaveBeenCalled();
return p4;
}).then(() => {
expect(onSuccessCritical2).toHaveBeenCalled();
expect(jasmine.Ajax.requests.count()).toBe(3);
jasmine.Ajax.requests.mostRecent().respondWith({status});
jasmine.Ajax.requests.mostRecent().respondWith({status});
expect(onSuccessLow2).toHaveBeenCalled();
return p1;
}).then(() => {
expect(onSuccessLow1).toHaveBeenCalled();
expect(jasmine.Ajax.requests.count()).toBe(4);
jasmine.Ajax.requests.mostRecent().respondWith({status});
return p3;
}).then(() => {
expect(onSuccessLow2).toHaveBeenCalled();
});
});
it('blocks the request with custom promise', () => {
SimpleWebRequestOptions.MaxSimultaneousRequests = 1;
const url = faker.internet.url();
const method = 'GET';
const blockDefer = SyncTasks.Defer<void>();
let blockResolver: () => void = () => undefined;
const blockPromise = new Promise<void>((res, rej) => { blockResolver = res; });
const onSuccess1 = jasmine.createSpy('onSuccess1');
new SimpleWebRequest<string>(url, method, {}, undefined, () => blockDefer.promise()).start().then(onSuccess1);
const p1 = new SimpleWebRequest<string>(method, url, {}, undefined, () => blockPromise).start().then(onSuccess1);
expect(jasmine.Ajax.requests.count()).toBe(0);
blockDefer.resolve(void 0);
asyncTick().then(() => {
expect(jasmine.Ajax.requests.count()).toBe(0);
blockResolver();
const request = jasmine.Ajax.requests.mostRecent();
request.respondWith({ status: 200 });
expect(onSuccess1).toHaveBeenCalled();
return asyncTick();
}).then(() => {
const request = jasmine.Ajax.requests.mostRecent();
request.respondWith({ status: 200 });
});
return p1.then(() => {
expect(onSuccess1).toHaveBeenCalled();
});
});
it('after the request is unblocked, it\'s returned to the queue with correct priority', () => {
const url = faker.internet.url();
const method = 'GET';
const blockDefer = SyncTasks.Defer<void>();
let blockResolver: () => void = () => undefined;
const blockPromise = new Promise<void>((res, rej) => { blockResolver = res; });
const onSuccessHigh = jasmine.createSpy('onSuccessHigh');
const onSuccessLow = jasmine.createSpy('onSuccessLow');
const onSuccessCritical = jasmine.createSpy('onSuccessCritical');
new SimpleWebRequest<string>(url, method, { priority: WebRequestPriority.High }, undefined, () => blockDefer.promise())
const p1 = new SimpleWebRequest<string>(method, url, { priority: WebRequestPriority.High }, undefined, () => blockPromise)
.start()
.then(onSuccessHigh);
jasmine.clock().tick(10);
new SimpleWebRequest<string>(url, method, { priority: WebRequestPriority.Low }).start().then(onSuccessLow);
const p2 = new SimpleWebRequest<string>(method, url, { priority: WebRequestPriority.Low }).start().then(onSuccessLow);
jasmine.clock().tick(10);
SimpleWebRequestOptions.MaxSimultaneousRequests = 1;
// add a new request to kick the queue
new SimpleWebRequest<string>(url, method, { priority: WebRequestPriority.Critical }).start().then(onSuccessCritical);
const p3 = new SimpleWebRequest<string>(method, url, { priority: WebRequestPriority.Critical }).start().then(onSuccessCritical);
// unblock the request
blockDefer.resolve(void 0);
blockResolver();
jasmine.Ajax.requests.mostRecent().respondWith({ status: 200 });
// first the critical one gets sent
expect(onSuccessCritical).toHaveBeenCalled();
// have to do an awkward async tick to get the blocking blocker to resolve before the request goes out
asyncTick().then(() => {
expect(jasmine.Ajax.requests.count()).toBe(1);
jasmine.Ajax.requests.mostRecent().respondWith({ status: 200 });
});
// then the high, which was returned to the queue at after getting unblocked
jasmine.Ajax.requests.mostRecent().respondWith({ status: 200 });
expect(onSuccessHigh).toHaveBeenCalled();
return p3.then(() => {
// first the critical one gets sent
expect(onSuccessCritical).toHaveBeenCalled();
expect(jasmine.Ajax.requests.count()).toBe(2);
// and the low priority one gets sent last
jasmine.Ajax.requests.mostRecent().respondWith({ status: 200 });
expect(onSuccessLow).toHaveBeenCalled();
// then the high, which was returned to the queue at after getting unblocked
jasmine.Ajax.requests.mostRecent().respondWith({ status: 200 });
return p1;
}).then(() => {
expect(onSuccessHigh).toHaveBeenCalled();
expect(jasmine.Ajax.requests.count()).toBe(3);
// and the low priority one gets sent last
jasmine.Ajax.requests.mostRecent().respondWith({ status: 200 });
return p2;
}).then(() => {
expect(onSuccessLow).toHaveBeenCalled();
});
});
it('checks the blocked function again, once the request is on top of the queue', () => {
const url = faker.internet.url();
const method = 'GET';
const blockDefer = SyncTasks.Defer<void>();
let blockResolver: () => void = () => undefined;
const blockPromise = new Promise<void>((res, rej) => { blockResolver = res; });
const onSuccessCritical = jasmine.createSpy('onSuccessCritical');
const onSuccessHigh = jasmine.createSpy('onSuccessHigh');
const onSuccessHigh2 = jasmine.createSpy('onSuccessHigh2');
const blockSpy = jasmine.createSpy('blockSpy').and.callFake(() => blockDefer.promise());
const blockSpy = jasmine.createSpy('blockSpy').and.callFake(() => blockPromise);
new SimpleWebRequest<string>(url, method, { priority: WebRequestPriority.Critical }, undefined, blockSpy)
const p1 = new SimpleWebRequest<string>(method, url, { priority: WebRequestPriority.Critical }, undefined, blockSpy)
.start()
.then(onSuccessCritical);
jasmine.clock().tick(10);
new SimpleWebRequest<string>(url, method, { priority: WebRequestPriority.High }).start().then(onSuccessHigh);
const p2 = new SimpleWebRequest<string>(method, url, { priority: WebRequestPriority.High }).start().then(onSuccessHigh);
jasmine.clock().tick(10);
SimpleWebRequestOptions.MaxSimultaneousRequests = 1;
// add a new request to kick the queue
new SimpleWebRequest<string>(url, method, { priority: WebRequestPriority.High }).start().then(onSuccessHigh2);
const p3 = new SimpleWebRequest<string>(method, url, { priority: WebRequestPriority.High }).start().then(onSuccessHigh2);
expect(blockSpy).toHaveBeenCalled();
asyncTick().then(() => {
expect(blockSpy).toHaveBeenCalled();
expect(jasmine.Ajax.requests.count()).toBe(1);
jasmine.Ajax.requests.mostRecent().respondWith({ status: 200 });
});
jasmine.Ajax.requests.mostRecent().respondWith({ status: 200 });
expect(onSuccessHigh).toHaveBeenCalled();
return p2.then(() => {
expect(onSuccessHigh).toHaveBeenCalled();
expect(jasmine.Ajax.requests.count()).toBe(2);
// unblock the request, it will go back to the queue after the currently executed request
blockDefer.resolve(void 0);
// unblock the request, it will go back to the queue after the currently executed request
blockResolver();
jasmine.Ajax.requests.mostRecent().respondWith({ status: 200 });
expect(onSuccessHigh2).toHaveBeenCalled();
jasmine.Ajax.requests.mostRecent().respondWith({ status: 200 });
// check if the request at the top of the queue got called again
expect(blockSpy).toHaveBeenCalledTimes(2);
return p3;
}).then(() => {
expect(onSuccessHigh2).toHaveBeenCalled();
expect(jasmine.Ajax.requests.count()).toBe(3);
jasmine.Ajax.requests.mostRecent().respondWith({ status: 200 });
expect(onSuccessCritical).toHaveBeenCalled();
jasmine.Ajax.requests.mostRecent().respondWith({ status: 200 });
return p1;
}).then(() => {
expect(onSuccessCritical).toHaveBeenCalled();
});
});
it('fails the request, if the blocking promise rejects', done => {
SimpleWebRequestOptions.MaxSimultaneousRequests = 1;
const url = faker.internet.url();
const method = 'GET';
const blockDefer = SyncTasks.Defer<void>();
let blockRejecter: (err: any) => void = () => undefined;
const blockPromise = new Promise<void>((res, rej) => { blockRejecter = rej; });
const errorString = 'Terrible error';
new SimpleWebRequest<string>(url, method, { priority: WebRequestPriority.Critical }, undefined, () => blockDefer.promise())
new SimpleWebRequest<string>(method, url, { priority: WebRequestPriority.Critical }, undefined, () => blockPromise)
.start()
.then(() => fail(), (err: WebErrorResponse) => {
expect(err.statusCode).toBe(0);
@ -294,21 +351,30 @@ describe('SimpleWebRequest', () => {
done();
});
blockDefer.reject(errorString);
blockRejecter(errorString);
});
it('does not attempt to fire aborted request, if it was aborted while blocked', () => {
SimpleWebRequestOptions.MaxSimultaneousRequests = 1;
const url = faker.internet.url();
const method = 'GET';
const blockDefer = SyncTasks.Defer<void>();
new SimpleWebRequest<string>(url, method, { priority: WebRequestPriority.Critical }, undefined, () => blockDefer.promise())
.start()
.cancel();
blockDefer.resolve(void 0);
expect(jasmine.Ajax.requests.count()).toBe(0);
let blockResolver: () => void = () => undefined;
const blockPromise = new Promise<void>((res, rej) => { blockResolver = res; });
const req = new SimpleWebRequest<string>(method, url, { priority: WebRequestPriority.Critical }, undefined, () => blockPromise);
const p1 = req.start();
return asyncTick().then(() => {
expect(jasmine.Ajax.requests.count()).toBe(0);
req.abort();
return asyncTick();
}).then(() => {
blockResolver();
return p1;
}).then(() => {
fail();
}, () => {
expect(jasmine.Ajax.requests.count()).toBe(0);
});
});
});
@ -324,7 +390,7 @@ describe('SimpleWebRequest', () => {
it('fails the request with "timedOut: true" if it times out without retries', done => {
const url = faker.internet.url();
const method = 'GET';
new SimpleWebRequest<string>(url, method, { timeout: 10, customErrorHandler: () => ErrorHandlingType.DoNotRetry })
new SimpleWebRequest<string>(method, url, { timeout: 10, customErrorHandler: () => ErrorHandlingType.DoNotRetry })
.start()
.then(() => {
expect(false).toBeTruthy();
@ -335,17 +401,20 @@ describe('SimpleWebRequest', () => {
done();
});
jasmine.clock().tick(10);
asyncTick().then(() => {
jasmine.clock().tick(10);
});
});
it('timedOut flag is reset on retry', done => {
const url = faker.internet.url();
const method = 'GET';
const requestPromise = new SimpleWebRequest<string>(url, method, {
const req = new SimpleWebRequest<string>(method, url, {
timeout: 10,
retries: 1,
customErrorHandler: () => ErrorHandlingType.RetryCountedWithBackoff,
}).start();
});
const requestPromise = req.start();
requestPromise
.then(() => {
@ -359,8 +428,10 @@ describe('SimpleWebRequest', () => {
});
// first try will time out, the second one will be aborted
jasmine.clock().tick(10);
requestPromise.cancel();
asyncTick().then(() => {
jasmine.clock().tick(10);
req.abort();
});
});
});

Просмотреть файл

@ -20,3 +20,10 @@ export const DETAILED_RESPONSE = {
body: '',
url: '',
};
export function asyncTick(): Promise<void> {
return new Promise((res, rej) => {
setTimeout(res, 0);
jasmine.clock().tick(10);
});
}

Просмотреть файл

@ -8,6 +8,7 @@
"noUnusedLocals": true,
"declaration": true,
"noResolve": false,
"sourceMap": true,
"module": "commonjs",
"target": "es5",
"outDir": "dist",