Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support for Shadow DOM methods #293

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions internal/seleniumtest/seleniumtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ func RunCommonTests(t *testing.T, c Config) {
t.Run("PageSource", runTest(testPageSource, c))
t.Run("FindElement", runTest(testFindElement, c))
t.Run("FindElements", runTest(testFindElements, c))
t.Run("TestShadowDOM", runTest(testShadowDOM, c))
t.Run("SendKeys", runTest(testSendKeys, c))
t.Run("Click", runTest(testClick, c))
t.Run("GetCookies", runTest(testGetCookies, c))
Expand Down Expand Up @@ -567,6 +568,43 @@ func testFindElements(t *testing.T, c Config) {
evaluateElement(t, wd, elems[0])
}

func testShadowDOM(t *testing.T, c Config) {
wd := newRemote(t, newTestCapabilities(t, c), c)
defer quitRemote(t, wd)

if err := wd.Get(c.ServerURL + "/shadow"); err != nil {
t.Fatalf("wd.Get(%q) returned error: %v", c.ServerURL, err)
}

we, err := wd.FindElement(selenium.ByID, "host-element")
if err != nil {
t.Fatalf("wd.FindElement('id', 'host-element') failed to obtain the host element of the shadow DOM: %s", err)
}

sr, err := we.GetElementShadowRoot()
if err != nil {
t.Fatalf("we.GetElementShadowRoot() failed to obtain the shadow root element: %s", err)
}

swe, err := sr.FindElement(selenium.ByCSSSelector, "button[id='shadow-button']")
if err != nil {
t.Fatalf("sr.FindElement('css selector', 'button['id=\\'shadow-button\\']) failed to obtain the shadow DOM element: %s", err)
}

if swe == nil {
t.Fatalf("obtained element from shadow DOM is null")
}

swes, err := sr.FindElements(selenium.ByCSSSelector, "button")
if err != nil {
t.Fatalf("sr.FindElements('css selector', 'button) failed to obtain the shadow DOM elements: %s", err)
}

if swes == nil || len(swes) != 2 {
t.Fatalf("could not obtained all elements from shadow DOM")
}
}

func testSendKeys(t *testing.T, c Config) {
wd := newRemote(t, newTestCapabilities(t, c), c)
defer quitRemote(t, wd)
Expand Down Expand Up @@ -1604,6 +1642,24 @@ var alertPage = `
</html>
`

var shadowDOMPage = `
<html>
<head>
<title>Go Selenium Test Suite - Shadow DOM Page</title>
</head>
<body>
This page contains a Shadow DOM.

<div id="host-element">
<template shadowroot="open">
<button id="shadow-button"/>
<button id="another-shadow-button"/>
</template>
</div>
</body>
</html>
`

var Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
page, ok := map[string]string{
Expand All @@ -1614,6 +1670,7 @@ var Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
"/frame": framePage,
"/title": titleChangePage,
"/alert": alertPage,
"/shadow": shadowDOMPage,
}[path]
if !ok {
http.NotFound(w, r)
Expand Down
72 changes: 72 additions & 0 deletions remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,15 @@ func (wd *remoteWD) boolCommand(urlTemplate string) (bool, error) {
return reply.Value, nil
}

func (wd *remoteWD) shadowRootCommand(urlTemplate string) (ShadowRoot, error) {
url := wd.requestURL(urlTemplate, wd.id)
response, err := wd.execute("GET", url, nil)
if err != nil {
return nil, err
}
return wd.DecodeShadowRoot(response)
}

func (wd *remoteWD) Status() (*Status, error) {
url := wd.requestURL("/status")
reply, err := wd.execute("GET", url, nil)
Expand Down Expand Up @@ -689,6 +698,39 @@ func (wd *remoteWD) find(by, value, suffix, url string) ([]byte, error) {
return wd.execute("POST", wd.requestURL(url+suffix, wd.id), data)
}

func (wd *remoteWD) DecodeShadowRoot(data []byte) (ShadowRoot, error) {
reply := new(struct{ Value map[string]string })
if err := json.Unmarshal(data, &reply); err != nil {
return nil, err
}

id := shadowRootIDFromValue(reply.Value)
if id == "" {
return nil, fmt.Errorf("invalid shadow root returned: %+v", reply)
}
return &remoteSR{
parent: wd,
id: id,
}, nil
}

const (
// shadowIdentifier is the string constant defined by the W3C
// specification that is the key for the map that contains a unique shadow root identifier.
shadowRootIdentifier = "shadow-6066-11e4-a52e-4f735466cecf"
)

func shadowRootIDFromValue(v map[string]string) string {
for _, key := range []string{shadowRootIdentifier} {
v, ok := v[key]
if !ok || v == "" {
continue
}
return v
}
return ""
}

func (wd *remoteWD) DecodeElement(data []byte) (WebElement, error) {
reply := new(struct{ Value map[string]string })
if err := json.Unmarshal(data, &reply); err != nil {
Expand Down Expand Up @@ -1440,6 +1482,11 @@ func (elem *remoteWE) FindElements(by, value string) ([]WebElement, error) {
return elem.parent.DecodeElements(response)
}

func (elem *remoteWE) GetElementShadowRoot() (ShadowRoot, error) {
url := fmt.Sprintf("/session/%%s/element/%s/shadow", elem.id)
return elem.parent.shadowRootCommand(url)
}

func (elem *remoteWE) boolQuery(urlTemplate string) (bool, error) {
return elem.parent.boolCommand(fmt.Sprintf(urlTemplate, elem.id))
}
Expand Down Expand Up @@ -1579,3 +1626,28 @@ func (elem *remoteWE) Screenshot(scroll bool) ([]byte, error) {
decoder := base64.NewDecoder(base64.StdEncoding, bytes.NewBuffer(buf))
return ioutil.ReadAll(decoder)
}

type remoteSR struct {
parent *remoteWD
id string
}

func (elem *remoteSR) FindElement(by, value string) (WebElement, error) {
url := fmt.Sprintf("/session/%%s/shadow/%s/element", elem.id)
response, err := elem.parent.find(by, value, "", url)
if err != nil {
return nil, err
}

return elem.parent.DecodeElement(response)
}

func (elem *remoteSR) FindElements(by, value string) ([]WebElement, error) {
url := fmt.Sprintf("/session/%%s/shadow/%s/element", elem.id)
response, err := elem.parent.find(by, value, "s", url)
if err != nil {
return nil, err
}

return elem.parent.DecodeElements(response)
}
10 changes: 10 additions & 0 deletions selenium.go
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,8 @@ type WebElement interface {
FindElement(by, value string) (WebElement, error)
// FindElement finds multiple children elements.
FindElements(by, value string) ([]WebElement, error)
// Gets the shadow root element of a shadow DOM whose host is this element
GetElementShadowRoot() (ShadowRoot, error)

// TagName returns the element's name.
TagName() (string, error)
Expand Down Expand Up @@ -479,3 +481,11 @@ type WebElement interface {
// Screenshot takes a screenshot of the attribute scroll'ing if necessary.
Screenshot(scroll bool) ([]byte, error)
}

// ShadowRoot defines methods supported by a shadow DOM root element
type ShadowRoot interface {
// FindElement finds a child element into the shadow DOM
FindElement(by, value string) (WebElement, error)
// FindElement finds multiple children elements into the shadow DOM
FindElements(by, value string) ([]WebElement, error)
}