Chromedp: consider APIs to help deal with iframes

Created on 13 Jun 2017  ·  33Comments  ·  Source: chromedp/chromedp

Hi,
Is there any way to set value in iframe and submit?

help wanted

Most helpful comment

Targetting the iframe appears to be just a matter of getting a new context that is pointing at the iframe target. In Chromedp each page can have several targets. Each target has a type ("page" for the main page, "background_page" for Chrome extensions, and "iframe" for iframes).

You can get a list of the page targets by calling chromedp.Targets with your main context ctx, and this will return an array of *target.Info objects. The important fields in those objects are Type, Url, and TargetID. Since you may have multiple iframes on a single page, you will want to iterate through all the targets and get the one with Type iframe and a matching Url. Once you have that, you can use the TargetID to create a new context that targets the iframe.

You can create a new context from a parent context that will let you target subtargets within a page as long as you specify the ContextOption chromedp.WithTargetId along with the TargetID for the frame you are after. From the documentation, it looks like when you call chromedp.NewContext with a parent context but no ContextOption, it just creates a new tab in the same browser, but including WithTargetId when you call NewContext will let you access targets in the already loaded page.

Once you have the new context pointing at the iframe, you would use it just like you would the parent context.

Here's how I am getting an iframe context:

func getIframeContext(ctx context.Context, uriPart string) context.Context {
    targets, _ := chromedp.Targets(ctx)
    var tgt *target.Info
    for _, t := range targets {
        fmt.Println(t.Title, "|", t.Type, "|", t.URL, "|", t.TargetID)
        if t.Type == "iframe" && strings.Contains(t.URL, uriPart) {
            tgt = t
        }
    }
    if tgt != nil {
        ictx, _ := chromedp.NewContext(ctx, chromedp.WithTargetID(tgt.TargetID))
        return ictx
    }
    return nil
}

And here's how you would use it.

ictx := getIframeContext(ctx, "/substring/in/iframe/uri/")
selector := "a.LinkInIframe"
script := fmt.Sprintf("document.querySelector(\"%s\").href;", selector)
var b []byte
_ = chromedp.Run(
    ictx, // <-- instead of ctx
    chromedp.WaitVisible(selector, chromedp.ByQuery),
    chromedp.Evaluate(script, &b),
)
fmt.Println("href in iframe:", string(b))

Also, the --disable-web-security flag is NOT required for this to work.

All 33 comments

Please stay tuned to the codebase. There are new high level actions for Inspect'ing nodes that are forthcoming. Because they are a combination of low-level protocol API calls, and Javascript, we've held off on releasing them (also because they have not been production ready yet).

@kenshaw maybe there is any workaround that could be used now? Thank you!

@kenshaw: Thank you very much for the "chromedp". It's fantastic. I would like to know any progress on this issue. What is equivalent in chromedp, for switching to the iframe as compared to selenium.
selenium code to switch to iframe: "driver.switchTo().frame()"

@yogesh-desai if it's not yet implemented you always may try to use Javascript to access frames, but you would probably need to disable chrome security

@cleancodeninja: Could you please point me out to any link to understand it in detail? I am a newbie to web-domain so please feel free to suggest any required details/ input.
Also, you can check the google-doc to understand what I want to do. Thanks

Having worked with nightmare.js, I can tell you that accessing content in iframes is blocked in web browsers. Its a cross-site protection, and I've never found a way to disable it - though I haven't tried with the developer protocol.

A workaround is to get the source url of the given iframe and then navigate to it, now you have full access to the iframe's DOM.

@nelsonkhan navigate to iframe url would be a good approach. If you are using Chrome It's also possible to access iframes like you usually do it with "--disable-web-security" chrome command line flag.

@nelsonkhan : Thank you. Yes, after googling about iframes, I also found that "same domain policy" is for security and safety.
@dreadfulangel : I have used the --disable-web-security option and was able to handle iframes.

I understand that we can target the iframe with an Evaluate call, but that means we can't use all the nice functions like Click() that come with chromedp. Is there some way to set the default target frame in chromedp so that the iframe becomes the "root" of all operations?

We're working on a redesigned version of chromedp; among many changes and improvements, it will have proper support for iframes. See the refactor branch if you want to take a peek. It should be ready soon, but still needs quite a bit of work to be usable.

That is amazing @mvdan. Thank you!

An update: I don't know if it really ever will make sense to have chromedp.* actions deal with _all_ iframes. It's all "all or nothing" aspect, and trying to make a simpler API that allows "selection" of specific iframes is going to be more cumbersome than just doing direct CDP commands/queries.

@mvdan do you have any thoughts on what we should do here? I keep thinking about this, but I haven't come up with any stable API that would make any kind of sense within the context of the project.

I don't have a clear idea either. If we decide to not add new API, I think we should instead add docs and examples to explain how to deal with iframes.

something like this?

self.driver.switch_to.frame(frame_reference=self.driver.find_element_by_xpath(x‌​path="//iframe[@name='editor_body']"))

https://stackoverflow.com/questions/28814916/how-do-i-select-elements-inside-an-iframe-with-xpath

@pu369 -- we're open to suggestions, however what you linked to is a Selenium instance using the WebElement API, which is fundamentally different than driving Chrome raw. If I remember correctly, chromedriver did a whole lot of hacks that I'd rather avoid doing with chromedp. Please note, you can more-or-less do the above already (ie, load a iframe via a xpath query, and then switch to it), you just need to do it in two parts.

@kenshaw How would you go by doing that? Can't find any examples

//my ugly example to deal with iframes:

package main

import (
"context"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"

"strings"
"time"

"github.com/chromedp/chromedp"

)

func main() {
Testiframe()

}
func Testiframe() {
//static file server
wd, _ := os.Getwd()
http.Handle("/", http.FileServer(http.Dir(wd)))
http.Handle("/index", writeHTML(`


the content


)) go http.ListenAndServe(":9090", nil) framefile123 :=

clickme!

`
f123 := []byte(framefile123)
if err := ioutil.WriteFile("./123.html", f123, 0666); err != nil {
panic(err)
}

//show chrome
options := []chromedp.ExecAllocatorOption{
    chromedp.Flag("headless", false),
    chromedp.Flag("hide-scrollbars", false),
    chromedp.Flag("mute-audio", false),
    chromedp.UserAgent(`Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36`),
}
options = append(chromedp.DefaultExecAllocatorOptions[:], options...)

allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), options...)
defer cancel()
ctx, cancel := chromedp.NewContext(allocCtx)
defer cancel()
var outer, url, frameurl string
var ok bool
if err := chromedp.Run(ctx,
    chromedp.Navigate("http://localhost:9090/index"),
    chromedp.Sleep(2*time.Second),
    chromedp.WaitVisible("iframe", chromedp.ByQuery),
    chromedp.AttributeValue("iframe", "src", &outer, &ok),
    chromedp.Location(&url),
); err != nil {
    panic(err)
}
fmt.Println("Outer:", outer, " url:", url)
frameurl = url[0:strings.LastIndex(url, "/")] + "/" + outer
fmt.Println("frameurl", frameurl)
if err := chromedp.Run(ctx,
    chromedp.Navigate(frameurl),
    chromedp.Sleep(2*time.Second),
    chromedp.WaitVisible("#clickme"),
    chromedp.Click("#clickme", chromedp.ByID),
    chromedp.Sleep(2*time.Second),
); err != nil {
    panic(err)
}

}
func writeHTML(content string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
//utf-8
w.Header().Set("Content-Type", "text/html; charset=utf-8")
io.WriteString(w, strings.TrimSpace(content))
})
}

@pu369 -- we're open to suggestions, however what you linked to is a Selenium instance using the WebElement API, which is fundamentally different than driving Chrome raw. If I remember correctly, chromedriver did a whole lot of hacks that I'd rather avoid doing with chromedp. Please note, you can more-or-less do the above already (ie, load a iframe via a xpath query, and then switch to it), you just need to do it in two parts.

Hi,

I'm interested in this. How do you indeed go about doing the "switch to it" part ? Loading an iframe via an xpath query is simple enough, but I'm getting confused as to how one would switch to it, creating a new child context and such.

Edit for clarity: previous answer navigates to the frame while parsing the url attribute. If I simply want to, say, click the first input element inside an iframe in the page, how would I make my current context the iframe's before I can click and simply jump back to parent context ?

Targetting the iframe appears to be just a matter of getting a new context that is pointing at the iframe target. In Chromedp each page can have several targets. Each target has a type ("page" for the main page, "background_page" for Chrome extensions, and "iframe" for iframes).

You can get a list of the page targets by calling chromedp.Targets with your main context ctx, and this will return an array of *target.Info objects. The important fields in those objects are Type, Url, and TargetID. Since you may have multiple iframes on a single page, you will want to iterate through all the targets and get the one with Type iframe and a matching Url. Once you have that, you can use the TargetID to create a new context that targets the iframe.

You can create a new context from a parent context that will let you target subtargets within a page as long as you specify the ContextOption chromedp.WithTargetId along with the TargetID for the frame you are after. From the documentation, it looks like when you call chromedp.NewContext with a parent context but no ContextOption, it just creates a new tab in the same browser, but including WithTargetId when you call NewContext will let you access targets in the already loaded page.

Once you have the new context pointing at the iframe, you would use it just like you would the parent context.

Here's how I am getting an iframe context:

func getIframeContext(ctx context.Context, uriPart string) context.Context {
    targets, _ := chromedp.Targets(ctx)
    var tgt *target.Info
    for _, t := range targets {
        fmt.Println(t.Title, "|", t.Type, "|", t.URL, "|", t.TargetID)
        if t.Type == "iframe" && strings.Contains(t.URL, uriPart) {
            tgt = t
        }
    }
    if tgt != nil {
        ictx, _ := chromedp.NewContext(ctx, chromedp.WithTargetID(tgt.TargetID))
        return ictx
    }
    return nil
}

And here's how you would use it.

ictx := getIframeContext(ctx, "/substring/in/iframe/uri/")
selector := "a.LinkInIframe"
script := fmt.Sprintf("document.querySelector(\"%s\").href;", selector)
var b []byte
_ = chromedp.Run(
    ictx, // <-- instead of ctx
    chromedp.WaitVisible(selector, chromedp.ByQuery),
    chromedp.Evaluate(script, &b),
)
fmt.Println("href in iframe:", string(b))

Also, the --disable-web-security flag is NOT required for this to work.

@jerblack Now that is some quality answer. Thank you so very much, I'll go try this out right away. Happy new year :)

So the only way to do this is disable --disable-web-security and use evaluate? In my case iFrame is not loaded when disabling web security and it's not on the target list.
So is there some way to make new target from Frame ID for example?

Apologies all for the silence here. I realise this is a problem for many people and that the passing of time can be frustrating.

We recently added a bit of new API for https://github.com/chromedp/chromedp/issues/463, to enable running queries on a specific node instead of always running them on the document root node.

It just occurred to me that we could use the same API for iframes. That is, using the FromNode query option with an iframe node should then run the query on that iframe's document node.

I've implemented that in the comment above, and it works. You can take a look at TestQueryIframe for a working example: https://github.com/chromedp/chromedp/blob/49daeb65bff2056e97c1eaee79c8e924566ae675/nav_test.go#L278-L286

The API for this is a bit clunky; it requires two calls to chromedp.Run, which I'm not very happy about. I'll be thinking about a nicer way to do this at a high level. Any API ideas welcome, but please keep it concise and provide a short example.

Err, I made a mistake in that code above; WaitVisible doesn't seem to work, but WaitReady does work. I'll investigate that separately. In any case, the node selectors work, which was the main blocker.

Apologies all for the silence here. I realise this is a problem for many people and that the passing of time can be frustrating.

We recently added a bit of new API for #463, to enable running queries on a specific node instead of always running them on the document root node.

It just occurred to me that we could use the same API for iframes. That is, using the FromNode query option with an iframe node should then run the query on that iframe's document node.

I've implemented that in the comment above, and it works. You can take a look at TestQueryIframe for a working example:

https://github.com/chromedp/chromedp/blob/49daeb65bff2056e97c1eaee79c8e924566ae675/nav_test.go#L278-L286

The API for this is a bit clunky; it requires two calls to chromedp.Run, which I'm not very happy about. I'll be thinking about a nicer way to do this at a high level. Any API ideas welcome, but please keep it concise and provide a short example.

This is an great update, but some functions still not work with FromNode ,such as Click, right?

Yes, the basics work, but some of the extras like Click and WaitVisible do not - I assume some extra code to handle iframe coordinates is needed.

Yes, the basics work, but some of the extras like Click and WaitVisible do not

If you can't even click the element inside an iframe, I feel it's better to say "basically doesn't work".

Maybe you can borrow some ideas of how rod handles it, you can do all the page operations with iframe. Not just iframe, you have to handle shadow DOM separately too:

https://github.com/ysmood/rod/blob/d6517f762538ac739aa27232110095e7436e708b/element_test.go#L36-L40

in some iframe, None of the following methods worked, but I typed a JS expression in the browser's Console and it worked

chromedp.EvaluateAsDevTools
chromedp.Evaluate
runtime.Evaluate

@zhangguanzhang If you use rod it's like:

https://github.com/go-rod/rod/blob/e549401/examples_test.go#L67-L81

// Example_search shows how to use Search to get element inside nested iframes or shadow DOMs.
// It works the same as https://developers.google.com/web/tools/chrome-devtools/dom#search
func Example_search() {
    page := rod.New().Connect().Page("https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe")

    // get the code mirror editor inside the iframe and click it
    page.Search(".CodeMirror").Click()
}

How rod works is to use the ObjectID of the window element inside the iframe: https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#type-RemoteObjectId

Here is a comparison of the examples between rod and chromedp: https://github.com/go-rod/rod/tree/master/lib/examples/compare-chromedp

@ysmood nice project, I decide to use rod instead of chromedp

Ok, all queries now work with iframes; see the PR above. Please try it out and let me know how it works.

var iframes []*cdp.Node
if err := Run(ctx, Nodes(iframe, &iframes, ByQuery)); err != nil {
t.Fatal(err)
}
if err := Run(ctx,
WaitVisible(#form, ByID, FromNode(iframes[0])),
); err != nil {
t.Fatal(err)
}

How would you use this? I am having trouble implementing this into my app. I am trying to access a cc input field and would like to send the information to the field without leaving my current page.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Tallone picture Tallone  ·  4Comments

iMaxopoly picture iMaxopoly  ·  3Comments

shailendrayadav1 picture shailendrayadav1  ·  4Comments

jafriyie1 picture jafriyie1  ·  3Comments

AmrAlfoly picture AmrAlfoly  ·  3Comments