diff --git a/browser.go b/browser.go index 281a597b..3356b9d6 100644 --- a/browser.go +++ b/browser.go @@ -10,6 +10,7 @@ package rod import ( "context" "reflect" + "strings" "sync" "time" @@ -459,6 +460,16 @@ func (b *Browser) pageInfo(id proto.TargetTargetID) (*proto.TargetTargetInfo, er return res.TargetInfo, nil } +func (b *Browser) isHeadless() (enabled bool) { + res, _ := proto.BrowserGetBrowserCommandLine{}.Call(b) + for _, v := range res.Arguments { + if strings.Contains(v, "headless") { + return true + } + } + return false +} + // IgnoreCertErrors switch. If enabled, all certificate errors will be ignored. func (b *Browser) IgnoreCertErrors(enable bool) error { return proto.SecuritySetIgnoreCertificateErrors{Ignore: enable}.Call(b) diff --git a/lib/js/helper.go b/lib/js/helper.go index 54cc568c..50c7995d 100644 --- a/lib/js/helper.go +++ b/lib/js/helper.go @@ -25,14 +25,14 @@ var ElementX = &Function{ // ElementsX ... var ElementsX = &Function{ Name: "elementsX", - Definition: `function(e){var t,n=functions.selectable(this);const i=document.evaluate(e,n,null,XPathResult.ORDERED_NODE_ITERATOR_TYPE),r=[];for(;t=i.iterateNext();)r.push(t);return r}`, + Definition: `function(e){var t,n=functions.selectable(this);const r=document.evaluate(e,n,null,XPathResult.ORDERED_NODE_ITERATOR_TYPE),i=[];for(;t=r.iterateNext();)i.push(t);return i}`, Dependencies: []*Function{Selectable}, } // ElementR ... var ElementR = &Function{ Name: "elementR", - Definition: `function(e,t){var n=t.match(/(\/?)(.+)\1([a-z]*)/i),i=n[3]&&!/^(?!.*?(.).*?\1)[gmixXsuUAJ]+$/.test(n[3])?new RegExp(t):new RegExp(n[2],n[3]);const r=functions.selectable(this);e=Array.from(r.querySelectorAll(e)).find(e=>i.test(functions.text.call(e)));return e||null}`, + Definition: `function(e,t){var n=t.match(/(\/?)(.+)\1([a-z]*)/i),r=n[3]&&!/^(?!.*?(.).*?\1)[gmixXsuUAJ]+$/.test(n[3])?new RegExp(t):new RegExp(n[2],n[3]);const i=functions.selectable(this);e=Array.from(i.querySelectorAll(e)).find(e=>r.test(functions.text.call(e)));return e||null}`, Dependencies: []*Function{Selectable, Text}, } @@ -53,14 +53,14 @@ var ContainsElement = &Function{ // InitMouseTracer ... var InitMouseTracer = &Function{ Name: "initMouseTracer", - Definition: `async function(e,t){if(await functions.waitLoad(),!document.getElementById(e)){const n=document.createElement("div");n.innerHTML=t;const i=n.lastChild;i.id=e,i.style="position: absolute; z-index: 2147483647; width: 17px; pointer-events: none;",i.removeAttribute("width"),i.removeAttribute("height"),document.body.parentElement.appendChild(i)}}`, + Definition: `async function(e,t){if(await functions.waitLoad(),!document.getElementById(e)){const n=document.createElement("div");n.innerHTML=t;const r=n.lastChild;r.id=e,r.style="position: absolute; z-index: 2147483647; width: 17px; pointer-events: none;",r.removeAttribute("width"),r.removeAttribute("height"),document.body.parentElement.appendChild(r)}}`, Dependencies: []*Function{WaitLoad}, } // UpdateMouseTracer ... var UpdateMouseTracer = &Function{ Name: "updateMouseTracer", - Definition: `function(e,t,n){const i=document.getElementById(e);return!!i&&(i.style.left=t-2+"px",i.style.top=n-3+"px",!0)}`, + Definition: `function(e,t,n){const r=document.getElementById(e);return!!r&&(r.style.left=t-2+"px",r.style.top=n-3+"px",!0)}`, Dependencies: []*Function{}, } @@ -74,22 +74,22 @@ var Rect = &Function{ // Overlay ... var Overlay = &Function{ Name: "overlay", - Definition: `async function(e,t,n,i,r,o){await functions.waitLoad();const s=document.createElement("div");if(s.id=e,s.style=` + "`" + `position: fixed; z-index:2147483647; border: 2px dashed red; + Definition: `async function(e,t,n,r,i,o){await functions.waitLoad();const s=document.createElement("div");if(s.id=e,s.style=` + "`" + `position: fixed; z-index:2147483647; border: 2px dashed red; border-radius: 3px; box-shadow: #5f3232 0 0 3px; pointer-events: none; box-sizing: border-box; left: ${t}px; top: ${n}px; - height: ${r}px; - width: ${i}px;` + "`" + `,i*r==0&&(s.style.border="none"),o){const a=document.createElement("div");a.style=` + "`" + `position: absolute; color: #cc26d6; font-size: 12px; background: #ffffffeb; + height: ${i}px; + width: ${r}px;` + "`" + `,r*i==0&&(s.style.border="none"),o){const a=document.createElement("div");a.style=` + "`" + `position: absolute; color: #cc26d6; font-size: 12px; background: #ffffffeb; box-shadow: #333 0 0 3px; padding: 2px 5px; border-radius: 3px; white-space: nowrap; - top: ${r}px;` + "`" + `,a.innerHTML=o,s.appendChild(a),document.body.parentElement.appendChild(s),window.innerHeight{const e=document.getElementById(n);var t;null!==e&&(t=r.getBoundingClientRect(),o.left===t.left&&o.top===t.top&&o.width===t.width&&o.height===t.height||(e.style.left=t.left+"px",e.style.top=t.top+"px",e.style.width=t.width+"px",e.style.height=t.height+"px",o=t),setTimeout(s,i))};setTimeout(s,i)}`, + Definition: `async function(n,e){const r=100,i=functions.tag(this);let o=i.getBoundingClientRect();await functions.overlay(n,o.left,o.top,o.width,o.height,e);const s=()=>{const e=document.getElementById(n);var t;null!==e&&(t=i.getBoundingClientRect(),o.left===t.left&&o.top===t.top&&o.width===t.width&&o.height===t.height||(e.style.left=t.left+"px",e.style.top=t.top+"px",e.style.width=t.width+"px",e.style.height=t.height+"px",o=t),setTimeout(s,r))};setTimeout(s,r)}`, Dependencies: []*Function{Tag, Overlay}, } @@ -124,7 +124,7 @@ var InputEvent = &Function{ // InputTime ... var InputTime = &Function{ Name: "inputTime", - Definition: `function(e){const t=new Date(e);var e=e=>e.toString().padStart(2,"0"),n=t.getFullYear(),i=e(t.getMonth()+1),r=e(t.getDate()),o=e(t.getHours()),s=e(t.getMinutes());switch(this.type){case"date":this.value=n+` + "`" + `-${i}-` + "`" + `+r;break;case"datetime-local":this.value=n+` + "`" + `-${i}-${r}T${o}:` + "`" + `+s;break;case"month":this.value=i;break;case"time":this.value=o+":"+s}functions.inputEvent.call(this)}`, + Definition: `function(e){const t=new Date(e);var e=e=>e.toString().padStart(2,"0"),n=t.getFullYear(),r=e(t.getMonth()+1),i=e(t.getDate()),o=e(t.getHours()),s=e(t.getMinutes());switch(this.type){case"date":this.value=n+` + "`" + `-${r}-` + "`" + `+i;break;case"datetime-local":this.value=n+` + "`" + `-${r}-${i}T${o}:` + "`" + `+s;break;case"month":this.value=r;break;case"time":this.value=o+":"+s}functions.inputEvent.call(this)}`, Dependencies: []*Function{InputEvent}, } @@ -135,6 +135,13 @@ var SelectText = &Function{ Dependencies: []*Function{}, } +// TriggerFavicon ... +var TriggerFavicon = &Function{ + Name: "triggerFavicon", + Definition: `function(){return new Promise((e,t)=>{var n=document.querySelector("link[rel~=icon]"),n=n&&n.href||"/favicon.ico",n=new URL(n,window.location).toString();const r=new XMLHttpRequest;r.open("GET",n),r.ontimeout=function(){t({errorType:"timeout_error",xhr:r})},r.onreadystatechange=function(){4===r.readyState&&(200<=r.status&&r.status<300||304===r.status?e(r.responseText):t({errorType:"status_error",xhr:r,status:r.status,statusText:r.statusText,responseText:r.responseText}))},r.onerror=function(){t({errorType:"onerror",xhr:r,status:r.status,statusText:r.statusText,responseText:r.responseText})},r.send()})}`, + Dependencies: []*Function{}, +} + // SelectAllText ... var SelectAllText = &Function{ Name: "selectAllText", @@ -145,7 +152,7 @@ var SelectAllText = &Function{ // Select ... var Select = &Function{ Name: "select", - Definition: `function(e,n,t){let i;switch(t){case"regex":i=e.map(e=>{const t=new RegExp(e);return e=>t.test(e.innerText)});break;case"css-selector":i=e.map(t=>e=>e.matches(t));break;default:i=e.map(t=>e=>e.innerText.includes(t))}const r=Array.from(this.options);let o=!1;return i.forEach(e=>{const t=r.find(e);t&&(t.selected=n,o=!0)}),this.dispatchEvent(new Event("input",{bubbles:!0})),this.dispatchEvent(new Event("change",{bubbles:!0})),o}`, + Definition: `function(e,n,t){let r;switch(t){case"regex":r=e.map(e=>{const t=new RegExp(e);return e=>t.test(e.innerText)});break;case"css-selector":r=e.map(t=>e=>e.matches(t));break;default:r=e.map(t=>e=>e.innerText.includes(t))}const i=Array.from(this.options);let o=!1;return r.forEach(e=>{const t=i.find(e);t&&(t.selected=n,o=!0)}),this.dispatchEvent(new Event("input",{bubbles:!0})),this.dispatchEvent(new Event("change",{bubbles:!0})),o}`, Dependencies: []*Function{}, } @@ -180,14 +187,14 @@ var Resource = &Function{ // AddScriptTag ... var AddScriptTag = &Function{ Name: "addScriptTag", - Definition: `function(i,r,o){if(!document.getElementById(i))return new Promise((e,t)=>{var n=document.createElement("script");r?(n.src=r,n.onload=e):(n.type="text/javascript",n.text=o,e()),n.id=i,n.onerror=t,document.head.appendChild(n)})}`, + Definition: `function(r,i,o){if(!document.getElementById(r))return new Promise((e,t)=>{var n=document.createElement("script");i?(n.src=i,n.onload=e):(n.type="text/javascript",n.text=o,e()),n.id=r,n.onerror=t,document.head.appendChild(n)})}`, Dependencies: []*Function{}, } // AddStyleTag ... var AddStyleTag = &Function{ Name: "addStyleTag", - Definition: `function(i,r,o){if(!document.getElementById(i))return new Promise((e,t)=>{var n;r?((n=document.createElement("link")).rel="stylesheet",n.href=r):((n=document.createElement("style")).type="text/css",n.appendChild(document.createTextNode(o)),e()),n.id=i,n.onload=e,n.onerror=t,document.head.appendChild(n)})}`, + Definition: `function(r,i,o){if(!document.getElementById(r))return new Promise((e,t)=>{var n;i?((n=document.createElement("link")).rel="stylesheet",n.href=i):((n=document.createElement("style")).type="text/css",n.appendChild(document.createTextNode(o)),e()),n.id=r,n.onload=e,n.onerror=t,document.head.appendChild(n)})}`, Dependencies: []*Function{}, } @@ -208,13 +215,13 @@ var Tag = &Function{ // ExposeFunc ... var ExposeFunc = &Function{ Name: "exposeFunc", - Definition: `function(e,t){let o=0;window[e]=e=>new Promise((n,i)=>{const r=t+"_cb"+o++;window[r]=(e,t)=>{delete window[r],t?i(t):n(e)},window[t](JSON.stringify({req:e,cb:r}))})}`, + Definition: `function(e,t){let o=0;window[e]=e=>new Promise((n,r)=>{const i=t+"_cb"+o++;window[i]=(e,t)=>{delete window[i],t?r(t):n(e)},window[t](JSON.stringify({req:e,cb:i}))})}`, Dependencies: []*Function{}, } // GetXPath ... var GetXPath = &Function{ Name: "getXPath", - Definition: `function(e){class r{constructor(e,t){this.value=e,this.optimized=t||!1}toString(){return this.value}}function o(t){function n(e,t){return e===t||(e.nodeType===Node.ELEMENT_NODE&&t.nodeType===Node.ELEMENT_NODE?e.localName===t.localName:e.nodeType===t.nodeType||(e.nodeType===Node.CDATA_SECTION_NODE?Node.TEXT_NODE:e.nodeType)===(t.nodeType===Node.CDATA_SECTION_NODE?Node.TEXT_NODE:t.nodeType))}var e=t.parentNode,i=e?e.children:null;if(!i)return 0;let r;for(let e=0;e { + const faviconElement = document.querySelector('link[rel~=icon]') + const href = (faviconElement && faviconElement.href) || '/favicon.ico' + const faviconUrl = new URL(href, window.location).toString() + const xhr = new XMLHttpRequest() + xhr.open('GET', faviconUrl) + + xhr.ontimeout = function () { + reject({ + errorType: 'timeout_error', + xhr: xhr + }) + } + + xhr.onreadystatechange = function () { + if (xhr.readyState === 4) { + if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) { + resolve(xhr.responseText) + } else { + reject({ + errorType: 'status_error', + xhr: xhr, + status: xhr.status, + statusText: xhr.statusText, + responseText: xhr.responseText + }) + } + } + } + + xhr.onerror = function () { + reject({ + errorType: 'onerror', + xhr: xhr, + status: xhr.status, + statusText: xhr.statusText, + responseText: xhr.responseText + }) + } + xhr.send() + }) + }, + selectAllText() { this.select() }, diff --git a/must.go b/must.go index 7bd11e0d..be9e6907 100644 --- a/must.go +++ b/must.go @@ -372,6 +372,12 @@ func (p *Page) MustCaptureDOMSnapshot() (domSnapshot *proto.DOMSnapshotCaptureSn return domSnapshot } +// MustTriggerFavicon is similar to TriggerFavicon. +func (p *Page) MustTriggerFavicon() *Page { + p.e(p.TriggerFavicon()) + return p +} + // MustScreenshotFullPage is similar to ScreenshotFullPage. // If the toFile is "", it Page.will save output to "tmp/screenshots" folder, time as the file name. func (p *Page) MustScreenshotFullPage(toFile ...string) []byte { diff --git a/page.go b/page.go index ad6e50d4..eab19dfa 100644 --- a/page.go +++ b/page.go @@ -350,6 +350,26 @@ func (p *Page) Close() error { return nil } +// TriggerFavicon supports when browser in headless mode +// to trigger favicon's request. Pay attention to this +// function only supported when browser in headless mode, +// if you call it in no-headless mode, it will raise an error +// with the message "browser is no-headless". +func (p *Page) TriggerFavicon() error { + + // check if browser whether in headless mode + // if not in headless mode then raise error + if !p.browser.isHeadless() { + return errors.New("browser is no-headless") + } + + _, err := p.Evaluate(evalHelper(js.TriggerFavicon).ByPromise()) + if err != nil { + return err + } + return nil +} + // HandleDialog accepts or dismisses next JavaScript initiated dialog (alert, confirm, prompt, or onbeforeunload). // Because modal dialog will block js, usually you have to trigger the dialog in another goroutine. // For example: diff --git a/page_test.go b/page_test.go index caa3eac0..cb27eacc 100644 --- a/page_test.go +++ b/page_test.go @@ -3,11 +3,13 @@ package rod_test import ( "bytes" "context" + "github.com/go-rod/rod/lib/launcher" "image/png" "math" "net/http" "os" "path/filepath" + "runtime" "sort" "sync" "testing" @@ -887,6 +889,55 @@ func TestPageElementFromObjectErr(t *testing.T) { g.Err(p.ElementFromObject(obj.Object)) } +func TestPageTriggerFavicon(t *testing.T) { + g := setup(t) + + // test browser in no-headless mode with an error + { + var l *launcher.Launcher + if runtime.GOOS == "darwin" { + l = launcher.New().Set("proxy-bypass-list", "<-loopback>").Headless(false) + } else { + l = launcher.New().Set("proxy-bypass-list", "<-loopback>").XVFB("--server-num=5", "--server-args=-screen 0 1600x900x16").Headless(false) + } + + browser := rod.New().ControlURL(l.MustLaunch()).MustConnect().MustIgnoreCertErrors(false).Context(g.Context()) + defer browser.MustClose() + + page := browser.MustPage("https://example.com") + err := page.TriggerFavicon() + g.Eq(err.Error(), "browser is no-headless") + } + + // test browser in headless mode to trigger favicon request + { + page := g.page + page.EnableDomain(proto.NetworkEnable{}) + defer page.DisableDomain(proto.NetworkDisable{})() + + page.MustNavigate("https://github.com") + page.MustWaitIdle() + go page.Context(g.Context()).EachEvent( + func(e *proto.NetworkRequestWillBeSent) { + if e.Request.URL == "https://github.githubassets.com/favicons/favicon.png" { + g.Eq(e.Request.URL, "https://github.githubassets.com/favicons/favicon.png") + } + }, + )() + page.MustTriggerFavicon() + } + + // test browser in headless mode to trigger favicon request with an error + { + g.Panic(func() { + p := g.page.MustNavigate("https://example.com") + g.mc.stubErr(1, proto.RuntimeCallFunctionOn{}) + p.MustTriggerFavicon() + }) + } + +} + func TestPageActionAfterClose(t *testing.T) { g := setup(t)