first approach to test framework UI

This commit is contained in:
Val Erastov 2016-12-23 12:45:19 -08:00
parent a2e90819b7
commit 9f3276fd87
13 changed files with 338 additions and 4 deletions

View file

@ -3,7 +3,7 @@
"version": "0.1.0",
"description": "JS.Sketcher is a parametric 2D and 3D CAD modeler written in pure javascript",
"scripts": {
"start": "webpack-dev-server --content-base web/ --port 3000",
"start": "webpack-dev-server --config webpack.config.dev.js --content-base web/ --port 3000",
"pack": "webpack --config webpack.config.js --progress --profile --colors",
"build": "grunt",
"lint": "eslint web/app -c ./build/.eslintrc.json --ignore-path ./build/.eslintignore"
@ -45,7 +45,7 @@
"diff-match-patch": "1.0.0",
"numeric": "1.2.6",
"jwerty": "0.3.2",
"mustache-loader": "0.3.3",
"handlebars-loader": "1.4.0",
"jquery": "2.1.0",
"less": "2.7.1",
"libtess": "1.2.2"

View file

@ -0,0 +1,7 @@
export default function( map, block ) {
let out = '';
Object.keys( map ).map(function( prop ) {
out += block.fn( {key: prop, value: map[ prop ]} );
});
return out;
};

5
web/test/cases/arc-io.js Normal file
View file

@ -0,0 +1,5 @@
export default {
testSaveLoad: function() {
}
}

View file

@ -0,0 +1,7 @@
import * as test from '../test'
export default {
testSaveLoad: function() {
test.fail('Nothing works');
}
}

40
web/test/menu.js Normal file
View file

@ -0,0 +1,40 @@
//import {DefaultMouseEvent} from '../app/3d/ui/utils'
export class Menu {
constructor(actions) {
//this.mouseInfo = new DefaultMouseEvent();
this.actions = actions;
this.popup = $('#popup-menu');
$(document)
.on('mousemove', (e) => this.mouseInfo = e)
.on('click', (e) => this.popup.hide())
.on('click', '.right-click-menu', (e) => this.onShowMenu(e, $(e.currentTarget)))
.on('contextmenu', (e) => {
const target = $(e.target).closest('.right-click-menu');
if (target.length == 0) return true;
return this.onShowMenu(e, target);
});
}
onShowMenu(e, target) {
const popup = this.popup;
popup.empty();
const actions = target.data('menu').split(',').map(s => s.trim());
for (let actionId of actions) {
const action = this.actions[actionId];
if (action) {
popup.append($('<div>', {text: action.label, 'class': 'menu-item'}).click(() => {
popup.hide();
action.invoke(target)
}))
}
}
popup.show();
popup.offset({
left: e.pageX,
top: e.pageY
});
return false;
}
}

18
web/test/runner.html Normal file
View file

@ -0,0 +1,18 @@
<html>
<head>
<title></title>
<script src="../static/test_runner.bundle.js"></script>
<link rel="stylesheet" href="../lib/font-awesome/css/font-awesome.min.css?modeler">
</head>
<body>
<div id="control-bar" class="page-row">
<button id="run-button"><i class="fa fa-play"></i></button>
<button id="pause-button"><i class="fa fa-pause"></i></button>
<span id="status" style="float: right">0 run / 0 passed / 0 failures</span>
</div>
<iframe id="main-sandbox"></iframe>
<div class="page-row" id="test-list">
</div>
<div id="popup-menu" style="position: absolute; display: none;"></div>
</body>
</html>

127
web/test/runner.js Normal file
View file

@ -0,0 +1,127 @@
import './runner.less'
import TestList from './tmpl/test-list.html'
import '../app/utils/jqueryfy'
import suites from './suites'
import {Menu} from './menu'
import {AssertionError} from './test'
$(() => {
const runBtn = $('#run-button');
const pauseBtn = $('#pause-button');
disableBtn(pauseBtn);
runBtn.click(() => {
run();
disableBtn(runBtn);
enableBtn(pauseBtn);
});
pauseBtn.click(() => {
disableBtn(pauseBtn);
enableBtn(runBtn);
});
console.log(suites);
$('#test-list').html(TestList({suites}));
new Menu(ACTIONS);
});
function runSuite(name) {
const testCases = suites[name];
let success = true;
for (let testCase of testCases) {
if (!runTestCase(testCase, name + ':' +testCase.name)) {
success = false;
}
}
updateIcon($('#suite-' + name), success);
}
function runTestCase(testCase, caseId) {
let success = true;
for (let test of testCase.tests) {
if (!runTest(test, caseId + ':' + test.name)) {
success = false;
}
}
updateIcon($('#case-' + caseId.replace(/:/g, '-')), success);
}
function runTest(test, testId) {
let success = true;
let testDom = $('#test-' + testId.replace(/:/g, '-'));
testDom.find('.status').hide();
testDom.find('.progress').show();
try {
test();
} catch (e) {
success = false;
if (e instanceof AssertionError) {
testDom.find('.result').text(e.msg);
}
}
testDom.find('.progress').hide();
testDom.find('.status').show();
updateIcon(testDom, success);
return success;
}
function run() {
for (let suite of Object.keys(suites)) {
runSuite(suite);
}
}
function pause() {
}
function updateIcon(dom, success) {
dom.find('.status').addClass(success ? 'status-success' : 'status-fail');
}
function findTestCaseById(id) {
const suite = suites[id[0]];
return suite.filter(tc => tc.name == id[1])[0];
}
function findTestById(id) {
const testCase = findTestCaseById(id);
return testCase.tests.filter(t => t.name == id[2])[0];
}
function disableBtn(btn) {
btn.attr('disabled', 'disabled');
}
function enableBtn(btn) {
btn.removeAttr('disabled');
}
const ACTIONS = {
RunSuite: {
label: "Run Suite",
invoke: (target) => runSuite(target.data('suiteName'))
},
RunTestCase: {
label: "Run Test Case",
invoke: (target) => {
var testCaseIdStr = target.data('testCaseId');
const testCaseId = testCaseIdStr.split(':');
runTestCase(findTestCaseById(testCaseId), testCaseIdStr);
}
},
RunTest: {
label: "Run Test",
invoke: (target) => {
var testIdStr = target.data('testId');
const testId = testIdStr.split(':');
runTest(findTestById(testId), testIdStr)
}
}
};

58
web/test/runner.less Normal file
View file

@ -0,0 +1,58 @@
body {
font-family: 'Helvetica Neue Light', HelveticaNeue-Light, 'Helvetica Neue', Helvetica, sans-serif;
margin: 0;
}
#main-sandbox {
border: 3px plum solid;
box-sizing: border-box;
width: 100%;
height: 60%;
}
.page-row {
margin: 1px 1px;
}
button {
font-size: 14px;
}
.status {
color: #ccc;
font-size: 11px;
}
#popup-menu {
padding: 5px 0 5px 0;
border: 2px #777 solid;
background: #eee;
}
.menu-item {
padding: 2px 3px 2px 3px;
cursor: pointer;
}
.menu-item:hover {
background-color: #0074D9;
color: #fff;
}
.right-click-menu {
cursor: pointer;
}
.status-success {
color: green;
}
.status-fail {
color: red;
}
.test-node .result {
font-style: italic;
font-size: 12px;
color: #aaa;
}

27
web/test/suites.js Normal file
View file

@ -0,0 +1,27 @@
export default {
SketcherIO: [
TestCase('segment-io'),
TestCase('arc-io')
],
SketcherTools: [
],
Sketcher: [
],
ModellerOperations: [
],
};
function TestCase(name) {
let tests = require('./cases/' + name).default;
tests = Object.keys(tests).filter(key => key.startsWith('test')).map(key => tests[key]);
return {
name, tests
}
}

15
web/test/test.js Normal file
View file

@ -0,0 +1,15 @@
export function fail(msg, optionalMsg) {
optionalMsg = (optionalMsg === undefined ? '' : ' ' + optionalMsg);
throw new AssertionError(msg + optionalMsg);
}
export function assertEquals(expected, actual, msg) {
if (expected !== actual) {
fail('assertEquals: Expected: ' + expected + ' but was ' + actual, msg);
}
}
export function AssertionError(msg) {
this.msg = msg;
}

View file

@ -0,0 +1,26 @@
{{#eachInMap suites}}
<div class="suite-node" id="suite-{{key}}">
<span class="status"><i class="fa fa-circle"></i></span>
<span class="right-click-menu" data-menu="RunSuite" data-suite-name="{{key}}">
<b>{{key}}</b>
</span>
{{#value}}
<div class="test-case-node" id="case-{{../key}}-{{name}}" style="margin-left: 15px;">
<span class="status"><i class="fa fa-circle"></i></span>
<span class="right-click-menu" data-menu="RunTestCase" data-test-case-id="{{../key}}:{{name}}">
{{name}}
</span>
{{#tests}}
<div class="test-node" id="test-{{../../key}}-{{../name}}-{{name}}" style="margin-left: 30px;">
<span class="status"><i class="fa fa-circle"></i></span>
<span class="progress" style="display: none"><i class="fa fa-cog fa-spin"></i></span>
<span class="right-click-menu" data-menu="RunTest" data-test-id="{{../../key}}:{{../name}}:{{name}}">
{{name}}
<span class="result"></span>
</span>
</div>
{{/tests}}
</div>
{{/value}}
</div>
{{/eachInMap}}

4
webpack.config.dev.js Normal file
View file

@ -0,0 +1,4 @@
const config = require('./webpack.config.js');
config.entry.test_runner = ['./web/test/runner'];
module.exports = config;

View file

@ -20,7 +20,7 @@ module.exports = {
loaders: [{
test: /\.js$/,
loaders: ['babel'],
include: path.join(__dirname, 'web/app')
include: [path.join(__dirname, 'web/app'), path.join(__dirname, 'web/test')]
}, {
test: /\.css$/,
loader: 'style!css'
@ -31,7 +31,7 @@ module.exports = {
},
{
test: /\.html$/,
loader: 'mustache'
loader: 'handlebars?helperDirs[]=' + __dirname + '/web/app/ui/helpers'
}]
}
};