UNPKG

siesta-lite

Version:

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

861 lines (641 loc) 34.1 kB
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>The source code</title> <link href="../resources/prettify/prettify.css" type="text/css" rel="stylesheet" /> <script type="text/javascript" src="../resources/prettify/prettify.js"></script> <style type="text/css"> .highlight { display: block; background-color: #ddd; } </style> <script type="text/javascript"> function highlight() { document.getElementById(location.hash.replace(/#/, "")).className = "highlight"; } </script> </head> <body onload="prettyPrint(); highlight();"> <pre class="prettyprint lang-js">/* Siesta 5.6.1 Copyright(c) 2009-2022 Bryntum AB https://bryntum.com/contact https://bryntum.com/products/siesta/license */ <span id='Siesta-Recorder-Recorder'>/** </span>@class Siesta.Recorder.Recorder @mixin Siesta.Recorder.Role.CanRecordScroll @mixin Siesta.Recorder.Role.CanRecordWindowResize @mixin Siesta.Recorder.Role.CanRecordPointOfInterest @mixin Siesta.Recorder.Role.CanRecordMouseMovePath @mixin Siesta.Recorder.Role.CanRecordMouseMoveOnIdle This class implements a recorder for user actions. It records the events of the window it&#39;s attached to. It has a number of options, defining what should be recorder. Since it&#39;s JS based, we cannot record native dialog interactions, such as alert, print or confirm etc. */ Class(&#39;Siesta.Recorder.Recorder&#39;, { does : [ // &quot;utility&quot; roles JooseX.Observable, Siesta.Util.Role.Dom, Siesta.Util.Role.CanParseOs, Siesta.Util.Role.CanGetType, Siesta.Recorder.Role.CanSwallowException, // &quot;feature&quot; roles Siesta.Recorder.Role.CanRecordMouseDownUp, Siesta.Recorder.Role.CanRecordMouseMove, Siesta.Recorder.Role.CanRecordMouseMoveOnIdle, Siesta.Recorder.Role.CanRecordPointOfInterest, Siesta.Recorder.Role.CanRecordWindowResize, Siesta.Recorder.Role.CanRecordScroll, Siesta.Recorder.Role.CanRecordWheel, // this has to go last (override will be executed first), // to be able to disable the previous roles Siesta.Recorder.Role.CanRecordMouseMovePath ], has : { active : null, extractor : null, extractorClass : Siesta.Recorder.TargetExtractor, extractorConfig : null, <span id='Siesta-Recorder-Recorder-cfg-uniqueComponentProperty'> /** </span> * @cfg {Array[String]/String} uniqueComponentProperty A string or an array of strings, containing attribute names * that the Recorder will use to identify Ext JS components. */ uniqueComponentProperty : null, <span id='Siesta-Recorder-Recorder-cfg-uniqueDomNodeProperty'> /** </span> * @cfg {String} uniqueDomNodeProperty A property that will be used to uniquely identify DOM nodes. By default the `id` * property is used. */ uniqueDomNodeProperty : &#39;id&#39;, <span id='Siesta-Recorder-Recorder-cfg-shouldIgnoreDomElementId'> /** </span> * @cfg {Function} shouldIgnoreDomElementId If provided, this function will be called to determine if DOM element&#39;s * id can be used as part of the queries, created by recorder. Its quite common for various frameworks, to assign * auto-generated ids to DOM elements. Such ids usually changes very often and should not be used in the queries. * * The function should return `true` if element&#39;s id should not be used and will be called with the following arguments: * * @cfg {String} shouldIgnoreDomElementId.id The id of the DOM element to check * @cfg {HTMLElement} shouldIgnoreDomElementId.el The DOM element itself */ shouldIgnoreDomElementId : null, <span id='Siesta-Recorder-Recorder-cfg-ignoreCssClasses'> /** </span> * @cfg {Array} ignoreCssClasses An array of CSS class strings, which should be ignored by the recorder as it builds CSS selector locators. Use * this to avoid certain CSS selectors which don&#39;t provide any useful distinction about a DOM node */ ignoreCssClasses : Joose.I.Array, // logic for offset: // we always record it, except for the &quot;click&quot; where element has not changed during the click, // and it was available at center point during &quot;mousedown&quot; - such clicks are considered &quot;simple&quot; and don&#39;t require offset // in the opposite, individually recorded &quot;mousedown&quot; or &quot;mouseup&quot; events are usually part of the drag operation // and for &quot;drag&quot; we want to be precise <span id='Siesta-Recorder-Recorder-cfg-recordOffsets'> /** </span> * @cfg {Boolean} recordOffsets Set to `true` to record the offset to each targeted DOM element for recorded * actions, to make sure the recorded action can be played back with exact precision. * Normally Siesta removes the offset of some actions, if target element is reachable at the center point. */ recordOffsets : false, // ignore events generated by Siesta (bypass in normal use, but for testing recorder we need it) ignoreSynthetic : true, // The window this recorder is observing for events window : null, // merge `mousedown+mouseup+click` to just `click` only if timespan between `mousedown` and `mouseup` // is less than this value (ms) clickMergeThreshold : 200, // console.logs all DOM events detected debugMode : false, eventsToRecord : { lazy : &#39;this.buildEventsToRecord&#39; }, // &quot;raw&quot; log of all dom events events : Joose.I.Array, // Strictly for debugging purposes processingEvent : null, actions : Joose.I.Array, actionsByEventId : Joose.I.Object, dragPixelThreshold : 3, // If mousedown/mouseup position differs by less, we consider it a click actionClass : Siesta.Recorder.Action, lastActionPosition : Joose.I.Array, warnAboutIframeMissingId : true }, methods : { buildEventsToRecord : function () { var events = [ &quot;keydown&quot;, &quot;keypress&quot;, &quot;keyup&quot;, &quot;click&quot;, &quot;dblclick&quot;, &quot;contextmenu&quot; ] if (window.PointerEvent) events.push( &#39;pointerdown&#39;, &#39;pointerup&#39; ) else if (window.MSPointerEvent) events.push( &#39;MSPointerDown&#39;, &#39;MSPointerUp&#39; ) else events.push( &#39;mousedown&#39;, &#39;mouseup&#39; ) return events }, initialize : function () { this.onUnload = this.onUnload.bind(this); this.onFrameLoad = this.onFrameLoad.bind(this); this.onDomEvent = this.safeBind(this.onDomEvent); var extractorConfig = this.extractorConfig || {} // used as bubble target for `exception` event extractorConfig.recorder = this extractorConfig.uniqueComponentProperty = extractorConfig.uniqueComponentProperty || this.uniqueComponentProperty; extractorConfig.uniqueDomNodeProperty = extractorConfig.uniqueDomNodeProperty || this.uniqueDomNodeProperty; extractorConfig.swallowExceptions = this.swallowExceptions; extractorConfig.shouldIgnoreDomElementId = this.shouldIgnoreDomElementId; extractorConfig.ignoreCssClasses = this.ignoreCssClasses; this.extractor = new this.extractorClass(extractorConfig); }, isSamePoint : function (event1, event2) { return Math.abs(Math.round(event1.x) - Math.round(event2.x)) &lt;= this.dragPixelThreshold &amp;&amp; Math.abs(Math.round(event1.y) - Math.round(event2.y)) &lt;= this.dragPixelThreshold; }, isSameTarget : function (event1, event2) { return event1.target == event2.target || this.contains(event1.target, event2.target) || this.contains(event2.target, event1.target); }, clear : function () { var me = this me.events = [] me.actions = [] me.actionsByEventId = {} me.fireEvent(&#39;clear&#39;, me) }, // We monitor page loads so the recorder can add a waitForPageLoad action onUnload : function () { var actions = this.actions, last = actions.length &amp;&amp; actions[ actions.length - 1 ]; if (last &amp;&amp; last.target) { last.waitForPageLoad = true; } this.stop(true); }, // After frame has loaded, stop listening to old window and restart on new frame window onFrameLoad : function (event) { // Frame could be violating same-origin policy at this point try { var win = event.target.contentWindow; // will throw if different origin var a = win.location.href; } catch (e) { win = null; } if (win) { this.attach(win); this.start(); } }, /* * Attaches the recorder to a Window object * @param {Window} window The window to attach to. **/ attach : function (window) { if (this.window !== window) { this.stop() } // clear only events, keep the actions this.events = [] this.window = window; }, /* * Starts recording events of the current Window object **/ start : function () { if (this.active) return if (!this.ignoreSynthetic &amp;&amp; !this.hasOwnProperty((&#39;clickMergeThreshold&#39;))) this.clickMergeThreshold = Infinity this.stop(); this.active = Date.now(); this.onStart(); this.fireEvent(&#39;start&#39;, this); }, /* * Stops the recording of events **/ stop : function (keepOnLoadListenerOnIframe) { if (this.active) { this.active = null; this.onStop(keepOnLoadListenerOnIframe); this.fireEvent(&#39;stop&#39;, this); } }, getRecordedEvents : function () { return this.events; }, getRecordedActions : function () { return this.actions }, getRecordedActionsAsSteps : function () { return Joose.A.map(this.actions, function (action) { return action.asStep() }) }, // main listener onDomEvent : function (e) { var target = e.target this.processingEvent = e; if (this.debugMode &amp;&amp; this.window.console &amp;&amp; typeof this.window.console.log === &#39;function&#39;) { console.log(&#39;[EVENT] : &#39;, e.type, target, e.keyIdentifier || e.key); } // Never trust IE - target may be absent // Ignore events from played back test (if user plays test and records before it&#39;s stopped) if (!target || (this.ignoreSynthetic &amp;&amp; (e.synthetic || !e.isTrusted))) return; var event = Siesta.Recorder.Event.fromDomEvent(e) this.convertToAction(event) this.events.push(event) // do not store more than 10 events if (this.events.length &gt; 10) this.events.shift() this.fireEvent(&#39;domevent&#39;, event) }, eventToAction : function (event, onlyXY) { var type = event.type var options = event.options; var actionName if (type === &#39;wheel&#39;) { var rawEvent = event.rawEvent; actionName = &#39;wheel&#39;; options = Joose.O.extend({ deltaX : rawEvent.deltaX || 0, deltaY : rawEvent.deltaY || 0, deltaZ : rawEvent.deltaZ || 0, deltaMode : rawEvent.deltaMode || 0}, options) } else if (type.match(/^key/)) // convert all key events to type for now actionName = &#39;type&#39; else if (this.isPointerDownEvent(event)) actionName = &#39;mousedown&#39; else if (this.isPointerUpEvent(event)) actionName = &#39;mouseup&#39; else actionName = type var config = { action : actionName, target : this.getPossibleTargets(event, type !== &#39;wheel&#39;, null, onlyXY), options : options, sourceEvent : event, sourceEventTargetReachableAtCenter : this.isElementReachableAtCenter(event.target, false) } // `window` object to which the event target belongs var win = event.target.ownerDocument.defaultView; // Case of nested iframe if (win !== this.window &amp;&amp; !onlyXY) { if (!win.frameElement.id &amp;&amp; this.warnAboutIframeMissingId) { throw new Error(&#39;To record events in a nested iframe, please set an &quot;id&quot; property on your frames&#39;); } // Prepend the frame id to each suggested target config.target = config.target.filter(function (actionTarget) { if (typeof actionTarget.target === &#39;string&#39;) { actionTarget.target = &#39;#&#39; + win.frameElement.id + &#39; -&gt; &#39; + actionTarget.target; return true; } // Skip array coordinates for nested iframes, make little sense return false; }); } return new this.actionClass(config) }, recordAsAction : function (event, omitTarget) { var action = this.eventToAction(event, omitTarget) if (action) { this.addAction(action) return action; } }, addAction : function (action) { if (!(action instanceof this.actionClass)) { action = new this.actionClass(action); } this.beforeAddAction(action); this.actions.push(action) if (action.sourceEvent) { this.actionsByEventId[ action.sourceEvent.id ] = action if (typeof action.sourceEvent.x === &#39;number&#39;) { this.lastActionPosition[0] = action.sourceEvent.rawEvent.clientX; this.lastActionPosition[1] = action.sourceEvent.rawEvent.clientY; } } if (this.debugMode &amp;&amp; this.window.console &amp;&amp; typeof this.window.console.log === &#39;function&#39;) { console.log(&#39;[ACTION] : &#39; + action.action, action.getTarget() &amp;&amp; action.asStep().target || &#39;&#39;, action); } this.fireEvent(&#39;actionadd&#39;, action) }, removeAction : function (actionToRemove) { var actions = this.actions; for (var i = 0; i &lt; actions.length; i++) { var action = actions[ i ] if (action == actionToRemove) { actions.splice(i, 1) if (action.sourceEvent) delete this.actionsByEventId[ action.sourceEvent.id ] this.fireEvent(&#39;actionremove&#39;, actionToRemove) break; } } }, removeActionByEventId : function (eventId) { this.removeAction(this.getActionByEventId(eventId)) }, removeActionByEvent : function (event) { this.removeAction(this.getActionByEventId(event.id)) }, getActionByEvent : function (event) { return this.actionsByEventId[ event.id ] }, getActionByEventId : function (eventId) { return this.actionsByEventId[ eventId ] }, getLastAction : function () { return this.actions[ this.actions.length - 1 ] }, getLastEvent : function () { return this.events[ this.events.length - 1 ] }, canCombineTypeActions : function (prevOptions, curOptions) { return prevOptions.ctrlKey == curOptions.ctrlKey &amp;&amp; prevOptions.metaKey == curOptions.metaKey &amp;&amp; prevOptions.shiftKey == curOptions.shiftKey &amp;&amp; prevOptions.altKey == curOptions.altKey; }, finalizeDragAction : function (mouseUpEvent, mouseDownEvent) { // omit the target queries finding for `mouseup` event, since we&#39;ll add coordinate target // manually anyway var action = this.eventToAction(mouseUpEvent, true); // For drag drop operations, we use a simple coordinate for mouseup always action.target = new Siesta.Recorder.Target({ targets : [{ target : [ mouseUpEvent.x, mouseUpEvent.y ], type : &#39;xy&#39; }] }) this.addAction(action) }, // Method which tries to identify &quot;composite&quot; DOM interactions such as &#39;click/contextmenu&#39; (3 events), double click // but also complex scenarios such as &#39;drag&#39; convertToAction : function (event) { var type = event.type var events = this.getRecordedEvents(), length = events.length, tail = this.getLastEvent(); var tailPrev = length &gt;= 2 ? events[ length - 2 ] : null; if (this.shouldIgnoreEvent(event, tail, tailPrev)) { return; } if (type === &#39;wheel&#39;) { this.recordAsAction(event); return } if (type === &#39;keypress&#39; || type === &#39;keyup&#39; || type === &#39;keydown&#39;) { this.convertKeyEventToAction(event); return } // if there&#39;s no already recorded events - there&#39;s nothing to coalesce if (!length) { this.recordAsAction(event) return } if (type === &#39;dblclick&#39;) { // On Android Mobile Safari it seems &#39;dblclick&#39; can be fired without 2 preceding clicks somehow if (this.getRecordedActions().length &gt; 1) { // removing the last `click` action - one click event will still remain this.removeAction(this.getLastAction()) } this.getLastAction().action = &#39;dblclick&#39; this.fireEvent(&#39;actionupdate&#39;, this.getLastAction()) return } // // if mousedown/up happened in a row in different points - this is considered to be a drag operation if ( this.isPointerDownEvent(tail) &amp;&amp; this.isPointerUpEvent(event) &amp;&amp; event.button === tail.button &amp;&amp; !this.isSamePoint(event, tail) ) { this.finalizeDragAction(event, tail) return } if ( tail &amp;&amp; this.isPointerUpEvent(event) &amp;&amp; this.isPointerDownEvent(tail) &amp;&amp; event.button === tail.button &amp;&amp; this.isSamePoint(event, tail) // FF57 does not fire `click` when target changes in mousedown, so we need the `mouseup` target and can not // optimize // possibly will be fixed in later FF releases since it contradicts what Chrome does &amp;&amp; !bowser.gecko ) { // record `mouseup` which happened in the same point as `mousedown` // w/o target - since it will be removed by the &quot;click&quot; processing anyway // this is to save some CPU cycles (which can be up to ~200-300ms in heavy DOM) this.recordAsAction(event, true) return } var isFF57BrokenOnWindows = bowser.gecko &amp;&amp; bowser.windows // Merge events to click action if (tailPrev &amp;&amp; type === &#39;click&#39;) { if ( // Verify tail this.isPointerUpEvent(tail) &amp;&amp; event.button === tail.button &amp;&amp; event.button === tailPrev.button &amp;&amp; this.isSamePoint(event, tail) &amp;&amp; // Verify previous tail this.isPointerDownEvent(tailPrev) &amp;&amp; this.isSameTarget(event, tail) &amp;&amp; this.isSameTarget(event, tailPrev) &amp;&amp; this.isSamePoint(event, tailPrev) ) { // Convert mousedown action to a click action, and use all context recorded at mousedown since DOM may have changed // at the time &#39;click&#39; was observed var mouseDownAction = this.getActionByEvent(tailPrev) // Don&#39;t merge to &#39;click&#39; if there is a noticable delay between mousedown/mouseup timestamp // Application could have logic implemented for longpress etc if (Date.now() - mouseDownAction.getTimestamp() &gt; this.clickMergeThreshold) { } else { this.removeActionByEvent(tail) if ( event.target == tail.target &amp;&amp; tail.target == tailPrev.target &amp;&amp; !this.recordOffsets &amp;&amp; mouseDownAction.sourceEventTargetReachableAtCenter ) { // do not completely remove the offset for FF57 on Windows but save it, // since it may be needed for &quot;contextmenu&quot; action, see below mouseDownAction.clearTargetOffset(isFF57BrokenOnWindows) } mouseDownAction.action = &#39;click&#39;; this.fireEvent(&#39;actionupdate&#39;, mouseDownAction); } return; } else { // click event is fired after a drag, which should not be recorded as a separate click return; } } else if (type === &#39;contextmenu&#39;) { var lastAction = this.getLastAction() // FF 57 on Windows fires mousedown/up/click/contextmenu for right click (crazy) if (isFF57BrokenOnWindows &amp;&amp; tail.type === &#39;click&#39; &amp;&amp; lastAction.action === &#39;click&#39;) { lastAction.action = &#39;contextmenu&#39; lastAction.restoreTargetOffset() this.fireEvent(&#39;actionupdate&#39;, lastAction) return } // Verify tail (Mac OSX doesn&#39;t fire mouse up) if ( (this.isPointerUpEvent(tail) || this.isPointerDownEvent(tail)) &amp;&amp; event.button === tail.button &amp;&amp; this.isSamePoint(event, tail) ) { // If on windows, first get rid of pointerup action if (this.isPointerUpEvent(tail)) { this.removeActionByEvent(tail) } // Convert last pointerdown action to contextmenu to maintain original target var lastAction = this.getLastAction(); lastAction.action = &#39;contextmenu&#39; this.fireEvent(&#39;actionupdate&#39;, lastAction) return } // Verify previous tail if (this.isPointerUpEvent(tail) &amp;&amp; this.isPointerDownEvent(tailPrev) &amp;&amp; this.isSameTarget(event, tail) &amp;&amp; this.isSameTarget(event, tailPrev) &amp;&amp; this.isSamePoint(event, tailPrev) ) { this.removeActionByEvent(tailPrev) } } this.recordAsAction(event) }, shouldIgnoreEvent : function (event, tail, tailPrev) { // In some situations the mouseup event may remove/overwrite the current element and no click will be triggered // so we need to catch drag operation on mouseup (see above) and ignore following &quot;click&quot; event // TODO NEEDED? // if (event.type === &#39;click&#39; &amp;&amp; this.getLastAction() &amp;&amp; this.getLastAction().action === &#39;drag&#39;) { // return true // } var eventType = event.type var isKeyEvent = eventType.match(/^key/) var keyCode = event.keyCode var keys = Siesta.Test.UserAgent.KeyCodes().keys // Ignore modifier keys which are used only in combination with other keys if (isKeyEvent &amp;&amp; (keyCode === keys.SHIFT || keyCode === keys.CTRL || keyCode === keys.ALT || keyCode === keys.CMD)) return true; // On Mac ignore mouseup happening after contextmenu if (this.isPointerUpEvent(event) &amp;&amp; tail &amp;&amp; tail.type === &#39;contextmenu&#39;) { return true } // ignore keypress for CTRL+KEY if ( event.type == &#39;keypress&#39; &amp;&amp; tail &amp;&amp; tail.type == &#39;keydown&#39; &amp;&amp; tailPrev &amp;&amp; tailPrev.type == &#39;keydown&#39; &amp;&amp; event.rawEvent.key == tail.rawEvent.key &amp;&amp; tailPrev.rawEvent.key == &#39;Control&#39; &amp;&amp; (event.rawEvent.timeStamp - tail.rawEvent.timeStamp) &lt; 3 ) { return true } // Clicks on a &lt;label&gt; with produces 2 &quot;click&quot; events, just ignore the 2nd event and do not record it as an action // in FF, the 2nd &quot;click&quot; will have 0, 0 coordinates, so we have to disable `isSamePoint` extra sanity check if (event.type == &#39;click&#39; &amp;&amp; tail &amp;&amp; tail.type == &#39;click&#39; &amp;&amp; tail.target.nodeName.toLowerCase() === &#39;label&#39;) { return true } }, convertKeyEventToAction : function (event) { var type = event.type var tail = this.getLastEvent(); var KC = Siesta.Test.UserAgent.KeyCodes(); var isSpecial = type == &#39;keydown&#39; &amp;&amp; (KC.isSpecial(event.keyCode) || KC.isNav(event.keyCode)); var isModifier = KC.isModifier(event.keyCode); var options = event.options; var prevType = tail &amp;&amp; tail.type; var prevSpecial = type == &#39;keypress&#39; &amp;&amp; prevType == &#39;keydown&#39; &amp;&amp; (KC.isSpecial(tail.keyCode) || KC.isNav(tail.keyCode)); var isWindows = this.parseOS(navigator.platform) === &#39;Windows&#39;; var isMac = this.parseOS(navigator.platform) === &#39;MacOS&#39;; // On Windows and Linux, no keypress is triggered if CTRL key is pressed along with a regular char (e.g Ctrl-C). // On Mac, no keypress is triggered if CMD key is pressed along with a regular char (e.g Cmd-C). if (type === &#39;keypress&#39; &amp;&amp; !isSpecial &amp;&amp; !prevSpecial || (type === &#39;keydown&#39; &amp;&amp; (isSpecial || isModifier || (!isMac &amp;&amp; options.ctrlKey) || (isMac &amp;&amp; options.metaKey)))) { var lastAction = this.getLastAction() var text = event.rawEvent.key ? event.rawEvent.key : (isSpecial ? KC.fromCharCode(event.charCode, true) : String.fromCharCode(event.charCode)); text = isSpecial ? &#39;[&#39; + text.toUpperCase() + &#39;]&#39; : text; // Crude check to make sure we don&#39;t merge a CTRL-C with the next &quot;normal&quot; keystroke if (lastAction &amp;&amp; lastAction.action === &#39;type&#39; &amp;&amp; this.canCombineTypeActions(lastAction.options, event.options)) { if (!KC.isModifier(event.keyCode)) { lastAction.value += text this.fireEvent(&#39;actionupdate&#39;, lastAction) } } else { this.addAction({ action : &#39;type&#39;, target : this.getPossibleTargets(event), value : text, sourceEvent : event, options : event.options }) } return } // ignore &#39;keydown&#39; events }, onStart : function () { var me = this, window = me.window, doc = window.document, body = doc.body, resizeTimeout = null, frameWindows = this.getNestedFrames().map(function(frame) { return frame.contentWindow; }); // Listen to test window and any frames nested in it [ window ].concat(frameWindows).forEach(function (win) { me.registerWindowListeners(win); }) window.frameElement &amp;&amp; window.frameElement.addEventListener(&#39;load&#39;, this.onFrameLoad); window.addEventListener(&#39;unload&#39;, this.onUnload); // To make sure we can record key events immediately window.focus(); }, onStop : function (keepOnLoadListenerOnIframe) { var me = this, window = me.window, doc = window.document, body = doc.body, frameWindows = this.getNestedFrames().map(function (frame) { return frame.contentWindow; }); // Unlisten to test window and any frames nested in it [ window ].concat(frameWindows).forEach(function (win) { me.deregisterWindowListeners(win); }) // if recorder is attached to a iframed window, in the &quot;unload&quot; event, we want to cleanup the listeners // but keep the &#39;load&#39; listener for the &lt;iframe&gt; element itself, to re-attach recorder to newly loaded page if (!keepOnLoadListenerOnIframe) { window.frameElement &amp;&amp; window.frameElement.removeEventListener(&#39;load&#39;, me.onFrameLoad); } window.removeEventListener(&#39;unload&#39;, this.onUnload); }, registerWindowListeners : function (win) { if (this.isCrossOriginWindow(win)) return var me = this; me.getEventsToRecord().forEach(function (name) { win.document.addEventListener(name, me.onDomEvent, true); }); }, deregisterWindowListeners : function (win) { if (this.isCrossOriginWindow(win)) return var me = this; me.getEventsToRecord().forEach(function (name) { win.document.removeEventListener(name, me.onDomEvent, true); }); }, // Returns only frames on the same domain getNestedFrames : function() { var me = this return Array.prototype.slice.apply(this.window.document.getElementsByTagName(&#39;iframe&#39;)).filter(function(frame) { return !me.isCrossOriginWindow(frame.contentWindow) }); }, // Hook called before adding actions to inject &#39;helping&#39; actions beforeAddAction : function (action) { }, getPossibleTargets : function (event, recordOffsets, targetOverride, onlyXY) { if (this.typeOf(event.target) == &#39;HTMLDocument&#39;) event.target = event.target.body return this.extractor.getTargets(event, recordOffsets, targetOverride, onlyXY); } } // eof methods }); </pre> </body> </html>