support use chrome
This commit is contained in:
parent
ea04fd82c8
commit
bf65f6ccfa
80
chrome.go
Normal file
80
chrome.go
Normal file
@ -0,0 +1,80 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"apigo.cc/gojs"
|
||||
"apigo.cc/gojs/goja"
|
||||
"github.com/go-rod/rod"
|
||||
"github.com/go-rod/rod/lib/launcher"
|
||||
"github.com/ssgo/log"
|
||||
"github.com/ssgo/u"
|
||||
)
|
||||
|
||||
type Chrome struct {
|
||||
id string
|
||||
launcher *launcher.Launcher
|
||||
browser *rod.Browser
|
||||
}
|
||||
|
||||
var chromes = map[string]*Chrome{}
|
||||
var chromesLock sync.Mutex
|
||||
|
||||
func (ch *Chrome) Close() {
|
||||
if ch.browser != nil {
|
||||
ch.browser.Close()
|
||||
ch.browser = nil
|
||||
}
|
||||
if ch.launcher != nil {
|
||||
ch.launcher.Cleanup()
|
||||
ch.launcher = nil
|
||||
}
|
||||
|
||||
chromesLock.Lock()
|
||||
delete(chromes, ch.id)
|
||||
chromesLock.Unlock()
|
||||
}
|
||||
|
||||
func CloseAllChrome() {
|
||||
n := len(chromes)
|
||||
if n > 0 {
|
||||
for _, ch := range chromes {
|
||||
ch.Close()
|
||||
}
|
||||
log.DefaultLogger.Info("关闭所有 Chrome 浏览器", "count", n)
|
||||
}
|
||||
}
|
||||
|
||||
func StartChrome(showWindow *bool, vm *goja.Runtime) (*Chrome, error) {
|
||||
logger := gojs.GetLogger(vm)
|
||||
ch := &Chrome{}
|
||||
ch.browser = rod.New()
|
||||
if localBrowserPath, hasLocalBrowser := launcher.LookPath(); hasLocalBrowser {
|
||||
ch.launcher = launcher.New().Bin(localBrowserPath).Headless(!u.Bool(showWindow)).Set("no-sandbox").Set("disable-gpu").Set("disable-dev-shm-usage").Set("single-process")
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
ch.launcher.Set("disable-setuid-sandbox")
|
||||
case "windows":
|
||||
ch.launcher.Set("disable-features=RendererCodeIntegrity")
|
||||
}
|
||||
localChromeURL, err := ch.launcher.Launch()
|
||||
if err != nil {
|
||||
ch.Close()
|
||||
return nil, gojs.Err(err)
|
||||
}
|
||||
logger.Info("启动本地 Chrome 浏览器", "url", localChromeURL, "path", localBrowserPath)
|
||||
ch.browser.ControlURL(localChromeURL)
|
||||
}
|
||||
if err := ch.browser.Connect(); err != nil {
|
||||
ch.Close()
|
||||
return nil, gojs.Err(err)
|
||||
}
|
||||
|
||||
ch.id = u.UniqueId()
|
||||
chromesLock.Lock()
|
||||
chromes[ch.id] = ch
|
||||
chromesLock.Unlock()
|
||||
return ch, nil
|
||||
|
||||
}
|
26
chrome_test.go
Normal file
26
chrome_test.go
Normal file
@ -0,0 +1,26 @@
|
||||
package http_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"apigo.cc/gojs"
|
||||
_ "apigo.cc/gojs/console"
|
||||
_ "apigo.cc/gojs/file"
|
||||
_ "apigo.cc/gojs/http"
|
||||
_ "apigo.cc/gojs/util"
|
||||
"github.com/ssgo/u"
|
||||
)
|
||||
|
||||
func TestChrome(t *testing.T) {
|
||||
gojs.ExportForDev()
|
||||
r, err := gojs.RunFile("chrome_test.js")
|
||||
if err != nil {
|
||||
t.Fatal(u.Red("chrome_test"), u.BRed(err.Error()))
|
||||
} else if r != true {
|
||||
t.Fatal(u.Red("chrome_test"), u.BRed(u.JsonP(r)))
|
||||
} else {
|
||||
fmt.Println(u.Green("chrome_test"), u.BGreen("test succeess"))
|
||||
}
|
||||
gojs.WaitAll()
|
||||
}
|
199
chrome_test.js
Normal file
199
chrome_test.js
Normal file
@ -0,0 +1,199 @@
|
||||
import http from 'apigo.cc/gojs/http'
|
||||
import co from 'apigo.cc/gojs/console'
|
||||
import u from 'apigo.cc/gojs/util'
|
||||
import fs from 'apigo.cc/gojs/file'
|
||||
|
||||
let t1, t2
|
||||
function printOK(...testResult) {
|
||||
t2 = u.timestampMS()
|
||||
co.info('. >> 成功', '耗时[' + (t2 - t1) + ']', !testResult ? '' : testResult.join(' '))
|
||||
t1 = t2
|
||||
}
|
||||
|
||||
co.log("开始测试...")
|
||||
t1 = u.timestampMS()
|
||||
|
||||
co.log("启动Chrome浏览器...")
|
||||
let ch = http.startChrome()
|
||||
printOK("Chrome启动成功")
|
||||
|
||||
var page
|
||||
|
||||
co.log("打开z20首页...")
|
||||
page = ch.open("https://z20.cc/")
|
||||
printOK()
|
||||
|
||||
co.log("设置视口大小...")
|
||||
page.setViewport(1280, 720)
|
||||
printOK("1280×720")
|
||||
|
||||
co.log("获取当前URL...")
|
||||
let url = page.url()
|
||||
printOK(url)
|
||||
|
||||
co.log("获取页面标题...")
|
||||
const title = page.title()
|
||||
printOK(title.substring(0, 15) + (title.length > 15 ? "..." : ""))
|
||||
|
||||
co.log("导航到Apigo.cc首页...")
|
||||
page.navigate("https://apigo.cc")
|
||||
printOK()
|
||||
|
||||
co.log("获取当前URL...")
|
||||
url = page.url()
|
||||
printOK(url)
|
||||
|
||||
co.log("获取资源列表...")
|
||||
let res = page.getResList()
|
||||
printOK(`资源列表(${res.length})获取成功`)
|
||||
|
||||
co.log("获取资源列表(按类型script)...")
|
||||
res = page.getResListByType("script")
|
||||
printOK(`资源列表(${res.length})获取成功`)
|
||||
|
||||
co.log("获取资源列表(按MimeType)...")
|
||||
res = page.getResListByMimeType("text/*")
|
||||
printOK(`资源列表(${res.length})获取成功`)
|
||||
|
||||
co.log("获取资源内容...")
|
||||
let content = page.getResContent(res[0].url)
|
||||
printOK(`资源(${res[0].url})内容(${content.length}/${res[0].size})获取成功`)
|
||||
|
||||
co.log("执行后退操作...")
|
||||
page.back()
|
||||
printOK()
|
||||
|
||||
co.log("获取当前URL...")
|
||||
url = page.url()
|
||||
printOK(url)
|
||||
|
||||
co.log("执行前进操作...")
|
||||
page.forward()
|
||||
printOK()
|
||||
|
||||
co.log("获取当前URL...")
|
||||
url = page.url()
|
||||
printOK(url)
|
||||
|
||||
co.log("滚动页面...")
|
||||
page.scrollTo(0, 500)
|
||||
printOK()
|
||||
|
||||
co.log("查找Logo...")
|
||||
const logo = page.find(".logo")
|
||||
printOK("已找到")
|
||||
|
||||
co.log("检查Logo可见性...")
|
||||
printOK(logo.isVisible())
|
||||
|
||||
co.log("获取Logo 尺寸...")
|
||||
printOK(logo.getAttribute("width"), logo.getAttribute("height"))
|
||||
|
||||
co.log("获取Logo alt...")
|
||||
printOK(logo.getAttribute("alt"))
|
||||
|
||||
co.log("获取Logo URL...")
|
||||
printOK(logo.getAttribute("src"))
|
||||
|
||||
co.log("获取Logo位置信息...")
|
||||
const rect = logo.getBoundingRect()
|
||||
printOK(`X:${rect.x} Y:${rect.y} W:${rect.width} H:${rect.height}`)
|
||||
|
||||
co.log("导航到Apigo.cc探索页...")
|
||||
// page = ch.open("https://apigo.cc/explore/repos")
|
||||
page.navigate("https://apigo.cc/explore/repos")
|
||||
printOK()
|
||||
|
||||
co.log("获取当前URL...")
|
||||
url = page.url()
|
||||
printOK(url)
|
||||
|
||||
co.log("查找搜索框...")
|
||||
var searchInput = page.findN("input[type=search]")
|
||||
if (!searchInput) {
|
||||
return "搜索框不存在"
|
||||
}
|
||||
printOK("存在")
|
||||
|
||||
co.log("查找搜索框父...")
|
||||
const parent = searchInput.parent()
|
||||
printOK("存在", parent.getAttribute('class'))
|
||||
|
||||
co.log("查找搜索按钮...")
|
||||
const searchBtn = parent.find("button")
|
||||
printOK("存在")
|
||||
printOK("存在", searchBtn.getAttribute('class') == searchBtn.getProperty('className') ? 'class检查通过' : 'class检查失败')
|
||||
|
||||
co.log("查找搜索按钮(findChild)...")
|
||||
parent.findChild("button")
|
||||
printOK("成功")
|
||||
|
||||
co.log("查找搜索按钮(findX)...")
|
||||
parent.findX("./button")
|
||||
printOK("成功")
|
||||
|
||||
co.log("查找表单...")
|
||||
const form = searchInput.findParent('form')
|
||||
printOK("存在", form.getAttribute('id'))
|
||||
|
||||
co.log("检查搜索框可交互性...")
|
||||
printOK(searchInput.isInteractable())
|
||||
|
||||
co.log("设置搜索框值...")
|
||||
searchInput.setValue("ag")
|
||||
printOK("值已设置")
|
||||
|
||||
co.log("等待跳转搜索结果...")
|
||||
page.waitPageLoad()
|
||||
searchInput = page.find("input[type=search]")
|
||||
printOK("完成", page.url())
|
||||
|
||||
co.log("获取搜索框值...")
|
||||
printOK(searchInput.getValue())
|
||||
|
||||
co.log("查找所有结果链接...")
|
||||
var list = page.findAll(".flex-list > .flex-item")
|
||||
printOK(`找到 ${list.length} 个链接`)
|
||||
|
||||
co.log("查找第二个项目的仓库连接...")
|
||||
const link = list[0].find("a:nth-child(2)")
|
||||
printOK(link.getAttribute("href"))
|
||||
|
||||
co.log("计算按钮中心位置...")
|
||||
const linkCenter = link.getCenter()
|
||||
printOK(`X:${linkCenter.x.toFixed(1)} Y:${linkCenter.y.toFixed(1)}`)
|
||||
|
||||
co.log("移动鼠标到按钮中心...")
|
||||
page.mouseMoveTo(linkCenter.x, linkCenter.y)
|
||||
printOK()
|
||||
|
||||
co.log("按下鼠标左键...")
|
||||
page.mouseClick()
|
||||
printOK()
|
||||
|
||||
co.log("等待搜索结果稳定...")
|
||||
page.waitPageLoad()
|
||||
printOK()
|
||||
|
||||
co.log("获取当前URL...")
|
||||
url = page.url()
|
||||
printOK(url)
|
||||
|
||||
co.log("截取页面截图...")
|
||||
const screenshot1 = page.screenshot()
|
||||
printOK(`size:${screenshot1.length}`)
|
||||
// fs.write('./screenshot1.png', screenshot1)
|
||||
|
||||
co.log("截取页面截图(全部)...")
|
||||
const screenshot2 = page.screenshotFullPage()
|
||||
printOK(`size:${screenshot2.length}`)
|
||||
// fs.write('./screenshot2.png', screenshot2)
|
||||
|
||||
co.log("生成PDF...")
|
||||
const pdf = page.pDF()
|
||||
printOK(`size:${pdf.length}`)
|
||||
// fs.write('./page.pdf', pdf)
|
||||
|
||||
co.log("所有测试步骤完成!")
|
||||
|
||||
return true
|
57
go.mod
57
go.mod
@ -1,41 +1,48 @@
|
||||
module apigo.cc/gojs/http
|
||||
|
||||
go 1.18
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
apigo.cc/gojs v0.0.1
|
||||
apigo.cc/gojs v0.0.21
|
||||
apigo.cc/gojs/console v0.0.2
|
||||
apigo.cc/gojs/file v0.0.4
|
||||
apigo.cc/gojs/util v0.0.12
|
||||
github.com/go-rod/rod v0.112.8
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/ssgo/config v1.7.9
|
||||
github.com/ssgo/httpclient v1.7.8
|
||||
github.com/ssgo/u v1.7.9
|
||||
github.com/ssgo/log v1.7.7
|
||||
github.com/ssgo/s v1.7.24
|
||||
github.com/ssgo/u v1.7.21
|
||||
)
|
||||
|
||||
require (
|
||||
apigo.cc/gojs/console v0.0.1 // indirect
|
||||
github.com/ZZMarquis/gm v1.3.2 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/emmansun/gmsm v0.30.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
|
||||
github.com/gomodule/redigo v1.9.2 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
|
||||
github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
|
||||
github.com/obscuren/ecies v0.0.0-20150213224233-7c0f4a9b18d9 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
|
||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||
github.com/ssgo/discover v1.7.8 // indirect
|
||||
github.com/ssgo/redis v1.7.7 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.14 // indirect
|
||||
github.com/tklauser/numcpus v0.8.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/dlclark/regexp2 v1.11.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
|
||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/ssgo/config v1.7.7 // indirect
|
||||
github.com/ssgo/log v1.7.7
|
||||
github.com/ssgo/s v1.7.13
|
||||
github.com/ssgo/discover v1.7.10 // indirect
|
||||
github.com/ssgo/redis v1.7.8 // indirect
|
||||
github.com/ssgo/standard v1.7.7 // indirect
|
||||
github.com/ssgo/tool v0.4.27 // indirect
|
||||
golang.org/x/net v0.30.0 // indirect
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
golang.org/x/text v0.19.0 // indirect
|
||||
github.com/ssgo/tool v0.4.29 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
||||
github.com/ysmood/goob v0.4.0 // indirect
|
||||
github.com/ysmood/gson v0.7.3 // indirect
|
||||
github.com/ysmood/leakless v0.9.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
golang.org/x/crypto v0.40.0 // indirect
|
||||
golang.org/x/net v0.42.0 // indirect
|
||||
golang.org/x/sys v0.34.0 // indirect
|
||||
golang.org/x/text v0.27.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
33
http.go
33
http.go
@ -12,6 +12,7 @@ import (
|
||||
"apigo.cc/gojs"
|
||||
"apigo.cc/gojs/goja"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/ssgo/config"
|
||||
"github.com/ssgo/httpclient"
|
||||
"github.com/ssgo/log"
|
||||
"github.com/ssgo/u"
|
||||
@ -31,8 +32,13 @@ var defaultHttp = &Http{
|
||||
globalHeaders: make(map[string]string),
|
||||
}
|
||||
|
||||
// TODO ws
|
||||
var conf = struct {
|
||||
Timeout int
|
||||
ChromePath string
|
||||
}{}
|
||||
|
||||
func init() {
|
||||
config.LoadConfig("http", &conf)
|
||||
obj := map[string]any{
|
||||
"new": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
|
||||
return newClient("HTTP", argsIn, vm)
|
||||
@ -51,11 +57,28 @@ func init() {
|
||||
"connect": defaultHttp.Connect,
|
||||
}
|
||||
|
||||
obj1 := map[string]any{
|
||||
"startChrome": StartChrome,
|
||||
"closeAllChrome": CloseAllChrome,
|
||||
}
|
||||
tsCode := gojs.MakeTSCode(obj1)
|
||||
|
||||
mappedObj := gojs.ToMap(obj1)
|
||||
for k, v := range mappedObj {
|
||||
obj[k] = v
|
||||
}
|
||||
|
||||
gojs.Register("apigo.cc/gojs/http", gojs.Module{
|
||||
Object: obj,
|
||||
TsCode: httpTS,
|
||||
Desc: "",
|
||||
Example: "",
|
||||
Object: obj,
|
||||
// TsCode: httpTS,
|
||||
TsCodeMaker: func() string {
|
||||
a := strings.SplitN(tsCode, "export default {", 2)
|
||||
exportPart1 := "export default {\n" + strings.TrimRight(a[1], "}")
|
||||
return strings.Replace(httpTS, "export default {", exportPart1, 1) + "\n" + a[0]
|
||||
},
|
||||
Desc: "",
|
||||
Example: "",
|
||||
WaitForStop: CloseAllChrome,
|
||||
})
|
||||
}
|
||||
|
||||
|
3
http.ts
3
http.ts
@ -26,6 +26,9 @@ function upload(url: string, form: Object, files: Object, headers?: Object): Res
|
||||
function download(filename: string, url: string, callback?: (finished: number, total: number) => void, headers?: Object): Result { return null as any }
|
||||
function connect(url: string, config?: WSConfig): WS { return null as any }
|
||||
|
||||
interface Chrome {
|
||||
}
|
||||
|
||||
interface Client {
|
||||
get(url: string, headers?: Object): Result
|
||||
head(url: string, headers?: Object): Result
|
||||
|
Loading…
x
Reference in New Issue
Block a user