github.com/web-platform-tests/wpt.fyi@v0.0.0-20240530210107-70cf978996f1/webdriver/webdriver.go (about) 1 package webdriver 2 3 import ( 4 "encoding/json" 5 "flag" 6 "fmt" 7 "io/ioutil" 8 "log" 9 "os" 10 "runtime" 11 "testing" 12 "time" 13 14 "github.com/phayes/freeport" 15 "github.com/tebeka/selenium" 16 ) 17 18 // Flags 19 var ( 20 debug = flag.Bool("debug", false, "Turn on debug logging") 21 browser = flag.String("browser", "firefox", "Which browser to run the tests with") 22 startFrameBuffer = flag.Bool("frame_buffer", frameBufferDefault(), "Whether to use a frame buffer") 23 ) 24 25 const ( 26 // LongTimeout is the timeout for waiting the full page to load, with 27 // data coming from Datastore. You may not need this if you only need 28 // to wait for the initial Polymer rendering. 29 LongTimeout = time.Second * 30 30 ) 31 32 func frameBufferDefault() bool { 33 return runtime.GOOS != "darwin" 34 } 35 36 func pickUnusedPort() int { 37 port, err := freeport.GetFreePort() 38 if err != nil { 39 panic(err) 40 } 41 return port 42 } 43 44 type webdriverTest func(t *testing.T, app AppServer, wd selenium.WebDriver) 45 46 // Generic helpers for WebDriver 47 48 // GetWebDriver starts a WebDriver service (server) and creates a remote 49 // (client). 50 // Note: Make sure to close the remote first and the service later, e.g. 51 // 52 // server, driver, err := GetWebDriver() 53 // if err != nil { 54 // panic(err) 55 // } 56 // defer server.Stop() 57 // defer driver.Quit() 58 func GetWebDriver() (*selenium.Service, selenium.WebDriver, error) { 59 var options []selenium.ServiceOption 60 if *startFrameBuffer { 61 // Use a 24-bit display to avoid https://github.com/web-platform-tests/wpt.fyi/issues/1788. 62 options = append(options, selenium.StartFrameBufferWithOptions( 63 selenium.FrameBufferOptions{ScreenSize: "1280x1024x24"})) 64 } 65 if *debug { 66 selenium.SetDebug(true) 67 options = append(options, selenium.Output(os.Stderr)) 68 } else { 69 options = append(options, selenium.Output(ioutil.Discard)) 70 } 71 72 port := pickUnusedPort() 73 switch *browser { 74 case "firefox": 75 return FirefoxWebDriver(port, options) 76 case "chrome": 77 return ChromeWebDriver(port, options) 78 } 79 panic("Invalid -browser value specified") 80 } 81 82 // FindShadowElements finds the shadow DOM children via the given query 83 // selectors, recursively. The function takes a variable number of selectors; 84 // the selectors are combined together similar to CSS descendant combinators. 85 // However, all but the the last selector are expected to match to hosts of 86 // shadow DOM, and the shadow DOM boundaries will be crossed. 87 // 88 // e.g. FindShadowElements(wd, node, "bar", "baz blah"). All matches of "bar" 89 // must have shadow roots, and the function finds all "baz blah" within each 90 // shadow DOM. 91 func FindShadowElements( 92 d selenium.WebDriver, 93 e selenium.WebElement, 94 selectors ...string) ([]selenium.WebElement, error) { 95 elements := []selenium.WebElement{e} 96 for _, selector := range selectors { 97 interfaces := make([]interface{}, len(elements)) 98 for i, e := range elements { 99 interfaces[i] = e 100 } 101 result, err := d.ExecuteScriptRaw( 102 fmt.Sprintf(`return Array.from(arguments) 103 .reduce((s, e) => { 104 return e.shadowRoot ? s.concat(Array.from(e.shadowRoot.querySelectorAll('%s'))) : s 105 }, [])`, 106 selector), 107 interfaces) 108 if err != nil { 109 panic(err.Error()) 110 } 111 elements, err = d.DecodeElements(result) 112 if err != nil { 113 return nil, err 114 } 115 } 116 return elements, nil 117 } 118 119 // FindShadowElement returns the first element found by an equivalent call to 120 // FindShadowElements. 121 func FindShadowElement( 122 d selenium.WebDriver, 123 e selenium.WebElement, 124 selectors ...string) (selenium.WebElement, error) { 125 elements, err := FindShadowElements(d, e, selectors...) 126 if err != nil || len(elements) < 1 { 127 return nil, err 128 } 129 return elements[0], nil 130 } 131 132 // FindShadowText returns the Text of the element returned by an equivalent 133 // call to FindShadowElement. 134 func FindShadowText( 135 d selenium.WebDriver, 136 e selenium.WebElement, 137 selectors ...string) (string, error) { 138 element, err := FindShadowElement(d, e, selectors...) 139 if err != nil { 140 return "", err 141 } 142 return element.Text() 143 } 144 145 // ExtractScriptRawValue extracts the value of a given key from the return 146 // value of webdriver.ExecuteScriptRaw (raw bytes). 147 func ExtractScriptRawValue(bytes []byte, key string) (value interface{}, err error) { 148 var parsed map[string]interface{} 149 if err = json.Unmarshal(bytes, &parsed); err != nil { 150 return nil, err 151 } 152 return parsed[key], nil 153 } 154 155 // The following are helpers specific to wpt.fyi. 156 157 // runWebdriverTest is a helper for starting both the server and WebDriver for a test. 158 func runWebdriverTest(t *testing.T, test webdriverTest) { 159 app, err := NewWebserver() 160 if err != nil { 161 log.Println("Failed to create webserver: " + err.Error()) 162 panic(err) 163 } 164 defer app.Close() 165 166 service, wd, err := GetWebDriver() 167 if err != nil { 168 log.Println("Failed to create webdriver: " + err.Error()) 169 panic(err) 170 } 171 defer service.Stop() 172 defer wd.Quit() 173 174 test(t, app, wd) 175 } 176 177 func getTestRunElements(wd selenium.WebDriver, element string) ([]selenium.WebElement, error) { 178 e, err := wd.FindElement(selenium.ByCSSSelector, "wpt-app") 179 if err != nil { 180 return nil, err 181 } 182 return FindShadowElements(wd, e, element, "test-run") 183 } 184 185 func getPathPartElements(wd selenium.WebDriver, element string) ([]selenium.WebElement, error) { 186 e, err := wd.FindElement(selenium.ByTagName, "wpt-app") 187 if err != nil { 188 return nil, err 189 } 190 return FindShadowElements(wd, e, element, "path-part") 191 }