devops 多浏览器同步测试工具的设计与实现

81—1 · 2022年10月10日 · 最后由 Pharaoh97 回复于 2024年04月19日 · 57036 次阅读
本帖已被设为精华帖!

背景

在做 Web 兼容测试时,测试人员往往需要在不同浏览器上重复执行相同的操作。
现有自动化录制手段,其实是后置的对比,效率与反馈都存在延迟,执行过程相对是黑盒的,过程中如果测试人员没细化到具体的校验点,即使是很明显的样式差异,脚本也很难发现。且如果是脚本或浏览器差异的问题,自动化运行的方式并不能够及时手动调整容错。

于是便思考有没有一种实时操作,而且可以便捷校验方案。

通过调研了 browsersync、uirecorder 等工具后,我设计了如下的同步兼容测试工具。

架构设计

系统主要由四个区块组成:

  • Web 前端:显示可用浏览器,触发同步操作任务,展示 VNC 连接。
  • 服务端:提供数据接口给前端,调用本地脚本,构建任务执行环境。
  • 同步驱动(自己起了个名字:yutu):下图中小兔子那部分,一个可单独调用的 npm 库,提供全局命令执行同步操作任务。
  • 浏览器池:我使用的是基于 selenoid 的本地容器管理系统,当然也可以换成别的,看自己需求。

总体来说前端和服务端的工作量是比较小的,只要关注任务创建和 vnc 连接展示即可。
系统的核心在于驱动层,它是每个任务同步操作、对比的中心。而浏览器池采用 selenium-gird 或 solenoid 都是可以的,选择适合维护的即可。

功能点

  • 云浏览器版本管理池
  • 多浏览器操作同步
  • 操作脚本录制,日志记录
  • 元素图片相似度对比

实现效果

操作同步过程中,可以实时看到从浏览器执行情况,也可以通过列表状态颜色来判断。

同步出错的浏览器,执行过程中可以手动进去确认下是不是问题

图像相似度对比,可以自定义允许的差异值。主要是方便测试人员快速识别差异的位置,辅助人工判断。

驱动设计

开源地址

https://github.com/t880216t/yutu-tools.git

开发背景

本系统的核心是同步驱动,这里我且称之为 yutu, 它是在uirecorder项目上,经深度自定义开发而来,如果你查看它源码,不难发现很多 uirecorder 的影子。起初本打算结合browsersync的侵入式脚本实现操作同步功能,但在建设f2etest版本的浏览器云项目时遇到了它,官方版本主要用来做操作录制的,其中有个本地实时对比校验的附属功能,这正合我意,开始撸它源码。

起初在本地 chrome 上一切顺利,但接入 selenoid 浏览器池后,开始对接 firefox 时发现了问题。uirecorder的核心jwebdriver不支持最新的 W3C 协议,而且从钉钉群里官方反馈情况来看,这个项目 2 年没更新了,多半是夭折了。没办法自己从头撸吧,于是采用最新的WebdriverIO客户端结合自身的需求,对其进行了深度的改造,从而有了yutu,在此也感谢下前人努力与开源。

数据流程

功能简介

它主要是一个命令行工具,通过 sudo npm i -g yutu-tools 全局安装到系统中.
其中它的图片对比能力是来自于graphicsmagick,因此还需要额外安装下
mac:brew install graphicsmagick

yutu 对外主要提供以下两个命令行功能:

  • yutu init:初始化任务目录及配置文件
  • yutu start:执行任务并输出日志

效果展示

所以其实从上面的设计图不难发现,它本身就是个独立的工具,可以不依赖于整体系统来使用,是否调用远程浏览器是可以通过config.json来配置的,如果是 serverIp 是127.0.0.1那么就会调用本地的 chromedriver 来操作(本地其它浏览器调用功能在开发设计中),开发过程中,本地调试时可以方便快速定位驱动问题。

主要改动点

命令行使用

原先的工程采用的是本地控制台交互问答式的参数配置方式,这肯定不适合我们平台化嵌入,同时为了更强大的功能开发和更复杂的参数支持,我将摒弃了命令行参数的配置方式,现在核心的配置参数都直接读取初始化后config.json,因此如果是接入系统,那么可以用脚本复写配置 json,如果本地调试,那么手动维护下 json 即可。

使用示例:

$ yutu init

修改 config.json 参数,配置内容及格式如下:

{
    "webdriver": {
        "host": "127.0.0.1",  // 远程hub地址
        "port": "4444",
        "mainBrowser": {
            "browserId": 2,
            "displayName": "chrome",
            "browserName": "chrome",
            "version": "106",
            "httpProxy": "",
            "binary": null
        },
        "syncBrowsers": [
            {
                "browserId": 1,  // 浏览器的唯一标识
                "proxy": "",  // 自定义参数,暂未启用
                "screenSize": "1920x1080x24", // 自定义参数,暂未启用
                "browserName": "firefox",  // 浏览器内核的名字,如:chrome、firefox
                "displayName": "firefox",   // 浏览器的名字如:qq、yandex
                "version": "105",
                "binary": null   // chromium内核的国产浏览器的exe执行文件路径
            }
        ]
    },
    "browserSize": "1920x1080x24",
    "defaultUrl": "https://www.baidu.com/",
    "vars": {},
    "serverIp": "192.168.1.101",  //本地执行命令机器的ip,非远程webdriver,可以使用127.0.0.1
    .....
}

启动同步服务

$ yutu start

自定义驱动

在用WebdriverIO替换掉jwebdriver后,原先的很多 api 都改变了,这需要我们对 driver 对象进行深度的包装改造,于是我在本地增加了个 browser 对象,用来代理WebdriverIO的 driver 对象,在其中增加我们需要的 driver 扩展能力。

这里需要用到 nodejs 的 Proxy 机制、链式调用、Promise 等写法。(感谢前端同事龙哥)

class mBrowser {
    constructor(browser) {
        const handers = {
            get(obj, key){
                return key in obj?obj[key]: browser[key]
            }
        }
        return new Proxy(this, handers);
    }
...

浏览器插件

yutu 能够将用户操作回传给 socket server 的关键是依靠一个浏览器插件,它只会在启动主控制浏览器时,通过goog:chromeOptions参数将插件以文件数据流传给 chrome 浏览器,因此我们的主控浏览器默认也必须是 chrome。这个插件本体还是uirecorder的,只做了对接yutu的适应的调整。

var crxPath = path.resolve(__dirname, '../tool/uirecorder.crx');
var extContent = fs.readFileSync(crxPath).toString('base64');
capabilities["goog:chromeOptions"] = {
    args: ['--disable-bundled-ppapi-flash'],
    prefs: {
        'plugins.plugins_disabled': ['Adobe Flash Player']
    },
    excludeSwitches: ['enable-automation'],
    extensions: [extContent],
};

在我的需求里,插件中还有很多需要优化的地方,后面有空慢慢搞吧,目前改动的主要是插件启动页面接收参数、动态服务器 ip 等:

if (mapParams.defaultUrl && txtUrl){
        txtUrl.value = decodeURIComponent(mapParams.defaultUrl);
    }
function connectServer(data){
    const {ip, port} = data
    console.log('data', data);
    if(!wsSocket){
        wsSocket = new WebSocket('ws://'+ ip + ':' + port, "protocolOne");
...

修改插件的 js 后,需要重新打包生成插件 crx。

$ ./buildcrx.sh

前端设计

这个系统前端部分主要是 2 个页面:

  • 浏览器列表
  • 同步操作页面(加几个弹层 Modal)


因为考虑到客户实际使用的是以 windows 为主,为了保证测试结果的准确性,所以我们这里浏览器运行镜像主要是自定义封装的 windows 系统镜像(太痛苦了,此处包含泪水,详见下文解读)。

在代码方面,继续秉承组件化思想,结合 antd pro 的高阶组件,对多处进行了抽象复用。

import { ProCard, ProTable } from '@ant-design/pro-components';

操作部分,结合了较为小众但稳定可靠的react-vnc库,同时为了降低用户的浏览器资源消耗,操作页面在同步浏览器列表展开时才会进行 vnc 连接展示。

<Sider width={'20%'} collapsible collapsed={collapsed} onCollapse={collapsed => this.setState({collapsed})}>
  <Card size="small" title={!collapsed? "同步浏览器列表": '同步'} >
    {syncBrowsers && syncBrowsers.length > 0? (
      syncBrowsers.map(item => (
        <Card.Grid key={item.sessionId} className={styles.syncContainer}>
          {!collapsed ? (
            <VncScreen
              url={item.vncUrl}
              rfbOptions={{
                credentials: {
                  password: 'selenoid',
                },
              }}
              scaleViewport
              background='#000000'
              style={{
                height: '100%'
              }}
            />
          ): (
            <div className={styles.browserName}><img src={`/${item?.browserName}.react.svg`} alt='' />{item.browserName}</div>
          )}
          <div ref={n => (this[`hover_${item.sessionId}`] = n)} className={styles.hoverContainer}>
            <SyncModal data={item} actions={this.state.syncActions[item.sessionId]} />
          </div>
        </Card.Grid>
      ))
    ): (
      <Empty />
    )}
  </Card>
</Sider>

服务端设计

服务端主要是提供数据给前端展示,以及启动脚本调用系统命令的,接口部分千篇一律,增删改查而已,不展开介绍了。

这里有个小细节,yutu本身不会和数据库已经服务端进行交互的,所以它的任务运行状态,需要告知脚本是个麻烦事。我是通过脚本监控单个任务进程的控制台信息,来达成的,这样的成本最小,也不必让 2 个工具过度耦合。

async def start_task(self):
    logger.info(f'Task {self.task_id} is starting...')
    run_cmd = f'yutu case.spec.js' \
              f' --browser_size={self.task_info["screen"]}' \
              f' --http_proxy={self.task_info["proxy"]}' \
              f' --default_url={self.task_info["url"]}'
    logger.info(f'start cmd: {run_cmd} ')
    p = subprocess.Popen(
      run_cmd,
      shell=True,
      stdout=subprocess.PIPE,
      stderr=subprocess.STDOUT,
      encoding='utf-8',
      cwd=self.task_dir_path
    )
    for i in iter(p.stdout.readline, 'utf-8'):
      if 'consoleParams:' in i:
        try:
          data = json.loads(i.replace('consoleParams:', ''))
          singal = await self.update_task_info(data)
          print(data)
          if not singal:
            break
        except Exception as e:
          print(e)
async def update_task_info(self, data):
    if not data:
      return
    row = BrcSyncTask.query.filter_by(id=self.task_id).first()
    if data['type'] == 'server':
      row.sync_server_ip = data['serverAddress']
      row.sync_server_port = data['serverPort']
      db.session.commit()
    elif data['type'] == 'main':
      row.main_session_id = data['sessionId']
      db.session.commit()
    elif data['type'] == 'sync':
      info = json.loads(row.sync_sessions) if row.sync_sessions else {}
      info[data['browserInfo']] = data['sessionId']
      row.sync_sessions = json.dumps(info)
      db.session.commit()
    elif data['type'] == 'signal':
      if data['status'] == 'ready':
        self.update_task_status(5)  # 开始同步
      if data['status'] == 'end':
        self.update_task_status(3)  # 同步结束
        db.session.flush()
        return False
    return True

自定义镜像封装

此处主要介绍本地封装 windows 版本的 selenoid 浏览器镜像的心得,懂得都懂,就不详细展开介绍了。
为什么要封装 windows 镜像,有 2 个原因。

  • 还原用户场景:用户用的是 windows 系统,官方提供的 liunx 版本浏览器镜像不能代表实际的浏览器使用场景。
  • 方便环境隔离:直接调用单个主机的 webdriver 的方式,无法实现同一时间不同用户的远程操作和代理隔离(f2etest 的 webdriver 云方案无法远程操作)。

关于 windows 封装的基础教程可以参考:windows-images

不过按照教程走下去后会发现,有可能你的容器能启动,但死活连不上浏览器 driver。
再通过反复试验后,我采用了 selenoid+selenoid 的方式,才让流程通起来。

容器内的 selenoid 服务

关键在于在浏览器和 driver 都安装后,再在 windows 里启动一个 selenoid 服务,让它来提供 4444 端口服务给外部的 selenoid hub 调用,由它来和容器内的浏览器 driver 进行交互。

为了方便复用,我在基础镜像中就加入这个基础工具包,文件目录如下:

start.bat 是一个封装后的执行文件,参数可以根据自己设备性能调整,内容如下:

C:\selenoid-windows\selenoid_windows_386.exe -conf C:\selenoid-windows\browsers.json -disable-docker -limit 4 -service-startup-timeout 240s -session-attempt-timeout 240s -session-delete-timeout 240s -timeout 240s > C:\selenoid-windows\selenoid.log 2>&1

browsers.json 如下

{
    "MicrosoftEdge": {
        "default": "18",
        "versions": {
            "18": {
                "image": [ "C:\\selenoid-windows\\webdrivers\\msedgedriver.exe", "--host=127.0.0.1", "--verbose" ]
            }
        }
    }
}

容器内的 flask 服务

看上面的工具包内容可以看到,我们还在里面起了个 flask 轻量服务,它的作用是接收外部传过来的配置参数,动态设置当前容器中的分辨率和系统代理。这个问题是 windows 镜像特有的,selenoid 官方团队说解决不了,为此也做过解释,
windows starts 1024x768 resolution even SCREEN_RESOLUTION changed to 1920x1080x24

我贡献的这个方法可以曲线解决这个问题,步骤也很简单,

  • 1.在基础镜像中安装个 python(2、3 随意)
  • 2.安装工具包中依赖(关键是 flask、pywin32、winproxy 这几个库)
  • 3.容器退出保存前,启动 flask、selenoid 服务

flask 中的内容如下:

from flask import Flask
from flask import request
from flask import jsonify
import win32api
from winproxy import ProxySetting

app = Flask(__name__)

def setProxy(host, port):
    proxy = ProxySetting()
    proxy.enable = True
    proxy.server = f"{host}:{port}"
    proxy.override = ["127.*","192.168.*","10.*"]
    proxy.registry_write()

def setScreen(width, height):
    dm = win32api.EnumDisplaySettings(None, 0)
    dm.PelsWidth = int(width)
    dm.PelsHeight = int(height)
    dm.BitsPerPel = 32
    dm.DisplayFixedOutput = 0
    win32api.ChangeDisplaySettings(dm, 0)

@app.route('/setDisplay', methods=['GET'])
def index():
    height = request.args.get('height')
    width = request.args.get('width')
    host = request.args.get('host')
    port = request.args.get('port')
    setScreen(width, height)
    if host and port:
        setProxy(host, port)

    return jsonify({'width': width, 'height': height})

if __name__ == '__main__':
    app.run(host='127.0.0.1', port=5000)

由于这个服务是起在容器里的,因此我们可以在 yutu 的 driver 建立后,通过固定的 url 来调用,而不必维护容器的 ip 和网络情况。

if (configJson.webdriver.host !== '127.0.0.1'){
    const setScreenUrl = `http://127.0.0.1:5000/setDisplay?width=${width}&height=${height}&host=${hostname}&port=${port}`
    await driver.url(setScreenUrl);
}

开发过程中坑

项目真正开发到完成,投入 1.5 人/月左右,按照时间顺序来回顾这过程中的坑吧。

  • 同步浏览器从一个改成 N 个后,浏览器关闭异常。
    结合异步转同步的方式,改造关闭浏览器方法,保证执行有序

    async function closeBrowser (){
    if(syncCheckBrowserDrivers){
        for (var browserIndex in syncCheckBrowserDrivers){
            var browser = syncCheckBrowserDrivers[browserIndex];
            await new Promise((resolve) => {
                const {browserName, browserVersion } = browser;
                browser.close(() => {
                    console.log(`${__('checker_browser_closed')} ${browserName} ${browserVersion}`.green);
                    resolve();
                })
            })
        }
    }
    }
    
  • 原先的 webdriver 客户端不支持 w3c 协议,导致新浏览器无法正常同步操作
    这是我万万没想到的,虽然知道前端技术日新月异,用外部老的工具,肯定会有需要兼容的差异,但没想到 ali 老大哥们也摆烂了,也导致了我在中后期几乎重写了大部分的 yutu 中的同步操作方法。

  • windows 镜像的产出后,运行不起来
    一开始并没有明确定义 windows 支持对于我们系统的重要性,因此一直在以 liunx 版本的浏览器作为最小试验对象,但后期流程通后,要交付时才发现没有以用户实际的使用场景为目标,这样即使交付也没啥用处。
    因此开始硬啃,为了突破这个技术难点,我的工作机被刷成 ubuntu 系统,好不容易产出了 windows 镜像后,又发现镜像运行很艰难,我的小破机根本带不动。

    终于在领导的关照下,搞个高配的开发机器,运行流畅。

  • windows 系统分辨率超出,操作界面显示不全的问题
    原先的 selenoid 系统,是通过传给 qemu 分辨率参数来设置系统环境变量的。但 windows 版本并没有这样的能力,老外给的方案是默认启动时就给最大的分辨率,在通过设置浏览器窗口大小来实现指定大小分辨率的测试执行,最起码这样显示内容是全的。但这对于我这种可能需要用户手动去 vnc 操作的方式来说,并不适用,用户一旦不小心点了最大化,那么同步浏览器的比例就完全乱套了。因此结合自己 python 脚本经验,深度挖掘了上述更为灵活的 flask server 方案。

  • windows 系统代理无法通过环境变量设置
    原先的 selenoid,我只要在启动容器时给个 env 变量就可以指定代理了,但 windows 版本这样不行。有了上面的分辨率解决经验,我通过 python 的 winproxy 库进行了处理。

  • 非主流浏览器借用的别人的内核,缺少浏览器驱动
    这里的非主流浏览器其实不光指的是我们的一众换皮国产浏览器,国外的 opera、yandex 等等其实也是换皮 chromium。这些家伙的兼容才是真的坑,我到现在还没填完。
    下面就 qqbrowser 举个例子吧:
    它的内核是 chrome 94 的,在启动时要传的浏览器名称 “chrome”,而我们系统中本身也可能有 chrome 这个版本。因此要区分开,我是自定义一个版本区间给它,如:chrome 1~11,同样在 yutu 中要做别名区分,启动时传 chrome,记录时要记 qq。

    {"chrome": {
        "default": "94",
        "versions": {
            "94": {
                "image": "windows/chrome:80",
                "port": "4444",
                "path": "/wd/hub"
            },
            "11": {
                "image": "windows/qq:11", 
                "port": "4444",
                "path": "/wd/hub"
            }
        }
    }
    }
    
    var browserNameToDriver = {
    'yandex': 'chrome',
    'qq': 'chrome',
    'chrome': 'chrome',
    'firefox': 'firefox',
    'MicrosoftEdge': 'MicrosoftEdge',
    'opera': 'opera',
    'safari': 'safari',
    'internet explorer': 'internet explorer',
    }
    capabilities['browserName'] = browserNameToDriver[options.browserName]
    

结语

项目目标算是达成了,但还不够完美,我会持续的优化。

通过此次的开发经历,也使我感触良多,技术类需求的不确定性,是软件行业的特性。以后对接公司工作中技术需求,我也要引以为戒,做好风险管理。

同时看到一个个曾经的明星项目的沉寂,也是让我百感交集,他们本该能够成长的更好,但或是公司环境的变化,或是创作者乏力无奈,总之慢慢淡出人们的记忆,甚至连创作团队自己都忘记,而我们就在这不断创造与消亡中轮回。

最后,借用尼采的警言与各位共勉:所有美好的事物都是曲折地接近自己的目标,一切笔直都是骗人的,所有真理都是弯曲的,时间本身就是一个圆圈。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 71 条回复 时间 点赞
陈恒捷 将本帖设为了精华贴 10月10日 19:37

好久没见到这么完整有干货的实践分享了,加个精

看着很棒,也挺复杂,未来用到了再来看
还没有做兼容浏览器的测试,就直接用户手册标注仅支持谷歌浏览器,哈哈哈 😂

牛逼啊,有开源的计划么?

写的挺完整的,不错

虽然很久没接触前端测试,但是你这帖子成功让我想起了当年被兼容性测试支配的痛苦~那会儿就想整个类似的,可惜当年能力不行。给楼主赞一个

有开源的话希望拜读一下

赞,期望有开源

项目周期较赶,代码有些糙,梳理清晰后,会把核心驱动部分开源的。

期待开源

点赞👍🏻

前年也有想法搞这个,后面因为各种事情耽搁就搁置了,挺好的,点个赞。

期待开源,膜拜

很不错,后面是不是会考虑加入 ui 自动化?

81—1 #16 · 2022年10月11日 Author
恒温 回复

是的,只是我们现在已有的 UI 自动化工具是基于 RF 关键字的用例 https://testerhome.com/topics/33179 ,这里录制得到的是 macaca 的用例,需要做个转换。

期待开源👏

666,第一次见到这种方案,测试效率大大提升

期待开源

大佬,没看太懂这个工具的作用是啥。这个工具是录制一次脚本后,在 chrome,火狐等浏览器同时运行你录制的这个操作吗。

有想法又落地, 赞

码起来,期待开源

81—1 #24 · 2022年10月12日 Author
迷龙 回复

不是的,这是同时启动多个不同浏览器,在 chrome 操作时,其它浏览器也会实时同步操作,并可以对比不同浏览器的页面情况,最后会自动生成执行过程的自动化脚本(暂时没有用)。

赞~

81—1 回复

多谢多谢,了解了,大佬太厉害了👍

精华帖,先马住了

这个真不错,作者有句话说的真对 非主流浏览器做兼容的时候真的是让人欲仙欲死,之前用 selenium 做自动化兼容测试的时候 就碰到过类似的问题

赞,很不错

有没办法驱动国产浏览器?

81—1 #31 · 2022年10月13日 Author
aabbcc 回复

能,不过不完全能,要看被阉割的程度。

赞,先收藏啦

厉害,收藏了

学习了👍🏻

先收藏吧

学习,收藏

驱动部分已经开源:https://github.com/t880216t/yutu-tools.git
欢迎体验反馈

干货好文

干货好文,👍

你好,作者你那边已经写成功了,我不懂代码,但是在找这种群控软件

我觉得我以后会用到,留着以后看

学习了👍


为啥 yutu 请求不到本地的地址,我看对应的端口也没被占用

81—1 #46 · 2022年11月11日 Author
周周周 回复

127.0.0.1:4444 这个是本地的 selenium-grid 服务地址,这个是要先自己搭建的,并确保能访问哦。另外从 v1.1.1 开始,增加了很多可配置项,用于差异代理下的浏览器对比,yutu 的 init 只提供一个初始化后的工程,具体的配置,要手动改下 config.json,然后在 yutu start。

大佬,请问一下,safari 多版本是怎么实现的呢?

Clouds 回复

safari 有两种方式:

  1. 通过 linux 模拟的 safari 内核的 docker 镜像。 2.基于 macosx 真机多账号的管理系统(已经初步调研完成,开发中)。
81—1 回复

通过 linux 模拟的 safari 内核的 docker 镜像

请问这个有相关资料吗?

81—1 #51 · 2022年11月28日 Author
Just4life 回复

browsers/safari,详见:https://hub.docker.com/r/browsers/safari

大佬请问下这个是什么原因导致的,因为之前没用过 js,是配置文件的问题么

81—1 #53 · 2022年12月08日 Author
回复

可以截图上面些报错,或者把日志保存到文件我看看

81—1 回复

大佬您好,反复拜读了全文,深深的佩服;
想使用这个工具做一些浏览器兼容性测试, 自己搭建好 selenium-grid 服务,并配置好【config.json】这个文件,能否实现和文中动图一样 ,进行一些产品兼容性测试?

81—1 #55 · 2022年12月22日 Author
ShJiie 回复

是的,可以的。

我已经按照步骤设置了,启动时报错,可以帮忙看下吗?

81—1 #57 · 2022年12月30日 Author
shirleyxyli 回复

这个看着是 108 版本 chromedriver 报错,可以用纯脚本的方式试试能正常启动 webdriver。

不错,加油💪🏻

楼主新年好,麻烦看下这个配置是否有问题,可以打开谷歌浏览器但是无法打开火狐

主控浏览器是用 chrome,同步浏览器使用 Firefox 看看,作者应该是用了 CDP,所以主控浏览器设置成 chrome 就可以了

另外还有个小问题请教作者,目前同步的多端浏览器好像只同步了操作,没有同步 cookies,所以会导致一些内部项目在做登陆账号时,账号被踢掉的情况,从而无法继续登录后的同步操作,想问下作者有没有这一块的优化考虑

flyweight 回复

应该不是,转换过来也不行,估计是我的 selenium gird 的配置有问题



主浏览器上操作,但是同步浏览器并没有同步,请问还需要配置什么

要是有一个交流群就好了

大佬厉害👍 👍

大佬,浏览器列表的功能目前还不能使用吗

弱弱的问下 config.json 这个文件路径是?

在用户的根目录找到了

试了下好像不兼容 edge 浏览器,配了不生效

请问 360 系列浏览器和 qq 浏览器应该如何启动 selenium grid node 节点,
我用这个命令启动后无法连接上(配置 config 文件)

wwww 回复

已经解决了,并可以启动 360 浏览器。
但请问当有两个 chrome 内核浏览器,一个在本地 A,一个在设备 B,config 应该如何填写设置 A 为主控浏览器。

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册