Skip to content

Commit

Permalink
feat: TriggerFavicon (#884)
Browse files Browse the repository at this point in the history
* feat: TriggerFavicon
1.support in headless mode to trigger favicon request.
2.when call it in no-headless mode will raise an error with message:browser is no-headless

* fix cli error

* fix cli error

* Update page_test.go

Co-authored-by: Yad Smood <ysmood@users.noreply.github.com>

* Update page_test.go

Co-authored-by: Yad Smood <ysmood@users.noreply.github.com>

* fix cli error

* remove favicon.ico

---------

Co-authored-by: Yad Smood <ysmood@users.noreply.github.com>
  • Loading branch information
Fly-Playgroud and ysmood authored Jun 6, 2023
1 parent 159b1ab commit 50b30c0
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 13 deletions.
11 changes: 11 additions & 0 deletions browser.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ package rod
import (
"context"
"reflect"
"strings"
"sync"
"time"

Expand Down Expand Up @@ -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)
Expand Down
33 changes: 20 additions & 13 deletions lib/js/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ var Element = &Function{
Dependencies: []*Function{Selectable},
}

// 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({status:r.status,statusText:r.statusText,responseText: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{},
}

// Elements ...
var Elements = &Function{
Name: "elements",
Expand All @@ -25,14 +32,14 @@ var ElementX = &Function{
// ElementsX ...
var ElementsX = &Function{
Name: "elementsX",
Definition: `function(e){for(var t,n=functions.selectable(this),i=document.evaluate(e,n,null,XPathResult.ORDERED_NODE_ITERATOR_TYPE),r=[];t=i.iterateNext();)r.push(t);return r}`,
Definition: `function(e){for(var t,n=functions.selectable(this),r=document.evaluate(e,n,null,XPathResult.ORDERED_NODE_ITERATOR_TYPE),i=[];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]),t=functions.selectable(this),n=Array.from(t.querySelectorAll(e)).find(e=>i.test(functions.text.call(e)));return n||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]),t=functions.selectable(this),n=Array.from(t.querySelectorAll(e)).find(e=>r.test(functions.text.call(e)));return n||null}`,
Dependencies: []*Function{Selectable, Text},
}

Expand Down Expand Up @@ -74,22 +81,22 @@ var Rect = &Function{
// Overlay ...
var Overlay = &Function{
Name: "overlay",
Definition: `async function(e,t,n,i,r,o){await functions.waitLoad();var s=document.createElement("div");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();var s=document.createElement("div");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?((e=document.createElement("div")).style=` + "`" + `position: absolute; color: #cc26d6; font-size: 12px; background: #ffffffeb;
height: ${i}px;
width: ${r}px;` + "`" + `,r*i==0&&(s.style.border="none"),o?((e=document.createElement("div")).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;` + "`" + `,e.innerHTML=o,s.appendChild(e),document.body.parentElement.appendChild(s),window.innerHeight<e.offsetHeight+n+r&&(e.style.top=-e.offsetHeight-2+"px"),window.innerWidth<e.offsetWidth+t&&(e.style.left=window.innerWidth-e.offsetWidth-t+"px")):document.body.parentElement.appendChild(s)}`,
top: ${i}px;` + "`" + `,e.innerHTML=o,s.appendChild(e),document.body.parentElement.appendChild(s),window.innerHeight<e.offsetHeight+n+i&&(e.style.top=-e.offsetHeight-2+"px"),window.innerWidth<e.offsetWidth+t&&(e.style.left=window.innerWidth-e.offsetWidth-t+"px")):document.body.parentElement.appendChild(s)}`,
Dependencies: []*Function{WaitLoad},
}

// ElementOverlay ...
var ElementOverlay = &Function{
Name: "elementOverlay",
Definition: `async function(n,e){const i=100,r=functions.tag(this);let o=r.getBoundingClientRect();await functions.overlay(n,o.left,o.top,o.width,o.height,e);const s=()=>{var e,t=document.getElementById(n);null!==t&&(e=r.getBoundingClientRect(),o.left===e.left&&o.top===e.top&&o.width===e.width&&o.height===e.height||(t.style.left=e.left+"px",t.style.top=e.top+"px",t.style.width=e.width+"px",t.style.height=e.height+"px",o=e),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=()=>{var e,t=document.getElementById(n);null!==t&&(e=i.getBoundingClientRect(),o.left===e.left&&o.top===e.top&&o.width===e.width&&o.height===e.height||(t.style.left=e.left+"px",t.style.top=e.top+"px",t.style.width=e.width+"px",t.style.height=e.height+"px",o=e),setTimeout(s,r))};setTimeout(s,r)}`,
Dependencies: []*Function{Tag, Overlay},
}

Expand Down Expand Up @@ -124,7 +131,7 @@ var InputEvent = &Function{
// InputTime ...
var InputTime = &Function{
Name: "inputTime",
Definition: `function(e){var e=new Date(e),t=e=>e.toString().padStart(2,"0"),n=e.getFullYear(),i=t(e.getMonth()+1),r=t(e.getDate()),o=t(e.getHours()),s=t(e.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){var e=new Date(e),t=e=>e.toString().padStart(2,"0"),n=e.getFullYear(),r=t(e.getMonth()+1),i=t(e.getDate()),o=t(e.getHours()),s=t(e.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},
}

Expand All @@ -145,7 +152,7 @@ var SelectAllText = &Function{
// Select ...
var Select = &Function{
Name: "select",
Definition: `function(e,t,n){let i;switch(n){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=>{e=r.find(e);e&&(e.selected=t,o=!0)}),this.dispatchEvent(new Event("input",{bubbles:!0})),this.dispatchEvent(new Event("change",{bubbles:!0})),o}`,
Definition: `function(e,t,n){let r;switch(n){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=>{e=i.find(e);e&&(e.selected=t,o=!0)}),this.dispatchEvent(new Event("input",{bubbles:!0})),this.dispatchEvent(new Event("change",{bubbles:!0})),o}`,
Dependencies: []*Function{},
}

Expand Down Expand Up @@ -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{},
}

Expand All @@ -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<i.length;++e)if(n(t,i[e])&&i[e]!==t){r=!0;break}if(!r)return 0;let o=1;for(let e=0;e<i.length;++e)if(n(t,i[e])){if(i[e]===t)return o;++o}return-1}if(this.nodeType===Node.DOCUMENT_NODE)return"/";var t=[];let n=this;for(;n;){var i=function(e,t){let n;var i=o(e);if(-1===i)return null;switch(e.nodeType){case Node.ELEMENT_NODE:if(t&&e.id)return new r(` + "`" + `//*[@id='${e.id}']` + "`" + `,!0);n=e.localName;break;case Node.ATTRIBUTE_NODE:n="@"+e.nodeName;break;case Node.TEXT_NODE:case Node.CDATA_SECTION_NODE:n="text()";break;case Node.PROCESSING_INSTRUCTION_NODE:n="processing-instruction()";break;case Node.COMMENT_NODE:n="comment()";break;default:Node.DOCUMENT_NODE;n=""}return 0<i&&(n+=` + "`" + `[${i}]` + "`" + `),new r(n,e.nodeType===Node.DOCUMENT_NODE)}(n,e);if(!i)break;if(t.push(i),i.optimized)break;n=n.parentNode}return t.reverse(),(t.length&&t[0].optimized?"":"/")+t.join("/")}`,
Definition: `function(e){class i{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,r=e?e.children:null;if(!r)return 0;let i;for(let e=0;e<r.length;++e)if(n(t,r[e])&&r[e]!==t){i=!0;break}if(!i)return 0;let o=1;for(let e=0;e<r.length;++e)if(n(t,r[e])){if(r[e]===t)return o;++o}return-1}if(this.nodeType===Node.DOCUMENT_NODE)return"/";var t=[];let n=this;for(;n;){var r=function(e,t){let n;var r=o(e);if(-1===r)return null;switch(e.nodeType){case Node.ELEMENT_NODE:if(t&&e.id)return new i(` + "`" + `//*[@id='${e.id}']` + "`" + `,!0);n=e.localName;break;case Node.ATTRIBUTE_NODE:n="@"+e.nodeName;break;case Node.TEXT_NODE:case Node.CDATA_SECTION_NODE:n="text()";break;case Node.PROCESSING_INSTRUCTION_NODE:n="processing-instruction()";break;case Node.COMMENT_NODE:n="comment()";break;default:Node.DOCUMENT_NODE;n=""}return 0<r&&(n+=` + "`" + `[${r}]` + "`" + `),new i(n,e.nodeType===Node.DOCUMENT_NODE)}(n,e);if(!r)break;if(t.push(r),r.optimized)break;n=n.parentNode}return t.reverse(),(t.length&&t[0].optimized?"":"/")+t.join("/")}`,
Dependencies: []*Function{},
}
48 changes: 48 additions & 0 deletions lib/js/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,54 @@ const functions = {
return s.querySelector(selector)
},

triggerFavicon() {
return new Promise((resolve, reject) => {
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({
status: xhr.status,
statusText: xhr.statusText,
responseText: 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()
})
},

elements(selector) {
return functions.selectable(this).querySelectorAll(selector)
},
Expand Down
6 changes: 6 additions & 0 deletions must.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
19 changes: 19 additions & 0 deletions page.go
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,25 @@ 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:
Expand Down
44 changes: 44 additions & 0 deletions page_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package rod_test
import (
"bytes"
"context"
"fmt"
"image/png"
"math"
"net/http"
Expand Down Expand Up @@ -892,6 +893,49 @@ func TestPageElementFromObjectErr(t *testing.T) {
g.Err(p.ElementFromObject(obj.Object))
}

func TestPageTriggerFavicon(t *testing.T) {
g := setup(t)
s := g.Serve()
// test browser in no-headless mode with an error
{
page := g.newPage()
page.MustNavigate(s.URL())
g.mc.stub(1, proto.BrowserGetBrowserCommandLine{}, func(send StubSend) (gson.JSON, error) {
commandLine := proto.BrowserGetBrowserCommandLineResult{Arguments: []string{""}}
return gson.New(commandLine), nil
})
err := page.TriggerFavicon()
g.Eq(err.Error(), "browser is no-headless")
}

// test browser in headless mode to trigger favicon request
{
faviconURL := fmt.Sprintf(s.HostURL.String(), "/favicon.ico")
s.Route("/test", "")
s.Route("/favicon.ico", filepath.FromSlash("./fixtures/icon.png"))
page := g.newPage()
page.MustNavigate(s.URL("/test"))
page.MustWaitIdle()
go page.Context(g.Context()).EachEvent(
func(e *proto.NetworkRequestWillBeSent) {
if e.Request.URL == faviconURL {
g.Eq(e.Request.URL, faviconURL)
}
},
)()
page.MustTriggerFavicon()
}

// test browser in headless mode to trigger favicon request with an error
{
p := g.newPage().MustNavigate(s.URL())
g.mc.stubErr(1, proto.RuntimeCallFunctionOn{})
g.Panic(func() {
p.MustTriggerFavicon()
})
}
}

func TestPageActionAfterClose(t *testing.T) {
g := setup(t)

Expand Down

0 comments on commit 50b30c0

Please sign in to comment.