initial commit
This commit is contained in:
commit
a9e8312934
183
driver.go
Normal file
183
driver.go
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
package webdriver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Driver struct {
|
||||||
|
server *exec.Cmd
|
||||||
|
|
||||||
|
host string
|
||||||
|
port uint16
|
||||||
|
sessionID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDriver() (*Driver, error) {
|
||||||
|
host := "127.0.0.1"
|
||||||
|
port := uint16(4444)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
server := exec.CommandContext(ctx, "geckodriver", "--host", host, "--port", strconv.Itoa(int(port)))
|
||||||
|
|
||||||
|
stdout, err := server.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := server.Start(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(stdout)
|
||||||
|
scanner.Scan()
|
||||||
|
|
||||||
|
return &Driver{
|
||||||
|
server: server,
|
||||||
|
host: host,
|
||||||
|
port: port,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func UseDriver(host string, port uint16) *Driver {
|
||||||
|
return &Driver{
|
||||||
|
host: host,
|
||||||
|
port: port,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) Close() error {
|
||||||
|
if d.server == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.sessionID != "" {
|
||||||
|
if err := d.CloseSession(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := d.server.Cancel(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := d.server.Wait(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) buildSessionUrl(path string) string {
|
||||||
|
uri, err := url.JoinPath(fmt.Sprintf("http://%s:%d/session/%s", d.host, d.port, d.sessionID), path)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return uri
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) buildUrl(path string) string {
|
||||||
|
uri, err := url.JoinPath(fmt.Sprintf("http://%s:%d", d.host, d.port), path)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return uri
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) DoRawSessionRequest(method string, path string, body io.Reader) (*http.Response, error) {
|
||||||
|
req, err := http.NewRequest(method, d.buildSessionUrl(path), body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
|
||||||
|
return http.DefaultClient.Do(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) DoRawRequest(method string, path string, body io.Reader) (*http.Response, error) {
|
||||||
|
req, err := http.NewRequest(method, d.buildUrl(path), body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
|
||||||
|
return http.DefaultClient.Do(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) DoSessionRequest(method string, path string, body map[string]interface{}) (map[string]interface{}, error) {
|
||||||
|
if body == nil {
|
||||||
|
body = map[string]interface{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := d.DoRawSessionRequest(method, path, bytes.NewReader(data))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
respBody := map[string]interface{}{}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&respBody); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return respBody, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) DoRequest(method string, path string, body map[string]interface{}) (map[string]interface{}, error) {
|
||||||
|
if body == nil {
|
||||||
|
body = map[string]interface{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := d.DoRawRequest(method, path, bytes.NewReader(data))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
respBody := map[string]interface{}{}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&respBody); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return respBody, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) NewSession() error {
|
||||||
|
data, err := d.DoRequest(http.MethodPost, "/session", nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.sessionID = data["value"].(map[string]interface{})["sessionId"].(string)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) CloseSession() error {
|
||||||
|
_, err := d.DoSessionRequest(http.MethodDelete, "", nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.sessionID = ""
|
||||||
|
return nil
|
||||||
|
}
|
140
driver_methods.go
Normal file
140
driver_methods.go
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
package webdriver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.milar.in/milarin/slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
// docs: https://w3c.github.io/webdriver/
|
||||||
|
|
||||||
|
func (d *Driver) Navigate(url string) error {
|
||||||
|
_, err := d.DoSessionRequest(http.MethodPost, "/url", map[string]interface{}{"url": url})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) GetUrl() (string, error) {
|
||||||
|
data, err := d.DoSessionRequest(http.MethodGet, "/url", nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return data["value"].(string), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) GetTitle() (string, error) {
|
||||||
|
data, err := d.DoSessionRequest(http.MethodGet, "/title", nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return data["value"].(string), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) FindElement(selector string) (string, error) {
|
||||||
|
req := map[string]interface{}{
|
||||||
|
"using": "css selector",
|
||||||
|
"value": selector,
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := d.DoSessionRequest(http.MethodPost, "/element", req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return data["value"].(map[string]interface{})[webElementIdentifier].(string), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) FindElements(selector string) ([]string, error) {
|
||||||
|
req := map[string]interface{}{
|
||||||
|
"using": "css selector",
|
||||||
|
"value": selector,
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := d.DoSessionRequest(http.MethodPost, "/elements", req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return slices.Map(data["value"].([]interface{}), func(e interface{}) string {
|
||||||
|
return e.(map[string]interface{})[webElementIdentifier].(string)
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) GetElementTagName(elementID string) (string, error) {
|
||||||
|
data, err := d.DoSessionRequest(http.MethodGet, fmt.Sprintf("/element/%s/name", elementID), nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return data["value"].(string), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) GetElementText(elementID string) (string, error) {
|
||||||
|
data, err := d.DoSessionRequest(http.MethodGet, fmt.Sprintf("/element/%s/text", elementID), nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return data["value"].(string), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) GetElementRect(elementID string) (*Rect, error) {
|
||||||
|
data, err := d.DoSessionRequest(http.MethodGet, fmt.Sprintf("/element/%s/rect", elementID), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rect := data["value"].(map[string]interface{})
|
||||||
|
return &Rect{
|
||||||
|
X: rect["x"].(float64),
|
||||||
|
Y: rect["y"].(float64),
|
||||||
|
Width: rect["width"].(float64),
|
||||||
|
Height: rect["height"].(float64),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) GetElementCssValue(elementID, property string) (string, error) {
|
||||||
|
data, err := d.DoSessionRequest(http.MethodGet, fmt.Sprintf("/element/%s/css/%s", elementID, property), nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return data["value"].(string), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) GetElementProperty(elementID, property string) (string, error) {
|
||||||
|
data, err := d.DoSessionRequest(http.MethodGet, fmt.Sprintf("/element/%s/property/%s", elementID, property), nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return data["value"].(string), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) GetElementAttribute(elementID, attribute string) (string, error) {
|
||||||
|
data, err := d.DoSessionRequest(http.MethodGet, fmt.Sprintf("/element/%s/attribute/%s", elementID, attribute), nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return data["value"].(string), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) ClickElement(elementID string) error {
|
||||||
|
data, err := d.DoSessionRequest(http.MethodPost, fmt.Sprintf("/element/%s/click", elementID), nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("%#v\n", data)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) SendKeysToElement(elementID, text string) error {
|
||||||
|
req := map[string]interface{}{
|
||||||
|
"text": text,
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := d.DoSessionRequest(http.MethodPost, fmt.Sprintf("/element/%s/value", elementID), req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("%#v\n", data)
|
||||||
|
return nil
|
||||||
|
}
|
18
driver_test.go
Normal file
18
driver_test.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package webdriver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDriver(t *testing.T) {
|
||||||
|
driver, err := NewDriver()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer driver.Close()
|
||||||
|
|
||||||
|
if err := driver.NewSession(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer driver.CloseSession()
|
||||||
|
}
|
7
go.mod
Normal file
7
go.mod
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
module git.milar.in/milarin/webdriver
|
||||||
|
|
||||||
|
go 1.21.5
|
||||||
|
|
||||||
|
require git.milar.in/milarin/slices v0.0.8
|
||||||
|
|
||||||
|
require git.milar.in/milarin/gmath v0.0.3 // indirect
|
4
go.sum
Normal file
4
go.sum
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
git.milar.in/milarin/gmath v0.0.3 h1:ii6rKNItS55O/wtIFhD1cTN2BMwDZjTBmiOocKURvxM=
|
||||||
|
git.milar.in/milarin/gmath v0.0.3/go.mod h1:HDLftG5RLpiNGKiIWh+O2G1PYkNzyLDADO8Cd/1abiE=
|
||||||
|
git.milar.in/milarin/slices v0.0.8 h1:qN9TE3tkArdTixMKSnwvNPcApwAjxpLVwA5a9k1rm2s=
|
||||||
|
git.milar.in/milarin/slices v0.0.8/go.mod h1:qMhdtMnfWswc1rHpwgNw33lB84aNEkdBn5BDiYA+G3k=
|
8
types.go
Normal file
8
types.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package webdriver
|
||||||
|
|
||||||
|
type Rect struct {
|
||||||
|
X float64
|
||||||
|
Y float64
|
||||||
|
Width float64
|
||||||
|
Height float64
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user