1
0
mirror of https://github.com/MontFerret/ferret.git synced 2024-12-14 11:23:02 +02:00

Feature/#265 dom manipulations ()

* Added SetInnerHTML method

* Added E2E tests

* Refactored GetInnerText* methods

* Updated e2e tests

* Moved related E2E tests to folders

* Added error message

* Added E2E tests

* Added E2E for static driver
This commit is contained in:
Tim Voronov 2019-07-11 17:16:34 -04:00 committed by GitHub
parent 8afa3b65d5
commit 347bae2e45
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 772 additions and 302 deletions

View File

@ -31,6 +31,7 @@ cover:
e2e:
go run ${DIR_E2E}/main.go --tests ${DIR_E2E}/tests --pages ${DIR_E2E}/pages
# --filter=e2e/tests/dynamic/**/inner_text/*.fql
bench:
go test -run=XXX -bench=. ${DIR_PKG}/...

View File

@ -1,6 +1,6 @@
(function(){function La(a){var b=1,c;if(a)for(b=0,c=a.length-1;0<=c;c--){var d=a.charCodeAt(c);b=(b<<6&268435455)+d+(d<<14);d=b&266338304;b=0!=d?b^d>>21:b}return b};(function(){function a(){for(var a=te,b={},c=0;c<a.length;++c)b[a[c]]=c;return b}function b(){var a="ABCDEFGHIJKLMNOPQRSTUVWXYZ";a+=a.toLowerCase()+"0123456789-_";return a+"."}function c(a,b){if(!a||b===He.location.hostname)return!1;for(var c=0;c<a.length;c++)if(a[c]instanceof RegExp){if(a[c].test(b))return!0}else if(0<=b.indexOf(a[c]))return!0;return!1}function d(a){var b=ze.exec(a);if(b){var c=b[2],d=b[4];a=b[1];d&&(a=a+c+d)}return a}function e(a,b,c){function e(a){a=d(a);var b=a.charAt(a.length-
1);a&&"&"!==b&&(a+="&");return a+Fe}c=void 0===c?!1:c;var Ee=we.exec(b);if(!Ee)return"";b=Ee[1];var g=Ee[2]||"";Ee=Ee[3]||"";var Fe="_gl="+a;c?Ee="#"+e(Ee.substring(1)):g="?"+e(g.substring(1));return""+b+g+Ee}function g(a,b,d){for(var e={},Ee={},g=Me().va,Fe=0;Fe<g.length;++Fe){var Te=g[Fe];(!d||Te.forms)&&c(Te.Aa,b)&&(Te.ea?ue(Ee,Te.za()):ue(e,Te.za()))}Ge(e)&&(b=Qe(e),d?l(b,a):ca(b,a,!1));!d&&Ge(Ee)&&(d=Qe(Ee),ca(d,a,!0))}function ca(a,b,c){b.href&&(a=e(a,b.href,void 0===c?!1:c),ye.test(a)&&(b.href=
a))}function l(a,b){if(b&&b.action){var c=(b.method||"").toLowerCase();if("get"===c){c=b.childNodes||[];for(var d=!1,Ee=0;Ee<c.length;Ee++){var g=c[Ee];if("_gl"===g.name){g.setAttribute("value",a);d=!0;break}}d||(c=He.createElement("input"),c.setAttribute("type","hidden"),c.setAttribute("name","_gl"),c.setAttribute("value",a),b.appendChild(c))}else"post"===c&&(a=e(a,b.action),ye.test(a)&&(b.action=a))}}var k=this,w=function(a,b){a=a.split(".");var c=k;a[0]in c||"undefined"==typeof c.execScript||c.execScript("var "+
a))}function l(a,b){if(b&&b.action){var c=(b.method||"").toLowerCase();if("get"===c){c=b.childNodes||[];for(var d=!1,Ee=0; Ee<c.length; Ee++){var g=c[Ee];if("_gl"===g.name){g.setAttribute("value",a);d=!0;break}}d||(c=He.createElement("input"),c.setAttribute("type","hidden"),c.setAttribute("name","_gl"),c.setAttribute("value",a),b.appendChild(c))}else"post"===c&&(a=e(a,b.action),ye.test(a)&&(b.action=a))}}var k=this,w=function(a, b){a=a.split(".");var c=k;a[0]in c||"undefined"==typeof c.execScript||c.execScript("var "+
a[0]);for(var d;a.length&&(d=a.shift());)a.length||void 0===b?c=c[d]&&c[d]!==Object.prototype[d]?c[d]:c[d]={}:c[d]=b},ha=function(a){var b=[];if(Array.prototype.indexOf)return a=b.indexOf(a),"number"==typeof a?a:-1;for(var c=0;c<b.length;c++)if(b[c]===a)return c;return-1},ue=function(a,b){for(var c in b)b.hasOwnProperty(c)&&(a[c]=b[c])},Ge=function(a){for(var b in a)if(a.hasOwnProperty(b))return!0;return!1},ye=/^(?:(?:https?|mailto|ftp):|[^:/?#]*(?:[/?#]|$))/i,Ie=window,He=document,Be=function(a,
b){He.addEventListener?He.addEventListener(a,b,!1):He.attachEvent&&He.attachEvent("on"+a,b)},Ce=/:[0-9]+$/,xe=function(a,b){var c=function(a){return a?a.replace(":","").toLowerCase():""};c=c(a.protocol)||c(Ie.location.protocol);b&&(b=String(b).toLowerCase());switch(b){case "protocol":a=c;break;case "host":a=(a.hostname||Ie.location.hostname).replace(Ce,"").toLowerCase();break;case "port":a=String(Number(a.hostname?a.port:Ie.location.port)||("http"==c?80:"https"==c?443:""));break;case "path":a="/"==
a.pathname.substr(0,1)?a.pathname:"/"+a.pathname;a=a.split("/");0<=ha(a[a.length-1])&&(a[a.length-1]="");a=a.join("/");break;case "query":a=a.search.replace("?","");break;case "extension":a=a.pathname.split(".");a=1<a.length?a[a.length-1]:"";a=a.split("/")[0];break;case "fragment":a=a.hash.replace("#","");break;default:a=a&&a.href}return a},da=function(a){var b=document.createElement("a");a&&(ye.test(a),b.href=a);a=b.pathname;"/"!==a[0]&&(a="/"+a);var c=b.hostname.replace(Ce,"");return{href:b.href,
@ -10,7 +10,7 @@ a[c];void 0!==d&&d===d&&null!==d&&"[object Object]"!==d.toString()&&(b.push(c),b
a.charCodeAt(c))&255];return((b^-1)>>>0).toString(36)},Oe=function(a){return function(b){var c=da(Ie.location.href),d=c.search.replace("?","");a:{for(var e=d.split("&"),g=0;g<e.length;g++){var ca=e[g].split("=");if("_gl"==decodeURIComponent(ca[0]).replace(/\+/g," ")){e=ca.slice(1).join("=");break a}}e=void 0}b.query=Re(e||"")||{};e=xe(c,"fragment");g=e.match(ze);b.ea=Re(g&&g[3]||"")||{};a&&Pe(c,d,e)}},Pe=function(a,b,c){function e(a,b){a=d(a);a.length&&(a=b+a);return a}Ie.history&&Ie.history.replaceState&&
(ze.test(b)||ze.test(c))&&(a=xe(a,"path"),b=e(b,"?"),c=e(c,"#"),Ie.history.replaceState({},void 0,""+a+b+c))},Re=function(a){var b=void 0===b?3:b;try{if(a){var c=Nd.exec(a);if(c&&"1"===c[1]){var d=c[2],e=c[3];a:{for(a=0;a<b;++a)if(d===Ne(e,a)){var g=!0;break a}g=!1}if(g){b={};var ca=e?e.split("*"):[];for(e=0;e<ca.length;e+=2)b[ca[e]]=Ke(ca[e+1]);return b}}}}catch(ef){}},Se=function(a){try{a:{var b=a.target||a.srcElement||{};for(a=100;b&&0<a;){if(b.href&&b.nodeName.match(/^a(?:rea)?$/i)){var c=b;break a}b=
b.parentNode;a--}c=null}if(c){var d=c.protocol;"http:"!==d&&"https:"!==d||g(c,c.hostname,!1)}}catch(ff){}},Je=function(a){try{var b=a.target||a.srcElement||{};if(b.action){var c=xe(da(b.action),"host");g(b,c,!0)}}catch(df){}};w("google_tag_data.glBridge.auto",function(a,b,c,d){var e=Me();e.N||(Be("mousedown",Se),Be("keyup",Se),Be("submit",Je),e.N=!0);a={za:a,Aa:b,ea:"fragment"===c,forms:!!d};Me().va.push(a)});w("google_tag_data.glBridge.decorate",function(a,b,c){c=!!c;a=Qe(a);if(b.tagName){if("a"==
b.tagName.toLowerCase())return ca(a,b,c);if("form"==b.tagName.toLowerCase())return l(a,b)}if("string"==typeof b)return e(a,b,c)});w("google_tag_data.glBridge.generate",Qe);w("google_tag_data.glBridge.get",function(a,b){var c=Oe(!!b);b=Me();b.data||(b.data={query:{},ea:{}},c(b.data));c={};if(b=b.data)ue(c,b.query),a&&ue(c,b.ea);return c})})(window);var $c=function(a){this.w=a||[]};$c.prototype.set=function(a){this.w[a]=!0};$c.prototype.encode=function(){for(var a=[],b=0;b<this.w.length;b++)this.w[b]&&(a[Math.floor(b/6)]^=1<<b%6);for(b=0;b<a.length;b++)a[b]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".charAt(a[b]||0);return a.join("")+"~"};var vd=new $c;function J(a){vd.set(a)}var Td=function(a){a=Dd(a);a=new $c(a);for(var b=vd.w.slice(),c=0;c<a.w.length;c++)b[c]=b[c]||a.w[c];return(new $c(b)).encode()},Dd=function(a){a=a.get(Gd);ka(a)||(a=[]);return a};var ea=function(a){return"function"==typeof a},ka=function(a){return"[object Array]"==Object.prototype.toString.call(Object(a))},qa=function(a){return void 0!=a&&-1<(a.constructor+"").indexOf("String")},D=function(a,b){return 0==a.indexOf(b)},sa=function(a){return a?a.replace(/^[\s\xa0]+|[\s\xa0]+$/g,""):""},ra=function(){for(var a=O.navigator.userAgent+(M.cookie?M.cookie:"")+(M.referrer?M.referrer:""),b=a.length,c=O.history.length;0<c;)a+=c--^b++;return[hd()^La(a)&2147483647,Math.round((new Date).getTime()/
b.tagName.toLowerCase())return ca(a,b,c);if("form"==b.tagName.toLowerCase())return l(a,b)}if("string"==typeof b)return e(a,b,c)});w("google_tag_data.glBridge.generate",Qe);w("get",function(a, b){var c=Oe(!!b);b=Me();b.data||(b.data={query:{},ea:{}},c(b.data));c={};if(b=b.data)ue(c,b.query),a&&ue(c,b.ea);return c})})(window);var $c=function(a){this.w=a||[]};$c.prototype.set=function(a){this.w[a]=!0};$c.prototype.encode=function(){for(var a=[],b=0; b<this.w.length; b++)this.w[b]&&(a[Math.floor(b/6)]^=1<<b%6);for(b=0; b<a.length; b++)a[b]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".charAt(a[b]||0);return a.join("")+"~"};var vd=new $c;function J(a){vd.set(a)}var Td=function(a){a=Dd(a);a=new $c(a);for(var b=vd.w.slice(),c=0;c<a.w.length;c++)b[c]=b[c]||a.w[c];return(new $c(b)).encode()},Dd=function(a){a=a.get(Gd);ka(a)||(a=[]);return a};var ea=function(a){return"function"==typeof a},ka=function(a){return"[object Array]"==Object.prototype.toString.call(Object(a))},qa=function(a){return void 0!=a&&-1<(a.constructor+"").indexOf("String")},D=function(a,b){return 0==a.indexOf(b)},sa=function(a){return a?a.replace(/^[\s\xa0]+|[\s\xa0]+$/g,""):""},ra=function(){for(var a=O.navigator.userAgent+(M.cookie?M.cookie:"")+(M.referrer?M.referrer:""),b=a.length,c=O.history.length;0<c;)a+=c--^b++;return[hd()^La(a)&2147483647,Math.round((new Date).getTime()/
1E3)].join(".")},ta=function(a){var b=M.createElement("img");b.width=1;b.height=1;b.src=a;return b},ua=function(){},K=function(a){if(encodeURIComponent instanceof Function)return encodeURIComponent(a);J(28);return a},L=function(a,b,c,d){try{a.addEventListener?a.addEventListener(b,c,!!d):a.attachEvent&&a.attachEvent("on"+b,c)}catch(e){J(27)}},f=/^[\w\-:/.?=&%!\[\]]+$/,wa=function(a,b,c){a&&(c?(c="",b&&f.test(b)&&(c=' id="'+b+'"'),f.test(a)&&M.write("<script"+c+' src="'+a+'">\x3c/script>')):(c=M.createElement("script"),
c.type="text/javascript",c.async=!0,c.src=a,b&&(c.id=b),a=M.getElementsByTagName("script")[0],a.parentNode.insertBefore(c,a)))},be=function(a,b){return E(M.location[b?"href":"search"],a)},E=function(a,b){return(a=a.match("(?:&|#|\\?)"+K(b).replace(/([.*+?^=!:${}()|\[\]\/\\])/g,"\\$1")+"=([^&#]*)"))&&2==a.length?a[1]:""},xa=function(){var a=""+M.location.hostname;return 0==a.indexOf("www.")?a.substring(4):a},de=function(a,b){var c=a.indexOf(b);if(5==c||6==c)if(a=a.charAt(c+b.length),"/"==a||"?"==a||
""==a||":"==a)return!0;return!1},ya=function(a,b){var c=M.referrer;if(/^(https?|android-app):\/\//i.test(c)){if(a)return c;a="//"+M.location.hostname;if(!de(c,a))return b&&(b=a.replace(/\./g,"-")+".cdn.ampproject.org",de(c,b))?void 0:c}},za=function(a,b){if(1==b.length&&null!=b[0]&&"object"===typeof b[0])return b[0];for(var c={},d=Math.min(a.length+1,b.length),e=0;e<d;e++)if("object"===typeof b[e]){for(var g in b[e])b[e].hasOwnProperty(g)&&(c[g]=b[e][g]);break}else e<a.length&&(c[a[e]]=b[e]);return c};var ee=function(){this.keys=[];this.values={};this.m={}};ee.prototype.set=function(a,b,c){this.keys.push(a);c?this.m[":"+a]=b:this.values[":"+a]=b};ee.prototype.get=function(a){return this.m.hasOwnProperty(":"+a)?this.m[":"+a]:this.values[":"+a]};ee.prototype.map=function(a){for(var b=0;b<this.keys.length;b++){var c=this.keys[b],d=this.get(c);d&&a(c,d)}};var O=window,M=document,va=function(a,b){return setTimeout(a,b)};var F=window,Ea=document,G=function(a){var b=F._gaUserPrefs;if(b&&b.ioo&&b.ioo()||a&&!0===F["ga-disable-"+a])return!0;try{var c=F.external;if(c&&c._gaUserPrefs&&"oo"==c._gaUserPrefs)return!0}catch(g){}a=[];b=String(Ea.cookie||document.cookie).split(";");for(c=0;c<b.length;c++){var d=b[c].split("="),e=d[0].replace(/^\s*|\s*$/g,"");e&&"AMP_TOKEN"==e&&((d=d.slice(1).join("=").replace(/^\s*|\s*$/g,""))&&(d=decodeURIComponent(d)),a.push(d))}for(b=0;b<a.length;b++)if("$OPT_OUT"==a[b])return!0;return!1};var Ca=function(a){var b=[],c=M.cookie.split(";");a=new RegExp("^\\s*"+a+"=\\s*(.*?)\\s*$");for(var d=0;d<c.length;d++){var e=c[d].match(a);e&&b.push(e[1])}return b},zc=function(a,b,c,d,e,g){e=G(e)?!1:eb.test(M.location.hostname)||"/"==c&&vc.test(d)?!1:!0;if(!e)return!1;b&&1200<b.length&&(b=b.substring(0,1200));c=a+"="+b+"; path="+c+"; ";g&&(c+="expires="+(new Date((new Date).getTime()+g)).toGMTString()+"; ");d&&"none"!==d&&(c+="domain="+d+";");d=M.cookie;M.cookie=c;if(!(d=d!=M.cookie))a:{a=Ca(a);
@ -44,7 +44,7 @@ else{J(14);d=Gc(d,lc(b).split(".").length,0);if(1==d.length)return d[0].s;d=Gc(d
b=M.location.hostname;eb.test(b)||vc.test(b)||a.push("none");return a},kc=function(a){if(!a)return"/";1<a.length&&a.lastIndexOf("/")==a.length-1&&(a=a.substr(0,a.length-1));0!=a.indexOf("/")&&(a="/"+a);return a},jc=function(a){a=kc(a);return"/"==a?1:a.split("/").length},le=function(a){a.ta&&J(77);a.na&&J(74);a.pa&&J(73);a.ua&&J(69)};function Xc(a,b,c){"none"==b&&(b="");var d=[],e=Ca(a);a="__utma"==a?6:2;for(var g=0;g<e.length;g++){var ca=(""+e[g]).split(".");ca.length>=a&&d.push({hash:ca[0],R:e[g],O:ca})}if(0!=d.length)return 1==d.length?d[0]:Zc(b,d)||Zc(c,d)||Zc(null,d)||d[0]}function Zc(a,b){if(null==a)var c=a=1;else c=La(a),a=La(D(a,".")?a.substring(1):"."+a);for(var d=0;d<b.length;d++)if(b[d].hash==c||b[d].hash==a)return b[d]};var od=new RegExp(/^https?:\/\/([^\/:]+)/),De=O.google_tag_data.glBridge,pd=/(.*)([?&#])(?:_ga=[^&#]*)(?:&?)(.*)/,me=/(.*)([?&#])(?:_gac=[^&#]*)(?:&?)(.*)/;function Bc(a){if(a.get(Ze))return J(35),De.generate($e(a));var b=a.get(Q),c=a.get(I)||"";b="_ga=2."+K(pa(c+b,0)+"."+c+"-"+b);(a=af(a))?(J(44),a="&_gac=1."+K([pa(a.qa,0),a.timestamp,a.qa].join("."))):a="";return b+a}
function Ic(a,b){var c=new Date,d=O.navigator,e=d.plugins||[];a=[a,d.userAgent,c.getTimezoneOffset(),c.getYear(),c.getDate(),c.getHours(),c.getMinutes()+b];for(b=0;b<e.length;++b)a.push(e[b].description);return La(a.join("."))}function pa(a,b){var c=new Date,d=O.navigator,e=c.getHours()+Math.floor((c.getMinutes()+b)/60);return La([a,d.userAgent,d.language||"",c.getTimezoneOffset(),c.getYear(),c.getDate()+Math.floor(e/24),(24+e)%24,(60+c.getMinutes()+b)%60].join("."))}
var Dc=function(a){J(48);this.target=a;this.T=!1};Dc.prototype.ca=function(a,b){if(a){if(this.target.get(Ze))return De.decorate($e(this.target),a,b);if(a.tagName){if("a"==a.tagName.toLowerCase()){a.href&&(a.href=qd(this,a.href,b));return}if("form"==a.tagName.toLowerCase())return rd(this,a)}if("string"==typeof a)return qd(this,a,b)}};
var qd=function(a,b,c){var d=pd.exec(b);d&&3<=d.length&&(b=d[1]+(d[3]?d[2]+d[3]:""));(d=me.exec(b))&&3<=d.length&&(b=d[1]+(d[3]?d[2]+d[3]:""));a=a.target.get("linkerParam");var e=b.indexOf("?");d=b.indexOf("#");c?b+=(-1==d?"#":"&")+a:(c=-1==e?"?":"&",b=-1==d?b+(c+a):b.substring(0,d)+c+a+b.substring(d));b=b.replace(/&+_ga=/,"&_ga=");return b=b.replace(/&+_gac=/,"&_gac=")},rd=function(a,b){if(b&&b.action)if("get"==b.method.toLowerCase()){a=a.target.get("linkerParam").split("&");for(var c=0;c<a.length;c++){var d=
var qd=function(a,b,c){var d=pd.exec(b);d&&3<=d.length&&(b=d[1]+(d[3]?d[2]+d[3]:""));(d=me.exec(b))&&3<=d.length&&(b=d[1]+(d[3]?d[2]+d[3]:""));a=a.target.get("linkerParam");var e=b.indexOf("?");d=b.indexOf("#");c?b+=(-1==d?"#":"&")+a:(c=-1==e?"?":"&",b=-1==d?b+(c+a):b.substring(0,d)+c+a+b.substring(d));b=b.replace(/&+_ga=/,"&_ga=");return b=b.replace(/&+_gac=/,"&_gac=")},rd=function(a,b){if(b&&b.action)if("get"==b.method.toLowerCase()){a=a.target.get("linkerParam").split("&");for(var c=0; c<a.length; c++){var d=
a[c].split("="),e=d[1];d=d[0];for(var g=b.childNodes||[],ca=!1,l=0;l<g.length;l++)if(g[l].name==d){g[l].setAttribute("value",e);ca=!0;break}ca||(g=M.createElement("input"),g.setAttribute("type","hidden"),g.setAttribute("name",d),g.setAttribute("value",e),b.appendChild(g))}}else"post"==b.method.toLowerCase()&&(b.action=qd(a,b.action))};
Dc.prototype.S=function(a,b,c){function d(c){try{c=c||O.event;a:{var d=c.target||c.srcElement;for(c=100;d&&0<c;){if(d.href&&d.nodeName.match(/^a(?:rea)?$/i)){var e=d;break a}d=d.parentNode;c--}e={}}("http:"==e.protocol||"https:"==e.protocol)&&sd(a,e.hostname||"")&&e.href&&(e.href=qd(g,e.href,b))}catch(w){J(26)}}var e=this;if(this.target.get(Ze))De.auto(function(){return $e(e.target)},a,b?"fragment":"",c);else{var g=this;this.T||(this.T=!0,L(M,"mousedown",d,!1),L(M,"keyup",d,!1));c&&L(M,"submit",function(b){b=
b||O.event;if((b=b.target||b.srcElement)&&b.action){var c=b.action.match(od);c&&sd(a,c[1])&&rd(g,b)}})}};function sd(a,b){if(b==M.location.hostname)return!1;for(var c=0;c<a.length;c++)if(a[c]instanceof RegExp){if(a[c].test(b))return!0}else if(0<=b.indexOf(a[c]))return!0;return!1}function ke(a,b){return b!=Ic(a,0)&&b!=Ic(a,-1)&&b!=Ic(a,-2)&&b!=pa(a,0)&&b!=pa(a,-1)&&b!=pa(a,-2)}function $e(a){var b=af(a);return{_ga:a.get(Q),_gid:a.get(I)||void 0,_gac:b?[b.qa,b.timestamp].join("."):void 0}}
@ -69,5 +69,5 @@ Z.v=function(a){try{if(a.u)a.u.call(O,N.j("t0"));else{var b=a.c==gb?N:N.j(a.c);i
N.create=function(a){var b=za(uc,[].slice.call(arguments));b[V]||(b[V]="t0");var c=""+b[V];if(N.h[c])return N.h[c];a:{if(b[Kd]){J(67);if(b[ac]&&"cookie"!=b[ac]){var d=!1;break a}if(ja.test(M.referrer)){d=M.location.hostname.replace(Ue,"");var e=M.referrer.replace(/^https?:\/\//,"").replace(/^[^/]+/,"").split("/");var g=e[2];e=("s"==g?decodeURIComponent(e[3]):decodeURIComponent(g)).replace(Ue,"");d==e?(d=(d=De.get())&&d._ga||void 0,void 0!==d&&(Ab=d,J(81))):J(78)}if(void 0!==Ab)b[Q]||(b[Q]=Ab);else{b:if(d=
String(b[W]||xa()),e=String(b[Yb]||"/"),g=Ca(String(b[U]||"_ga")),d=na(g,d,e),!d||jd.test(d))d=!0;else if(d=Ca("AMP_TOKEN"),0==d.length)d=!0;else{if(1==d.length&&(d=decodeURIComponent(d[0]),"$RETRIEVING"==d||"$OPT_OUT"==d||"$ERROR"==d||"$NOT_FOUND"==d)){d=!0;break b}d=!1}if(d&&tc(ic,String(b[Na]))){d=!0;break a}}}d=!1}if(d)return null;b=new pc(b);N.h[c]=b;N.P.push(b);return b};N.remove=function(a){for(var b=0;b<N.P.length;b++)if(N.P[b].get(V)==a){N.P.splice(b,1);N.h[a]=null;break}};N.j=function(a){return N.h[a]};
N.getAll=function(){return N.P.slice(0)};
N.N=function(){"ga"!=gb&&J(49);var a=O[gb];if(!a||42!=a.answer){N.L=a&&a.l;N.ya=1*new Date;N.loaded=!0;var b=O[gb]=N;X("create",b,b.create);X("remove",b,b.remove);X("getByName",b,b.j,5);X("getAll",b,b.getAll,6);b=pc.prototype;X("get",b,b.get,7);X("set",b,b.set,4);X("send",b,b.send);X("requireSync",b,b.ma);b=Ya.prototype;X("get",b,b.get);X("set",b,b.set);if("https:"!=M.location.protocol&&!Ba){a:{b=M.getElementsByTagName("script");for(var c=0;c<b.length&&100>c;c++){var d=b[c].src;if(d&&0==d.indexOf("https://www.google-analytics.com/analytics")){b=
N.N=function(){"ga"!=gb&&J(49);var a=O[gb];if(!a||42!=a.answer){N.L=a&&a.l;N.ya=1*new Date;N.loaded=!0;var b=O[gb]=N;X("create",b,b.create);X("remove",b,b.remove);X("getByName",b,b.j,5);X("getAll",b,b.getAll,6);b=pc.prototype;X("get",b,b.get,7);X("set",b,b.set,4);X("send",b,b.send);X("requireSync",b,b.ma);b=Ya.prototype;X("get",b,b.get);X("set",b,b.set);if("https:"!=M.location.protocol&&!Ba){a:{b=M.getElementsByTagName("script");for(var c=0; c<b.length&&100>c; c++){var d=b[c].src;if(d&&0==d.indexOf("https://www.google-analytics.com/analytics")){b=
!0;break a}}b=!1}b&&(Ba=!0)}(O.gaplugins=O.gaplugins||{}).Linker=Dc;b=Dc.prototype;C("linker",Dc);X("decorate",b,b.ca,20);X("autoLink",b,b.S,25);C("displayfeatures",fd);C("adfeatures",fd);a=a&&a.q;ka(a)?Z.D.apply(N,a):J(50)}};N.da=function(){for(var a=N.getAll(),b=0;b<a.length;b++)a[b].get(V)};var bf=N.N,cf=O[gb];cf&&cf.r?bf():z(bf);z(function(){Z.D(["provide","render",ua])});})(window);

File diff suppressed because one or more lines are too long

View File

@ -6,6 +6,7 @@ import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
"github.com/MontFerret/ferret/pkg/compiler"
@ -185,6 +186,8 @@ func (r *Runner) runQuery(ctx context.Context, c *compiler.FqlCompiler, name, sc
}
}
mustFail := r.mustFail(name)
out, err := p.Run(
ctx,
runtime.WithLog(zerolog.ConsoleWriter{Out: os.Stdout}),
@ -195,6 +198,13 @@ func (r *Runner) runQuery(ctx context.Context, c *compiler.FqlCompiler, name, sc
duration := time.Since(start)
if err != nil {
if mustFail {
return Result{
name: name,
duration: duration,
}
}
return Result{
name: name,
duration: duration,
@ -202,6 +212,14 @@ func (r *Runner) runQuery(ctx context.Context, c *compiler.FqlCompiler, name, sc
}
}
if mustFail {
return Result{
name: name,
duration: duration,
err: errors.New("expected to fail"),
}
}
var result string
if err := json.Unmarshal(out, &result); err != nil {
@ -281,3 +299,7 @@ func (r *Runner) traverseDir(ctx context.Context, dir string, iteratee func(name
return nil
}
func (r *Runner) mustFail(name string) bool {
return strings.HasSuffix(name, ".fail.fql")
}

View File

@ -0,0 +1,13 @@
LET url = @dynamic
LET doc = DOCUMENT(url, true)
LET expected = `<span>Hello</span>`
INNER_HTML_SET(doc, "body", "<span>Hello</span>")
LET actual = INNER_HTML(doc, "body")
LET r1 = '(\s|\")'
LET r2 = '(\n|\s|\")'
RETURN EXPECT(REGEXP_REPLACE(expected, r1, ''), REGEXP_REPLACE(TRIM(actual), r2, ''))

View File

@ -0,0 +1,13 @@
LET url = @dynamic
LET doc = DOCUMENT(url, true)
LET expected = `Hello`
INNER_TEXT_SET(doc, "body", expected)
LET actual = INNER_TEXT(doc, "body")
LET r1 = '(\s|\")'
LET r2 = '(\n|\s|\")'
RETURN EXPECT(REGEXP_REPLACE(expected, r1, ''), REGEXP_REPLACE(TRIM(actual), r2, ''))

View File

@ -0,0 +1,11 @@
LET url = @dynamic
LET doc = DOCUMENT(url, true)
LET el = ELEMENT(doc, ".jumbotron")
LET expected = `Welcome to Ferret E2E test page!`
LET actual = INNER_HTML(el, "h1")
LET r1 = '(\n|\s)'
LET r2 = '(\n|\s)'
RETURN EXPECT(REGEXP_REPLACE(expected, r1, ''), REGEXP_REPLACE(TRIM(actual), r2, ''))

View File

@ -0,0 +1,5 @@
LET url = @dynamic
LET doc = DOCUMENT(url, true)
LET el = ELEMENT(doc, ".jumbotron")
RETURN INNER_HTML(el, "h5")

View File

@ -0,0 +1,14 @@
LET url = @dynamic
LET doc = DOCUMENT(url, true)
LET el = ELEMENT(doc, "#index")
LET expected = "<span>This node was injected by Ferret</span>"
INNER_HTML_SET(el, "<span>This node was injected by Ferret</span>")
LET actual = INNER_HTML(el)
WAIT(100)
LET r1 = '(\s|\")'
LET r2 = '(\n|\s|\")'
RETURN EXPECT(REGEXP_REPLACE(expected, r1, ''), REGEXP_REPLACE(TRIM(actual), r2, ''))

View File

@ -0,0 +1,11 @@
LET url = @dynamic
LET doc = DOCUMENT(url, true)
LET el = ELEMENT(doc, ".jumbotron")
LET expected = `Welcome to Ferret E2E test page!`
LET actual = INNER_TEXT(el, "h1")
LET r1 = '(\n|\s)'
LET r2 = '(\n|\s)'
RETURN EXPECT(REGEXP_REPLACE(expected, r1, ''), REGEXP_REPLACE(TRIM(actual), r2, ''))

View File

@ -0,0 +1,5 @@
LET url = @dynamic
LET doc = DOCUMENT(url, true)
LET el = ELEMENT(doc, ".jumbotron")
RETURN INNER_TEXT(el, "h5")

View File

@ -0,0 +1,13 @@
LET url = @dynamic
LET doc = DOCUMENT(url, true)
LET el = ELEMENT(doc, ".jumbotron")
LET expected = `Foobar`
INNER_TEXT_SET(el, expected)
WAIT(100)
LET actual = INNER_TEXT(el)
LET r1 = '(\n|\s)'
LET r2 = '(\n|\s)'
RETURN EXPECT(REGEXP_REPLACE(expected, r1, ''), REGEXP_REPLACE(TRIM(actual), r2, ''))

View File

@ -0,0 +1,5 @@
LET url = @dynamic
LET doc = DOCUMENT(url, true)
LET el = ELEMENT(doc, ".jumbotron")
RETURN INNER_TEXT_SET(el, "h4", "foobar")

View File

@ -0,0 +1,14 @@
LET url = @static + '/simple.html'
LET doc = DOCUMENT(url)
LET el = ELEMENT(doc, "body")
LET expected = `<div><h1>Injected by Ferret</h1></div>`
INNER_HTML_SET(el, expected)
LET actual = INNER_HTML(el)
LET r1 = '(\s|\")'
LET r2 = '(\n|\s|\")'
RETURN EXPECT(REGEXP_REPLACE(expected, r1, ''), REGEXP_REPLACE(TRIM(actual), r2, ''))

View File

@ -0,0 +1,14 @@
LET url = @static + '/simple.html'
LET doc = DOCUMENT(url)
LET el = ELEMENT(doc, "body")
LET expected = `Injected by Ferret`
INNER_TEXT_SET(el, expected)
LET actual = INNER_TEXT(el)
LET r1 = '(\s|\")'
LET r2 = '(\n|\s|\")'
RETURN EXPECT(REGEXP_REPLACE(expected, r1, ''), REGEXP_REPLACE(TRIM(actual), r2, ''))

View File

@ -22,8 +22,6 @@ import (
"github.com/MontFerret/ferret/pkg/runtime/values"
)
const BlankPageURL = "about:blank"
type HTMLDocument struct {
logger *zerolog.Logger
client *cdp.Client

View File

@ -3,6 +3,7 @@ package cdp
import (
"context"
"sync"
"time"
"github.com/mafredri/cdp"
"github.com/mafredri/cdp/devtool"
@ -16,6 +17,8 @@ import (
)
const DriverName = "cdp"
const BlankPageURL = "about:blank"
const DefaultTimeout = 5000 * time.Millisecond
type Driver struct {
mu sync.Mutex

View File

@ -4,9 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/MontFerret/ferret/pkg/drivers/cdp/templates"
"github.com/pkg/errors"
"golang.org/x/net/html"
"hash/fnv"
"strconv"
"strings"
@ -16,6 +13,7 @@ import (
"github.com/MontFerret/ferret/pkg/drivers/cdp/eval"
"github.com/MontFerret/ferret/pkg/drivers/cdp/events"
"github.com/MontFerret/ferret/pkg/drivers/cdp/input"
"github.com/MontFerret/ferret/pkg/drivers/cdp/templates"
"github.com/MontFerret/ferret/pkg/drivers/common"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
@ -24,7 +22,9 @@ import (
"github.com/mafredri/cdp"
"github.com/mafredri/cdp/protocol/dom"
"github.com/mafredri/cdp/protocol/runtime"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"golang.org/x/net/html"
)
var emptyNodeID = dom.NodeID(0)
@ -46,7 +46,7 @@ type (
id HTMLElementIdentity
nodeType html.NodeType
nodeName values.String
innerHTML values.String
innerHTML *common.LazyValue
innerText *common.LazyValue
value core.Value
attributes *common.LazyValue
@ -118,7 +118,7 @@ func LoadHTMLElementWithID(
return nil, core.Error(err, strconv.Itoa(int(id.nodeID)))
}
innerHTML, err := loadInnerHTML(ctx, client, exec, id, common.ToHTMLType(node.Node.NodeType))
innerHTML, err := getInnerHTML(ctx, client, exec, id, common.ToHTMLType(node.Node.NodeType))
if err != nil {
return nil, core.Error(err, strconv.Itoa(int(id.nodeID)))
@ -168,7 +168,7 @@ func NewHTMLElement(
el.id = id
el.nodeType = common.ToHTMLType(nodeType)
el.nodeName = values.NewString(nodeName)
el.innerHTML = innerHTML
el.innerHTML = common.NewVolatileValue(innerHTML, el.loadInnerHTML)
el.innerText = common.NewLazyValue(el.loadInnerText)
el.attributes = common.NewLazyValue(el.loadAttrs)
el.style = common.NewLazyValue(el.parseStyle)
@ -212,17 +212,22 @@ func (el *HTMLElement) Type() core.Type {
}
func (el *HTMLElement) MarshalJSON() ([]byte, error) {
val, err := el.innerText.Read(context.Background())
if err != nil {
return nil, err
}
return json.Marshal(val.String())
return json.Marshal(el.String())
}
func (el *HTMLElement) String() string {
return el.GetInnerHTML(context.Background()).String()
ctx, cancel := context.WithTimeout(context.Background(), DefaultTimeout)
defer cancel()
res, err := el.GetInnerHTML(ctx)
if err != nil {
el.logError(errors.Wrap(err, "HTMLElement.String"))
return ""
}
return res.String()
}
func (el *HTMLElement) Compare(other core.Value) int64 {
@ -230,9 +235,7 @@ func (el *HTMLElement) Compare(other core.Value) int64 {
case drivers.HTMLElementType:
other := other.(drivers.HTMLElement)
ctx := context.Background()
return el.GetInnerHTML(ctx).Compare(other.GetInnerHTML(ctx))
return int64(strings.Compare(el.String(), other.String()))
default:
return drivers.Compare(el.Type(), other.Type())
}
@ -243,14 +246,11 @@ func (el *HTMLElement) Unwrap() interface{} {
}
func (el *HTMLElement) Hash() uint64 {
el.mu.Lock()
defer el.mu.Unlock()
h := fnv.New64a()
h.Write([]byte(el.Type().String()))
h.Write([]byte(":"))
h.Write([]byte(el.innerHTML))
h.Write([]byte(strconv.Itoa(int(el.id.nodeID))))
return h.Sum64()
}
@ -724,209 +724,136 @@ func (el *HTMLElement) XPath(ctx context.Context, expression values.String) (res
}
}
func (el *HTMLElement) GetInnerText(ctx context.Context) values.String {
func (el *HTMLElement) GetInnerText(ctx context.Context) (values.String, error) {
val, err := el.innerText.Read(ctx)
if err != nil {
return values.EmptyString
return values.EmptyString, err
}
if val == values.None {
return values.EmptyString
return values.EmptyString, nil
}
return val.(values.String)
return val.(values.String), nil
}
func (el *HTMLElement) InnerTextBySelector(ctx context.Context, selector values.String) values.String {
func (el *HTMLElement) SetInnerText(ctx context.Context, innerText values.String) error {
if el.IsDetached() {
return values.EmptyString
return drivers.ErrDetached
}
// TODO: Can we use RemoteObjectID or BackendID instead of NodeId?
found, err := el.client.DOM.QuerySelector(ctx, dom.NewQuerySelectorArgs(el.id.nodeID, selector.String()))
el.innerText.Reset()
if err != nil {
el.logError(err).
Str("selector", selector.String()).
Msg("failed to retrieve a node by selector")
return values.EmptyString
}
if found.NodeID == emptyNodeID {
el.logError(err).
Str("selector", selector.String()).
Msg("failed to find a node by selector. returned 0 NodeID")
return values.EmptyString
}
childNodeID := found.NodeID
obj, err := el.client.DOM.ResolveNode(ctx, dom.NewResolveNodeArgs().SetNodeID(childNodeID))
if err != nil {
el.logError(err).
Int("childNodeID", int(childNodeID)).
Str("selector", selector.String()).
Msg("failed to resolve remote object for child el")
return values.EmptyString
}
if obj.Object.ObjectID == nil {
el.logError(err).
Int("childNodeID", int(childNodeID)).
Str("selector", selector.String()).
Msg("failed to resolve remote object for child el")
return values.EmptyString
}
objID := *obj.Object.ObjectID
text, err := el.exec.ReadProperty(ctx, objID, "innerText")
if err != nil {
el.logError(err).
Str("childObjectID", string(objID)).
Str("selector", selector.String()).
Msg("failed to load inner text for found child el")
return values.EmptyString
}
return values.NewString(text.String())
return setInnerText(ctx, el.client, el.exec, el.id, innerText)
}
func (el *HTMLElement) InnerTextBySelectorAll(ctx context.Context, selector values.String) *values.Array {
// TODO: Can we use RemoteObjectID or BackendID instead of NodeId?
res, err := el.client.DOM.QuerySelectorAll(ctx, dom.NewQuerySelectorAllArgs(el.id.nodeID, selector.String()))
if err != nil {
el.logError(err).
Str("selector", selector.String()).
Msg("failed to retrieve nodes by selector")
return values.NewArray(0)
}
arr := values.NewArray(len(res.NodeIDs))
for idx, id := range res.NodeIDs {
if id == emptyNodeID {
el.logError(err).
Str("selector", selector.String()).
Msg("failed to find a node by selector. returned 0 NodeID")
continue
}
obj, err := el.client.DOM.ResolveNode(ctx, dom.NewResolveNodeArgs().SetNodeID(id))
if err != nil {
el.logError(err).
Int("index", idx).
Int("childNodeID", int(id)).
Str("selector", selector.String()).
Msg("failed to resolve remote object for child el")
continue
}
if obj.Object.ObjectID == nil {
continue
}
objID := *obj.Object.ObjectID
text, err := el.exec.ReadProperty(ctx, objID, "innerText")
if err != nil {
el.logError(err).
Str("childObjectID", string(objID)).
Str("selector", selector.String()).
Msg("failed to load inner text for found child el")
continue
}
arr.Push(text)
}
return arr
}
func (el *HTMLElement) GetInnerHTML(_ context.Context) values.String {
el.mu.Lock()
defer el.mu.Unlock()
return el.innerHTML
}
func (el *HTMLElement) InnerHTMLBySelector(ctx context.Context, selector values.String) values.String {
func (el *HTMLElement) GetInnerTextBySelector(ctx context.Context, selector values.String) (values.String, error) {
if el.IsDetached() {
return values.EmptyString
return values.EmptyString, drivers.ErrDetached
}
// TODO: Can we use RemoteObjectID or BackendID instead of NodeId?
found, err := el.client.DOM.QuerySelector(ctx, dom.NewQuerySelectorArgs(el.id.nodeID, selector.String()))
out, err := el.exec.EvalWithValue(ctx, templates.GetInnerTextBySelector(selector.String()))
if err != nil {
el.logError(err).
Str("selector", selector.String()).
Msg("failed to retrieve nodes by selector")
return values.EmptyString
return values.EmptyString, err
}
text, err := loadInnerHTMLByNodeID(ctx, el.client, el.exec, found.NodeID)
if err != nil {
el.logError(err).
Str("selector", selector.String()).
Int("childNodeID", int(found.NodeID)).
Msg("failed to load inner HTML for found child el")
return values.EmptyString
}
return text
return values.NewString(out.String()), nil
}
func (el *HTMLElement) InnerHTMLBySelectorAll(ctx context.Context, selector values.String) *values.Array {
// TODO: Can we use RemoteObjectID or BackendID instead of NodeId?
selectorArgs := dom.NewQuerySelectorAllArgs(el.id.nodeID, selector.String())
res, err := el.client.DOM.QuerySelectorAll(ctx, selectorArgs)
func (el *HTMLElement) SetInnerTextBySelector(ctx context.Context, selector, innerText values.String) error {
if el.IsDetached() {
return drivers.ErrDetached
}
return el.exec.Eval(ctx, templates.SetInnerTextBySelector(selector.String(), innerText.String()))
}
func (el *HTMLElement) GetInnerTextBySelectorAll(ctx context.Context, selector values.String) (*values.Array, error) {
if el.IsDetached() {
return values.NewArray(0), drivers.ErrDetached
}
out, err := el.exec.EvalWithValue(ctx, templates.GetInnerTextBySelectorAll(selector.String()))
if err != nil {
el.logError(err).
Str("selector", selector.String()).
Msg("failed to retrieve nodes by selector")
return values.NewArray(0)
return values.NewArray(0), err
}
arr := values.NewArray(len(res.NodeIDs))
arr, ok := out.(*values.Array)
for _, id := range res.NodeIDs {
text, err := loadInnerHTMLByNodeID(ctx, el.client, el.exec, id)
if err != nil {
el.logError(err).
Str("selector", selector.String()).
Int("childNodeID", int(id)).
Msg("failed to load inner HTML for found child el")
// return what we have
return arr
}
arr.Push(text)
if !ok {
return values.NewArray(0), errors.New("unexpected output")
}
return arr
return arr, nil
}
func (el *HTMLElement) GetInnerHTML(ctx context.Context) (values.String, error) {
val, err := el.innerHTML.Read(ctx)
if err != nil {
return values.EmptyString, err
}
if val == values.None {
return values.EmptyString, nil
}
return val.(values.String), nil
}
func (el *HTMLElement) SetInnerHTML(ctx context.Context, innerHTML values.String) error {
if el.IsDetached() {
return drivers.ErrDetached
}
el.innerHTML.Reset()
return setInnerHTML(ctx, el.client, el.exec, el.id, innerHTML)
}
func (el *HTMLElement) GetInnerHTMLBySelector(ctx context.Context, selector values.String) (values.String, error) {
if el.IsDetached() {
return values.EmptyString, drivers.ErrDetached
}
out, err := el.exec.EvalWithValue(ctx, templates.GetInnerHTMLBySelector(selector.String()))
if err != nil {
return values.EmptyString, err
}
return values.NewString(out.String()), nil
}
func (el *HTMLElement) SetInnerHTMLBySelector(ctx context.Context, selector, innerHTML values.String) error {
if el.IsDetached() {
return drivers.ErrDetached
}
return el.exec.Eval(ctx, templates.SetInnerHTMLBySelector(selector.String(), innerHTML.String()))
}
func (el *HTMLElement) GetInnerHTMLBySelectorAll(ctx context.Context, selector values.String) (*values.Array, error) {
if el.IsDetached() {
return values.NewArray(0), drivers.ErrDetached
}
out, err := el.exec.EvalWithValue(ctx, templates.GetInnerHTMLBySelectorAll(selector.String()))
if err != nil {
return values.NewArray(0), err
}
arr, ok := out.(*values.Array)
if !ok {
return values.NewArray(0), errors.New("unexpected output")
}
return arr, nil
}
func (el *HTMLElement) CountBySelector(ctx context.Context, selector values.String) values.Int {
@ -1081,9 +1008,17 @@ func (el *HTMLElement) IsDetached() values.Boolean {
return !el.connected
}
func (el *HTMLElement) loadInnerHTML(ctx context.Context) (core.Value, error) {
if el.IsDetached() {
return values.None, drivers.ErrDetached
}
return getInnerHTML(ctx, el.client, el.exec, el.id, el.nodeType)
}
func (el *HTMLElement) loadInnerText(ctx context.Context) (core.Value, error) {
if !el.IsDetached() {
text, err := loadInnerText(ctx, el.client, el.exec, el.id, el.nodeType)
text, err := getInnerText(ctx, el.client, el.exec, el.id, el.nodeType)
if err == nil {
return text, nil
@ -1094,7 +1029,13 @@ func (el *HTMLElement) loadInnerText(ctx context.Context) (core.Value, error) {
// and just parse cached innerHTML
}
h := el.GetInnerHTML(ctx)
h, err := el.GetInnerHTML(ctx)
if err != nil {
el.logError(err).Msg("failed to get inner html from remote object")
return values.None, err
}
if h == values.EmptyString {
return h, nil
@ -1329,15 +1270,7 @@ func (el *HTMLElement) handleChildInserted(ctx context.Context, message interfac
loadedArr.Insert(values.NewInt(targetIDx), loadedEl)
newInnerHTML, err := loadInnerHTML(ctx, el.client, el.exec, el.id, el.nodeType)
if err != nil {
el.logError(err).Msg("failed to update element")
return
}
el.innerHTML = newInnerHTML
el.innerHTML.Reset()
el.innerText.Reset()
})
}
@ -1391,19 +1324,7 @@ func (el *HTMLElement) handleChildRemoved(ctx context.Context, message interface
loadedArr := v.(*values.Array)
loadedArr.RemoveAt(values.NewInt(targetIDx))
newInnerHTML, err := loadInnerHTML(ctx, el.client, el.exec, el.id, el.nodeType)
if err != nil {
el.logger.Error().
Timestamp().
Err(err).
Int("nodeID", int(el.id.nodeID)).
Msg("failed to update element")
return
}
el.innerHTML = newInnerHTML
el.innerHTML.Reset()
el.innerText.Reset()
})
}

View File

@ -3,6 +3,9 @@ package eval
import (
"context"
"fmt"
"github.com/MontFerret/ferret/pkg/drivers"
"github.com/mafredri/cdp/protocol/dom"
"strings"
"github.com/mafredri/cdp"
"github.com/mafredri/cdp/protocol/page"
@ -111,6 +114,24 @@ func (ec *ExecutionContext) CallMethod(
return &found.Result, nil
}
func (ec *ExecutionContext) ReadPropertyByNodeID(
ctx context.Context,
nodeID dom.NodeID,
propName string,
) (core.Value, error) {
obj, err := ec.client.DOM.ResolveNode(ctx, dom.NewResolveNodeArgs().SetNodeID(nodeID))
if err != nil {
return values.None, err
}
if obj.Object.ObjectID == nil {
return values.None, nil
}
return ec.ReadProperty(ctx, *obj.Object.ObjectID, propName)
}
func (ec *ExecutionContext) ReadProperty(
ctx context.Context,
objectID runtime.RemoteObjectID,
@ -254,11 +275,20 @@ func (ec *ExecutionContext) evalInternal(ctx context.Context, args *runtime.Eval
if out.ExceptionDetails != nil {
ex := out.ExceptionDetails
desc := *ex.Exception.Description
return runtime.RemoteObject{}, core.Error(
core.ErrUnexpected,
fmt.Sprintf("%s: %s", ex.Text, *ex.Exception.Description),
)
var err error
if strings.Contains(desc, drivers.ErrNotFound.Error()) {
err = drivers.ErrNotFound
} else {
err = core.Error(
core.ErrUnexpected,
fmt.Sprintf("%s: %s", ex.Text, desc),
)
}
return runtime.RemoteObject{}, err
}
return out.Result, nil

View File

@ -3,7 +3,9 @@ package cdp
import (
"bytes"
"context"
"encoding/json"
"errors"
"github.com/MontFerret/ferret/pkg/drivers/cdp/templates"
"golang.org/x/net/html"
"strings"
"time"
@ -66,7 +68,44 @@ func parseAttrs(attrs []string) *values.Object {
return res
}
func loadInnerHTML(ctx context.Context, client *cdp.Client, exec *eval.ExecutionContext, id HTMLElementIdentity, nodeType html.NodeType) (values.String, error) {
func setInnerHTML(ctx context.Context, client *cdp.Client, exec *eval.ExecutionContext, id HTMLElementIdentity, innerHTML values.String) error {
var objID *runtime.RemoteObjectID
if id.objectID != "" {
objID = &id.objectID
} else {
repl, err := client.DOM.ResolveNode(ctx, dom.NewResolveNodeArgs().SetNodeID(id.nodeID))
if err != nil {
return err
}
if repl.Object.ObjectID == nil {
return errors.New("unable to resolve node")
}
objID = repl.Object.ObjectID
}
b, err := json.Marshal(innerHTML.String())
if err != nil {
return err
}
_, err = exec.CallFunction(ctx, templates.SetInnerHTML(),
runtime.CallArgument{
ObjectID: objID,
},
runtime.CallArgument{
Value: json.RawMessage(b),
},
)
return err
}
func getInnerHTML(ctx context.Context, client *cdp.Client, exec *eval.ExecutionContext, id HTMLElementIdentity, nodeType html.NodeType) (values.String, error) {
// not a document
if nodeType != html.DocumentNode {
var objID runtime.RemoteObjectID
@ -105,19 +144,44 @@ func loadInnerHTML(ctx context.Context, client *cdp.Client, exec *eval.Execution
return values.NewString(repl.String()), nil
}
func loadInnerHTMLByNodeID(ctx context.Context, client *cdp.Client, exec *eval.ExecutionContext, nodeID dom.NodeID) (values.String, error) {
node, err := client.DOM.DescribeNode(ctx, dom.NewDescribeNodeArgs().SetNodeID(nodeID))
func setInnerText(ctx context.Context, client *cdp.Client, exec *eval.ExecutionContext, id HTMLElementIdentity, innerText values.String) error {
var objID *runtime.RemoteObjectID
if err != nil {
return values.EmptyString, err
if id.objectID != "" {
objID = &id.objectID
} else {
repl, err := client.DOM.ResolveNode(ctx, dom.NewResolveNodeArgs().SetNodeID(id.nodeID))
if err != nil {
return err
}
if repl.Object.ObjectID == nil {
return errors.New("unable to resolve node")
}
objID = repl.Object.ObjectID
}
return loadInnerHTML(ctx, client, exec, HTMLElementIdentity{
nodeID: nodeID,
}, common.ToHTMLType(node.Node.NodeType))
b, err := json.Marshal(innerText.String())
if err != nil {
return err
}
_, err = exec.CallFunction(ctx, templates.SetInnerText(),
runtime.CallArgument{
ObjectID: objID,
},
runtime.CallArgument{
Value: json.RawMessage(b),
},
)
return err
}
func loadInnerText(ctx context.Context, client *cdp.Client, exec *eval.ExecutionContext, id HTMLElementIdentity, nodeType html.NodeType) (values.String, error) {
func getInnerText(ctx context.Context, client *cdp.Client, exec *eval.ExecutionContext, id HTMLElementIdentity, nodeType html.NodeType) (values.String, error) {
// not a document
if nodeType != html.DocumentNode {
var objID runtime.RemoteObjectID
@ -156,18 +220,6 @@ func loadInnerText(ctx context.Context, client *cdp.Client, exec *eval.Execution
return values.NewString(repl.String()), nil
}
//func loadInnerTextByNodeID(ctx context.Context, client *cdp.Client, exec *eval.ExecutionContext, nodeID dom.NodeID) (values.String, error) {
// node, err := client.DOM.DescribeNode(ctx, dom.NewDescribeNodeArgs().SetNodeID(nodeID))
//
// if err != nil {
// return values.EmptyString, err
// }
//
// return loadInnerText(ctx, client, exec, HTMLElementIdentity{
// nodeID: nodeID,
// }, common.ToHTMLType(node.Node.NodeType))
//}
func parseInnerText(innerHTML string) (values.String, error) {
buff := bytes.NewBuffer([]byte(innerHTML))

View File

@ -0,0 +1,32 @@
package templates
import (
"fmt"
"github.com/MontFerret/ferret/pkg/drivers"
)
func GetInnerHTMLBySelector(selector string) string {
return fmt.Sprintf(`
const selector = "%s";
const found = document.querySelector(selector);
if (found == null) {
throw new Error('%s');
}
return found.innerHTML;
`, selector, drivers.ErrNotFound)
}
func GetInnerHTMLBySelectorAll(selector string) string {
return fmt.Sprintf(`
const selector = "%s";
const found = document.querySelectorAll(selector);
if (found == null) {
throw new Error('%s');
}
return Array.from(found).map(i => i.innerHTML);
`, selector, drivers.ErrNotFound)
}

View File

@ -0,0 +1,32 @@
package templates
import (
"fmt"
"github.com/MontFerret/ferret/pkg/drivers"
)
func GetInnerTextBySelector(selector string) string {
return fmt.Sprintf(`
const selector = "%s";
const found = document.querySelector(selector);
if (found == null) {
throw new Error('%s');
}
return found.innerText;
`, selector, drivers.ErrNotFound)
}
func GetInnerTextBySelectorAll(selector string) string {
return fmt.Sprintf(`
const selector = "%s";
const found = document.querySelectorAll(selector);
if (found == null) {
throw new Error('%s');
}
return Array.from(found).map(i => i.innerText);
`, selector, drivers.ErrNotFound)
}

View File

@ -0,0 +1,33 @@
package templates
import (
"fmt"
"github.com/MontFerret/ferret/pkg/drivers"
)
const setInnerHTMLTemplate = `
(element, value) => {
element.innerHTML = value;
}
`
func SetInnerHTML() string {
return setInnerHTMLTemplate
}
func SetInnerHTMLBySelector(selector, innerHTML string) string {
return fmt.Sprintf(`
const selector = "%s";
const found = document.querySelector(selector)
if (found == null) {
throw new Error('%s');
}
found.innerHTML = "%s"
`,
selector,
drivers.ErrNotFound,
innerHTML,
)
}

View File

@ -0,0 +1,33 @@
package templates
import (
"fmt"
"github.com/MontFerret/ferret/pkg/drivers"
)
const setInnerTextTemplate = `
(element, value) => {
element.innerText = value;
}
`
func SetInnerText() string {
return setInnerTextTemplate
}
func SetInnerTextBySelector(selector, innerText string) string {
return fmt.Sprintf(`
const selector = "%s";
const found = document.querySelector(selector)
if (found == null) {
throw new Error('%s');
}
found.innerText = "%s"
`,
selector,
drivers.ErrNotFound,
innerText,
)
}

View File

@ -127,9 +127,9 @@ func GetInDocument(ctx context.Context, doc drivers.HTMLDocument, path []core.Va
return GetInElement(ctx, el, path[1:])
case "innerHTML":
return doc.GetElement().GetInnerHTML(ctx), nil
return doc.GetElement().GetInnerHTML(ctx)
case "innerText":
return doc.GetElement().GetInnerText(ctx), nil
return doc.GetElement().GetInnerText(ctx)
default:
return GetInNode(ctx, doc.GetElement(), path)
}
@ -150,9 +150,9 @@ func GetInElement(ctx context.Context, el drivers.HTMLElement, path []core.Value
switch segment {
case "innerText":
return el.GetInnerText(ctx), nil
return el.GetInnerText(ctx)
case "innerHTML":
return el.GetInnerHTML(ctx), nil
return el.GetInnerHTML(ctx)
case "value":
return el.GetValue(ctx), nil
case "attributes":

View File

@ -29,6 +29,17 @@ func NewLazyValue(factory LazyValueFactory) *LazyValue {
return lz
}
func NewVolatileValue(value core.Value, factory LazyValueFactory) *LazyValue {
lz := NewLazyValue(factory)
if value != values.None {
lz.ready = true
lz.value = value
}
return lz
}
// Ready indicates whether the value is ready.
// @returns (Boolean) - Boolean value indicating whether the value is ready.
func (lv *LazyValue) Ready() bool {

8
pkg/drivers/errors.go Normal file
View File

@ -0,0 +1,8 @@
package drivers
import "github.com/pkg/errors"
var (
ErrDetached = errors.New("element detached")
ErrNotFound = errors.New("element(s) not found")
)

View File

@ -32,7 +32,7 @@ func NewHTMLElement(node *goquery.Selection) (drivers.HTMLElement, error) {
}
func (el *HTMLElement) MarshalJSON() ([]byte, error) {
return json.Marshal(el.GetInnerText(context.Background()).String())
return json.Marshal(el.String())
}
func (el *HTMLElement) Type() core.Type {
@ -40,7 +40,13 @@ func (el *HTMLElement) Type() core.Type {
}
func (el *HTMLElement) String() string {
return el.GetInnerHTML(context.Background()).String()
ih, err := el.GetInnerHTML(context.Background())
if err != nil {
return ""
}
return ih.String()
}
func (el *HTMLElement) Compare(other core.Value) int64 {
@ -48,10 +54,7 @@ func (el *HTMLElement) Compare(other core.Value) int64 {
case drivers.HTMLElementType:
other := other.(drivers.HTMLElement)
ctx, fn := drivers.WithDefaultTimeout(context.Background())
defer fn()
return el.GetInnerHTML(ctx).Compare(other.GetInnerHTML(ctx))
return int64(strings.Compare(el.String(), other.String()))
default:
return drivers.Compare(el.Type(), other.Type())
}
@ -129,18 +132,30 @@ func (el *HTMLElement) SetValue(_ context.Context, value core.Value) error {
return nil
}
func (el *HTMLElement) GetInnerText(_ context.Context) values.String {
return values.NewString(el.selection.Text())
func (el *HTMLElement) GetInnerText(_ context.Context) (values.String, error) {
return values.NewString(el.selection.Text()), nil
}
func (el *HTMLElement) GetInnerHTML(_ context.Context) values.String {
func (el *HTMLElement) SetInnerText(_ context.Context, innerText values.String) error {
el.selection.SetText(innerText.String())
return nil
}
func (el *HTMLElement) GetInnerHTML(_ context.Context) (values.String, error) {
h, err := el.selection.Html()
if err != nil {
return values.EmptyString
return values.EmptyString, err
}
return values.NewString(h)
return values.NewString(h), nil
}
func (el *HTMLElement) SetInnerHTML(_ context.Context, value values.String) error {
el.selection.SetHtml(value.String())
return nil
}
func (el *HTMLElement) GetStyles(ctx context.Context) (*values.Object, error) {
@ -356,42 +371,82 @@ func (el *HTMLElement) XPath(_ context.Context, expression values.String) (core.
}
}
func (el *HTMLElement) InnerHTMLBySelector(_ context.Context, selector values.String) values.String {
func (el *HTMLElement) SetInnerHTMLBySelector(_ context.Context, selector, innerHTML values.String) error {
selection := el.selection.Find(selector.String())
if selection == nil {
return drivers.ErrNotFound
}
selection.SetHtml(innerHTML.String())
return nil
}
func (el *HTMLElement) GetInnerHTMLBySelector(_ context.Context, selector values.String) (values.String, error) {
selection := el.selection.Find(selector.String())
if selection == nil {
return values.EmptyString, drivers.ErrNotFound
}
str, err := selection.Html()
// TODO: log error
if err != nil {
return values.EmptyString
return values.EmptyString, err
}
return values.NewString(str)
return values.NewString(str), nil
}
func (el *HTMLElement) InnerHTMLBySelectorAll(_ context.Context, selector values.String) *values.Array {
func (el *HTMLElement) GetInnerHTMLBySelectorAll(_ context.Context, selector values.String) (*values.Array, error) {
var err error
selection := el.selection.Find(selector.String())
arr := values.NewArray(selection.Length())
selection.Each(func(_ int, selection *goquery.Selection) {
str, err := selection.Html()
selection.EachWithBreak(func(_ int, selection *goquery.Selection) bool {
str, e := selection.Html()
// TODO: log error
if err == nil {
arr.Push(values.NewString(strings.TrimSpace(str)))
if e != nil {
err = e
return false
}
arr.Push(values.NewString(strings.TrimSpace(str)))
return true
})
return arr
if err != nil {
return values.NewArray(0), err
}
return arr, nil
}
func (el *HTMLElement) InnerTextBySelector(_ context.Context, selector values.String) values.String {
func (el *HTMLElement) GetInnerTextBySelector(_ context.Context, selector values.String) (values.String, error) {
selection := el.selection.Find(selector.String())
return values.NewString(selection.Text())
if selection == nil {
return values.EmptyString, drivers.ErrNotFound
}
return values.NewString(selection.Text()), nil
}
func (el *HTMLElement) InnerTextBySelectorAll(_ context.Context, selector values.String) *values.Array {
func (el *HTMLElement) SetInnerTextBySelector(_ context.Context, selector, innerText values.String) error {
selection := el.selection.Find(selector.String())
if selection == nil {
return drivers.ErrNotFound
}
selection.SetHtml(innerText.String())
return nil
}
func (el *HTMLElement) GetInnerTextBySelectorAll(_ context.Context, selector values.String) (*values.Array, error) {
selection := el.selection.Find(selector.String())
arr := values.NewArray(selection.Length())
@ -399,7 +454,7 @@ func (el *HTMLElement) InnerTextBySelectorAll(_ context.Context, selector values
arr.Push(values.NewString(selection.Text()))
})
return arr
return arr, nil
}
func (el *HTMLElement) CountBySelector(_ context.Context, selector values.String) values.Int {

View File

@ -349,7 +349,7 @@ func TestElement(t *testing.T) {
So(err, ShouldBeNil)
v := el.GetInnerText(context.Background())
v, _ := el.GetInnerText(context.Background())
So(v, ShouldEqual, "Ferret")
})
@ -376,8 +376,9 @@ func TestElement(t *testing.T) {
So(err, ShouldBeNil)
v := el.GetInnerHTML(context.Background())
v, err := el.GetInnerHTML(context.Background())
So(err, ShouldBeNil)
So(v, ShouldEqual, "<h2>Ferret</h2>")
})

View File

@ -49,9 +49,13 @@ type (
HTMLElement interface {
HTMLNode
GetInnerText(ctx context.Context) values.String
GetInnerText(ctx context.Context) (values.String, error)
GetInnerHTML(ctx context.Context) values.String
SetInnerText(ctx context.Context, innerText values.String) error
GetInnerHTML(ctx context.Context) (values.String, error)
SetInnerHTML(ctx context.Context, innerHTML values.String) error
GetValue(ctx context.Context) core.Value
@ -77,13 +81,17 @@ type (
RemoveAttribute(ctx context.Context, name ...values.String) error
InnerHTMLBySelector(ctx context.Context, selector values.String) values.String
GetInnerHTMLBySelector(ctx context.Context, selector values.String) (values.String, error)
InnerHTMLBySelectorAll(ctx context.Context, selector values.String) *values.Array
SetInnerHTMLBySelector(ctx context.Context, selector, innerHTML values.String) error
InnerTextBySelector(ctx context.Context, selector values.String) values.String
GetInnerHTMLBySelectorAll(ctx context.Context, selector values.String) (*values.Array, error)
InnerTextBySelectorAll(ctx context.Context, selector values.String) *values.Array
GetInnerTextBySelector(ctx context.Context, selector values.String) (values.String, error)
SetInnerTextBySelector(ctx context.Context, selector, innerText values.String) error
GetInnerTextBySelectorAll(ctx context.Context, selector values.String) (*values.Array, error)
Click(ctx context.Context) (values.Boolean, error)

View File

@ -13,7 +13,7 @@ import (
// @param doc (Open|GetElement) - Parent document or element.
// @param selector (String, optional) - String of CSS selector.
// @returns (String) - Inner HTML string if an element found, otherwise empty string.
func InnerHTML(ctx context.Context, args ...core.Value) (core.Value, error) {
func GetInnerHTML(ctx context.Context, args ...core.Value) (core.Value, error) {
err := core.ValidateArgs(args, 1, 2)
if err != nil {
@ -27,7 +27,7 @@ func InnerHTML(ctx context.Context, args ...core.Value) (core.Value, error) {
}
if len(args) == 1 {
return el.GetInnerHTML(ctx), nil
return el.GetInnerHTML(ctx)
}
err = core.ValidateType(args[1], types.String)
@ -38,5 +38,5 @@ func InnerHTML(ctx context.Context, args ...core.Value) (core.Value, error) {
selector := args[1].(values.String)
return el.InnerHTMLBySelector(ctx, selector), nil
return el.GetInnerHTMLBySelector(ctx, selector)
}

View File

@ -9,11 +9,11 @@ import (
"github.com/MontFerret/ferret/pkg/runtime/values/types"
)
// InnerHTMLAll returns an array of inner HTML strings of matched elements.
// GetInnerHTMLAll returns an array of inner HTML strings of matched elements.
// @param doc (HTMLDocument|HTMLElement) - Parent document or element.
// @param selector (String) - String of CSS selector.
// @returns (String) - An array of inner HTML strings if any element found, otherwise empty array.
func InnerHTMLAll(ctx context.Context, args ...core.Value) (core.Value, error) {
func GetInnerHTMLAll(ctx context.Context, args ...core.Value) (core.Value, error) {
err := core.ValidateArgs(args, 2, 2)
if err != nil {
@ -34,5 +34,5 @@ func InnerHTMLAll(ctx context.Context, args ...core.Value) (core.Value, error) {
selector := args[1].(values.String)
return el.InnerHTMLBySelectorAll(ctx, selector), nil
return el.GetInnerHTMLBySelectorAll(ctx, selector)
}

View File

@ -13,7 +13,7 @@ import (
// @param doc (HTMLDocument|HTMLElement) - Parent document or element.
// @param selector (String, optional) - String of CSS selector.
// @returns (String) - Inner text if an element found, otherwise empty string.
func InnerText(ctx context.Context, args ...core.Value) (core.Value, error) {
func GetInnerText(ctx context.Context, args ...core.Value) (core.Value, error) {
err := core.ValidateArgs(args, 1, 2)
if err != nil {
@ -27,7 +27,7 @@ func InnerText(ctx context.Context, args ...core.Value) (core.Value, error) {
}
if len(args) == 1 {
return el.GetInnerText(ctx), nil
return el.GetInnerText(ctx)
}
err = core.ValidateType(args[1], types.String)
@ -38,5 +38,5 @@ func InnerText(ctx context.Context, args ...core.Value) (core.Value, error) {
selector := args[1].(values.String)
return el.InnerTextBySelector(ctx, selector), nil
return el.GetInnerTextBySelector(ctx, selector)
}

View File

@ -9,11 +9,11 @@ import (
"github.com/MontFerret/ferret/pkg/runtime/values/types"
)
// InnerTextAll returns an array of inner text of matched elements.
// GetInnerTextAll returns an array of inner text of matched elements.
// @param doc (HTMLDocument|HTMLElement) - Parent document or element.
// @param selector (String) - String of CSS selector.
// @returns (String) - An array of inner text if any element found, otherwise empty array.
func InnerTextAll(ctx context.Context, args ...core.Value) (core.Value, error) {
func GetInnerTextAll(ctx context.Context, args ...core.Value) (core.Value, error) {
err := core.ValidateArgs(args, 2, 2)
if err != nil {
@ -34,5 +34,5 @@ func InnerTextAll(ctx context.Context, args ...core.Value) (core.Value, error) {
selector := args[1].(values.String)
return el.InnerTextBySelectorAll(ctx, selector), nil
return el.GetInnerTextBySelectorAll(ctx, selector)
}

View File

@ -29,10 +29,12 @@ func NewLib() map[string]core.Function {
"ELEMENTS": Elements,
"ELEMENTS_COUNT": ElementsCount,
"HOVER": Hover,
"INNER_HTML": InnerHTML,
"INNER_HTML_ALL": InnerHTMLAll,
"INNER_TEXT": InnerText,
"INNER_TEXT_ALL": InnerTextAll,
"INNER_HTML": GetInnerHTML,
"INNER_HTML_SET": SetInnerHTML,
"INNER_HTML_ALL": GetInnerHTMLAll,
"INNER_TEXT": GetInnerText,
"INNER_TEXT_SET": SetInnerText,
"INNER_TEXT_ALL": GetInnerTextAll,
"INPUT": Input,
"MOUSE": MouseMoveXY,
"NAVIGATE": Navigate,

View File

@ -0,0 +1,55 @@
package html
import (
"context"
"github.com/MontFerret/ferret/pkg/drivers"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/MontFerret/ferret/pkg/runtime/values/types"
)
// SetInnerHTML sets inner HTML string to a given or matched by CSS selector element
// @param doc (Open|GetElement) - Parent document or element.
// @param selector (String, optional) - String of CSS selector.
// @param innerHTML (String) - String of inner HTML.
func SetInnerHTML(ctx context.Context, args ...core.Value) (core.Value, error) {
err := core.ValidateArgs(args, 2, 3)
if err != nil {
return values.None, err
}
el, err := drivers.ToElement(args[0])
if err != nil {
return values.None, err
}
if len(args) == 2 {
err := core.ValidateType(args[1], types.String)
if err != nil {
return values.None, err
}
return values.None, el.SetInnerHTML(ctx, values.ToString(args[1]))
}
err = core.ValidateType(args[1], types.String)
if err != nil {
return values.None, err
}
err = core.ValidateType(args[2], types.String)
if err != nil {
return values.None, err
}
selector := values.ToString(args[1])
innerHTML := values.ToString(args[2])
return values.None, el.SetInnerHTMLBySelector(ctx, selector, innerHTML)
}

View File

@ -0,0 +1,55 @@
package html
import (
"context"
"github.com/MontFerret/ferret/pkg/drivers"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/MontFerret/ferret/pkg/runtime/values/types"
)
// SetInnerText sets inner text string to a given or matched by CSS selector element
// @param doc (Open|GetElement) - Parent document or element.
// @param selector (String, optional) - String of CSS selector.
// @param innerText (String) - String of inner text.
func SetInnerText(ctx context.Context, args ...core.Value) (core.Value, error) {
err := core.ValidateArgs(args, 2, 3)
if err != nil {
return values.None, err
}
el, err := drivers.ToElement(args[0])
if err != nil {
return values.None, err
}
if len(args) == 2 {
err := core.ValidateType(args[1], types.String)
if err != nil {
return values.None, err
}
return values.None, el.SetInnerText(ctx, values.ToString(args[1]))
}
err = core.ValidateType(args[1], types.String)
if err != nil {
return values.None, err
}
err = core.ValidateType(args[2], types.String)
if err != nil {
return values.None, err
}
selector := values.ToString(args[1])
innerHTML := values.ToString(args[2])
return values.None, el.SetInnerTextBySelector(ctx, selector, innerHTML)
}