From 7239cbe42653ab1f0ea9fc87de75881f09807088 Mon Sep 17 00:00:00 2001 From: Dmitry Kropachev Date: Sat, 15 Apr 2023 10:30:35 -0400 Subject: [PATCH] implement shadow root --- internal/seleniumtest/seleniumtest.go | 56 ++++++++++++++++++ remote.go | 81 +++++++++++++++++++++++++-- selenium.go | 10 ++++ 3 files changed, 142 insertions(+), 5 deletions(-) diff --git a/internal/seleniumtest/seleniumtest.go b/internal/seleniumtest/seleniumtest.go index dc7ad84..43a9ece 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)) @@ -589,6 +590,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) @@ -1625,6 +1663,23 @@ 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{ @@ -1635,6 +1690,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 21ac387..1cac320 100644 --- a/remote.go +++ b/remote.go @@ -61,8 +61,13 @@ type remoteWD struct { // server. var HTTPClient = http.DefaultClient -// jsonContentType is JSON content type. -const jsonContentType = "application/json" +const ( + // jsonContentType is JSON content type. + jsonContentType = "application/json" + // 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 newRequest(method string, url string, data []byte) (*http.Request, error) { request, err := http.NewRequest(method, url, bytes.NewBuffer(data)) @@ -316,6 +321,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) @@ -331,6 +345,33 @@ func (wd *remoteWD) Status() (*Status, error) { return &status.Value, nil } +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 +} + +func shadowRootIDFromValue(v map[string]string) string { + for _, key := range []string{shadowRootIdentifier} { + v, ok := v[key] + if !ok || v == "" { + continue + } + return v + } + return "" +} + // parseVersion sanitizes the browser version enough for semver.ParseTolerant // to parse it. func parseVersion(v string) (semver.Version, error) { @@ -707,6 +748,11 @@ func (wd *remoteWD) DecodeElement(data []byte) (WebElement, error) { }, nil } +func (elem *remoteWE) GetElementShadowRoot() (ShadowRoot, error) { + url := fmt.Sprintf("/session/%%s/element/%s/shadow", elem.id) + return elem.parent.shadowRootCommand(url) +} + const ( // legacyWebElementIdentifier is the string constant used in the old // WebDriver JSON protocol that is the key for the map that contains an @@ -1563,6 +1609,31 @@ func (elem *remoteWE) GetAttribute(name string) (string, error) { return elem.parent.stringCommand(urlTemplate) } +type remoteSR struct { + parent *remoteWD + id string +} + +func (elem *remoteSR) FindElement(by, value string) (WebElement, error) { + u := fmt.Sprintf("/session/%%s/shadow/%s/element", elem.id) + response, err := elem.parent.find(by, value, "", u) + if err != nil { + return nil, err + } + + return elem.parent.DecodeElement(response) +} + +func (elem *remoteSR) FindElements(by, value string) ([]WebElement, error) { + u := fmt.Sprintf("/session/%%s/shadow/%s/element", elem.id) + response, err := elem.parent.find(by, value, "s", u) + if err != nil { + return nil, err + } + + return elem.parent.DecodeElements(response) +} + func round(f float64) int { if f < -0.5 { return int(f - 0.5) @@ -1576,9 +1647,9 @@ func round(f float64) int { func (elem *remoteWE) location(suffix string) (*Point, error) { if !elem.parent.w3cCompatible { wd := elem.parent - path := "/session/%s/element/%s/location" + suffix - url := wd.requestURL(path, wd.id, elem.id) - response, err := wd.execute("GET", url, nil) + p := "/session/%s/element/%s/location" + suffix + u := wd.requestURL(p, wd.id, elem.id) + response, err := wd.execute("GET", u, nil) if err != nil { return nil, err } diff --git a/selenium.go b/selenium.go index 5bc9d46..22c00ec 100644 --- a/selenium.go +++ b/selenium.go @@ -484,6 +484,8 @@ type WebElement interface { FindElement(by, value string) (WebElement, error) // FindElement finds multiple children elements. FindElements(by, value string) ([]WebElement, error) + // GetElementShadowRoot 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) @@ -513,3 +515,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) + // FindElements finds multiple children elements into the shadow DOM + FindElements(by, value string) ([]WebElement, error) +}