diff --git a/package.json b/package.json index a9a56048..c358824a 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/web/app/ui/helpers/eachInMap.js b/web/app/ui/helpers/eachInMap.js new file mode 100644 index 00000000..02fae500 --- /dev/null +++ b/web/app/ui/helpers/eachInMap.js @@ -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; +}; \ No newline at end of file diff --git a/web/test/cases/arc-io.js b/web/test/cases/arc-io.js new file mode 100644 index 00000000..1d01ccb9 --- /dev/null +++ b/web/test/cases/arc-io.js @@ -0,0 +1,5 @@ +export default { + testSaveLoad: function() { + + } +} diff --git a/web/test/cases/segment-io.js b/web/test/cases/segment-io.js new file mode 100644 index 00000000..a74e375e --- /dev/null +++ b/web/test/cases/segment-io.js @@ -0,0 +1,7 @@ +import * as test from '../test' + +export default { + testSaveLoad: function() { + test.fail('Nothing works'); + } +} diff --git a/web/test/menu.js b/web/test/menu.js new file mode 100644 index 00000000..7ca5e004 --- /dev/null +++ b/web/test/menu.js @@ -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($('
', {text: action.label, 'class': 'menu-item'}).click(() => { + popup.hide(); + action.invoke(target) + })) + } + } + popup.show(); + popup.offset({ + left: e.pageX, + top: e.pageY + }); + return false; + } +} diff --git a/web/test/runner.html b/web/test/runner.html new file mode 100644 index 00000000..9dcd4fc7 --- /dev/null +++ b/web/test/runner.html @@ -0,0 +1,18 @@ + + + + + + + +
+ + + 0 run / 0 passed / 0 failures +
+ +
+
+ + + diff --git a/web/test/runner.js b/web/test/runner.js new file mode 100644 index 00000000..e52c54e5 --- /dev/null +++ b/web/test/runner.js @@ -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) + } + } +}; diff --git a/web/test/runner.less b/web/test/runner.less new file mode 100644 index 00000000..360c75bc --- /dev/null +++ b/web/test/runner.less @@ -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; +} \ No newline at end of file diff --git a/web/test/suites.js b/web/test/suites.js new file mode 100644 index 00000000..7f399b4f --- /dev/null +++ b/web/test/suites.js @@ -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 + } +} diff --git a/web/test/test.js b/web/test/test.js new file mode 100644 index 00000000..2fac06a8 --- /dev/null +++ b/web/test/test.js @@ -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; +} \ No newline at end of file diff --git a/web/test/tmpl/test-list.html b/web/test/tmpl/test-list.html new file mode 100644 index 00000000..70899687 --- /dev/null +++ b/web/test/tmpl/test-list.html @@ -0,0 +1,26 @@ +{{#eachInMap suites}} +
+ + + {{key}} + + {{#value}} +
+ + + {{name}} + + {{#tests}} +
+ + + + {{name}} + + +
+ {{/tests}} +
+ {{/value}} +
+{{/eachInMap}} diff --git a/webpack.config.dev.js b/webpack.config.dev.js new file mode 100644 index 00000000..4ead5cdd --- /dev/null +++ b/webpack.config.dev.js @@ -0,0 +1,4 @@ +const config = require('./webpack.config.js'); + +config.entry.test_runner = ['./web/test/runner']; +module.exports = config; diff --git a/webpack.config.js b/webpack.config.js index 11a26911..c5b7297c 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -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' }] } };