From 1955d3f8f5c4b42418af333630960b4cff3cce91 Mon Sep 17 00:00:00 2001 From: jdescottes Date: Thu, 21 Aug 2014 00:50:59 +0200 Subject: [PATCH] First implementation of tool tester --- src/js/Events.js | 10 +- src/js/app.js | 11 +- src/js/controller/DrawingController.js | 13 +- src/js/controller/ToolController.js | 9 ++ .../piskel/PublicPiskelController.js | 22 ++-- .../controller/settings/ImportController.js | 26 ++-- src/js/devtools/MouseEvent.js | 25 ++++ src/js/devtools/TestRecordController.js | 76 ++++++++++++ src/js/devtools/TestRecordPlayer.js | 116 ++++++++++++++++++ src/js/devtools/TestRecorder.js | 115 +++++++++++++++++ src/js/rendering/frame/FrameRenderer.js | 22 ++++ src/piskel-script-list.js | 7 ++ 12 files changed, 422 insertions(+), 30 deletions(-) create mode 100644 src/js/devtools/MouseEvent.js create mode 100644 src/js/devtools/TestRecordController.js create mode 100644 src/js/devtools/TestRecordPlayer.js create mode 100644 src/js/devtools/TestRecorder.js diff --git a/src/js/Events.js b/src/js/Events.js index 4c943a70..31d1d0e3 100644 --- a/src/js/Events.js +++ b/src/js/Events.js @@ -2,6 +2,8 @@ var Events = { TOOL_SELECTED : "TOOL_SELECTED", + SELECT_TOOL : "SELECT_TOOL", + TOOL_RELEASED : "TOOL_RELEASED", SELECT_PRIMARY_COLOR: "SELECT_PRIMARY_COLOR", SELECT_SECONDARY_COLOR: "SELECT_SECONDARY_COLOR", @@ -47,5 +49,11 @@ var Events = { ZOOM_CHANGED : "ZOOM_CHANGED", - CURRENT_COLORS_UPDATED : "CURRENT_COLORS_UPDATED" + CURRENT_COLORS_UPDATED : "CURRENT_COLORS_UPDATED", + + MOUSE_EVENT : "MOUSE_EVENT", + + // Tests + + TEST_RECORD_END : "TEST_RECORD_END" }; \ No newline at end of file diff --git a/src/js/app.js b/src/js/app.js index 8c940368..4142bc2f 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -29,8 +29,8 @@ var layer = new pskl.model.Layer("Layer 1"); var frame = new pskl.model.Frame(size.width, size.height); - layer.addFrame(frame); + layer.addFrame(frame); piskel.addLayer(layer); this.corePiskelController = new pskl.controller.piskel.PiskelController(piskel); @@ -115,6 +115,15 @@ } this.storageService.init(); + // test tools + var testModeOn = document.location.href.toLowerCase().indexOf('test=true') !== -1; + if (testModeOn) { + this.testRecorder = new pskl.devtools.TestRecorder(this.piskelController); + this.testRecorder.init(); + + this.testRecordController = new pskl.devtools.TestRecordController(this.testRecorder); + this.testRecordController.init(); + } var drawingLoop = new pskl.rendering.DrawingLoop(); drawingLoop.addCallback(this.render, this); diff --git a/src/js/controller/DrawingController.js b/src/js/controller/DrawingController.js index 838db2d3..b4415679 100644 --- a/src/js/controller/DrawingController.js +++ b/src/js/controller/DrawingController.js @@ -134,8 +134,9 @@ * @private */ ns.DrawingController.prototype.onMousedown_ = function (event) { + $.publish(Events.MOUSE_EVENT, [event, this]); var frame = this.piskelController.getCurrentFrame(); - var coords = this.renderer.getCoordinates(event.clientX, event.clientY); + var coords = this.getSpriteCoordinates(event.clientX, event.clientY); if (event.button === Constants.MIDDLE_BUTTON) { if (frame.containsPixel(coords.x, coords.y)) { @@ -204,10 +205,11 @@ }; ns.DrawingController.prototype.moveTool_ = function (x, y, event) { - var coords = this.renderer.getCoordinates(x, y); + var coords = this.getSpriteCoordinates(x, y); var currentFrame = this.piskelController.getCurrentFrame(); if (this.isClicked) { + $.publish(Events.MOUSE_EVENT, [event, this]); // Warning : do not call setCurrentButton here // mousemove do not have the correct mouse button information on all browsers this.currentToolBehavior.moveToolAt( @@ -248,6 +250,7 @@ */ ns.DrawingController.prototype.onMouseup_ = function (event) { if(this.isClicked) { + $.publish(Events.MOUSE_EVENT, [event, this]); // A mouse button was clicked on the drawing canvas before this mouseup event, // the user was probably drawing on the canvas. // Note: The mousemove movement (and the mouseup) may end up outside @@ -256,7 +259,7 @@ this.isClicked = false; this.setCurrentButton(event); - var coords = this.renderer.getCoordinates(event.clientX, event.clientY); + var coords = this.getSpriteCoordinates(event.clientX, event.clientY); this.currentToolBehavior.releaseToolAt( coords.x, coords.y, @@ -280,6 +283,10 @@ return this.renderer.getCoordinates(screenX, screenY); }; + ns.DrawingController.prototype.getScreenCoordinates = function(spriteX, spriteY) { + return this.renderer.reverseCoordinates(spriteX, spriteY); + }; + ns.DrawingController.prototype.setCurrentButton = function (event) { this.currentMouseButton_ = event.button; }; diff --git a/src/js/controller/ToolController.js b/src/js/controller/ToolController.js index 4aaa988f..270718c8 100644 --- a/src/js/controller/ToolController.js +++ b/src/js/controller/ToolController.js @@ -38,6 +38,8 @@ this.selectTool_(this.tools[0]); // Activate listener on tool panel: $("#tool-section").mousedown($.proxy(this.onToolIconClicked_, this)); + + $.subscribe(Events.SELECT_TOOL, this.onSelectToolEvent_.bind(this)); }; /** @@ -54,6 +56,13 @@ stage.data("selected-tool-class", tool.instance.toolId); }; + ns.ToolController.prototype.onSelectToolEvent_ = function(event, toolId) { + var tool = this.getToolById_(toolId); + if (tool) { + this.selectTool_(tool); + } + }; + /** * @private */ diff --git a/src/js/controller/piskel/PublicPiskelController.js b/src/js/controller/piskel/PublicPiskelController.js index 24ebb4e3..39264cef 100644 --- a/src/js/controller/piskel/PublicPiskelController.js +++ b/src/js/controller/piskel/PublicPiskelController.js @@ -47,17 +47,6 @@ this.duplicateFrameAt(this.getCurrentFrameIndex()); }; - ns.PublicPiskelController.prototype.raiseSaveStateEvent_ = function (fn, args) { - $.publish(Events.PISKEL_SAVE_STATE, { - type : pskl.service.HistoryService.REPLAY_NO_SNAPSHOT, - scope : this, - replay : { - fn : fn, - args : args - } - }); - }; - ns.PublicPiskelController.prototype.replay = function (frame, replayData) { replayData.fn.apply(this.piskelController, replayData.args); }; @@ -141,4 +130,15 @@ return this.piskelController.piskel; }; + ns.PublicPiskelController.prototype.raiseSaveStateEvent_ = function (fn, args) { + $.publish(Events.PISKEL_SAVE_STATE, { + type : pskl.service.HistoryService.REPLAY_NO_SNAPSHOT, + scope : this, + replay : { + fn : fn, + args : args + } + }); + }; + })(); \ No newline at end of file diff --git a/src/js/controller/settings/ImportController.js b/src/js/controller/settings/ImportController.js index b626dbee..3821a1e6 100644 --- a/src/js/controller/settings/ImportController.js +++ b/src/js/controller/settings/ImportController.js @@ -39,7 +39,10 @@ }; ns.ImportController.prototype.onOpenPiskelChange_ = function (evt) { - this.openPiskelFile_(); + var files = this.hiddenOpenPiskelInput.get(0).files; + if (files.length == 1) { + this.openPiskelFile_(files[0]); + } }; ns.ImportController.prototype.onOpenPiskelClick_ = function (evt) { @@ -51,19 +54,14 @@ this.closeDrawer_(); }; - ns.ImportController.prototype.openPiskelFile_ = function () { - var files = this.hiddenOpenPiskelInput.get(0).files; - if (files.length == 1) { - - var file = files[0]; - if (this.isPiskel_(file)){ - pskl.utils.PiskelFileUtils.loadFromFile(file, function (piskel, descriptor, fps) { - piskel.setDescriptor(descriptor); - pskl.app.piskelController.setPiskel(piskel); - pskl.app.animationController.setFPS(fps); - }); - this.closeDrawer_(); - } + ns.ImportController.prototype.openPiskelFile_ = function (file) { + if (this.isPiskel_(file)){ + pskl.utils.PiskelFileUtils.loadFromFile(file, function (piskel, descriptor, fps) { + piskel.setDescriptor(descriptor); + pskl.app.piskelController.setPiskel(piskel); + pskl.app.animationController.setFPS(fps); + }); + this.closeDrawer_(); } }; diff --git a/src/js/devtools/MouseEvent.js b/src/js/devtools/MouseEvent.js new file mode 100644 index 00000000..7fe44904 --- /dev/null +++ b/src/js/devtools/MouseEvent.js @@ -0,0 +1,25 @@ +(function () { + var ns = $.namespace('pskl.devtools'); + + ns.MouseEvent = function (event, coords) { + this.event = { + type : event.type, + button : event.button, + shiftKey : event.shiftKey, + altKey : event.altKey, + ctrlKey : event.ctrlKey + }; + this.coords = coords; + this.type = 'mouse-event'; + }; + + ns.MouseEvent.prototype.equals = function (otherEvent) { + if (otherEvent && otherEvent instanceof ns.MouseEvent) { + var sameEvent = JSON.stringify(otherEvent.event) == JSON.stringify(this.event); + var sameCoords = JSON.stringify(otherEvent.coords) == JSON.stringify(this.coords); + } else { + return false; + } + }; + +})(); \ No newline at end of file diff --git a/src/js/devtools/TestRecordController.js b/src/js/devtools/TestRecordController.js new file mode 100644 index 00000000..22016a1f --- /dev/null +++ b/src/js/devtools/TestRecordController.js @@ -0,0 +1,76 @@ +(function () { + var ns = $.namespace('pskl.devtools'); + + ns.TestRecordController = function (testRecorder) { + this.testRecorder = testRecorder; + $.subscribe(Events.TEST_RECORD_END, this.onTestRecordEnd_.bind(this)); + }; + + ns.TestRecordController.prototype.init = function () { + var fileInput = document.createElement('input'); + fileInput.setAttribute('type', 'file'); + fileInput.addEventListener('change', this.onFileInputChange_.bind(this)); + fileInput.style.display = 'none'; + + var container = document.createElement('div'); + container.style.cssText = 'position:absolute;z-index:10000;margin:5px;padding:10px;background:lightgrey'; + document.body.appendChild(container); + + var loadInput = document.createElement('button'); + loadInput.innerHTML = 'Load Test ...'; + loadInput.addEventListener('click', this.onLoadInputClick_.bind(this)); + + var startInput = document.createElement('button'); + startInput.innerHTML = 'Start record'; + startInput.addEventListener('click', this.onStartInputClick_.bind(this)); + + var stopInput = document.createElement('button'); + stopInput.innerHTML = 'Stop record'; + stopInput.addEventListener('click', this.onStopInputClick_.bind(this)); + stopInput.setAttribute('disabled','disabled'); + + this.container = container; + this.fileInput = this.container.appendChild(fileInput); + this.loadInput = this.container.appendChild(loadInput); + this.startInput = this.container.appendChild(startInput); + this.stopInput = this.container.appendChild(stopInput); + }; + + ns.TestRecordController.prototype.onLoadInputClick_ = function () { + this.fileInput.click(); + }; + + ns.TestRecordController.prototype.onFileInputChange_ = function () { + var files = this.fileInput.files; + if (files.length == 1) { + var file =files[0]; + pskl.utils.FileUtils.readFile(file, function (content) { + var testRecord = JSON.parse(window.atob(content.replace(/data\:.*?\;base64\,/,''))); + var testRecordPlayer = new ns.TestRecordPlayer(testRecord); + testRecordPlayer.start(); + }.bind(this)); + } + }; + + ns.TestRecordController.prototype.onStartInputClick_ = function () { + this.testRecorder.startRecord(); + this.startInput.setAttribute('disabled','disabled'); + this.stopInput.removeAttribute('disabled'); + }; + + ns.TestRecordController.prototype.onStopInputClick_ = function () { + var testRecord = this.testRecorder.stopRecord(); + + pskl.utils.BlobUtils.stringToBlob(testRecord, function(blob) { + pskl.utils.FileUtils.downloadAsFile(blob, 'record_piskel.json'); + }.bind(this), "application/json"); + + this.startInput.removeAttribute('disabled'); + this.stopInput.setAttribute('disabled','disabled'); + }; + + ns.TestRecordController.prototype.onTestRecordEnd_ = function (evt, success) { + console.log('test finished : ', success); + }; + +})(); \ No newline at end of file diff --git a/src/js/devtools/TestRecordPlayer.js b/src/js/devtools/TestRecordPlayer.js new file mode 100644 index 00000000..eef846c6 --- /dev/null +++ b/src/js/devtools/TestRecordPlayer.js @@ -0,0 +1,116 @@ +(function () { + var ns = $.namespace('pskl.devtools'); + + ns.TestRecordPlayer = function (testRecord) { + this.initialState = testRecord.initialState; + this.events = testRecord.events; + this.png = testRecord.png; + this.shim = null; + }; + + ns.TestRecordPlayer.STEP = 30; + + ns.TestRecordPlayer.prototype.start = function () { + this.setupInitialState_(); + this.createMouseShim_(); + this.playEvent_(0); + }; + + ns.TestRecordPlayer.prototype.setupInitialState_ = function () { + var size = this.initialState.size; + var piskel = this.createPiskel_(size.width, size.height); + pskl.app.piskelController.setPiskel(piskel, true); + + $.publish(Events.SELECT_PRIMARY_COLOR, [this.initialState.primaryColor]); + $.publish(Events.SELECT_SECONDARY_COLOR, [this.initialState.secondaryColor]); + $.publish(Events.SELECT_TOOL, [this.initialState.selectedTool]); + }; + + ns.TestRecordPlayer.prototype.createMouseShim_ = function () { + this.shim = document.createElement('DIV'); + this.shim.style.cssText = 'position:fixed;top:0;left:0;right:0;left:0;bottom:0;z-index:15000'; + this.shim.addEventListener('mousemove', function (e) { + e.stopPropagation(); + e.preventDefault(); + }); + document.body.appendChild(this.shim); + }; + + ns.TestRecordPlayer.prototype.createPiskel_ = function (width, height) { + var descriptor = new pskl.model.piskel.Descriptor('TestPiskel', ''); + var piskel = new pskl.model.Piskel(width, height, descriptor); + var layer = new pskl.model.Layer("Layer 1"); + var frame = new pskl.model.Frame(width, height); + + layer.addFrame(frame); + piskel.addLayer(layer); + + return piskel; + }; + + ns.TestRecordPlayer.prototype.playEvent_ = function (index) { + this.timer = window.setTimeout(function () { + var recordEvent = this.events[index]; + + if (recordEvent.type === 'mouse-event') { + this.playMouseEvent_(recordEvent); + } else if (recordEvent.type === 'color-event') { + this.playColorEvent_(recordEvent); + } else if (recordEvent.type === 'tool-event') { + this.playToolEvent_(recordEvent); + } else if (recordEvent.type === 'instrumented-event') { + this.playInstrumentedEvent_(recordEvent); + } + + if (this.events[index+1]) { + this.playEvent_(index+1); + } else { + this.onTestEnd_(); + } + }.bind(this), ns.TestRecordPlayer.STEP); + }; + + ns.TestRecordPlayer.prototype.onTestEnd_ = function () { + var renderer = new pskl.rendering.PiskelRenderer(pskl.app.piskelController); + var png = renderer.renderAsCanvas().toDataURL(); + + var success = png === this.png; + + this.shim.parentNode.removeChild(this.shim); + this.shim = null; + + $.publish(Events.TEST_RECORD_END, [success]); + }; + + ns.TestRecordPlayer.prototype.playMouseEvent_ = function (recordEvent) { + var event = recordEvent.event; + var screenCoordinates = pskl.app.drawingController.getScreenCoordinates(recordEvent.coords.x, recordEvent.coords.y); + event.clientX = screenCoordinates.x; + event.clientY = screenCoordinates.y; + if (event.type == 'mousedown') { + pskl.app.drawingController.onMousedown_(event); + } else if (event.type == 'mouseup') { + pskl.app.drawingController.onMouseup_(event); + } else if (event.type == 'mousemove') { + pskl.app.drawingController.onMousemove_(event); + } + }; + + ns.TestRecordPlayer.prototype.playColorEvent_ = function (recordEvent) { + if (recordEvent.isPrimary) { + $.publish(Events.SELECT_PRIMARY_COLOR, [recordEvent.color]); + } else { + $.publish(Events.SELECT_SECONDARY_COLOR, [recordEvent.color]); + } + }; + + ns.TestRecordPlayer.prototype.playToolEvent_ = function (recordEvent) { + $.publish(Events.SELECT_TOOL, [recordEvent.toolId]); + }; + + ns.TestRecordPlayer.prototype.playInstrumentedEvent_ = function (recordEvent) { + pskl.app.piskelController[recordEvent.methodName].apply(pskl.app.piskelController, recordEvent.args); + }; + + +})(); \ No newline at end of file diff --git a/src/js/devtools/TestRecorder.js b/src/js/devtools/TestRecorder.js new file mode 100644 index 00000000..b04b7b97 --- /dev/null +++ b/src/js/devtools/TestRecorder.js @@ -0,0 +1,115 @@ +(function () { + var ns = $.namespace('pskl.devtools'); + + ns.TestRecorder = function (piskelController) { + this.piskelController = piskelController; + this.isRecording = false; + this.reset(); + }; + + ns.TestRecorder.prototype.init = function () { + $.subscribe(Events.MOUSE_EVENT, this.onMouseEvent_.bind(this)); + $.subscribe(Events.TOOL_SELECTED, this.onToolEvent_.bind(this)); + $.subscribe(Events.PRIMARY_COLOR_SELECTED, this.onColorEvent_.bind(this, true)); + $.subscribe(Events.SECONDARY_COLOR_SELECTED, this.onColorEvent_.bind(this, false)); + + for (var key in this.piskelController) { + if (typeof this.piskelController[key] == 'function') { + var methodTriggersReset = this.piskelController[key].toString().indexOf('Events.PISKEL_RESET') != -1; + if (methodTriggersReset) { + this.piskelController[key] = this.instrumentMethod_(this.piskelController, key); + } + } + } + }; + + ns.TestRecorder.prototype.instrumentMethod_ = function (object, methodName) { + var method = object[methodName]; + var testRecorder = this; + return function () { + testRecorder.onInstrumentedMethod_(object, methodName, arguments); + return method.apply(this, arguments); + }; + }; + + ns.TestRecorder.prototype.reset = function () { + this.initialState = {}; + this.events = []; + }; + + ns.TestRecorder.prototype.startRecord = function () { + this.isRecording = true; + this.initialState = { + size : { + width : this.piskelController.getWidth(), + height : this.piskelController.getHeight() + }, + primaryColor : pskl.app.paletteController.getPrimaryColor(), + secondaryColor : pskl.app.paletteController.getSecondaryColor(), + selectedTool : pskl.app.toolController.currentSelectedTool.instance.toolId + }; + }; + + ns.TestRecorder.prototype.stopRecord = function () { + this.isRecording = false; + + var renderer = new pskl.rendering.PiskelRenderer(this.piskelController); + var png = renderer.renderAsCanvas().toDataURL(); + + var testRecord = JSON.stringify({ + events : this.events, + initialState : this.initialState, + png : png + }); + + this.reset(); + + return testRecord; + }; + + ns.TestRecorder.prototype.onMouseEvent_ = function (evt, mouseEvent, originator) { + if (this.isRecording) { + this.recordMouseEvent_(mouseEvent); + } + }; + + ns.TestRecorder.prototype.onColorEvent_ = function (isPrimary, evt, color) { + if (this.isRecording) { + var recordEvent = {}; + recordEvent.type = 'color-event'; + recordEvent.color = color; + recordEvent.isPrimary = isPrimary; + this.events.push(recordEvent); + } + }; + + ns.TestRecorder.prototype.onToolEvent_ = function (evt, tool) { + if (this.isRecording) { + var recordEvent = {}; + recordEvent.type = 'tool-event'; + recordEvent.toolId = tool.toolId; + this.events.push(recordEvent); + } + }; + + ns.TestRecorder.prototype.onInstrumentedMethod_ = function (callee, methodName, args) { + if (this.isRecording) { + var recordEvent = {}; + recordEvent.type = 'instrumented-event'; + recordEvent.methodName = methodName; + recordEvent.args = Array.prototype.slice.call(args, 0); + this.events.push(recordEvent); + } + }; + + ns.TestRecorder.prototype.recordMouseEvent_ = function (mouseEvent) { + var coords = pskl.app.drawingController.getSpriteCoordinates(mouseEvent.clientX, mouseEvent.clientY); + var recordEvent = new ns.MouseEvent(mouseEvent, coords); + var lastEvent = this.events[this.events.length-1]; + + if (!recordEvent.equals(lastEvent)) { + this.events.push(recordEvent); + } + }; + +})(); \ No newline at end of file diff --git a/src/js/rendering/frame/FrameRenderer.js b/src/js/rendering/frame/FrameRenderer.js index ae16f7a9..94d5a440 100644 --- a/src/js/rendering/frame/FrameRenderer.js +++ b/src/js/rendering/frame/FrameRenderer.js @@ -196,6 +196,28 @@ }; }; + ns.FrameRenderer.prototype.reverseCoordinates = function(x, y) { + var cellSize = this.zoom; + + x = x * cellSize; + y = y * cellSize; + + x = x - this.offset.x * cellSize; + y = y - this.offset.y * cellSize; + + x = x + this.margin.x; + y = y + this.margin.y; + + var containerOffset = this.container.offset(); + x = x + containerOffset.left; + y = y + containerOffset.top; + + return { + x : x + (cellSize/2), + y : y + (cellSize/2) + }; + }; + /** * @private */ diff --git a/src/piskel-script-list.js b/src/piskel-script-list.js index 09e4041b..2fc6e651 100644 --- a/src/piskel-script-list.js +++ b/src/piskel-script-list.js @@ -137,6 +137,13 @@ "js/drawingtools/selectiontools/ShapeSelect.js", "js/drawingtools/ColorPicker.js", "js/drawingtools/ColorSwap.js", + + // Devtools + "js/devtools/MouseEvent.js", + "js/devtools/TestRecorder.js", + "js/devtools/TestRecordPlayer.js", + "js/devtools/TestRecordController.js", + // Application controller and initialization "js/app.js", // Bonus features !!