1146 lines
28 KiB
Go
1146 lines
28 KiB
Go
package http
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
"io"
|
||
"strings"
|
||
"time"
|
||
|
||
"apigo.cc/gojs"
|
||
"github.com/go-rod/rod"
|
||
"github.com/go-rod/rod/lib/input"
|
||
"github.com/go-rod/rod/lib/proto"
|
||
"github.com/ssgo/u"
|
||
)
|
||
|
||
type Page struct {
|
||
page *rod.Page
|
||
longTimeoutPage *rod.Page
|
||
originPage *rod.Page
|
||
// Res []Resource
|
||
}
|
||
|
||
type Element struct {
|
||
element *rod.Element
|
||
page *rod.Page
|
||
}
|
||
|
||
type Resource struct {
|
||
Url string
|
||
Type string
|
||
Size int
|
||
MimeType string
|
||
LastModified string
|
||
Success bool
|
||
}
|
||
|
||
type Position struct {
|
||
X float64
|
||
Y float64
|
||
}
|
||
|
||
type Rect struct{ X, Y, Width, Height float64 }
|
||
|
||
func (ch *Chrome) Open(url string) (*Page, error) {
|
||
page, err := ch.browser.Page(proto.TargetCreateTarget{URL: url})
|
||
if err != nil {
|
||
return nil, gojs.Err(err)
|
||
}
|
||
|
||
pg := &Page{originPage: page}
|
||
pg.ResetTimeout()
|
||
pg.WaitPageLoad()
|
||
return pg, nil
|
||
}
|
||
|
||
func (pg *Page) Close() error {
|
||
// pg.Res = []Resource{}
|
||
return gojs.Err(pg.page.Close())
|
||
}
|
||
|
||
func (pg *Page) GetResList() ([]Resource, error) {
|
||
r, err := proto.PageGetResourceTree{}.Call(pg.page)
|
||
if err != nil {
|
||
return nil, gojs.Err(err)
|
||
}
|
||
list := []Resource{}
|
||
for _, res := range r.FrameTree.Resources {
|
||
list = append(list, Resource{
|
||
Url: res.URL,
|
||
Type: string(res.Type),
|
||
Size: u.Int(res.ContentSize),
|
||
MimeType: res.MIMEType,
|
||
LastModified: res.LastModified.String(),
|
||
Success: !res.Failed && !res.Canceled,
|
||
})
|
||
}
|
||
return list, nil
|
||
}
|
||
|
||
func (pg *Page) GetResListByType(resType string) ([]Resource, error) {
|
||
list, err := pg.GetResList()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
result := []Resource{}
|
||
for _, res := range list {
|
||
if strings.EqualFold(res.Type, resType) {
|
||
result = append(result, res)
|
||
}
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
func (pg *Page) GetResListByMimeType(mimeType string) ([]Resource, error) {
|
||
list, err := pg.GetResList()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
isMatch := strings.HasSuffix(mimeType, "/*")
|
||
mimeType = strings.TrimSuffix(mimeType, "*")
|
||
result := []Resource{}
|
||
for _, res := range list {
|
||
if isMatch {
|
||
if strings.HasPrefix(res.MimeType, mimeType) {
|
||
result = append(result, res)
|
||
}
|
||
} else {
|
||
if strings.EqualFold(res.MimeType, mimeType) {
|
||
result = append(result, res)
|
||
}
|
||
}
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
func (pg *Page) GetResContent(url string) ([]byte, error) {
|
||
return pg.longTimeoutPage.GetResource(url)
|
||
}
|
||
|
||
func (pg *Page) Find(selector string) (*Element, error) {
|
||
el, err := pg.page.Element(selector)
|
||
if err != nil {
|
||
return nil, gojs.Err(err)
|
||
}
|
||
return &Element{element: el, page: pg.page}, nil
|
||
}
|
||
|
||
func (pg *Page) FindN(selector string) *Element {
|
||
els, err := pg.FindAll(selector)
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
if len(els) == 0 {
|
||
return nil
|
||
}
|
||
return els[0]
|
||
}
|
||
|
||
func (pg *Page) FindAll(selector string) ([]*Element, error) {
|
||
elements, err := pg.page.Elements(selector)
|
||
if err != nil {
|
||
return nil, gojs.Err(err)
|
||
}
|
||
result := make([]*Element, len(elements))
|
||
for i, el := range elements {
|
||
result[i] = &Element{element: el, page: pg.page}
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
// FindX 通过XPath查找元素
|
||
func (pg *Page) FindX(xpath string) (*Element, error) {
|
||
el, err := pg.page.ElementX(xpath)
|
||
if err != nil {
|
||
return nil, gojs.Err(err)
|
||
}
|
||
return &Element{element: el, page: pg.page}, nil
|
||
}
|
||
|
||
func (pg *Page) FindXN(selector string) *Element {
|
||
el, err := pg.FindX(selector)
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
return el
|
||
}
|
||
|
||
// FindsX 通过XPath查找多个元素
|
||
func (pg *Page) FindAllX(xpath string) ([]*Element, error) {
|
||
elements, err := pg.page.ElementsX(xpath)
|
||
if err != nil {
|
||
return nil, gojs.Err(err)
|
||
}
|
||
result := make([]*Element, len(elements))
|
||
for i, el := range elements {
|
||
result[i] = &Element{element: el, page: pg.page}
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
func (pg *Page) Navigate(url string) error {
|
||
err := pg.longTimeoutPage.Navigate(url)
|
||
if err != nil {
|
||
return gojs.Err(err)
|
||
}
|
||
pg.WaitPageLoad()
|
||
return nil
|
||
}
|
||
|
||
// WaitLoad 等待页面加载完成
|
||
func (pg *Page) WaitPageLoad() error {
|
||
time.Sleep(100 * time.Millisecond)
|
||
err := pg.page.WaitLoad()
|
||
if err != nil {
|
||
return gojs.Err(err)
|
||
}
|
||
pg.ResetTimeout()
|
||
return nil
|
||
}
|
||
|
||
// WaitIdle 等待页面空闲(无网络请求和JS执行)
|
||
func (pg *Page) WaitIdle(ms *int) error {
|
||
if ms == nil {
|
||
ms = u.IntPtr(1000)
|
||
}
|
||
return gojs.Err(pg.page.WaitIdle(time.Millisecond * time.Duration(*ms)))
|
||
}
|
||
|
||
// Wait 等待回调函数函数返回true
|
||
func (pg *Page) Wait(ms int, fn *func() bool) {
|
||
for i := 0; i < ms; i += 100 {
|
||
if fn != nil && (*fn)() {
|
||
break
|
||
}
|
||
time.Sleep(100 * time.Millisecond)
|
||
}
|
||
pg.ResetTimeout()
|
||
}
|
||
|
||
// RandWait 随机等待
|
||
func (pg *Page) RandWait(msMin int, msMax int) {
|
||
ms := u.GlobalRand1.Intn(msMax-msMin) + msMin
|
||
pg.Wait(ms, nil)
|
||
}
|
||
|
||
func (pg *Page) ResetTimeout() {
|
||
pg.page = pg.originPage.Timeout(time.Second * 5)
|
||
pg.longTimeoutPage = pg.originPage.Timeout(time.Second * 15)
|
||
}
|
||
|
||
// WaitStable 等待页面DOM稳定
|
||
// func (pg *Page) WaitStable(ms *int) error {
|
||
// if ms == nil {
|
||
// ms = u.IntPtr(100)
|
||
// }
|
||
// return gojs.Err(pg.page.WaitStable(time.Millisecond * time.Duration(*ms)))
|
||
// }
|
||
|
||
// Screenshot 截图
|
||
func (pg *Page) ScreenshotFullPage() ([]byte, error) {
|
||
buf, err := pg.longTimeoutPage.Screenshot(true, nil)
|
||
return buf, gojs.Err(err)
|
||
}
|
||
|
||
func (pg *Page) Screenshot() ([]byte, error) {
|
||
buf, err := pg.longTimeoutPage.Screenshot(false, nil)
|
||
return buf, gojs.Err(err)
|
||
}
|
||
|
||
// PDF 生成页面PDF
|
||
func (pg *Page) PDF() ([]byte, error) {
|
||
r, err := pg.longTimeoutPage.PDF(&proto.PagePrintToPDF{})
|
||
if err != nil {
|
||
return nil, gojs.Err(err)
|
||
}
|
||
bin, err := io.ReadAll(r)
|
||
if err != nil {
|
||
return nil, gojs.Err(err)
|
||
}
|
||
|
||
return bin, nil
|
||
}
|
||
|
||
func (pg *Page) Title() (string, error) {
|
||
info, err := pg.page.Info()
|
||
if err != nil {
|
||
return "", gojs.Err(err)
|
||
}
|
||
return info.Title, nil
|
||
}
|
||
|
||
func (pg *Page) Html() (string, error) {
|
||
html, err := pg.page.HTML()
|
||
return html, gojs.Err(err)
|
||
}
|
||
|
||
func (pg *Page) Url() (string, error) {
|
||
info, err := pg.page.Info()
|
||
if err != nil {
|
||
return "", gojs.Err(err)
|
||
}
|
||
return info.URL, nil
|
||
}
|
||
|
||
func (pg *Page) Eval(js string) (interface{}, error) {
|
||
r, err := pg.page.Eval(js)
|
||
if err != nil {
|
||
return nil, gojs.Err(err)
|
||
}
|
||
return r.Value.Val(), nil
|
||
}
|
||
|
||
func (pg *Page) MouseMoveTo(x, y float64) error {
|
||
return gojs.Err(pg.page.Mouse.MoveTo(proto.Point{X: x, Y: y}))
|
||
}
|
||
|
||
func (pg *Page) MouseClick() error {
|
||
return gojs.Err(pg.page.Mouse.Click(proto.InputMouseButtonLeft, 1))
|
||
}
|
||
|
||
func (pg *Page) MouseRightClick() error {
|
||
return gojs.Err(pg.page.Mouse.Click(proto.InputMouseButtonRight, 1))
|
||
}
|
||
|
||
func (pg *Page) MouseMiddleClick() error {
|
||
return gojs.Err(pg.page.Mouse.Click(proto.InputMouseButtonMiddle, 1))
|
||
}
|
||
|
||
func (pg *Page) MouseDown() error {
|
||
return gojs.Err(pg.page.Mouse.Down(proto.InputMouseButtonLeft, 1))
|
||
}
|
||
|
||
func (pg *Page) MouseUp() error {
|
||
return gojs.Err(pg.page.Mouse.Up(proto.InputMouseButtonLeft, 1))
|
||
}
|
||
|
||
func (pg *Page) SetCookies(cookies map[string]string) error {
|
||
cks := []*proto.NetworkCookieParam{}
|
||
for k, v := range cookies {
|
||
cks = append(cks, &proto.NetworkCookieParam{
|
||
Name: k,
|
||
Value: v,
|
||
})
|
||
}
|
||
err := pg.page.SetCookies(cks)
|
||
if err != nil {
|
||
return gojs.Err(err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (pg *Page) GetCookies() (map[string]string, error) {
|
||
cookies, err := pg.page.Cookies(nil)
|
||
if err != nil {
|
||
return nil, gojs.Err(err)
|
||
}
|
||
|
||
result := make(map[string]string)
|
||
for _, c := range cookies {
|
||
result[c.Name] = c.Value
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
// SetViewport 设置视口大小
|
||
func (pg *Page) SetViewport(width, height int) error {
|
||
return gojs.Err(pg.page.SetViewport(&proto.EmulationSetDeviceMetricsOverride{
|
||
Width: width,
|
||
Height: height,
|
||
DeviceScaleFactor: 0,
|
||
Mobile: false,
|
||
}))
|
||
}
|
||
|
||
func (pg *Page) SetPhoneViewport(width, height int) error {
|
||
return gojs.Err(pg.page.SetViewport(&proto.EmulationSetDeviceMetricsOverride{
|
||
Width: width,
|
||
Height: height,
|
||
DeviceScaleFactor: 0,
|
||
Mobile: true,
|
||
}))
|
||
}
|
||
|
||
// ScrollTo 滚动到指定位置
|
||
func (pg *Page) ScrollTo(x, y float64) error {
|
||
_, err := pg.page.Eval(`(x, y) => window.scrollTo(x, y)`, x, y)
|
||
return gojs.Err(err)
|
||
}
|
||
|
||
// Frame 获取iframe内容
|
||
func (pg *Page) Frame(selector string) (*Page, error) {
|
||
el, err := pg.page.Element(selector)
|
||
if err != nil {
|
||
return nil, gojs.Err(err)
|
||
}
|
||
p, err := el.Frame()
|
||
if err != nil {
|
||
return nil, gojs.Err(err)
|
||
}
|
||
return &Page{page: p}, nil
|
||
}
|
||
|
||
// Page级
|
||
func (pg *Page) Press(key string) error {
|
||
return gojs.Err(pg.page.Keyboard.Press(input.Key(key[0])))
|
||
}
|
||
|
||
func (pg *Page) SetUserAgent(ua string) error {
|
||
return gojs.Err(pg.page.SetUserAgent(&proto.NetworkSetUserAgentOverride{
|
||
UserAgent: ua,
|
||
}))
|
||
}
|
||
|
||
func (pg *Page) SetUserAgentInfo(ua string, platform string, acceptLanguage string) error {
|
||
return gojs.Err(pg.page.SetUserAgent(&proto.NetworkSetUserAgentOverride{
|
||
UserAgent: ua,
|
||
Platform: platform,
|
||
AcceptLanguage: acceptLanguage,
|
||
}))
|
||
}
|
||
|
||
func (pg *Page) Back() error {
|
||
err := pg.page.NavigateBack()
|
||
if err != nil {
|
||
return gojs.Err(err)
|
||
}
|
||
pg.WaitPageLoad()
|
||
return nil
|
||
}
|
||
|
||
func (pg *Page) Forward() error {
|
||
err := pg.page.NavigateForward()
|
||
if err != nil {
|
||
return gojs.Err(err)
|
||
}
|
||
pg.WaitPageLoad()
|
||
return nil
|
||
}
|
||
|
||
// ================== 对话框控制 ==================
|
||
|
||
// HandleDialog 设置弹框处理器
|
||
// 示例:pg.HandleDialog(true, "") 接受所有弹框
|
||
func (pg *Page) HandleDialog(accept bool, promptText string) error {
|
||
wait, handle := pg.longTimeoutPage.HandleDialog()
|
||
wait()
|
||
return gojs.Err(handle(&proto.PageHandleJavaScriptDialog{
|
||
Accept: accept,
|
||
PromptText: promptText,
|
||
}))
|
||
}
|
||
|
||
// AutoAcceptAlerts 自动接受所有弹框
|
||
func (pg *Page) AccepDialog() error {
|
||
return gojs.Err(pg.HandleDialog(true, ""))
|
||
}
|
||
|
||
// AutoDismissAlerts 自动取消所有弹框
|
||
func (pg *Page) DismissDialog() error {
|
||
return gojs.Err(pg.HandleDialog(false, ""))
|
||
}
|
||
|
||
func (pg *Page) SetPrompt(text string) error {
|
||
return gojs.Err(pg.HandleDialog(true, text))
|
||
}
|
||
|
||
// ================== Element 方法 ==================
|
||
|
||
func (el *Element) Click() error {
|
||
return gojs.Err(el.element.Click(proto.InputMouseButtonLeft, 1))
|
||
}
|
||
|
||
func (el *Element) Input(text string) error {
|
||
return gojs.Err(el.element.Input(text))
|
||
}
|
||
|
||
func (el *Element) Text() (string, error) {
|
||
text, err := el.element.Text()
|
||
return text, gojs.Err(err)
|
||
}
|
||
|
||
func (el *Element) InnerHTML() (string, error) {
|
||
r, err := el.element.Eval(`el => {return el.innerHTML}`)
|
||
if err != nil {
|
||
return "", gojs.Err(err)
|
||
}
|
||
return r.Value.String(), nil
|
||
}
|
||
|
||
func (el *Element) OuterHTML() (string, error) {
|
||
html, err := el.element.HTML()
|
||
return html, gojs.Err(err)
|
||
}
|
||
|
||
func (el *Element) SetInnerHTML(html string) error {
|
||
_, err := el.element.Eval(`(el, html) => el.innerHTML = html`, html)
|
||
return gojs.Err(err)
|
||
}
|
||
|
||
func (el *Element) SetText(text string) error {
|
||
_, err := el.element.Eval(`(el, text) => el.innerText = text`, text)
|
||
return gojs.Err(err)
|
||
}
|
||
|
||
// ----------
|
||
|
||
func (el *Element) WaitVisible() error {
|
||
return gojs.Err(el.element.WaitVisible())
|
||
}
|
||
|
||
func (el *Element) WaitInvisible() error {
|
||
return gojs.Err(el.element.WaitInvisible())
|
||
}
|
||
|
||
func (el *Element) WaitEnabled() error {
|
||
return gojs.Err(el.element.WaitEnabled())
|
||
}
|
||
|
||
func (el *Element) Focus() error {
|
||
return gojs.Err(el.element.Focus())
|
||
}
|
||
|
||
func (el *Element) ScrollIntoView() error {
|
||
return gojs.Err(el.element.ScrollIntoView())
|
||
}
|
||
|
||
func (el *Element) UploadFile(filePaths []string) error {
|
||
return gojs.Err(el.element.SetFiles(filePaths))
|
||
}
|
||
|
||
func (el *Element) Hover() error {
|
||
return gojs.Err(el.element.Hover())
|
||
}
|
||
|
||
// Select 选择下拉框的选项(单选或多选)
|
||
func (el *Element) Select(values ...string) error {
|
||
if len(values) == 0 {
|
||
return nil
|
||
}
|
||
return gojs.Err(el.element.Select(values, true, rod.SelectorTypeText))
|
||
}
|
||
|
||
// GetSelectedValues 获取当前选中的值(单选返回字符串,多选返回切片)
|
||
func (el *Element) GetChecked() ([]string, error) {
|
||
selected, err := el.element.Elements(":checked")
|
||
if err != nil {
|
||
return nil, gojs.Err(err)
|
||
}
|
||
values := make([]string, len(selected))
|
||
for i, opt := range selected {
|
||
val, err := opt.Attribute("value")
|
||
if err != nil {
|
||
return nil, gojs.Err(err)
|
||
}
|
||
values[i] = u.String(val)
|
||
}
|
||
return values, nil
|
||
}
|
||
|
||
// GetAllOptions 获取所有选项(值和文本)
|
||
func (el *Element) GetOptions() ([]map[string]string, error) {
|
||
options, err := el.element.Elements("option")
|
||
if err != nil {
|
||
return nil, gojs.Err(err)
|
||
}
|
||
result := make([]map[string]string, len(options))
|
||
for i, opt := range options {
|
||
val, err := opt.Attribute("value")
|
||
if err != nil {
|
||
return nil, gojs.Err(err)
|
||
}
|
||
text, err := opt.Text()
|
||
if err != nil {
|
||
return nil, gojs.Err(err)
|
||
}
|
||
result[i] = map[string]string{
|
||
"value": u.String(val),
|
||
"text": text,
|
||
}
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
// RadioGroupValue 获取单选按钮组的当前值
|
||
func (el *Element) RadioGroupValue() (string, error) {
|
||
name, err := el.GetAttribute("name")
|
||
if err != nil {
|
||
return "", gojs.Err(err)
|
||
}
|
||
if name == nil {
|
||
return "", nil
|
||
}
|
||
|
||
checked, err := el.page.ElementX(fmt.Sprintf("input[type='radio'][name='%s']:checked", *name))
|
||
if err != nil {
|
||
return "", gojs.Err(err)
|
||
}
|
||
if checked == nil {
|
||
return "", nil
|
||
}
|
||
val, err := checked.Attribute("value")
|
||
if err != nil {
|
||
return "", gojs.Err(err)
|
||
}
|
||
return u.String(val), nil
|
||
}
|
||
|
||
// SetRadioGroupValue 设置单选按钮组的值
|
||
func (el *Element) SetRadioGroupValue(value string) error {
|
||
name, err := el.GetAttribute("name")
|
||
if err != nil {
|
||
return gojs.Err(err)
|
||
}
|
||
if name == nil {
|
||
return nil
|
||
}
|
||
|
||
radio, err := el.page.ElementX(fmt.Sprintf("input[type='radio'][name='%s'][value='%s']", *name, value))
|
||
if err != nil {
|
||
return gojs.Err(err)
|
||
}
|
||
return gojs.Err(radio.Click(proto.InputMouseButtonLeft, 1))
|
||
}
|
||
|
||
// SelectByValue 通过值选择选项
|
||
func (el *Element) SelectByValue(value string) error {
|
||
opt, err := el.element.ElementX(fmt.Sprintf(".//option[@value='%s']", value))
|
||
if err != nil {
|
||
return gojs.Err(err)
|
||
}
|
||
return gojs.Err(opt.Click(proto.InputMouseButtonLeft, 1))
|
||
}
|
||
|
||
// SelectByText 通过文本选择选项
|
||
func (el *Element) SelectByText(text string) error {
|
||
opt, err := el.element.ElementX(fmt.Sprintf(".//option[normalize-space()='%s']", text))
|
||
if err != nil {
|
||
return gojs.Err(err)
|
||
}
|
||
return gojs.Err(opt.Click(proto.InputMouseButtonLeft, 1))
|
||
}
|
||
|
||
// SetRadioValue 设置单选框值
|
||
func (el *Element) SetRadioValue(value string) error {
|
||
radio, err := el.element.ElementX(fmt.Sprintf(".//input[@type='radio'][@value='%s']", value))
|
||
if err != nil {
|
||
return gojs.Err(err)
|
||
}
|
||
return gojs.Err(radio.Click(proto.InputMouseButtonLeft, 1))
|
||
}
|
||
|
||
// GetForm 获取当前元素所属的表单
|
||
func (el *Element) GetForm() (*Element, error) {
|
||
form, err := el.element.ElementX("ancestor::form")
|
||
if err != nil {
|
||
return nil, gojs.Err(err)
|
||
}
|
||
return &Element{element: form, page: el.page}, nil
|
||
}
|
||
|
||
// GetFormData 获取表单数据
|
||
func (el *Element) GetFormData() (map[string]string, error) {
|
||
r, err := el.element.Eval(`form => {
|
||
const formData = new FormData(form);
|
||
return Object.fromEntries(formData.entries());
|
||
}`)
|
||
if err != nil {
|
||
return nil, gojs.Err(err)
|
||
}
|
||
data := r.Value.Map()
|
||
result := make(map[string]string)
|
||
for k, v := range data {
|
||
result[k] = v.String()
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
// SetValue 设置表单元素的值(input/textarea)
|
||
func (el *Element) SetValue(value string) error {
|
||
// _, err := el.element.Eval(`(el, v) => { el.value = v }`, value)
|
||
err := el.element.Input(value)
|
||
if err == nil {
|
||
return nil
|
||
}
|
||
el.WaitStable(nil)
|
||
return nil
|
||
}
|
||
|
||
// GetValue 获取表单元素的值
|
||
func (el *Element) GetValue() (string, error) {
|
||
prop, err := el.element.Property("value")
|
||
if err != nil {
|
||
return "", gojs.Err(err)
|
||
}
|
||
return prop.Str(), nil
|
||
}
|
||
|
||
// SetChecked 设置复选框/单选框的选中状态
|
||
func (el *Element) SetChecked(checked bool) error {
|
||
current, err := el.IsChecked()
|
||
if err != nil {
|
||
return gojs.Err(err)
|
||
}
|
||
if current != checked {
|
||
return gojs.Err(el.element.Click(proto.InputMouseButtonLeft, 1))
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// IsChecked 获取复选框/单选框的选中状态
|
||
func (el *Element) IsChecked() (bool, error) {
|
||
prop, err := el.element.Property("checked")
|
||
if err != nil {
|
||
return false, gojs.Err(err)
|
||
}
|
||
return prop.Bool(), nil
|
||
}
|
||
|
||
// IsDisabled 判断元素是否被禁用
|
||
func (el *Element) IsDisabled() (bool, error) {
|
||
prop, err := el.element.Property("disabled")
|
||
if err != nil {
|
||
return false, gojs.Err(err)
|
||
}
|
||
return prop.Bool(), nil
|
||
}
|
||
|
||
const maxParentDepth = 100 // 防止无限递归
|
||
func findParentRecursive(el *rod.Element, selector string, depth int) (*rod.Element, error) {
|
||
if depth > maxParentDepth {
|
||
return nil, gojs.Err(fmt.Errorf("max recursion depth (%d) reached", maxParentDepth))
|
||
}
|
||
|
||
// 获取直接父元素
|
||
parent, err := el.Parent()
|
||
if err != nil || parent == nil {
|
||
return nil, gojs.Err(err)
|
||
}
|
||
|
||
// 检查是否匹配选择器
|
||
matches, err := parent.Matches(selector)
|
||
if err != nil {
|
||
return nil, gojs.Err(err)
|
||
}
|
||
|
||
if matches {
|
||
return parent, nil
|
||
}
|
||
|
||
// 继续向上递归查找
|
||
return findParentRecursive(parent, selector, depth+1)
|
||
}
|
||
|
||
// FindParent 向上查找匹配选择器的父元素
|
||
func (el *Element) FindParent(selector string) (*Element, error) {
|
||
parent, err := findParentRecursive(el.element, selector, 0)
|
||
if err != nil {
|
||
return nil, gojs.Err(err)
|
||
}
|
||
return &Element{element: parent, page: el.page}, nil
|
||
}
|
||
|
||
func (el *Element) FindParentN(selector string) *Element {
|
||
el, err := el.FindParent(selector)
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
return el
|
||
}
|
||
|
||
// FindParent 向上查找匹配选择器的父元素
|
||
func (el *Element) FindParentX(selector string) (*Element, error) {
|
||
parent, err := el.element.ElementX(fmt.Sprintf("ancestor::%s", selector))
|
||
if err != nil {
|
||
return nil, gojs.Err(err)
|
||
}
|
||
return &Element{element: parent, page: el.page}, nil
|
||
}
|
||
|
||
func (el *Element) FindParentXN(selector string) *Element {
|
||
el, err := el.FindParentX(selector)
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
return el
|
||
}
|
||
|
||
// Parent 获取直接父元素
|
||
func (el *Element) Parent() (*Element, error) {
|
||
parent, err := el.element.ElementX("..")
|
||
if err != nil {
|
||
return nil, gojs.Err(err)
|
||
}
|
||
return &Element{element: parent, page: el.page}, nil
|
||
}
|
||
|
||
// Children 获取所有直接子元素
|
||
func (el *Element) Children() ([]*Element, error) {
|
||
children, err := el.element.ElementsX("./*")
|
||
if err != nil {
|
||
return nil, gojs.Err(err)
|
||
}
|
||
result := make([]*Element, len(children))
|
||
for i, c := range children {
|
||
result[i] = &Element{element: c, page: el.page}
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
// Find 在当前元素下查找匹配选择器的第一个元素
|
||
func (el *Element) FindChild(selector string) (*Element, error) {
|
||
elem, err := el.element.Element(":scope > " + selector)
|
||
if err != nil {
|
||
return nil, gojs.Err(err)
|
||
}
|
||
return &Element{element: elem, page: el.page}, nil
|
||
}
|
||
|
||
func (el *Element) FindChildN(selector string) *Element {
|
||
el, err := el.FindChild(selector)
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
return el
|
||
}
|
||
|
||
// FindAll 在当前元素下查找所有匹配选择器的元素
|
||
func (el *Element) FindChildren(selector string) ([]*Element, error) {
|
||
elements, err := el.element.Elements(":scope > " + selector)
|
||
if err != nil {
|
||
return nil, gojs.Err(err)
|
||
}
|
||
result := make([]*Element, len(elements))
|
||
for i, e := range elements {
|
||
result[i] = &Element{element: e, page: el.page}
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
// Find 在当前元素下查找匹配选择器的第一个元素
|
||
func (el *Element) Find(selector string) (*Element, error) {
|
||
elem, err := el.element.Element(selector)
|
||
if err != nil {
|
||
return nil, gojs.Err(err)
|
||
}
|
||
return &Element{element: elem, page: el.page}, nil
|
||
}
|
||
|
||
func (el *Element) FindN(selector string) *Element {
|
||
el, err := el.Find(selector)
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
return el
|
||
}
|
||
|
||
// FindAll 在当前元素下查找所有匹配选择器的元素
|
||
func (el *Element) FindAll(selector string) ([]*Element, error) {
|
||
elements, err := el.element.Elements(selector)
|
||
if err != nil {
|
||
return nil, gojs.Err(err)
|
||
}
|
||
result := make([]*Element, len(elements))
|
||
for i, e := range elements {
|
||
result[i] = &Element{element: e, page: el.page}
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
// Find 在当前元素下查找匹配选择器的第一个元素
|
||
func (el *Element) FindX(selector string) (*Element, error) {
|
||
elem, err := el.element.ElementX(selector)
|
||
if err != nil {
|
||
return nil, gojs.Err(err)
|
||
}
|
||
return &Element{element: elem, page: el.page}, nil
|
||
}
|
||
|
||
func (el *Element) FindXN(selector string) *Element {
|
||
el, err := el.FindX(selector)
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
return el
|
||
}
|
||
|
||
// FindAll 在当前元素下查找所有匹配选择器的元素
|
||
func (el *Element) FindAllX(selector string) ([]*Element, error) {
|
||
elements, err := el.element.ElementsX(selector)
|
||
if err != nil {
|
||
return nil, gojs.Err(err)
|
||
}
|
||
result := make([]*Element, len(elements))
|
||
for i, e := range elements {
|
||
result[i] = &Element{element: e, page: el.page}
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
// Submit 提交表单(必须在FORM元素上调用)
|
||
func (el *Element) Submit() error {
|
||
tagName, err := el.element.Eval("el => el.tagName")
|
||
if err != nil {
|
||
return gojs.Err(err)
|
||
}
|
||
if !strings.EqualFold(tagName.Value.String(), "FORM") {
|
||
return gojs.Err(fmt.Errorf("Submit can only be called on a FORM element"))
|
||
}
|
||
_, err = el.element.Eval("form => form.submit()")
|
||
return gojs.Err(err)
|
||
}
|
||
|
||
// Check 勾选复选框
|
||
func (el *Element) Check() error {
|
||
checked, err := el.IsChecked()
|
||
if err != nil {
|
||
return gojs.Err(err)
|
||
}
|
||
if !checked {
|
||
return gojs.Err(el.element.Click(proto.InputMouseButtonLeft, 1))
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// Uncheck 取消勾选
|
||
func (el *Element) Uncheck() error {
|
||
checked, err := el.IsChecked()
|
||
if err != nil {
|
||
return gojs.Err(err)
|
||
}
|
||
if checked {
|
||
return gojs.Err(el.element.Click(proto.InputMouseButtonLeft, 1))
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (el *Element) GetBoundingRect() (*Rect, error) {
|
||
shape, err := el.element.Shape()
|
||
if err != nil {
|
||
return nil, gojs.Err(err)
|
||
}
|
||
|
||
if box := shape.Box(); box != nil {
|
||
return &Rect{X: box.X, Y: box.Y, Width: box.Width, Height: box.Height}, nil
|
||
}
|
||
|
||
return &Rect{}, nil
|
||
}
|
||
|
||
func (el *Element) GetCenter() (*Position, error) {
|
||
rect, err := el.GetBoundingRect()
|
||
if err != nil {
|
||
return nil, gojs.Err(err)
|
||
}
|
||
return &Position{X: rect.X + rect.Width/2, Y: rect.Y + rect.Height/2}, nil
|
||
}
|
||
|
||
// GetName 获取元素名称(如<input name="username">)
|
||
func (el *Element) GetName() (*string, error) {
|
||
return el.GetAttribute("name")
|
||
}
|
||
|
||
// GetID 获取元素ID
|
||
func (el *Element) GetID() (*string, error) {
|
||
return el.GetAttribute("id")
|
||
}
|
||
|
||
// GetType 获取元素类型(如"text"/"checkbox"等)
|
||
func (el *Element) GetType() (*string, error) {
|
||
return el.GetAttribute("type")
|
||
}
|
||
|
||
func (el *Element) AddClass(className string) error {
|
||
_, err := el.element.Eval(`(el, className) => {
|
||
if (!el) return;
|
||
if (el.classList) {
|
||
el.classList.add(className);
|
||
} else if (el.className) {
|
||
const classes = el.className.split(' ');
|
||
if (!classes.includes(className)) {
|
||
classes.push(className);
|
||
el.className = classes.join(' ').trim();
|
||
}
|
||
}
|
||
}`, className)
|
||
return gojs.Err(err)
|
||
}
|
||
|
||
func (el *Element) GetClass() ([]string, error) {
|
||
r, err := el.element.Property("className")
|
||
if err != nil {
|
||
return nil, gojs.Err(err)
|
||
}
|
||
return u.SplitWithoutNone(r.Str(), " "), nil
|
||
}
|
||
|
||
func (el *Element) RemoveClass(className string) error {
|
||
_, err := el.element.Eval(`(el, className) => {
|
||
if (!el) return;
|
||
if (el.classList) {
|
||
el.classList.remove(className);
|
||
} else if (el.className) {
|
||
const classes = el.className.split(' ');
|
||
const index = classes.indexOf(className);
|
||
if (index !== -1) {
|
||
classes.splice(index, 1);
|
||
el.className = classes.join(' ').trim();
|
||
}
|
||
}
|
||
}`, className)
|
||
return gojs.Err(err)
|
||
}
|
||
|
||
// GetStyle 获取指定CSS样式值
|
||
func (el *Element) GetStyle(property string) (string, error) {
|
||
r, err := el.element.Eval(`(el, prop) =>
|
||
window.getComputedStyle(el).getPropertyValue(prop)`,
|
||
property)
|
||
if err != nil {
|
||
return "", gojs.Err(err)
|
||
}
|
||
return r.Value.Str(), nil
|
||
}
|
||
|
||
// SetStyle 设置CSS样式
|
||
func (el *Element) SetStyle(property, value string) error {
|
||
_, err := el.element.Eval(`(el, prop, val) => el.style.setProperty(prop, val)`, property, value)
|
||
return gojs.Err(err)
|
||
}
|
||
|
||
// SetAttribute 设置元素属性值
|
||
// name: 属性名称 value: 属性值
|
||
func (el *Element) SetAttribute(name, value string) error {
|
||
_, err := el.element.Eval(`(e, n, v) => e.setAttribute(n, v)`, name, value)
|
||
return gojs.Err(err)
|
||
}
|
||
|
||
// HasAttribute 检查属性是否存在
|
||
func (el *Element) HasAttribute(name string) (bool, error) {
|
||
r, err := el.element.Eval(`(el, name) => el.hasAttribute(name)`, name)
|
||
if err != nil {
|
||
return false, gojs.Err(err)
|
||
}
|
||
return r.Value.Bool(), nil
|
||
}
|
||
|
||
// GetAttribute 获取元素属性值
|
||
// name: 属性名称
|
||
// 返回值: 属性值字符串,若不存在则返回空字符串
|
||
func (el *Element) GetAttribute(name string) (*string, error) {
|
||
val, err := el.element.Attribute(name)
|
||
return val, gojs.Err(err)
|
||
}
|
||
|
||
// RemoveAttribute 移除元素属性
|
||
// name: 要移除的属性名称
|
||
func (el *Element) RemoveAttribute(name string) error {
|
||
_, err := el.element.Eval(`(e, n) => e.removeAttribute(n)`, name)
|
||
return gojs.Err(err)
|
||
}
|
||
|
||
// SetProperty 设置 JavaScript 属性
|
||
// name: 属性名称 value: 属性值(可以是任意类型)
|
||
func (el *Element) SetProperty(name string, value interface{}) error {
|
||
_, err := el.element.Eval(`(el, n, v) => el[n] = v`, name, value)
|
||
return gojs.Err(err)
|
||
}
|
||
|
||
// GetProperty 获取 JavaScript 属性值
|
||
// name: 属性名称
|
||
// 返回值: 属性值(interface{} 类型)
|
||
func (el *Element) GetProperty(name string) (interface{}, error) {
|
||
prop, err := el.element.Property(name)
|
||
if err != nil {
|
||
return nil, gojs.Err(err)
|
||
}
|
||
return prop.Val(), nil
|
||
}
|
||
|
||
// HasProperty 检查属性是否存在
|
||
func (el *Element) HasProperty(name string) (bool, error) {
|
||
r, err := el.element.Eval(`(el, name) => el.hasOwnProperty(name)`, name)
|
||
if err != nil {
|
||
return false, gojs.Err(err)
|
||
}
|
||
return r.Value.Bool(), nil
|
||
}
|
||
|
||
func (el *Element) RemoveProperty(name string) error {
|
||
_, err := el.element.Eval(`(el, name) => delete el[name]`, name)
|
||
return gojs.Err(err)
|
||
}
|
||
|
||
// IsInteractable 元素是否可交互
|
||
func (el *Element) IsInteractable() (bool, error) {
|
||
_, err := el.element.Interactable()
|
||
if errors.Is(err, &rod.NotInteractableError{}) {
|
||
return false, nil
|
||
}
|
||
if err != nil {
|
||
return false, gojs.Err(err)
|
||
}
|
||
return true, nil
|
||
}
|
||
|
||
func (el *Element) WaitStable(ms *int) error {
|
||
if ms == nil {
|
||
ms = u.IntPtr(100)
|
||
}
|
||
return gojs.Err(el.element.Timeout(time.Second).WaitStable(time.Millisecond * time.Duration(*ms)))
|
||
}
|
||
|
||
// 元素级
|
||
func (el *Element) Press(key string) error {
|
||
if err := el.element.Focus(); err != nil {
|
||
return gojs.Err(err)
|
||
}
|
||
return gojs.Err(el.element.Page().Keyboard.Press(input.Key(key[0])))
|
||
}
|
||
|
||
// IsVisible 判断元素是否可见
|
||
func (el *Element) IsVisible() (bool, error) {
|
||
visible, err := el.element.Visible()
|
||
if err != nil {
|
||
return false, gojs.Err(err)
|
||
}
|
||
return visible, nil
|
||
}
|
||
|
||
// GetPosition 获取元素位置(中心点)
|
||
func (el *Element) GetPosition() (*Position, error) {
|
||
shape, err := el.element.Shape()
|
||
if err != nil {
|
||
return nil, gojs.Err(err)
|
||
}
|
||
rect := shape.Box()
|
||
return &Position{
|
||
X: rect.X + rect.Width/2,
|
||
Y: rect.Y + rect.Height/2,
|
||
}, nil
|
||
}
|
||
|
||
// GetComputedStyle 获取元素所有计算样式
|
||
func (el *Element) GetComputedStyle() (map[string]string, error) {
|
||
r, err := el.element.Eval(`(el) => {
|
||
const styles = window.getComputedStyle(el);
|
||
const result = {};
|
||
for (let i = 0; i < styles.length; i++) {
|
||
const prop = styles[i];
|
||
result[prop] = styles.getPropertyValue(prop);
|
||
}
|
||
return result;
|
||
}`)
|
||
if err != nil {
|
||
return nil, gojs.Err(err)
|
||
}
|
||
|
||
style := r.Value.Map()
|
||
result := make(map[string]string)
|
||
for key, value := range style {
|
||
result[key] = value.String()
|
||
}
|
||
return result, nil
|
||
}
|