UNPKG

siesta-lite

Version:

Stress-free JavaScript unit testing and functional testing tool, works in NodeJS and browsers

975 lines (748 loc) 30.2 kB
/* Siesta 5.6.1 Copyright(c) 2009-2022 Bryntum AB https://bryntum.com/contact https://bryntum.com/products/siesta/license */ Ext.define('Siesta.Recorder.UI.RecorderPanel', { extend : 'Ext.tree.Panel', alias : 'widget.recorderpanel', requires : [ 'Ext.grid.plugin.CellEditing', 'Siesta.Recorder.UI.ActionColumn', 'Siesta.Recorder.UI.ActionIconColumn', 'Siesta.Recorder.UI.TargetColumn', 'Siesta.Recorder.UI.ContextMenu', 'Siesta.Recorder.UI.Store.Action' ], mixins : [ 'Siesta.Project.Browser.UI.CanCopyToClipboard' ], buttonAlign : 'left', border : false, cls : 'siesta-recorderpanel', selModel : { mode : 'MULTI' }, store : { type : 'actionstore' }, rootVisible : false, viewConfig : { markDirty : false, stripeRows : false, allowCopy : true, plugins : { ptype : 'treeviewdragdrop' } }, newActionDefaults : { action : 'click' }, lines : false, test : null, recorder : null, project : null, domContainer : null, recorderConfig : null, editing : null, bufferedRenderer : false, enableColumnMove : false, enableContextMenu : true, enableEditing : true, showToolbars : true, playbackOnly : false, /** * @event startrecord * Fires when a recording is started * @param {Siesta.Recorder.UI.RecorderPanel} this * @param {Siesta.Test} test The test instance to which the recorder is attached */ /** * @event stoprecord * Fires when a recording is stopped * @param {Siesta.Recorder.UI.RecorderPanel} this */ /** * @event play * Fires when a recording is being played back * @param {Siesta.Recorder.UI.RecorderPanel} this */ initComponent : function () { var me = this; var R = Siesta.Resource('Siesta.Recorder.UI.RecorderPanel'); var recorderConfig = this.recorderConfig || {}; if (!this.playbackOnly) { var recorderClass = this.project && this.project.recorderClass || recorderConfig.recorderClass || Siesta.Recorder.ExtJS; if (typeof recorderClass === 'string') { recorderClass = Joose.S.strToClass(recorderClass); } var recorder = me.recorder = me.recorder || new recorderClass(recorderConfig); recorder.on("actionadd", this.onActionAdded, this) recorder.on("actionremove", this.onActionRemoved, this) recorder.on("actionupdate", this.onActionUpdated, this) recorder.on("clear", this.onRecorderClear, this) recorder.on("start", this.onRecorderStart, this) recorder.on("stop", this.onRecorderStop, this) } if (this.enableEditing) { me.plugins = me.plugins ? [].concat(me.plugins) : []; me.editing = me.editing || new Ext.grid.plugin.CellEditing({ clicksToEdit : 1 }); me.editing.on({ beforeedit : me.onBeforeEdit, validateedit : me.onValidateEdit, edit : me.afterEdit, canceledit : me.afterEdit, scope : me }); this.relayEvents(me.editing, ['beforeedit', 'afteredit', 'validateedit']) me.plugins.push(me.editing); this.on('hide', function () { if (this.editing) { this.editing.completeEdit(); } }); this.on({ afteredit : this.onAfterEdit, validateedit : this.onAfterEdit, canceledit : this.onAfterEdit, scope : this, buffer : 200 }); } Ext.applyIf(me, { columns : [ { xtype : 'recorderactioniconcolumn' }, { xtype : 'recorderactioncolumn' }, { xtype : 'targetcolumn', highlightTarget : this.highlightTarget.bind(this) } ].concat( { header : R.get('offsetColumnHeader'), dataIndex : '__offset__', width : 60, sortable : false, menuDisabled : true, tdCls : 'siesta-recorderpanel-offsetcolumn', renderer : function (value, meta, record) { var target = record.getTarget() if (target && target.offset) { return target.offset + '<div class="siesta-recorderpanel-clearoffset fa fa-close"></div>' } }, editor : 'textfield' } ).concat({ xtype : 'actioncolumn', width : 55, align : 'center', sortable : false, menuDisabled : true, tdCls : 'siesta-recorderpanel-actioncolumn', items : [ { iconCls : 'step-icon fa fa-close icon-delete-row', tooltip : 'Delete this action', handler : this.onDeleteStepClick, scope : this }, { iconCls : 'step-icon fa fa-play icon-play-row', tooltip : 'Play this action', handler : this.onPlaySingleStepClick, scope : this }, { iconCls : 'step-icon fa fa-forward icon-play-from-row', tooltip : 'Play from this action', handler : this.onPlayFromStepClick, scope : this } ] }) }); if (this.showToolbars) { me.createToolbars(); } me.callParent(); this.mon(Ext.getBody(), 'mousedown', this.onBodyMouseDown, this, { delegate : '.target-inspector-label' }) if (this.enableContextMenu) { this.contextMenu = new Siesta.Recorder.UI.ContextMenu({ panel : this }); } }, onAfterEdit : function () { if (!this.editing.editing) { this.hideHighlighter(); } }, onBodyMouseDown : function (e, t) { var focusedEl = document.activeElement; if (Ext.fly(focusedEl).up('.siesta-targeteditor')) { e.stopEvent(); e.preventDefault(); focusedEl.value = Ext.htmlDecode(t.innerHTML); } }, onRecorderStart : function () { this.fireEvent('startrecord', this, this.test); this.addCls('recorder-recording'); }, onRecorderStop : function () { this.fireEvent('stoprecord', this); this.removeCls('recorder-recording'); }, hideHighlighter : function () { if (this.test && this.domContainer) { this.domContainer.clearHighlight(); } }, highlightTarget : function (target, offset) { if (!target) { // Pass no target => simply hide highlighter this.hideHighlighter(); return; } var test = this.test; if (!test) { this.hideHighlighter(); return { success : true } } var R = Siesta.Resource('Siesta.Recorder.UI.RecorderPanel'); var resolved, el try { resolved = this.test.normalizeElement(target, true, true, true); el = resolved.el } catch (e) { // sizzle may break on various characters in the query (=, $, etc) } finally { if (!el) { return { success : false, warning : R.get('queryMatchesNothing') } } } var warning = resolved.matchingMultiple ? R.get('queryMatchesMultiple') : '' if (test.isElementVisible(el) && this.domContainer) { var pointToVisualize = Ext.isArray(target) ? target : (offset || ['50%', '50%']); this.domContainer.highlightTarget(el, '<span class="target-inspector-label">' + target + '</span>', pointToVisualize); } else { // If target was provided but no element could be located, return false so // caller can get a hint there is potential trouble warning = warning || R.get('noVisibleElsFound') } return { success : !warning, message : warning }; }, createToolbars : function () { var me = this; var R = Siesta.Resource('Siesta.Recorder.UI.RecorderPanel'); me.dockedItems = [ { xtype : 'toolbar', padding : '3 5', cls : 'siesta-toolbar recorder-toolbar', dock : 'top', height : 45, style : 'border-color:transparent', items : [ { xtype : 'textfield', itemId : 'recording-name', fieldLabel : 'Name', height : 30, width : 200, labelWidth : 50, value : R.get('newRecording') }, { xtype : 'textfield', itemId : 'pageUrl', height : 30, flex : 1, labelWidth : 70, fieldLabel : R.get('pageUrl'), enableKeyEvents : true, listeners : { keyup : function (field, e) { if (e.getKey() == e.ENTER) { this.onPageUrlFieldEnterKey(); } }, scope : this } } ] }, { xtype : 'toolbar', cls : 'siesta-toolbar siesta-recorder-button-toolbar', dock : 'top', height : 45, defaults : { scale : 'medium', tooltipType : 'title', scope : me }, items : [ { iconCls : 'fa fa-circle icon-record', action : 'recorder-start', cls : 'recorder-tool', whenIdle : true, tooltip : R.get('recordTooltip'), handler : me.onRecordClick }, { iconCls : 'fa fa-square', cls : 'recorder-tool', action : 'recorder-stop', handler : me.stop, tooltip : R.get('stopTooltip') }, { iconCls : 'fa fa-play', action : 'recorder-play', cls : 'recorder-tool', handler : me.onPlayClick, tooltip : R.get('playTooltip') }, { iconCls : 'fa fa-close', action : 'recorder-remove-all', cls : 'recorder-tool icon-clear', handler : function () { var me = this; if (me.store.getCount() === 0) return; Ext.Msg.confirm(R.get('removeAllPromptTitle'), R.get('removeAllPromptMessage'), function (btn) { if (btn == 'yes') { // process text value and close... me.clear(); } }); }, tooltip : R.get('clearTooltip') }, { iconCls : 'fa fa-plus', action : 'recorder-add-step', tooltip : R.get('addNewTooltip'), cls : 'recorder-tool', tooltipType : 'title', scope : me, handler : function () { var store = me.store; var selected = me.getSelectionModel().selected.first(); var model = new store.model(Ext.apply({}, this.newActionDefaults)); if (selected && selected.isVisible()) { selected.parentNode.insertChild(selected.get('index') + 1, model); } else { store.getRootNode().appendChild(model); } me.editing.startEdit(model, 1); } }, '->', { xtype : 'splitbutton', text : 'Show source', cls : 'recorder-tool', action : 'recorder-generate-code', handler : this.onGenerateCodeClick, scope : this, menu : { items : [ { text : R.get('showSourceInNewWindow'), scope : this, handler : function () { var win = window.open(null); var body = win.document.body; var recordingName = this.getRecordingName(); var code = this.store.generateCode(recordingName); body.innerHTML = '<pre>' + code + '</pre>'; } } ] } }, window.document.queryCommandSupported('copy') ? { iconCls : 'fa fa-clipboard', cls : 'recorder-tool', action : 'recorder-copy-to-clipboard', handler : me.onCopyToClipboard, tooltip : 'Copy generated code to clipboard' } : null, me.closeButton ] }]; me.bbar = { xtype : 'component', cls : 'cheatsheet', // height : 70, html : this.getCheatSheetText() }; }, getCheatSheetText : function () { var text = '<table><tr><td class="cheatsheet-type">CSS Query:</td><td class="cheatsheet-sample"> .x-btn</td></tr>' if (this.recorder instanceof Siesta.Recorder.ExtJS) { text += '<tr><td class="cheatsheet-type">Component Query:</td><td class="cheatsheet-sample"> &gt;&gt;toolbar button</td></tr>' + '<tr><td class="cheatsheet-type">Composite Query:</td><td class="cheatsheet-sample"> toolbar =&gt; .x-btn</td></tr>' } text += '</table>' return text }, // Attach to a test (and optionally a specific iframe, only used for testing) attachTo : function (test, iframe) { var me = this; var doClear = me.test && me.test.url !== test.url; var recorder = this.recorder this.setTest(test); var recWindow = (iframe && iframe.contentWindow) || (test.scopeProvider && test.scopeProvider.scope); if (recWindow) { me.recorder.attach(recWindow); } if (doClear) me.clear(); }, onPageUrlFieldEnterKey : function () { var descriptor = this.getRecorderTestDescriptor(); if (descriptor && (descriptor.hostPageUrl || descriptor.pageUrl)) { this.domContainer.expand(); this.project.startSingle(descriptor); this.project.on('teststart', function (event, test) { if (test.url === descriptor.url) { // To ensure test is visible this.fireEvent('play', this, test, 0); } }, this, { single : true }); } }, onRecordClick : function () { var test = this.test if (!test) return; var R = Siesta.Resource('Siesta.Recorder.UI.RecorderPanel'); var descriptor = this.getRecorderTestDescriptor(); if (descriptor) { if (this.isRecording()) { this.stop(); } else { var project = this.project; var pageUrl = this.down('#pageUrl').getValue(); // Grab a new test reference from project in case test was rerun while recorder was open test = project.getTestByURL(test.url) || test; this.setTest(test); var scopeProvider = test.scopeProvider; // If we're recording a new URL, or the test has no window (already was cleaned up) - // first launch the test to load the URL into the test iframe if (!scopeProvider || pageUrl && scopeProvider.sourceURL !== pageUrl) { project.on('teststart', function (event, test) { if (test.url === descriptor.url) { this.attachTo(test); this.recorder.start(); } }, this, { single : true }); project.startSingle(descriptor); } else if (test && test.global) { this.attachTo(test); this.recorder.start(); } } } else { Ext.Msg.alert('Error', R.get('noTestStarted')) } }, onPlayClick : function () { var me = this; var descriptor = this.getRecorderTestDescriptor(); if (descriptor) { me.stop(); var testStartListener = function (ev, runningTestInstance) { if (runningTestInstance.url === descriptor.url) { runningTestInstance.on('beforetestfinalizeearly', testFinalizeListener, null, { single : true }); } }; var testFinalizeListener = function (ev, test2) { // important, need to update our reference to the test me.setTest(test2); // Run test first, and before it ends - fire off the recorded steps me.playSteps(); }; var project = me.project; project.on('teststart', testStartListener, null, { single : true }); if (this.store.getCount() > 0) { this.scrollRecordIntoView( this.store.getAt(0)); } project.startSingle(descriptor); } }, onCopyToClipboard : function (button) { var recordingName = this.getRecordingName() var success = this.copyToClipboard(this.store.generateCode(recordingName)) Ext.Msg.show({ animateTarget : button.getEl(), message : success ? 'Code copied successfully' : 'Something went wrong, code was not copied', title : 'Copy to clipboard', modal : false }) setTimeout(function () { Ext.Msg.hide() }, 800) }, stop : function () { this.recorder.stop(); }, clear : function () { this.recorder.clear(); }, onRecorderClear : function () { this.store.getRootNode().removeAll(); }, getRecorderTestDescriptor : function () { var project = this.project; var pageUrl = this.down('#pageUrl').getValue(); var test = this.test; var descriptor = test && project.getScriptDescriptor(test.url); var testHostUrl = descriptor && project.getDescriptorConfig(descriptor, 'pageUrl'); // If user changes target page URL - use an empty virtual test descriptor return pageUrl && testHostUrl !== pageUrl ? { url : '/', enablePageRedirect : true, testCode : 'StartTest(function(t) {})', pageUrl : pageUrl } : (test ? descriptor : null); }, setTest : function (test) { if (this.test) { this.test.un('beforechainstep', this.onBeforeActionExecute, this) } this.test = test if (test) { test.on('beforechainstep', this.onBeforeActionExecute, this) var field = this.down('#pageUrl') if (field && test.scopeProvider) field.setValue(test.scopeProvider.sourceURL || '') test.on('testfinalize', function () { if (test.scopeProvider && test.scopeProvider.crossOriginFailed) { Ext.Msg.alert('Error', 'Recorder attached to the page from different origin - can\'t record anything') } }) } }, generateSteps : function (events) { var me = this; var t = me.test; var steps = (events || this.store.getRange()).map(function (action, index) { if (action.isLeaf()) { var step = action.asStep(t); // wait for targets when playing entire test, // and // when playing actions manually from a certain step, wait for all steps but the first one if (action.getTarget()) step.waitForTarget = !events || index > 0; return [ function (next) { index = events ? me.store.indexOf(action) : index; t.fireEvent('beforechainstep', t, index, step); next(); }, step ] } else { return me.generateSteps(action.childNodes); } }); return steps; }, onBeforeActionExecute : function(event, test, index, step) { var count = this.store.getCount(); this.getSelectionModel().select(index); if (index < count - 2) { this.scrollRecordIntoView( this.store.getAt(index + 2)); } }, onActionAdded : function (event, action) { var root = this.store.getRootNode(); var targetParent = (root.lastChild && root.lastChild.parentNode) || root; var newRecord = targetParent.appendChild(action); this.scrollRecordIntoView(newRecord) }, onActionRemoved : function (event, action) { this.store.getNodeById(action.id).remove(); }, onActionUpdated : function (event, action) { var model = this.store.getNodeById(action.id); model.callJoined('afterEdit', [['target', 'action', '__offset__']]) }, getActions : function (asJooseInstances) { var actionModels = this.store.getRange() return asJooseInstances ? Ext.Array.pluck(actionModels, 'data') : actionModels }, onDestroy : function () { if (this.recorder) { this.recorder.stop(); } this.callParent(arguments); }, scrollRecordIntoView : function (record) { if (this.view.rendered) { this.ensureVisible(record, { animate : { duration : 100 } }); } }, onBeforeEdit : function (cellEditing, editingContext) { var column = editingContext.column; var editor; if (column.xtype === "targetcolumn") { cellEditing.completeEdit(); column.setTargetEditor(editingContext.record); editingContext.value = editingContext.record.get(column.dataIndex); } else { editor = editingContext.column.getEditor(); if (editor.xtype === 'typeeditor') { // Can't populate until we have a test bound editor.populate(this.test); } } if (editingContext.column.dataIndex === 'target' && this.domContainer) { this.domContainer.startInspection(false); } // Offset only relevant for mouseinput actions return editingContext.field !== '__offset__' || editingContext.record.isMouseAction(); }, afterEdit : function (plug, e) { if (e.field === 'action') { var store = e.column.field.store; store.clearFilter(); if (store.getById(e.value).get('type') !== store.getById(e.originalValue).get('type')) { e.record.resetValues(); } } }, onValidateEdit : function (plug, e) { var value = e.value; if (e.field === 'action' && !value) return false; if (e.field === '__offset__') { e.cancel = true; if (value) { var parsed = e.record.parseOffset(value); if (parsed) { e.record.setTargetOffset(parsed); } } else { e.record.clearTargetOffset(); } } else if (e.column.getEditor().applyChanges) { e.cancel = true; e.column.getEditor().applyChanges(e.record); } // Trigger manual refresh of node when 'set' operation is more complex if (e.cancel) { this.afterEdit(plug, e); this.getView().refreshNode(e.record); } }, afterRender : function () { this.callParent(arguments); var view = this.getView(); view.el.on({ mousedown : function (e, t) { var record = view.getRecord(view.findItemByChild(t)); record.clearTargetOffset() view.refreshNode(record); e.stopEvent(); }, delegate : '.siesta-recorderpanel-clearoffset' }) }, isRecording : function () { return this.recorder.active; }, onGenerateCodeClick : function () { var R = Siesta.Resource('Siesta.Recorder.UI.RecorderPanel'); var win = new Ext.Window({ title : R.get('codeWindowTitle'), layout : 'fit', id : 'codeWindow', itemId : 'codeWindow', cls : 'si-recorder-sourcewindow', height : 400, width : 600, autoScroll : true, autoShow : true, constrain : true, modal : true, closeAction : 'destroy', stateful : true, items : { xtype : 'jseditor', mode : 'text/javascript' } }); var field = win.items.first(); var recordingName = this.getRecordingName(); var code = this.store.generateCode(recordingName); field.setValue(code); field.editor.focus(); }, getRecordingName : function () { return this.down('#recording-name').getValue(); }, onDeleteStepClick : function (grid, rowIndex, colIndex, item, e, record) { this.editing && this.editing.completeEdit(); record.remove(); }, onPlaySingleStepClick : function (cmp, rowIndex) { this.playSingle(rowIndex); }, onPlayFromStepClick : function (cmp, rowIndex) { this.playFromStep(rowIndex); }, playSingle : function(index) { if (this.test) { var action = this.store.getAt(index); var steps = this.generateSteps([action]); this.playSteps(steps); } }, playFromStep : function (startIndex) { if (this.test) { this.playRange(startIndex); } }, playRange : function (startIndex, endIndex) { if (this.test) { var steps = this.generateSteps(this.store.getRange(startIndex, endIndex)); this.playSteps(steps); } }, playSteps : function (steps) { var test = this.test if (test) { var me = this; steps = steps || this.generateSteps(); steps = steps instanceof Array ? steps : [ steps ]; test.chain( function (next) { me.fireEvent('play', me, test); next(); }, steps, function (next) { me.fireEvent('stop', me, test); next(); } ); } }, getRecorder : function () { return this.recorder; } });