flagpole
Version:
Simple and fast DOM integration and REST API testing framework.
643 lines (581 loc) • 17.3 kB
text/typescript
import { Flagpole } from "./index";
import { Suite } from "./suite";
import { iLogLine, SubheadingLine, CommentLine, PassLine, FailLine, ConsoleColor } from "./consoleline";
import { JsonResponse } from "./jsonresponse";
import { HtmlResponse } from "./htmlresponse";
import { ResponseType, SimplifiedResponse } from "./response";
import { ImageResponse } from "./imageresponse";
import { ResourceResponse } from "./resourceresponse";
import { ScriptResponse } from "./scriptresponse";
import { CssResponse } from "./cssresponse";
import { Mock } from "./mock";
let request = require('request');
/**
* A scenario contains tests that run against one request
*/
export class Scenario {
public readonly suite: Suite;
protected title: string;
protected log: Array<iLogLine> = [];
protected failures: Array<string> = [];
protected passes: Array<string> = [];
protected onDone: Function;
protected initialized: number | null = null;
protected start: number | null = null;
protected end: number | null = null;
protected requestStart: number | null = null;
protected requestLoaded: number | null = null;
protected responseType: ResponseType = ResponseType.html;
protected url: string | null = null;
protected waitToExecute: boolean = false;
protected nextLabel: string | null = null;
protected flipAssertion: boolean = false;
protected optionalAssertion: boolean = false;
protected ignoreAssertion: boolean = false;
protected _then: Function | null = null;
protected _isMock: boolean = false;
protected options: any = {
method: 'GET',
headers: {}
};
constructor(suite: Suite, title: string, onDone: Function) {
this.initialized = Date.now();
this.suite = suite;
this.title = title;
this.onDone = onDone;
this.subheading(title);
}
/**
* Did any assertions in this scenario fail?
*
* @returns {boolean}
*/
public failed(): boolean {
return (this.failures.length > 0);
}
/**
* Did all assertions in this scenario pass? This also requires that the scenario has completed
*
* @returns {boolean}
*/
public passed(): boolean {
return !!(this.end && this.failures.length == 0);
}
/**
* Set body to submit
*
* @param jsonObject
* @returns {Scenario}
*/
public jsonBody(jsonObject: any): Scenario {
this.header('Content-Type', 'application/json');
return this.body(JSON.stringify(jsonObject));
}
/**
*
* @param str
*/
public body(str: string): Scenario {
this.options.body = str;
return this;
}
/**
* Make sure the web page has valid SSL certificate
*
* @param verify
*/
public verifySslCert(verify: boolean): Scenario {
this.options.strictSSL = verify;
this.options.rejectUnauthorized = verify;
return this;
}
/**
*
* @param proxyUri
*/
public proxy(proxyUri: string): Scenario {
this.options.proxy = proxyUri;
return this;
}
/**
* Set the timeout for how long the request should wait for a response
*
* @param {number} timeout
* @returns {Scenario}
*/
public timeout(timeout: number): Scenario {
this.options.timeout = timeout;
return this;
}
/**
* Do not run this scenario until execute() is called
*
* @param {boolean} bool
* @returns {Scenario}
*/
public wait(bool: boolean = true): Scenario {
this.waitToExecute = bool;
return this;
}
/**
* Set the form options that will be submitted with the request
*
* @param form
* @returns {Scenario}
*/
public form(form: {}): Scenario {
this.options.form = form;
return this;
}
/**
*
* @param n
*/
public maxRedirects(n: number): Scenario {
this.options.maxRedirects = n;
return this;
}
/**
*
* @param onRedirect
*/
public followRedirect(onRedirect: boolean | Function): Scenario {
this.options.followRedirect = onRedirect;
return this;
}
/**
* Set the basic authentication headers to be sent with this request
*
* @param authorization
* @returns {Scenario}
*/
public auth(authorization: { username: string, password: string }): Scenario {
this.options.auth = authorization;
return this;
}
/**
* Set the full list of headers to submit with this request
*
* @param headers
* @returns {Scenario}
*/
public headers(headers: {}): Scenario {
this.options.headers = Object.assign(this.options.headers, headers);
return this;
}
/**
* Set a single header key-value without overriding others
*
* @param {string} key
* @param value
* @returns {Scenario}
*/
public header(key: string, value: any): Scenario {
this.options.headers = this.options.headers || {};
this.options.headers[key] = value;
return this;
}
/**
* Set the HTTP method of this request
*
* @param {string} method
* @returns {Scenario}
*/
public method(method: string): Scenario {
this.options.method = method.toUpperCase();
return this;
}
/**
* Has this scenario completed?
*
* @returns {boolean}
*/
public isDone(): boolean {
return (this.end !== null);
}
/**
* Add a subheading log message to buffer
*/
public subheading(message: string): Scenario {
this.log.push(new SubheadingLine(message));
return this;
}
/**
* Add a neutral line to the output
*/
public comment(message: string): Scenario {
this.log.push(
new CommentLine(message)
);
//this.passes.push(message);
return this;
}
/**
* Assert something is true, with respect to the flipped not()
* Also respect ignore assertions flag
*/
public assert(statement: boolean, message: string, actualValue?: string): Scenario {
if (!this.ignoreAssertion) {
let passed: boolean = this.flipAssertion ? !statement : !!statement;
if (this.flipAssertion) {
message = 'NOT: ' + message;
}
if (this.optionalAssertion) {
message += ' - Optional';
}
if (passed) {
this.pass(message);
}
else {
if (actualValue) {
message += ' (' + actualValue + ')';
}
this.fail(message, this.optionalAssertion);
}
return this.reset();
}
return this;
}
/**
* Push in a new passing assertion
*/
public pass(message: string): Scenario {
if (this.nextLabel) {
message = this.nextLabel;
this.nextLabel = null;
}
this.log.push(new PassLine(message));
this.passes.push(message);
return this;
}
/**
* Push in a new failing assertion
*/
public fail(message: string, isOptional: boolean = false): Scenario {
if (this.nextLabel) {
message = this.nextLabel;
this.nextLabel = null;
}
let line: FailLine = new FailLine(message);
if (isOptional) {
line.color = ConsoleColor.FgMagenta;
line.textSuffix = '(Optional)';
}
this.log.push(line);
if (!isOptional) {
this.failures.push(message);
}
return this;
}
/**
* Clear out any previous settings
*/
protected reset(): Scenario {
this.flipAssertion = false;
this.optionalAssertion = false;
return this;
}
/**
* Flip the next assertion
*/
public not(): Scenario {
this.flipAssertion = true;
return this;
}
/**
* Consider the next set of tests optional, until the next selector
*/
public optional(): Scenario {
this.optionalAssertion = true;
return this;
}
/**
* Ignore assertions until further notice
*/
public ignore(assertions: boolean | Function = true): Scenario {
if (typeof assertions == 'boolean') {
this.ignoreAssertion = assertions;
}
else if (typeof assertions == 'function') {
this.ignore(true);
assertions();
this.ignore(false);
}
return this;
}
protected executeWhenReady() {
if (!this.waitToExecute && this.canExecute()) {
this.execute();
}
}
/**
* Set the URL that this scenario will hit
*
* @param {string} url
* @returns {Scenario}
*/
public open(url: string): Scenario {
// You can only load the url once per scenario
if (!this.hasExecuted()) {
this.url = url;
this._isMock = false;
this.executeWhenReady();
}
return this;
}
/**
* Set the callback for the assertions to run after the request has a response
*/
public then(callback: Function): Scenario {
// If it hasn't already been executed
if (!this.hasExecuted()) {
this._then = callback;
this.executeWhenReady();
}
return this;
}
/**
* Alias for then()
*/
public assertions(callback: Function): Scenario {
return this.then(callback);
}
/**
* Skip this scenario completely and mark it done
*
* @returns {Scenario}
*/
public skip(message?: string): Scenario {
if (!this.hasExecuted()) {
message = "Skipped" + (message ? ': ' + message : '');
this.start = Date.now();
this.log.push(new CommentLine(message));
this.end = Date.now();
this.onDone(this);
}
return this;
}
protected getScenarioType(): { name: string, responseObject } {
if (this.responseType == ResponseType.json) {
return {
name: 'REST End Point',
responseObject: JsonResponse
}
}
else if (this.responseType == ResponseType.image) {
return {
name: 'Image',
responseObject: ImageResponse
}
}
else if (this.responseType == ResponseType.script) {
return {
name: 'Script',
responseObject: ScriptResponse
}
}
else if (this.responseType == ResponseType.stylesheet) {
return {
name: 'Stylesheet',
responseObject: CssResponse
}
}
else if (this.responseType == ResponseType.resource) {
return {
name: 'Resource',
responseObject: ResourceResponse
}
}
else {
return {
name: 'HTML Page',
responseObject: HtmlResponse
}
}
}
protected processResponse(simplifiedResponse: SimplifiedResponse) {
let scenarioType: { name: string, responseObject } = this.getScenarioType();
this.requestLoaded = Date.now();
this.pass('Loaded ' + scenarioType.name + ' ' + this.url);
if (this._then !== null && this.url !== null) {
this._then(
new scenarioType.responseObject(this, this.url, simplifiedResponse)
);
}
this.done();
}
protected executeRequest() {
if (!this.requestStart && this.url !== null) {
let scenario: Scenario = this;
this.requestStart = Date.now();
this.options.uri = this.suite.buildUrl(this.url);
if (this.responseType == ResponseType.image) {
require('probe-image-size')(this.options.uri, this.options, function(error, result) {
if (!error) {
scenario.processResponse({
statusCode: 200,
body: JSON.stringify(result),
headers: {
'content-type': result.mime
}
});
}
else {
scenario.fail('Failed to load image ' + scenario.url);
scenario.done();
}
});
}
else {
request(this.options, function (error, response, body) {
if (!error) {
scenario.processResponse(Flagpole.toSimplifiedResponse(response, body));
}
else {
scenario.fail('Failed to load ' + scenario.url);
scenario.comment(error);
scenario.done();
}
});
}
}
}
protected executeMock() {
if (!this.requestStart && this.url !== null) {
let scenario: Scenario = this;
this.requestStart = Date.now();
Mock.loadLocalFile(this.url).then(function (mock: Mock) {
scenario.processResponse(mock);
}).catch(function () {
scenario.fail('Failed to load page ' + scenario.url);
scenario.done();
});
}
}
/**
* Execute this scenario
*/
public execute(): Scenario {
if (!this.hasExecuted() && this.url !== null) {
this.start = Date.now();
// If we waited first
if (this.waitToExecute && this.initialized !== null) {
this.log.push(new CommentLine('Waited ' + (this.start - this.initialized) + 'ms'));
}
// Execute it
this._isMock ?
this.executeMock() :
this.executeRequest();
}
return this;
}
/**
* Fake response from local file for testing
*/
public mock(localPath: string): Scenario {
this.url = localPath;
this._isMock = true;
this.executeWhenReady();
return this;
}
/**
* Override the next test's default pass/fail message with something custom and more human readable
*
* @param {string} message
* @returns {Scenario}
*/
public label(message: string): Scenario {
this.nextLabel = message;
return this;
}
public getTitle(): string {
return this.title;
}
/**
* Get the log buffer
*
* @returns {Array<ConsoleLine>}
*/
public getLog(): Array<iLogLine> {
return this.log;
}
/**
* Find the total execution time of this scenario
*
* @returns {number}
*/
protected getExecutionTime(): number {
return (this.end !== null && this.start !== null) ?
(this.end - this.start) : 0;
}
/**
* Mark this scenario as completed
*
* @returns {Scenario}
*/
public done(): Scenario {
this.end = Date.now();
this.log.push(new CommentLine("Took " + this.getExecutionTime() + 'ms'));
this.onDone(this);
return this;
}
/**
* Get the url
*/
public getUrl(): string | null {
return this.url;
}
/**
*
*/
public getRequestLoadTime(): number | null {
return (this.requestLoaded && this.requestStart) ?
(this.requestLoaded - this.requestStart): null;
}
/**
* We ready to pull the trigger on this one?
*/
public canExecute(): boolean {
return (!this.hasExecuted() && this.url !== null && this._then !== null);
}
/**
* Has this scenario already been executed?
*/
public hasExecuted(): boolean {
return this.start !== null;
}
/**
* Did this scenario finish executing?
*/
public hasFinished(): boolean {
return this.hasExecuted() && this.end !== null;
}
/**
* SET RESPONSE TYPE
*/
protected setResponseType(type: ResponseType): Scenario {
if (this.hasExecuted()) {
throw new Error('Scenario was already executed. Can not change type.');
}
this.responseType = type;
return this;
}
public image(): Scenario {
return this.setResponseType(ResponseType.image);
}
public html(): Scenario {
return this.setResponseType(ResponseType.html);
}
public json(): Scenario {
return this.setResponseType(ResponseType.json);
}
public script(): Scenario {
return this.setResponseType(ResponseType.script);
}
public stylesheet(): Scenario {
return this.setResponseType(ResponseType.stylesheet);
}
public resource(): Scenario {
return this.setResponseType(ResponseType.resource);
}
}