From a9e8312934bf1810d51492c94c149867b1965bd9 Mon Sep 17 00:00:00 2001 From: milarin Date: Sun, 10 Dec 2023 19:09:23 +0100 Subject: [PATCH] initial commit --- driver.go | 183 ++++++++++++++++++++++++++++++++++++++++++++++ driver_methods.go | 140 +++++++++++++++++++++++++++++++++++ driver_test.go | 18 +++++ go.mod | 7 ++ go.sum | 4 + types.go | 8 ++ utils.go | 3 + 7 files changed, 363 insertions(+) create mode 100644 driver.go create mode 100644 driver_methods.go create mode 100644 driver_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 types.go create mode 100644 utils.go diff --git a/driver.go b/driver.go new file mode 100644 index 0000000..a009103 --- /dev/null +++ b/driver.go @@ -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 +} diff --git a/driver_methods.go b/driver_methods.go new file mode 100644 index 0000000..4c9c9e7 --- /dev/null +++ b/driver_methods.go @@ -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 +} diff --git a/driver_test.go b/driver_test.go new file mode 100644 index 0000000..82a5e52 --- /dev/null +++ b/driver_test.go @@ -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() +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4035498 --- /dev/null +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3107e78 --- /dev/null +++ b/go.sum @@ -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= diff --git a/types.go b/types.go new file mode 100644 index 0000000..7ad1587 --- /dev/null +++ b/types.go @@ -0,0 +1,8 @@ +package webdriver + +type Rect struct { + X float64 + Y float64 + Width float64 + Height float64 +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..7858348 --- /dev/null +++ b/utils.go @@ -0,0 +1,3 @@ +package webdriver + +const webElementIdentifier = "element-6066-11e4-a52e-4f735466cecf"