UNPKG

flagpole

Version:

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

901 lines 32.5 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; 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 }); exports.ProtoScenario = void 0; const enums_1 = require("./interfaces/enums"); const assertion_result_1 = require("./logging/assertion-result"); const http_response_1 = require("./http/http-response"); const heading_1 = require("./logging/heading"); const comment_1 = require("./logging/comment"); const log_collection_1 = require("./logging/log-collection"); const http_request_1 = require("./http/http-request"); const util_1 = require("./util"); const bluebird = require("bluebird"); const minikin_1 = require("minikin"); const helpers_1 = require("./helpers"); const internal_1 = require("./decorators/internal"); const jpath_1 = require("./json/jpath"); const http_1 = require("./interfaces/http"); const _1 = require("."); var ScenarioRequestType; (function (ScenarioRequestType) { ScenarioRequestType["httpRequest"] = "httpRequest"; ScenarioRequestType["localFile"] = "localFile"; ScenarioRequestType["manual"] = "manual"; ScenarioRequestType["webhook"] = "webhook"; })(ScenarioRequestType || (ScenarioRequestType = {})); class ProtoScenario { constructor(suite, title, type, opts) { this.suite = suite; this.title = title; this.type = type; this.defaultRequestOptions = { method: "get", }; this._log = new log_collection_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._requestType = ScenarioRequestType.httpRequest; this._redirectChain = []; this._finalUrl = null; this._waitToExecute = false; this._waitTime = 0; this._flipAssertion = false; this._ignoreAssertion = false; this._mockResponseOptions = null; this._aliasedData = {}; this._requestResolve = () => { }; this._finishedResolve = () => { }; this._disposition = enums_1.ScenarioDisposition.pending; this._webhookResolver = () => { }; this._requestPromise = new Promise((resolve) => { this._requestResolve = resolve; }); this._finishedPromise = new Promise((resolve) => { this._finishedResolve = resolve; }); this._webhookPromise = new Promise((resolve) => { this._webhookResolver = resolve; }); this.request = new http_request_1.HttpRequest(Object.assign(Object.assign({}, this.defaultRequestOptions), opts)); } get context() { return new _1.AssertionContext(this, this.response); } 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 hasAborted() { return this._disposition == enums_1.ScenarioDisposition.aborted; } get hasBeenCancelled() { return this._disposition == enums_1.ScenarioDisposition.cancelled; } get hasBeenSkipped() { return this._disposition == enums_1.ScenarioDisposition.skipped; } get isPending() { return this._disposition == enums_1.ScenarioDisposition.pending; } get isExecuting() { return this._disposition == enums_1.ScenarioDisposition.excuting; } get isCompleted() { return this._disposition == enums_1.ScenarioDisposition.completed; } get disposition() { return this._disposition; } get opts() { return this.request.options; } 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() { if (this._requestType == ScenarioRequestType.httpRequest) { return this.url === null || /{[A-Za-z0-9_ -]+}/.test(this.url); } else if (this._requestType == ScenarioRequestType.manual || this._requestType == ScenarioRequestType.webhook) { return !this._mockResponseOptions; } return false; } 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 (http_1.HttpMethodVerbArray.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 nextCallbacks() { return this._nextCallbacks.map((callback, i) => { return { message: this._nextMessages[i] || "", callback: callback, }; }); } _getArray(key) { const type = util_1.toType(this._aliasedData[key]); if (type == "undefined") { this._aliasedData[key] = []; } else if (type !== "array") { throw Error(`${key} was of type ${type} and not an array. Can only push into an array.`); } return this._aliasedData[key]; } push(key, value) { this._getArray(key).push(value); return this; } 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; } setCookies(cookies) { Object.keys((key) => { this.setCookie(key, cookies[key]); }); 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); })); thatScenario.failure(() => __awaiter(this, void 0, void 0, function* () { this.cancel(`This scenario was pending completion of "${thatScenario.title}", but it failed.`); })); return this; } comment(input) { const type = util_1.toType(input); const message = type === "string" ? input : (input === null || input === void 0 ? void 0 : 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(a, opts) { if (this.hasExecuted) { throw `Can call open after scenario has executed`; } if (opts) { this.request.setOptions(opts); } this._requestType = ScenarioRequestType.httpRequest; if (typeof a == "string") { this.url = String(a); } else { util_1.runAsync(() => __awaiter(this, void 0, void 0, function* () { this.url = (yield a.getUrl()).toString(); })); } 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* () { yield this._fireBefore(); this._publish(enums_1.ScenarioStatusEvent.executionProgress); this.comment(`Skipped ${message ? ": " + message : ""}`); yield this._markScenarioCompleted(null, null, enums_1.ScenarioDisposition.skipped); return this; }); } cancelOrAbort(message) { return __awaiter(this, void 0, void 0, function* () { return this.hasExecuted ? this.abort(message) : this.cancel(message); }); } abort(message) { return __awaiter(this, void 0, void 0, function* () { this._markScenarioCompleted(`Aborted ${message ? ": " + message : ""}`, null, enums_1.ScenarioDisposition.aborted); return this; }); } cancel(message) { return __awaiter(this, void 0, void 0, function* () { yield this._fireBefore(); this._publish(enums_1.ScenarioStatusEvent.executionProgress); this._markScenarioCompleted(`Cancelled ${message ? ": " + message : ""}`, null, enums_1.ScenarioDisposition.cancelled); return this; }); } execute(pathParams) { return __awaiter(this, void 0, void 0, function* () { 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* () { yield this._fireBefore(); this._requestType == ScenarioRequestType.httpRequest ? this._executeHttpRequest() : this._requestType == ScenarioRequestType.localFile ? this._executeLocalRequest() : this._executeMock(); 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(opts = "") { this._requestType = ScenarioRequestType.manual; this._mockResponseOptions = typeof opts == "string" ? { body: opts } : opts || {}; return this; } local(localPath) { this._requestType = ScenarioRequestType.localFile; this.url = localPath; return this; } webhook(a, b, c) { const route = typeof a == "string" ? a : "*"; const port = typeof a == "number" ? a : typeof b == "number" ? b : undefined; const opts = (() => { if (c) { return c; } if (typeof b !== "number") { return b; } if (typeof a !== "number" && typeof a !== "string") { return a; } return undefined; })(); this._requestType = ScenarioRequestType.webhook; util_1.runAsync(() => __awaiter(this, void 0, void 0, function* () { const server = yield minikin_1.default.server(port, opts); this._webhookResolver({ port: server.port, opts: opts, server: server, }); server.route(route, (req) => { this.url = req.url; this._mockResponseOptions = { body: req.body, headers: req.headers, cookies: req.cookies, trailers: req.trailers, url: req.url, method: req.method, }; util_1.runAsync(() => { server.close(); }, 100); return minikin_1.Response.fromString("OK"); }); })); return this; } server() { return this._webhookPromise; } waitForFinished() { return this._finishedPromise; } waitForResponse() { return this._requestPromise; } promise() { return new Promise((resolve, reject) => { this.success(resolve); this.failure(reject); }); } repeat(count) { if (typeof count == "number") { if (count < 1) { throw "Count must be an integer greater than or equal to one."; } const scenarios = []; for (let i = 1; i <= count; i++) { scenarios.push(this.suite.import(this)); } return scenarios; } return this.suite.import(this); } 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); this.result(new assertion_result_1.AssertionPass(`Loaded ${this.typeName} ${this.url ? this.url : "[manual input]"}`)); let lastReturnValue = null; this._publish(enums_1.ScenarioStatusEvent.executionProgress); bluebird .mapSeries(this._nextCallbacks, (_then, index) => { const context = this.response.context; const comment = this._nextMessages[index]; if (comment !== null) { this._pushToLog(new heading_1.LogScenarioSubHeading(comment)); } context.result = lastReturnValue; const callbackArgNames = util_1.getFunctionArgs(_then); const args = callbackArgNames.slice(1).map((name) => { if (context[name]) return context[name]; else if (context.response[name]) return context.response[name]; return context; }); lastReturnValue = _then.apply(context, [context, ...args]); context.incompleteAssertions.forEach((assertion) => { this.result(new assertion_result_1.AssertionFailWarning(`Incomplete assertion: ${assertion.name}`, assertion)); }); return bluebird .all([ lastReturnValue, context.assertionsResolved, context.subScenariosResolved, ]) .timeout(this.suite.maxScenarioDuration); }) .then(() => { this._markScenarioCompleted(); }) .catch(bluebird.TimeoutError, (e) => { this._markScenarioCompleted("Timed out.", e.message, enums_1.ScenarioDisposition.aborted); }) .catch((err) => { this._markScenarioCompleted(err, null, enums_1.ScenarioDisposition.aborted); }); this._publish(enums_1.ScenarioStatusEvent.executionProgress); }); } _executeDefaultRequest() { this.request .fetch({ redirect: (url) => { this._finalUrl = url; this._redirectChain.push(url); }, }, this.adapter) .then((response) => { this._processResponse(response); }) .catch((err) => { this._markScenarioCompleted(`Failed to load ${this.request.uri}`, err, enums_1.ScenarioDisposition.aborted); }); } _markRequestAsStarted() { this._timeRequestStarted = Date.now(); } _executeHttpRequest() { if (this.url === null) { throw "Can not execute request with null URL."; } this.url = this.buildUrl().href; this._markRequestAsStarted(); this._finalUrl = this.request.uri; this._executeDefaultRequest(); } _executeLocalRequest() { if (this.url === null) { throw "Can not execute request with null URL."; } this._markRequestAsStarted(); http_response_1.parseResponseFromLocalFile(this.url) .then((res) => { this._processResponse(res); }) .catch((err) => { this._markScenarioCompleted(`Failed to load local file ${this.url}`, err, enums_1.ScenarioDisposition.aborted); }); } _executeMock() { if (this._mockResponseOptions === null) { throw "Can not execute a mock request with no mocked response."; } this._markRequestAsStarted(); try { const response = new http_response_1.HttpResponse(this._mockResponseOptions); this._processResponse(response); } catch (err) { this._markScenarioCompleted(`Failed to load page ${this.url}`, String(err), enums_1.ScenarioDisposition.aborted); } } _markScenarioCompleted(message = null, details = null, disposition = enums_1.ScenarioDisposition.completed) { return __awaiter(this, void 0, void 0, function* () { if (!this.hasFinished) { this._disposition = disposition; if (disposition == enums_1.ScenarioDisposition.cancelled) { this._publish(enums_1.ScenarioStatusEvent.executionCancelled); } else if (disposition == enums_1.ScenarioDisposition.skipped) { this._publish(enums_1.ScenarioStatusEvent.executionSkipped); this.comment(message); } else if (disposition == enums_1.ScenarioDisposition.aborted) { this._requestResolve(this); this._publish(enums_1.ScenarioStatusEvent.executionAborted); } this._timeScenarioFinished = Date.now(); if (disposition == enums_1.ScenarioDisposition.completed || disposition == enums_1.ScenarioDisposition.aborted) { this.comment(`Took ${this.executionDuration}ms`); } if (disposition !== enums_1.ScenarioDisposition.completed && disposition !== enums_1.ScenarioDisposition.skipped) { this.result(new assertion_result_1.AssertionFail(message || disposition, details)); } yield this._fireAfter(); this.hasPassed ? yield this._fireSuccess() : yield this._fireFailure(details || message || disposition); yield this._fireFinally(); } 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(this); }); } _getOverloads(a, b) { return { message: this._getMessageOverload(a), callback: this._getCallbackOverload(a, b), }; } _expect(responseValues) { return (context) => __awaiter(this, void 0, void 0, function* () { const json = new jpath_1.JsonDoc(context.response.serialize()); const paths = Object.keys(responseValues); yield context.each(paths, (path) => __awaiter(this, void 0, void 0, function* () { const data = yield json.search(path); const thisValue = helpers_1.wrapAsValue(context, data, path, data); const thatValue = responseValues[path]; const type = util_1.toType(thatValue); if (type === "function") { const result = thatValue(thisValue.$); context.assert(thisValue.name, result).equals(true); } else if (type === "array") { context.assert(thisValue).in(thatValue); } else if (type === "regexp") { context.assert(thisValue).matches(thatValue); } else { context.assert(thisValue).equals(thatValue); } })); }); } _getCallbackOverload(a, b) { const aType = util_1.toType(a); const bType = util_1.toType(b); if (bType == "function") { return b; } else if (aType == "function") { return a; } else if (bType == "asyncfunction") { return b; } else if (aType == "asyncfunction") { return a; } else if (bType == "object") { return this._expect(b); } else if (aType == "object") { return this._expect(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 (append) { this._nextCallbacks.push(callback); this._nextMessages.push(message); } else { this._nextCallbacks.unshift(callback); this._nextMessages.unshift(message); } 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; } } __decorate([ internal_1.beforeScenarioExecuted ], ProtoScenario.prototype, "skip", null); __decorate([ internal_1.afterScenarioExecuted, internal_1.beforeScenarioFinished ], ProtoScenario.prototype, "abort", null); __decorate([ internal_1.beforeScenarioExecuted ], ProtoScenario.prototype, "cancel", null); __decorate([ internal_1.beforeScenarioExecuted ], ProtoScenario.prototype, "execute", null); __decorate([ internal_1.beforeScenarioExecuted, internal_1.afterScenarioReady ], ProtoScenario.prototype, "go", null); __decorate([ internal_1.beforeScenarioRequestStarted ], ProtoScenario.prototype, "_executeHttpRequest", null); __decorate([ internal_1.beforeScenarioRequestStarted ], ProtoScenario.prototype, "_executeLocalRequest", null); __decorate([ internal_1.beforeScenarioRequestStarted ], ProtoScenario.prototype, "_executeMock", null); __decorate([ internal_1.beforeScenarioFinished ], ProtoScenario.prototype, "_next", null); exports.ProtoScenario = ProtoScenario; //# sourceMappingURL=scenario.js.map