tagui
Version:
Command-line tool for digital process automation (RPA)
1,268 lines (1,084 loc) • 43.1 kB
JavaScript
//----------------------------------------------------------------------------
//Copyright (c) 2005 Zope Foundation and Contributors.
//This software is subject to the provisions of the Zope Public License,
//Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
//THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
//WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
//WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
//FOR A PARTICULAR PURPOSE.
//TestRecorder - a javascript library to support browser test recording. It
//is designed to be as cross-browser compatible as possible and to promote
//loose coupling with the user interface, making it easier to evolve the UI
//and experiment with alternative interfaces.
//caveats: popup windows undefined, cant handle framesets
//todo:
//- capture submit (w/lookback for doctest)
//- cleanup strings
//Contact Brian Lloyd (brian@zope.com) with questions or comments.
//---------------------------------------------------------------------------
if (typeof(TestRecorder) == "undefined") {
TestRecorder = {};
}
//---------------------------------------------------------------------------
//Browser -- a singleton that provides a cross-browser API for managing event
//handlers and miscellaneous browser functions.
//Methods:
//captureEvent(window, name, handler) -- capture the named event occurring
//in the given window, setting the function handler as the event handler.
//The event name should be of the form "click", "blur", "change", etc.
//releaseEvent(window, name, handler) -- release the named event occurring
//in the given window. The event name should be of the form "click", "blur",
//"change", etc.
//getSelection(window) -- return the text currently selected, or the empty
//string if no text is currently selected in the browser.
//---------------------------------------------------------------------------
if (typeof(TestRecorder.Browser) == "undefined") {
TestRecorder.Browser = {};
}
TestRecorder.Browser.captureEvent = function(wnd, name, func) {
var lname = name.toLowerCase();
var doc = wnd.document;
wnd.captureEvents(Event[name.toUpperCase()]);
wnd["on" + lname] = func;
}
TestRecorder.Browser.releaseEvent = function(wnd, name, func) {
var lname = name.toLowerCase();
var doc = wnd.document;
wnd.releaseEvents(Event[name.toUpperCase()]);
wnd["on" + lname] = null;
}
TestRecorder.Browser.getSelection = function(wnd) {
var doc = wnd.document;
if (wnd.getSelection) {
return wnd.getSelection() + "";
}
else if (doc.getSelection) {
return doc.getSelection() + "";
}
else if (doc.selection && doc.selection.createRange) {
return doc.selection.createRange().text + "";
}
return "";
}
TestRecorder.Browser.windowHeight = function(wnd) {
var doc = wnd.document;
if (wnd.innerHeight) {
return wnd.innerHeight;
}
else if (doc.documentElement && doc.documentElement.clientHeight) {
return doc.documentElement.clientHeight;
}
else if (document.body) {
return document.body.clientHeight;
}
return -1;
}
TestRecorder.Browser.windowWidth = function(wnd) {
var doc = wnd.document;
if (wnd.innerWidth) {
return wnd.innerWidth;
}
else if (doc.documentElement && doc.documentElement.clientWidth) {
return doc.documentElement.clientWidth;
}
else if (document.body) {
return document.body.clientWidth;
}
return -1;
}
//---------------------------------------------------------------------------
//Event -- a class that provides a cross-browser API dealing with most of the
//interesting information about events.
//Methods:
//type() -- returns the string type of the event (e.g. "click")
//target() -- returns the target of the event
//button() -- returns the mouse button pressed during the event. Because
//it is not possible to reliably detect a middle button press, this method
//only recognized the left and right mouse buttons. Returns one of the
//constants Event.LeftButton, Event.RightButton or Event.UnknownButton for
//a left click, right click, or indeterminate (or no mouse click).
//keycode() -- returns the index code of the key pressed. Note that this
//value may differ across browsers because of character set differences.
//Whenever possible, it is suggested to use keychar() instead.
//keychar() -- returns the char version of the key pressed rather than a
//raw numeric code. The resulting value is subject to all of the vagaries
//of browsers, character encodings in use, etc.
//shiftkey() -- returns true if the shift key was pressed.
//posX() -- return the X coordinate of the mouse relative to the document.
//posY() -- return the y coordinate of the mouse relative to the document.
//stopPropagation() -- stop event propagation (if supported)
//preventDefault() -- prevent the default action (if supported)
//---------------------------------------------------------------------------
TestRecorder.Event = function(e) {
this.event = (e) ? e : window.event;
}
TestRecorder.Event.LeftButton = 0;
TestRecorder.Event.MiddleButton = 1;
TestRecorder.Event.RightButton = 2;
TestRecorder.Event.UnknownButton = 3;
TestRecorder.Event.prototype.stopPropagation = function() {
if (this.event.stopPropagation)
this.event.stopPropagation();
}
TestRecorder.Event.prototype.preventDefault = function() {
if (this.event.preventDefault)
this.event.preventDefault();
}
TestRecorder.Event.prototype.type = function() {
return this.event.type;
}
TestRecorder.Event.prototype.button = function() {
if (this.event.button) {
if (this.event.button == 2) {
return TestRecorder.Event.RightButton;
}
return TestRecorder.Event.LeftButton;
}
else if (this.event.which) {
if (this.event.which > 1) {
return TestRecorder.Event.RightButton;
}
return TestRecorder.Event.LeftButton;
}
return TestRecorder.Event.UnknownButton;
}
TestRecorder.Event.prototype.target = function() {
var t = (this.event.target) ? this.event.target : this.event.srcElement;
if (t && t.nodeType == 3) // safari bug
return t.parentNode;
return t;
}
TestRecorder.Event.prototype.keycode = function() {
return (this.event.keyCode) ? this.event.keyCode : this.event.which;
}
TestRecorder.Event.prototype.keychar = function() {
return String.fromCharCode(this.keycode());
}
TestRecorder.Event.prototype.shiftkey = function() {
if (this.event.shiftKey)
return true;
return false;
}
TestRecorder.Event.prototype.posX = function() {
if (this.event.pageX)
return this.event.pageX;
else if (this.event.clientX) {
return this.event.clientX + document.body.scrollLeft;
}
return 0;
}
TestRecorder.Event.prototype.posY = function() {
if (this.event.pageY)
return this.event.pageY;
else if (this.event.clientY) {
return this.event.clientY + document.body.scrollTop;
}
return 0;
}
//---------------------------------------------------------------------------
//TestCase -- this class contains the interesting events that happen in
//the course of a test recording and provides some testcase metadata.
//Attributes:
//title -- the title of the test case.
//items -- an array of objects representing test actions and checks
//---------------------------------------------------------------------------
TestRecorder.TestCase = function() {
this.title = "Test Case";
// maybe some items are already stored in the background
// but we do not need them here anyway
this.items = new Array();
}
TestRecorder.TestCase.prototype.append = function(o) {
this.items[this.items.length] = o;
chrome.runtime.sendMessage({action: "append", obj: o});
}
TestRecorder.TestCase.prototype.peek = function() {
return this.items[this.items.length - 1];
}
TestRecorder.TestCase.prototype.poke = function(o) {
this.items[this.items.length - 1] = o;
chrome.runtime.sendMessage({action: "poke", obj: o});
}
//---------------------------------------------------------------------------
//Event types -- whenever an interesting event happens (an action or a check)
//it is recorded as one of the object types defined below. All events have a
//'type' attribute that marks the type of the event (one of the values in the
//EventTypes enumeration) and different attributes to capture the pertinent
//information at the time of the event.
//---------------------------------------------------------------------------
if (typeof(TestRecorder.EventTypes) == "undefined") {
TestRecorder.EventTypes = {};
}
TestRecorder.EventTypes.OpenUrl = 0;
TestRecorder.EventTypes.Click = 1;
TestRecorder.EventTypes.Change = 2;
TestRecorder.EventTypes.Comment = 3;
TestRecorder.EventTypes.Submit = 4;
TestRecorder.EventTypes.CheckPageTitle = 5;
TestRecorder.EventTypes.CheckPageLocation = 6;
TestRecorder.EventTypes.CheckTextPresent = 7;
TestRecorder.EventTypes.CheckValue = 8;
TestRecorder.EventTypes.CheckValueContains = 9;
TestRecorder.EventTypes.CheckText = 10;
TestRecorder.EventTypes.CheckHref = 11;
TestRecorder.EventTypes.CheckEnabled = 12;
TestRecorder.EventTypes.CheckDisabled = 13;
TestRecorder.EventTypes.CheckSelectValue = 14;
TestRecorder.EventTypes.CheckSelectOptions = 15;
TestRecorder.EventTypes.CheckImageSrc = 16;
TestRecorder.EventTypes.PageLoad = 17;
TestRecorder.EventTypes.ScreenShot = 18;
TestRecorder.EventTypes.ElementScreenShot = 24;
TestRecorder.EventTypes.MoveCursorToElement = 25;
TestRecorder.EventTypes.PrintElementText = 26;
TestRecorder.EventTypes.SaveElementText = 27;
TestRecorder.EventTypes.ExplicitWait = 28;
TestRecorder.EventTypes.FetchElementText = 29;
TestRecorder.EventTypes.SelectElementOption = 30;
TestRecorder.EventTypes.CancelLastStep = 31;
TestRecorder.EventTypes.NoteDownElement = 32;
TestRecorder.EventTypes.InspectElement = 33;
TestRecorder.EventTypes.Cancel = 99;
TestRecorder.EventTypes.MouseDown = 19;
TestRecorder.EventTypes.MouseUp = 20;
TestRecorder.EventTypes.MouseDrag = 21;
TestRecorder.EventTypes.MouseDrop = 22;
TestRecorder.EventTypes.KeyPress = 23;
TestRecorder.ElementInfo = function(element) {
this.action = element.action;
this.method = element.method;
this.href = element.href;
this.tagName = element.tagName;
this.selector = this.getCleanCSSSelector(element);
this.value = element.value;
this.checked = element.checked;
this.name = element.name;
this.type = element.type;
if (this.type)
this.type = this.type.toLowerCase();
if (element.form)
this.form = {id: element.form.id, name: element.form.getAttribute('name')};
this.src = element.src;
this.id = element.id;
this.title = element.title;
this.selectedIndex = element.selectedIndex;
this.options = [];
if (element.selectedIndex) {
for (var i=0; i < element.options.length; i++) {
var o = element.options[i];
this.options[i] = {text:o.text, value:o.value};
}
}
this.label = this.findLabelText(element);
}
TestRecorder.ElementInfo.prototype.findLabelText = function(element) {
var label = this.findContainingLabel(element)
var text;
if (!label) {
label = this.findReferencingLabel(element);
}
if (label) {
text = label.innerHTML;
// remove newlines
text = text.replace('\n', ' ');
// remove tags
text = text.replace(/<[^>]*>/g, ' ');
// remove non-alphanumeric prefixes or suffixes
text = text.replace(/^\W*/mg, '')
text = text.replace(/\W*$/mg, '')
// remove extra whitespace
text = text.replace(/^\s*/, '').replace(/\s*$/, '').replace(/\s+/g, ' ');
}
return text;
}
TestRecorder.ElementInfo.prototype.findReferencingLabel = function(element) {
var labels = window.document.getElementsByTagName('label')
for (var i = 0; i < labels.length; i++) {
if (labels[i].attributes['for'] &&
labels[i].attributes['for'].value == element.id)
return labels[i]
}
}
TestRecorder.ElementInfo.prototype.findContainingLabel = function(element) {
var parent = element.parentNode;
if (!parent)
return undefined;
if (parent.tagName && parent.tagName.toLowerCase() == 'label')
return parent;
else
return this.findContainingLabel(parent);
}
TestRecorder.ElementInfo.prototype.getCleanCSSSelector = function(element) {
if(!element) return;
var selector = element.tagName ? element.tagName.toLowerCase() : '';
if(selector == '' || selector == 'html') return '';
var tmp_selector = '';
var accuracy = document.querySelectorAll(selector).length;
if(element.id) {
selector = "#" + element.id.replace(/\./g, '\\.');
accuracy = document.querySelectorAll(selector).length
if(accuracy==1) return selector;
}
if(element.className) {
tmp_selector = '.' + element.className.trim().replace(/ /g,".");
if(document.querySelectorAll(tmp_selector).length < accuracy) {
selector = tmp_selector;
accuracy = document.querySelectorAll(selector).length
if(accuracy==1) return selector;
}
}
var parent = element.parentNode;
var parent_selector = this.getCleanCSSSelector(parent);
if(parent_selector) {
// resolve sibling ambiguity
var matching_sibling = 0;
var matching_nodes = document.querySelectorAll(parent_selector + ' > ' + selector);
for(var i=0; i<matching_nodes.length;i++) {
if(matching_nodes[i].parentNode == parent) matching_sibling++;
}
if(matching_sibling > 1) {
var index = 1;
for (var sibling = element.previousElementSibling; sibling; sibling = sibling.previousElementSibling) index++;
selector = selector + ':nth-child(' + index + ')';
}
// remove useless intermediary parent
selector_array = parent_selector.split(' ');
if(selector_array.length>1) {
for(var i=1;i<selector_array.length;i++) {
tmp_selector = selector_array.slice(0,i).join(' ') + ' ' + selector;
if(document.querySelectorAll(tmp_selector).length == 1) {
selector = tmp_selector;
break;
}
}
}
// improve accuracy if still not correct
accuracy = document.querySelectorAll(selector).length
if(accuracy>1) {
tmp_selector = parent_selector + " " + selector;
if(document.querySelectorAll(tmp_selector).length==1) {
selector = tmp_selector;
} else {
selector = parent_selector + " > " + selector;
}
}
}
return selector;
}
TestRecorder.DocumentEvent = function(type, target) {
this.type = type;
this.url = target.URL;
this.title = target.title;
}
TestRecorder.ElementEvent = function(type, target, text) {
this.type = type;
this.info = new TestRecorder.ElementInfo(target);
this.text = text ? text : recorder.strip(contextmenu.innertext(target));
}
TestRecorder.CommentEvent = function(text) {
this.type = TestRecorder.EventTypes.Comment;
this.text = text;
}
TestRecorder.KeyEvent = function(target, text) {
this.type = TestRecorder.EventTypes.KeyPress;
this.info = new TestRecorder.ElementInfo(target);
this.text = text;
}
TestRecorder.MouseEvent = function(type, target, x, y) {
this.type = type;
this.info = new TestRecorder.ElementInfo(target);
this.x = x;
this.y = y;
this.text = recorder.strip(contextmenu.innertext(target));
}
TestRecorder.ScreenShotEvent = function() {
this.type = TestRecorder.EventTypes.ScreenShot;
}
TestRecorder.ElementScreenShotEvent = function() {
this.type = TestRecorder.EventTypes.ElementScreenShot;
}
TestRecorder.InspectElement = function() {
this.type = TestRecorder.EventTypes.InspectElement;
}
TestRecorder.NoteDownElement = function() {
this.type = TestRecorder.EventTypes.NoteDownElement;
}
TestRecorder.MoveCursorToElementEvent = function() {
this.type = TestRecorder.EventTypes.MoveCursorToElement;
}
TestRecorder.FetchElementTextEvent = function() {
this.type = TestRecorder.EventTypes.FetchElementText;
}
TestRecorder.SelectElementOptionEvent = function() {
this.type = TestRecorder.EventTypes.SelectElementOption;
}
TestRecorder.PrintElementTextEvent = function() {
this.type = TestRecorder.EventTypes.PrintElementText;
}
TestRecorder.SaveElementTextEvent = function() {
this.type = TestRecorder.EventTypes.SaveElementText;
}
TestRecorder.ExplicitWaitEvent = function() {
this.type = TestRecorder.EventTypes.ExplicitWait;
}
TestRecorder.CancelLastStepEvent = function() {
this.type = TestRecorder.EventTypes.CancelLastStep;
}
TestRecorder.CancelEvent = function() {
this.type = TestRecorder.EventTypes.Cancel;
}
TestRecorder.OpenURLEvent = function(url) {
this.type = TestRecorder.EventTypes.OpenUrl;
this.url = url;
this.width = window.innerWidth;
this.height = window.innerHeight;
}
TestRecorder.PageLoadEvent = function(url) {
this.type = TestRecorder.EventTypes.OpenUrl;
this.url = url;
// this.viaBack = back
}
//---------------------------------------------------------------------------
//ContextMenu -- this class is responsible for managing the right-click
//context menu that shows appropriate checks for targeted elements.
//All methods and attributes are private to this implementation.
//---------------------------------------------------------------------------
TestRecorder.ContextMenu = function() {
this.selected = null;
this.target = null;
this.window = null;
this.visible = false;
this.over = false;
this.menu = null;
}
contextmenu = new TestRecorder.ContextMenu();
TestRecorder.ContextMenu.prototype.build = function(t, x, y) {
var d = recorder.window.document;
var b = d.getElementsByTagName("body").item(0);
var menu = d.createElement("div");
// Needed to deal with various cross-browser insanities...
menu.setAttribute("style", "backgroundColor:#ffffff;color:#000000;border:1px solid #000000;padding:2px;position:absolute;display:none;top:" + y + "px;left:" + x + "px;border:1px;z-index:10000;");
menu.style.backgroundColor="#ffffff";
menu.style.color="#000000";
menu.style.border = "1px solid #000000";
menu.style.padding="2px";
menu.style.position = "absolute";
menu.style.display = "none";
menu.style.zIndex = "10000";
menu.style.top = y.toString();
menu.style.left = x.toString();
menu.onmouseover=contextmenu.onmouseover;
menu.onmouseout=contextmenu.onmouseout;
var selected = TestRecorder.Browser.getSelection(recorder.window).toString();
// if (t.width && t.height) {
// menu.appendChild(this.item("Check Image Src", this.checkImgSrc));
// }
//
// else if (t.type == "text" || t.type == "textarea") {
// menu.appendChild(this.item("Check Text Value", this.checkValue));
// menu.appendChild(this.item("Check Enabled", this.checkEnabled));
// menu.appendChild(this.item("Check Disabled", this.checkDisabled));
// }
//
// else if (selected && (selected != "")) {
// this.selected = recorder.strip(selected);
// menu.appendChild(this.item("Check Text Appears On Page",
// this.checkTextPresent));
// }
//
// else if (t.href) {
// menu.appendChild(this.item("Check Link Text", this.checkText));
// menu.appendChild(this.item("Check Link Href", this.checkHref));
// }
//
// else if (t.selectedIndex || t.type == "option") {
// var name = "Check Selected Value";
// if (t.type != "select-one") {
// name = name + "s";
// }
// menu.appendChild(this.item(name, this.checkSelectValue));
// menu.appendChild(this.item("Check Select Options",
// this.checkSelectOptions));
// menu.appendChild(this.item("Check Enabled", this.checkEnabled));
// menu.appendChild(this.item("Check Disabled", this.checkDisabled));
// }
//
// else if (t.type == "button" || t.type == "submit") {
// menu.appendChild(this.item("Check Button Text", this.checkText));
// menu.appendChild(this.item("Check Button Value", this.checkValue));
// menu.appendChild(this.item("Check Enabled", this.checkEnabled));
// menu.appendChild(this.item("Check Disabled", this.checkDisabled));
// }
//
// else if (t.value) {
// menu.appendChild(this.item("Check Value", this.checkValue));
// menu.appendChild(this.item("Check Enabled", this.checkEnabled));
// menu.appendChild(this.item("Check Disabled", this.checkDisabled));
// }
//
// else {
// menu.appendChild(this.item("Check Page Location", this.checkPageLocation));
// menu.appendChild(this.item("Check Page Title", this.checkPageTitle));
// menu.appendChild(this.item("Screenshot", this.doScreenShot));
// }
menu.appendChild(this.item("Inspect element", this.doInspectElement));
menu.appendChild(this.item("Note down element", this.doNoteDownElement));
menu.appendChild(this.item("Read element text", this.doFetchElementText));
menu.appendChild(this.item("Show element text", this.doPrintElementText));
menu.appendChild(this.item("Save element text", this.doSaveElementText));
menu.appendChild(this.item("Save webpage screenshot", this.doScreenShot));
menu.appendChild(this.item("Save element screenshot", this.doElementScreenShot));
menu.appendChild(this.item("Hover cursor at element", this.doMoveCursorToElement));
menu.appendChild(this.item("Wait for a few seconds", this.doExplicitWait));
menu.appendChild(this.item("Close shortcuts menu", this.cancel));
// menu.appendChild(this.item("Cancel the last step", this.doCancelLastStep));
b.insertBefore(menu, b.firstChild);
return menu;
}
TestRecorder.ContextMenu.prototype.item = function(text, func) {
var doc = recorder.window.document;
var div = doc.createElement("div");
var txt = doc.createTextNode(text);
div.setAttribute("style", "padding:6px;border:1px solid #ffffff;");
div.style.border = "1px solid #ffffff";
div.style.padding = "6px";
div.appendChild(txt);
div.onmouseover = this.onitemmouseover;
div.onmouseout = this.onitemmouseout;
div.onclick = func;
return div;
}
TestRecorder.ContextMenu.prototype.show = function(e) {
if (this.menu) {
this.hide();
}
var wnd = recorder.window;
var doc = wnd.document;
this.target = e.target();
TestRecorder.Browser.captureEvent(wnd, "mousedown", this.onmousedown);
var wh = TestRecorder.Browser.windowHeight(wnd);
var ww = TestRecorder.Browser.windowWidth(wnd);
var x = e.posX();
var y = e.posY();
if ((ww >= 0) && ((ww - x) < 100)) {
x = x - 100;
}
if ((wh >= 0) && ((wh - y) < 100)) {
y = y - 100;
}
var menu = this.build(e.target(), x, y);
this.menu = menu;
menu.style.display = "";
this.visible = true;
return;
}
TestRecorder.ContextMenu.prototype.hide = function() {
var wnd = recorder.window;
TestRecorder.Browser.releaseEvent(wnd, "mousedown", this.onmousedown);
var d = wnd.document;
var b = d.getElementsByTagName("body").item(0);
this.menu.style.display = "none" ;
b.removeChild(this.menu);
this.target = null;
this.visible = false;
this.over = false;
this.menu = null;
}
TestRecorder.ContextMenu.prototype.onitemmouseover = function(e) {
this.style.backgroundColor = "#efefef";
this.style.border = "1px solid #c0c0c0";
return true;
}
TestRecorder.ContextMenu.prototype.onitemmouseout = function(e) {
this.style.backgroundColor = "#ffffff";
this.style.border = "1px solid #ffffff";
return true;
}
TestRecorder.ContextMenu.prototype.onmouseover = function(e) {
contextmenu.over = true;
}
TestRecorder.ContextMenu.prototype.onmouseout = function(e) {
contextmenu.over = false;
}
TestRecorder.ContextMenu.prototype.onmousedown = function(e) {
if(contextmenu.visible) {
if (contextmenu.over == false) {
contextmenu.hide();
return true;
}
return true ;
}
return false;
}
TestRecorder.ContextMenu.prototype.record = function(o) {
recorder.testcase.append(o);
recorder.log(o.type);
contextmenu.hide();
}
TestRecorder.ContextMenu.prototype.checkPageTitle = function() {
var doc = recorder.window.document;
var et = TestRecorder.EventTypes;
var e = new TestRecorder.DocumentEvent(et.CheckPageTitle, doc);
contextmenu.record(e);
}
TestRecorder.ContextMenu.prototype.doScreenShot = function() {
var e = new TestRecorder.ScreenShotEvent();
contextmenu.record(e);
}
TestRecorder.ContextMenu.prototype.doElementScreenShot = function() {
var t = contextmenu.target;
var et = TestRecorder.EventTypes;
var e = new TestRecorder.ElementEvent(et.ElementScreenShot, t);
contextmenu.record(e);
}
TestRecorder.ContextMenu.prototype.doInspectElement = function() {
var t = contextmenu.target;
var et = TestRecorder.EventTypes;
var e = new TestRecorder.ElementEvent(et.InspectElement, t);
contextmenu.record(e);
// pop-up to show identifier of element inspected
var inspected_selector = e.info.selector;
if (inspected_selector.charAt(0) == '#') {inspected_selector = inspected_selector.substring(1);}
alert("The identifier for this element is ▹\n\n" + inspected_selector);
}
TestRecorder.ContextMenu.prototype.doNoteDownElement = function() {
var t = contextmenu.target;
var et = TestRecorder.EventTypes;
var e = new TestRecorder.ElementEvent(et.NoteDownElement, t);
contextmenu.record(e);
}
TestRecorder.ContextMenu.prototype.doMoveCursorToElement = function() {
var t = contextmenu.target;
var et = TestRecorder.EventTypes;
var e = new TestRecorder.ElementEvent(et.MoveCursorToElement, t);
contextmenu.record(e);
}
TestRecorder.ContextMenu.prototype.doFetchElementText = function() {
var t = contextmenu.target;
var et = TestRecorder.EventTypes;
var e = new TestRecorder.ElementEvent(et.FetchElementText, t);
contextmenu.record(e);
}
TestRecorder.ContextMenu.prototype.doSelectElementOption = function() {
var t = contextmenu.target;
var et = TestRecorder.EventTypes;
var e = new TestRecorder.ElementEvent(et.SelectElementOption, t);
contextmenu.record(e);
}
TestRecorder.ContextMenu.prototype.doPrintElementText = function() {
var t = contextmenu.target;
var et = TestRecorder.EventTypes;
var e = new TestRecorder.ElementEvent(et.PrintElementText, t);
contextmenu.record(e);
}
TestRecorder.ContextMenu.prototype.doSaveElementText = function() {
var t = contextmenu.target;
var et = TestRecorder.EventTypes;
var e = new TestRecorder.ElementEvent(et.SaveElementText, t);
contextmenu.record(e);
}
TestRecorder.ContextMenu.prototype.doExplicitWait = function() {
var e = new TestRecorder.ExplicitWaitEvent();
contextmenu.record(e);
}
TestRecorder.ContextMenu.prototype.doCancelLastStep = function() {
var e = new TestRecorder.CancelLastStepEvent();
contextmenu.record(e);
}
TestRecorder.ContextMenu.prototype.cancel = function() {
contextmenu.hide();
var e = new TestRecorder.CancelEvent();
contextmenu.record(e);
}
TestRecorder.ContextMenu.prototype.checkPageLocation = function() {
var doc = recorder.window.document;
var et = TestRecorder.EventTypes;
var e = new TestRecorder.DocumentEvent(et.CheckPageLocation, doc);
contextmenu.record(e);
}
TestRecorder.ContextMenu.prototype.checkValue = function() {
var t = contextmenu.target;
var et = TestRecorder.EventTypes;
var e = new TestRecorder.ElementEvent(et.CheckValue, t);
contextmenu.record(e);
}
TestRecorder.ContextMenu.prototype.checkValueContains = function() {
var s = contextmenu.selected;
var t = contextmenu.target;
var et = TestRecorder.EventTypes;
var e = new TestRecorder.ElementEvent(et.CheckValueContains, t, s);
contextmenu.selected = null;
contextmenu.record(e);
}
TestRecorder.ContextMenu.prototype.innertext = function(e) {
var doc = recorder.window.document;
if (document.createRange) {
var r = recorder.window.document.createRange();
r.selectNodeContents(e);
return r.toString();
} else {
return e.innerText;
}
}
TestRecorder.ContextMenu.prototype.checkText = function() {
var t = contextmenu.target;
var s = "";
if (t.type == "button" || t.type == "submit") {
s = t.value;
}
else {
s = contextmenu.innertext(t);
}
s = recorder.strip(s);
var et = TestRecorder.EventTypes;
var e = new TestRecorder.ElementEvent(et.CheckText, t, s);
contextmenu.record(e);
}
TestRecorder.ContextMenu.prototype.checkTextPresent = function() {
var t = contextmenu.target;
var s = contextmenu.selected;
var et = TestRecorder.EventTypes;
var e = new TestRecorder.ElementEvent(et.CheckTextPresent, t, s);
contextmenu.selected = null;
contextmenu.record(e);
}
TestRecorder.ContextMenu.prototype.checkHref = function() {
var t = contextmenu.target;
var et = TestRecorder.EventTypes;
var e = new TestRecorder.ElementEvent(et.CheckHref, t);
contextmenu.record(e);
}
TestRecorder.ContextMenu.prototype.checkEnabled = function() {
var t = contextmenu.target;
var et = TestRecorder.EventTypes;
var e = new TestRecorder.ElementEvent(et.CheckEnabled, t);
contextmenu.record(e);
}
TestRecorder.ContextMenu.prototype.checkDisabled = function() {
var t = contextmenu.target;
var et = TestRecorder.EventTypes;
var e = new TestRecorder.ElementEvent(et.CheckDisabled, t);
contextmenu.record(e);
}
TestRecorder.ContextMenu.prototype.checkSelectValue = function() {
var t = contextmenu.target;
var et = TestRecorder.EventTypes;
var e = new TestRecorder.ElementEvent(et.CheckSelectValue, t);
contextmenu.record(e);
}
TestRecorder.ContextMenu.prototype.checkSelectOptions = function() {
var t = contextmenu.target;
var et = TestRecorder.EventTypes;
var e = new TestRecorder.ElementEvent(et.CheckSelectOptions, t);
contextmenu.record(e);
}
TestRecorder.ContextMenu.prototype.checkImgSrc = function() {
var t = contextmenu.target;
var et = TestRecorder.EventTypes;
var e = new TestRecorder.ElementEvent(et.CheckImageSrc, t);
contextmenu.record(e);
}
//---------------------------------------------------------------------------
//Recorder -- a controller class that manages the recording of web browser
//activities to produce a test case.
//Instance Methods:
//start() -- start recording browser events.
//stop() -- stop recording browser events.
//reset() -- reset the recorder and initialize a new test case.
//---------------------------------------------------------------------------
TestRecorder.Recorder = function() {
this.testcase = new TestRecorder.TestCase();
this.logfunc = null;
this.window = null;
this.active = false;
}
//The recorder is a singleton -- there is no real reason to have more than
//one instance, and many of its methods are event handlers which need a
//stable reference to the instance.
recorder = new TestRecorder.Recorder();
recorder.logfunc = function(msg) {console.log(msg);};
TestRecorder.Recorder.prototype.start = function() {
this.window = window;
this.captureEvents();
// OVERRIDE stopPropagation
var actualCode = '(' + function() {
var overloadStopPropagation = Event.prototype.stopPropagation;
Event.prototype.stopPropagation = function(){
console.log(this);
overloadStopPropagation.apply(this, arguments);
};
} + ')();';
var script = document.createElement('script');
script.textContent = actualCode;
(document.head||document.documentElement).appendChild(script);
script.parentNode.removeChild(script);
this.active = true;
this.log("recorder started");
}
TestRecorder.Recorder.prototype.stop = function() {
this.releaseEvents();
this.active = false;
this.log("recorder stopped");
return;
}
TestRecorder.Recorder.prototype.open = function(url) {
var e = new TestRecorder.OpenURLEvent(url);
this.testcase.append(e);
this.log("open url: " + url);
}
TestRecorder.Recorder.prototype.pageLoad = function() {
var doc = recorder.window.document;
var et = TestRecorder.EventTypes;
var e = new TestRecorder.DocumentEvent(et.PageLoad, doc);
this.testcase.append(e);
this.log("page loaded url: " + e.url);
}
TestRecorder.Recorder.prototype.captureEvents = function() {
var wnd = this.window;
TestRecorder.Browser.captureEvent(wnd, "contextmenu", this.oncontextmenu);
TestRecorder.Browser.captureEvent(wnd, "drag", this.ondrag);
TestRecorder.Browser.captureEvent(wnd, "mousedown", this.onmousedown);
TestRecorder.Browser.captureEvent(wnd, "mouseup", this.onmouseup);
TestRecorder.Browser.captureEvent(wnd, "click", this.onclick);
TestRecorder.Browser.captureEvent(wnd, "change", this.onchange);
TestRecorder.Browser.captureEvent(wnd, "keypress", this.onkeypress);
TestRecorder.Browser.captureEvent(wnd, "select", this.onselect);
TestRecorder.Browser.captureEvent(wnd, "submit", this.onsubmit);
}
TestRecorder.Recorder.prototype.releaseEvents = function() {
var wnd = this.window;
TestRecorder.Browser.releaseEvent(wnd, "contextmenu", this.oncontextmenu);
TestRecorder.Browser.releaseEvent(wnd, "drag", this.ondrag);
TestRecorder.Browser.releaseEvent(wnd, "mousedown", this.onmousedown);
TestRecorder.Browser.releaseEvent(wnd, "mouseup", this.onmouseup);
TestRecorder.Browser.releaseEvent(wnd, "click", this.onclick);
TestRecorder.Browser.releaseEvent(wnd, "change", this.onchange);
TestRecorder.Browser.releaseEvent(wnd, "keypress", this.onkeypress);
TestRecorder.Browser.releaseEvent(wnd, "select", this.onselect);
TestRecorder.Browser.releaseEvent(wnd, "submit", this.onsubmit);
}
TestRecorder.Recorder.prototype.clickaction = function(e) {
// This method is called by our low-level event handler when the mouse
// is clicked in normal mode. Its job is decide whether the click is
// something we care about. If so, we record the event in the test case.
//
// If the context menu is visible, then the click is either over the
// menu (selecting a check) or out of the menu (cancelling it) so we
// always discard clicks that happen when the menu is visible.
if (!contextmenu.visible) {
var et = TestRecorder.EventTypes;
var t = e.target();
if (t.href || (t.type && t.type == "submit"))
{
this.testcase.append(new TestRecorder.ElementEvent(et.Click,e.target()));
}
// else if (t.selectedIndex || t.type == "option")
// {
// this.testcase.append(new TestRecorder.ElementEvent(et.SelectElementOption,e.target()));
// }
else {
recorder.testcase.append(
new TestRecorder.MouseEvent(
TestRecorder.EventTypes.Click, e.target(), e.posX(), e.posY()
));
}
}
}
TestRecorder.Recorder.prototype.addComment = function(text) {
this.testcase.append(new TestRecorder.CommentEvent(text));
}
TestRecorder.Recorder.prototype.check = function(e) {
// This method is called by our low-level event handler when the mouse
// is clicked in check mode. Its job is decide whether the click is
// something we care about. If so, we record the check in the test case.
contextmenu.show(e);
var target = e.target();
if (target.type) {
var type = target.type.toLowerCase();
if (type == "submit" || type == "button" || type == "image") {
recorder.log('check button == "' + target.value + '"');
}
}
else if (target.href) {
if (target.innerText) {
var text = recorder.strip(target.innerText);
recorder.log('check link == "' + target.text + '"');
}
}
}
TestRecorder.Recorder.prototype.onpageload = function() {
if (this.active) {
// This must be called each time a new document is fully loaded into the
// testing target frame to ensure that events are captured for the page.
recorder.captureEvents();
// if a new page has loaded, but there doesn't seem to be a reason why,
// then we need to record the fact or the information will be lost
if (this.testcase.peek()) {
var last_event_type = this.testcase.peek().type;
if (last_event_type != TestRecorder.EventTypes.OpenUrl &&
last_event_type != TestRecorder.EventTypes.Click &&
last_event_type != TestRecorder.EventTypes.Submit) {
this.open(this.window.location.toString());
}
}
// record the fact that a page load happened
if (this.window)
this.pageLoad();
}
}
TestRecorder.Recorder.prototype.onchange = function(e) {
var e = new TestRecorder.Event(e);
var et = TestRecorder.EventTypes;
var t = e.target();
if (t.selectedIndex || t.type == "option")
{
var v = new TestRecorder.ElementEvent(et.SelectElementOption, e.target());
recorder.testcase.append(v);
recorder.log("value changed: " + e.target().value);
}
else
{
var v = new TestRecorder.ElementEvent(et.Change, e.target());
recorder.testcase.append(v);
recorder.log("value changed: " + e.target().value);
}
}
TestRecorder.Recorder.prototype.onselect = function(e) {
var e = new TestRecorder.Event(e);
recorder.log("select: " + e.target());
// var e = new TestRecorder.Event(e);
// var et = TestRecorder.EventTypes;
// var v = new TestRecorder.ElementEvent(et.SelectElementOption, e.target());
// recorder.testcase.append(v);
// recorder.log("select: " + e.target().value);
}
TestRecorder.Recorder.prototype.onsubmit = function(e) {
var e = new TestRecorder.Event(e);
var et = TestRecorder.EventTypes;
// We want to save the form element as the event target
var t = e.target();
while (t.parentNode && t.tagName != "FORM") {
t = t.parentNode;
}
var v = new TestRecorder.ElementEvent(et.Submit, t);
recorder.testcase.append(v);
recorder.log("submit: " + e.target());
}
TestRecorder.Recorder.prototype.ondrag = function(e) {
var e = new TestRecorder.Event(e);
recorder.testcase.append(
new TestRecorder.MouseEvent(
TestRecorder.EventTypes.MouseDrag, e.target(), e.posX(), e.posY()
));
}
TestRecorder.Recorder.prototype.onmousedown = function(e) {
if(!contextmenu.visible) {
var e = new TestRecorder.Event(e);
if (e.button() == TestRecorder.Event.LeftButton) {
recorder.testcase.append(
new TestRecorder.MouseEvent(
TestRecorder.EventTypes.MouseDown, e.target(), e.posX(), e.posY()
));
}
}
}
TestRecorder.Recorder.prototype.onmouseup = function(e) {
if(!contextmenu.visible) {
var e = new TestRecorder.Event(e);
if (e.button() == TestRecorder.Event.LeftButton) {
recorder.testcase.append(
new TestRecorder.MouseEvent(
TestRecorder.EventTypes.MouseUp, e.target(), e.posX(), e.posY()
));
}
}
}
//The dance here between onclick and oncontextmenu requires a bit of
//explanation. IE and Moz/Firefox have wildly different behaviors when
//a right-click occurs. IE6 fires only an oncontextmenu event; Firefox
//gets an onclick event first followed by an oncontextment event. So
//to do the right thing here, we need to silently consume oncontextmenu
//on Firefox, and reroute oncontextmenu to look like a click event for
//IE. In both cases, we need to prevent the default action for cmenu.
TestRecorder.Recorder.prototype.onclick = function(e) {
var e = new TestRecorder.Event(e);
if (e.shiftkey()) {
recorder.check(e);
e.stopPropagation();
e.preventDefault();
return false;
}
if (e.button() == TestRecorder.Event.RightButton) {
recorder.check(e);
return true;
} else if (e.button() == TestRecorder.Event.LeftButton) {
recorder.clickaction(e);
return true;
}
e.stopPropagation();
e.preventDefault();
return false;
}
TestRecorder.Recorder.prototype.oncontextmenu = function(e) {
var e = new TestRecorder.Event(e);
recorder.check(e);
e.stopPropagation();
e.preventDefault();
return false;
}
TestRecorder.Recorder.prototype.onkeypress = function(e) {
var e = new TestRecorder.Event(e);
// if (e.shiftkey() && (e.keychar() == 'C')) {
// // TODO show comment box here
// }
// if (e.shiftkey() && (e.keychar() == 'S')) {
// recorder.testcase.append(new TestRecorder.ScreenShotEvent());
// e.stopPropagation();
// e.preventDefault();
// return false;
// }
var last = recorder.testcase.peek();
if(last.type == TestRecorder.EventTypes.KeyPress) {
last.text = last.text + e.keychar();
recorder.testcase.poke(last);
} else {
recorder.testcase.append(
new TestRecorder.KeyEvent(e.target(), e.keychar())
);
}
return true;
}
TestRecorder.Recorder.prototype.strip = function(s) {
// return s.replace('\n', ' ').replace(/^\s*/, "").replace(/\s*$/, "");
// change code to replace with [whitespace] as it will break element text match if simply strip
return s.replace(/^\s+/, "[whitespace]").replace(/\s+$/, "[whitespace]");
}
TestRecorder.Recorder.prototype.log = function(text) {
if (this.logfunc) {
this.logfunc(text);
}
}
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
if (request.action == "start") {
recorder.start();
sendResponse({});
}
if (request.action == "stop") {
recorder.stop();
sendResponse({});
}
if (request.action == "open") {
recorder.open(request.url);
sendResponse({});
}
if (request.action == "addComment") {
recorder.addComment(request.text);
sendResponse({});
}
});
//get current status from background
chrome.runtime.sendMessage({action: "get_status"}, function(response) {
if (response.active) {
recorder.start();
}
});