UNPKG

flagpole

Version:

Simple and fast DOM integration, headless or headful browser, and REST API testing framework.

753 lines 26.4 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); const enums_1 = require("./enums"); const browsercontrol_1 = require("./browsercontrol"); const responsefactory_1 = require("./responsefactory"); const assertionresult_1 = require("./logging/assertionresult"); const httpresponse_1 = require("./httpresponse"); const resourceresponse_1 = require("./resourceresponse"); const heading_1 = require("./logging/heading"); const comment_1 = require("./logging/comment"); const logcollection_1 = require("./logging/logcollection"); const httprequest_1 = require("./httprequest"); const flagpoleexecution_1 = require("./flagpoleexecution"); const util_1 = require("./util"); const assertioncontext_1 = require("./assertioncontext"); const bluebird = require("bluebird"); class Scenario { constructor(suite, title) { this._log = new logcollection_1.LogCollection(); this._subscribers = []; this._nextCallbacks = []; this._nextMessages = []; this._beforeCallbacks = []; this._afterCallbacks = []; this._finallyCallbacks = []; this._failureCallbacks = []; this._successCallbacks = []; this._pipeCallbacks = []; this._timeScenarioInitialized = Date.now(); this._timeScenarioExecuted = null; this._timeRequestStarted = null; this._timeRequestLoaded = null; this._timeScenarioFinished = null; this._responseType = "html"; this._redirectChain = []; this._finalUrl = null; this._waitToExecute = false; this._waitTime = 0; this._flipAssertion = false; this._ignoreAssertion = false; this._browserControl = null; this._isMock = false; this._defaultBrowserOptions = { headless: true, recordConsole: true, outputConsole: false, }; this._defaultRequestOptions = { method: "get", }; this._aliasedData = {}; this._requestResolve = () => { }; this._finishedResolve = () => { }; this._disposition = "pending"; this.suite = suite; this._request = new httprequest_1.HttpRequest(this._defaultRequestOptions); this._title = title; this._response = new resourceresponse_1.ResourceResponse(this); this._requestPromise = new Promise((resolve) => { this._requestResolve = resolve; }); this._finishedPromise = new Promise((resolve) => { this._finishedResolve = resolve; }); } get responseType() { return this._responseType; } get title() { return this._title; } set title(newTitle) { if (this.hasExecuted) { throw "Can not change the scenario's title after execution has started."; } this._title = newTitle; } get totalDuration() { return this._timeScenarioFinished !== null ? this._timeScenarioFinished - this._timeScenarioInitialized : Date.now() - this._timeScenarioInitialized; } get executionDuration() { return this._timeScenarioFinished !== null && this._timeScenarioExecuted !== null ? this._timeScenarioFinished - this._timeScenarioExecuted : null; } get requestDuration() { return this._timeRequestStarted !== null && this._timeRequestLoaded !== null ? this._timeRequestLoaded - this._timeRequestStarted : null; } get hasFailed() { return this._log.items.some((item) => { return item.type == "resultFailure"; }); } get hasPassed() { return this.hasFinished && !this.hasFailed; } get isReadyToExecute() { return (!this.hasExecuted && !this.isImplicitWait && !this.isExplicitWait && this.hasNextCallbacks); } get hasExecuted() { return this._timeScenarioExecuted !== null; } get hasFinished() { return this.hasExecuted && this._timeScenarioFinished !== null; } get isImplicitWait() { return this.url === null || /{[A-Za-z0-9_ -]+}/.test(this.url); } get isExplicitWait() { return this._waitToExecute; } get hasNextCallbacks() { return this._nextCallbacks.length > 0; } get hasRequestStarted() { return this._timeRequestStarted !== null; } get url() { return this._request.uri; } set url(value) { if (this.hasRequestStarted) { throw "Can not change the URL after the request has already started."; } if (value !== null) { const match = /([A-Z]+) (.*)/.exec(value); if (match !== null) { const verb = match[1].toLowerCase(); if (httprequest_1.HttpMethodVerbAllowedValues.includes(verb)) { this.setMethod(verb); } value = match[2]; } if (/{[A-Za-z0-9_ -]+}/.test(value)) { this.wait(); } } this._request.uri = value; } get finalUrl() { return this._finalUrl; } get redirectCount() { return this._redirectChain.length; } get redirectChain() { return this._redirectChain; } get request() { return this._request; } static create(suite, title, type, opts) { return new Scenario(suite, title).setResponseType(type, opts); } set(aliasName, value) { this._aliasedData[aliasName] = value; return this; } get(aliasName) { return this._aliasedData[aliasName]; } getLog() { return __awaiter(this, void 0, void 0, function* () { return this._log.items; }); } subscribe(callback) { this._subscribers.push(callback); return this; } setJsonBody(json) { this.request.setJsonData(json); return this; } setRawBody(str) { this._request.data = str; return this; } verifyCert(verify) { this._request.verifyCert = verify; return this; } setProxy(proxy) { this._request.proxy = proxy; return this; } setTimeout(timeout) { this._request.timeout = typeof timeout === "number" ? { open: timeout, } : timeout; return this; } setFormData(form, isMultipart) { this._request.setFormData(form, isMultipart); return this; } setMaxRedirects(n) { this._request.maxRedirects = n; return this; } setBasicAuth(auth) { this._request.auth = auth; this._request.authType = "basic"; return this; } setDigestAuth(auth) { this._request.auth = auth; this._request.authType = "digest"; return this; } setBearerToken(token) { this.setHeader("Authorization", `Bearer ${token}`); return this; } setCookie(key, value) { this._request.setCookie(key, value); return this; } setHeaders(headers) { this._request.headers = Object.assign(Object.assign({}, this._request.headers), headers); return this; } setHeader(key, value) { this._request.setHeader(key, value); return this; } setMethod(method) { this._request.method = method; return this; } wait(bool = true) { if (this._waitToExecute && !bool) { this._waitTime = Date.now() - this._timeScenarioInitialized; } this._waitToExecute = bool; return this; } waitFor(thatScenario) { if (this === thatScenario) { throw new Error("Scenario can't wait for itself"); } this.wait(); thatScenario.success(() => __awaiter(this, void 0, void 0, function* () { this.wait(false); })); return this; } comment(input) { const type = util_1.toType(input); const message = type === "string" ? input : input.isFlagpoleValue ? input.toString() : JSON.stringify(input, null, 2); return this._pushToLog(new comment_1.LogComment(message)); } result(result) { return this._pushToLog(result); } ignore(assertions = true) { if (typeof assertions == "boolean") { this._ignoreAssertion = assertions; } else if (typeof assertions == "function") { this.ignore(true); assertions(); this.ignore(false); } return this; } pause(milliseconds) { this.next((context) => { context.comment(`Pause for ${milliseconds}ms`); return context.pause(milliseconds); }); return this; } open(url, opts) { if (this.hasExecuted) { throw `Can call open after scenario has executed`; } if (opts) { this._request.setOptions(opts); } this.url = String(url); this._isMock = false; return this; } next(a, b) { if (Array.isArray(a)) { a.forEach((callback) => { this._next(callback, null, true); }); } else { this._next(a, b, true); } return this; } nextPrepend(a, b) { return this._next(a, b, false); } skip(message) { return __awaiter(this, void 0, void 0, function* () { if (this.hasExecuted) { throw `Can't skip Scenario since it already started executing.`; } yield this._fireBefore(); this._publish(enums_1.ScenarioStatusEvent.executionProgress); this.comment(`Skipped ${message ? ": " + message : ""}`); yield this._markScenarioCompleted(null, null, "skipped"); return this; }); } cancel(message) { return __awaiter(this, void 0, void 0, function* () { if (this.hasExecuted) { throw new Error(`Can't cancel Scenario since it already started executing.`); } yield this._fireBefore(); this._publish(enums_1.ScenarioStatusEvent.executionProgress); this._markScenarioCompleted(`Cancelled ${message ? ": " + message : ""}`, null, "cancelled"); return this; }); } getBrowserControl() { this._browserControl = this._browserControl !== null ? this._browserControl : new browsercontrol_1.BrowserControl(); return this._browserControl; } execute(pathParams) { return __awaiter(this, void 0, void 0, function* () { if (this.hasExecuted) { throw "Scenario has already started executing. Can not call execute again."; } if (pathParams) { Object.keys(pathParams).forEach((key) => { var _a; this.url = ((_a = this.url) === null || _a === void 0 ? void 0 : _a.replace(`{${key}}`, String(pathParams[key]))) || null; }); } this.wait(false); return this; }); } go() { return __awaiter(this, void 0, void 0, function* () { if (this.hasExecuted) { throw "Scenario has already started executing. Can not call execute again."; } if (!this.isReadyToExecute) { throw "This scenario is not ready to execute."; } yield this._fireBefore(); this._isMock ? this._executeMock() : this._executeRequest(); this._publish(enums_1.ScenarioStatusEvent.executionProgress); return this; }); } success(a, b) { return this._pushCallbacks("success", "_successCallbacks", a, b); } failure(a, b) { return this._pushCallbacks("failure", "_failureCallbacks", a, b); } pipe(a, b) { return this._pushCallbacks("pipe", "_pipeCallbacks", a, b); } before(a, b) { return this._pushCallbacks("before", "_beforeCallbacks", a, b); } after(a, b) { return this._pushCallbacks("after", "_afterCallbacks", a, b); } finally(a, b) { return this._pushCallbacks("finally", "_finallyCallbacks", a, b); } mock(localPath) { this.url = localPath; this._isMock = true; return this; } setResponseType(type, opts = {}) { if (this.hasExecuted) { throw "Scenario was already executed. Can not change type."; } this._responseType = type; if (["browser", "extjs"].includes(type)) { const overrides = {}; if (flagpoleexecution_1.FlagpoleExecution.global.headless !== undefined) { overrides.headless = flagpoleexecution_1.FlagpoleExecution.global.headless; } this._request.setOptions({ browserOptions: Object.assign(Object.assign(Object.assign({}, this._defaultBrowserOptions), opts), overrides), }); } else { this._request .setOptions(Object.assign(Object.assign({}, this._defaultRequestOptions), opts)) .setOptions({ type: this._responseType === "json" ? "json" : this._responseType === "image" ? "image" : "generic", }); } this._response = responsefactory_1.createResponse(this); return this; } waitForFinished() { return this._finishedPromise; } waitForResponse() { return this._requestPromise; } promise() { return new Promise((resolve, reject) => { this.success(resolve); this.failure(reject); }); } buildUrl() { const path = this.url || "/"; if (this.suite.baseUrl === null) { return new URL(path); } else if (/^https?:\/\//.test(path) || /^data:/.test(path)) { return new URL(path); } else if (/^\/\//.test(path)) { return new URL(`${this.suite.baseUrl.protocol}//${path}`); } else if (/^\//.test(path)) { return new URL(`${this.suite.baseUrl.protocol}//${this.suite.baseUrl.host}${path}`); } return new URL(path, this.suite.baseUrl.href); } _pushCallbacks(name, callbacksName, a, b) { if (this.hasFinished) { throw `Can not add ${name} callbacks after execution has finished.`; } if (Array.isArray(a)) { a.forEach((callback) => { this[callbacksName].push({ message: "", callback: callback, }); }); } else { const { message, callback } = this._getOverloads(a, b); this[callbacksName].push({ callback: callback, message: message, }); } return this; } _reset() { this._flipAssertion = false; return this; } _pipeResponses(httpResponse) { return __awaiter(this, void 0, void 0, function* () { yield bluebird.mapSeries(this._pipeCallbacks, (cb) => __awaiter(this, void 0, void 0, function* () { cb.message && this.comment(cb.message); const result = yield cb.callback(httpResponse); if (result) { httpResponse = result; } })); return httpResponse; }); } _processResponse(httpResponse) { return __awaiter(this, void 0, void 0, function* () { httpResponse = yield this._pipeResponses(httpResponse); this._response.init(httpResponse); this._timeRequestLoaded = Date.now(); this._requestResolve(); this.result(new assertionresult_1.AssertionPass("Loaded " + this._response.responseTypeName + " " + this.url)); let lastReturnValue = null; this._publish(enums_1.ScenarioStatusEvent.executionProgress); Promise.mapSeries(this._nextCallbacks, (_then, index) => { const context = new assertioncontext_1.AssertionContext(this, this._response); const comment = this._nextMessages[index]; if (comment !== null) { this._pushToLog(new heading_1.LogScenarioSubHeading(comment)); } context.result = lastReturnValue; lastReturnValue = _then.apply(context, [context]); context.incompleteAssertions.forEach((assertion) => { this.result(new assertionresult_1.AssertionFailWarning(`Incomplete assertion: ${assertion.name}`, assertion)); }); return Promise.all([ lastReturnValue, context.assertionsResolved, context.subScenariosResolved, ]).timeout(30000); }) .then(() => { this._markScenarioCompleted(); }) .catch((err) => { this._markScenarioCompleted(err, null, "aborted"); }); this._publish(enums_1.ScenarioStatusEvent.executionProgress); }); } _executeBrowserRequest() { const browserControl = this.getBrowserControl(); browserControl .open(this._request) .then((next) => { const puppeteerResponse = next.response; if (puppeteerResponse !== null) { this._finalUrl = puppeteerResponse.url(); puppeteerResponse .request() .redirectChain() .forEach((req) => { this._redirectChain.push(req.url()); }); this._processResponse(httpresponse_1.HttpResponse.fromPuppeteer(puppeteerResponse, next.body, next.cookies)); } else { this._markScenarioCompleted(`Failed to load ${this._request.uri}`, null, "aborted"); } return; }) .catch((err) => this._markScenarioCompleted(`Failed to load ${this._request.uri}`, err, "aborted")); } _executeDefaultRequest() { this._request .fetch({ redirect: (url) => { this._finalUrl = url; this._redirectChain.push(url); }, }) .then((response) => { this._processResponse(response); }) .catch((err) => { this._markScenarioCompleted(`Failed to load ${this._request.uri}`, err, "aborted"); }); } _markRequestAsStarted() { this._timeRequestStarted = Date.now(); } _executeRequest() { if (this.url === null) { throw "Can not execute request with null URL."; } if (this.hasRequestStarted) { throw "Request has already started."; } this.url = this.buildUrl().href; this._markRequestAsStarted(); this._finalUrl = this._request.uri; if (["extjs", "browser"].includes(this._responseType)) { this._executeBrowserRequest(); } else { this._executeDefaultRequest(); } } _executeMock() { if (this.url === null) { throw "Can not execute request with null URL."; } if (this.hasRequestStarted) { throw "Request has already started."; } this._markRequestAsStarted(); const scenario = this; httpresponse_1.HttpResponse.fromLocalFile(this.url) .then((mock) => { scenario._processResponse(mock); }) .catch((err) => { scenario._markScenarioCompleted(`Failed to load page ${scenario.url}`, err, "aborted"); }); } _markScenarioCompleted(message = null, details = null, disposition = "completed") { return __awaiter(this, void 0, void 0, function* () { if (!this.hasFinished) { this._disposition = disposition; if (disposition == "cancelled") { this._publish(enums_1.ScenarioStatusEvent.executionCancelled); } else if (disposition == "skipped") { this._publish(enums_1.ScenarioStatusEvent.executionSkipped); this.comment(message); } else if (disposition == "aborted") { this._publish(enums_1.ScenarioStatusEvent.executionAborted); } this._timeScenarioFinished = Date.now(); if (disposition == "completed" || disposition == "aborted") { this.comment(`Took ${this.executionDuration}ms`); } if (disposition !== "completed" && disposition !== "skipped") { this.result(new assertionresult_1.AssertionFail(message || disposition, details)); } yield this._fireAfter(); this.hasPassed ? yield this._fireSuccess() : yield this._fireFailure(details || message || disposition); yield this._fireFinally(); if (this._browserControl !== null) { yield this._browserControl.close(); } } return this; }); } _fireCallbacks(callbacks) { return __awaiter(this, void 0, void 0, function* () { yield util_1.asyncForEach(callbacks, (cb) => __awaiter(this, void 0, void 0, function* () { cb.message && this._pushToLog(new comment_1.LogComment(cb.message)); return yield cb.callback(this, this.suite); })); }); } _logScenarioHeading() { this._pushToLog(new heading_1.LogScenarioHeading(this.title)); if (this._waitTime > 0) { this.comment(`Waited ${this._waitTime}ms`); } } _markExecutionAsStarted() { this._timeScenarioExecuted = Date.now(); } _fireBefore() { return __awaiter(this, void 0, void 0, function* () { this._markExecutionAsStarted(); yield this._fireCallbacks(this._beforeCallbacks); this._publish(enums_1.ScenarioStatusEvent.beforeExecute); this._logScenarioHeading(); this._publish(enums_1.ScenarioStatusEvent.executionStart); }); } _fireAfter() { return __awaiter(this, void 0, void 0, function* () { yield this._fireCallbacks(this._afterCallbacks); this._publish(enums_1.ScenarioStatusEvent.afterExecute); }); } _fireSuccess() { return __awaiter(this, void 0, void 0, function* () { yield this._fireCallbacks(this._successCallbacks); this._publish(enums_1.ScenarioStatusEvent.finished); }); } _fireFailure(errorMessage) { return __awaiter(this, void 0, void 0, function* () { yield this._fireCallbacks(this._failureCallbacks); errorMessage && this._pushToLog(new comment_1.LogComment(errorMessage)); this._publish(enums_1.ScenarioStatusEvent.finished); }); } _fireFinally() { return __awaiter(this, void 0, void 0, function* () { yield this._fireCallbacks(this._finallyCallbacks); this._publish(enums_1.ScenarioStatusEvent.finished); this._finishedResolve(); }); } _getOverloads(a, b) { return { message: this._getMessageOverload(a), callback: this._getCallbackOverload(a, b), }; } _getCallbackOverload(a, b) { return (() => { if (typeof b == "function") { return b; } else if (typeof a == "function") { return a; } else { throw new Error("No callback provided."); } })(); } _getMessageOverload(a) { return (() => { if (typeof a == "string" && a.trim().length > 0) { return a; } return null; })(); } _next(a, b, append = true) { const callback = (this._getCallbackOverload(a, b)); const message = this._getMessageOverload(a); if (!this.hasFinished) { if (append) { this._nextCallbacks.push(callback); this._nextMessages.push(message); } else { this._nextCallbacks.unshift(callback); this._nextMessages.unshift(message); } } else { throw "Scenario already finished."; } return this; } _publish(statusEvent) { return __awaiter(this, void 0, void 0, function* () { const scenario = this; this._subscribers.forEach((callback) => __awaiter(this, void 0, void 0, function* () { callback(scenario, statusEvent); })); }); } _pushToLog(logItem) { this._log.add(logItem); return this; } } exports.Scenario = Scenario; //# sourceMappingURL=scenario.js.map