initial commit

This commit is contained in:
milarin 2023-12-10 19:09:23 +01:00
commit a9e8312934
7 changed files with 363 additions and 0 deletions

183
driver.go Normal file
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,8 @@
package webdriver
type Rect struct {
X float64
Y float64
Width float64
Height float64
}

3
utils.go Normal file
View File

@ -0,0 +1,3 @@
package webdriver
const webElementIdentifier = "element-6066-11e4-a52e-4f735466cecf"