├── .gitattributes ├── .github └── workflows │ ├── release.yml │ └── scala.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build.gradle ├── demo.yml ├── doc ├── Android遍历.md ├── Log插件.md ├── README.md ├── SUMMARY.md ├── TagLimit插件.md ├── XPath表达式学习.md ├── iOS遍历.md ├── 代理插件.md ├── 兼容性测试.md ├── 动作触发器.md ├── 启动参数介绍.md ├── 常见问题.md ├── 插件.md ├── 插件开发.md ├── 自动化测试结合.md ├── 遍历控制.md └── 霍格沃兹测试学院.md ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── lib └── chkbugreport-0.5-215.jar ├── package.json ├── project ├── build.properties └── plugins.sbt ├── settings.gradle ├── src ├── main │ ├── java │ │ └── com │ │ │ └── testerhome │ │ │ └── appcrawler │ │ │ ├── AppiumTouchAction.java │ │ │ └── Demo.java │ ├── resources │ │ └── log4j2.properties │ └── scala │ │ └── com │ │ └── testerhome │ │ └── appcrawler │ │ ├── AppCrawler.scala │ │ ├── AppiumSuite.scala │ │ ├── AutomationSuite.scala │ │ ├── CommonLog.scala │ │ ├── Crawler.scala │ │ ├── CrawlerConf.scala │ │ ├── CrawlerSuite.scala │ │ ├── DataObject.scala │ │ ├── DataRecord.scala │ │ ├── DiffSuite.scala │ │ ├── GA.scala │ │ ├── ReactTestCase.scala │ │ ├── Report.scala │ │ ├── ReportSuite.scala │ │ ├── Runtimes.scala │ │ ├── SuiteToClass.scala │ │ ├── TData.scala │ │ ├── Template.scala │ │ ├── TemplateTestCase.scala │ │ ├── TreeNode.scala │ │ ├── URIElement.scala │ │ ├── URIElementStore.scala │ │ ├── Util.scala │ │ ├── XPathUtil.scala │ │ ├── driver │ │ ├── AppiumClient.scala │ │ ├── MacacaDriver.scala │ │ └── ReactWebDriver.scala │ │ └── plugin │ │ ├── AndroidTrace.scala │ │ ├── DemoPlugin.scala │ │ ├── FlowDiff.scala │ │ ├── FreeMind.scala │ │ ├── IDeviceScreenshot.scala │ │ ├── LogPlugin.scala │ │ ├── Plugin.scala │ │ ├── ProxyPlugin.scala │ │ ├── ReportPlugin.scala │ │ └── TagLimitPlugin.scala └── test │ ├── java │ ├── PageFactoryDemo.java │ ├── PageObjectDemo.java │ └── XueqiuDemo.java │ └── scala │ └── com │ └── testerhome │ └── appcrawler │ ├── it │ ├── AppiumService.scala │ ├── TestAndroidTrace.scala │ ├── TestAppium.scala │ ├── TestIOS.scala │ ├── TestImage.scala │ ├── TestJianShu.scala │ ├── TestMacaca.scala │ ├── TestNW.scala │ ├── TestOCR.scala │ ├── TestSauceLabs.scala │ ├── TestTesterHome.scala │ ├── TestWebDriverAgent.scala │ ├── TestWebView.scala │ ├── TestWeixin.scala │ ├── TestXueQiu.scala │ ├── TestZhangZhongTong.scala │ ├── keep.yml │ ├── keep_test.yml │ ├── xueqiu_automation.yml │ ├── xueqiu_private.yml │ └── xueqiu_sikuli.yml │ └── ut │ ├── AppCrawlerTest.scala │ ├── DemoCrawlerSuite.scala │ ├── PageObjectDemo.java.ssp │ ├── PageObjectDemoID.java.ssp │ ├── SuiteToClassTest.scala │ ├── TestConf.scala │ ├── TestCrawler.scala │ ├── TestDataObject.scala │ ├── TestDataRecord.scala │ ├── TestElementStore.scala │ ├── TestGA.scala │ ├── TestGetClassFile.scala │ ├── TestJUnit.scala │ ├── TestJUnit5.scala │ ├── TestJava.scala │ ├── TestReportPlugin.scala │ ├── TestRuntimes.scala │ ├── TestSpec.scala │ ├── TestStringTemplate.scala │ ├── TestSuites.scala │ ├── TestThread.scala │ ├── TestTreeNode.scala │ ├── TestURIElement.scala │ ├── TestUtil.scala │ ├── TestXPathUtil.scala │ └── scalate.ssp └── yarn.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | # These are explicitly windows files and should use crlf 5 | *.bat text eol=crlf 6 | 7 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | on: 3 | push: 4 | # Sequence of patterns matched against refs/tags 5 | tags: 6 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 7 | 8 | name: Upload Release Asset 9 | 10 | jobs: 11 | build: 12 | name: Upload Release Asset 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v2 17 | - name: Get latest release version number 18 | id: get_version 19 | uses: battila7/get-version-action@v2 20 | - name: Build project # This would actually build your project, using zip for an example artifact 21 | env: 22 | APP_VERSION: ${{ steps.get_version.outputs.version-without-v }} 23 | run: | 24 | ./gradlew shadowJar 25 | ls build/libs 26 | - name: Create Release 27 | id: create_release 28 | uses: actions/create-release@v1 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | with: 32 | tag_name: ${{ github.ref }} 33 | release_name: Release ${{ github.ref }} 34 | draft: false 35 | prerelease: false 36 | - name: Upload Release Asset 37 | id: upload-release-asset 38 | uses: actions/upload-release-asset@v1 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | with: 42 | upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps 43 | asset_path: ./build/libs/appcrawler2-all.jar 44 | asset_name: appcrawler2-${{ steps.get_version.outputs.version-without-v }}.jar 45 | asset_content_type: application/java-archive 46 | -------------------------------------------------------------------------------- /.github/workflows/scala.yml: -------------------------------------------------------------------------------- 1 | name: Scala CI 2 | 3 | on: 4 | push: 5 | branches: [ develop ] 6 | pull_request: 7 | branches: [ develop ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Set up JDK 1.8 17 | uses: actions/setup-java@v1 18 | with: 19 | java-version: 1.8 20 | - name: Run tests 21 | run: | 22 | ./gradlew compileScala 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | project/target/ 3 | project/project/ 4 | target/ 5 | conf.json 6 | xueqiu.json 7 | *.apk 8 | *.swp 9 | .DS_Store 10 | .java-version 11 | lib/tools.jar 12 | 13 | # Ignore Gradle project-specific cache directory 14 | .gradle 15 | 16 | # Ignore Gradle build output directory 17 | build 18 | node_modules 19 | output 20 | *.ipa 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2.2.0 [TODO] 2 | - 支持从历史数据中寻找最优点击路径 3 | - 支持web 4 | - 支持游戏app遍历 5 | - 使用节点树模型 6 | 7 | # 2.1.2 8 | - 跟进支持appium 1.7[完成] 9 | 10 | # 2.1.0 11 | ### bugfix 12 | mark图片异常的问题 13 | ### 自动化用例 14 | 只是demo. 还有很多细节需要设计的更好. 15 | 支持given when then风格, 也支持简化的xpath action then的简单风格. 16 | ```yaml 17 | #设置这个跳过遍历 18 | autoCrawl: false 19 | #测试用例入口 20 | testcase: 21 | #测试用例名字 22 | name: demo1 23 | steps: 24 | - when: 25 | xpath: //* 26 | action: driver.swipe(0.5, 0.8, 0.5, 0.2) 27 | - when: 28 | xpath: //* 29 | action: driver.swipe(0.5, 0.2, 0.5, 0.8) 30 | #简化风格. 没有when 31 | - xpath: 自选 32 | action: click 33 | then: 34 | - //*[contains(@text, "港股")] 35 | ``` 36 | 所有的xpath的设置都支持如下三种形式 37 | - xpath //*[contains(@resource-id, 'ddd')] 38 | - regex ^确定$ 39 | - contains关系 取消 确定 40 | # 2.0.0 41 | 支持macaca[完成] 42 | 失败重试[完成] 43 | 支持简单的测试用例[完成] 44 | 架构重新设计[完成] 45 | 新老版本对比报告改进[完成] 46 | # 1.9.0 47 | 支持遍历断言[完成] 48 | 支持历史对比断言[完成] 49 | 修正不支持uiautomator2的问题[完成] 50 | 支持yaml自动化测试用例[完成] 51 | action支持长按[完成] 52 | 重构用例生成方式[完成] 53 | 54 | # 1.8.0 55 | 对子菜单的支持, 智能判断是否有子菜单 56 | 支持断点续传机制 57 | 支持自动重启appium机制, 用于防止iOS遍历内存占用太大问题 58 | 分离插件到独立项目 59 | 60 | # 1.7.0 61 | android跳到其他app后自动后退[完成] 62 | 截图复用优化提速 [完成] 63 | 报告增加点击前后的截图 [完成] 64 | 独立的report子命令 [完成] 65 | 配置支持动态指令 [完成] 66 | 配置与老版本不兼容 [重要提醒] 67 | 支持自定义报告title [完成] 68 | 69 | # 1.6.0 [内测] 70 | 增加动态插件 [完成] 71 | 支持beforeElementAction的afterElementAction配置 [完成] 72 | 修复app的http连接支持 [完成] 73 | 支持url白名单 [完成] 74 | 支持defineUrl的xpath属性提取 [完成] 75 | 未遍历控件用测试用例的cancel状态表示 [完成] 76 | 两次back之间的时间间隔设定为不低于4s防止粘连 [完成] 77 | 78 | # 1.5.0 79 | 配置文件内容变更 此版本不再向下兼容, 推荐使用yaml配置文件 80 | 标准的html报告 [完成] 81 | windows下中文编码问题 [完成] 82 | windows下命令行超长问题[完成] 83 | 加入yaml配置格式支持并添加注释 [完成] 84 | startupActions支持scala表达式 [完成] 85 | 86 | # 1.4.0 87 | 元素点击之前开始截图并高亮要点击的控件[完成] 88 | 修复freemind文件无法打开的问题[完成] 89 | # 1.3.1 90 | 增加最大后退尝试次数 91 | 增加跳出app的判断 92 | bugfix: 93 | 解决文件名特殊符号问题 94 | 修复不同界面漏掉截图的问题 95 | 96 | # 1.3.0 97 | 98 | # 1.2.2 99 | 支持相对路径的apk地址. 100 | android的端口指定不再使用4730而是和ios一样 101 | # 1.2.1 102 | url定义优化, 内容变更改进 103 | 支持自动化测试. 添加了兼容性测试的例子 104 | # 1.2.0 105 | 兼容appium1.5去掉了不支持的findElementByName方法 106 | 对xpath元素查找进行了优化 解析dom结构时生成合适的xpath表达式 107 | # 1.1.4 108 | 增加log和tagLimit两个插件 109 | 截图时间超过5s自动跳过 110 | 增加Android和iOS的log输出 111 | 增加scroll方向支持 112 | # 1.1.3 113 | 自动判断页面是否变化. 界面变化才截图. 能减少大量的重复截图 114 | 导出界面dom结构用于diff分析 115 | # 1.1.2 116 | 增加-t参数. 支持最大遍历时间 117 | 增加-o参数, 支持设定结果目录 118 | # 1.1.1 119 | 增加每个url最大滚动的次数 120 | # 1.1.0 121 | 增加思维导图生成 122 | 增加插件支持 123 | 清理大量的无关文件和测试用例 124 | 125 | # 1.0.1 126 | 支持基本遍历 127 | 支持命令行运行方式 128 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AppCrawler2 2 | 3 | > 改名为 appcrawler2 以示区分 4 | > 合并原来版本 2.4 的代码 5 | 6 | ## 从源代码编译 7 | 8 | ### 使用 gradle 编译 9 | ``` 10 | ./gradlew shadowJar 11 | ``` 12 | 会在 `build/libs` 目录下生成 `appcrawler2-all.jar` 文件 13 | 14 | 一个基于自动遍历的app爬虫工具. 支持android和iOS, 支持真机和模拟器. 最大的特点是灵活性. 可通过配置来设定遍历的规则. 15 | 16 | ## 为什么做这个工具 17 | 18 | * 各大云市场上自动遍历功能都多有限制企业无法自由定制. 19 | * 解决monkey等工具可控性差的缺点 20 | * 发现深层次的UI兼容性问题 21 | * 通过新老版本的diff可以发现每个版本的UI变动范围 22 | 23 | ## 设计目标 24 | 25 | * 自动爬取加上规则引导(完成) 26 | * 支持定制化, 可以自己设定遍历深度(完成) 27 | * 支持插件化, 允许别人改造和增强(完成) 28 | * 支持滑动等更多动作(完成) 29 | * 支持自动截获接口请求(完成) 30 | * 支持新老版本的界面对比(Doing) 31 | * 云端兼容性测试服务利用, 支持Testin MQC MTC(Doing) 32 | 33 | 34 | ## 安装依赖 35 | 36 | ### mac下安装appium 37 | 38 | ```bash 39 | #安装node和依赖 40 | brew install node 41 | brew install ideviceinstaller 42 | brew install libimobiledevice 43 | #安装appium 44 | npm install -g appium 45 | #检查appium环境正确性 46 | appium-doctor 47 | ``` 48 | 49 | 真机或者模拟器均可. 确保adb devices可以看到就行 50 | 51 | ### 启动appium 52 | 53 | 使用此工具需要一定的appium基础知识, 请自行google. 54 | 目前已经在appium 1.5.3下做过测试 55 | 56 | 启动appium 57 | 58 | ```bash 59 | appium --session-override 60 | ``` 61 | 62 | ### 下载appcrawler. 63 | 64 | 最新版本下载地址: [https://pan.baidu.com/s/1dE0JDCH](https://pan.baidu.com/s/1dE0JDCH) 65 | 66 | ### 运行 67 | 工具以jar包方式发布,需要java8以上的运行环境 68 | ```bash 69 | java -jar appcrawler.jar 70 | ``` 71 | 72 | ### 快速遍历 73 | 74 | ```bash 75 | #查看帮助文档 76 | java -jar appcrawler.jar 77 | #运行测试 78 | java -jar appcrawler.jar -a xueqiu.apk 79 | ``` 80 | 81 | ### 配置文件运行方式 82 | 83 | ```bash 84 | #配置文件的方式运行 85 | #Android测试 86 | java -jar appcrawler.jar -c conf/xueqiu.yaml -a xueqiu.apk 87 | #iOS测试 88 | java -jar appcrawler.jar -c conf/xueqiu.yaml -a xueqiu.app 89 | ``` 90 | 91 | ### 输出结果 92 | 93 | 默认在当前目录下会生成一个包含输出结果的目录, 以时间命名. 包含了如下的测试结果 94 | 95 | * 所有遍历过的控件组成的思维导图 96 | * 包含了遍历覆盖的html报告 97 | * 用于做diff分析的数据文件 98 | 99 | ### 示例 100 | ![](https://testerhome.com/photo/2016/fa0f926206242ee24eab0c47d2030759.png) 101 | 102 | ## 更多技术交流 103 | 104 | 移动测试技术交流社区: [https://testerhome.com](https://testerhome.com) 105 | 106 | QQ技术交流群: 177933995 107 | 108 | # 更多细节 109 | * [启动参数介绍](doc/启动参数介绍.md) 110 | * [遍历控制](doc/遍历控制.md) 111 | * [Android遍历](doc/Android遍历.md) 112 | * [动作触发器](doc/动作触发器.md) 113 | * [iOS遍历](doc/iOS遍历.md) 114 | * [自动化测试结合](doc/自动化测试结合.md) 115 | * [兼容性测试](doc/兼容性测试.md) 116 | * [XPath表达式学习](doc/XPath表达式学习.md) 117 | * [插件](doc/插件.md) 118 | * [插件开发](doc/插件开发.md) 119 | * [代理插件](doc/代理插件.md) 120 | * [Log插件](doc/Log插件.md) 121 | * [TagLimit插件](doc/TagLimit插件.md) 122 | * [常见问题](doc/常见问题.md) 123 | 124 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | import org.gradle.internal.jvm.Jvm 2 | 3 | buildscript { 4 | repositories { 5 | maven { url 'https://maven.aliyun.com/repository/gradle-plugin' } 6 | maven { url 'https://maven.aliyun.com/repository/spring-plugin' } 7 | } 8 | } 9 | plugins { 10 | // Apply the scala plugin to add support for Scala 11 | id 'scala' 12 | 13 | // Apply the java-library plugin for API and implementation separation. 14 | id 'java-library' 15 | id 'application' 16 | id 'com.github.johnrengelman.shadow' version '6.0.0' 17 | } 18 | repositories { 19 | // Use jcenter for resolving dependencies. 20 | // You can declare any Maven/Ivy/file repository here. 21 | maven { url 'https://maven.aliyun.com/repository/public' } 22 | maven { url 'https://jitpack.io' } 23 | } 24 | 25 | mainClassName = "com.testerhome.appcrawler.AppCrawler" 26 | 27 | jar { 28 | manifest { 29 | attributes 'Implementation-Version': System.getenv("APP_VERSION") ?: "1.0" 30 | } 31 | } 32 | 33 | tasks.withType(ScalaCompile) { 34 | scalaCompileOptions.forkOptions.with { 35 | memoryMaximumSize = '1g' 36 | jvmArgs = ['-XX:MaxPermSize=512m'] 37 | } 38 | scalaCompileOptions.with { 39 | force = true 40 | } 41 | } 42 | 43 | 44 | 45 | dependencies { 46 | implementation 'org.scala-lang:scala-library:2.12.6' 47 | implementation 'org.scala-lang:scala-reflect:2.12.6' 48 | implementation 'org.scala-lang:scala-compiler:2.12.6' 49 | implementation 'com.fasterxml.jackson.module:jackson-module-scala_2.12:2.9.5' 50 | implementation 'org.scalactic:scalactic_2.12:3.0.5' 51 | implementation 'org.scalatest:scalatest_2.12:3.0.5' 52 | implementation 'org.scalatra.scalate:scalate-core_2.12:1.8.0' 53 | implementation 'com.github.poslegm:scala-phash_2.12:1.0.3' 54 | implementation 'com.github.tototoshi:scala-csv_2.12:1.3.4' 55 | implementation 'com.github.scopt:scopt_2.12:3.5.0' 56 | 57 | implementation 'io.appium:java-client:7.3.0' 58 | implementation 'com.brsanthu:google-analytics-java:1.1.2' 59 | implementation 'org.slf4j:slf4j-api:1.7.18' 60 | implementation 'org.slf4j:slf4j-log4j12:1.7.18' 61 | implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.9.5' 62 | implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.9.5' 63 | implementation 'net.lightbody.bmp:browsermob-core:2.1.5' 64 | implementation 'org.lucee:commons-codec:1.10.L001' 65 | implementation 'org.jsoup:jsoup:1.9.2' 66 | implementation 'com.jayway.jsonpath:json-path:2.2.0' 67 | implementation 'org.apache.directory.studio:org.apache.commons.io:2.4' 68 | implementation 'org.apache.logging.log4j:log4j-core:2.7' 69 | implementation 'macaca.webdriver.client:macacaclient:2.0.20' 70 | implementation 'org.javassist:javassist:3.22.0-CR2' 71 | implementation 'us.codecraft:xsoup:0.3.1' 72 | implementation 'junit:junit:4.12' 73 | implementation 'org.junit.jupiter:junit-jupiter-api:5.2.0' 74 | implementation 'com.github.spullara.mustache.java:compiler:0.9.5' 75 | implementation 'org.ow2.asm:asm:5.2' 76 | implementation 'io.qameta.allure:allure-junit5:2.6.0' 77 | implementation 'org.apache.commons:commons-text:1.4' 78 | implementation 'org.pegdown:pegdown:1.6.0' 79 | implementation 'com.google.guava:guava:24.1-jre' 80 | implementation files(Jvm.current().toolsJar) 81 | 82 | scalaCompilerPlugins "org.typelevel:kind-projector_2.12:0.10.3" 83 | } 84 | 85 | 86 | -------------------------------------------------------------------------------- /demo.yml: -------------------------------------------------------------------------------- 1 | --- 2 | logLevel: 'TRACE' 3 | pluginList: [] 4 | saveScreen: true 5 | reportTitle: '报告名称' 6 | waitLoading: 500 7 | waitLaunch: 6000 8 | showCancel: true 9 | maxTime: 1080 10 | maxDepth: 10 11 | capability: 12 | appPackage: "your app's package name" 13 | autoGrantPermissions: true 14 | automationName: 'uiautomator2' 15 | # noReset: 'true' 16 | fullReset: 'true' 17 | appium: 'http://127.0.0.1:4723/wd/hub' 18 | 19 | selectedList: 20 | - //*[@clickable="true"] 21 | - //*[@text="我选好了"] 22 | - "//*[contains(name(), 'Button')]" 23 | - "//*[contains(name(), 'Text') and @clickable='true' and string-length(@text)<10]" 24 | - "//*[@clickable='true']/*[contains(name(), 'Text') and string-length(@text)<10]" 25 | - "//*[contains(name(), 'Image') and @clickable='true']" 26 | - "//*[contains(name(), 'Image') and @name!='']" 27 | - "//*[contains(name(), 'Text') and @name!='' and string-length(@label)<10]" 28 | firstList: [] 29 | triggerActions: 30 | - xpath: "//*[@resource-id='package_name:id/viewPager']" 31 | action: 'driver.swipe("left")' 32 | times: 2 33 | -------------------------------------------------------------------------------- /doc/Android遍历.md: -------------------------------------------------------------------------------- 1 | # Android遍历 2 | 3 | ## 在android上运行 4 | 5 | ## 启动appium 6 | ``` 7 | appium --session-override 8 | ``` 9 | 10 | ## 简单的启动遍历 11 | ``` 12 | java -jar appcrawler.jar \ 13 | -a ~/Downloads/xueqiu.apk 14 | ``` 15 | 16 | ## 定制文件运行方式 17 | ```bash 18 | java -jar appcrawler.jar \ 19 | -a ~/Downloads/xueqiu.apk \ 20 | -c conf/xueqiu.yaml 21 | ``` 22 | 23 | ## 跳过重新安装app 24 | 25 | ```bash 26 | java -jar appcrawler.jar \ 27 | -a ~/Downloads/xueqiu.apk \ 28 | -c conf/xueqiu.yaml \ 29 | --capability \ 30 | appPackage=com.xueqiu.android,appActivity=.view.WelcomeActivityAlias 31 | ``` 32 | -------------------------------------------------------------------------------- /doc/Log插件.md: -------------------------------------------------------------------------------- 1 | # Log插件 2 | 3 | ## 作用 4 | 自动记录Android的LogCat或者iOS的syslog. 5 | ## 安装 6 | 目前是默认自带. 7 | 8 | ## 启用 9 | 在配置文件中加入插件 10 | ``` 11 | "pluginList" : [ 12 | "com.testerhome.appcrawler.plugin.LogPlugin" 13 | ], 14 | ``` 15 | 16 | ## 结果 17 | 记录一次点击事件后所发生的log记录. 并保存为后缀名为.log的文件中. 18 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | # AppCrawler 2 | 3 | 一个基于自动遍历的app爬虫工具. 支持android和iOS, 支持真机和模拟器. 最大的特点是灵活性. 可通过配置来设定遍历的规则. 4 | 5 | ## 为什么做这个工具 6 | 7 | * 各大云市场上自动遍历功能都多有限制企业无法自由定制. 8 | * 解决monkey等工具可控性差的缺点 9 | * 发现深层次的UI兼容性问题 10 | * 通过新老版本的diff可以发现每个版本的UI变动范围 11 | 12 | ## 设计目标 13 | 14 | * 自动爬取加上规则引导(完成) 15 | * 支持定制化, 可以自己设定遍历深度(完成) 16 | * 支持插件化, 允许别人改造和增强(完成) 17 | * 支持滑动等更多动作(完成) 18 | * 支持自动截获接口请求(完成) 19 | * 支持新老版本的界面对比(Doing) 20 | * 云端兼容性测试服务利用, 支持Testin MQC MTC(Doing) 21 | 22 | ## 安装依赖 23 | 24 | ### mac下安装appium 25 | 26 | ```bash 27 | #安装node和依赖 28 | brew install node 29 | brew install ideviceinstaller 30 | brew install libimobiledevice 31 | #安装appium 32 | npm install -g appium 33 | #检查appium环境正确性 34 | appium-doctor 35 | ``` 36 | 37 | 真机或者模拟器均可. 确保adb devices可以看到就行 38 | 39 | ### 启动appium 40 | 41 | 使用此工具需要一定的appium基础知识, 请自行google. 42 | 目前已经在appium 1.5.3下做过测试 43 | 44 | 启动appium 45 | 46 | ```bash 47 | appium --session-override 48 | ``` 49 | 50 | ### 下载appcrawler. 51 | 52 | 最新版本下载地址: [https://pan.baidu.com/s/1dE0JDCH](https://pan.baidu.com/s/1dE0JDCH) 53 | 54 | ### 运行 55 | 工具以jar包方式发布,需要java8以上的运行环境 56 | ```bash 57 | java -jar appcrawler.jar 58 | ``` 59 | 60 | ### 快速遍历 61 | 62 | ```bash 63 | #查看帮助文档 64 | java -jar appcrawler.jar 65 | #运行测试 66 | java -jar appcrawler.jar -a xueqiu.apk 67 | ``` 68 | 69 | ### 配置文件运行方式 70 | 71 | ```bash 72 | #配置文件的方式运行 73 | #Android测试 74 | java -jar appcrawler.jar -c conf/xueqiu.yaml -a xueqiu.apk 75 | #iOS测试 76 | java -jar appcrawler.jar -c conf/xueqiu.yaml -a xueqiu.app 77 | ``` 78 | 79 | ### 输出结果 80 | 81 | 默认在当前目录下会生成一个包含输出结果的目录, 以时间命名. 包含了如下的测试结果 82 | 83 | * 所有遍历过的控件组成的思维导图 84 | * 包含了遍历覆盖的html报告 85 | * 用于做diff分析的数据文件 86 | 87 | ### 示例 88 | ![](https://testerhome.com/photo/2016/fa0f926206242ee24eab0c47d2030759.png) 89 | 90 | ## 更多技术交流 91 | 92 | 移动测试技术交流社区: [https://testerhome.com](https://testerhome.com) 93 | 94 | QQ技术交流群: 177933995 95 | 96 | # 更多细节 97 | * [启动参数介绍](启动参数介绍.md) 98 | * [遍历控制](遍历控制.md) 99 | * [Android遍历](Android遍历.md) 100 | * [动作触发器](动作触发器.md) 101 | * [iOS遍历](iOS遍历.md) 102 | * [自动化测试结合](自动化测试结合.md) 103 | * [兼容性测试](兼容性测试.md) 104 | * [XPath表达式学习](XPath表达式学习.md) 105 | * [插件](插件.md) 106 | * [插件开发](插件开发.md) 107 | * [代理插件](代理插件.md) 108 | * [Log插件](Log插件.md) 109 | * [TagLimit插件](TagLimit插件.md) 110 | * [常见问题](常见问题.md) 111 | 112 | -------------------------------------------------------------------------------- /doc/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # AppCrawler 2 | 3 | * [介绍](README.md) 4 | * [启动参数介绍](启动参数介绍.md) 5 | * [遍历控制](遍历控制.md) 6 | * [Android遍历](Android遍历.md) 7 | * [动作触发器](动作触发器.md) 8 | * [iOS遍历](iOS遍历.md) 9 | * [自动化测试结合](自动化测试结合.md) 10 | * [兼容性测试](兼容性测试.md) 11 | * [XPath表达式学习](XPath表达式学习.md) 12 | * [插件](插件.md) 13 | * [插件开发](插件开发.md) 14 | * [代理插件](代理插件.md) 15 | * [Log插件](Log插件.md) 16 | * [TagLimit插件](TagLimit插件.md) 17 | * [常见问题](常见问题.md) 18 | 19 | -------------------------------------------------------------------------------- /doc/TagLimit插件.md: -------------------------------------------------------------------------------- 1 | # TagLimit插件 2 | 3 | ## 作用 4 | 智能判断列表和其他的相似布局元素.只遍历前3个相似空间. 适用于微博这种无限刷新的列表. 用于节省时间. 5 | 原理是利用特定元素的tag布局层级是否完全一样. 6 | ## 安装 7 | 目前是默认自带. 8 | ## 启用 9 | 在配置文件中加入插件 10 | ``` 11 | "pluginList" : [ 12 | "com.testerhome.appcrawler.plugin.TagLimitPlugin" 13 | ], 14 | ``` 15 | 16 | ## 结果 17 | 无 18 | -------------------------------------------------------------------------------- /doc/XPath表达式学习.md: -------------------------------------------------------------------------------- 1 | # XPath表达式学习 2 | 3 | # 学习渠道 4 | w3school肯定是最好的教程 5 | # 获取控件XPath路径的工具 6 | 7 | | 名字 | 平台 | 介绍| 8 | | -- | --- | --- | 9 | | uiautomatorviewer | Android | 只能直接生成xpath, 需要自己拼凑 | 10 | | Appium Inspector | Android iOS | 只能工作在mac上 | 11 | | app-insecptor | Android iOS | macaca的生态工具 | 12 | 13 | # 常见用法 14 | 15 | # Android和iOS控件差异 16 | tag名字是不一样的. 17 | ``` 18 | UIAXXXX 19 | android.view.View 20 | android.widget.XXXXX 21 | ``` 22 | 关键的定位属性也不一样 23 | 24 | iOS 25 | ``` 26 | name 27 | label 28 | value 29 | ``` 30 | Android 31 | ``` 32 | resource-id 33 | content-desc 34 | text 35 | ``` 36 | 37 | # 常见XPath表达式用法 38 | 39 | ``` 40 | //*[not(ancestor-or-self::UIATableView)] 41 | //*[not(ancestor-or-self::UIAStatusBar)] 42 | //*[@resource-id='com.xueqiu.android:id/action_search']/parent::* 43 | //*[@resource-id='com.xueqiu.android:id/action_search'] 44 | //*[contains(name(), 'Text')] 45 | //*[@resource-id!='' and not(contains(name(), 'Layout'))] 46 | //*[../*[@selected='true']] 47 | ``` 48 | -------------------------------------------------------------------------------- /doc/iOS遍历.md: -------------------------------------------------------------------------------- 1 | # iOS遍历 2 | 3 | ## 模拟器运行 4 | 启动appium 5 | ``` 6 | appium --session-override 7 | ``` 8 | 9 | 开始遍历 10 | ``` 11 | java -jar appcrawler.jar \ 12 | -c conf/xueqiu.yaml \ 13 | -a <你的app地址比如xueqiu.app> 14 | ``` 15 | xcode编译出来的app地址可通过编译过程自己查看 16 | 17 | 18 | ## 真机运行 19 | 使用xcode编译源代码. 使用开发证书才能做自动化. 编译出真机可自动化的.app或者.ipa包 20 | ``` 21 | java -jar appcrawler.jar \ 22 | -c conf/xueqiu.yaml \ 23 | -a Snowball.app 24 | ``` 25 | -------------------------------------------------------------------------------- /doc/代理插件.md: -------------------------------------------------------------------------------- 1 | # 代理插件 2 | 自动获取app上每次点击对应的网络请求. 支持http和https 3 | ## 安装 4 | 目前是默认自带. 5 | 6 | ## 启用 7 | 在配置文件中加入插件 8 | ``` 9 | "pluginList" : [ 10 | "com.testerhome.appcrawler.plugin.ProxyPlugin" 11 | ], 12 | ``` 13 | 14 | 代理插件默认开启7771端口. 15 | 配置你的Android或者iOS的设备的代理. 指向你当前运行appcrawler的机器和7771端口 16 | ## 结果 17 | 在做每个点击的时候都会保存这期间发送的请求. 也就是记录前后两次点击中间的所有通过代理的请求. 18 | 最后会在结果目录里面生成后缀名为har的文件. 19 | 20 | ## https支持 21 | 如果要录制https, 需要安装一个特殊的证书. 22 | 或者用burp把当前端口设置为burp的上游代理. 23 | 对于一些用url中包含有ip的https请求不支持. 后续会重构. 24 | -------------------------------------------------------------------------------- /doc/兼容性测试.md: -------------------------------------------------------------------------------- 1 | # 兼容性测试 [实验特性, 不建议使用] 2 | 3 | 4 | # ToDo 5 | 结合MTC MQC和Testin在线测试服务 6 | 7 | # 本地兼容性测试 8 | 本地通过截图来判断不同设备上的部分功能是否满足需要. 9 | 只是一个初级的demo供参考. 10 | 真实的应用需要做到设备参数和用例分离. 11 | ```scala 12 | class TestAppiumDSL extends AppiumDSL { 13 | import org.scalatest.prop.TableDrivenPropertyChecks._ 14 | val table = Table( 15 | ("iPhone 4s", "9.1"), 16 | ("iPhone 5", "8.1"), 17 | ("iPhone 5", "9.2"), 18 | ("iPhone 5s", "9.1"), 19 | ("iPhone 6", "8.1"), 20 | ("iPhone 6", "9.2"), 21 | ("iPhone 6 Plus", "9.1"), 22 | ("iPhone 6s", "9.1"), 23 | ("iPhone 6s", "9.2"), 24 | ("iPad Air", "9.1"), 25 | ("iPad Air 2", "9.1"), 26 | ("iPad Pro", "9.1"), 27 | ("iPad Retina", "8.1"), 28 | ("iPad Retina", "8.2") 29 | ) 30 | forAll(table) { (device: String, version: String) => { 31 | test(s"兼容性测试-${device}-${version}_登录验证iphone", Tag("7.7"), Tag("iOS"), Tag("兼容性测试")) { 32 | iOS(true) 33 | config("deviceName", device) 34 | config("platformVersion", version) 35 | setCaptureDir("/Users/seveniruby/temp/crawl4") 36 | appium() 37 | captureTo(s"${device}-${version}_init.png") 38 | click on see("手机号") 39 | send("1560053xxxx") 40 | click on see("//UIASecureTextField") 41 | send("password") 42 | captureTo(s"${device}-${version}_login.png") 43 | click on see("登 录") 44 | captureTo(s"${device}-${version}_main.png") 45 | if(device.matches(".*iPad.*")){ 46 | click on see("//UIAButton[@path=\"/0/0/0/5\"]") 47 | }else { 48 | click on see("//UIAButton[@path=\"/0/0/3/5\"]") 49 | } 50 | tree("seveniruby")("name") should be equals "seveniruby" 51 | captureTo(s"${device}-${version}_profile.png") 52 | } 53 | } 54 | } 55 | 56 | override def afterEach(): Unit ={ 57 | log.info("quit") 58 | quit() 59 | } 60 | } 61 | 62 | ``` 63 | -------------------------------------------------------------------------------- /doc/动作触发器.md: -------------------------------------------------------------------------------- 1 | # 动作触发器 2 | 3 | 4 | 5 | ## 启动脚本 6 | 用于划过开屏的各种操作. 遍历开始前会先运行这个命令序列. 目前默认就会尝试滑动 你可以用这个配置作微调. 7 | ```yaml 8 | startupActions: 9 | - swipe("left") 10 | - swipe("left") 11 | - swipe("down") 12 | - println(driver) 13 | ``` 14 | ## 触发配置 15 | triggerActions表示遇到什么样的元素需要执行多少次的什么动作. 所以他有三个主要的配置. 16 | xpath字段也支持严格正则表达式. 比如某个按钮的文本是```功能搬到这里啦```的提示控件可以通过```.*这里.*```来匹配到. 17 | action如果是click就是点击. 如果是非click 就认为是输入内容. 18 | action支持如下动作 19 | ``` 20 | click 21 | back 22 | swipe("left") 23 | ``` 24 | action也支持scala的表达式. 25 | times表示规则被应用几次后删除, 如果是0表示永久生效. 26 | 示例 27 | ```yaml 28 | triggerActions: 29 | - action: "click" 30 | xpath: "//*[@resource-id='com.xueqiu.android:id/button_login']" 31 | times: 1 32 | - action: 123 33 | xpath: //*[contains(name(), "EditText")] 34 | times: 10 35 | - action: click 36 | xpath: 我知道了 37 | times: 0 38 | ``` 39 | -------------------------------------------------------------------------------- /doc/启动参数介绍.md: -------------------------------------------------------------------------------- 1 | # 启动参数介绍 2 | 3 | 4 | ```bash 5 | java -jar appcrawler.jar 1.7.0 6 | app爬虫, 用于自动遍历测试. 支持Android和iOS, 支持真机和模拟器 7 | 帮助文档: http://seveniruby.gitbooks.io/appcrawler 8 | 移动测试技术交流: https://testerhome.com 9 | 感谢: 晓光 泉龙 杨榕 恒温 mikezhou yaming116 10 | 11 | Usage: java -jar appcrawler.jar [options] 12 | 13 | -a, --app Android或者iOS的文件地址, 可以是网络地址, 赋值给appium的app选项 14 | -c, --conf 配置文件地址 15 | -p, --platform 平台类型android或者ios, 默认会根据app后缀名自动判断 16 | -t, --maxTime 最大运行时间. 单位为秒. 超过此值会退出. 默认最长运行3个小时 17 | -u, --appium appium的url地址 18 | -o, --output 遍历结果的保存目录. 里面会存放遍历生成的截图, 思维导图和日志 19 | --capability k1=v1,k2=v2... 20 | appium capability选项, 这个参数会覆盖-c指定的配置模板参数, 用于在模板配置之上的参数微调 21 | -r, --report 输出html和xml报告 22 | -vv, --verbose 是否展示更多debug信息 23 | --help 24 | 示例 25 | java -jar appcrawler.jar -a xueqiu.apk 26 | java -jar appcrawler.jar -a xueqiu.apk --capability noReset=true 27 | java -jar appcrawler.jar -c conf/xueqiu.yaml -p android -o result/ 28 | java -jar appcrawler.jar -c xueqiu.yaml --capability udid=[你的udid] -a Snowball.app 29 | java -jar appcrawler.jar -c xueqiu.yaml -a Snowball.app -u 4730 30 | java -jar appcrawler.jar -c xueqiu.yaml -a Snowball.app -u http://127.0.0.1:4730/wd/hub 31 | java -jar appcrawler.jar --report result/ 32 | ``` 33 | -------------------------------------------------------------------------------- /doc/常见问题.md: -------------------------------------------------------------------------------- 1 | # Path must be a string 2 | 错误堆栈为 3 | ``` 4 | packageAndLaunchActivityFromManifest failed. Original error: Path must be a string. Received null 5 | ``` 6 | 这是appium的bug导致的. 一般表示没有成功读取app的appPackage和appActivity, 自己在配置文件或者命令行中显式指定即可. 7 | 比如 8 | 9 | ```bash 10 | java -jar appcrawler.jar -a xueqiu.apk -u 4730 \ 11 | --capability appPackage=com.xueqiu.android,appActivity=.view.WelcomeActivityAlias -vv 12 | ``` 13 | 14 | # 找不到appcrawler工具 15 | appcrawler的分发形式有两种. jar包方式和正常的zip格式. 16 | appcrawler在zip解压后的bin目录下. 如果下载的是jar包, 可执行 17 | ```bash 18 | java -jar appcrawler.jar --help 19 | ``` 20 | -------------------------------------------------------------------------------- /doc/插件.md: -------------------------------------------------------------------------------- 1 | # 插件 2 | 3 | -------------------------------------------------------------------------------- /doc/插件开发.md: -------------------------------------------------------------------------------- 1 | # 插件开发 2 | 3 | 期望是和burp suite一样, 允许用户可以使用python ruby java scala等语言来设计插件. 4 | 5 | 目前项目内的插件分两种插件. 一种是框架自带的插件. 比如tagLimit log插件等, 只需要在配置文件中直接写上插件的名字即可调用. 6 | 7 | 还有一种插件是动态插件. 在项目的plugins目录下有一个demo插件可以参考. 8 | 9 | # 动态插件 10 | 11 | 动态插件是存在与plugins目录下, 在运行时会把目录内的scala文件自动编译为class文件并加载进来. 所以可以随时修改里面的scala文件来做自定义的插件. 12 | 13 | 动态插件的示例 14 | 15 | ```scala 16 | import com.testerhome.appcrawler._ 17 | 18 | //继承Plugin类 19 | class DynamicPlugin extends Plugin{ 20 | //重载start方法, 启动时执行 21 | override def start(): Unit ={ 22 | log.info("hello from seveniruby") 23 | } 24 | //在每个element的动作执行前进行针对性的处理. 比如跳过 25 | override def beforeElementAction(element: UrlElement): Unit ={ 26 | log.info("you can add some logic in here") 27 | log.info(element) 28 | } 29 | //当进入新页面会回调此接口 30 | override def afterUrlRefresh(url:String): Unit ={ 31 | log.info(s"url=${getCrawler().url}") 32 | } 33 | 34 | } 35 | 36 | ``` 37 | 更多方法可参考Plugin类的接口定义. 38 | -------------------------------------------------------------------------------- /doc/自动化测试结合.md: -------------------------------------------------------------------------------- 1 | # 自动化测试结合 [实验性] 2 | 暂未成熟不建议尝试. 目前只是我个人写用例使用中. 还在逐渐的完善. 3 | 算是提供一种思路给大家. 4 | 5 | ## 测试框架选择 6 | 使用了scalatest这个强大的测试框架作为依托. 7 | 他本身是支持BDD和TDD风格的. 8 | http://www.scalatest.org/user_guide/selecting_a_style 9 | 10 | 11 | ## scalatest自带的selenium支持 12 | 13 | 详情可参考 14 | http://www.scalatest.org/user_guide/using_selenium 15 | 这个selenium框架易用性很好. 支持selenium意味着可以支持appium, 16 | 先看示例 17 | ```scala 18 | class BlogSpec extends FlatSpec with ShouldMatchers with WebBrowser { 19 | implicit val webDriver: WebDriver = new HtmlUnitDriver 20 | val host = "http://localhost:9000/" 21 | 22 | "The blog app home page" should "have the correct title" in { 23 | go to (host + "index.html") 24 | pageTitle should be ("Awesome Blog") 25 | } 26 | } 27 | ``` 28 | 29 | ## Appium支持 30 | appcrawler在selenium支持的基础上做了一个针对appium的封装,类名叫MiniAppium.他具有如下的特色 31 | - 设计极简, 除了selenium的自身支持外,增加了几个api用于app的测试 32 | - 封装了appium命令的启动停止 33 | - 强大的断言 34 | 35 | ```scala 36 | test("验证雪球登陆功能"){ 37 | //启动appium 38 | start() 39 | //配置appium client 40 | config("app", "/Users/seveniruby/Downloads/xueqiu.apk") 41 | config("appPackage", "com.xueqiu.android") 42 | config("appActivity", ".view.WelcomeActivityAlias") 43 | appium() 44 | //自动化 45 | see("输入手机号").send("13067754297") 46 | see("password").send("xueqiu4297") 47 | see("button_next").tap() 48 | see("tip").tap().tap().tap() 49 | swipe("down") 50 | see("user_profile_icon").tap() 51 | //断言 52 | see("screen_name").nodes.head("text") should equal("huangyansheng") 53 | see("screen_name")("text") shouldEqual "huangyansheng" 54 | } 55 | ``` 56 | 57 | ### appium关键字 58 | 在selenium支持的基础上只增加了少数几个方法. 59 | 60 | | 命令 | 用途 | 61 | | -- | -- | 62 | | see | 元素定位与属性提取 | 63 | | tap | 点击 | 64 | | send | 输入文本 | 65 | | swipe | 滑动 | 66 | 67 | 原来scalatest的selenium的支持仍然全部可用. 比如 68 | ```scala 69 | click on id("login") 70 | ``` 71 | 72 | ### see 73 | 唯一的元素定位api. 74 | 75 | see是引用了<阿凡达>电影里面一句台词"I See You". 76 | 它的作用是当你看到一个控件, 你应该可以根据看见的东西就可以定位它,并获取到这个控件的属性, 无须借助其他工具或者使用findElementByXXX之类的函数. 77 | 比如有个Button, 名字是"登录", 它的id是account, 定位它可以通过如下多种方式的任何一种 78 | - see("登录") 79 | - see("登") 80 | - see("录") 81 | - see("account") 82 | - see("acc") 83 | - see("//UIAButton[@id="account"]") 84 | - see("screen_name")("text") 85 | - see("screen_name").nodes.head("text") 86 | - see("action_bar_title")("text") 文本 87 | - see("action_bar_title")("tag") 类型 88 | - see("action_bar_title")("selected") 是否选中 89 | 90 | 91 | 如果当前界面中存在了有歧义的空间, 比如其他一个名字为"登录"的输入框. 那么上述定位方法中定位中两个控件的定位方法会失败, 你需要自己调整即可. 92 | **这就是关于元素定位你只需要用see这个方法即可**. 93 | 94 | ### 动作 95 | 目前只封装了3个动作. tap send swipe. 96 | ```scala 97 | see("输入手机号").send("13067754297") 98 | see("password").send("xueqiu4297") 99 | see("button_next").tap() 100 | ``` 101 | 支持链式调用. 当然不推荐日常使用 102 | ```scala 103 | //对三次连续出现的tip控件点击三次. 104 | see("tip").tap().tap().tap() 105 | see("输入手机号").send("13067754297").see("password").send("xueqiu4297") 106 | ``` 107 | ### 断言 108 | 支持标准的scalatest的should风格的断言. 支持两种风格的断言 109 | #### assert风格 110 | ```scala 111 | assert(2>1) 112 | assert(attempted == 1, "Execution was attempted " + left + " times instead of 1 time") 113 | ``` 114 | 用法参考 http://www.scalatest.org/user_guide/using_assertions 115 | 116 | 117 | #### should风格 118 | 这也是我喜欢的风格 119 | ```scala 120 | //数字比较 121 | one should be < 7 122 | one should be > 0 123 | one should be <= 7 124 | one should be >= 0 125 | sevenDotOh should be (6.9 +- 0.2) 126 | //字符串断言 127 | traversable should contain ("five") 128 | string should startWith regex "Hel*o" 129 | string should fullyMatch regex """(-)?(\d+)(\.\d*)?""" 130 | "howdy" should contain oneOf ('a', 'b', 'c', 'd') 131 | ``` 132 | 完整用法参考 http://www.scalatest.org/user_guide/using_matchers 133 | 134 | ## 第一个自动化测试用例 135 | 安装scala与sbt, windows同学请自行找办法解决 136 | ```bash 137 | brew install scala sbt 138 | ``` 139 | 进入appcrawler的项目目录, 或者自己创建一个目录也是可以的. 140 | 目录结构如下 141 | 在lib下存放appcrawler.jar文件. 142 | 在src/test/scala下存放测试用例. 143 | build.sbt是个固定模板.使用appcrawler自己的即可. 144 | 具体的配置可参考scala的sbt构建管理帮助文档, 可以使用idea等IDE管理 145 | ``` 146 | build.sbt 147 | lib/ 148 | src/ 149 | main/ 150 | test/ 151 | scala/ 152 | ``` 153 | 在src/test/scala下编写自己的测试用例. 154 | ```scala 155 | package com.testerhome.appcrawler.it 156 | 157 | import com.testerhome.appcrawler.MiniAppium 158 | 159 | /** 160 | * Created by seveniruby on 16/5/21. 161 | */ 162 | class TestAndroidSimulator extends MiniAppium { 163 | override def beforeAll(): Unit = { 164 | start() 165 | config("app", "/Users/seveniruby/Downloads/xueqiu.apk") 166 | config("appPackage", "com.xueqiu.android") 167 | config("appActivity", ".view.WelcomeActivityAlias") 168 | config("fullReset", "false") 169 | config("noReset", "true") 170 | appium() 171 | login() 172 | quit() 173 | } 174 | 175 | def login(): Unit = { 176 | swipe("left") 177 | swipe("down") 178 | see("输入手机号").send("13067754297") 179 | see("password").send("xueqiu4297") 180 | see("button_next").tap() 181 | see("tip").tap().tap().tap() 182 | swipe("down") 183 | } 184 | 185 | override def beforeEach(): Unit = { 186 | config("appPackage", "com.xueqiu.android") 187 | config("appActivity", ".view.WelcomeActivityAlias") 188 | appium() 189 | } 190 | 191 | test("test android simulator") { 192 | see("user_profile_icon").tap() 193 | see("screen_name").nodes.head("text") should equal("huangyansheng") 194 | see("screen_name").nodes.last("text") should be("huangyansheng") 195 | see("screen_name").attribute("text") shouldBe "huangyansheng" 196 | see("screen_name")("text") shouldEqual "huangyansheng" 197 | } 198 | 199 | test("自选") { 200 | swipe("down") 201 | see("自选").tap 202 | see("雪球100").tap 203 | swipe("down") 204 | see("stock_current_price")("text").toDouble should be > 1000.0 205 | } 206 | 207 | override def afterEach: Unit = { 208 | quit() 209 | } 210 | override def afterAll(): Unit = { 211 | stop() 212 | } 213 | } 214 | 215 | ``` 216 | 执行测试 217 | ```bash 218 | sbt test 219 | ``` 220 | xml和html格式的测试报告会生成在项目的target/report目录下 221 | 222 | ## TODO 223 | 目前的功能已经可以支持基础的自动化测试了. 仍然有一些内容需要完善. 我可能没有精力去维护了. 留给大家做参考吧. 224 | - 支持单选 多选 滑块等封装动作 225 | - 交互式调试 226 | - 生成robotframework style的测试报告 227 | -------------------------------------------------------------------------------- /doc/遍历控制.md: -------------------------------------------------------------------------------- 1 | # 遍历控制 2 | 配置选项的作用可参考项目目录下的yaml格式的配置文件, 里面有详细的注释解释每个配置项的作用. 3 | 4 | # 唯一性 5 | appcrawler试图用接口测试的理念来定义app遍历测试. 每个app界面认为是一个url. 6 | 默认是用当前的activity名字或者navigatorbar来表示. 这个可以通过defineUrl配置实现自定义. 7 | 8 | 控件的唯一性通过配置项的xpathAttributes来定义. 默认是通过基本属性iOS的为name label value path, Androd的为resource-id content-desc text index这几个属性来唯一定义一个元素. 可以通过修改这个配置项实现自定义. 9 | 10 | url的定义是一门艺术, 可以决定如何优雅快速有效的遍历 11 | ### 遍历行为控制 12 | 整体的配置项应用顺序为 13 | ## 1.capability 14 | androidCapability和iosCapability分别用来存放不同的平台的设置. 最后会和capability合并为一个. 15 | ## 2.startupActions 16 | 用于启动时候自定义一些划屏或者刷新的动作. 17 | ## 3.selectedList 18 | 适用于在一些列表页或者tab页中精确的控制点击顺序 19 | selectedList表示要遍历的元素特征 20 | firstList表示优先遍历元素特征 21 | lastList表示最后应该遍历的元素特征 22 | tagLimit定义特定类型的控件遍历的最大次数. 比如列表项只需要遍历少数 23 | **需要注意的是firstList和lastList指定的元素必须包含在selectedList中** 24 | 25 | # 元素定位方法 26 | appcrawler大量的使用XPath来表示元素的范围. 大部分的选项都可以通过XPath来指定范围. 比如黑白名单, 遍历顺序等 27 | # 元素操纵方法 28 | 在一些配置的action字段里面, 除了支持简单的dsl方法列表外, 也支持scala语句. 这意味着你可以完全定义自己的流程. 29 | 比如action可以定义为click back等方法. 也可以定位为driver.findElementBy...方法. 甚至是Thread.sleep(3000)等编程语句. 30 | -------------------------------------------------------------------------------- /doc/霍格沃兹测试学院.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # 关于霍格沃兹测试学院 4 | 霍格沃兹测试学院是StuQ移动测试小班课的承办机构发展而来。 5 | StuQ小班课是TesterHome社区与极客邦公司StuQ学院的合作项目,旨在以微盈利的方式传授测试技能知识推动移动测试的发展。 6 | 小班课经过了一年多的历练,已经服务了**数百名**测试行业的学员,获得了不俗的测试行业口碑,课程内容也越来越精良。 7 | 鉴于这个项目影响力越来越大,为了更好的服务学员,扩大社区影响力,项目运作团队正式更名为**霍格沃兹测试学院**。 8 | 9 | 霍格沃兹测试学院课程体系 10 | - 移动测试开发**快速进阶**课程 11 | - 移动测试开发**强化训练**课程 12 | 13 | ### 移动测试开发培训快速进阶课程[线上直播课程] 26课时 14 | 线上授课,与极客邦StuQ学院合作,面向初中级测试工程师,实现测试开发能力的掌握,晚上授课为主,总共26课时。 15 | 16 | ### 移动测试开发培训强化训练课程 [线上线下同时支持] 40课时 17 | 线下线上同步授课,是快速进阶课程的加长版,提供更多强化训练,连续5个周日40课时。 18 | 会提供linux jenkins sonar docker elk appium stf等演练环境。 19 | **学员也可带着自己公司的app来实训,让实训变的更有针对性**。确保课程后每位同学都能达到熟练的对自己公司的产品进行app自动化和接口自动化,提升公司的产品质量保证。 20 | 21 | # 讲师阵容 22 | 六位讲师,皆来自于互联网一线公司。工作经验在5-10年。授课时有**助教**手把手指导。 23 | 思寒:十年工作经验,先后工作于阿里巴巴 百度 雪球 24 | 卡斯:十年以上工作经验,先后工作于华为,Testin 25 | 校长:十年以上工作经验,某著名互联网车企公司测试经理 26 | 大东:五年左右工作经验,前沪江网测试开发工程师 27 | 神秘美女讲师:八年工作经验,工作于Intel 28 | 阿飞:五年左右工作经验,社区技术达人,工作于某一线AI技术公司 29 | 30 | 31 | # 授课形式 32 | 快速进阶课程的形式为在线直播,并支持一年内观看录播。 33 | 34 | 强化训练班支持**线上授课和线下授课**,北京本地同学可直接来地图标记的地点现场实训,讲师会手把手指导。 35 | 外地同学可以通过手机或电脑上的工具直连听课,讲师提供在线的答疑和远程指导。 36 | 授课内容提供课程视频回放,可无限观看。 37 | 38 | # 课程受众 39 | 快速进阶班和强化训练班的主要区别是: 40 | **强化训练班面向学渣和学痴,支持线下面对面授课和线上同步直播,每期五十人左右。面向没有测试开发基础的同学,提供手把手的指导确保掌握每个技术细节并落地应用** 41 | **快速进阶班适合学痴和学霸,只支持在线直播授课,每期一百人左右。适合有测试开发基础的同学,提供最关键的技术讲解和演练,确保短时间掌握技术精髓和学习方向** 42 | 43 | # 测试开发技能大纲 44 | 课程内容由思寒和校长等多位行业技术专家操刀,邀请了腾讯、阿里、百度、360等公司的测试技术专家进行了评估和改进。 45 | 保证学习的内容足够深入使用,实现让学员对测试开发技能有深入理解和应用的水平。 46 | 47 | ### 移动测试技术体系 思寒 48 | - 移动测试基础 49 | - 研发阶段的质量保证 白盒测试 代码审计 单元测试 50 | - 测试阶段的质量保证 接口测试 专项测试 场景测试 业务测试 51 | - 发布后的质量监控 接口监控 问题收集 52 | 53 | ### Bash课程基础 思寒 54 | - Bash 55 | - Linux和Android iOS的基本命令使用 56 | - 脚本编写与自动化相关 57 | - awk grep sed mail curl工具使用 58 | 59 | ### Appium自动化-Android 大东 60 | - Appium安装 61 | - Appium客户端安装 62 | - Android自动化测试用例编写 63 | - 自动化测试演练 64 | - 自动化测试常见技术点分析 65 | 66 | ### Appium自动化-iOS 大东 67 | - iOS自动化基础知识 68 | - iOS自动化测试用例编写 69 | - iOS自动化测试演练 70 | - Appium流程分析与错误定位 71 | 72 | ### 自动遍历测试技术 思寒 73 | - Monkey工具使用 74 | - AppCrawler在Android上的遍历测试 75 | - AppCrawler在iOS上的遍历测试分析 76 | - 弹框处理与Watch机制 77 | - Android与iOS dom分析 78 | - XPath定位高级技巧 79 | 80 | ### 接口测试入门 校长 81 | - 接口测试基本概念 82 | - 接口测试用例编写 83 | - 接口测试的cookie和session机制 84 | - 接口测试断言机制 JsonPath与XmlPath 85 | - 接口schema校验 86 | - 接口测试演练 87 | 88 | ### 接口测试进阶 校长与思寒 89 | - dubbo与数据库协议的接口测试 90 | - excel xml等数据驱动的测试用例 91 | - 测试用例和测试套件管理机制 92 | - Jenkins与接口测试的集成 93 | - 接口测试平台建设 94 | 95 | ### 持续集成 96 | - Jenkins的搭建部署 97 | - Jenkins workflow任务管理机制 98 | - Jenkins与svn git的对接和代码构建 99 | - Jenkins调用移动测试框架appium的演练 100 | - Jenkins调用web测试框架selenium的演练 101 | - Jenkins调用接口测试框架RestAssured的演练 102 | 103 | ### 持续集成进阶 104 | - 持续集成 devops 持续交付讲解 105 | - Jenkins 2.0 pipeline机制 106 | - pipeline定义和使用 107 | - blueocean的使用 108 | - 自定义测试报告与图表 109 | 110 | ### docker容器技术 高飞 111 | - Docker的基础和特点 112 | - docker的生态体系 113 | - 演练用docker搭建Jenkins 114 | - 演练用docker搭建Selenium 115 | - 演练用docker搭建sonarqube 116 | - 演练用docker搭建反向代理 117 | - dockerfile和镜像管理 118 | - 动作制作docker镜像 119 | 120 | 121 | # 咨询报名 122 | 在线报名地址: https://www.bagevent.com/event/863420 123 | 感兴趣可联络思寒同学 124 | 微信:seveniruby 手机:15600534760 125 | ![](/uploads/photo/2017/d1faea84-a4a5-485e-9666-7fbd60d08259.png!large =300x) 126 | 127 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trevorwang/AppCrawler/01dc3a648e8a1a790ee0cd03d2fc27a179331cef/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.6-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /lib/chkbugreport-0.5-215.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trevorwang/AppCrawler/01dc3a648e8a1a790ee0cd03d2fc27a179331cef/lib/chkbugreport-0.5-215.jar -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "appium": "^1.18.1" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 1.3.13 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | logLevel := Level.Warn 2 | addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.2.2") 3 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.5") 4 | //addSbtPlugin("org.scala-debugger" % "sbt-scala-debugger" % "1.1.0-M3") 5 | //addSbtPlugin("org.scala-debugger" % "sbt-jdi-tools" % "1.0.0") 6 | //addSbtPlugin("org.senkbeil" %% "sbt-grus" % "0.1.0") 7 | //addSbtPlugin("org.scala-debugger" %% "sbt-scala-debugger" % "1.1.0-M3") 8 | //addSbtPlugin("com.typesafe.sbt" % "sbt-proguard" % "0.2.2") 9 | //addSbtPlugin("com.artima.supersafe" % "sbtplugin" % "1.1.0") 10 | //addSbtPlugin("io.get-coursier" % "sbt-coursier" % "1.0.0-M14-3") 11 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * This file was generated by the Gradle 'init' task. 3 | * 4 | * The settings file is used to specify which projects to include in your build. 5 | * 6 | * Detailed information about configuring a multi-project build in Gradle can be found 7 | * in the user manual at https://docs.gradle.org/6.6/userguide/multi_project_builds.html 8 | */ 9 | 10 | rootProject.name = 'appcrawler2' 11 | -------------------------------------------------------------------------------- /src/main/java/com/testerhome/appcrawler/AppiumTouchAction.java: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler; 2 | 3 | import io.appium.java_client.AppiumDriver; 4 | import io.appium.java_client.TouchAction; 5 | import io.appium.java_client.touch.TapOptions; 6 | import io.appium.java_client.touch.WaitOptions; 7 | import io.appium.java_client.touch.offset.ElementOption; 8 | import io.appium.java_client.touch.offset.PointOption; 9 | import org.openqa.selenium.WebElement; 10 | 11 | import java.time.Duration; 12 | 13 | public class AppiumTouchAction { 14 | TouchAction action; 15 | int width; 16 | int height; 17 | public AppiumTouchAction(AppiumDriver driver){ 18 | action=new TouchAction(driver); 19 | } 20 | public AppiumTouchAction(AppiumDriver driver, int width, int height){ 21 | action=new TouchAction(driver); 22 | this.width=width; 23 | this.height=height; 24 | } 25 | public AppiumTouchAction swipe(Double startX, Double startY, Double endX, Double endY){ 26 | action.press( 27 | PointOption.point((int)(width*startX), (int)(height*startY))) 28 | .waitAction(WaitOptions.waitOptions(Duration.ofSeconds(1))) 29 | .moveTo(PointOption.point((int)(width*endX), (int)(height*endY))) 30 | .release() 31 | .perform(); 32 | return this; 33 | } 34 | 35 | public AppiumTouchAction tap(WebElement currentElement){ 36 | action.tap( 37 | TapOptions.tapOptions().withElement(ElementOption.element(currentElement)) 38 | ).perform(); 39 | return this; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/testerhome/appcrawler/Demo.java: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler; 2 | 3 | public class Demo { 4 | public void hello(){ 5 | System.out.println("hello"); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/resources/log4j2.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trevorwang/AppCrawler/01dc3a648e8a1a790ee0cd03d2fc27a179331cef/src/main/resources/log4j2.properties -------------------------------------------------------------------------------- /src/main/scala/com/testerhome/appcrawler/AppiumSuite.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler 2 | 3 | import java.awt.{Color, BasicStroke} 4 | import java.io.File 5 | import java.net.URL 6 | import javax.imageio.ImageIO 7 | 8 | import io.appium.java_client.AppiumDriver 9 | import io.appium.java_client.android.AndroidDriver 10 | import io.appium.java_client.ios.IOSDriver 11 | import io.appium.java_client.remote.{IOSMobileCapabilityType, AndroidMobileCapabilityType, MobileCapabilityType} 12 | import org.apache.commons.io.FileUtils 13 | import org.apache.log4j.Level 14 | import org.openqa.selenium.{OutputType, TakesScreenshot, WebElement} 15 | import org.openqa.selenium.remote.DesiredCapabilities 16 | import org.scalatest._ 17 | import org.scalatest.selenium.WebBrowser 18 | import org.scalatest.time.{Seconds, Span} 19 | 20 | import scala.sys.process.ProcessLogger 21 | import scala.util.{Failure, Success, Try} 22 | 23 | import scala.sys.process._ 24 | /** 25 | * Created by seveniruby on 16/3/26. 26 | */ 27 | class AppiumSuite extends FunSuite 28 | with Matchers 29 | with WebBrowser 30 | with BeforeAndAfterAll 31 | with BeforeAndAfterEach 32 | with CommonLog { 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/scala/com/testerhome/appcrawler/AutomationSuite.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler 2 | 3 | import org.scalatest 4 | import org.scalatest.{BeforeAndAfterAllConfigMap, ConfigMap, FunSuite, Matchers} 5 | 6 | /** 7 | * Created by seveniruby on 2017/4/17. 8 | */ 9 | class AutomationSuite extends FunSuite with Matchers with BeforeAndAfterAllConfigMap with CommonLog { 10 | var crawler: Crawler = _ 11 | 12 | override def beforeAll(configMap: ConfigMap): Unit = { 13 | log.info("beforeAll") 14 | crawler = configMap.get("crawler").get.asInstanceOf[Crawler] 15 | } 16 | 17 | 18 | //todo: 利用suite排序进入延迟执行 19 | 20 | test("run steps") { 21 | log.info("testcase start") 22 | val conf = crawler.conf 23 | val driver = crawler.driver 24 | 25 | val cp = new scalatest.Checkpoints.Checkpoint 26 | 27 | conf.testcase.steps.foreach(step => { 28 | log.info(step) 29 | val xpath=step.getXPath() 30 | val action=step.getAction() 31 | log.info(xpath) 32 | log.info(action) 33 | 34 | driver.findMapWithRetry(xpath).headOption match { 35 | case Some(v) => { 36 | val ele = new URIElement(v, "Steps") 37 | crawler.doElementAction(ele, action) 38 | } 39 | case None => { 40 | //用于生成steps的用例 41 | val ele = URIElement(url="Steps", tag="", id="", name="NOT_FOUND", xpath=xpath) 42 | crawler.doElementAction(ele, "") 43 | withClue("NOT_FOUND"){ 44 | log.info(xpath) 45 | fail(s"ELEMENT_NOT_FOUND xpath=${xpath}") 46 | } 47 | } 48 | } 49 | 50 | 51 | 52 | if(step.then!=null) { 53 | step.then.foreach(existAssert => { 54 | cp { 55 | withClue(s"${existAssert} 不存在\n") { 56 | val result=driver.getNodeListByKey(existAssert) 57 | log.info(s"${existAssert}\n${TData.toJson(result)}") 58 | result.size should be > 0 59 | } 60 | } 61 | }) 62 | } 63 | }) 64 | 65 | cp.reportAll() 66 | log.info("finish run steps") 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/scala/com/testerhome/appcrawler/CommonLog.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler 2 | 3 | import java.io.OutputStreamWriter 4 | 5 | import com.fasterxml.jackson.annotation.JsonIgnore 6 | import org.apache.log4j._ 7 | 8 | 9 | /** 10 | * Created by seveniruby on 16/3/31. 11 | */ 12 | trait CommonLog { 13 | BasicConfigurator.configure() 14 | Logger.getRootLogger.setLevel(Level.INFO) 15 | @JsonIgnore 16 | val layout=new PatternLayout("%d{yyyy-MM-dd HH:mm:ss} %p [%c{1}.%L.%M] %m%n") 17 | @JsonIgnore 18 | lazy val log = initLog() 19 | 20 | def initLog(): Logger ={ 21 | val log = Logger.getLogger(this.getClass.getName) 22 | //val log=Logger.getRootLogger 23 | if(log.getAppender("console")==null){ 24 | val console=new ConsoleAppender() 25 | console.setName("console") 26 | console.setWriter(new OutputStreamWriter(System.out)) 27 | console.setLayout(layout) 28 | log.addAppender(console) 29 | }else{ 30 | log.info("already exist") 31 | } 32 | log.trace(s"set ${this} log level to ${GA.logLevel}") 33 | log.setLevel(GA.logLevel) 34 | log.setAdditivity(false) 35 | log 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/scala/com/testerhome/appcrawler/CrawlerSuite.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler 2 | 3 | import org.scalatest.Sequential 4 | 5 | /** 6 | * Created by seveniruby on 2017/4/17. 7 | */ 8 | class CrawlerSuite extends Sequential( 9 | new AutomationSuite 10 | ) -------------------------------------------------------------------------------- /src/main/scala/com/testerhome/appcrawler/DataObject.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler 2 | 3 | import com.fasterxml.jackson.databind.{DeserializationFeature, SerializationFeature, ObjectMapper} 4 | import com.fasterxml.jackson.dataformat.yaml.YAMLFactory 5 | import com.fasterxml.jackson.module.scala.DefaultScalaModule 6 | 7 | import scala.collection.mutable 8 | import scala.collection.mutable.ArrayBuffer 9 | import scala.reflect.ClassTag 10 | import scala.reflect.ClassTag 11 | import scala.reflect._ 12 | 13 | import org.jsoup.Jsoup 14 | import org.jsoup.nodes.{Document, Element} 15 | import org.jsoup.select.Elements 16 | 17 | import scala.collection.JavaConversions._ 18 | 19 | 20 | /** 21 | * Created by seveniruby on 16/8/13. 22 | */ 23 | trait DataObject { 24 | 25 | def toYaml(data: Any): String = { 26 | val mapper = new ObjectMapper(new YAMLFactory()) 27 | mapper.registerModule(DefaultScalaModule) 28 | mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) 29 | mapper.writerWithDefaultPrettyPrinter().writeValueAsString(data) 30 | } 31 | 32 | def fromYaml[T: ClassTag](data: String): T = { 33 | val mapper = new ObjectMapper(new YAMLFactory()) 34 | mapper.registerModule(DefaultScalaModule) 35 | mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); 36 | mapper.readValue(data, classTag[T].runtimeClass.asInstanceOf[Class[T]]) 37 | } 38 | 39 | 40 | def toJson(data: Any): String = { 41 | val mapper = new ObjectMapper() 42 | mapper.registerModule(DefaultScalaModule) 43 | mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) 44 | mapper.writerWithDefaultPrettyPrinter().writeValueAsString(data) 45 | } 46 | 47 | 48 | def fromJson[T: ClassTag](str: String): T = { 49 | val mapper = new ObjectMapper() 50 | mapper.registerModule(DefaultScalaModule) 51 | mapper.readValue(str, classTag[T].runtimeClass.asInstanceOf[Class[T]]) 52 | } 53 | 54 | def fromXML(str: String): Map[String, Any] = { 55 | val node=Jsoup.parse(str) 56 | def lift(node: Element): Map[String, Any] = node match { 57 | case doc: Document => 58 | Map[String, Any]( 59 | "head" -> lift(doc.head), 60 | "body" -> lift(doc.body) 61 | ) 62 | 63 | case doc: Element => { 64 | val children: Elements = doc.children 65 | val attributes = 66 | doc.attributes.asList map { attribute => 67 | attribute.getKey -> attribute.getValue 68 | } toMap 69 | 70 | Map( 71 | "tag" -> doc.tagName, 72 | "text" -> doc.ownText, 73 | "attributes" -> attributes, 74 | "children" -> children.map(element => lift(element)) 75 | ) 76 | 77 | } 78 | } 79 | lift(node) 80 | } 81 | 82 | 83 | def flatten(data: Map[String, Any]): mutable.Map[String, Any] = { 84 | val stack = new mutable.Stack[String]() 85 | val result = mutable.Map[String, Any]() 86 | def loop(dataKV: scala.collection.Map[String, Any]): Unit = { 87 | 88 | dataKV.foreach(data => { 89 | stack.push(data._1) 90 | data match { 91 | case (key: String, valueMap: scala.collection.Map[String, _]) => { 92 | val tag = valueMap.getOrElse("tag", "").toString 93 | val key = tag.split('.').lastOption.getOrElse(tag) 94 | if (tag.nonEmpty) { 95 | stack.push(key) 96 | } 97 | 98 | valueMap.foreach(kv => { 99 | loop(scala.collection.Map(kv._1 -> kv._2)) 100 | }) 101 | 102 | if (tag.nonEmpty) { 103 | stack.pop() 104 | } 105 | 106 | } 107 | case (key: String, values: Seq[_]) => { 108 | var index = 0 109 | values.foreach(value => { 110 | loop(Map(index.toString -> value)) 111 | index += 1 112 | }) 113 | } 114 | case (key, value: Any) => { 115 | result(stack.reverse.mkString(".")) = value 116 | } 117 | } 118 | stack.pop() 119 | }) 120 | } 121 | loop(data) 122 | result 123 | } 124 | 125 | 126 | } 127 | -------------------------------------------------------------------------------- /src/main/scala/com/testerhome/appcrawler/DataRecord.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler 2 | 3 | import scala.collection.mutable.ListBuffer 4 | 5 | /** 6 | * Created by seveniruby on 16/8/25. 7 | */ 8 | class DataRecord extends CommonLog { 9 | val record=ListBuffer[(Long, Any)]() 10 | private val size=10 11 | def append(any: Any): Unit ={ 12 | record.append(System.currentTimeMillis()->any) 13 | } 14 | def intervalMS(): Long ={ 15 | if(record.size<2){ 16 | return 0 17 | }else { 18 | val lastRecords = record.takeRight(2) 19 | lastRecords.last._1 - lastRecords.head._1 20 | } 21 | } 22 | def isDiff(): Boolean ={ 23 | if(record.size<2){ 24 | log.info("just only record return false") 25 | return false 26 | }else { 27 | val lastRecords = record.takeRight(2) 28 | lastRecords.last._2 != lastRecords.head._2 29 | } 30 | } 31 | def last(count: Int): List[Any] ={ 32 | record.takeRight(count).map(_._2).toList 33 | } 34 | def pre(): Any ={ 35 | record.takeRight(2).head._2 36 | } 37 | def last(): Any ={ 38 | record.last._2 39 | } 40 | def pop(): Unit ={ 41 | record.remove(record.size-1) 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/scala/com/testerhome/appcrawler/DiffSuite.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler 2 | 3 | import com.testerhome.appcrawler.plugin.FlowDiff 4 | import org.scalatest._ 5 | 6 | import scala.io.Source 7 | import scala.reflect.io.File 8 | 9 | /** 10 | * Created by seveniruby on 16/9/26. 11 | */ 12 | 13 | class DiffSuite extends FunSuite with Matchers with CommonLog{ 14 | //只取列表的第一项 15 | var name="新老版本对比" 16 | var suite="Diff" 17 | 18 | override def suiteName=name 19 | 20 | def addTestCase(): Unit = { 21 | //每个点击事件 22 | 23 | val allKeys = DiffSuite.masterStore.filter(_._2.element.url==suite).keys ++ 24 | DiffSuite.candidateStore.filter(_._2.element.url==suite).keys 25 | log.debug(allKeys.size) 26 | 27 | allKeys.foreach(key => { 28 | val masterElements=DiffSuite.masterStore.get(key) match { 29 | case Some(elementInfo) if elementInfo.action==ElementStatus.Clicked && elementInfo.resDom.nonEmpty => { 30 | log.debug(elementInfo) 31 | log.debug(elementInfo.resDom) 32 | DiffSuite.range.map(XPathUtil.getNodeListFromXPath(_, elementInfo.resDom)) 33 | .flatten.map(m=>{ 34 | val ele=new URIElement(m, key) 35 | ele.xpath->ele 36 | }).toMap 37 | } 38 | case _ =>{ 39 | Map[String, URIElement]() 40 | } 41 | } 42 | 43 | val candidateElements=DiffSuite.candidateStore.get(key) match { 44 | //todo: 老版本点击过, 新版本没点击过, 没有resDom如何做 45 | case Some(elementInfo) if elementInfo.action==ElementStatus.Clicked && elementInfo.resDom.nonEmpty => { 46 | DiffSuite.range.map(XPathUtil.getNodeListFromXPath(_, elementInfo.resDom)) 47 | .flatten.map(m=>{ 48 | val ele=new URIElement(m, key) 49 | ele.xpath->ele 50 | }).toMap 51 | } 52 | case _ =>{ 53 | Map[String, URIElement]() 54 | } 55 | } 56 | 57 | 58 | val testcase = s"url=${key}" 59 | 60 | //todo: 支持结构对比, 数据对比 yaml配置 61 | test(testcase) { 62 | 63 | 64 | val allElementKeys=masterElements.keys++candidateElements.keys 65 | //todo: 去掉黑名单里面的字段 66 | val cp = new Checkpoints.Checkpoint() 67 | var markOnce=false 68 | allElementKeys.foreach(subKey => { 69 | val masterElement = masterElements.getOrElse(subKey, URIElement()) 70 | val candidateElement = candidateElements.getOrElse(subKey, URIElement()) 71 | val message = 72 | s""" 73 | |key=${subKey} 74 | | 75 | |candidate=${candidateElement.xpath} 76 | | 77 | |master=${masterElement.xpath} 78 | |________________ 79 | | 80 | | 81 | """.stripMargin 82 | 83 | if (masterElement != candidateElement && !markOnce) { 84 | markOnce=true 85 | markup( 86 | s""" 87 | |candidate image 88 | |------- 89 | | 90 | | 91 | |master image 92 | |-------- 93 | | 94 | | 95 | """.stripMargin) 96 | } 97 | 98 | cp { 99 | withClue(message) { 100 | candidateElement.id should equal(masterElement.id) 101 | candidateElement.name should equal(masterElement.name) 102 | candidateElement.xpath should equal(masterElement.xpath) 103 | //assert(candidate == master, message) 104 | } 105 | } 106 | }) 107 | cp.reportAll() 108 | 109 | } 110 | }) 111 | 112 | } 113 | } 114 | 115 | object DiffSuite { 116 | val masterStore = Report.loadResult(s"${Report.master}/elements.yml").elementStore 117 | val candidateStore = Report.loadResult(s"${Report.candidate}/elements.yml").elementStore 118 | val blackList = List(".*\\.instance.*", ".*bounds.*") 119 | val range=List("//*[contains(name(), 'Text')]", "//*[contains(name(), 'Image')]", "//*[contains(name(), 'Button')]") 120 | def saveTestCase(): Unit ={ 121 | val suites=masterStore.map(_._2.element.url)++candidateStore.map(_._2.element.url) 122 | suites.foreach(suite=> { 123 | SuiteToClass.genTestCaseClass(suite, "com.testerhome.appcrawler.DiffSuite", Map("suite"->suite, "name"->suite), Report.testcaseDir) 124 | }) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/main/scala/com/testerhome/appcrawler/GA.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler 2 | 3 | import com.brsanthu.googleanalytics.{GoogleAnalytics, PageViewHit} 4 | import org.apache.log4j.{BasicConfigurator, Level, Logger} 5 | 6 | /** 7 | * Created by seveniruby on 16/2/26. 8 | */ 9 | 10 | 11 | object GA { 12 | var logLevel=Level.INFO 13 | 14 | BasicConfigurator.configure() 15 | Logger.getRootLogger.setLevel(Level.OFF) 16 | Logger.getLogger(classOf[GoogleAnalytics]).setLevel(Level.OFF) 17 | val ga = new GoogleAnalytics("UA-74406102-1") 18 | var appName="default" 19 | def setAppName(app:String): Unit ={ 20 | appName=app 21 | } 22 | def log(action: String): Unit ={ 23 | ga.postAsync(new PageViewHit(s"http://appcrawler.io/${appName}/${action}", "test")) 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/scala/com/testerhome/appcrawler/ReactTestCase.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler 2 | 3 | case class ReactTestCase(name: String = "", steps: List[Step] = List[Step]()) 4 | 5 | //given表示多个条件满足 when表示执行多个动作,then表示多个断言,xpath和action为when的简写 6 | /** 7 | * 当given先决条件满足时,在when的find指定内容上执行action操作。并断言then里面的所有表达式 8 | * @param given 9 | * @param when 10 | * @param then 11 | * @param xpath 12 | * @param action 13 | * @param actions 14 | * @param times 15 | */ 16 | case class Step(given: List[String]=List[String](), 17 | var when: When=null, 18 | //todo: testcase和trigger 遍历都支持断言和报告输出 19 | then: List[String]=List[String](), 20 | xpath: String="//*", 21 | action: String=null, 22 | actions: List[String]=List[String](), 23 | var times:Int = 0 24 | ){ 25 | def use(): Int ={ 26 | times-=1 27 | times 28 | } 29 | def getXPath(): String ={ 30 | if(when==null){ 31 | if(this.xpath==null){ 32 | "/*" 33 | }else{ 34 | this.xpath 35 | } 36 | }else{ 37 | if(when.xpath==null){ 38 | "/*" 39 | }else{ 40 | when.xpath 41 | } 42 | } 43 | } 44 | 45 | def getAction(): String ={ 46 | val result=if(when==null){ 47 | action 48 | }else{ 49 | when.action 50 | } 51 | if(result==null){ 52 | "" 53 | }else{ 54 | result 55 | } 56 | } 57 | } 58 | case class When(xpath: String=null, 59 | action: String=null, 60 | actions: List[String]=List[String]() 61 | ) 62 | 63 | -------------------------------------------------------------------------------- /src/main/scala/com/testerhome/appcrawler/Report.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler 2 | 3 | import org.apache.commons.io.FileUtils 4 | import org.scalatest.tools.Runner 5 | 6 | import scala.collection.mutable 7 | import scala.collection.mutable.ListBuffer 8 | import scala.io.{Source, Codec} 9 | import scala.reflect.io.File 10 | import collection.JavaConversions._ 11 | 12 | /** 13 | * Created by seveniruby on 16/8/15. 14 | */ 15 | trait Report extends CommonLog { 16 | var reportPath = "" 17 | var testcaseDir = "" 18 | 19 | def saveTestCase(store: URIElementStore, resultDir: String): Unit = { 20 | log.info("save testcase") 21 | reportPath = resultDir 22 | testcaseDir = reportPath + "/tmp/" 23 | //为了保持独立使用 24 | val path = new java.io.File(resultDir).getCanonicalPath 25 | 26 | val suites = store.elementStore.map(x => x._2.element.url).toList.distinct 27 | suites.foreach(suite => { 28 | log.info(s"gen testcase class ${suite}") 29 | //todo: 基于规则的多次点击事件只会被保存到一个状态中. 需要区分 30 | SuiteToClass.genTestCaseClass( 31 | suite, 32 | "com.testerhome.appcrawler.TemplateTestCase", 33 | Map("uri"->suite, "name"->suite), 34 | testcaseDir 35 | ) 36 | }) 37 | } 38 | 39 | 40 | //todo: 用junit+allure代替 41 | def runTestCase(namespace: String=""): Unit = { 42 | var cmdArgs = Array("-R", testcaseDir, 43 | "-oF", "-u", reportPath, "-h", reportPath) 44 | 45 | if(namespace.nonEmpty){ 46 | cmdArgs++=Array("-s", namespace) 47 | } 48 | 49 | /* 50 | val testcaseDirFile=new java.io.File(testcaseDir) 51 | FileUtils.listFiles(testcaseDirFile, Array(".class"), true).map(_.split(".class").head) 52 | val suites= testcaseDirFile.list().filter(_.endsWith(".class")).map(_.split(".class").head).toList 53 | suites.map(suite => Array("-s", s"${namespace}${suite}")).foreach(array => { 54 | cmdArgs = cmdArgs ++ array 55 | }) 56 | 57 | if (suites.size > 0) { 58 | log.info(s"run ${cmdArgs.toList}") 59 | Runner.run(cmdArgs) 60 | Runtimes.reset 61 | changeTitle 62 | } 63 | */ 64 | log.info(s"run ${cmdArgs.mkString(" ")}") 65 | Runner.run(cmdArgs) 66 | changeTitle() 67 | } 68 | 69 | def changeTitle(title:String=Report.title): Unit ={ 70 | val originTitle="ScalaTest Results" 71 | val indexFile=reportPath+"/index.html" 72 | val newContent=Source.fromFile(indexFile).mkString.replace(originTitle, title) 73 | scala.reflect.io.File(indexFile).writeAll(newContent) 74 | } 75 | 76 | } 77 | 78 | object Report extends Report{ 79 | var showCancel=false 80 | var title="AppCrawler" 81 | var master="" 82 | var candidate="" 83 | var reportDir="" 84 | var store=new URIElementStore 85 | 86 | 87 | def loadResult(elementsFile: String): URIElementStore ={ 88 | TData.fromYaml[URIElementStore](Source.fromFile(elementsFile).mkString) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/scala/com/testerhome/appcrawler/ReportSuite.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler 2 | 3 | import org.scalatest.FunSuite 4 | 5 | /** 6 | * Created by seveniruby on 16/9/27. 7 | */ 8 | class ReportSuite extends FunSuite{ 9 | var name="demo" 10 | override def suiteName=name 11 | } 12 | -------------------------------------------------------------------------------- /src/main/scala/com/testerhome/appcrawler/Runtimes.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler 2 | 3 | import java.io.File 4 | 5 | import com.testerhome.appcrawler.plugin.Plugin 6 | 7 | import scala.reflect.internal.util.ScalaClassLoader.URLClassLoader 8 | import scala.tools.nsc.interpreter.IMain 9 | import scala.tools.nsc.{Global, Settings} 10 | 11 | /** 12 | * Created by seveniruby on 16/8/13. 13 | */ 14 | class Runtimes(val outputDir: String = "") extends CommonLog { 15 | private val settingsCompile = new Settings() 16 | 17 | if (outputDir.nonEmpty) { 18 | val tempDir = new File(outputDir) 19 | if (tempDir.exists() == false) { 20 | tempDir.mkdir() 21 | } 22 | settingsCompile.outputDirs.setSingleOutput(this.outputDir) 23 | } 24 | 25 | settingsCompile.deprecation.value = true // enable detailed deprecation warnings 26 | settingsCompile.unchecked.value = true // enable detailed unchecked warnings 27 | settingsCompile.usejavacp.value = true 28 | 29 | val global = new Global(settingsCompile) 30 | val run = new global.Run 31 | 32 | private val settingsEval = new Settings() 33 | settingsEval.deprecation.value = true // enable detailed deprecation warnings 34 | settingsEval.unchecked.value = true // enable detailed unchecked warnings 35 | settingsEval.usejavacp.value = true 36 | 37 | val interpreter = new IMain(settingsEval) 38 | 39 | def compile(fileNames: List[String]): Unit = { 40 | run.compile(fileNames) 41 | } 42 | 43 | def eval(code: String): Unit = { 44 | interpreter.interpret(code) 45 | } 46 | 47 | def reset(): Unit = { 48 | 49 | } 50 | 51 | 52 | } 53 | 54 | object Runtimes extends CommonLog { 55 | var instance = new Runtimes() 56 | var isLoaded = false 57 | 58 | def apply(): Unit = { 59 | 60 | } 61 | 62 | def eval(code: String): Unit = { 63 | if (isLoaded == false) { 64 | log.info("first import") 65 | instance.eval("val driver=com.testerhome.appcrawler.AppCrawler.crawler.driver") 66 | instance.eval("def crawl(depth:Int)=com.testerhome.appcrawler.AppCrawler.crawler.crawl(depth)") 67 | isLoaded = true 68 | } 69 | log.info(code) 70 | instance.eval(code) 71 | log.info("eval finish") 72 | } 73 | 74 | def compile(fileNames: List[String]): Unit = { 75 | instance.compile(fileNames) 76 | isLoaded = false 77 | } 78 | 79 | def init(classDir: String = ""): Unit = { 80 | instance = new Runtimes(classDir) 81 | } 82 | 83 | def reset(): Unit = { 84 | 85 | } 86 | 87 | def loadPlugins(pluginDir: String = ""): List[Plugin] = { 88 | val pluginDirFile = new java.io.File(pluginDir) 89 | if (pluginDirFile.exists() == false) { 90 | log.warn(s"no ${pluginDir} directory, skip") 91 | return Nil 92 | } 93 | val pluginFiles = pluginDirFile.list().filter(_.endsWith(".scala")).toList 94 | val pluginClassNames = pluginFiles.map(_.split(".scala").head) 95 | log.info(s"find plugins in ${pluginDir}") 96 | log.info(pluginFiles) 97 | log.info(pluginClassNames) 98 | val runtimes = new Runtimes(pluginDir) 99 | runtimes.compile(pluginFiles.map(pluginDirFile.getCanonicalPath + File.separator + _)) 100 | val urls = Seq(pluginDirFile.toURI.toURL, getClass.getProtectionDomain.getCodeSource.getLocation) 101 | val loader = new URLClassLoader(urls, Thread.currentThread().getContextClassLoader) 102 | pluginClassNames.map(loader.loadClass(_).newInstance().asInstanceOf[Plugin]) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/main/scala/com/testerhome/appcrawler/SuiteToClass.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler 2 | 3 | import org.apache.commons.lang3.StringEscapeUtils 4 | import javassist.{ClassPool, CtConstructor} 5 | 6 | import scala.util.{Failure, Success, Try} 7 | 8 | /** 9 | * Created by seveniruby on 2017/4/15. 10 | */ 11 | object SuiteToClass extends CommonLog { 12 | 13 | var index=0 14 | 15 | //todo: 特殊字符处理 16 | def format(name:String): String ={ 17 | name 18 | .replaceAllLiterally("\\", "\\\\") 19 | .replaceAllLiterally("\"", "\\\"") 20 | .replaceAllLiterally("#", "") 21 | .replaceAllLiterally("&", "") 22 | .replaceAll("[#;/]", "") 23 | } 24 | /** 25 | * 生成用例对应的class文件,用于调用scalatest执行 26 | * 27 | */ 28 | def genTestCaseClass(className: String, superClassName: String, fields: Map[String, Any], directory: String): Unit = { 29 | val pool = ClassPool.getDefault 30 | val classNameFormat = format(className) 31 | Try(pool.makeClass(classNameFormat)) match { 32 | case Success(classNew) => { 33 | classNew.setSuperclass(pool.get(superClassName)) 34 | val init = new CtConstructor(null, classNew) 35 | val body = fields.map(field => { 36 | s"${field._1}_$$eq(${'"' + format(field._2.toString) + '"'}); " 37 | }).mkString("\n") 38 | init.setBody(s"{ ${body}\naddTestCase(); }") 39 | classNew.addConstructor(init) 40 | classNew.writeFile(directory) 41 | } 42 | case Failure(e) => { 43 | log.error(s"makeClass error with ${className}") 44 | } 45 | } 46 | } 47 | 48 | } 49 | 50 | -------------------------------------------------------------------------------- /src/main/scala/com/testerhome/appcrawler/Template.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler 2 | 3 | import java.io.File 4 | 5 | import org.apache.commons.io.FileUtils 6 | import org.fusesource.scalate.TemplateEngine 7 | 8 | import scala.collection.mutable 9 | import scala.collection.mutable.ListBuffer 10 | import scala.io.Source 11 | 12 | /** 13 | * Created by seveniruby on 2017/1/10. 14 | */ 15 | class Template { 16 | 17 | val elements = mutable.HashMap[String, ListBuffer[Map[String, Any]]]() 18 | 19 | 20 | def getPageSource(url:String): Unit ={ 21 | val page=Source.fromURL(s"${url}/source/xml").mkString 22 | val xml=TData.fromJson[Map[String, String]](page).getOrElse("value", "") 23 | .asInstanceOf[Map[String, String]].getOrElse("tree", "") 24 | elements("Demo")=ListBuffer[Map[String, Any]]() 25 | elements("Demo")++=XPathUtil.getNodeListFromXPath("//*[]", xml) 26 | 27 | } 28 | def read(path:String): Unit = { 29 | 30 | //val path = "/Users/seveniruby/projects/AppCrawlerSuite/AppCrawler/android_20170109145102/elements.yml" 31 | val store = (TData.fromYaml[URIElementStore](Source.fromFile(path).mkString)).elementStore 32 | 33 | store.foreach(s => { 34 | val reqDom = s._2.reqDom 35 | val url = s._2.element.url 36 | if (reqDom.size != 0) { 37 | 38 | if (elements.contains(url) == false) { 39 | elements.put(url, ListBuffer[Map[String, Any]]()) 40 | } 41 | elements(url) ++= XPathUtil.getNodeListFromXPath("//*", reqDom) 42 | val tagsLimit=List("Image", "Button", "Text") 43 | elements(url) = elements(url) 44 | .filter(_.getOrElse("visible", "true")=="true") 45 | .filter(_.getOrElse("tag", "").toString.contains("StatusBar")==false) 46 | .filter(e=>tagsLimit.exists(t=>e.getOrElse("tag", "").toString.contains(t))) 47 | .distinct 48 | } 49 | 50 | }) 51 | } 52 | 53 | def write(template:String, dir:String) { 54 | val engine = new TemplateEngine 55 | elements.foreach(e => { 56 | val file:String = e._1 57 | println(s"file=${file}") 58 | e._2.foreach(m => { 59 | val name = m("name") 60 | val value = m("value") 61 | val label = m("label") 62 | val xpath = m("xpath") 63 | println(s"name=${name} label=${label} value=${value} xpath=${xpath}") 64 | }) 65 | 66 | val output = engine.layout(template, Map( 67 | "file" -> s"Template_${file.split('-').takeRight(1).head.toString}", 68 | "elements" -> elements(file)) 69 | ) 70 | println(output) 71 | 72 | val directory=new File(dir) 73 | if(directory.exists()==false){ 74 | FileUtils.forceMkdir(directory) 75 | } 76 | println(s"template source directory = ${dir}") 77 | val appdex=template.split('.').takeRight(2).head 78 | scala.reflect.io.File(s"${dir}/${file}.${appdex}").writeAll(output) 79 | 80 | }) 81 | 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/main/scala/com/testerhome/appcrawler/TemplateTestCase.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler 2 | 3 | import org.apache.commons.lang3.StringUtils 4 | import org.scalatest 5 | import org.scalatest._ 6 | 7 | import scala.reflect.io.File 8 | 9 | 10 | /** 11 | * Created by seveniruby on 2017/3/25. 12 | */ 13 | class TemplateTestCase extends FunSuite with BeforeAndAfterAllConfigMap with Matchers with CommonLog { 14 | var name = "template" 15 | var uri = "" 16 | 17 | override def suiteName = name 18 | 19 | def addTestCase() { 20 | val sortedElements = Report.store.elementStore 21 | .filter(x => x._2.element.url == uri) 22 | .map(_._2).toList 23 | .sortBy(_.clickedIndex) 24 | 25 | log.info(sortedElements.size) 26 | 27 | val selected = if (Report.showCancel) { 28 | log.info("show all elements") 29 | //把未遍历的放到后面 30 | sortedElements.filter(_.action == ElementStatus.Clicked) ++ 31 | //sortedElements.filter(_.action == ElementStatus.Skipped) ++ 32 | sortedElements.filter(_.action == ElementStatus.Ready) 33 | } else { 34 | log.info("only show clicked elements") 35 | sortedElements.filter(_.action == ElementStatus.Clicked) 36 | } 37 | log.info(selected.size) 38 | selected.foreach(ele => { 39 | val testcase = ele.element.xpath.replace("\\", "\\\\") 40 | .replace("\"", "\\\"") 41 | .replace("\n", "") 42 | .replace("\r", "") 43 | 44 | //todo: 增加ignore和cancel的区分 45 | test(s"clickedIndex=${ele.clickedIndex} action=${ele.action}\nxpath=${testcase}") { 46 | ele.action match { 47 | case ElementStatus.Clicked => { 48 | markup( 49 | s""" 50 | | 51 | | 52 | |

53 | |

after clicked

54 | | 55 | """.stripMargin 56 | ) 57 | 58 | /* 59 | markup( 60 | s""" 61 | | 62 | |
 63 |                 |
 64 |                 |${ele.reqDom.replaceFirst("xml", "x_m_l")}
 65 |                 |
 66 |                 |
67 | """.stripMargin 68 | ) 69 | */ 70 | log.debug(ele.reqDom) 71 | 72 | AppCrawler.crawler.conf.assertGlobal.foreach(step => { 73 | if (XPathUtil.getNodeListFromXPath(step.when.xpath, ele.reqDom) 74 | .map(_.getOrElse("xpath", "")) 75 | .headOption == Some(ele.element.xpath) 76 | ) { 77 | log.info(s"match testcase ${ele.element.xpath}") 78 | 79 | if(step.then!=null) { 80 | val cp = new scalatest.Checkpoints.Checkpoint 81 | step.then.foreach(existAssert => { 82 | log.debug(existAssert) 83 | cp { 84 | withClue(s"${existAssert} 不存在\n") { 85 | XPathUtil.getNodeListFromXPath(existAssert, ele.resDom).size should be > 0 86 | } 87 | } 88 | }) 89 | cp.reportAll() 90 | } 91 | } else { 92 | log.info("not match") 93 | } 94 | }) 95 | 96 | } 97 | case ElementStatus.Ready => { 98 | cancel(s"${ele.action} not click") 99 | } 100 | case ElementStatus.Skipped => { 101 | cancel(s"${ele.action} skipped") 102 | } 103 | } 104 | 105 | } 106 | }) 107 | } 108 | } 109 | 110 | object TemplateTestCase extends CommonLog { 111 | def saveTestCase(store: URIElementStore, resultDir: String): Unit = { 112 | log.info("save testcase") 113 | Report.reportPath = resultDir 114 | Report.testcaseDir = Report.reportPath + "/tmp/" 115 | //为了保持独立使用 116 | val path = new java.io.File(resultDir).getCanonicalPath 117 | 118 | val suites = store.elementStore.map(x => x._2.element.url).toList.distinct 119 | suites.foreach(suite => { 120 | log.info(s"gen testcase class ${suite}") 121 | //todo: 基于规则的多次点击事件只会被保存到一个状态中. 需要区分 122 | SuiteToClass.genTestCaseClass( 123 | //todo: Illegal class name Ⅱ[@]][() 124 | suite, 125 | "com.testerhome.appcrawler.TemplateTestCase", 126 | Map("uri" -> suite, "name" -> suite), 127 | Report.testcaseDir 128 | ) 129 | }) 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /src/main/scala/com/testerhome/appcrawler/TreeNode.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler 2 | 3 | import java.io.{BufferedWriter, FileWriter} 4 | 5 | import scala.collection.mutable.ListBuffer 6 | 7 | /** 8 | * Created by seveniruby on 15/12/18. 9 | */ 10 | case class TreeNode[T]( 11 | value: T, 12 | children: ListBuffer[TreeNode[T]] = ListBuffer[TreeNode[T]]() 13 | ) { 14 | 15 | def equals(node: TreeNode[T]): Boolean = { 16 | node.value == this.value 17 | } 18 | 19 | 20 | def find(tree: TreeNode[T], node: TreeNode[T]): Option[TreeNode[T]] = { 21 | if (tree.equals(node)) { 22 | return Some(tree) 23 | } 24 | tree.children.foreach(t => { 25 | find(t, node) match { 26 | case Some(v) => return Some(v) 27 | case None => {} 28 | } 29 | }) 30 | None 31 | } 32 | 33 | def appendNode(currenTree: TreeNode[T], node: TreeNode[T]): TreeNode[T] = { 34 | find(currenTree, node) match { 35 | case Some(v) => { 36 | v 37 | } 38 | case None => { 39 | this.children.append(node) 40 | node 41 | } 42 | } 43 | } 44 | 45 | 46 | def toXml(tree: TreeNode[T]): String = { 47 | val s=new StringBuffer() 48 | val before = (tree: TreeNode[T]) => { 49 | s.append(s"""""") 50 | //todo: 增加图片地址链接 LINK="file:///Users/seveniruby/projects/LBSRefresh/Android_20160216105737/946_StockDetail-Back--.png" 51 | } 52 | val after = (tree: TreeNode[T]) => { 53 | s.append("") 54 | s.append("\n") 55 | } 56 | 57 | s.append("""""") 58 | s.append("\n") 59 | traversal[T](tree, before, after) 60 | s.append("") 61 | s.toString 62 | } 63 | 64 | def traversal[T](tree: TreeNode[T], 65 | before: (TreeNode[T]) => Any = (x: TreeNode[T]) => Unit, 66 | after: (TreeNode[T]) => Any = (x: TreeNode[T]) => Unit): Unit = { 67 | before(tree) 68 | tree.children.foreach(t => { 69 | traversal(t, before, after) 70 | }) 71 | after(tree) 72 | } 73 | 74 | def generateFreeMind(list: ListBuffer[T], path:String=null): String = { 75 | if(list.isEmpty){ 76 | return "" 77 | } 78 | val root=TreeNode(list.head) 79 | var currentNode=root 80 | list.slice(1, list.size).foreach(e=>{ 81 | currentNode=currentNode.appendNode(root, TreeNode(e)) 82 | }) 83 | val xml=toXml(root) 84 | if(path!=null){ 85 | val file = new java.io.File(path) 86 | val bw = new BufferedWriter(new FileWriter(file)) 87 | bw.write(xml) 88 | bw.close() 89 | } 90 | xml 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /src/main/scala/com/testerhome/appcrawler/URIElement.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler 2 | 3 | import java.io.File 4 | 5 | import javax.xml.bind.annotation.XmlAttribute 6 | import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty 7 | import org.apache.commons.text.StringEscapeUtils 8 | import org.openqa.selenium.{Dimension, Point} 9 | 10 | import scala.collection.immutable 11 | 12 | /** 13 | * Created by seveniruby on 15/12/18. 14 | */ 15 | case class URIElement( 16 | @XmlAttribute(name = "url") 17 | var url: String="", 18 | @XmlAttribute(name = "tag") 19 | var tag: String="", 20 | @XmlAttribute(name = "id") 21 | var id: String="", 22 | @XmlAttribute(name = "name") 23 | var name: String="", 24 | @XmlAttribute(name = "text") 25 | var text: String="", 26 | @XmlAttribute(name = "instance") 27 | var instance: String="", 28 | @XmlAttribute(name = "depth") 29 | var depth: String="", 30 | @XmlAttribute(name = "valid") 31 | var valid: String="true", 32 | @XmlAttribute(name = "selected") 33 | var selected: String="false", 34 | @XmlAttribute(name = "loc") 35 | var xpath:String="", 36 | @XmlAttribute(name = "ancestor") 37 | var ancestor:String="", 38 | @XmlAttribute(name = "x") 39 | var x:Int=0, 40 | @XmlAttribute(name = "y") 41 | var y: Int=0, 42 | @XmlAttribute(name = "width") 43 | var width: Int=0, 44 | @XmlAttribute(name = "height") 45 | var height:Int=0 46 | ) { 47 | //用来代表唯一的控件, 每个特定的命名控件只被点击一次. 所以这个element的构造决定了控件是否可被点击多次. 48 | //比如某个输入框被命名为url=xueqiu id=input, 那么就只能被点击一次 49 | //如果url修改为url=xueqiu/xxxActivity id=input 就可以被点击多次 50 | //定义url是遍历的关键. 这是一门艺术 51 | 52 | def this(nodeMap:scala.collection.Map[String, Any], uri:String) = { 53 | //name为id/name属性. 为空的时候为value属性 54 | //id表示android的resource-id或者iOS的name属性 55 | 56 | this() 57 | this.url=uri 58 | this.tag = nodeMap.getOrElse("tag", "").toString 59 | this.id = nodeMap.getOrElse("name", "").toString 60 | this.name = nodeMap.getOrElse("label", "").toString 61 | this.text = nodeMap.getOrElse("value", "").toString 62 | this.instance = nodeMap.getOrElse("instance", "").toString 63 | this.depth = nodeMap.getOrElse("depth", "").toString 64 | this.xpath = nodeMap.getOrElse("xpath", "").toString 65 | this.x=nodeMap.getOrElse("x", "0").toString.toInt 66 | this.y=nodeMap.getOrElse("y", "0").toString.toInt 67 | this.width=nodeMap.getOrElse("width", "0").toString.toInt 68 | this.height=nodeMap.getOrElse("height", "0").toString.toInt 69 | this.ancestor=nodeMap.getOrElse("ancestor", "").toString 70 | this.selected=nodeMap.getOrElse("selected", "false").toString 71 | this.valid=nodeMap.getOrElse("valid", "true").toString 72 | 73 | 74 | } 75 | /** 76 | * 提取元素的tag组成的路径 77 | * @return 78 | */ 79 | def getAncestor(): String ={ 80 | ancestor 81 | } 82 | def center(): Point ={ 83 | new Point(x+width/2, y+height/2) 84 | } 85 | 86 | def location(): Point={ 87 | new Point(x, y) 88 | } 89 | 90 | def size(): Dimension ={ 91 | new Dimension(width, height) 92 | } 93 | def toXml(): String ={ 94 | s""" 95 | | 98 | """.stripMargin 99 | 100 | } 101 | 102 | override def toString: String = { 103 | val fileName=new StringBuilder() 104 | fileName.append(url) 105 | fileName.append(s".tag=${tag.replace("android.widget.", "").replace("Activity", "")}") 106 | if(instance.nonEmpty){ 107 | fileName.append(s".${instance}") 108 | } 109 | if(depth.nonEmpty){ 110 | fileName.append(s".depth=${depth}") 111 | } 112 | if(id.nonEmpty){ 113 | fileName.append(s".id=${id.split('/').last}") 114 | } 115 | if(name.nonEmpty){ 116 | fileName.append(s".name=${name}") 117 | } 118 | if(text.nonEmpty){ 119 | fileName.append(s".text=${ StringEscapeUtils.unescapeHtml4(text).replace(File.separator, "+")}") 120 | } 121 | 122 | fileName.toString().take(100) 123 | } 124 | 125 | 126 | def hash(s:String)={ 127 | val m = java.security.MessageDigest.getInstance("MD5") 128 | val b = s.getBytes("UTF-8") 129 | m.update(b,0,b.length) 130 | new java.math.BigInteger(1,m.digest()).toString(16) 131 | } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /src/main/scala/com/testerhome/appcrawler/URIElementStore.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler 2 | 3 | import scala.beans.BeanProperty 4 | import scala.collection.mutable 5 | import scala.collection.mutable.ListBuffer 6 | 7 | /** 8 | * Created by seveniruby on 16/9/8. 9 | */ 10 | class URIElementStore { 11 | //todo: 用枚举替代 0表示未遍历 1表示已遍历 -1表示跳过 12 | @BeanProperty 13 | var elementStore = scala.collection.mutable.Map[String, ElementInfo]() 14 | /** 点击顺序, 留作画图用 */ 15 | @BeanProperty 16 | var clickedElementsList = ListBuffer[URIElement]() 17 | 18 | def setElementSkip(element: URIElement): Unit = { 19 | //todo: 待改进 20 | //clickedElementsList.remove(clickedElementsList.size - 1) 21 | if(elementStore.contains(element.toString)==false){ 22 | elementStore(element.toString)=ElementInfo() 23 | elementStore(element.toString).element=element 24 | } 25 | elementStore(element.toString).action=ElementStatus.Skipped 26 | } 27 | 28 | def setElementClicked(element: URIElement): Unit = { 29 | if(elementStore.contains(element.toString)==false){ 30 | elementStore(element.toString)=ElementInfo() 31 | elementStore(element.toString).element=element 32 | } 33 | clickedElementsList.append(element) 34 | elementStore(element.toString).action=ElementStatus.Clicked 35 | elementStore(element.toString).clickedIndex=clickedElementsList.indexOf(element) 36 | } 37 | def setElementClear(element: URIElement=clickedElementsList.last): Unit = { 38 | if(elementStore.contains(element.toString)){ 39 | elementStore.remove(element.toString) 40 | } 41 | } 42 | 43 | 44 | def saveElement(element: URIElement): Unit = { 45 | if(elementStore.contains(element.toString)==false){ 46 | elementStore(element.toString)=ElementInfo() 47 | elementStore(element.toString).element=element 48 | } 49 | if (elementStore.contains(element.toString) == false) { 50 | elementStore(element.toString).action=ElementStatus.Clicked 51 | AppCrawler.log.info(s"first found ${element}") 52 | } 53 | } 54 | 55 | 56 | def saveReqHash(hash: String = ""): Unit = { 57 | val head = clickedElementsList.last.toString 58 | if(elementStore(head).reqHash.isEmpty){ 59 | AppCrawler.log.info(s"save reqHash to ${clickedElementsList.size-1}") 60 | elementStore(head).reqHash=hash 61 | } 62 | } 63 | 64 | def saveResHash(hash: String = ""): Unit = { 65 | val head = clickedElementsList.last.toString 66 | if(elementStore(head).resHash.isEmpty){ 67 | AppCrawler.log.info(s"save resHash to ${clickedElementsList.size-1}") 68 | elementStore(head).resHash=hash 69 | } 70 | } 71 | 72 | 73 | def saveReqDom(dom: String = ""): Unit = { 74 | val head = clickedElementsList.last.toString 75 | if(elementStore(head).reqDom.isEmpty){ 76 | AppCrawler.log.info(s"save reqDom to ${clickedElementsList.size-1}") 77 | elementStore(head).reqDom=dom 78 | } 79 | } 80 | 81 | def saveResDom(dom: String = ""): Unit = { 82 | val head = clickedElementsList.last.toString 83 | if(elementStore(head).resDom.isEmpty){ 84 | AppCrawler.log.info(s"save resDom to ${clickedElementsList.size-1}") 85 | elementStore(head).resDom=dom 86 | } 87 | } 88 | 89 | def saveReqImg(imgName:String): Unit = { 90 | val head = clickedElementsList.last.toString 91 | if (elementStore(head).reqImg.isEmpty) { 92 | AppCrawler.log.info(s"save reqImg ${imgName} to ${clickedElementsList.size - 1}") 93 | elementStore(head.toString).reqImg = imgName 94 | } 95 | } 96 | 97 | 98 | def saveResImg(imgName:String): Unit = { 99 | val head = clickedElementsList.last.toString 100 | if (elementStore(head).resImg.isEmpty) { 101 | AppCrawler.log.info(s"save resImg ${imgName} to ${clickedElementsList.size - 1}") 102 | elementStore(head).resImg = imgName.split('.').dropRight(2).mkString(".")+".clicked.png" 103 | } 104 | } 105 | 106 | def getLastResponseImage(): Unit ={ 107 | 108 | } 109 | 110 | 111 | def isDiff(): Boolean = { 112 | val currentElement = clickedElementsList.last 113 | elementStore(currentElement.toString).reqHash!=elementStore(currentElement.toString).resHash 114 | } 115 | 116 | 117 | def isClicked(ele: URIElement): Boolean = { 118 | if (elementStore.contains(ele.toString)) { 119 | elementStore(ele.toString).action == ElementStatus.Clicked 120 | } else { 121 | AppCrawler.log.trace(s"element=${ele} first show, need click") 122 | false 123 | } 124 | } 125 | 126 | def isSkiped(ele: URIElement): Boolean = { 127 | if (elementStore.contains(ele.toString)) { 128 | elementStore(ele.toString).action == ElementStatus.Skipped 129 | } else { 130 | AppCrawler.log.trace(s"element=${ele} first show, need click") 131 | false 132 | } 133 | } 134 | 135 | 136 | } 137 | 138 | object ElementStatus extends Enumeration { 139 | val Ready, Clicked, Skipped = Value 140 | } 141 | 142 | case class ElementInfo( 143 | var reqDom: String = "", 144 | var resDom: String = "", 145 | var reqHash: String = "", 146 | var resHash: String = "", 147 | var reqImg:String="", 148 | var resImg:String="", 149 | var clickedIndex: Int = -1, 150 | var action: ElementStatus.Value = ElementStatus.Ready, 151 | var element: URIElement = URIElement(url="Init", tag="", id="", name="", xpath="") 152 | ) -------------------------------------------------------------------------------- /src/main/scala/com/testerhome/appcrawler/Util.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler 2 | 3 | import java.io.File 4 | 5 | import com.testerhome.appcrawler.plugin.Plugin 6 | 7 | import scala.reflect.internal.util.ScalaClassLoader.URLClassLoader 8 | import scala.tools.nsc.interpreter.IMain 9 | import scala.tools.nsc.{Global, Settings} 10 | import scala.util.{Failure, Success, Try} 11 | 12 | /** 13 | * Created by seveniruby on 16/8/13. 14 | */ 15 | class Util(val outputDir:String="") extends CommonLog{ 16 | private val settingsCompile=new Settings() 17 | 18 | if(outputDir.nonEmpty){ 19 | val tempDir=new File(outputDir) 20 | if(tempDir.exists()==false){ 21 | tempDir.mkdir() 22 | } 23 | settingsCompile.outputDirs.setSingleOutput(this.outputDir) 24 | } 25 | 26 | settingsCompile.deprecation.value = true // enable detailed deprecation warnings 27 | settingsCompile.unchecked.value = true // enable detailed unchecked warnings 28 | settingsCompile.usejavacp.value = true 29 | 30 | val global = new Global(settingsCompile) 31 | val run = new global.Run 32 | 33 | private val settingsEval=new Settings() 34 | settingsEval.deprecation.value = true // enable detailed deprecation warnings 35 | settingsEval.unchecked.value = true // enable detailed unchecked warnings 36 | settingsEval.usejavacp.value = true 37 | 38 | val interpreter = new IMain(settingsEval) 39 | 40 | def compile(fileNames:List[String]): Unit ={ 41 | run.compile(fileNames) 42 | } 43 | 44 | def eval(code:String)={ 45 | interpreter.interpret(code) 46 | } 47 | def reset(): Unit ={ 48 | 49 | } 50 | 51 | 52 | 53 | } 54 | 55 | object Util extends CommonLog{ 56 | var instance=new Util() 57 | var isLoaded=false 58 | def apply(): Unit ={ 59 | 60 | } 61 | 62 | def dsl(command: String): Unit = { 63 | log.info(s"eval ${command}") 64 | Try(Util.eval(command)) match { 65 | case Success(v) => log.info(v) 66 | case Failure(e) => log.warn(e.getMessage) 67 | } 68 | log.info("eval finish") 69 | //new Eval().inPlace(s"com.testerhome.appcrawler.MiniAppium.${command.trim}") 70 | } 71 | private def eval(code:String): Unit ={ 72 | if(isLoaded==false){ 73 | log.info("first import") 74 | instance.eval("import sys.process._") 75 | instance.eval("val driver=com.testerhome.appcrawler.AppCrawler.crawler.driver") 76 | instance.eval("def crawl(depth:Int)=com.testerhome.appcrawler.AppCrawler.crawler.crawl(depth)") 77 | isLoaded=true 78 | } 79 | log.info(code) 80 | log.info(instance.eval(code)) 81 | log.info("eval finish") 82 | } 83 | 84 | def compile(fileNames:List[String]): Unit ={ 85 | instance.compile(fileNames) 86 | isLoaded=false 87 | } 88 | def init(classDir:String=""): Unit ={ 89 | instance=new Util(classDir) 90 | } 91 | def reset(): Unit ={ 92 | 93 | } 94 | def loadPlugins(pluginDir:String=""): List[Plugin] ={ 95 | val pluginDirFile=new java.io.File(pluginDir) 96 | if(pluginDirFile.exists()==false){ 97 | log.warn(s"no ${pluginDir} directory, skip") 98 | return Nil 99 | } 100 | val pluginFiles=pluginDirFile.list().filter(_.endsWith(".scala")).toList 101 | val pluginClassNames=pluginFiles.map(_.split(".scala").head) 102 | log.info(s"find plugins in ${pluginDir}") 103 | log.info(pluginFiles) 104 | log.info(pluginClassNames) 105 | val runtimes=new Util(pluginDir) 106 | runtimes.compile(pluginFiles.map(pluginDirFile.getCanonicalPath+File.separator+_)) 107 | val urls=Seq(pluginDirFile.toURI.toURL, getClass.getProtectionDomain.getCodeSource.getLocation) 108 | val loader=new URLClassLoader(urls, Thread.currentThread().getContextClassLoader) 109 | pluginClassNames.map(loader.loadClass(_).newInstance().asInstanceOf[Plugin]) 110 | } 111 | } -------------------------------------------------------------------------------- /src/main/scala/com/testerhome/appcrawler/driver/MacacaDriver.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.driver 2 | 3 | import java.awt.{BasicStroke, Color} 4 | import java.io.File 5 | import javax.imageio.ImageIO 6 | 7 | import com.alibaba.fastjson.JSONObject 8 | import com.testerhome.appcrawler.{AppCrawler, CommonLog, DataObject, URIElement} 9 | import com.testerhome.appcrawler._ 10 | import macaca.client.MacacaClient 11 | import org.apache.log4j.Level 12 | import org.openqa.selenium.Rectangle 13 | import org.scalatest.selenium.WebBrowser 14 | 15 | import scala.sys.process._ 16 | 17 | /** 18 | * Created by seveniruby on 16/8/9. 19 | */ 20 | class MacacaDriver extends ReactWebDriver{ 21 | Util.init() 22 | var conf: CrawlerConf = _ 23 | 24 | implicit var driver: MacacaClient = _ 25 | var appiumProcess: Process = null 26 | var currentElement: macaca.client.commands.Element =_ 27 | 28 | def this(url: String = "http://127.0.0.1:4723/wd/hub", configMap: Map[String, Any]=Map[String, Any]()) { 29 | this 30 | appium(url, configMap) 31 | } 32 | 33 | def appium(url: String = "http://127.0.0.1:4723/wd/hub", configMap: Map[String, Any]=Map[String, Any]()): Unit = { 34 | driver=new MacacaClient() 35 | val porps = new JSONObject() 36 | configMap.foreach(m=>porps.put(m._1, m._2)) 37 | porps.put("package", configMap("appPackage")) 38 | porps.put("activity", configMap("appActivity")) 39 | 40 | val desiredCapabilities = new JSONObject() 41 | desiredCapabilities.put("desiredCapabilities", porps) 42 | driver.initDriver(desiredCapabilities) 43 | 44 | getDeviceInfo 45 | } 46 | 47 | 48 | override def stop(): Unit = { 49 | appiumProcess.destroy() 50 | } 51 | 52 | override def hideKeyboard(): Unit = { 53 | //todo: 54 | } 55 | 56 | 57 | /* 58 | override def tap(): this.type = { 59 | click on (XPathQuery(tree(loc, index)("xpath").toString)) 60 | this 61 | } 62 | */ 63 | 64 | override def event(keycode: Int): Unit = { 65 | //todo: 66 | log.error("not implete") 67 | } 68 | 69 | 70 | override def getDeviceInfo(): Unit = { 71 | val size=driver.getWindowSize 72 | log.info(s"size=${size}") 73 | screenHeight = size.get("height").toString.toInt 74 | screenWidth = size.get("width").toString.toInt 75 | log.info(s"screenWidth=${screenWidth} screenHeight=${screenHeight}") 76 | } 77 | 78 | override def swipe(startX: Double = 0.9, endX: Double = 0.1, startY: Double = 0.9, endY: Double = 0.1): Unit = { 79 | //macaca 2.0.20有api变动 80 | asyncTask() { 81 | val json = new JSONObject() 82 | json.put("fromX", (screenWidth * startX).toInt) 83 | json.put("fromY", (screenHeight * startY).toInt) 84 | json.put("toX", (screenWidth * endX).toInt) 85 | json.put("toY", (screenHeight * endY).toInt) 86 | json.put("duration", 2) 87 | driver.touch("drag", json) 88 | } 89 | 90 | } 91 | 92 | 93 | override def screenshot(): File = { 94 | val location="/tmp/1.png" 95 | driver.saveScreenshot(location) 96 | new File(location) 97 | } 98 | 99 | //todo: 重构到独立的trait中 100 | override def mark(fileName: String, newImageName:String, x: Int, y: Int, w: Int, h: Int): Unit = { 101 | val file = new java.io.File(fileName) 102 | log.info(s"platformName=${platformName}") 103 | log.info("getScreenshot") 104 | val img = ImageIO.read(file) 105 | val graph = img.createGraphics() 106 | 107 | if (platformName.toLowerCase == "ios") { 108 | log.info("scale the origin image") 109 | graph.drawImage(img, 0, 0, screenWidth, screenHeight, null) 110 | } 111 | graph.setStroke(new BasicStroke(5)) 112 | graph.setColor(Color.RED) 113 | graph.drawRect(x, y, w, h) 114 | graph.dispose() 115 | 116 | log.info(s"write png ${fileName}") 117 | if (platformName.toLowerCase == "ios") { 118 | val subImg=img.getSubimage(0, 0, screenWidth, screenHeight) 119 | ImageIO.write(subImg, "png", new java.io.File(newImageName)) 120 | } else { 121 | ImageIO.write(img, "png", new java.io.File(newImageName)) 122 | } 123 | } 124 | 125 | 126 | /* 127 | def tap(x: Int = screenWidth / 2, y: Int = screenHeight / 2): Unit = { 128 | log.info("tap") 129 | driver.tap(1, x, y, 100) 130 | //driver.findElementByXPath("//UIAWindow[@path='/0/2']").click() 131 | //new TouchAction(driver).tap(x, y).perform() 132 | }*/ 133 | 134 | //todo: 用真正的tap替代 135 | override def tap(): this.type = { 136 | currentElement.click() 137 | this 138 | } 139 | override def click(): this.type = { 140 | currentElement.click() 141 | this 142 | } 143 | 144 | override def longTap(): this.type = { 145 | currentElement.click() 146 | this 147 | } 148 | 149 | override def back(): Unit = { 150 | log.info("navigate back") 151 | driver.back() 152 | } 153 | 154 | override def backApp(): Unit = { 155 | /* 156 | sleep(10) 157 | event(AndroidKeyCode.BACK) 158 | sleep(2) 159 | event(AndroidKeyCode.ENTER) 160 | */ 161 | back() 162 | } 163 | 164 | override def getPageSource(): String = { 165 | driver.source() 166 | } 167 | 168 | override def findElementsByURI(element: URIElement, findBy:String): List[AnyRef] = { 169 | //todo: 改进macaca定位 170 | val s=driver.elementsByXPath(element.xpath) 171 | 0 until s.size() map(s.getIndex(_)) toList 172 | } 173 | 174 | override def findElementByURI(element: URIElement, findBy:String): AnyRef = { 175 | currentElement=super.findElementByURI(element, findBy).asInstanceOf[macaca.client.commands.Element] 176 | currentElement 177 | } 178 | 179 | override def getAppName(): String = { 180 | val xpath="(//*[@package!=''])[1]" 181 | getNodeListByKey(xpath).head.getOrElse("package", "").toString 182 | } 183 | 184 | override def getUrl(): String = { 185 | //todo: macaca的url没设定 186 | //driver.title() 187 | "" 188 | } 189 | 190 | override def getRect(): Rectangle ={ 191 | val rect=currentElement.getRect.asInstanceOf[JSONObject] 192 | new Rectangle(rect.getIntValue("x"), rect.getIntValue("y"), rect.getIntValue("height"), rect.getIntValue("width")) 193 | } 194 | 195 | 196 | override def sendKeys(content: String): Unit = { 197 | currentElement.sendKeys(content) 198 | } 199 | 200 | 201 | 202 | } 203 | 204 | -------------------------------------------------------------------------------- /src/main/scala/com/testerhome/appcrawler/plugin/AndroidTrace.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.plugin 2 | 3 | import com.sun.jdi.event._ 4 | import com.sun.jdi.request.{EventRequest, EventRequestManager} 5 | import com.sun.jdi.{Bootstrap, ReferenceType, VirtualMachine} 6 | import com.sun.tools.jdi.SocketAttachingConnector 7 | import com.testerhome.appcrawler.CommonLog 8 | import com.testerhome.appcrawler.CommonLog 9 | 10 | import collection.JavaConversions._ 11 | 12 | /** 13 | * Created by seveniruby on 16/4/24. 14 | */ 15 | 16 | 17 | class AndroidTrace extends CommonLog { 18 | var eventRequestManager: EventRequestManager = _ 19 | var vm: VirtualMachine = _ 20 | 21 | def getConnections(): Unit = { 22 | val sac = Bootstrap.virtualMachineManager().attachingConnectors().toArray.filter(_.isInstanceOf[SocketAttachingConnector]).head.asInstanceOf[SocketAttachingConnector] 23 | log.info(sac) 24 | log.info(sac.defaultArguments()) 25 | val arguments = sac.defaultArguments() 26 | arguments.get("hostname").setValue("127.0.0.1") 27 | arguments.get("port").setValue("8000") 28 | log.info(arguments) 29 | vm = sac.attach(arguments) 30 | val process = vm.process() 31 | log.info(process) 32 | vm.allClasses().map(_.name().split('.').take(4).mkString(".")).distinct.foreach(log.info) 33 | vm.setDebugTraceMode(VirtualMachine.TRACE_EVENTS) 34 | registEvent() 35 | eventLoop() 36 | 37 | } 38 | 39 | def eventLoop(): Unit = { 40 | val queue = vm.eventQueue() 41 | 42 | //vm.resume() 43 | 44 | var eventIndex = 0 45 | while (true) { 46 | val eventSet = queue.remove() 47 | eventSet.foreach(e => { 48 | eventIndex += 1 49 | e match { 50 | case e: VMStartEvent => { 51 | log.info(e) 52 | } 53 | case e: BreakpointEvent => { 54 | log.info("breakpoint catch") 55 | log.info(e.thread().status()) 56 | val frame = e.thread().frame(0) 57 | log.info("frames") 58 | log.info(e.thread().frames().map(f=>s"${f.location().declaringType().name()}:${f.location().lineNumber()}:${f.location().method().name()}").mkString("\n")) 59 | log.info("arguments") 60 | frame.getArgumentValues.foreach(av=>log.info(av)) 61 | log.info("value") 62 | frame.visibleVariables().foreach(v => log.info(s"${v}=${frame.getValue(v)}")) 63 | } 64 | /* 65 | case e: MethodEntryEvent => { 66 | log.info("method show") 67 | log.info(e.thread().status()) 68 | if(e.thread().status()!=Thread.State.TIMED_WAITING && e.thread().status()!=Thread.State.TIMED_WAITING) { 69 | 70 | //log.info(e.method().allLineLocations()) 71 | //log.info(e.method().arguments()) 72 | //log.info(e.method().variables()) 73 | log.info(e.thread().frames().map(_.location())) 74 | //log.info(e.method().arguments()) 75 | log.info(e.toString) 76 | //log.info(e.method().arguments()) 77 | } 78 | }*/ 79 | case e: ClassPrepareEvent => { 80 | log.info(s"class prepare ${e.referenceType().getClass}") 81 | } 82 | case e: VMDeathEvent => { 83 | log.info("quit") 84 | System.exit(0) 85 | } 86 | case _ => { 87 | } 88 | } 89 | }) 90 | 91 | eventSet.resume() 92 | 93 | } 94 | 95 | } 96 | 97 | def registEvent(): Unit = { 98 | val suspend = EventRequest.SUSPEND_EVENT_THREAD 99 | eventRequestManager = vm.eventRequestManager() 100 | /* 101 | val methodEntry = eventRequestManager.createMethodEntryRequest() 102 | methodEntry.setSuspendPolicy(suspend) 103 | methodEntry.addClassFilter("org.json.JSONObject") 104 | methodEntry.enable() 105 | */ 106 | 107 | val breakpoints=List( 108 | "com.google.gson.JsonObject:get", 109 | "com.google.gson.JsonObject:getAs.*", 110 | "com.google.gson.JsonObject:get.*", 111 | ".*json.*:isJsonNull", 112 | ".*gson.*:isJsonNull", 113 | "android.view.View:onClick", 114 | ".*TextView.*:onClick", 115 | "android.*:onClick" 116 | ) 117 | val blackList=List("getAsJsonPrimitive", ".*Class.*", "getAsJsonObject") 118 | breakpoints.foreach(b=>{ 119 | val classMatcher=b.split(":").head 120 | val methodMatcher=b.split(":").last 121 | log.info(s"${classMatcher} ${methodMatcher}") 122 | vm.allClasses().filter(_.name().matches(classMatcher)) 123 | .flatMap(_.allMethods()).filter(m=>m.name().matches(methodMatcher) && blackList.map(m.name().matches(_)).contains(true)==false) 124 | .flatMap(_.allLineLocations()) 125 | .distinct.foreach(location => { 126 | log.info(s"set break on ${location.method().name()} ${location}") 127 | val bp = eventRequestManager.createBreakpointRequest(location) 128 | bp.enable() 129 | }) 130 | }) 131 | log.info("all breakpoints enable") 132 | /* 133 | val methodExit = eventRequestManager.createMethodExitRequest() 134 | methodExit.setSuspendPolicy(suspend) 135 | methodExit.enable() 136 | 137 | val classPrepare = eventRequestManager.createClassPrepareRequest() 138 | classPrepare.setSuspendPolicy(suspend) 139 | classPrepare.enable() 140 | */ 141 | 142 | val tsr = eventRequestManager.createThreadStartRequest(); 143 | tsr.setSuspendPolicy(suspend); 144 | tsr.enable(); 145 | // 注册线程结束事件 146 | val tdr = eventRequestManager.createThreadDeathRequest(); 147 | tdr.setSuspendPolicy(suspend); 148 | tdr.enable() 149 | } 150 | 151 | } 152 | -------------------------------------------------------------------------------- /src/main/scala/com/testerhome/appcrawler/plugin/DemoPlugin.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.plugin 2 | 3 | import com.testerhome.appcrawler.URIElement 4 | 5 | /** 6 | * Created by seveniruby on 16/1/21. 7 | */ 8 | class DemoPlugin extends Plugin{ 9 | override def beforeElementAction(element: URIElement): Unit ={ 10 | log.info("demo com.testerhome.appcrawler.plugin before element action") 11 | log.info(element) 12 | log.info("demo com.testerhome.appcrawler.plugin end") 13 | } 14 | override def afterUrlRefresh(url:String): Unit ={ 15 | getCrawler().currentUrl=url.split('|').last 16 | log.info(s"new url=${getCrawler().currentUrl}") 17 | if(getCrawler().currentUrl.contains("Browser")){ 18 | getCrawler().getBackButton() 19 | } 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/scala/com/testerhome/appcrawler/plugin/FlowDiff.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.plugin 2 | 3 | import java.io 4 | 5 | import com.testerhome.appcrawler.URIElement 6 | import com.testerhome.appcrawler.DataObject 7 | import org.apache.commons.io.FileUtils 8 | 9 | import scala.reflect.io.File 10 | 11 | /** 12 | * Created by seveniruby on 16/9/25. 13 | */ 14 | class FlowDiff extends Plugin{ 15 | override def start(): Unit ={ 16 | } 17 | 18 | override def afterElementAction(element: URIElement): Unit ={ 19 | //getCrawler().store.saveResDom(getCrawler().driver.currentPageSource) 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/scala/com/testerhome/appcrawler/plugin/FreeMind.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.plugin 2 | 3 | import com.testerhome.appcrawler.TreeNode 4 | 5 | import scala.collection.mutable.ListBuffer 6 | import scala.reflect.io.File 7 | 8 | /** 9 | * Created by seveniruby on 16/9/19. 10 | */ 11 | class FreeMind extends Plugin{ 12 | 13 | private val elementTree = TreeNode[String]("AppCrawler") 14 | private val elementTreeList = ListBuffer[String]() 15 | 16 | override def stop(): Unit ={ 17 | log.info(s"genereate freemind file freemind.mm") 18 | report() 19 | } 20 | 21 | def report(): Unit ={ 22 | getCrawler().store.clickedElementsList.foreach(element=>{ 23 | elementTreeList.append(element.url) 24 | elementTreeList.append(element.xpath) 25 | }) 26 | 27 | File(s"${getCrawler().conf.resultDir}/freemind.mm").writeAll( 28 | elementTree.generateFreeMind(elementTreeList) 29 | ) 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/scala/com/testerhome/appcrawler/plugin/IDeviceScreenshot.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.plugin 2 | 3 | import sys.process._ 4 | /** 5 | * Created by seveniruby on 16/4/26. 6 | */ 7 | class IDeviceScreenshot extends Plugin{ 8 | 9 | var use=false 10 | override def start(): Unit ={ 11 | getCrawler().conf.capability("udid") match { 12 | case null=> { 13 | use=false 14 | log.info("udid=null use simulator") 15 | } 16 | case ""=>{ 17 | use=false 18 | log.info("udid= use simulator") 19 | } 20 | case _ =>{ 21 | use=true 22 | log.info("use idevicescreenshot") 23 | } 24 | } 25 | } 26 | override def screenshot(path:String): Boolean ={ 27 | //非真机不使用 28 | if(use==false) return false 29 | val cmd=s"idevicescreenshot ${path}" 30 | log.info(s"cmd=${cmd}") 31 | cmd.! 32 | true 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/scala/com/testerhome/appcrawler/plugin/LogPlugin.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.plugin 2 | 3 | import java.util.logging.Level 4 | 5 | import com.testerhome.appcrawler.driver.AppiumClient 6 | import com.testerhome.appcrawler.URIElement 7 | 8 | import scala.collection.mutable.ListBuffer 9 | import scala.reflect.io.File 10 | 11 | /** 12 | * Created by seveniruby on 16/1/21. 13 | * 14 | * 如果某种类型的控件点击次数太多, 就跳过. 设定一个阈值 15 | */ 16 | class LogPlugin extends Plugin { 17 | private var logs = ListBuffer[String]() 18 | val driver = getCrawler().driver.asInstanceOf[AppiumClient].driver 19 | 20 | override def afterElementAction(element: URIElement): Unit = { 21 | //第一次先试验可用的log 后续就可以跳过从而加速 22 | if (logs.isEmpty) { 23 | driver.manage().logs().getAvailableLogTypes.toArray().foreach(logName => { 24 | log.info(s"read log=${logName.toString}") 25 | try { 26 | saveLog(logName.toString) 27 | logs += logName.toString 28 | } catch { 29 | case ex: Exception => log.warn(s"log=${logName.toString} not exist") 30 | } 31 | }) 32 | } 33 | if(getCrawler().getElementAction()!="skip") { 34 | logs.foreach(log => { 35 | saveLog(log) 36 | }) 37 | } 38 | } 39 | 40 | def saveLog(logName:String): Unit ={ 41 | log.info(s"read log=${logName.toString}") 42 | val logMessage = driver.manage().logs.get(logName.toString).filter(Level.ALL).toArray() 43 | log.info(s"log=${logName} size=${logMessage.size}") 44 | if (logMessage.size > 0) { 45 | val fileName = getCrawler().getBasePathName()+".log" 46 | log.info(s"save ${logName} to $fileName") 47 | File(fileName).writeAll(logMessage.mkString("\n")) 48 | log.info(s"save ${logName} end") 49 | } 50 | } 51 | 52 | 53 | override def afterUrlRefresh(url: String): Unit = { 54 | 55 | } 56 | override def stop(): Unit ={ 57 | logs.foreach(log => { 58 | saveLog(log) 59 | }) 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/main/scala/com/testerhome/appcrawler/plugin/Plugin.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.plugin 2 | 3 | import com.testerhome.appcrawler.{CommonLog, Crawler, URIElement} 4 | 5 | /** 6 | * Created by seveniruby on 16/1/7. 7 | */ 8 | abstract class Plugin extends CommonLog{ 9 | private var crawler: Crawler=_ 10 | def getCrawler(): Crawler ={ 11 | this.crawler 12 | } 13 | def setCrawer(crawler:Crawler): Unit ={ 14 | this.crawler=crawler 15 | } 16 | def init(crawler: Crawler): Unit ={ 17 | this.crawler=crawler 18 | log.addAppender(crawler.fileAppender) 19 | log.info(this.getClass.getName+" init") 20 | } 21 | def start(): Unit ={ 22 | 23 | } 24 | def afterUrlRefresh(url:String): Unit ={ 25 | 26 | } 27 | 28 | def beforeBack(): Unit ={ 29 | 30 | } 31 | def beforeElementAction(element: URIElement): Unit ={ 32 | 33 | } 34 | def afterElementAction(element: URIElement): Unit ={ 35 | 36 | } 37 | 38 | /** 39 | * 如果实现了请设置返回值为true 40 | * @param path 41 | * @return 42 | */ 43 | def screenshot(path:String): Boolean ={ 44 | false 45 | } 46 | 47 | def stop(): Unit ={ 48 | 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/scala/com/testerhome/appcrawler/plugin/ProxyPlugin.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.plugin 2 | 3 | import java.io.File 4 | 5 | import com.brsanthu.googleanalytics.GoogleAnalytics 6 | import com.testerhome.appcrawler.URIElement 7 | import net.lightbody.bmp.BrowserMobProxyServer 8 | import net.lightbody.bmp.proxy.CaptureType 9 | import org.apache.log4j.{BasicConfigurator, Level, Logger} 10 | 11 | import scala.util.Try 12 | 13 | /** 14 | * Created by seveniruby on 16/4/26. 15 | */ 16 | class ProxyPlugin extends Plugin { 17 | private var proxy: BrowserMobProxyServer = _ 18 | val port = 7777 19 | 20 | //todo: 支持代理 21 | override def start(): Unit = { 22 | BasicConfigurator.configure() 23 | Logger.getRootLogger.setLevel(Level.INFO) 24 | Logger.getLogger("ProxyServer").setLevel(Level.WARN) 25 | 26 | proxy = new BrowserMobProxyServer() 27 | proxy.setHarCaptureTypes(CaptureType.getNonBinaryContentCaptureTypes) 28 | proxy.setTrustAllServers(true) 29 | proxy.start(port) 30 | 31 | //proxy.setHarCaptureTypes(CaptureType.getAllContentCaptureTypes) 32 | //proxy.setHarCaptureTypes(CaptureType.getHeaderCaptureTypes) 33 | log.info(s"proxy server listen on ${port}") 34 | proxy.newHar("start") 35 | } 36 | 37 | override def beforeElementAction(element: URIElement): Unit = { 38 | log.info("clear har") 39 | proxy.endHar() 40 | //创建新的har 41 | val harFileName = getCrawler().getBasePathName() + ".har" 42 | proxy.newHar(harFileName) 43 | } 44 | 45 | override def afterElementAction(element: URIElement): Unit = { 46 | log.info("save har") 47 | val harFileName = getCrawler().getBasePathName() + ".har" 48 | val file = new File(harFileName) 49 | try { 50 | log.info(proxy.getHar) 51 | log.info(proxy.getHar.getLog) 52 | log.info(proxy.getHar.getLog.getEntries.size()) 53 | log.info(s"har entry size = ${proxy.getHar.getLog.getEntries.size()}") 54 | if (proxy.getHar.getLog.getEntries.size() > 0) { 55 | proxy.getHar.writeTo(file) 56 | } 57 | } catch { 58 | case e: Exception =>{ 59 | log.error("read har error") 60 | log.error(e.getCause) 61 | log.error(e.getMessage) 62 | e.getStackTrace.foreach(log.error) 63 | } 64 | } 65 | 66 | } 67 | 68 | override def stop(): Unit ={ 69 | log.info("prpxy stop") 70 | proxy.stop() 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/scala/com/testerhome/appcrawler/plugin/ReportPlugin.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.plugin 2 | 3 | import java.io 4 | 5 | import com.testerhome.appcrawler.{Report, URIElement} 6 | import com.testerhome.appcrawler._ 7 | import org.scalatest.FunSuite 8 | import org.scalatest.tools.Runner 9 | import sun.misc.{Signal, SignalHandler} 10 | 11 | import scala.collection.mutable.ListBuffer 12 | import scala.reflect.io.File 13 | 14 | /** 15 | * Created by seveniruby on 16/8/12. 16 | */ 17 | class ReportPlugin extends Plugin with Report { 18 | var lastSize=0 19 | override def start(): Unit ={ 20 | reportPath=new java.io.File(getCrawler().conf.resultDir).getCanonicalPath 21 | log.info(s"reportPath=${reportPath}") 22 | val tmpDir=new io.File(s"${reportPath}/tmp/") 23 | if(tmpDir.exists()==false){ 24 | log.info(s"create ${reportPath}/tmp/ directory") 25 | tmpDir.mkdir() 26 | } 27 | } 28 | 29 | override def stop(): Unit ={ 30 | this.getCrawler().saveLog() 31 | generateReport() 32 | } 33 | 34 | override def afterElementAction(element: URIElement): Unit ={ 35 | val count=getCrawler().store.clickedElementsList.length 36 | log.info(s"clickedElementsList size = ${count}") 37 | val curSize=getCrawler().store.clickedElementsList.size 38 | if(curSize-lastSize > curSize/10+20 ){ 39 | log.info(s"${curSize}-${lastSize} > ${curSize}/10+20 ") 40 | log.info("generate test report ") 41 | generateReport() 42 | } 43 | } 44 | 45 | def generateReport(): Unit ={ 46 | Report.saveTestCase(getCrawler().store, getCrawler().conf.resultDir) 47 | Report.store=getCrawler().store 48 | Report.runTestCase() 49 | 50 | lastSize=getCrawler().store.clickedElementsList.size 51 | } 52 | 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/main/scala/com/testerhome/appcrawler/plugin/TagLimitPlugin.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.plugin 2 | 3 | import com.testerhome.appcrawler.URIElement 4 | import com.testerhome.appcrawler.ElementStatus 5 | 6 | /** 7 | * Created by seveniruby on 16/1/21. 8 | * 9 | * 如果某种类型的控件点击次数太多, 就跳过. 设定一个阈值 10 | */ 11 | class TagLimitPlugin extends Plugin { 12 | private val tagLimit = scala.collection.mutable.Map[String, Int]() 13 | private var tagLimitMax = 3 14 | 15 | override def start(): Unit = { 16 | log.addAppender(getCrawler().fileAppender) 17 | tagLimitMax = getCrawler().conf.tagLimitMax 18 | } 19 | 20 | //todo: conf.tagLimit未生效 21 | override def beforeElementAction(element: URIElement): Unit = { 22 | val key =getKey(element) 23 | if (!tagLimit.contains(key)) { 24 | //跳过具备selected=true的菜单栏 25 | getCrawler().driver.getNodeListByKey("//*[@selected='true']").foreach(m=>{ 26 | val selectedElement=getCrawler().getUrlElementByMap(m) 27 | val selectedKey=getKey(selectedElement) 28 | tagLimit(selectedKey)=20 29 | log.info(s"tagLimit[${selectedKey}]=20") 30 | }) 31 | //应用定制化的规则 32 | getTimesFromTagLimit(element) match { 33 | case Some(v)=> { 34 | tagLimit(key)=v 35 | log.info(s"tagLimit[${key}]=${tagLimit(key)} with conf.tagLimit") 36 | } 37 | case None => tagLimit(key)=tagLimitMax 38 | } 39 | } 40 | 41 | //如果达到限制次数就退出 42 | if (key.nonEmpty && tagLimit(key) <= 0) { 43 | log.warn(s"tagLimit[${key}]=${tagLimit(key)}") 44 | getCrawler().setElementAction("skip") 45 | log.info(s"$element need skip") 46 | } 47 | } 48 | 49 | override def afterElementAction(element: URIElement): Unit = { 50 | if(getCrawler().getElementAction()!="clear") { 51 | val key = getKey(element) 52 | if (tagLimit.contains(key)) { 53 | tagLimit(key) -= 1 54 | log.info(s"tagLimit[${key}]=${tagLimit(key)}") 55 | } 56 | } 57 | } 58 | 59 | def getKey(element: URIElement): String ={ 60 | getCrawler().currentUrl + element.getAncestor() 61 | } 62 | 63 | 64 | override def afterUrlRefresh(url: String): Unit = { 65 | 66 | } 67 | 68 | def getTimesFromTagLimit(element: URIElement): Option[Int] = { 69 | this.getCrawler().conf.tagLimit.foreach(tag => { 70 | if(getCrawler().driver.getNodeListByKey(tag.getXPath()) 71 | .map(new URIElement(_, getCrawler().currentUrl)) 72 | .contains(element)){ 73 | return Some(tag.times) 74 | }else{ 75 | None 76 | } 77 | }) 78 | None 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/test/java/PageFactoryDemo.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by seveniruby on 2017/1/10. 3 | */ 4 | 5 | 6 | import io.appium.java_client.pagefactory.*; 7 | import org.openqa.selenium.remote.RemoteWebDriver; 8 | import org.openqa.selenium.support.PageFactory; 9 | 10 | import java.net.URL; 11 | import java.util.concurrent.TimeUnit; 12 | 13 | 14 | public class PageFactoryDemo { 15 | public static void main(){ 16 | PageObjectDemo pageObject=new PageObjectDemo(); 17 | RemoteWebDriver driver=new RemoteWebDriver(null); 18 | // PageFactory.initElements(new AppiumFieldDecorator(driver, 19 | /*searchContext is a WebDriver or WebElement 20 | instance */ 21 | // new TimeOutDuration(15, //default implicit waiting timeout for all strategies 22 | // TimeUnit.SECONDS)), 23 | // pageObject //an instance of PageObject.class 24 | // ); 25 | 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/test/java/PageObjectDemo.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by seveniruby on 2017/1/10. 3 | */ 4 | 5 | import org.openqa.selenium.remote.RemoteWebElement; 6 | import io.appium.java_client.pagefactory.*; 7 | import org.openqa.selenium.support.FindBy; 8 | import org.openqa.selenium.support.FindAll; 9 | 10 | import io.appium.java_client.android.AndroidElement; 11 | import org.openqa.selenium.remote.RemoteWebElement; 12 | import io.appium.java_client.pagefactory.*; 13 | import io.appium.java_client.ios.IOSElement; 14 | 15 | import java.util.List; 16 | 17 | 18 | public class PageObjectDemo { 19 | @FindBy(xpath = "") 20 | private List titles; 21 | 22 | //convinient locator 23 | private List scores; 24 | 25 | //convinient locator 26 | private List castings; 27 | //element declaration goes on 28 | } 29 | -------------------------------------------------------------------------------- /src/test/java/XueqiuDemo.java: -------------------------------------------------------------------------------- 1 | import io.appium.java_client.MobileElement; 2 | import io.appium.java_client.android.AndroidDriver; 3 | import junit.framework.TestCase; 4 | import org.junit.After; 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | import static org.junit.Assert.*; 8 | import java.net.MalformedURLException; 9 | import java.net.URL; 10 | import java.util.concurrent.TimeUnit; 11 | 12 | import org.openqa.selenium.By; 13 | import org.openqa.selenium.remote.DesiredCapabilities; 14 | 15 | public class XueqiuDemo { 16 | 17 | private AndroidDriver driver; 18 | 19 | @Before 20 | public void setUp() throws MalformedURLException { 21 | DesiredCapabilities desiredCapabilities = new DesiredCapabilities(); 22 | desiredCapabilities.setCapability("appPackage", "com.xueqiu.android"); 23 | desiredCapabilities.setCapability("appActivity", ".view.WelcomeActivityAlias"); 24 | desiredCapabilities.setCapability("platformName", "android"); 25 | desiredCapabilities.setCapability("deviceName", "ddd"); 26 | 27 | URL remoteUrl = new URL("http://localhost:4723/wd/hub"); 28 | 29 | driver = new AndroidDriver(remoteUrl, desiredCapabilities); 30 | } 31 | 32 | @Test 33 | public void sampleTest() throws InterruptedException { 34 | //Thread.sleep(12000); 35 | driver.manage().timeouts().implicitlyWait(60, TimeUnit.SECONDS); 36 | MobileElement el1 = (MobileElement) driver.findElementById("com.xueqiu.android:id/login_account"); 37 | el1.sendKeys("15600534760"); 38 | assertTrue(el1.isDisplayed()); 39 | } 40 | 41 | @After 42 | public void tearDown() { 43 | driver.quit(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/scala/com/testerhome/appcrawler/it/AppiumService.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.it 2 | 3 | import io.appium.java_client.service.local.{AppiumDriverLocalService, AppiumServiceBuilder} 4 | import org.junit.runner.RunWith 5 | import org.scalatest.FunSuite 6 | import org.scalatest.junit.JUnitRunner 7 | 8 | @RunWith(classOf[JUnitRunner]) 9 | class TestAppiumService extends FunSuite{ 10 | 11 | test("start appium service") { 12 | 13 | // System.setProperty("APPIUM_BINARY_PATH","node_modules/appium/lib/main.js") 14 | System.setProperty("JAVA_HOME","/Library/Java/JavaVirtualMachines/adoptopenjdk-11.jdk/Contents/Home") 15 | val builder = new AppiumServiceBuilder 16 | val service = AppiumDriverLocalService.buildService(builder) 17 | 18 | service.start() 19 | 20 | println(service.isRunning) 21 | 22 | service.stop() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/test/scala/com/testerhome/appcrawler/it/TestAndroidTrace.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.it 2 | 3 | import com.testerhome.appcrawler.plugin.AndroidTrace 4 | import org.scalatest.FunSuite 5 | 6 | /** 7 | * Created by seveniruby on 16/4/24. 8 | */ 9 | class TestAndroidTrace extends FunSuite{ 10 | test("list"){ 11 | new AndroidTrace().getConnections() 12 | 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/test/scala/com/testerhome/appcrawler/it/TestAppium.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.it 2 | 3 | import java.net.URL 4 | import java.util.concurrent.TimeUnit 5 | 6 | import com.testerhome.appcrawler.driver.AppiumClient 7 | import io.appium.java_client.android.AndroidDriver 8 | import io.appium.java_client.remote.{AndroidMobileCapabilityType, MobileCapabilityType} 9 | import org.openqa.selenium.WebElement 10 | import org.openqa.selenium.remote.DesiredCapabilities 11 | import org.scalatest.FunSuite 12 | 13 | import scala.io.Source 14 | 15 | /** 16 | * Created by seveniruby on 16/9/24. 17 | */ 18 | class TestAppium extends FunSuite{ 19 | val a=new AppiumClient() 20 | test("appium success"){ 21 | a.start() 22 | println(Source.fromURL("http://127.0.0.1:4723/wd/hub/sessions").mkString) 23 | a.stop() 24 | } 25 | 26 | test("single session"){ 27 | val capa=new DesiredCapabilities() 28 | capa.setCapability(AndroidMobileCapabilityType.APP_PACKAGE, "com.xueqiu.android") 29 | capa.setCapability(AndroidMobileCapabilityType.APP_ACTIVITY, ".view.WelcomeActivityAlias") 30 | capa.setCapability(MobileCapabilityType.DEVICE_NAME, "demo") 31 | val driver=new AndroidDriver[WebElement](new URL("http://127.0.0.1:4723/wd/hub/"), capa) 32 | } 33 | 34 | test("test get window size"){ 35 | //System.setProperty("webdriver.http.factory", "apache") 36 | val capa=new DesiredCapabilities() 37 | capa.setCapability(AndroidMobileCapabilityType.APP_PACKAGE, "com.xueqiu.android") 38 | capa.setCapability(AndroidMobileCapabilityType.APP_ACTIVITY, ".view.WelcomeActivityAlias") 39 | capa.setCapability(MobileCapabilityType.DEVICE_NAME, "demo") 40 | val driver=new AndroidDriver[WebElement](new URL("http://127.0.0.1:5723/wd/hub/"), capa) 41 | Thread.sleep(10000) 42 | println(driver.manage().window().getSize) 43 | driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS) 44 | println(driver.getPageSource) 45 | } 46 | 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/test/scala/com/testerhome/appcrawler/it/TestIOS.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.it 2 | 3 | import scala.collection.JavaConversions._ 4 | import java.net.URL 5 | import java.time.Duration 6 | 7 | import com.testerhome.appcrawler.AppCrawler 8 | import io.appium.java_client.ios.{IOSDriver, IOSElement} 9 | import org.openqa.selenium.remote.{DesiredCapabilities, RemoteWebDriver} 10 | import org.scalatest.FunSuite 11 | 12 | /** 13 | * Created by seveniruby on 2017/5/12. 14 | */ 15 | class TestIOS extends FunSuite{ 16 | 17 | //val app = "/Users/seveniruby/projects/ios-uicatalog/build/Debug-iphonesimulator/UICatalog.app" 18 | val app="/Users/seveniruby/Library/Developer/Xcode/DerivedData/UICatalog-ftyzdbgapjmxxobezrnrxsshpdqh/Build/Products/Debug-iphonesimulator/UICatalog.app" 19 | val bundleID="com.testerhome.ios" 20 | test("ios测试"){ 21 | val capability=new DesiredCapabilities() 22 | capability.setCapability("app", app) 23 | //capability.setCapability("bundleId", "com.example.apple-samplecode.UICatalog") 24 | capability.setCapability("bundleId", bundleID ) 25 | //capability.setCapability("appPackage", "com.example.apple-samplecode.UICatalog") 26 | //capability.setCapability("appActivity", "com.example.apple-samplecode.UICatalog") 27 | 28 | capability.setCapability("fullReset", "false") 29 | capability.setCapability("noReset", "true") 30 | //capability.setCapability("udid", "4F05E384-FE32-43DE-8539-4DC3E2EBC117") 31 | capability.setCapability("automationName", "XCUITest") 32 | capability.setCapability("platformName", "ios") 33 | capability.setCapability("platformVersion", "10.2") 34 | capability.setCapability("deviceName", "iPhone 7") 35 | capability.setCapability("autoAcceptAlerts", true) 36 | 37 | 38 | //val url="http://192.168.100.65:7771" 39 | //val url="http://127.0.0.1:8100" 40 | val url="http://127.0.0.1:4723/wd/hub" 41 | val driver=new RemoteWebDriver(new URL(url), capability) 42 | println(driver.getPageSource) 43 | driver.findElementsByXPath("//*[@label='OK']") match { 44 | case array if array.size()>0 => array.head.click() 45 | case _ => println("no OK alert") 46 | } 47 | driver.findElementsByXPath("//*").foreach(x=>{ 48 | println(x) 49 | println(x.getText) 50 | }) 51 | 52 | } 53 | 54 | test("ios测试 IOSDriver"){ 55 | val capability=new DesiredCapabilities() 56 | capability.setCapability("app", app) 57 | capability.setCapability("bundleId", bundleID) 58 | //capability.setCapability("appPackage", "com.example.apple-samplecode.UICatalog") 59 | //capability.setCapability("appActivity", "com.example.apple-samplecode.UICatalog") 60 | 61 | capability.setCapability("fullReset", "false") 62 | capability.setCapability("noReset", "true") 63 | capability.setCapability("automationName", "XCUITest") 64 | capability.setCapability("platformName", "ios") 65 | capability.setCapability("platformVersion", "11.2") 66 | capability.setCapability("deviceName", "iPhone 8") 67 | capability.setCapability("autoAcceptAlerts", true) 68 | 69 | val url="http://127.0.0.1:4723/wd/hub" 70 | val driver=new IOSDriver[IOSElement](new URL(url), capability) 71 | println(driver.getPageSource) 72 | driver.findElementById("Buttons").click() 73 | } 74 | 75 | test("android"){ 76 | val capability=new DesiredCapabilities() 77 | capability.setCapability("app", "") 78 | capability.setCapability("appPackage", "com.gotokeep.keep") 79 | capability.setCapability("appActivity", ".activity.SplashActivity") 80 | //capability.setCapability("appWaitActivity", "MainActivity") 81 | 82 | capability.setCapability("fullReset", "false") 83 | capability.setCapability("noReset", "true") 84 | //capability.setCapability("udid", "4F05E384-FE32-43DE-8539-4DC3E2EBC117") 85 | //capability.setCapability("automationName", "uiautomator") 86 | capability.setCapability("automationName", "appium") 87 | capability.setCapability("platformName", "android") 88 | capability.setCapability("platformVersion", "") 89 | capability.setCapability("deviceName", "dddd") 90 | //capability.setCapability("autoAcceptAlerts", true) 91 | 92 | 93 | //val url="http://192.168.100.65:7771" 94 | //val url="http://127.0.0.1:8100" 95 | val url="http://127.0.0.1:4723/wd/hub" 96 | val driver=new RemoteWebDriver(new URL(url), capability) 97 | Thread.sleep(3000) 98 | 99 | driver.findElementsByXPath("//*").foreach(x=>{ 100 | println(x.getText) 101 | }) 102 | 103 | driver.findElementByXPath("//*[@text='跑步']").click() 104 | 105 | 106 | } 107 | 108 | test("appcrawler ios"){ 109 | AppCrawler.main(Array( 110 | "-c", "src/test/scala/com/testerhome/appcrawler/it/xueqiu_private.yml", 111 | "-a", app, 112 | "-p", "ios", 113 | "-o", s"/tmp/xueqiu/${System.currentTimeMillis()}", "--verbose" 114 | )) 115 | } 116 | 117 | 118 | 119 | 120 | } 121 | -------------------------------------------------------------------------------- /src/test/scala/com/testerhome/appcrawler/it/TestImage.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.it 2 | 3 | import org.scalatest.FunSuite 4 | 5 | //todo: 图像识别 6 | class TestImage extends FunSuite{ 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/test/scala/com/testerhome/appcrawler/it/TestJianShu.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.it 2 | 3 | import java.net.URL 4 | 5 | import io.appium.java_client.android.AndroidDriver 6 | import org.openqa.selenium.WebElement 7 | import org.openqa.selenium.remote.DesiredCapabilities 8 | import org.scalatest._ 9 | import scala.collection.JavaConversions._ 10 | 11 | /** 12 | * Created by seveniruby on 2017/6/6. 13 | */ 14 | class TestJianShu extends FunSuite with BeforeAndAfterAll with BeforeAndAfterEach with Matchers { 15 | 16 | val capabilities=new DesiredCapabilities() 17 | capabilities.setCapability("deviceName", "emulator-5554") 18 | capabilities.setCapability("appPackage", "com.jianshu.haruki") 19 | capabilities.setCapability("appActivity", "com.baiji.jianshu.account.SplashScreenActivity") 20 | capabilities.setCapability("unicodeKeyboard", "true") 21 | 22 | var driver=new AndroidDriver[WebElement](new URL("http://127.0.0.1:4723/wd/hub/"), capabilities) 23 | 24 | override def beforeAll(): Unit ={ 25 | capabilities.setCapability("app", "/Users/seveniruby/Downloads/Jianshu-2.3.1-17051515-1495076675.apk") 26 | driver=new AndroidDriver[WebElement](new URL("http://127.0.0.1:4723/wd/hub/"), capabilities) 27 | Thread.sleep(3000) 28 | verbose() 29 | } 30 | 31 | override def beforeEach(): Unit = { 32 | capabilities.setCapability("app", "") 33 | driver=new AndroidDriver[WebElement](new URL("http://127.0.0.1:4723/wd/hub/"), capabilities) 34 | Thread.sleep(3000) 35 | verbose() 36 | 37 | } 38 | 39 | def verbose(): Unit ={ 40 | println() 41 | println(driver.currentActivity()) 42 | println(driver.getPageSource) 43 | } 44 | 45 | test("绕过登陆"){ 46 | driver.findElementByXPath("//*[@text='跳过']").click() 47 | driver.findElementById("iv_close").click() 48 | driver.findElementsByXPath("//*[@text='登录']").size() should be >= 1 49 | } 50 | 51 | test("错误密码登录"){ 52 | driver.findElementByXPath("//*[@text='跳过']").click() 53 | driver.findElementByXPath("//*[@text='已有帐户登录']").click() 54 | driver.findElementByXPath("//*[@text='手机或邮箱']").sendKeys("seveniruby@gmail.com") 55 | driver.findElementByXPath("//*[@password='true']").sendKeys("wrong") 56 | driver.findElementByXPath("//*[@text='登录']").click() 57 | verbose() 58 | driver.findElementsByXPath("//*[contains(@text, '错误')]").size() should be >= 1 59 | } 60 | 61 | test("随便看看"){ 62 | driver.findElementByXPath("//*[@text='跳过']").click() 63 | driver.findElementByXPath("//*[@text='随便看看']").click() 64 | verbose() 65 | driver.findElementsByXPath("//*[contains(@resource-id, 'tag_flow_layout')]//*[contains(name(),'TextView')]").foreach(tag => { 66 | tag.click() 67 | Thread.sleep(1000) 68 | driver.findElementsByXPath("//*[@text='关注']").size() should be >=1 69 | driver.navigate().back() 70 | }) 71 | } 72 | 73 | override def afterEach(): Unit = { 74 | driver.quit() 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/test/scala/com/testerhome/appcrawler/it/TestMacaca.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.it 2 | 3 | import org.scalatest.{BeforeAndAfterAll, FunSuite} 4 | import org.apache.log4j.Logger 5 | import com.alibaba.fastjson.JSONObject 6 | import macaca.client.MacacaClient 7 | 8 | 9 | /** 10 | * Created by seveniruby on 2017/4/17. 11 | */ 12 | class TestMacaca extends FunSuite with BeforeAndAfterAll{ 13 | 14 | val driver=new MacacaClient() 15 | override def beforeAll(): Unit = { 16 | 17 | val porps = new JSONObject() 18 | porps.put("autoAcceptAlerts", true) 19 | porps.put("browserName", "") 20 | porps.put("platformName", "android") 21 | porps.put("package", "com.gotokeep.keep") 22 | porps.put("activity", ".activity.SplashActivity") 23 | porps.put("reuse", 3) 24 | 25 | val desiredCapabilities = new JSONObject() 26 | desiredCapabilities.put("desiredCapabilities", porps) 27 | driver.initDriver(desiredCapabilities) 28 | 29 | } 30 | test("macaca android"){ 31 | println(driver.source()) 32 | } 33 | test("macaca chrome"){ 34 | val porps = new JSONObject() 35 | porps.put("autoAcceptAlerts", true) 36 | porps.put("browserName", "Chrome") 37 | porps.put("platformName", "desktop") // android or ios 38 | 39 | porps.put("javascriptEnabled", true) 40 | porps.put("platform", "ANY") 41 | 42 | val desiredCapabilities = new JSONObject() 43 | desiredCapabilities.put("desiredCapabilities", porps) 44 | driver.initDriver(desiredCapabilities) 45 | driver.get("http://www.baidu.com/") 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/test/scala/com/testerhome/appcrawler/it/TestNW.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.it 2 | 3 | import java.net.URL 4 | 5 | import org.openqa.selenium.chrome.{ChromeOptions, ChromeDriver} 6 | import org.openqa.selenium.remote.{RemoteWebDriver, DesiredCapabilities} 7 | import org.scalatest.FunSuite 8 | import collection.JavaConversions._ 9 | 10 | /** 11 | * Created by seveniruby on 16/11/14. 12 | */ 13 | class TestNW extends FunSuite{ 14 | test("test nw"){ 15 | 16 | System.setProperty("webdriver.chrome.driver", 17 | "/Users/seveniruby/projects/nwjs/ics4_debug_nw0.14.7/chromedriver") 18 | val options=new ChromeOptions() 19 | options.addArguments("nwapp=/Users/seveniruby/projects/nwjs/ics4_debug_nw0.14.7/app") 20 | val driver=new ChromeDriver(options) 21 | println(driver.getPageSource) 22 | Thread.sleep(2000) 23 | driver.findElementsByXPath("//label").foreach(x=>{ 24 | println(x.getTagName) 25 | println(x.getLocation) 26 | println(x.getText) 27 | println("text()="+x.getAttribute("text()")) 28 | println("text="+x.getAttribute("text")) 29 | println("value="+x.getAttribute("value")) 30 | println("name="+x.getAttribute("name")) 31 | println("id="+x.getAttribute("id")) 32 | println("class="+x.getAttribute("class")) 33 | println("type="+x.getAttribute("type")) 34 | println("placeholder="+x.getAttribute("placeholder")) 35 | println("============") 36 | }) 37 | driver.findElementByXPath("//label[contains(., 'selectedRegion')]").click() 38 | 39 | //driver.quit() 40 | 41 | } 42 | 43 | test("test nw remote"){ 44 | val options=new ChromeOptions() 45 | options.addArguments("nwapp=/Users/seveniruby/projects/nwjs/ics4_debug_nw0.14.7/app") 46 | val url="http://10.3.2.65:4444/wd/hub" 47 | 48 | val dc = DesiredCapabilities.chrome() 49 | dc.setCapability(ChromeOptions.CAPABILITY, options) 50 | 51 | val driver=new RemoteWebDriver(new URL(url), dc) 52 | println(driver.getPageSource) 53 | Thread.sleep(2000) 54 | driver.findElementsByXPath("//label").foreach(x=>{ 55 | println(x.getTagName) 56 | println(x.getLocation) 57 | println(x.getText) 58 | println("text()="+x.getAttribute("text()")) 59 | println("text="+x.getAttribute("text")) 60 | println("value="+x.getAttribute("value")) 61 | println("name="+x.getAttribute("name")) 62 | println("id="+x.getAttribute("id")) 63 | println("class="+x.getAttribute("class")) 64 | println("type="+x.getAttribute("type")) 65 | println("placeholder="+x.getAttribute("placeholder")) 66 | println("============") 67 | }) 68 | driver.findElementByXPath("//label[contains(., 'selectedRegion')]").click() 69 | 70 | //driver.quit() 71 | 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/test/scala/com/testerhome/appcrawler/it/TestOCR.scala: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | package com.testerhome.appcrawler.ut 4 | 5 | import java.awt.{BasicStroke, Color} 6 | import javax.imageio.ImageIO 7 | 8 | import net.sourceforge.tess4j.ITessAPI.TessPageIteratorLevel 9 | import org.scalatest.FunSuite 10 | import net.sourceforge.tess4j._ 11 | import scala.collection.JavaConversions._ 12 | 13 | 14 | /** 15 | * Created by seveniruby on 16/9/20. 16 | */ 17 | class TestOCR extends FunSuite{ 18 | 19 | test("test ocr"){ 20 | val api=new Tesseract() 21 | api.setTessVariable("TESSDATA_PREFIX", "/Users/seveniruby/temp/ocr/") 22 | api.setDatapath("/Users/seveniruby/temp/ocr/tessdata/") 23 | api.setLanguage("eng+chi_sim") 24 | 25 | //val path = "/Users/seveniruby/temp/ocr/ocr.png" 26 | //val path="/Users/seveniruby/temp/ocr/103_com.gotokeep.keep-PersonalPageActivity_android.widget.RelativeLayout-title_bar-android.widget.RelativeLayout-left_button.clicked.png" 27 | val path="/Users/seveniruby/temp/ocr/t1t.jpeg" 28 | val img=new java.io.File(path) 29 | val imgFile=ImageIO.read(img) 30 | val graph=imgFile.createGraphics() 31 | graph.setStroke(new BasicStroke(5)) 32 | graph.setColor(Color.RED) 33 | 34 | val result=api.doOCR(img) 35 | 36 | val words=api.getWords(imgFile, TessPageIteratorLevel.RIL_WORD).toList 37 | words.foreach(word=>{ 38 | 39 | val box=word.getBoundingBox 40 | val x=box.getX.toInt 41 | val y=box.getY.toInt 42 | val w=box.getWidth.toInt 43 | val h=box.getHeight.toInt 44 | 45 | graph.drawRect(x, y, w, h) 46 | graph.drawString(word.getText, x, y) 47 | 48 | println(word.getBoundingBox) 49 | println(word.getText) 50 | }) 51 | graph.dispose() 52 | ImageIO.write(imgFile, "png", new java.io.File(s"${img}.mark.png")) 53 | 54 | 55 | 56 | println(result) 57 | 58 | } 59 | 60 | } 61 | */ 62 | -------------------------------------------------------------------------------- /src/test/scala/com/testerhome/appcrawler/it/TestSauceLabs.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.it 2 | 3 | import java.net.URL 4 | 5 | import org.openqa.selenium.remote.{DesiredCapabilities, RemoteWebDriver} 6 | import org.scalatest.FunSuite 7 | import scala.collection.JavaConversions._ 8 | 9 | class TestSauceLabs extends FunSuite{ 10 | 11 | val app = "/Users/seveniruby/Desktop/baiduyun/百度云同步盘/seven/Dropbox/sihanjishu/startup/测吧/业务/如期/如期-iOS安装包/PLM.ipa" 12 | //val app="/Users/seveniruby/Desktop/baiduyun/百度云同步盘/seven/Dropbox/sihanjishu/startup/测吧/业务/如期/PLM.zip" 13 | 14 | test("ios测试"){ 15 | val capability=new DesiredCapabilities() 16 | //capability.setCapability("app", app) 17 | //capability.setCapability("bundleId", "com.example.apple-samplecode.UICatalog") 18 | //capability.setCapability("appPackage", "com.example.apple-samplecode.UICatalog") 19 | //capability.setCapability("appActivity", "com.example.apple-samplecode.UICatalog") 20 | 21 | 22 | capability.setCapability("fullReset", "false") 23 | capability.setCapability("noReset", "false") 24 | //capability.setCapability("udid", "4F05E384-FE32-43DE-8539-4DC3E2EBC117") 25 | capability.setCapability("automationName", "XCUITest") 26 | capability.setCapability("platformName", "ios") 27 | capability.setCapability("platformVersion", "10.2") 28 | capability.setCapability("deviceName", "iPhone 7") 29 | capability.setCapability("autoAcceptAlerts", true) 30 | 31 | 32 | //val url="http://192.168.100.65:7771" 33 | //val url="http://127.0.0.1:8100" 34 | val url="http://127.0.0.1:4723/wd/hub" 35 | val driver=new RemoteWebDriver(new URL(url), capability) 36 | println(driver.getPageSource) 37 | driver.findElementsByXPath("//*[@label='OK']") match { 38 | case array if array.size()>0 => array.head.click() 39 | case _ => println("no OK alert") 40 | } 41 | driver.findElementsByXPath("//*").foreach(x=>{ 42 | println(x) 43 | println(x.getText) 44 | }) 45 | 46 | } 47 | 48 | test("ios测试 saucelabs"){ 49 | val capability=new DesiredCapabilities() 50 | capability.setCapability("app", app) 51 | 52 | capability.setCapability("bundleId", "www.ruqi.com") 53 | //capability.setCapability("bundleId", "com.example.apple-samplecode.UICatalog") 54 | //capability.setCapability("appPackage", "com.example.apple-samplecode.UICatalog") 55 | //capability.setCapability("appActivity", "com.example.apple-samplecode.UICatalog") 56 | capability.setCapability("fullReset", "false") 57 | capability.setCapability("noReset", "false") 58 | //capability.setCapability("udid", "4F05E384-FE32-43DE-8539-4DC3E2EBC117") 59 | capability.setCapability("automationName", "XCUITest") 60 | capability.setCapability("platformName", "ios") 61 | capability.setCapability("platformVersion", "10.2") 62 | capability.setCapability("deviceName", "iPhone 7") 63 | capability.setCapability("autoAcceptAlerts", true) 64 | 65 | capability.setCapability("testobject_api_key", "E571F6B0932E4DB1BD8E554A97904A0C") 66 | capability.setCapability("testobject_app_id", "ruqi") 67 | capability.setCapability("testobject_suite_name ", "My Suite 1!") 68 | capability.setCapability("testobject_test_name", "My Test 1!") 69 | //capability.setCapability("name", "My Test 1!") 70 | 71 | 72 | // Set Appium version 73 | capability.setCapability("appiumVersion", "1.7.1") 74 | val url = "https://us1.appium.testobject.com/wd/hub" 75 | 76 | 77 | //val url="http://192.168.100.65:7771" 78 | //val url="http://127.0.0.1:8100" 79 | //val url="http://127.0.0.1:4723/wd/hub" 80 | val driver=new RemoteWebDriver(new URL(url), capability) 81 | println(driver.getPageSource) 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/test/scala/com/testerhome/appcrawler/it/TestTesterHome.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.it 2 | 3 | import java.net.URL 4 | 5 | import io.appium.java_client.android.AndroidDriver 6 | import org.openqa.selenium.WebElement 7 | import org.openqa.selenium.remote.DesiredCapabilities 8 | import org.scalatest._ 9 | 10 | import scala.collection.JavaConversions._ 11 | 12 | /** 13 | * Created by seveniruby on 2017/6/6. 14 | */ 15 | class TestTesterHome extends FunSuite with BeforeAndAfterAll with BeforeAndAfterEach with Matchers { 16 | 17 | val capabilities=new DesiredCapabilities() 18 | capabilities.setCapability("deviceName", "emulator-5554") 19 | capabilities.setCapability("app", "/Users/seveniruby/Downloads/app-release.apk_1.1.0.apk") 20 | capabilities.setCapability("appPackage", "com.testerhome.nativeandroid") 21 | capabilities.setCapability("appActivity", ".views.MainActivity") 22 | capabilities.setCapability("unicodeKeyboard", "true") 23 | 24 | var driver=new AndroidDriver[WebElement](new URL("http://127.0.0.1:4723/wd/hub/"), capabilities) 25 | 26 | override def beforeEach(): Unit = { 27 | capabilities.setCapability("app", "") 28 | driver=new AndroidDriver[WebElement](new URL("http://127.0.0.1:4723/wd/hub/"), capabilities) 29 | Thread.sleep(3000) 30 | verbose() 31 | 32 | } 33 | 34 | def verbose(): Unit ={ 35 | println() 36 | println(driver.currentActivity()) 37 | println(driver.getPageSource) 38 | } 39 | 40 | test("招聘"){ 41 | driver.findElementByXPath("//*[@content-desc='Open navigation drawer']").click() 42 | driver.findElementByXPath("//*[@text='招聘']").click() 43 | driver.getContextHandles.foreach(println) 44 | verbose() 45 | driver.findElementsByXPath("//*[@text='欢迎报名第三届中国移动互联网测试开发大会']").size() should be >=1 46 | } 47 | test("精华帖"){ 48 | driver.findElementByXPath("//*[@content-desc='Open navigation drawer']").click() 49 | driver.findElementByXPath("//*[@text='社区']").click() 50 | //等待动画切换完成 51 | Thread.sleep(3000) 52 | driver.findElementByXPath("//*[@text='精华']").click() 53 | driver.findElementByXPath("//*[contains(@text, '王者荣耀')]").click() 54 | driver.findElementByXPath("//*[contains(@text, '评论')]").click() 55 | driver.findElementsByXPath("//*[@text='恒温']").size() should be >=1 56 | } 57 | 58 | override def afterEach(): Unit = { 59 | driver.quit() 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/test/scala/com/testerhome/appcrawler/it/TestWebDriverAgent.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.it 2 | 3 | import java.net.URL 4 | 5 | import com.testerhome.appcrawler.AppiumSuite 6 | import org.openqa.selenium.Capabilities 7 | import org.openqa.selenium.remote.{DesiredCapabilities, RemoteWebDriver} 8 | import org.scalatest.FunSuite 9 | import scala.collection.JavaConversions._ 10 | 11 | /** 12 | * Created by seveniruby on 16/6/3. 13 | */ 14 | class TestWebDriverAgent extends AppiumSuite{ 15 | test("use remote webdriver"){ 16 | val capability=new DesiredCapabilities() 17 | capability.setCapability("app", "/Users/seveniruby/projects/snowball-ios/DerivedData/Snowball/Build/Products/Debug-iphonesimulator/Snowball.app") 18 | capability.setCapability("bundleId", "com.xueqiu") 19 | capability.setCapability("fullReset", "true") 20 | capability.setCapability("noReset", "true") 21 | capability.setCapability("udid", "4F05E384-FE32-43DE-8539-4DC3E2EBC117") 22 | capability.setCapability("automationName", "XCUITest") 23 | capability.setCapability("platformName", "ios") 24 | capability.setCapability("deviceName", "iPhone Simulator") 25 | capability.setCapability("bundleId", "com.xueqiu") 26 | 27 | //val url="http://192.168.100.65:7771" 28 | val url="http://127.0.0.1:4723/wd/hub" 29 | val driver=new RemoteWebDriver(new URL(url), capability) 30 | println(driver.getPageSource) 31 | } 32 | 33 | 34 | test("use remote webdriver meituan"){ 35 | val capability=new DesiredCapabilities() 36 | capability.setCapability("app", "/Users/seveniruby/Downloads/app/waimai.app") 37 | capability.setCapability("bundleId", "com.meituan.iToGo.ep") 38 | //capability.setCapability("fullReset", false) 39 | //capability.setCapability("noReset", true) 40 | //capability.setCapability("udid", "4F05E384-FE32-43DE-8539-4DC3E2EBC117") 41 | capability.setCapability("automationName", "XCUITest") 42 | capability.setCapability("platformName", "ios") 43 | capability.setCapability("deviceName", "iPhone 6") 44 | capability.setCapability("platformVersion", "10.2") 45 | capability.setCapability("autoAcceptAlerts", true) 46 | //capability.setCapability("webDriverAgentUrl", "http://172.18.118.90:8100/") 47 | 48 | //val url="http://192.168.100.65:7771" 49 | //val url="http://127.0.0.1:8100" 50 | val url="http://127.0.0.1:4730/wd/hub" 51 | val driver=new RemoteWebDriver(new URL(url), capability) 52 | 53 | while(true){ 54 | Thread.sleep(2000) 55 | println(driver.getPageSource) 56 | } 57 | 58 | } 59 | 60 | test("use remote webdriver xueqiu"){ 61 | val capability=new DesiredCapabilities() 62 | capability.setCapability("app", "/Users/seveniruby/projects/snowball-ios/DerivedData/Snowball/Build/Products/Debug-iphonesimulator/Snowball.app") 63 | capability.setCapability("bundleId", "com.xueqiu") 64 | capability.setCapability("fullReset", "false") 65 | capability.setCapability("noReset", "true") 66 | //capability.setCapability("udid", "4F05E384-FE32-43DE-8539-4DC3E2EBC117") 67 | capability.setCapability("automationName", "XCUITest") 68 | capability.setCapability("platformName", "ios") 69 | capability.setCapability("deviceName", "iPhone Simulator") 70 | capability.setCapability("bundleId", "com.xueqiu") 71 | capability.setCapability("autoAcceptAlerts", true) 72 | 73 | 74 | //val url="http://192.168.100.65:7771" 75 | //val url="http://127.0.0.1:8100" 76 | val url="http://127.0.0.1:4730/wd/hub" 77 | val driver=new RemoteWebDriver(new URL(url), capability) 78 | 79 | while(true){ 80 | Thread.sleep(2000) 81 | driver.findElementsByXPath("//*").foreach(e=>{ 82 | println(s"tag=${e.getTagName} text=${e.getText}") 83 | }) 84 | println(driver.getPageSource) 85 | println("==============") 86 | } 87 | 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/test/scala/com/testerhome/appcrawler/it/TestWebView.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.it 2 | 3 | import java.net.URL 4 | 5 | import com.testerhome.appcrawler.XPathUtil 6 | import io.appium.java_client.TouchAction 7 | import io.appium.java_client.android.AndroidDriver 8 | import org.openqa.selenium.{By, WebElement} 9 | import org.openqa.selenium.remote.{DesiredCapabilities, RemoteWebDriver} 10 | import org.scalatest.FunSuite 11 | 12 | import collection.JavaConversions._ 13 | 14 | /** 15 | * Created by seveniruby on 16/11/3. 16 | */ 17 | class TestWebView extends FunSuite{ 18 | 19 | test("webview"){ 20 | val capability=new DesiredCapabilities() 21 | capability.setCapability("app", "") 22 | capability.setCapability("appPackage", "iHealth.AiJiaKang.MI") 23 | capability.setCapability("fastReset", "true") 24 | capability.setCapability("noReset", "false") 25 | capability.setCapability("automationName", "appium") 26 | capability.setCapability("deviceName", "ddd") 27 | capability.setCapability("platformName", "android") 28 | capability.setCapability("appActivity", "com.ihealth.aijiakang.ui.user.User_Welcome") 29 | 30 | //val url="http://192.168.100.65:7771" 31 | val url="http://127.0.0.1:4723/wd/hub" 32 | val driver=new AndroidDriver[WebElement](new URL(url), capability) 33 | Thread.sleep(3000) 34 | var xml=driver.getPageSource 35 | println(XPathUtil.toPrettyXML(xml)) 36 | driver.getContextHandles.toArray.foreach(x=>println(x)) 37 | //driver.context("WEBVIEW_com.ihealthlabs.ijiankang.patient.android") 38 | val size=driver.manage().window().getSize 39 | xml=driver.getPageSource 40 | println(XPathUtil.toPrettyXML(xml)) 41 | driver.getContextHandles.toArray.foreach(x=>println(x)) 42 | 43 | driver.findElements(By.xpath("//*")).foreach(x=>{ 44 | println(x.getText) 45 | println(x.getTagName) 46 | println(x.getLocation) 47 | println(x.getAttribute("text")) 48 | println(x.getAttribute("value")) 49 | println(x.getAttribute("name")) 50 | println(x.getAttribute("id")) 51 | println(x.getAttribute("class")) 52 | println(x.getAttribute("type")) 53 | println(x.getAttribute("placeholder")) 54 | println("============") 55 | }) 56 | 57 | driver.findElementByXPath("//*[text()='立即体验']") 58 | 59 | 60 | driver.context("WEBVIEW_com.android.browser") 61 | 62 | driver.findElement(By.tagName("input")).sendKeys("18201578100") 63 | driver.findElement(By.tagName("button")).click() 64 | Thread.sleep(3000) 65 | 66 | 67 | 68 | 69 | driver.findElement(By.xpath("//input[@type='password']")).sendKeys("12344321") 70 | Thread.sleep(1000) 71 | 72 | println("ddd") 73 | driver.findElements(By.xpath("//*[contains(text(), '录')]")).foreach(x=>{ 74 | println(x.getText) 75 | println(x.getTagName) 76 | println(x.getLocation) 77 | println(x.getAttribute("text")) 78 | println(x.getAttribute("value")) 79 | println(x.getAttribute("name")) 80 | println(x.getAttribute("id")) 81 | println(x.getAttribute("class")) 82 | println(x.getAttribute("type")) 83 | println(x.getAttribute("placeholder")) 84 | println("============") 85 | }) 86 | 87 | println("ddd") 88 | driver.findElements(By.xpath("//button")).foreach(x=>{ 89 | println(x.getText) 90 | println(x.getTagName) 91 | println(x.getLocation) 92 | println(x.getAttribute("text")) 93 | println(x.getAttribute("value")) 94 | println(x.getAttribute("name")) 95 | println(x.getAttribute("id")) 96 | println(x.getAttribute("class")) 97 | println(x.getAttribute("type")) 98 | println(x.getAttribute("placeholder")) 99 | println(x.getAttribute("innerText")) 100 | println("============") 101 | }) 102 | 103 | driver.findElement(By.xpath("//*[contains(text(), '录')]")).click() 104 | Thread.sleep(1000) 105 | 106 | println("ddd") 107 | driver.findElements(By.xpath("//*[contains(., '健康')]")).foreach(x=>{ 108 | println(x.getText) 109 | println(x.getTagName) 110 | println(x.getLocation) 111 | println(x.getAttribute("text")) 112 | println(x.getAttribute("value")) 113 | println(x.getAttribute("name")) 114 | println(x.getAttribute("id")) 115 | println(x.getAttribute("class")) 116 | println(x.getAttribute("type")) 117 | println(x.getAttribute("placeholder")) 118 | println(x.getAttribute("innerText")) 119 | println("============") 120 | }) 121 | 122 | 123 | Thread.sleep(2000) 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /src/test/scala/com/testerhome/appcrawler/it/TestXueQiu.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.it 2 | 3 | import java.net.URL 4 | 5 | import com.testerhome.appcrawler.AppCrawler 6 | import io.appium.java_client.android.{AndroidDriver, AndroidElement} 7 | import org.openqa.selenium.remote.DesiredCapabilities 8 | import org.scalatest.FunSuite 9 | 10 | class TestXueQiu extends FunSuite{ 11 | val capability=new DesiredCapabilities() 12 | capability.setCapability("app", "") 13 | capability.setCapability("appPackage", "com.tencent.mm") 14 | capability.setCapability("appActivity", ".ui.LauncherUI") 15 | capability.setCapability("deviceName", "emulator-5554") 16 | capability.setCapability("fastReset", "false") 17 | capability.setCapability("fullReset", "false") 18 | capability.setCapability("noReset", "true") 19 | capability.setCapability("unicodeKeyboard", "true") 20 | capability.setCapability("resetKeyboard", "true") 21 | capability.setCapability("automationName", "appium") 22 | 23 | test("all app "){ 24 | capability.setCapability("app", "") 25 | capability.setCapability("appPackage", "com.xueqiu.android") 26 | capability.setCapability("appActivity", ".view.WelcomeActivityAlias") 27 | val driver=new AndroidDriver[AndroidElement](new URL("http://127.0.0.1:4723/wd/hub"), capability) 28 | Thread.sleep(30000) 29 | 30 | } 31 | 32 | test("appcrawler xueqiu by default conf"){ 33 | AppCrawler.main(Array( 34 | "--capability", "appPackage=com.xueqiu.android,appActivity=.view.WelcomeActivityAlias,noReset=false", 35 | "-u", "6723", 36 | "-o", s"/Volumes/ram/xueqiu/${new java.text.SimpleDateFormat("YYYYMMddHHmmss").format(new java.util.Date().getTime)}", 37 | "--verbose" 38 | ) 39 | ) 40 | } 41 | 42 | test("appcrawler base example"){ 43 | AppCrawler.main(Array( 44 | "-c", "src/test/scala/com/testerhome/appcrawler/it/xueqiu_automation.yml", 45 | "-o", s"/tmp/xueqiu/${System.currentTimeMillis()}", "--verbose" 46 | ) 47 | ) 48 | } 49 | 50 | test("appcrawler base example ios"){ 51 | 52 | val app = "/Users/seveniruby/projects/ios-uicatalog/build/Debug-iphonesimulator/UICatalog.app" 53 | AppCrawler.main(Array("-c", "src/test/scala/com/testerhome/appcrawler/it/xueqiu_base.yml", 54 | "-a", app, 55 | //"-a", "/Users/seveniruby/projects/snowball-ios/DerivedData/Snowball/Build/Products/Debug-iphonesimulator/Snowball.app", 56 | "-o", s"/tmp/xueqiu/${System.currentTimeMillis()}", "--verbose" 57 | ) 58 | ) 59 | } 60 | 61 | test("test automation"){ 62 | AppCrawler.main(Array("-c", "src/test/scala/com/testerhome/appcrawler/it/xueqiu_automation.yml", 63 | "-o", s"/tmp/xueqiu/${System.currentTimeMillis()}", "--verbose" 64 | ) 65 | ) 66 | } 67 | 68 | 69 | test("test default crawler"){ 70 | AppCrawler.main(Array( 71 | "--capability", "appPackage=com.xueqiu.android,appActivity=.view.WelcomeActivityAlias,dontStopAppOnReset=true", 72 | "-o", s"/tmp/xueqiu/${System.currentTimeMillis()}", "--verbose" 73 | ) 74 | ) 75 | } 76 | 77 | test("test sikuli"){ 78 | AppCrawler.main(Array("-c", "src/test/scala/com/testerhome/appcrawler/it/xueqiu_sikuli.yml", 79 | "-o", s"/tmp/xueqiu/${System.currentTimeMillis()}", "--verbose" 80 | ) 81 | ) 82 | } 83 | test("test xiaomi"){ 84 | AppCrawler.main(Array("-c", "src/tes" + 85 | "" + 86 | "t/scala/com/testerhome/appcrawler/it/xiaomi.yml", 87 | "-o", s"/tmp/xiaomi/${System.currentTimeMillis()}", "--verbose" 88 | ) 89 | ) 90 | } 91 | 92 | test("xiaomi click"){ 93 | capability.setCapability("app", "") 94 | capability.setCapability("appPackage", "com.xueqiu.android") 95 | capability.setCapability("appActivity", ".view.WelcomeActivityAlias") 96 | val driver=new AndroidDriver[AndroidElement](new URL("http://127.0.0.1:4723/wd/hub"), capability) 97 | 98 | } 99 | 100 | 101 | 102 | 103 | } 104 | -------------------------------------------------------------------------------- /src/test/scala/com/testerhome/appcrawler/it/TestZhangZhongTong.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.it 2 | 3 | import java.time.LocalDateTime 4 | 5 | import com.testerhome.appcrawler.AppCrawler 6 | import org.junit.runner.RunWith 7 | import org.scalatest.FunSuite 8 | import org.scalatest.junit.JUnitRunner 9 | 10 | @RunWith(classOf[JUnitRunner]) 11 | class TestZhangZhongTong extends FunSuite { 12 | test("android") { 13 | AppCrawler.main(Array("-c", "demo.yml", 14 | "-o", s"output/${LocalDateTime.now()}", 15 | "-a", "zzt.apk", "-vv" 16 | ) 17 | ) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/test/scala/com/testerhome/appcrawler/it/keep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | pluginList: 3 | - "com.testerhome.appcrawler.plugin.TagLimitPlugin" 4 | - "com.testerhome.appcrawler.plugin.ReportPlugin" 5 | logLevel: "TRACE" 6 | reportTitle: "Keep" 7 | saveScreen: true 8 | screenshotTimeout: 20 9 | currentDriver: "android" 10 | showCancel: true 11 | tagLimitMax: 3 12 | tagLimit: 13 | - xpath: //*[../*[@selected='true']] 14 | count: 12 15 | maxTime: 10800 16 | resultDir: "" 17 | capability: 18 | newCommandTimeout: 120 19 | launchTimeout: 120000 20 | platformVersion: "" 21 | platformName: "Android" 22 | autoWebview: "false" 23 | autoLaunch: "true" 24 | noReset: "true" 25 | androidInstallTimeout: 180000 26 | androidCapability: 27 | deviceName: "192.168.0.102:5555" 28 | appPackage: "com.gotokeep.keep" 29 | appActivity: ".activity.SplashActivity" 30 | app: "" 31 | appium: "http://127.0.0.1:4723/wd/hub" 32 | # automationName: uiautomator2 33 | automationName: macaca 34 | reuse: 3 35 | # nativeWebScreenshot: "true" 36 | defineUrl: 37 | - //*[@selected='true' and contains(name(), 'TextView')]/@text 38 | appWhiteList: 39 | - android 40 | - com.shafa.market 41 | baseUrl: 42 | - ".*MainActivity" 43 | - ".*SNBHomeView.*" 44 | maxDepth: 20 45 | headFirst: true 46 | enterWebView: true 47 | urlBlackList: 48 | - .*OutdoorSummaryMap.* 49 | - .*PersonalPage.* 50 | - .*Training.* 51 | - .*FriendRank.* 52 | - .*\\.base\\.Container.* 53 | - ".*球友.*" 54 | - ".*png.*" 55 | - ".*Talk.*" 56 | - ".*Chat.*" 57 | - ".*Safari.*" 58 | - "WriteStatus.*" 59 | - "Browser.*" 60 | - "MyselfUser" 61 | - ".*MyselfUser.*" 62 | - ".*股市直播.*" 63 | #urlWhiteList: 64 | #- ".*Main.*" 65 | backButton: 66 | - //*[contains(@resource-id, "left_button")] 67 | #defaultBackAction: 68 | #- import sys.process._; 69 | #- Thread.sleep(5000) 70 | #- val name=Seq("adb", "shell", "dumpsys window windows | grep mCurrentFocus").!!.split(" ")(4).split("/")(0) 71 | #- println(s"kill package ${name}") 72 | #- Seq("adb", "shell", s"am force-stop ${name}").!! 73 | #firstList: 74 | #- //*[contains(@resource-id, "layout_picker_view_container"] 75 | selectedList: 76 | #android非空标签 77 | - //*[@clickable='true'] 78 | - //*[@clickable='true']//*[contains(name(), 'Text') and string-length(@text)>0 and string-length(@text)<10 ] 79 | #通用的button和image 80 | - //*[@clickable='true']//*[contains(name(), 'Button')] 81 | - //*[@clickable='true']//*[contains(name(), 'Image')] 82 | #todo:如果多个规则都包含相同控件, 如何排序 83 | #处于选中状态的同级控件最后点击 84 | lastList: 85 | - //*[../*[@selected='true']] 86 | - //*[../../*/*[@selected='true']] 87 | - //*[../../*/*[@selected='true'] and contains(@resource-id, 'tab_')] 88 | - //*[contains(name(), "HorizontalScrollView")] 89 | - //*[@resource-id='com.gotokeep.keep:id/layout_bottom'] 90 | blackList: 91 | - ".*\\.[0-9].*" 92 | - ".*[0-9][0-9].*" 93 | - //*[contains(@resource-id, "wrapper_in_custom_title_bar")]//*[contains(@resource-id, "right_button")] 94 | - //*[contains(@resource-id, "share")] 95 | - //*[contains(@text, "开始第")] 96 | - //*[contains(@resource-id, "lock")] 97 | - //*[contains(@text, "举报")] 98 | triggerActions: 99 | - xpath: //*[contains(@resource-id, "layout_picker_view_container")]//*[@text="确定"] 100 | - xpath: //*[contains(@resource-id, "content-wrapper_dialog")]//*[@text="不发了"] 101 | - xpath: //*[@text="拒绝"] 102 | - xpath: //*[@text="结束训练"] 103 | - xpath: //*[contains(@resource-id, "quit_confirm_button")]//*[contains(@text, "确定")] 104 | - xpath: //*[contains(@resource-id, "layout_right_second_button")]//*[contains(@resource-id, "right_second_button")] 105 | action: yoga 106 | times: 1 107 | - xpath: //*[contains(@text, "微信朋友圈")] 108 | - xpath: //*[contains(@text, "发送")] 109 | - xpath: 110 | asserts: 111 | - given: 112 | - //*[@text="胸部"] 113 | when: [] 114 | then: 115 | - //*[contains(@text, "success")] 116 | - //*[@package="com.gotokeep.keep"] 117 | 118 | 119 | #所有view的叶子节点 一般表示游戏 120 | #- action: monkey 121 | # xpath: //android.view.View[not(*) and contains(@bounds, "[0,0]") ] 122 | # times: 20 123 | #startupActions: 124 | #- println(driver) 125 | #beforeElementAction: 126 | #- xpath: //*[@resource-id="com.shafa.market:id/nav"]//android.widget.TextView 127 | # action: MiniAppium.event(21) 128 | #- Thread.sleep(3000) 129 | #- println(driver.getPageSource()) 130 | #afterElementAction: 131 | #- println(driver) 132 | #afterUrlFinished: 133 | #- monkey() 134 | -------------------------------------------------------------------------------- /src/test/scala/com/testerhome/appcrawler/it/keep_test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | logLevel: "TRACE" 3 | reportTitle: "Keep" 4 | saveScreen: true 5 | screenshotTimeout: 20 6 | currentDriver: "android" 7 | showCancel: true 8 | tagLimitMax: 2 9 | tagLimit: 10 | - xpath: //*[../*[@selected='true']] 11 | count: 12 12 | - xpath: //*[contains(@resource-id, "layout_bottom")] 13 | count: 12 14 | maxTime: 10800 15 | resultDir: "" 16 | capability: 17 | newCommandTimeout: 120 18 | launchTimeout: 120000 19 | platformVersion: "" 20 | platformName: "Android" 21 | autoWebview: "false" 22 | autoLaunch: "true" 23 | noReset: "true" 24 | androidInstallTimeout: 180000 25 | androidCapability: 26 | deviceName: "192.168.0.102:5555" 27 | appPackage: "com.gotokeep.keep" 28 | appActivity: ".activity.SplashActivity" 29 | dontStopAppOnReset: true 30 | app: "" 31 | appium: "http://127.0.0.1:4723/wd/hub" 32 | # automationName: uiautomator2 33 | automationName: uiautomator2 34 | reuse: 3 35 | # nativeWebScreenshot: "true" 36 | defineUrl: 37 | - //*[@selected='true' and contains(name(), 'TextView')]/@text 38 | #- //*[contains(@resource-id, 'title')]/@text 39 | appWhiteList: 40 | - android 41 | - com.shafa.market 42 | baseUrl: [] 43 | maxDepth: 20 44 | headFirst: true 45 | enterWebView: true 46 | urlBlackList: 47 | - .*OutdoorSummaryMap.* 48 | - .*PersonalPage.* 49 | - .*Training.* 50 | - .*FriendRank.* 51 | - .*\\.base\\.Container.* 52 | - DataCenterActivity 53 | #urlWhiteList: 54 | #- ".*Main.*" 55 | backButton: 56 | - //*[contains(@resource-id, "left_button") and @clickable='true'] 57 | #defaultBackAction: 58 | #- import sys.process._; 59 | #- Thread.sleep(5000) 60 | #- val name=Seq("adb", "shell", "dumpsys window windows | grep mCurrentFocus").!!.split(" ")(4).split("/")(0) 61 | #- println(s"kill package ${name}") 62 | #- Seq("adb", "shell", s"am force-stop ${name}").!! 63 | #firstList: 64 | #- //*[contains(@resource-id, "layout_picker_view_container"] 65 | selectedList: 66 | #android非空标签 67 | - //*[@clickable='true'] 68 | - //*[@clickable='true']//*[contains(name(), 'Text') and string-length(@text)>0 and string-length(@text)<10 ] 69 | #通用的button和image 70 | - //*[@clickable='true']//*[contains(name(), 'Button')] 71 | - //*[@clickable='true']//*[contains(name(), 'Image')] 72 | #todo:如果多个规则都包含相同控件, 如何排序 73 | #处于选中状态的同级控件最后点击 74 | lastList: 75 | - //*[../*[@selected='true']] 76 | - //*[../../*/*[@selected='true']] 77 | - //*[../../*/*[@selected='true'] and contains(@resource-id, 'tab_')] 78 | - //*[contains(name(), "HorizontalScrollView")] 79 | - //*[@resource-id='com.gotokeep.keep:id/layout_bottom'] 80 | blackList: 81 | - ".*\\.[0-9].*" 82 | - ".*[0-9]{2,}.*" 83 | - //*[contains(@resource-id, "wrapper_in_custom_title_bar")]//*[contains(@resource-id, "right_button")] 84 | - //*[contains(@resource-id, "share")] 85 | - //*[contains(@text, "开始第")] 86 | - //*[contains(@resource-id, "lock")] 87 | - button_resume_run 88 | triggerActions: 89 | - xpath: //*[contains(@resource-id, "layout_picker_view_container")]//*[@text="确定"] 90 | - xpath: //*[contains(@resource-id, "content-wrapper_dialog")]//*[@text="不发了"] 91 | - xpath: //*[@text="拒绝"] 92 | - xpath: //*[@text="放弃"] 93 | - xpath: 确定 94 | - xpath: 不发了 95 | - xpath: //*[@text="结束训练"] 96 | - xpath: //*[contains(@text, "举报")] 97 | action: back 98 | - xpath: //*[contains(@resource-id, "quit_confirm_button")]//*[contains(@text, "确定")] 99 | - xpath: 结束 100 | action: tap 101 | - xpath: 确定退出 102 | asserts: 103 | - given: 104 | - //* 105 | then: 106 | - //*[@package="com.gotokeep.keep"] 107 | - given: 108 | - //*[@text="胸部"] 109 | then: 110 | - //*[contains(@text, "离心俯卧撑")] 111 | testcase: 112 | name: demo1 113 | steps: 114 | - when: 115 | xpath: //* 116 | action: driver.swipe(0.5, 0.8, 0.5, 0.2) 117 | then: [] 118 | - when: 119 | xpath: //* 120 | action: driver.swipe(0.5, 0.2, 0.5, 0.8) 121 | then: [] 122 | - when: 123 | xpath: //*[contains(@resource-id, 'text_home_train_collection_title')] 124 | action: tap 125 | then: 126 | - //*[contains(@text, "置顶")] 127 | - when: 128 | xpath: //*[contains(@text, '置顶')] 129 | action: click 130 | then: 131 | - //*[contains(@text, "添加训练")] 132 | - //*[contains(@text, "故意错误")] 133 | #所有view的叶子节点 一般表示游戏 134 | #- action: monkey 135 | # xpath: //android.view.View[not(*) and contains(@bounds, "[0,0]") ] 136 | # times: 20 137 | #startupActions: 138 | #- println(driver) 139 | beforeElementAction: 140 | - xpath: //*[@resource-id="com.shafa.market:id/nav"]//android.widget.TextView 141 | action: MiniAppium.event(21) 142 | - Thread.sleep(3000) 143 | - println(driver.getPageSource()) 144 | #afterElementAction: 145 | #- println(driver) 146 | #- Thread.sleep(1000) 147 | #afterUrlFinished: 148 | #- monkey() 149 | -------------------------------------------------------------------------------- /src/test/scala/com/testerhome/appcrawler/it/xueqiu_automation.yml: -------------------------------------------------------------------------------- 1 | --- 2 | pluginList: 3 | - "com.testerhome.appcrawler.plugin.FlowDiff" 4 | #- "com.testerhome.appcrawler.plugin.ProxyPlugin" 5 | logLevel: "TRACE" 6 | saveScreen: false 7 | showCancel: true 8 | reportTitle: AppCrawler雪球内部版 9 | screenshotTimeout: 20 10 | tagLimitMax: 5 11 | currentDriver: "android" 12 | maxTime: 10800 13 | resultDir: "" 14 | capability: 15 | newCommandTimeout: 120 16 | launchTimeout: 120000 17 | platformVersion: "" 18 | platformName: "" 19 | autoWebview: "false" 20 | autoLaunch: "true" 21 | noReset: "true" 22 | fullReset: "false" 23 | dontStopAppOnReset: "true" 24 | androidCapability: 25 | deviceName: "demo" 26 | appPackage: "com.xueqiu.android" 27 | appActivity: ".view.WelcomeActivityAlias" 28 | app: "" 29 | appium: "http://127.0.0.1:4723/wd/hub" 30 | #automationName: uiautomator2 31 | iosCapability: 32 | deviceName: "iPhone 6 Plus" 33 | bundleId: "com.xueqiu" 34 | screenshotWaitTimeout: "10" 35 | platformVersion: "9.3" 36 | autoAcceptAlerts: "true" 37 | app: "/Users/seveniruby/Library/Developer/Xcode/DerivedData/Snowball-ckpjegabufjxgxfeqyxgkmjuwmct/Build/Products/Debug-iphonesimulator/Snowball.app" 38 | appium: "http://192.168.31.27:4723/wd/hub" 39 | defineUrl: 40 | - "//*[@selected='true']/@text" 41 | - "//*[@selected='true']/@text" 42 | - "//*[contains(name(), 'NavigationBar')]/@label" 43 | #baseUrl: 44 | #- ".*MainActivity" 45 | #- ".*SNBHomeView.*" 46 | maxDepth: 1 47 | headFirst: true 48 | enterWebView: true 49 | urlBlackList: 50 | - ".*球友.*" 51 | - ".*png.*" 52 | - ".*Talk.*" 53 | - ".*Chat.*" 54 | - ".*Safari.*" 55 | - "WriteStatus.*" 56 | - "Browser.*" 57 | - "MyselfUser" 58 | - ".*MyselfUser.*" 59 | - ".*股市直播.*" 60 | #urlWhiteList: 61 | #- ".*Main.*" 62 | backButton: 63 | - xpath: //*[@resource-id='action_back'] 64 | - xpath: //*[@resource-id='android:id/up'] 65 | - xpath: //*[@resource-id='android:id/home'] 66 | - xpath: //*[@resource-id='android:id/action_bar_title'] 67 | - xpath: //*[@name='nav_icon_back'] 68 | - xpath: //*[@name='Back'] 69 | - xpath: //*[@name='返回'] 70 | - xpath: "//*[contains(name(), 'Button') and @name='取消']" 71 | - xpath: "//*[contains(name(), 'Button') and @label='返回']" 72 | - xpath: "//*[contains(name(), 'Button') and @name='关闭']" 73 | - xpath: "//*[contains(name(), 'Button') and @name='首页']" 74 | triggerActions: 75 | - xpath: "//*[contains(@resource-id, 'iv_close')]" 76 | - xpath: "//*[@resource-id='com.xueqiu.android:id/button_login']" 77 | times: 1 78 | - action: "15600534760" 79 | xpath: "//*[@resource-id='com.xueqiu.android:id/login_account']" 80 | times: 1 81 | - xpath: "//*[@resource-id='com.xueqiu.android:id/login_account']" 82 | times: 1 83 | - action: "hys2xueqiu" 84 | xpath: "//*[@resource-id='com.xueqiu.android:id/login_password']" 85 | times: 1 86 | - xpath: "button_next" 87 | times: 1 88 | - action: "15600534760" 89 | xpath: "//*[contains(name(), 'StaticText') and contains(@name, '登录')]" 90 | times: 1 91 | - action: "15600534760" 92 | xpath: "//*[contains(name(), 'TextField') and contains(@value, '手机')]" 93 | times: 1 94 | - action: "hys2xueqiu" 95 | xpath: "//*[contains(name(), 'SecureTextField')]" 96 | times: 1 97 | - xpath: "//*[contains(name(), 'Button') and contains(@name, '登 录')]" 98 | times: 1 99 | - xpath: ".*立即登录" 100 | times: 2 101 | - xpath: "//*[@name='登 录']" 102 | times: 2 103 | - xpath: "//*[@name='登录']" 104 | times: 2 105 | - action: "scroll left" 106 | xpath: "专题" 107 | times: 1 108 | - xpath: "点此.*" 109 | times: 3 110 | - xpath: "放弃" 111 | - xpath: "不保存" 112 | - xpath: "^确定$" 113 | - xpath: "^关闭$" 114 | - xpath: "取消" 115 | - xpath: "稍后再说" 116 | - xpath: "Cancel" 117 | - xpath: "这里可以.*" 118 | - xpath: ".*搬到这里.*" 119 | - xpath: "我要退出" 120 | - xpath: "tip_click_position" 121 | - xpath: "common guide icon ok" 122 | - xpath: "icon quotationinformation day" 123 | times: 1 124 | - xpath: "icon stock close" 125 | - xpath: "隐藏键盘" 126 | #一个神奇的符号 127 | - xpath: //*[@label='✕' and visible='true'] 128 | times: 10 129 | - action: 123 130 | xpath: //*[contains(name(), "EditText")] 131 | times: 10 132 | pri: 0 133 | - xpath: 我知道了 134 | testcase: 135 | name: demo1 136 | steps: 137 | - when: 138 | xpath: //* 139 | action: driver.swipe(0.8, 0.8, 0.2, 0.2) 140 | then: ["//*[@resource-id!='']"] 141 | - when: { xpath: //*, action: driver.swipe(0.5, 0.2, 0.5, 0.8) } 142 | - { xpath: 自选, action: click, then: [ "//*[contains(@text, '港股')]" ] } 143 | - { xpath: 沪深, action: click, then: [ "//*[contains(@text, '中国平安')]" ] } -------------------------------------------------------------------------------- /src/test/scala/com/testerhome/appcrawler/it/xueqiu_private.yml: -------------------------------------------------------------------------------- 1 | --- 2 | pluginList: 3 | - "com.testerhome.appcrawler.plugin.FlowDiff" 4 | #- "com.testerhome.appcrawler.plugin.ProxyPlugin" 5 | logLevel: "TRACE" 6 | saveScreen: true 7 | showCancel: true 8 | reportTitle: AppCrawler雪球内部版 9 | screenshotTimeout: 20 10 | tagLimitMax: 2 11 | currentDriver: "android" 12 | maxTime: 10800 13 | resultDir: "" 14 | capability: 15 | newCommandTimeout: 120 16 | launchTimeout: 120000 17 | platformVersion: "" 18 | platformName: "" 19 | autoWebview: "false" 20 | autoLaunch: "true" 21 | noReset: "false" 22 | androidCapability: 23 | deviceName: "demo" 24 | appPackage: "com.xueqiu.android" 25 | appActivity: ".view.WelcomeActivityAlias" 26 | app: "" 27 | appium: "http://127.0.0.1:4723/wd/hub" 28 | fullReset: false 29 | noReset: true 30 | automationName: appium 31 | unicodeKeyboard: true 32 | iosCapability: 33 | deviceName: "iPhone 7 Plus" 34 | #bundleId: "com.xueqiu" 35 | bundleId: com.example.apple-samplecode.UICatalog 36 | screenshotWaitTimeout: "10" 37 | platformVersion: "10.2" 38 | autoAcceptAlerts: "true" 39 | automationName: xcuitest 40 | app: /Users/seveniruby/projects/ios-uicatalog/build/Debug-iphonesimulator/UICatalog.app 41 | #app: "/Users/seveniruby/Library/Developer/Xcode/DerivedData/Snowball-ckpjegabufjxgxfeqyxgkmjuwmct/Build/Products/Debug-iphonesimulator/Snowball.app" 42 | appium: "http://127.0.0.1:4723/wd/hub" 43 | defineUrl: 44 | - "//*[@selected='true']/@text" 45 | - "//*[@selected='true']/@text" 46 | - "//*[contains(name(), 'NavigationBar')]/@label" 47 | baseUrl: 48 | - ".*MainActivity" 49 | - ".*SNBHomeView.*" 50 | maxDepth: 8 51 | headFirst: true 52 | enterWebView: true 53 | urlBlackList: 54 | - ".*球友.*" 55 | - ".*png.*" 56 | - ".*Talk.*" 57 | - ".*Chat.*" 58 | - ".*Safari.*" 59 | - "WriteStatus.*" 60 | - "Browser.*" 61 | - "MyselfUser" 62 | - ".*MyselfUser.*" 63 | - ".*股市直播.*" 64 | #urlWhiteList: 65 | #- ".*Main.*" 66 | backButton: 67 | - //*[@resource-id='action_back'] 68 | - //*[@resource-id='android:id/up'] 69 | - //*[@resource-id='android:id/home'] 70 | - //*[@resource-id='android:id/action_bar_title'] 71 | - //*[@name='nav_icon_back'] 72 | - //*[@name='Back'] 73 | - //*[@name='返回'] 74 | - "//*[contains(name(), 'Button') and @name='取消']" 75 | - "//*[contains(name(), 'Button') and @label='返回']" 76 | - "//*[contains(name(), 'Button') and @name='关闭']" 77 | - "//*[contains(name(), 'Button') and @name='首页']" 78 | firstList: 79 | - "//*[contains(name(), 'Popover')]//*" 80 | - "//*[contains(name(), 'Window')][3]//*" 81 | - "//*[contains(name(), 'Window')][2]//*" 82 | selectedList: 83 | #android非空标签 84 | - //*[@clickable="true"]//android.widget.TextView[string-length(@text)>0 and string-length(@text)<20] 85 | - //android.widget.EditText 86 | - //android.widget.TextView[string-length(@text)>0 and string-length(@text)<20 and @clickable="true"] 87 | #ios 88 | - //*[contains(name(), 'Text') and string-length(@value)>0 and string-length(@value)<20 ] 89 | #通用的button和image 90 | - //*[contains(name(), 'Button')] 91 | - //*[contains(name(), 'Image')] 92 | #todo:如果多个规则都包含相同控件, 如何排序 93 | #处于选中状态的同级控件最后点击 94 | lastList: 95 | - //*[contains(@resource-id, 'header')]//* 96 | - //*[contains(@resource-id, 'indicator')]//* 97 | #股票 组合 98 | - //*[../*[@selected='true']] 99 | #港股 美股 100 | - //*[../../*/*[@selected='true'] and @resource-id=''] 101 | #tab标签 102 | - //*[../../*/*[@selected='true'] and contains(@resource-id, 'tab_')] 103 | #ios 沪深 港股等栏目 104 | - //*[../*[@value='1']] 105 | #ios 底层tab栏 106 | - //*[contains(name(), 'Button') and ../*[contains(name(), 'Button') and @value='1']] 107 | #tab低栏 108 | - //*[contains(@resource-id,'tabs')]//* 109 | blackList: 110 | #排除掉ios的状态栏 111 | - "//*[contains(name(), 'StatusBar')]//*" 112 | #股票分组编辑. 同一个imageview有2个图代表不同的状态. 没法区分, 只能设置为黑名单 113 | - //*[@resource-id='com.xueqiu.android:id/edit_group'] 114 | - ".*Safari" 115 | - ".*电话.*" 116 | - ".*Safari.*" 117 | - "发布" 118 | - "action_bar_title" 119 | - ".*浏览器.*" 120 | - "message" 121 | - ".*home" 122 | - "首页" 123 | - "Photos" 124 | - "地址" 125 | - "网址" 126 | - "拉黑" 127 | - "举报" 128 | - "camera" 129 | - "Camera" 130 | - "nav_icon_home" 131 | - "stock_item_.*" 132 | - ".*[0-9]{2}.*" 133 | - "发送" 134 | - "保存" 135 | - "确定" 136 | - "up" 137 | - "user_profile_icon" 138 | - "selectAll" 139 | - "cut" 140 | - "copy" 141 | - "send" 142 | - "买[0-9]*" 143 | - "卖[0-9]*" 144 | - "聊天.*" 145 | - "拍照.*" 146 | - "发表.*" 147 | - "回复.*" 148 | - "加入.*" 149 | - "赞助.*" 150 | - "微博.*" 151 | - "球友.*" 152 | - ".*开户.*" 153 | triggerActions: 154 | #- xpath: "//*[contains(@resource-id, 'iv_close')]" 155 | - xpath: "//*[@resource-id='com.xueqiu.android:id/button_login']" 156 | times: 1 157 | - action: "15600534760" 158 | xpath: "//*[@resource-id='com.xueqiu.android:id/login_account']" 159 | times: 1 160 | - xpath: "//*[@resource-id='com.xueqiu.android:id/login_account']" 161 | times: 1 162 | - action: "click" 163 | xpath: //*[@password='true'] 164 | times: 1 165 | - action: "1234" 166 | xpath: //*[@password='true'] 167 | times: 2 168 | - xpath: "button_next" 169 | times: 1 170 | - action: "15600534760" 171 | xpath: "//*[contains(name(), 'StaticText') and contains(@name, '登录')]" 172 | times: 1 173 | - action: "15600534760" 174 | xpath: "//*[contains(name(), 'TextField') and contains(@value, '手机')]" 175 | times: 1 176 | - action: "dsssssdd" 177 | xpath: "//*[contains(name(), 'SecureTextField')]" 178 | times: 1 179 | - xpath: "//*[contains(name(), 'Button') and contains(@name, '登 录')]" 180 | times: 1 181 | - xpath: ".*立即登录" 182 | times: 2 183 | - xpath: "//*[@name='登 录']" 184 | times: 2 185 | - xpath: "//*[@name='登录']" 186 | times: 2 187 | - action: "driver.swipe(0.5, 0.1, 0.5, 0.9)" 188 | xpath: "专题" 189 | times: 1 190 | - xpath: "点此.*" 191 | - xpath: "^放弃$" 192 | - xpath: "不保存" 193 | - xpath: "^确定$" 194 | - xpath: "^关闭$" 195 | - xpath: "^取消$" 196 | - xpath: "稍后再说" 197 | - xpath: "Cancel" 198 | - xpath: "这里可以.*" 199 | - xpath: ".*搬到这里.*" 200 | - xpath: "我要退出" 201 | - xpath: "tip_click_position" 202 | - xpath: "common guide icon ok" 203 | - xpath: "icon quotationinformation day" 204 | times: 1 205 | - xpath: "icon stock close" 206 | - xpath: "隐藏键盘" 207 | #一个神奇的符号 208 | - xpath: //*[@label='✕' and visible='true'] 209 | times: 10 210 | - xpath: 我知道了 211 | tagLimit: 212 | - xpath: //*[../*[@selected='true']] 213 | count: 12 214 | - xpath: //*[../../*/*[@selected='true']] 215 | count: 12 -------------------------------------------------------------------------------- /src/test/scala/com/testerhome/appcrawler/it/xueqiu_sikuli.yml: -------------------------------------------------------------------------------- 1 | --- 2 | pluginList: 3 | - "com.testerhome.appcrawler.plugin.FlowDiff" 4 | #- "com.testerhome.appcrawler.plugin.ProxyPlugin" 5 | logLevel: "TRACE" 6 | saveScreen: true 7 | showCancel: true 8 | reportTitle: AppCrawler雪球内部版 9 | screenshotTimeout: 20 10 | tagLimitMax: 2 11 | currentDriver: "android" 12 | maxTime: 10800 13 | resultDir: "" 14 | sikuliImages: "/Users/seveniruby/temp/ocr/images" 15 | capability: 16 | newCommandTimeout: 120 17 | launchTimeout: 120000 18 | platformVersion: "" 19 | platformName: "" 20 | autoWebview: "false" 21 | autoLaunch: "true" 22 | noReset: "true" 23 | fullReset: "false" 24 | dontStopAppOnReset: "true" 25 | androidCapability: 26 | deviceName: "demo" 27 | appPackage: "com.xueqiu.android" 28 | appActivity: ".view.WelcomeActivityAlias" 29 | app: "" 30 | appium: "http://127.0.0.1:4723/wd/hub" 31 | automationName: sikuli 32 | iosCapability: 33 | deviceName: "iPhone 6 Plus" 34 | bundleId: "com.xueqiu" 35 | screenshotWaitTimeout: "10" 36 | platformVersion: "9.3" 37 | autoAcceptAlerts: "true" 38 | app: "/Users/seveniruby/Library/Developer/Xcode/DerivedData/Snowball-ckpjegabufjxgxfeqyxgkmjuwmct/Build/Products/Debug-iphonesimulator/Snowball.app" 39 | appium: "http://127.0.0.1:4730/wd/hub" 40 | defineUrl: 41 | - "//*[@selected='true']/@text" 42 | - "//*[@selected='true']/@text" 43 | - "//*[contains(name(), 'NavigationBar')]/@label" 44 | #baseUrl: 45 | #- ".*MainActivity" 46 | #- ".*SNBHomeView.*" 47 | maxDepth: 2 48 | headFirst: true 49 | selectedList: 50 | - xpath: //* 51 | triggerActions: 52 | - { xpath: "//*[contains(@name, '行情灰')]", times: 1 } 53 | - { xpath: 沪深港通, times: 1} -------------------------------------------------------------------------------- /src/test/scala/com/testerhome/appcrawler/ut/AppCrawlerTest.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.ut 2 | 3 | import java.io.File 4 | 5 | import com.testerhome.appcrawler.AppCrawler 6 | import org.scalatest.FunSuite 7 | 8 | /** 9 | * Created by seveniruby on 2017/5/25. 10 | */ 11 | class AppCrawlerTest extends FunSuite{ 12 | test("parse test"){ 13 | 14 | var uri="http://xxxx.com/aa.apk" 15 | var res=AppCrawler.parsePath(uri) 16 | println(res) 17 | 18 | uri="http:\\xxxx.com/aa.apk" 19 | res=AppCrawler.parsePath(uri) 20 | println(res) 21 | 22 | 23 | uri="/Users/seveniruby/Downloads/base.apk" 24 | res=AppCrawler.parsePath(uri) 25 | println(res) 26 | 27 | 28 | uri="./project/build.properties" 29 | res=AppCrawler.parsePath(uri) 30 | println(res) 31 | 32 | 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/test/scala/com/testerhome/appcrawler/ut/DemoCrawlerSuite.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.ut 2 | 3 | import org.scalatest.FunSuite 4 | 5 | /** 6 | * Created by seveniruby on 16/8/12. 7 | */ 8 | class DemoCrawlerSuite extends FunSuite{ 9 | var name="自动遍历" 10 | override def suiteName=name 11 | 1 to 10 foreach(i=>{ 12 | test(s"xxx ${i}"){ 13 | markup("") 14 | assert(1==i) 15 | } 16 | }) 17 | 18 | 1 to 10 foreach(i=>{ 19 | test(s"xxx ignore ${i}"){ 20 | markup("") 21 | cancel("未遍历") 22 | } 23 | }) 24 | 25 | 1 to 10 foreach(i=>{ 26 | test(s"xxx ignore ${i}"){ 27 | markup("") 28 | } 29 | }) 30 | 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/test/scala/com/testerhome/appcrawler/ut/PageObjectDemo.java.ssp: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by seveniruby on 2017/1/10. 3 | */ 4 | 5 | <%@ val elements: scala.collection.mutable.ListBuffer[Map[String, Any]] %> 6 | <%@ val file:String %> 7 | 8 | import org.openqa.selenium.remote.RemoteWebElement; 9 | import io.appium.java_client.pagefactory.*; 10 | import org.openqa.selenium.support.FindBy; 11 | import org.openqa.selenium.support.FindAll; 12 | 13 | import io.appium.java_client.android.AndroidElement; 14 | import org.openqa.selenium.remote.RemoteWebElement; 15 | import io.appium.java_client.pagefactory.*; 16 | 17 | 18 | import java.util.List; 19 | 20 | 21 | public class PageObjectDemo_${file} { 22 | <% elements.foreach(element => {%> 23 | @FindBy(xpath = "${unescape(element("xpath").toString.replace("\"", "\\\""))}") 24 | private RemoteWebElement ${List(element("name"), element("content-desc"), element("text")) 25 | .filter(_.toString.nonEmpty) 26 | .mkString("_").replaceAll("[^a-zA-Z0-9_\\u4e00-\\u9fa5]", "")}; 27 | 28 | <% }) %> 29 | } 30 | -------------------------------------------------------------------------------- /src/test/scala/com/testerhome/appcrawler/ut/PageObjectDemoID.java.ssp: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by seveniruby on 2017/1/10. 3 | */ 4 | 5 | <%@ val elements: scala.collection.mutable.ListBuffer[Map[String, Any]] %> 6 | <%@ val file:String %> 7 | 8 | import org.openqa.selenium.remote.RemoteWebElement; 9 | import io.appium.java_client.pagefactory.*; 10 | import org.openqa.selenium.support.FindBy; 11 | import org.openqa.selenium.support.FindAll; 12 | 13 | import io.appium.java_client.android.AndroidElement; 14 | import org.openqa.selenium.remote.RemoteWebElement; 15 | import io.appium.java_client.pagefactory.*; 16 | 17 | 18 | import java.util.List; 19 | 20 | 21 | public class PageObjectDemo_${file} { 22 | <% elements.filter(e=>e.getOrElse("visible", "true")=="true") 23 | .filter(e=>e.getOrElse("name", "").toString.nonEmpty) 24 | .filter(e=>e.getOrElse("xpath", "").toString.contains("StatusBar")==false) 25 | .foreach(element => { 26 | %> 27 | @FindBy(id = "${unescape(element("name").toString.replace("\"", "\\\""))}") 28 | private RemoteWebElement ${List(element("name"), element("label"), element("value")).distinct 29 | .filter(_.toString.nonEmpty) 30 | .mkString("_").replaceAll("[^a-zA-Z0-9_\\u4e00-\\u9fa5]", "")}; 31 | 32 | <% }) %> 33 | 34 | <% elements.filter(e=>e.getOrElse("visible", "true")=="true") 35 | .filter(e=>{e.getOrElse("name", "").toString.isEmpty}) 36 | .filter(e=>e.getOrElse("xpath", "").toString.contains("StatusBar")==false) 37 | .foreach(element => { 38 | %> 39 | @FindBy(xpath = "${unescape(element("xpath").toString.replace("\"", "\\\""))}") 40 | private RemoteWebElement ${List(element("name"), element("label"), element("value"), element("xpath")) 41 | .filter(_.toString.nonEmpty).map(_.toString.replace("XCUIElementType", "")) 42 | .mkString("_").replaceAll("[^a-zA-Z0-9_\\u4e00-\\u9fa5]", "")}; 43 | 44 | <% }) %> 45 | } 46 | -------------------------------------------------------------------------------- /src/test/scala/com/testerhome/appcrawler/ut/SuiteToClassTest.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.ut 2 | 3 | import com.testerhome.appcrawler.SuiteToClass 4 | import org.scalatest.FunSuite 5 | 6 | class SuiteToClassTest extends FunSuite { 7 | test("class name"){ 8 | val name ="com.tencent.mobileqq-加好友-☞ Mr.never \"day \"心(571529295)" 9 | SuiteToClass.genTestCaseClass(name, "com.testerhome.appcrawler.DiffSuite", Map("suite"->name, "name"->name), "/tmp/class") 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/test/scala/com/testerhome/appcrawler/ut/TestConf.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.ut 2 | 3 | import com.testerhome.appcrawler.CommonLog 4 | import com.testerhome.appcrawler.CrawlerConf 5 | import org.scalatest.{FunSuite, Matchers} 6 | 7 | /** 8 | * Created by seveniruby on 16/8/11. 9 | */ 10 | class TestConf extends FunSuite with CommonLog with Matchers{ 11 | 12 | 13 | 14 | test("save config"){ 15 | val conf=new CrawlerConf 16 | conf.save("conf.json") 17 | } 18 | 19 | /* 20 | test("load config"){ 21 | var conf=new com.testerhome.appcrawler.CrawlerConf 22 | conf.baseUrl="xxx" 23 | println(conf.baseUrl) 24 | conf=conf.loadByJson4s("conf.json").get 25 | println(conf.baseUrl) 26 | } 27 | */ 28 | 29 | test("load config by jackson"){ 30 | var conf=new CrawlerConf 31 | conf.baseUrl=List("xxx") 32 | println(conf.baseUrl) 33 | conf.save("conf.json") 34 | conf=conf.load("conf.json") 35 | println(conf.baseUrl) 36 | assert(conf.baseUrl==List("xxx")) 37 | } 38 | 39 | 40 | 41 | test("yaml save"){ 42 | val conf=new CrawlerConf 43 | conf.waitLaunch=100 44 | val yaml=conf.toYaml() 45 | log.info(yaml) 46 | 47 | val conf2=new CrawlerConf 48 | conf2.loadYaml(yaml) 49 | conf2.waitLaunch should be equals(conf.waitLaunch) 50 | conf2.waitLaunch should be equals(100) 51 | 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/test/scala/com/testerhome/appcrawler/ut/TestDataRecord.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.ut 2 | 3 | import com.testerhome.appcrawler.CommonLog 4 | import com.testerhome.appcrawler.DataRecord 5 | import org.scalatest.FunSuite 6 | 7 | /** 8 | * Created by seveniruby on 16/8/25. 9 | */ 10 | class TestDataRecord extends FunSuite with CommonLog{ 11 | test("diff int"){ 12 | val stringDiff=new DataRecord() 13 | stringDiff.append(22) 14 | Thread.sleep(1000) 15 | stringDiff.append(33333) 16 | log.info(stringDiff.isDiff()) 17 | log.info(stringDiff.intervalMS()) 18 | } 19 | 20 | test("test interval"){ 21 | val diff=new DataRecord 22 | assert(0==diff.intervalMS(), diff) 23 | diff.append("0") 24 | Thread.sleep(500) 25 | diff.append("500") 26 | assert(diff.intervalMS()>=500, diff) 27 | Thread.sleep(2000) 28 | diff.append("2000") 29 | assert(diff.intervalMS()>=2000, diff) 30 | assert(diff.intervalMS()<=2200, diff) 31 | 32 | 33 | 34 | } 35 | 36 | test("diff first"){ 37 | val stringDiff=new DataRecord 38 | assert(false==stringDiff.isDiff, stringDiff) 39 | stringDiff.append("xxxx") 40 | assert(false==stringDiff.isDiff, stringDiff) 41 | stringDiff.append("3333") 42 | assert(true==stringDiff.isDiff, stringDiff) 43 | stringDiff.append("3333") 44 | assert(false==stringDiff.isDiff, stringDiff) 45 | 46 | 47 | 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/test/scala/com/testerhome/appcrawler/ut/TestElementStore.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.ut 2 | 3 | import com.testerhome.appcrawler.{CommonLog, DataObject, URIElement} 4 | import com.testerhome.appcrawler._ 5 | import org.scalatest.{FunSuite, Matchers} 6 | 7 | /** 8 | * Created by seveniruby on 16/9/17. 9 | */ 10 | class TestElementStore extends FunSuite with Matchers with CommonLog{ 11 | test("save to yaml"){ 12 | val store=new URIElementStore 13 | 14 | val element_1=URIElement("a", "b", "c", "d", "e") 15 | val info_1=new ElementInfo() 16 | info_1.element=element_1 17 | info_1.action=ElementStatus.Skipped 18 | 19 | 20 | val element_2=URIElement("aa", "bb", "cc", "dd", "ee") 21 | val info_2=new ElementInfo() 22 | info_2.element=element_2 23 | info_2.action=ElementStatus.Clicked 24 | 25 | store.elementStore ++= scala.collection.mutable.Map( 26 | element_1.toString->info_1, 27 | element_2.toString->info_2 28 | ) 29 | 30 | store.clickedElementsList.append(element_2) 31 | 32 | log.info(store) 33 | val str=TData.toYaml(store) 34 | log.info(str) 35 | val store2=TData.fromYaml[URIElementStore](str) 36 | log.info(store2) 37 | val str2=TData.toYaml(store2) 38 | 39 | 40 | str should be equals str2 41 | store should be equals store2 42 | 43 | } 44 | 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/test/scala/com/testerhome/appcrawler/ut/TestGA.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.ut 2 | 3 | import com.brsanthu.googleanalytics.{GoogleAnalytics, PageViewHit} 4 | import org.apache.log4j.{BasicConfigurator, Level, Logger} 5 | import org.scalatest.FunSuite 6 | 7 | /** 8 | * Created by seveniruby on 16/2/27. 9 | */ 10 | class TestGA extends FunSuite{ 11 | test("google analyse"){ 12 | println("ga start") 13 | 14 | BasicConfigurator.configure() 15 | Logger.getRootLogger().setLevel(Level.WARN) 16 | val ga = new GoogleAnalytics("UA-74406102-1") 17 | 1 to 10 foreach(x=>{ 18 | ga.postAsync(new PageViewHit(s"http://appcrawler.io/demo${x}", "test")) 19 | }) 20 | Thread.sleep(10000) 21 | 22 | 1 to 10 foreach(x=>{ 23 | ga.postAsync(new PageViewHit(s"http://appcrawler.io/dem1${x}", "test")) 24 | }) 25 | 26 | Thread.sleep(10000) 27 | 1 to 10 foreach(x=>{ 28 | ga.postAsync(new PageViewHit(s"http://appcrawler.io/dem2${x}", "test")) 29 | }) 30 | //ga.post(new PageViewHit("http://appcrawler.io/test2", "test")) 31 | println("ga end") 32 | 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/test/scala/com/testerhome/appcrawler/ut/TestGetClassFile.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.ut 2 | 3 | import com.testerhome.appcrawler.plugin.FlowDiff 4 | import com.testerhome.appcrawler.{DiffSuite, Report} 5 | import org.apache.commons.io.FileUtils 6 | import org.scalatest.Checkpoints.Checkpoint 7 | import org.scalatest.{FunSuite, Matchers} 8 | 9 | /** 10 | * Created by seveniruby on 16/9/27. 11 | */ 12 | class TestGetClassFile extends FunSuite with Matchers{ 13 | 14 | 15 | 16 | test("test checkpoints"){ 17 | markup { 18 | """ 19 | |dddddddd 20 | """.stripMargin 21 | } 22 | markup("xxxx") 23 | val cp = new Checkpoint() 24 | val (x, y) = (1, 2) 25 | cp { x should be < 0 } 26 | cp { y should be > 9 } 27 | cp.reportAll() 28 | } 29 | 30 | test("test markup"){ 31 | markup { 32 | """ 33 | |dddddddd 34 | """.stripMargin 35 | } 36 | markup("xxxx") 37 | 38 | } 39 | 40 | test("get class file"){ 41 | val location=classOf[DiffSuite].getProtectionDomain.getCodeSource.getLocation 42 | println(location) 43 | val f=getClass.getResource("/com/xueqiu/qa/appcrawler/ut/TestDiffReport.class").getFile 44 | println(f) 45 | FileUtils.copyFile(new java.io.File(f), new java.io.File("/tmp/1.class")) 46 | 47 | 48 | 49 | println(getClass.getClassLoader.getResources("com/xueqiu/qa/appcrawler/ut/TestDiffReport.class")) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/test/scala/com/testerhome/appcrawler/ut/TestJUnit.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.ut 2 | 3 | import org.scalatest.junit.JUnitSuite 4 | 5 | import scala.collection.mutable.ListBuffer 6 | import org.junit.Test 7 | import org.junit.Before 8 | import io.qameta.allure.Allure 9 | import io.qameta.allure.model.{Status, TestResult} 10 | 11 | class TestJUnit extends JUnitSuite { 12 | 13 | var sb: StringBuilder = _ 14 | var lb: ListBuffer[String] = _ 15 | 16 | @Before def initialize() { 17 | sb = new StringBuilder("ScalaTest is ") 18 | lb = new ListBuffer[String] 19 | } 20 | 21 | @Test def verifyEasy() { 22 | sb.append("easy!") 23 | assert(sb.toString === "ScalaTest is easy!") 24 | assert(lb.isEmpty) 25 | lb += "sweet" 26 | } 27 | 28 | @Test def verifyFun() { 29 | sb.append("fun!") 30 | assert(sb.toString === "ScalaTest is fun!") 31 | assert(lb.isEmpty) 32 | 33 | } 34 | 35 | @Test 36 | def testAllure(): Unit = { 37 | println(Allure.getLifecycle.startTestCase("testcase")) 38 | Allure.getLifecycle.writeTestCase("uuid") 39 | val result=new TestResult() 40 | result.setUuid("testcase") 41 | result.setStatus(Status.PASSED) 42 | Allure.getLifecycle.scheduleTestCase(result) 43 | 44 | //Allure.addDescription("test allure") 45 | //Allure.addAttachment("file", "file content") 46 | val link = new io.qameta.allure.model.Link() 47 | link.setName("link demo") 48 | link.setUrl("http://www.baidu.com") 49 | //Allure.addLinks(link) 50 | 51 | Allure.getLifecycle.stopTestCase("testcase") 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/test/scala/com/testerhome/appcrawler/ut/TestJUnit5.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.ut 2 | 3 | import org.junit.jupiter.api.{Test, TestFactory} 4 | import org.junit.jupiter.api.Assertions._ 5 | import org.junit.jupiter.api.DynamicTest.dynamicTest 6 | import org.junit.jupiter.api.DynamicTest 7 | import java.util 8 | 9 | import io.qameta.allure.Description 10 | 11 | 12 | class TestJUnit5 { 13 | @Test 14 | @Description("Some detailed test description") 15 | def x(): Unit ={ 16 | assertTrue(1==2) 17 | } 18 | 19 | @TestFactory 20 | def dynamicTestsFromCollection: util.Collection[DynamicTest] = { 21 | util.Arrays.asList( 22 | dynamicTest("1st dynamic test", () => assertTrue(true)), 23 | dynamicTest("2nd dynamic test", () => assertEquals(4, 2 * 2))) 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/test/scala/com/testerhome/appcrawler/ut/TestJava.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.ut 2 | 3 | import com.testerhome.appcrawler.Demo 4 | import org.scalatest.FunSuite 5 | 6 | class TestJava extends FunSuite{ 7 | test("test java code"){ 8 | val d=new Demo(); 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/test/scala/com/testerhome/appcrawler/ut/TestReportPlugin.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.ut 2 | 3 | import com.testerhome.appcrawler.plugin.ReportPlugin 4 | import com.testerhome.appcrawler.{CommonLog, URIElement} 5 | import com.testerhome.appcrawler._ 6 | import com.testerhome.appcrawler.plugin.ReportPlugin 7 | import org.scalatest.FunSuite 8 | import org.scalatest.tools.Runner 9 | 10 | /** 11 | * Created by seveniruby on 16/8/12. 12 | */ 13 | class TestReportPlugin extends FunSuite with CommonLog{ 14 | test("gen suite"){ 15 | val report=new ReportPlugin() 16 | val crawler=new Crawler() 17 | report.setCrawer(crawler) 18 | 19 | val element_1=URIElement("a", "b", "c", "d", "e") 20 | val info_1=new ElementInfo() 21 | info_1.element=element_1 22 | info_1.action=ElementStatus.Skipped 23 | 24 | 25 | val element_2=URIElement("aa", "bb", "cc", "dd", "ee") 26 | val info_2=new ElementInfo() 27 | info_2.element=element_2 28 | info_2.action=ElementStatus.Clicked 29 | 30 | val elementsStore=scala.collection.mutable.Map( 31 | element_1.toString->info_1, 32 | element_2.toString->info_2 33 | ) 34 | val store=new URIElementStore 35 | store.elementStore ++= elementsStore 36 | report.saveTestCase(store, "/tmp/") 37 | 38 | } 39 | 40 | test("run"){ 41 | 42 | val report=new ReportPlugin() 43 | val crawler=new Crawler() 44 | report.setCrawer(crawler) 45 | 46 | //Runner.run(Array("-R", "target", "-w", "com.testerhome.appcrawler.report", "-o", "-u", "target/test-reports", "-h", "target/test-reports")) 47 | Runner.run(Array( 48 | "-R", "/Users/seveniruby/projects/LBSRefresh/target", 49 | "-w", "com.testerhome.appcrawler", 50 | "-o", "-u", "target/test-reports", "-h", "target/test-reports")) 51 | 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/test/scala/com/testerhome/appcrawler/ut/TestRuntimes.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.ut 2 | 3 | import java.io.File 4 | 5 | import com.testerhome.appcrawler.{CommonLog, _} 6 | import com.testerhome.appcrawler.driver.AppiumClient 7 | import com.testerhome.appcrawler.plugin.{DemoPlugin, Plugin} 8 | import org.scalatest.FunSuite 9 | 10 | import scala.reflect.internal.util.ScalaClassLoader.URLClassLoader 11 | import scala.tools.nsc.{Global, Settings} 12 | 13 | /** 14 | * Created by seveniruby on 16/8/10. 15 | */ 16 | class TestRuntimes extends FunSuite with CommonLog{ 17 | 18 | val fileName="/Users/seveniruby/projects/LBSRefresh/iOS_20160813203343/AppCrawler_8.scala" 19 | test("MiniAppium dsl"){ 20 | // AppiumClient.dsl("hello(\"seveniruby\", 30000)") 21 | // AppiumClient.dsl("hello(\"ruby\", 30000)") 22 | // AppiumClient.dsl(" hello(\"seveniruby\", 30000)") 23 | // AppiumClient.dsl("hello(\"seveniruby\", 30000 ) ") 24 | // AppiumClient.dsl("sleep(3)") 25 | // AppiumClient.dsl("hello(\"xxxxx\")") 26 | // AppiumClient.dsl("println(com.testerhome.appcrawler.AppCrawler.crawler.driver)") 27 | 28 | } 29 | 30 | test("compile by scala"){ 31 | Runtimes.init(new File(fileName).getParent) 32 | Runtimes.compile(List(fileName)) 33 | 34 | 35 | 36 | } 37 | 38 | test("native compile"){ 39 | 40 | 41 | val outputDir=new File(fileName).getParent 42 | 43 | val settings = new Settings() 44 | settings.deprecation.value = true // enable detailed deprecation warnings 45 | settings.unchecked.value = true // enable detailed unchecked warnings 46 | settings.outputDirs.setSingleOutput(outputDir) 47 | settings.usejavacp.value = true 48 | 49 | val global = new Global(settings) 50 | val run = new global.Run 51 | run.compile(List(fileName)) 52 | 53 | } 54 | 55 | 56 | test("imain"){ 57 | 58 | Runtimes.init() 59 | Runtimes.eval( 60 | """ 61 | |import com.testerhome.appcrawler.MiniAppium 62 | |println("xxx") 63 | |println("ddd") 64 | |MiniAppium.hello("222") 65 | """.stripMargin) 66 | 67 | 68 | } 69 | 70 | 71 | test("imain q"){ 72 | 73 | Runtimes.init() 74 | Runtimes.eval("import com.testerhome.appcrawler.MiniAppium") 75 | Runtimes.eval( 76 | """ 77 | |println("xxx") 78 | |println("ddd") 79 | |MiniAppium.hello("222") 80 | """.stripMargin) 81 | 82 | 83 | } 84 | 85 | 86 | test("imain with MiniAppium"){ 87 | 88 | Runtimes.init() 89 | Runtimes.eval("import com.testerhome.appcrawler.MiniAppium._") 90 | Runtimes.eval( 91 | """ 92 | |hello("222") 93 | |println(driver) 94 | """.stripMargin) 95 | } 96 | 97 | test("compile plugin"){ 98 | Runtimes.init() 99 | Runtimes.compile(List("src/universal/plugins/DynamicPlugin.scala")) 100 | val p=Class.forName("com.testerhome.appcrawler.plugin.DynamicPlugin").newInstance() 101 | log.info(p) 102 | 103 | 104 | } 105 | 106 | test("test classloader"){ 107 | val classPath="target/tmp/" 108 | Runtimes.init(classPath) 109 | Runtimes.compile(List("/Users/seveniruby/projects/LBSRefresh/src/universal/plugins/")) 110 | val urls=Seq(new java.io.File(classPath).toURI.toURL) 111 | val loader=new URLClassLoader(urls, ClassLoader.getSystemClassLoader) 112 | val x=loader.loadClass("AppCrawler_5").newInstance().asInstanceOf[FunSuite] 113 | log.info(x.testNames) 114 | log.info(getClass.getCanonicalName) 115 | 116 | log.info(getClass.getProtectionDomain.getCodeSource.getLocation.getPath) 117 | 118 | } 119 | 120 | test("load plugins"){ 121 | 122 | val a=new DemoPlugin() 123 | log.info(a.asInstanceOf[Plugin]) 124 | //getClass.getClassLoader.asInstanceOf[URLClassLoader].loadClass("DynamicPlugin") 125 | val plugins=Runtimes.loadPlugins("/Users/seveniruby/projects/LBSRefresh/src/universal/plugins/") 126 | plugins.foreach(log.info) 127 | 128 | } 129 | 130 | 131 | test("crawl keyword"){ 132 | Runtimes.eval("def crawl(depth:Int)=com.testerhome.appcrawler.AppCrawler.crawler.crawl(depth)") 133 | Runtimes.eval("crawl(1)") 134 | } 135 | 136 | 137 | } 138 | 139 | -------------------------------------------------------------------------------- /src/test/scala/com/testerhome/appcrawler/ut/TestSpec.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.ut 2 | 3 | import org.scalatest.FunSpec 4 | 5 | /** 6 | * Created by seveniruby on 16/8/12. 7 | */ 8 | class TestSpec extends FunSpec{ 9 | describe("A Set") { 10 | describe("when empty") { 11 | it("should have size 0") { 12 | assert(Set.empty.size == 0) 13 | } 14 | 15 | it("should produce NoSuchElementException when head is invoked") { 16 | assert(1==2) 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/test/scala/com/testerhome/appcrawler/ut/TestStringTemplate.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.ut 2 | 3 | import com.testerhome.appcrawler.CommonLog 4 | import org.scalatest.FunSuite 5 | 6 | /** 7 | * Created by seveniruby on 16/8/12. 8 | */ 9 | class TestStringTemplate extends FunSuite with CommonLog{ 10 | 11 | def genNumber(): String ={ 12 | 1 to 5 map (_.toString) mkString ("\n"+" "*4) 13 | } 14 | test("string template"){ 15 | val s= 16 | s""" 17 | |class A extends B { 18 | | test("ddddd"){ 19 | | ${genNumber()} 20 | | } 21 | |} 22 | """.stripMargin 23 | log.info(s) 24 | } 25 | 26 | test("string template from file"){ 27 | //todo: 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/test/scala/com/testerhome/appcrawler/ut/TestSuites.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.ut 2 | 3 | import org.scalatest.{FunSuite, Sequential} 4 | 5 | /** 6 | * Created by seveniruby on 2017/4/17. 7 | */ 8 | class TestSuites extends Sequential( 9 | new Demo1Suite 10 | ) 11 | 12 | class Demo1Suite extends FunSuite { 13 | test("1"){ 14 | 15 | } 16 | test("2"){ 17 | 18 | } 19 | } -------------------------------------------------------------------------------- /src/test/scala/com/testerhome/appcrawler/ut/TestThread.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.ut 2 | 3 | import com.testerhome.appcrawler.driver.AppiumClient 4 | import org.scalatest.FunSuite 5 | 6 | import scala.sys.process.Process 7 | 8 | 9 | /** 10 | * Created by seveniruby on 16/3/30. 11 | */ 12 | class TestThread extends FunSuite{ 13 | test("test start new thread and kill"){ 14 | var a=1 15 | var hello = new Thread(new Runnable { 16 | def run() { 17 | println("hello world") 18 | for(i<- 1 to 5){ 19 | Thread.sleep(1000) 20 | a+=1 21 | println(a) 22 | } 23 | println("thread end") 24 | } 25 | }) 26 | 27 | hello.start() 28 | Thread.sleep(7000) 29 | println(s"a=$a") 30 | 31 | hello = new Thread(new Runnable { 32 | def run() { 33 | println("hello world") 34 | Thread.sleep(5000) 35 | println("thread end") 36 | } 37 | }) 38 | 39 | hello.start() 40 | Thread.sleep(3000) 41 | hello.stop() 42 | } 43 | 44 | test("logger testing"){ 45 | 46 | import org.apache.log4j.{BasicConfigurator, Logger} 47 | 48 | BasicConfigurator.configure() 49 | var log=Logger.getRootLogger() 50 | log.trace("trace") 51 | log.debug("debug") 52 | log.info("info") 53 | log.warn("warnning") 54 | log.error("error") 55 | log.fatal("fatal") 56 | 57 | log=Logger.getLogger(this.getClass) 58 | log.trace("trace") 59 | log.debug("debug") 60 | log.info("info") 61 | log.warn("warnning") 62 | log.error("error") 63 | log.fatal("fatal") 64 | 65 | log=Logger.getLogger("demo") 66 | log.trace("trace") 67 | log.debug("debug") 68 | log.info("info") 69 | log.warn("warnning") 70 | log.error("error") 71 | log.fatal("fatal") 72 | 73 | 74 | } 75 | 76 | test("test slf4j"){ 77 | 78 | import org.slf4j.LoggerFactory 79 | val log = LoggerFactory.getLogger(classOf[TestThread]) 80 | log.trace("trace") 81 | log.debug("debug") 82 | log.info("info") 83 | log.warn("warnning") 84 | log.error("error") 85 | } 86 | 87 | 88 | /* 89 | test("test console"){ 90 | import scala.tools.nsc.Settings 91 | import scala.tools.nsc.interpreter.ILoop 92 | 93 | val settings=new Settings() 94 | val loop = new ILoop 95 | settings.usejavacp.value=true 96 | loop.process(settings) 97 | } 98 | */ 99 | 100 | 101 | def callbyname(count:Int =3)(callback: =>Unit): Unit ={ 102 | 1 to count foreach(x=>callback) 103 | } 104 | 105 | 106 | def callbythread(count:Int =3)(callback: =>Unit): Unit ={ 107 | 1 to count foreach(x=>{ 108 | val thread = new Thread(new Runnable { 109 | override def run(): Unit = { 110 | callback 111 | } 112 | }) 113 | thread.start() 114 | thread.join(3000) 115 | thread.stop() 116 | }) 117 | } 118 | 119 | test("test by name callback"){ 120 | println("before") 121 | callbythread(3){ 122 | println("xx start") 123 | Thread.sleep(5000) 124 | println("xx stop") 125 | } 126 | println("after") 127 | 128 | } 129 | 130 | test("executor service default"){ 131 | 132 | val pre=System.currentTimeMillis() 133 | val r=AppiumClient.asyncTask(5){ 134 | Thread.sleep(100000) 135 | "xxxx" 136 | } 137 | assert(r==None) 138 | val now=System.currentTimeMillis() 139 | println((now-pre)/1000) 140 | 141 | } 142 | 143 | 144 | test("executor service expect"){ 145 | 146 | val pre=System.currentTimeMillis() 147 | val r=AppiumClient.asyncTask(5){ 148 | Thread.sleep(1000) 149 | "xxxx" 150 | } 151 | assert(r.left.get=="xxxx") 152 | val now=System.currentTimeMillis() 153 | println((now-pre)/1000) 154 | 155 | } 156 | 157 | test("executor service Int expect"){ 158 | 159 | val pre=System.currentTimeMillis() 160 | val r=AppiumClient.asyncTask(5) { 161 | Thread.sleep(100000) 162 | 1 163 | } 164 | assert(r==None) 165 | val now=System.currentTimeMillis() 166 | println((now-pre)/1000) 167 | 168 | } 169 | 170 | test("executor service Int"){ 171 | 172 | val pre=System.currentTimeMillis() 173 | val r=AppiumClient.asyncTask(5){ 174 | Thread.sleep(1000) 175 | 1 176 | } 177 | assert(r.left.get==1) 178 | val now=System.currentTimeMillis() 179 | println((now-pre)/1000) 180 | 181 | } 182 | 183 | test("-1 async"){ 184 | val x=AppiumClient.asyncTask(-1){ 185 | println("start") 186 | Thread.sleep(6000) 187 | 3 188 | } 189 | 0 to 10 foreach{ i=> 190 | Thread.sleep(1000) 191 | println(x) 192 | } 193 | } 194 | 195 | test("appium start"){ 196 | val process=Process("appium -p 4445") 197 | val pb=process.run() 198 | val x=AppiumClient.asyncTask(10){ 199 | pb.exitValue() 200 | } 201 | println(x) 202 | Thread.sleep(20000) 203 | pb.destroy() 204 | } 205 | 206 | 207 | 208 | 209 | } 210 | -------------------------------------------------------------------------------- /src/test/scala/com/testerhome/appcrawler/ut/TestTreeNode.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.ut 2 | 3 | import com.testerhome.appcrawler.TreeNode 4 | import org.scalatest.FunSuite 5 | 6 | import scala.collection.mutable.ListBuffer 7 | 8 | /** 9 | * Created by seveniruby on 16/2/10. 10 | */ 11 | class TestTreeNode extends FunSuite{ 12 | test("generate tree"){ 13 | val root=TreeNode("root") 14 | root.appendNode(root, TreeNode("1")).appendNode(root, TreeNode("11")).appendNode(root, TreeNode("111")) 15 | root.appendNode(root, TreeNode("2")).appendNode(root, TreeNode("21")) 16 | root.appendNode(root, TreeNode("3")) 17 | root.toXml(root) 18 | 19 | } 20 | 21 | test("generate tree by list"){ 22 | val list=ListBuffer(1, 2, 3, 4, 1, 5, 6, 5, 7) 23 | TreeNode(0).generateFreeMind(list, "1.mm") 24 | } 25 | 26 | 27 | test("generate tree by list string"){ 28 | val list=ListBuffer("1", "2", "3", "4", "1", "5", "66\"66", "5", "7") 29 | TreeNode("demo").generateFreeMind(list, "2.mm") 30 | } 31 | 32 | test("append node single"){ 33 | val root=TreeNode(0) 34 | var current1=root.appendNode(root, TreeNode(1)) 35 | println(current1) 36 | var current2=current1.appendNode(root, TreeNode(2)) 37 | println(root) 38 | println(current1) 39 | println(current2) 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/test/scala/com/testerhome/appcrawler/ut/TestURIElement.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.ut 2 | 3 | import com.testerhome.appcrawler.URIElement 4 | import org.scalatest.FunSuite 5 | 6 | /** 7 | * Created by seveniruby on 16/9/29. 8 | */ 9 | class TestURIElement extends FunSuite { 10 | test("windows file name"){ 11 | val element=URIElement("", "", "", "", "//xxfxx[@index=\"11\" and @text=\"fff>>dddff\"]") 12 | println(element.toString()) 13 | } 14 | 15 | test("tag path"){ 16 | 17 | val element=URIElement("", "", "", "", "//xxfxx[@index=\"11\" and @index=\"2\" and @text=\"fff>>dddff\"]") 18 | println(element.getAncestor()) 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/test/scala/com/testerhome/appcrawler/ut/TestUtil.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.ut 2 | 3 | import java.io.File 4 | import java.io.File 5 | import java.util.jar.JarFile 6 | 7 | import com.testerhome.appcrawler.CommonLog 8 | import com.testerhome.appcrawler.plugin.{DemoPlugin, Plugin} 9 | import com.testerhome.appcrawler._ 10 | import com.testerhome.appcrawler.driver.AppiumClient 11 | import org.scalatest.FunSuite 12 | import org.xml.sax.ErrorHandler 13 | 14 | import scala.reflect.internal.settings.MutableSettings 15 | import scala.reflect.internal.util.ScalaClassLoader 16 | import scala.reflect.internal.util.ScalaClassLoader.URLClassLoader 17 | import scala.reflect.io.AbstractFile 18 | import scala.tools.nsc.util.BatchSourceFile 19 | import scala.tools.nsc.{GenericRunnerSettings, Global, Settings} 20 | import scala.tools.nsc.interpreter.IMain 21 | 22 | /** 23 | * Created by seveniruby on 16/8/10. 24 | */ 25 | class TestUtil extends FunSuite with CommonLog{ 26 | 27 | val fileName="/Users/seveniruby/projects/LBSRefresh/iOS_20160813203343/AppCrawler_8.scala" 28 | test("MiniAppium dsl"){ 29 | Util.dsl("hello(\"seveniruby\", 30000)") 30 | Util.dsl("hello(\"ruby\", 30000)") 31 | Util.dsl(" hello(\"seveniruby\", 30000)") 32 | Util.dsl("hello(\"seveniruby\", 30000 ) ") 33 | Util.dsl("sleep(3)") 34 | Util.dsl("hello(\"xxxxx\")") 35 | Util.dsl("hello(\"xxxxx\"); hello(\"double\")") 36 | Util.dsl("println(com.testerhome.appcrawler.AppCrawler.crawler.driver)") 37 | 38 | } 39 | 40 | test("MiniAppium dsl re eval"){ 41 | Util.dsl("val a=new java.util.Date") 42 | Util.dsl("val b=a") 43 | Util.dsl("val a=new java.util.Date") 44 | Util.dsl("println(a)") 45 | Util.dsl("println(b)") 46 | } 47 | 48 | test("shell"){ 49 | Util.dsl("\"12345\"") 50 | //todo: not work 51 | Util.dsl("\"sh -c 'adb devices; echo xxx;' \" !") 52 | Util.dsl(" \"sh /tmp/1.sh\"!") 53 | 54 | } 55 | 56 | test("compile by scala"){ 57 | Util.init(new File(fileName).getParent) 58 | Util.compile(List(fileName)) 59 | 60 | } 61 | 62 | test("native compile"){ 63 | 64 | 65 | val outputDir=new File(fileName).getParent 66 | 67 | val settings = new Settings() 68 | settings.deprecation.value = true // enable detailed deprecation warnings 69 | settings.unchecked.value = true // enable detailed unchecked warnings 70 | settings.outputDirs.setSingleOutput(outputDir) 71 | settings.usejavacp.value = true 72 | 73 | val global = new Global(settings) 74 | val run = new global.Run 75 | run.compile(List(fileName)) 76 | 77 | } 78 | 79 | 80 | test("imain"){ 81 | 82 | Util.init() 83 | Util.dsl( 84 | """ 85 | |import com.testerhome.appcrawler.MiniAppium 86 | |println("xxx") 87 | |println("ddd") 88 | |MiniAppium.hello("222") 89 | """.stripMargin) 90 | 91 | 92 | } 93 | 94 | 95 | test("imain q"){ 96 | 97 | Util.init() 98 | Util.dsl("import com.testerhome.appcrawler.MiniAppium") 99 | Util.dsl( 100 | """ 101 | |println("xxx") 102 | |println("ddd") 103 | |MiniAppium.hello("222") 104 | """.stripMargin) 105 | 106 | 107 | } 108 | 109 | 110 | test("imain with MiniAppium"){ 111 | 112 | Util.init() 113 | Util.dsl("import com.testerhome.appcrawler.MiniAppium._") 114 | Util.dsl( 115 | """ 116 | |hello("222") 117 | |println(driver) 118 | """.stripMargin) 119 | } 120 | 121 | test("compile plugin"){ 122 | Util.init() 123 | Util.compile(List("src/universal/plugins/DynamicPlugin.scala")) 124 | val p=Class.forName("com.testerhome.appcrawler.plugin.DynamicPlugin").newInstance() 125 | log.info(p) 126 | 127 | 128 | } 129 | 130 | test("test classloader"){ 131 | val classPath="target/tmp/" 132 | Util.init(classPath) 133 | Util.compile(List("/Users/seveniruby/projects/LBSRefresh/src/universal/plugins/")) 134 | val urls=Seq(new java.io.File(classPath).toURI.toURL) 135 | val loader=new URLClassLoader(urls, ClassLoader.getSystemClassLoader) 136 | val x=loader.loadClass("AppCrawler_5").newInstance().asInstanceOf[FunSuite] 137 | log.info(x.testNames) 138 | log.info(getClass.getCanonicalName) 139 | 140 | log.info(getClass.getProtectionDomain.getCodeSource.getLocation.getPath) 141 | 142 | } 143 | 144 | test("load plugins"){ 145 | 146 | val a=new DemoPlugin() 147 | log.info(a.asInstanceOf[Plugin]) 148 | //getClass.getClassLoader.asInstanceOf[URLClassLoader].loadClass("DynamicPlugin") 149 | val plugins=Util.loadPlugins("/Users/seveniruby/projects/LBSRefresh/src/universal/plugins/") 150 | plugins.foreach(log.info) 151 | 152 | } 153 | 154 | test("crawl keyword"){ 155 | Util.dsl("def crawl(depth:Int)=com.testerhome.appcrawler.AppCrawler.crawler.crawl(depth)") 156 | Util.dsl("crawl(1)") 157 | } 158 | 159 | 160 | } 161 | 162 | -------------------------------------------------------------------------------- /src/test/scala/com/testerhome/appcrawler/ut/scalate.ssp: -------------------------------------------------------------------------------- 1 |
    2 | #for (i <- 1 to 5) 3 |
  • ${i}
  • 4 |
  • ${unescape("<'\"")}
  • 5 | #end 6 | 7 |
--------------------------------------------------------------------------------