Skip to content

Commit

Permalink
Merge branch 'master' into DevExpressgh-1956
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexKamaev authored Apr 19, 2018
2 parents cfae5da + 5bccb81 commit 752e810
Show file tree
Hide file tree
Showing 58 changed files with 1,259 additions and 93 deletions.
2 changes: 1 addition & 1 deletion .publishrc
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"gitTag": true
},
"confirm": true,
"publishTag": "latest",
"publishTag": "alpha",
"prePublishScript": "gulp test-server",
"postPublishScript": "gulp docker-publish"
}
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "testcafe",
"description": "Automated browser testing for the modern web development stack.",
"license": "MIT",
"version": "0.19.2",
"version": "0.20.0-alpha.1",
"author": {
"name": "Developer Express Inc.",
"url": "https://www.devexpress.com/"
Expand Down Expand Up @@ -104,7 +104,7 @@
"source-map-support": "^0.4.0",
"strip-bom": "^2.0.0",
"testcafe-browser-tools": "1.5.1",
"testcafe-hammerhead": "13.4.0",
"testcafe-hammerhead": "13.4.1",
"testcafe-legacy-api": "3.1.7",
"testcafe-reporter-json": "^2.1.0",
"testcafe-reporter-list": "^2.1.0",
Expand All @@ -130,7 +130,7 @@
"cross-spawn": "^4.0.0",
"del": "^2.2.0",
"dom-walk": "^0.1.1",
"eslint-plugin-hammerhead": "0.1.6",
"eslint-plugin-hammerhead": "0.1.7",
"express": "^4.13.3",
"express-ntlm": "^2.1.5",
"gulp": "^3.9.0",
Expand Down
17 changes: 17 additions & 0 deletions src/api/exportable-lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,22 @@ import ClientFunctionBuilder from '../../client-functions/client-function-builde
import SelectorBuilder from '../../client-functions/selectors/selector-builder';
import { createRole, createAnonymousRole } from '../../role';
import testControllerProxy from '../test-controller/proxy';
import createRequestLogger from '../request-hooks/request-logger';
import createRequestMock from '../request-hooks/request-mock';
import RequestHook from '../request-hooks/hook';

function Role (loginPage, initFn, options) {
return createRole(loginPage, initFn, options);
}

function RequestMock () {
return createRequestMock();
}

function RequestLogger (requestFilterRuleInit, logOptions) {
return createRequestLogger(requestFilterRuleInit, logOptions);
}

Role.anonymous = createAnonymousRole;

export default {
Expand All @@ -24,6 +35,12 @@ export default {
return builder.getFunction();
},

RequestLogger,

RequestMock,

RequestHook,

t: testControllerProxy
};

5 changes: 5 additions & 0 deletions src/api/request-hooks/assert-type.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { assertType, is } from '../../errors/runtime/type-assertions';

export default function assertRequestHookType (hooks) {
hooks.forEach(hook => assertType(is.requestHookSubclass, 'requestHooks', `Hook`, hook));
}
42 changes: 42 additions & 0 deletions src/api/request-hooks/hook.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { RequestFilterRule } from 'testcafe-hammerhead';
import { castArray } from 'lodash';

export default class RequestHook {
constructor (requestFilterRules, responseEventConfigureOpts) {
this.requestFilterRules = this._prepareRequestFilterRules(requestFilterRules);
this._instantiatedRequestFilterRules = [];
this.responseEventConfigureOpts = responseEventConfigureOpts;
}

_prepareRequestFilterRules (rules) {
if (rules)
return castArray(rules);

return [RequestFilterRule.ANY];
}

_instantiateRequestFilterRules () {
this.requestFilterRules.forEach(rule => {
if (rule instanceof RequestFilterRule)
this._instantiatedRequestFilterRules.push(rule);
else
this._instantiatedRequestFilterRules.push(new RequestFilterRule(rule));
});
}

onRequest (/*RequestEvent event*/) {
throw new Error('Not implemented');
}

_onConfigureResponse (event) {
if (!this.responseEventConfigureOpts)
return;

event.opts.includeHeaders = this.responseEventConfigureOpts.includeHeaders;
event.opts.includeBody = this.responseEventConfigureOpts.includeBody;
}

onResponse (/*ResponseEvent event*/) {
throw new Error('Not implemented');
}
}
121 changes: 121 additions & 0 deletions src/api/request-hooks/request-logger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { ConfigureResponseEventOptions } from 'testcafe-hammerhead';
import RequestHook from './hook';
import { parse as parseUserAgent } from 'useragent';
import testRunTracker from '../test-run-tracker';
import ReExecutablePromise from '../../utils/re-executable-promise';
import { RequestHookConfigureAPIError } from '../../errors/test-run/index';

const DEFAULT_OPTIONS = {
logRequestHeaders: false,
logRequestBody: false,
stringifyRequestBody: false,
logResponseHeaders: false,
logResponseBody: false,
stringifyResponseBody: false
};

class RequestLogger extends RequestHook {
constructor (requestFilterRuleInit, options) {
options = Object.assign({}, DEFAULT_OPTIONS, options);
RequestLogger._assertLogOptions(options);

const configureResponseEventOptions = new ConfigureResponseEventOptions(options.logResponseHeaders, options.logResponseBody);

super(requestFilterRuleInit, configureResponseEventOptions);

this.options = options;

this._internalRequests = {};
}

static _assertLogOptions (logOptions) {
if (!logOptions.logRequestBody && logOptions.stringifyRequestBody)
throw new RequestHookConfigureAPIError(RequestLogger.name, 'Cannot stringify the request body because it is not logged. Specify { logRequestBody: true } in log options.');

if (!logOptions.logResponseBody && logOptions.stringifyResponseBody)
throw new RequestHookConfigureAPIError(RequestLogger.name, 'Cannot stringify the response body because it is not logged. Specify { logResponseBody: true } in log options.');
}

onRequest (event) {
const userAgent = parseUserAgent(event._requestInfo.userAgent).toString();

const loggedReq = {
id: event._requestInfo.requestId,
testRunId: event._requestInfo.sessionId,
userAgent,
request: {
url: event._requestInfo.url,
method: event._requestInfo.method,
}
};

if (this.options.logRequestHeaders)
loggedReq.request.headers = Object.assign({}, event._requestInfo.headers);

if (this.options.logRequestBody)
loggedReq.request.body = this.options.stringifyRequestBody ? event._requestInfo.body.toString() : event._requestInfo.body;

this._internalRequests[loggedReq.id] = loggedReq;
}

onResponse (event) {
const loggerReq = this._internalRequests[event.requestId];

if (!loggerReq)
throw new TypeError(`Cannot find a recorded request with id=${event.id}. This is an internal TestCafe problem. Please contact the TestCafe team and provide an example to reproduce the problem.`);

loggerReq.response = {};
loggerReq.response.statusCode = event.statusCode;

if (this.options.logResponseHeaders)
loggerReq.response.headers = Object.assign({}, event.headers);

if (this.options.logResponseBody)
loggerReq.response.body = this.options.stringifyResponseBody ? event.body.toString() : event.body;
}

_prepareInternalRequestInfo () {
const testRun = testRunTracker.resolveContextTestRun();
let preparedRequests = Object.values(this._internalRequests);

if (testRun)
preparedRequests = preparedRequests.filter(r => r.testRunId === testRun.id);

return preparedRequests;
}

// API
contains (predicate) {
return ReExecutablePromise.fromFn(async () => {
return !!this._prepareInternalRequestInfo().find(predicate);
});
}

count (predicate) {
return ReExecutablePromise.fromFn(async () => {
return this._prepareInternalRequestInfo().filter(predicate).length;
});
}

clear () {
const testRun = testRunTracker.resolveContextTestRun();

if (testRun) {
Object.keys(this._internalRequests).forEach(id => {
if (this._internalRequests[id].testRunId === testRun.id)
delete this._internalRequests[id];
});
}
else
this._internalRequests = {};
}

get requests () {
return this._prepareInternalRequestInfo();
}
}

export default function createRequestLogger (requestFilterRuleInit, logOptions) {
return new RequestLogger(requestFilterRuleInit, logOptions);
}

48 changes: 48 additions & 0 deletions src/api/request-hooks/request-mock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import RequestHook from './hook';
import { ResponseMock, RequestFilterRule } from 'testcafe-hammerhead';
import { RequestHookConfigureAPIError } from '../../errors/test-run/index';

class RequestMock extends RequestHook {
constructor () {
super();

this.pendingRequestFilterRuleInit = null;
this.mocks = new Map();
}

onRequest (event) {
const mock = this.mocks.get(event._requestFilterRule);

event.setMock(mock);
}

onResponse () {}

// API
onRequestTo (requestFilterRuleInit) {
if (this.pendingRequestFilterRuleInit)
throw new RequestHookConfigureAPIError(RequestMock.name, "The 'respond' method was not called after 'onRequestTo'. You must call the 'respond' method to provide the mocked response.");

this.pendingRequestFilterRuleInit = requestFilterRuleInit;

return this;
}

respond (body, statusCode, headers) {
if (!this.pendingRequestFilterRuleInit)
throw new RequestHookConfigureAPIError(RequestMock.name, "The 'onRequestTo' method was not called before 'respond'. You must call the 'onRequestTo' method to provide the URL requests to which are mocked.");

const mock = new ResponseMock(body, statusCode, headers);
const rule = new RequestFilterRule(this.pendingRequestFilterRuleInit);

this.requestFilterRules.push(rule);
this.mocks.set(rule, mock);
this.pendingRequestFilterRuleInit = null;

return this;
}
}

export default function createRequestMock () {
return new RequestMock();
}
14 changes: 14 additions & 0 deletions src/api/structure/fixture.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { assertType, is } from '../../errors/runtime/type-assertions';
import handleTagArgs from '../../utils/handle-tag-args';
import TestingUnit from './testing-unit';
import wrapTestFunction from '../wrap-test-function';
import assertRequestHookType from '../request-hooks/assert-type';
import { flattenDeep as flatten } from 'lodash';

export default class Fixture extends TestingUnit {
constructor (testFile) {
Expand All @@ -17,6 +19,8 @@ export default class Fixture extends TestingUnit {
this.beforeFn = null;
this.afterFn = null;

this.requestHooks = [];

return this.apiOrigin;
}

Expand Down Expand Up @@ -62,6 +66,16 @@ export default class Fixture extends TestingUnit {

return this.apiOrigin;
}

_requestHooks$ (...hooks) {
hooks = flatten(hooks);

assertRequestHookType(hooks);

this.requestHooks = hooks;

return this.apiOrigin;
}
}

TestingUnit._makeAPIListForChildClass(Fixture);
19 changes: 16 additions & 3 deletions src/api/structure/test.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import TestingUnit from './testing-unit';
import { assertType, is } from '../../errors/runtime/type-assertions';
import wrapTestFunction from '../wrap-test-function';
import assertRequestHookType from '../request-hooks/assert-type';
import { flattenDeep as flatten, union } from 'lodash';

export default class Test extends TestingUnit {
constructor (testFile) {
super(testFile);

this.fixture = testFile.currentFixture;

this.fn = null;
this.beforeFn = null;
this.afterFn = null;
this.fn = null;
this.beforeFn = null;
this.afterFn = null;
this.requestHooks = this.fixture.requestHooks.length ? Array.from(this.fixture.requestHooks) : [];

return this.apiOrigin;
}
Expand Down Expand Up @@ -43,6 +46,16 @@ export default class Test extends TestingUnit {

return this.apiOrigin;
}

_requestHooks$ (...hooks) {
hooks = flatten(hooks);

assertRequestHookType(hooks);

this.requestHooks = union(this.requestHooks, hooks);

return this.apiOrigin;
}
}

TestingUnit._makeAPIListForChildClass(Test);
2 changes: 1 addition & 1 deletion src/api/test-controller/assertion.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default class Assertion {
expected: assertionArgs.expected,
expected2: assertionArgs.expected2,
message: message,
options: { timeout: options.timeout }
options: { timeout: options.timeout, allowUnawaitedPromise: options.allowUnawaitedPromise }
});
}

Expand Down
Loading

0 comments on commit 752e810

Please sign in to comment.