diff options
author | Thomas Steur <thomas.steur@googlemail.com> | 2014-09-05 13:42:51 +0400 |
---|---|---|
committer | Thomas Steur <thomas.steur@googlemail.com> | 2014-09-05 13:42:51 +0400 |
commit | 96ed663d643b55b2f6fa86777d58e25e53eb3fce (patch) | |
tree | 7e3bea0542b9c66a1732b8c63e79363c2720dd39 | |
parent | e89de039de65e9bc8c92b738fd73ed3cf3fac371 (diff) |
refs #4996 a lot of new tests and bugfixes, also some code improvements
-rw-r--r-- | js/piwik.js | 243 | ||||
-rw-r--r-- | misc/internal-docs/content-tracking.md | 1 | ||||
-rw-r--r-- | tests/javascript/content-fixtures/contentUtilities.html | 58 | ||||
-rw-r--r-- | tests/javascript/index.php | 87 |
4 files changed, 275 insertions, 114 deletions
diff --git a/js/piwik.js b/js/piwik.js index 2d85834e07..9ae51a4b9a 100644 --- a/js/piwik.js +++ b/js/piwik.js @@ -1116,6 +1116,9 @@ if (typeof Piwik !== 'object') { }, find: function (selector) { + // we use querySelectorAll only on document, not on nodes because of its unexpected behavior. See for + // instance http://stackoverflow.com/questions/11503534/jquery-vs-document-queryselectorall and + // http://jsfiddle.net/QdMc5/ and http://ejohn.org/blog/thoughts-on-queryselectorall if (!document.querySelectorAll || !selector) { return []; // we do not support all browsers } @@ -1141,6 +1144,15 @@ if (typeof Piwik !== 'object') { return nodes; }, + findNodesByTagName: function (node, tagName) { + if (!node || !tagName || !node.getElementsByTagName) { + return []; + } + + var foundNodes = node.getElementsByTagName(tagName); + + return this.htmlCollectionToArray(foundNodes); + }, makeNodesUnique: function (nodes) { nodes.sort(function (n1, n2) { @@ -1337,6 +1349,16 @@ if (typeof Piwik !== 'object') { if (nodes && nodes.length) { return nodes[0]; // TODO here we should check whether this node is also present within another nested piece of content and if there are multiple other nodes maybe use the next one } + }, + isLinkElement: function (node) { + if (!node) { + return false; + } + + var elementName = (node.nodeName + '').toLowerCase(); + var linkElementNames = ['a', 'area']; + + return linkElementNames.indexOf(elementName) !== -1; } } @@ -1369,6 +1391,8 @@ if (typeof Piwik !== 'object') { return []; } + // NOTE: we do not use query.findMultiple here as querySelectorAll would most likely not deliver the result we want + var nodes1 = query.findNodesHavingCssClass(node, this.CONTENT_CLASS); var nodes2 = query.findNodesHavingAttribute(node, this.CONTENT_ATTR); @@ -1560,7 +1584,8 @@ if (typeof Piwik !== 'object') { var mediaElements = ['img', 'embed', 'video', 'audio']; var elementName = node.nodeName.toLowerCase(); - if (-1 !== mediaElements.indexOf(elementName) && query.findFirstNodeHavingAttributeWithValue(node, 'src')) { + if (-1 !== mediaElements.indexOf(elementName) && + query.findFirstNodeHavingAttributeWithValue(node, 'src')) { var sourceNode = query.findFirstNodeHavingAttributeWithValue(node, 'src'); @@ -1570,6 +1595,24 @@ if (typeof Piwik !== 'object') { if (elementName === 'object' && query.hasNodeAttributeWithValue(node, 'data')) { return query.getAttributeValueFromNode(node, 'data'); + + } else if (elementName === 'object') { + var params = query.findNodesByTagName(node, 'param'); + if (params && params.length) { + var index; + for (index = 0; index < params.length; index++) { + if ('movie' === query.getAttributeValueFromNode(params[index], 'name') && + query.hasNodeAttributeWithValue(params[index], 'value')) { + + return query.getAttributeValueFromNode(params[index], 'value'); + } + } + } + + var embed = query.findNodesByTagName(node, 'embed'); + if (embed && embed.length) { + return this.findMediaUrlInNode(embed[0]); + } } }, trim: function (text) { @@ -1704,8 +1747,8 @@ if (typeof Piwik !== 'object') { return this.getLocation().origin + this.getLocation().pathname + url; } - // Eg mailto:x@y.z tel:012345, ... market:... sms:... and many more - if (0 === url.search('^[a-zA-Z]{2,8}:')) { + // Eg mailto:x@y.z tel:012345, ... market:... sms:..., javasript:... ecmascript: ... and many more + if (0 === url.search('^[a-zA-Z]{2,11}:')) { return url; } @@ -1718,43 +1761,16 @@ if (typeof Piwik !== 'object') { var base = this.getLocation().origin + this.getLocation().pathname.match(/(.*\/)/)[0] return base + url }, - findInternalTargetLinkFromHref: function (targetNode) - { - if (!targetNode) { - return; - } - - var link; - if (query.hasNodeAttributeWithValue(targetNode, 'href')) { - link = query.getAttributeValueFromNode(targetNode, 'href'); - } - - if (!link) { - return; - } - - if (link !== this.removeDomainIfIsUrl(link)) { - return; // outlink or download? - } - - return link; - }, - replaceTargetLink: function (node, url) + setHrefAttribute: function (node, url) { - var clickNode = this.findTargetNode(node); - - if (!clickNode) { - return; - } - - var elementName = clickNode.nodeName.toLowerCase(); - - if (elementName !== 'a') { + if (!node || !url) { return; } - if (clickNode && clickNode.setAttribute) { - clickNode.setAttribute('href', url); + if (node.setAttribute) { + node.setAttribute('href', url); + } else { + node.href = url; } }, shouldIgnoreInteraction: function (targetNode) { @@ -2930,72 +2946,52 @@ if (typeof Piwik !== 'object') { } for (index = 0; index < contentNodes.length; index++) { - // TODO should we consult getLinkType()? var targetNode = content.findTargetNode(contentNodes[index]); - if (!targetNode) { + if (!targetNode || targetNode.contentInteractionTrackingSetupDone) { continue; } - if (targetNode.contentTrackingRedirectSetupDone) { - continue; - } - - targetNode.contentTrackingRedirectSetupDone = true; + targetNode.contentInteractionTrackingSetupDone = true; if (content.shouldIgnoreInteraction(targetNode)) { continue; } - if (targetNode.nodeName.toLowerCase() === 'a' && - query.hasNodeAttributeWithValue(targetNode, 'href')) { + addEventListener(targetNode, 'click', function () { - var internalLink = content.findInternalTargetLinkFromHref(targetNode); + var link = getLinkIfShouldBeProcessed(this); - if (internalLink) { - - content.replaceTargetLink(targetNode, buildTrackingRedirectUrl(internalLink)); + if (link && link.type) { + // click ignore, will be tracked via processClick, we do not want to track it twice + return; + } - addEventListener(targetNode, 'click', function () { - var url = content.findInternalTargetLinkFromHref(this); - if (!url) { - return; - } + var contentBlock = content.findParentContentNode(this); + var contentName = content.findContentName(contentBlock); + var contentPiece = content.findContentPiece(contentBlock); + var contentTarget = content.findContentTarget(contentBlock); + var targetNode = content.findTargetNode(contentBlock); // should be actually same as 'this' - var contentName = content.findContentName(this); - var contentPiece = content.findContentPiece(this); - var contentTarget = content.findContentTarget(this); + if (query.isLinkElement(targetNode) && + query.hasNodeAttributeWithValue(targetNode, 'href')) { + var url = query.getAttributeValueFromNode(targetNode, 'href'); - if (0 === internalLink.indexOf('#')) { - var request = buildContentInteractionTrackingRequest('click', contentName, contentPiece, contentTarget); - sendRequest(request); - } else { - var targetUrl = buildContentInteractionTrackingRedirectUrl(internalLink, contentName, contentPiece, contentTarget); + if (0 !== url.indexOf('#')) { - // location.href does not respect target=_blank so we prefer to use this - content.replaceTargetLink(content.findTargetNode(this), targetUrl); - } + var targetUrl = buildContentInteractionTrackingRedirectUrl(url, contentName, contentPiece, contentTarget); - }); + // location.href does not respect target=_blank so we prefer to use this + content.setHrefAttribute(targetNode, targetUrl); + return; + } - } else { - // outlink or download } - } else { - // trigger event - - addEventListener(targetNode, 'click', function () { - var contentName = content.findContentName(this); - var contentPiece = content.findContentPiece(this); - var contentTarget = content.findContentTarget(this); - - var request = buildContentInteractionTrackingRequest('', contentName, contentPiece, contentTarget); - - sendRequest(request); - }); - } - + // click on any non link element, or on a link element that has not an href attribute or on an anchor + var request = buildContentInteractionTrackingRequest('click', contentName, contentPiece, contentTarget); + sendRequest(request, configTrackerPause); + }); } var index, request; @@ -3236,52 +3232,83 @@ if (typeof Piwik !== 'object') { // does file extension indicate that it is a download? downloadExtensionsPattern = new RegExp('\\.(' + configDownloadExtensions + ')([?&#]|$)', 'i'); - // optimization of the if..elseif..else construct below - return linkPattern.test(className) ? 'link' : (downloadPattern.test(className) || downloadExtensionsPattern.test(href) ? 'download' : (isInLink ? 0 : 'link')); + if (linkPattern.test(className)) { + return 'link'; + } + + if (downloadPattern.test(className) || downloadExtensionsPattern.test(href)) { + return 'download'; + } + + if (isInLink) { + return 0; + } + + return 'link'; } - /* - * Process clicks - */ - function processClick(sourceElement) { - var parentElement, - tag, - linkType, - originalSource; + function getSourceElement(sourceElement) + { + var parentElement; - originalSource = sourceElement; parentElement = sourceElement.parentNode; while (parentElement !== null && /* buggy IE5.5 */ isDefined(parentElement)) { - tag = sourceElement.tagName.toUpperCase(); - if (tag === 'A' || tag === 'AREA') { + + if (query.isLinkElement(sourceElement)) { break; } sourceElement = parentElement; parentElement = sourceElement.parentNode; } - if (isDefined(sourceElement.href)) { - // browsers, such as Safari, don't downcase hostname and href - var originalSourceHostName = sourceElement.hostname || getHostName(sourceElement.href), - sourceHostName = originalSourceHostName.toLowerCase(), - sourceHref = sourceElement.href.replace(originalSourceHostName, sourceHostName), - scriptProtocol = new RegExp('^(javascript|vbscript|jscript|mocha|livescript|ecmascript|mailto):', 'i'); + return sourceElement; + } + + function getLinkIfShouldBeProcessed(sourceElement) + { + sourceElement = getSourceElement(sourceElement); + + if (!isDefined(sourceElement.href)) { + return; + } + + // browsers, such as Safari, don't downcase hostname and href + var originalSourceHostName = sourceElement.hostname || getHostName(sourceElement.href); + var sourceHostName = originalSourceHostName.toLowerCase(); + var sourceHref = sourceElement.href.replace(originalSourceHostName, sourceHostName); + + // browsers, such as Safari, don't downcase hostname and href + var scriptProtocol = new RegExp('^(javascript|vbscript|jscript|mocha|livescript|ecmascript|mailto):', 'i'); - // ignore script pseudo-protocol links - if (!scriptProtocol.test(sourceHref)) { - // track outlinks and all downloads - linkType = getLinkType(sourceElement.className, sourceHref, isSiteHostName(sourceHostName)); + if (!scriptProtocol.test(sourceHref)) { + // track outlinks and all downloads + var linkType = getLinkType(sourceElement.className, sourceHref, isSiteHostName(sourceHostName)); - // urldecode %xx - sourceHref = urldecode(sourceHref); - logLink(sourceHref, linkType, undefined, null, originalSource); + if (linkType) { + return { + type: linkType, + href: sourceHref + }; } } } /* + * Process clicks + */ + function processClick(sourceElement) { + var link = getLinkIfShouldBeProcessed(sourceElement); + + if (link && link.type) { + // urldecode %xx + link.href = urldecode(link.href); + logLink(link.href, link.type, undefined, null, sourceElement); + } + } + + /* * Handle click event */ function clickHandler(evt) { diff --git a/misc/internal-docs/content-tracking.md b/misc/internal-docs/content-tracking.md index 92ab8b3c23..a61fbe98f8 100644 --- a/misc/internal-docs/content-tracking.md +++ b/misc/internal-docs/content-tracking.md @@ -62,6 +62,7 @@ If we do not find any specific content piece element, we will use the content bl * The simplest scenario is to provide an HTML attribute `data-content-piece="foo"` including a value anywhere within the content block or in the content block element itself. * If there is no such attribute we will check whether the content piece element is a media (audio, video, image) and we will try to detect the URL to the media automatically. For instance using the `src` attribute. + * In case of video and audio elements, when there are multiple sources defined, we will choose the URL of the first source * If we haven't found anything we will fall back to use the value "Unknown". In such a case you should set the attribute `data-content-piece` telling us explicitly what the content is. Examples: diff --git a/tests/javascript/content-fixtures/contentUtilities.html b/tests/javascript/content-fixtures/contentUtilities.html index 92d6a690ae..e9a3e83b99 100644 --- a/tests/javascript/content-fixtures/contentUtilities.html +++ b/tests/javascript/content-fixtures/contentUtilities.html @@ -4,6 +4,64 @@ <div id="ignoreInteraction2" data-track-content> <a href="http://www.example.com" class="piwikContentTarget" data-content-ignoreinteraction>Link</a> </div> +<!-- targetNode in this example is the content block node --> +<div id="ignoreInteraction3" data-track-content data-content-ignoreinteraction> + <a href="http://www.example.com">Link</a> +</div> +<!-- targetNode in this example is the content block node --> +<div id="ignoreInteraction4" data-track-content class="piwikContentIgnoreInteraction"> + <a href="http://www.example.com">Link</a> +</div> + <div id="notIgnoreInteraction1" data-track-content> <a href="http://www.example.com" class="piwikContentTarget">Link</a> +</div> +<!-- Will not be ignored since set on wrong element, has to be set on content target node --> +<div id="notIgnoreInteraction2" data-track-content data-content-ignoreinteraction class="piwikContentIgnoreInteraction"> + <a href="http://www.example.com" class="piwikContentTarget">Link</a> +</div> + +<a href="http://www.example.com" id="aLinkToBeChanged">Link</a> + +<div class="media"> + <div id="mediaDiv" src="test/img.jpg"/> + <img id="mediaImg" src="test/img.jpg"/> + + <video id="mediaVideo" width="320" height="240" controls> + <source src="movie.mp4" type="video/mp4"> + <source src="movie.ogg" type="video/ogg"> + Your browser does not support the video tag. + </video> + + <audio id="mediaAudio" controls> + <source src="audio.ogg" type="audio/ogg"> + <source src="audio.mp3" type="audio/mpeg"> + Your browser does not support the audio element. + </audio> + + <embed id="mediaEmbed" src="embed.swf"> + + <object id="mediaObjectSimple" width="400" height="400" data="objectSimple.swf"></object> + + <object id="mediaObjectParam" classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" width="550" + height="400" id="movie_name" align="middle"> + <param name="anything" value="anyvalue"/> + <param name="movie" value="movie_param1.swf"/> + <!--[if !IE]>--> + <object type="application/x-shockwave-flash" data="movie_inner.swf" width="550" height="400"> + <param name="movie" value="movie_param2.swf"/> + <!<![endif]--> <a href="http://www.adobe.com/go/getflash"> <img + src="http://www.adobe.com/de/images/shared/download_buttons/get_flash_player.gif" + alt="Get Adobe Flash player"/> </a> <!--[if !IE]>--> </object> + <!<![endif]--> + </object> + + <object id="mediaObjectPdf" data="document.pdf" type="application/pdf"> + <embed src="document2.pdf" type="application/pdf" /> + </object> + + <!-- should fall back to embed as no data specified --> + <object id="mediaObjectEmbed" data="" type="application/pdf"> + <embed src="document2.pdf" type="application/pdf" /> + </object> </div>
\ No newline at end of file diff --git a/tests/javascript/index.php b/tests/javascript/index.php index 9e876445f6..dca94b2db4 100644 --- a/tests/javascript/index.php +++ b/tests/javascript/index.php @@ -200,6 +200,10 @@ function setupContentTrackingFixture(name) { </ul> <div id="clickDiv"></div> </div> + <map name="map"> + <area id="area1" shape="rect" coords="0,0,10,10" href="img.jpg" alt="Piwik"> + <area shape="circle" coords="10,10,10,20" href="img2.jpg" alt="Piwik2"> + </map> <ol id="qunit-tests"></ol> @@ -562,6 +566,25 @@ function PiwikTest() { ok(-1 !== actual.indexOf(_e('click1')), 'htmlCollectionToArray, random check to make sure it contains a link'); + actual = query.isLinkElement(); + strictEqual(actual, false, "isLinkElement, no element set"); + + actual = query.isLinkElement(_e('div1')); + strictEqual(actual, false, "isLinkElement, a div is not a link element"); + + actual = query.isLinkElement(document.createTextNode('ff')); + strictEqual(actual, false, "isLinkElement, a text node is not a link element"); + + actual = query.isLinkElement(document.createComment('tt')); + strictEqual(actual, false, "isLinkElement, a comment is not a link element"); + + actual = query.isLinkElement(_e('area1')); + strictEqual(actual, true, "isLinkElement, an area element is a link element"); + + actual = query.isLinkElement(_e('click1')); + strictEqual(actual, true, "isLinkElement, an a element is a link element"); + + actual = query.find(); propEqual(actual, [], "find, no selector passed should return an empty array"); @@ -598,6 +621,26 @@ function PiwikTest() { actual = query.findMultiple(['.piwik_link', '[data-content-piece]', '#image2', '#div1']); propEqual(actual, [_e('image2'), _e('div1'), _e('click5')], "findMultiple, should make nodes unique in case we select the same multiple times"); + + + actual = query.findNodesByTagName(); + propEqual(actual, [], "findNodesByTagName, no element and no tag name set"); + + actual = query.findNodesByTagName(document.body); + propEqual(actual, [], "findNodesByTagName, no tag name set"); + + actual = query.findNodesByTagName(document.body, 'notExistingOne'); + propEqual(actual, [], "findNodesByTagName, should not find any such element"); + + actual = query.findNodesByTagName(document.body, 'a'); + ok($.isArray(actual), "findNodesByTagName, should always return an array"); + + actual = query.findNodesByTagName(document.body, 'h1'); + propEqual(actual, [_e('qunit-header')], "findNodesByTagName, find exactly one"); + + actual = query.findNodesByTagName(document.body, 'a'); + ok(actual.length > 10, "findNodesByTagName, find many, even nested ones"); + ok(actual.indexOf(_e('click1')), "findNodesByTagName, just a random test to make sure it actually contains a link"); }); test("contentFindContentBlock", function() { @@ -764,6 +807,7 @@ function PiwikTest() { var tracker = Piwik.getTracker(); var content = tracker.getContent(); + var query = tracker.getQuery(); content.setLocation(); // clear possible previous location var actual, expected; @@ -806,6 +850,11 @@ function PiwikTest() { strictEqual(content.shouldIgnoreInteraction(node), false, message); } + function assertFoundMediaUrl(id, expected, message) { + var node = content.findPieceNode(_e(id)); + strictEqual(content.findMediaUrlInNode(node), expected, message); + } + var locationAlias = $.extend({}, window.location); var origin = locationAlias.origin; @@ -852,10 +901,11 @@ function PiwikTest() { assertBuildsAbsoluteUrl('path/x?p=5', origin + '/tests/javascript/path/x?p=5', 'relative path'); assertBuildsAbsoluteUrl('#test', origin + '/tests/javascript/#test', 'anchor url'); assertBuildsAbsoluteUrl('//' + locationAlias.host + '/test/img.jpg', origin + '/test/img.jpg', 'inherit protocol url'); - assertBuildsAbsoluteUrl('mailto:test@example.com', 'mailto:test@example.com', 'mailto special url'); - assertBuildsAbsoluteUrl('tel:0123456789', 'tel:0123456789', 'tel special url'); - assertBuildsAbsoluteUrl('anythingg:test', origin + '/tests/javascript/anythingg:test', 'we do not treat this one as special url as we recognize max 8 characters currently'); - assertBuildsAbsoluteUrl('k1dm:test', origin + '/tests/javascript/k1dm:test', 'we do not treat this one as special url as it contains a number'); + assertBuildsAbsoluteUrl('mailto:test@example.com', 'mailto:test@example.com', 'mailto pseudo-protocol url'); + assertBuildsAbsoluteUrl('javascript:void 0', 'javascript:void 0', 'javascript pseudo-protocol url'); + assertBuildsAbsoluteUrl('tel:0123456789', 'tel:0123456789', 'tel pseudo-protocol url'); + assertBuildsAbsoluteUrl('anythinggggggggg:test', origin + '/tests/javascript/anythinggggggggg:test', 'we do not treat this one as pseudo-protocol url as there are too many characters before colon'); + assertBuildsAbsoluteUrl('k1dm:test', origin + '/tests/javascript/k1dm:test', 'we do not treat this one as pseudo-protocol url as it contains a number'); locationAlias.pathname = '/test/'; content.setLocation(locationAlias); @@ -886,12 +936,37 @@ function PiwikTest() { ok("test shouldIgnoreInteraction(targetNode)"); assertShouldIgnoreInteraction('ignoreInteraction1', 'should be ignored because of CSS class'); assertShouldIgnoreInteraction('ignoreInteraction2', 'should be ignored because of Attribute'); + assertShouldIgnoreInteraction('ignoreInteraction3', 'should be ignored because of CSS class'); + assertShouldIgnoreInteraction('ignoreInteraction4', 'should be ignored because of Attribute'); assertShouldNotIgnoreInteraction('notIgnoreInteraction1', 'should not be ignored'); + assertShouldNotIgnoreInteraction('notIgnoreInteraction2', 'should not be ignored as set in wrong element'); + + + ok("test setHrefAttribute(node, url)"); + var aElement = _e('aLinkToBeChanged'); + content.setHrefAttribute(); // should not fail if no arguments + strictEqual(query.getAttributeValueFromNode(aElement, 'href'), 'http://www.example.com', 'setHrefAttribute, check initial link value'); + content.setHrefAttribute(aElement); + content.setHrefAttribute(aElement, ''); + strictEqual(query.getAttributeValueFromNode(aElement, 'href'), 'http://www.example.com', 'setHrefAttribute, an empty URL should not be set'); + content.setHrefAttribute(aElement, '/test'); + strictEqual(query.getAttributeValueFromNode(aElement, 'href'), '/test', 'setHrefAttribute, link should be changed now'); + + strictEqual(content.findMediaUrlInNode(), undefined, 'should not fail if no node passed'); + ok(_e('click1') && _e('mediaDiv'), 'make sure both nodes exist otherwise following two assertions to not test what we want'); + assertFoundMediaUrl('click1', undefined, 'should not find anything in a link as it is not a media'); + assertFoundMediaUrl('mediaDiv', undefined, 'should not find anything in a non media element even if it defines a src attribute'); + assertFoundMediaUrl('mediaImg', 'test/img.jpg', 'should find url of image'); + assertFoundMediaUrl('mediaVideo', 'movie.mp4', 'should find url of video, first one should be used'); + assertFoundMediaUrl('mediaAudio', 'audio.ogg', 'should find url of audio, first one should be used'); + assertFoundMediaUrl('mediaEmbed', 'embed.swf', 'should find url of embed element'); + assertFoundMediaUrl('mediaObjectSimple', 'objectSimple.swf', 'should find url of a simple object element'); + assertFoundMediaUrl('mediaObjectParam', 'movie_param1.swf', 'should find url of a simple object element'); + assertFoundMediaUrl('mediaObjectPdf', 'document.pdf', 'should find url of an object that contains non flash resources such as pdf'); + assertFoundMediaUrl('mediaObjectEmbed', 'document2.pdf', 'should fallback to an embed in an object'); // isOrWasNodeInViewport // isNodeVisible - // findInternalTargetLinkFromHref - // replaceTargetLink }); |