flagpole
Version:
Simple and fast DOM integration, headless or headful browser, and REST API testing framework.
675 lines • 26.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const webserver_1 = require("../webserver");
const cli_1 = require("./cli");
const config_1 = require("./config");
const url_1 = require("url");
const flagpoleexecutionoptions_1 = require("../flagpoleexecutionoptions");
const suiteexecution_1 = require("./suiteexecution");
const flagpoleexecutionoptions_2 = require("../flagpoleexecutionoptions");
const open = require("open");
const qs = require("querystring");
const possibleEnvironments = [
"dev",
"stag",
"prod",
"qa",
"rc",
"preprod",
"alpha",
"beta"
];
const routes = {
"GET /api/suites": (url, response) => {
sendJson(response, { suites: cli_1.Cli.config.suites });
},
"GET /import/suite": (url, response) => {
sendGetImport(response);
},
"POST /import/suite": (url, response, postData) => {
const name = postData.name;
if (name) {
cli_1.Cli.config.addSuite({
name: name
});
cli_1.Cli.config.save();
return sendOutput(response, `<p>Imported new suite ${name} from file ${name}.js</p><p><a href="/">Back</a>`);
}
fileNotFound(response);
},
"GET /init": (url, response) => {
sendGetInit(response);
},
"POST /init": (url, response, postData) => {
initProject(response, postData);
},
"GET /add/scenario": (url, response) => {
const suiteName = url.searchParams.get("suite");
const suite = getSuite(response, suiteName);
if (suite) {
return sendGetAddScenario(response, suite);
}
fileNotFound(response);
},
"POST /add/scenario": (url, response, postData) => {
if (!postData.suite ||
!postData.description ||
!postData.path ||
!postData.type) {
return sendOutput(response, `<p>All fields are required.</p>`);
}
const suite = getSuite(response, postData.suite);
if (!suite) {
return fileNotFound(response);
}
cli_1.Cli.addScenario(suite, {
description: postData.description,
path: postData.path,
type: postData.type
})
.then(() => {
return sendOutput(response, `
<p>Added scenario to <em>${suite.name}</em></p>
<p><a href="/">Back</a></p>
`);
})
.catch(err => {
return sendOutput(response, `<p>${err}</p>`);
});
},
"GET /add/suite": (url, response) => {
sendGetAddSuite(response);
},
"POST /add/suite": (url, response, postData) => {
if (!postData.suiteName ||
!postData.suiteDescription ||
!postData.scenarioDescription ||
!postData.scenarioPath ||
!postData.scenarioType) {
return sendOutput(response, `<p>All fields are required. <a href="/add/suite">Try again</a></p>`);
}
let domains = {};
Object.keys(postData).forEach((key) => {
if (key.startsWith("baseDomain[") && key.endsWith("]")) {
let envName = key.split("[")[1].slice(0, -1);
domains[envName] = postData[key];
}
});
if (Object.keys(domains).length == 0) {
domains.dev = "http://localhost";
}
addSuite(response, {
name: postData.suiteName,
description: postData.suiteDescription
}, {
description: postData.scenarioDescription,
path: postData.scenarioPath,
type: postData.scenarioType
});
},
"GET /suite": (url, response) => {
const suite = getSuite(response, url.searchParams.get("name"));
if (suite) {
return sendGetEditSuite(response, suite);
}
fileNotFound(response);
},
"POST /suite": (url, response, postData) => {
const suite = getSuite(response, postData.name);
if (suite) {
cli_1.Cli.config.suites[suite.name].clearTags();
String(postData.tags)
.split(",")
.forEach(tag => {
cli_1.Cli.config.suites[suite.name].addTag(tag);
});
cli_1.Cli.config
.save()
.then(() => {
sendOutput(response, `<p>Saved changes to suite.</p><p><a href="/">Back</a>`);
})
.catch(err => {
sendOutput(response, `<p>Error adding tag. ${err}</p><p><a href="/suite?name=${suite.name}">Back</a>`);
});
}
},
"POST /rm": (url, response) => {
const env = url.searchParams.get("env");
const suite = url.searchParams.get("suite");
if (suite) {
return removeSuite(response, suite);
}
else if (env) {
return removeEnv(response, env);
}
fileNotFound(response);
},
"GET /add/env": (url, response) => {
sendGetAddEnv(response);
},
"POST /add/env": (url, response, postData) => {
const envName = postData.name;
const defaultDomain = postData.domain;
if (!envName || !defaultDomain) {
return sendOutput(response, `<p>Imported new suite ${name} from file ${name}.js</p><p><a href="/">Back</a>`);
}
if (envName) {
addEnv(response, new config_1.EnvConfig(cli_1.Cli.config, {
name: envName,
defaultDomain: defaultDomain
}));
}
},
"POST /run": (url, response, postData) => {
const suiteName = postData.suite;
const envName = postData.env;
if (suiteName && cli_1.Cli.config.suites[suiteName]) {
return runSuite(response, suiteName, envName || flagpoleexecutionoptions_2.FlagpoleExecution.opts.environment);
}
fileNotFound(response);
}
};
const getSuite = (response, suiteName) => {
if (!suiteName) {
return sendOutput(response, `<p>No suite name. <a href="/">Back</a></p>`);
}
const suite = cli_1.Cli.config.suites[suiteName];
if (!suite) {
return sendOutput(response, `<p>That suite does not exist. <a href="/">Back</a></p>`);
}
return suite;
};
const getTemplate = (httpResponse) => {
const response = webserver_1.WebResponse.createFromTemplate(httpResponse, `${__dirname}/report.html`);
response.replace("nav", '<a href="/">Project Home</a>');
return response;
};
const sendOutput = (response, output) => {
getTemplate(response).send({
output: output
});
};
const sendJson = (response, json) => {
webserver_1.WebResponse.createFromInput(response, JSON.stringify(json)).send();
};
const fileNotFound = (response) => {
sendOutput(response, "File not found.");
};
const sendGetInit = (response) => {
let output = `
<h2>Initialize Flagpole</h2>
<p>
Flagpole has not yet been set up in this project. Complete the form below to configure.
</p>
<form method="POST" action="/init" id="frm">
<div class="field">
<label for="name">Project Name</label>
<input type="text" name="projectName" id="name" value="${process
.cwd()
.split("/")
.pop()}">
</div>
<div class="field">
<label for="folder">Tests Folder</label>
<input type="text" name="testsPath" id="folder" value="tests">
</div>
<div class="field">
Environments (check all that you want to use)
</div>`;
possibleEnvironments.forEach((envName) => {
output += `
<div class="field">
<input type="checkbox" name="envName[${envName}]" id="env_${envName}" value="${envName}">
<label for="env_${envName}">${envName}</label>
<input type="text" name="envDomain[${envName}]" id="domain_${envName}" placeholder="https://www.flagpolejs.com">
</div>
`;
});
output += `
<div class="field button">
<button type="submit">Initialize Project</button>
</div>
</form>`;
sendOutput(response, output);
};
const sendGetAddEnv = (response) => {
let output = `
<h2>Add Environment</h2>
<form method="POST" action="/add/env" id="frm">
<div class="field">
<label for="name">Environment Name</label>
<input type="text" name="name" id="name" placeholder="dev">
</div>
<div class="field">
<label for="domain">Base Path</label>
<input type="url" name="domain" id="domain" placeholder="http://www.google.com/">
</div>
<div class="field button">
<button type="submit">Add Environment</button>
<button type="button" onclick="window.location.href='/'">Cancel</button>
</div>
</form>`;
sendOutput(response, output);
};
const sendGetEditSuite = (response, suite) => {
let output = `
<h2>Edit Suite</h2>
<form method="POST" action="/suite" id="frm">
<div class="field">
<label for="name">Suite Name</label>
<input type="text" name="name" id="name" value="${suite.name}" readonly>
</div>
<div class="field">
<label for="tags">Tags (comma-separated)</label>
<input type="text" name="tags" id="tags" placeholder="tag1, tag2" value="${suite.tags.join(", ")}">
</div>
<div class="field button">
<button type="submit">Save Changes</button>
<button type="button" onclick="window.location.href='/'">Cancel</button>
</div>
</form>`;
sendOutput(response, output);
};
const sendGetAddScenario = (response, suite) => {
let output = `
<h2>Add Scenario</h2>
<p>Appending a new scenario to suite ${suite.name}.</p>
<form method="POST" action="/add/scenario" id="frm">
<div class="field">
<label for="suite">Suite</label>
<input type="text" readonly name="suite" id="suite" value="${suite.name}">
</div>
<div class="field">
<label for="description">Description</label>
<input type="text" name="description" id="description" placeholder="Make sure homepage loads">
</div>
<div class="field">
<label for="path">Path</label>
<input type="text" name="path" id="path" value="/" placeholder="/">
</div>
<div class="field">
<label for="type">Type</label>
<select name="type" id="type">
<option value="html">HTML/DOM (Cheerio)</option>
<option value="browser">Browser (Puppeteer)</option>
<option value="json">JSON/REST API</option>
</select>
</div>
<div class="field button">
<button type="submit">Add Scenario</button>
<button type="button" onclick="window.location.href = '/'">Cancel</button>
</div>
</form>
`;
return sendOutput(response, output);
};
const sendGetAddSuite = (response) => {
let output = `
<h2>New Suite</h2>
<form method="POST" action="/add/suite" id="frm">
<div class="field">
<label for="suiteName">Suite Name</label>
<input type="text" name="suiteName" id="suiteName" placeholder="smoke">
</div>
<div class="field">
<label for="suiteDescription">Suite Description</label>
<input type="text" name="suiteDescription" id="suiteDescription" placeholder="Basic smoke test of the site">
</div>
<fieldset>
<legend>Base Domain</legend>`;
cli_1.Cli.config.getEnvironments().forEach((env) => {
output += `
<div class="field">
<label for="env_${env.name}">${env.name}</label>
<input type="text" name="baseDomain[${env.name}]" id="env_${env.name}" value="${env.defaultDomain}">
</div>`;
});
output += `</fieldset>
<fieldset>
<legend>First Scenario</legend>
<div class="field">
<label for="scenarioDescription">Title</label>
<input type="text" name="scenarioDescription" id="scenarioDescription" placeholder="Make sure homepage loads">
</div>
<div class="field">
<label for="scenarioPath">Path</label>
<input type="text" name="scenarioPath" id="scenarioPath" value="/" placeholder="/">
</div>
<div class="field">
<label for="scenarioType">Type</label>
<select name="scenarioType" id="scenarioType">
<option value="html">HTML/DOM (Cheerio)</option>
<option value="browser">Browser (Puppeteer)</option>
<option value="json">JSON/REST API</option>
</select>
</div>
</fieldset>
<div class="field button">
<button type="submit">Add Suite</button>
<button type="button" onclick="document.location.href='/'">Cancel</button>
</div>
</form>
`;
sendOutput(response, output);
};
const sendGetImport = (response) => {
let output = `
<h2>Import Suite</h2>
`;
const detachedSuites = cli_1.Cli.findDetachedSuites();
if (detachedSuites.length == 0) {
output += "<p>There are no unattached *.js files in test folder.</p>";
}
else {
output += `
<script>
function importSuite() {
var form = document.getElementById("frmImport");
var e = document.getElementById("ddFile");
var file = e.options[e.selectedIndex].value;
if (file) {
if (confirm('Import this file ' + file + '.js?')) {
form.submit();
}
}
else {
alert('No file selected.');
}
}
</script>
<form method="POST" id="frmImport" action="/import/suite">
<div class="field">
<label for="ddFile">File to Import</label>
<select name="suite" id="ddFile">
`;
detachedSuites.forEach((file) => {
output += `
<option value="${file}">${file}</option>
`;
});
output += `
</select>
</div>
<div class="field button">
<button type="button" onclick="importSuite()">Import</button>
<button type="button" onclick="window.location.href = '/'">Cancel</button>
</div>
</form>
`;
}
sendOutput(response, output);
};
const removeSuite = (response, suiteName) => {
cli_1.Cli.config.removeSuite(suiteName);
cli_1.Cli.config
.save()
.then(() => {
sendOutput(response, `Removed suite <em>${suiteName}</em>, but did not delete the file. <a href="/">Back</a>`);
})
.catch(ex => {
sendOutput(response, `Error: ${ex}`);
});
};
const removeEnv = (response, envName) => {
cli_1.Cli.config.removeEnvironment(envName);
cli_1.Cli.config
.save()
.then(() => {
sendOutput(response, `Removed environment <em>${envName}</em>, no test scenarios were altered. <a href="/">Back</a>`);
})
.catch(ex => {
sendOutput(response, `Error: ${ex}`);
});
};
const initProject = (response, postData) => {
if (!postData.projectName || !postData.testsPath) {
return sendOutput(response, `<p>Project name and test path are required. <a href="/init">Try again</a></p>`);
}
cli_1.Cli.init({
project: {
name: postData.projectName,
path: postData.testsPath
},
environments: []
})
.then(() => {
let countEnvs = 0;
possibleEnvironments.forEach(env => {
if (postData[`envName[${env}]`]) {
const domain = postData[`envDomain[${env}]`];
cli_1.Cli.config.addEnvironment({
name: env,
defaultDomain: domain
});
countEnvs++;
}
});
if (countEnvs == 0) {
cli_1.Cli.config.addEnvironment({
name: "dev"
});
countEnvs++;
}
cli_1.Cli.config
.save()
.then(() => {
sendOutput(response, `
<p><em>Awesome!</em> You're ready to get going. Flagpole has been initialized in this project.</p>
<p>Next, we recommend you <strong><a href="/add/suite">add your first test suite</a></strong>.</p>
<p>Or you can <a href="/">skip this step</a>.</p>
`);
})
.catch(err => {
sendOutput(response, `
<p>Flagpole was initialized, but there was a problem saving environments: ${err}</p>
<p><a href="/">Continue</a></p>
`);
});
})
.catch(err => {
sendOutput(response, `<p>Error initializing: ${err}</p>p><a href="/init">Try again</a></p>`);
});
};
const addSuite = (response, suite, scenario) => {
cli_1.Cli.addSuite(suite, scenario)
.then(() => {
sendOutput(response, `Added new suite <em>${suite.name}</em>. <a href="/">Back</a>`);
})
.catch(err => {
sendOutput(response, `Error: ${err}`);
});
};
const addEnv = (response, env) => {
if (cli_1.Cli.config.environments[env.name]) {
sendOutput(response, "Error: Environment name is already taken.");
}
else {
cli_1.Cli.config.addEnvironment({
name: env.name,
defaultDomain: env.defaultDomain
});
cli_1.Cli.config
.save()
.then(() => {
sendOutput(response, `Added new environment <em>${env.name}</em>. <a href="/">Back</a>`);
})
.catch(ex => {
sendOutput(response, `Error: ${ex}`);
});
}
};
const runSuite = (response, suiteName, envName) => {
let opts = flagpoleexecutionoptions_1.FlagpoleExecutionOptions.createFromString(`-h -o json -e ${envName} -x`);
const execution = suiteexecution_1.SuiteExecution.executePath(cli_1.Cli.config.suites[suiteName].getTestPath(), opts);
execution.result
.then((result) => {
const json = JSON.parse(result.output.join(" "));
let output = `<h2>${json.title}</h2>`;
json.scenarios.forEach((scenario) => {
output += `<article><h3>${scenario.title}</h3>`;
output += "<ul>";
scenario.log.forEach((logLine) => {
output += `<li class="${logLine.type.toLowerCase()}">${logLine.message}</li>`;
});
output += "</ul></article>";
});
output += `<p><a href="/">Back</a></p>`;
sendOutput(response, output);
})
.catch(err => {
sendOutput(response, err);
});
};
const sendIndex = (response) => {
const suites = cli_1.Cli.config.getSuites();
let output = `
<ul>
<li>Project Name: ${cli_1.Cli.config.project.name}</li>
<li>Config Path: ${cli_1.Cli.configPath}</li>
<li>Project Path: ${cli_1.Cli.projectPath}</li>
<li>Environment: ${flagpoleexecutionoptions_2.FlagpoleExecution.opts.environment}</li>
</ul>
<h2>List of Suites</h2>
<table>
<thead>
<tr>
<th>Name</th>
<th>Tags</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
`;
suites.forEach((suite) => {
output += `
<tr>
<td>${suite.name}</td>
<td>
${suite.tags.length
? suite.tags.join(", ")
: "<em>no tags</em>"}
</td>
<td>
<form method="POST" action="/run">
<button type="submit" name="suite" value="${suite.name}">Run</button>
</form>
<form method="GET" action="/suite">
<button type="submit" name="name" value="${suite.name}">Edit</button>
</form>
<form method="GET" action="/add/scenario">
<button type="submit" name="suite" value="${suite.name}">Add Scenario</button>
</form>
<form method="POST" id="rm_suite_${suite.name}">
<button type="button" onclick="removeSuite('${suite.name}')">Remove</button>
</form>
</td>
</tr>
`;
});
output += `
</tbody>
</table>
<div class="field button">
<form method="GET" action="/add/suite">
<button type="subit">New Suite</button>
</form>
<form method="GET" action="/import/suite">
<button type="submit">Import Suite</button>
</form>
</div>
`;
output += `
<h2>List of Environments</h2>
<table>
<thead>
<tr>
<th>Name</th>
<th>Base Path</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
`;
cli_1.Cli.config.getEnvironments().forEach((env) => {
output += `
<tr>
<td>${env.name}</td>
<td>
<a href="${env.defaultDomain}" target="_new">${env.defaultDomain}</a>
</td>
<td>
<form method="POST" action="/rm?env=${env.name}" id="rm_env_${env.name}">
<button type="button" onclick="removeEnv('${env.name}')">Remove</button>
</form>
</td>
</tr>
`;
});
output += `
</tbody>
</table>
<div class="field button">
<form method="GET" id="addEnv" action="/add/env">
<button type="submit">Add Environment</button>
</form>
</aside>
<script>
function removeEnv(envName) {
const yes = confirm('Remove this environment ' + envName + '?')
if (yes) {
const form = document.querySelector('#rm_env_' + envName);
form.setAttribute('action', '/rm?env=' + envName);
form.submit();
}
}
function removeSuite(suiteName) {
const yes = confirm('Remove this suite ' + suiteName + '?')
if (yes) {
const form = document.querySelector('#rm_suite_' + suiteName);
form.setAttribute('action', '/rm?suite=' + suiteName);
form.submit();
}
}
</script>
`;
sendOutput(response, output);
};
function serve(port = 3000) {
const handler = (request, response) => {
const requestPath = request.url || "/";
const method = (request.method || "GET").toUpperCase();
const url = new url_1.URL(requestPath, `http://localhost:${server.httpPort}`);
const route = `${method} ${url.pathname}`;
function respond(postData) {
return routes[route]
? routes[route](url, response, postData)
: sendIndex(response);
}
if (!cli_1.Cli.isInitialized() && requestPath != "/init") {
return sendGetInit(response);
}
else if (method == "POST") {
let body = "";
request.on("data", function (data) {
body += String(data);
if (body.length > 1e6) {
request.connection.destroy();
}
});
request.on("end", function () {
respond(qs.parse(body));
});
}
else {
respond();
}
};
const server = new webserver_1.WebServer(handler);
server.listen(port).then(() => {
const url = `http://localhost:${server.httpPort}/`;
console.log(`Flagpole Web Server running at: ${url}`);
open(url);
});
}
exports.serve = serve;
//# sourceMappingURL=serve.js.map