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

implement shadow root #17

Merged
merged 1 commit into from
Apr 15, 2023
Merged
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
56 changes: 56 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 @@ -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)
Expand Down Expand Up @@ -1625,6 +1663,23 @@ 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 @@ -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)
Expand Down
81 changes: 76 additions & 5 deletions remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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)
Expand All @@ -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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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
}
Expand Down
10 changes: 10 additions & 0 deletions selenium.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}