From be1930089292b2a2e668593a438ea4810bb384ed Mon Sep 17 00:00:00 2001 From: Roberto Mier Escandon Date: Tue, 14 Jun 2022 18:48:34 +0200 Subject: [PATCH] feat: support for Shadow DOM methods Includes GetElementShadowRoot, FindElement and FindElements as specified in https://www.w3.org/TR/webdriver --- internal/seleniumtest/seleniumtest.go | 57 +++++++++++++++++++++ remote.go | 72 +++++++++++++++++++++++++++ selenium.go | 10 ++++ 3 files changed, 139 insertions(+) diff --git a/internal/seleniumtest/seleniumtest.go b/internal/seleniumtest/seleniumtest.go index b84f731..21d0419 100644 --- a/internal/seleniumtest/seleniumtest.go +++ b/internal/seleniumtest/seleniumtest.go @@ -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)) @@ -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) @@ -1604,6 +1642,24 @@ var alertPage = ` ` +var shadowDOMPage = ` + + + Go Selenium Test Suite - Shadow DOM Page + + + This page contains a Shadow DOM. + +
+ +
+ + +` + var Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { path := r.URL.Path page, ok := map[string]string{ @@ -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) diff --git a/remote.go b/remote.go index 653b5f4..cac593d 100644 --- a/remote.go +++ b/remote.go @@ -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) @@ -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 { @@ -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)) } @@ -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) +} diff --git a/selenium.go b/selenium.go index ecd8c91..fac3e1f 100644 --- a/selenium.go +++ b/selenium.go @@ -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) @@ -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) +}