├── .gitignore ├── CHANGELOG.md ├── README.md ├── build.sbt ├── 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 ├── lib └── chkbugreport-0.5-215.jar ├── project ├── build.properties └── plugins.sbt └── src ├── main ├── 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 │ ├── Plugin.scala │ ├── Report.scala │ ├── ReportSuite.scala │ ├── Runtimes.scala │ ├── SuiteToClass.scala │ ├── TData.scala │ ├── Template.scala │ ├── TemplateTestCase.scala │ ├── TreeNode.scala │ ├── URIElement.scala │ ├── URIElementStore.scala │ ├── XPathUtil.scala │ ├── driver │ ├── AppiumClient.scala │ ├── MacacaDriver.scala │ └── WebDriver.scala │ └── plugin │ ├── AndroidTrace.scala │ ├── DemoPlugin.scala │ ├── FlowDiff.scala │ ├── FreeMind.scala │ ├── IDeviceScreenshot.scala │ ├── LogPlugin.scala │ ├── ProxyPlugin.scala │ ├── ReportPlugin.scala │ └── TagLimitPlugin.scala └── test ├── java ├── PageFactoryDemo.java ├── PageObjectDemo.java └── XueqiuDemo.java └── scala └── com └── testerhome └── appcrawler ├── it ├── TestAndroidTrace.scala ├── TestAppium.scala ├── TestIOS.scala ├── TestJianShu.scala ├── TestMacaca.scala ├── TestNW.scala ├── TestOCR.scala ├── TestTesterHome.scala ├── TestWebDriverAgent.scala ├── TestWebView.scala ├── TestWeixin.scala ├── TestXueQiu.scala ├── keep.yml ├── keep_test.yml ├── xueqiu_automation.yml └── xueqiu_private.yml └── ut ├── AppCrawlerTest.scala ├── DemoCrawlerSuite.scala ├── PageObjectDemo.java.ssp ├── PageObjectDemoID.java.ssp ├── TestConf.scala ├── TestCrawler.scala ├── TestDataObject.scala ├── TestDataRecord.scala ├── TestElementStore.scala ├── TestGA.scala ├── TestGetClassFile.scala ├── TestReportPlugin.scala ├── TestRuntimes.scala ├── TestSpec.scala ├── TestStringTemplate.scala ├── TestSuites.scala ├── TestThread.scala ├── TestTreeNode.scala ├── TestURIElement.scala ├── TestXPathUtil.scala └── scalate.ssp /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | project/target/ 3 | project/project/ 4 | target/ 5 | conf.json 6 | xueqiu.json 7 | *.apk 8 | *.swp 9 | -------------------------------------------------------------------------------- /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 | # 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 | * [启动参数介绍](doc/启动参数介绍.md) 98 | * [遍历控制](doc/遍历控制.md) 99 | * [Android遍历](doc/Android遍历.md) 100 | * [动作触发器](doc/动作触发器.md) 101 | * [iOS遍历](doc/iOS遍历.md) 102 | * [自动化测试结合](doc/自动化测试结合.md) 103 | * [兼容性测试](doc/兼容性测试.md) 104 | * [XPath表达式学习](doc/XPath表达式学习.md) 105 | * [插件](doc/插件.md) 106 | * [插件开发](doc/插件开发.md) 107 | * [代理插件](doc/代理插件.md) 108 | * [Log插件](doc/Log插件.md) 109 | * [TagLimit插件](doc/TagLimit插件.md) 110 | * [常见问题](doc/常见问题.md) 111 | 112 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | name := "AppCrawler" 2 | version := "2.1.2" 3 | scalaVersion := "2.12.3" 4 | 5 | libraryDependencies ++= Seq( 6 | //"org.scala-lang" % "scala-compiler" % scalaVersion.value, 7 | //"org.scala-lang" % "scala-library" % scalaVersion.value, 8 | //"org.scala-lang" % "scala-reflect" % scalaVersion.value, 9 | "io.appium" % "java-client" % "5.0.4", 10 | "org.seleniumhq.selenium" % "selenium-java" % "2.53.1" % "test", 11 | //"io.selendroid" % "selendroid" % "0.16.0", 12 | "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.8.7", 13 | "com.github.scopt" %% "scopt" % "3.5.0", 14 | "com.brsanthu" % "google-analytics-java" % "1.1.2", 15 | "org.slf4j" % "slf4j-api" % "1.7.18", 16 | "org.slf4j" % "slf4j-log4j12" % "1.7.18", 17 | //"org.slf4j" % "slf4j-simple" % "1.7.18", 18 | //"org.apache.logging.log4j" % "log4j" % "2.5", 19 | //"com.android.tools.ddms" % "ddmlib" % "24.5.0", 20 | //"org.lucee" % "xml-xerces" % "2.11.0", 21 | "com.fasterxml.jackson.dataformat" % "jackson-dataformat-yaml" % "2.8.7", 22 | "net.lightbody.bmp" % "browsermob-core" % "2.1.2", 23 | "org.lucee" % "commons-codec" % "1.10.L001", 24 | "org.jsoup" % "jsoup" % "1.9.2", 25 | "com.jayway.jsonpath" % "json-path" % "2.2.0" , 26 | "org.scalactic" %% "scalactic" % "3.0.3" , 27 | "org.scalatest" %% "scalatest" % "3.0.3" , 28 | "org.apache.directory.studio" % "org.apache.commons.io" % "2.4", 29 | "org.scalatra.scalate" %% "scalate-core" % "1.8.0", 30 | "org.apache.logging.log4j" % "log4j-core" % "2.7", 31 | "macaca.webdriver.client" % "macacaclient" % "2.0.7", 32 | "org.javassist" % "javassist" % "3.22.0-CR2", 33 | "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.8.7" , 34 | "com.fasterxml.jackson.dataformat" % "jackson-dataformat-xml" % "2.8.7" , 35 | "com.fasterxml.jackson.dataformat" % "jackson-dataformat-yaml" % "2.8.7" , 36 | "com.github.tototoshi" %% "scala-csv" % "1.3.4" , 37 | "us.codecraft" % "xsoup" % "0.3.1" , 38 | "junit" % "junit" % "4.12" % "test", 39 | "org.pegdown" % "pegdown" % "1.6.0" //html report 40 | ) 41 | 42 | //libraryDependencies ~= { _.map(_.exclude("ch.qos.logback", "logback-classic")) } 43 | 44 | enablePlugins(JavaAppPackaging) 45 | /* 46 | proguardSettings 47 | ProguardKeys.proguardVersion in Proguard := "5.2.1" 48 | inConfig(Proguard)(javaOptions in ProguardKeys.proguard := Seq("-Xmx2g")) 49 | ProguardKeys.merge in Proguard := true 50 | ProguardKeys.options in Proguard ++= Seq("-dontnote", "-dontwarn", "-ignorewarnings") 51 | ProguardKeys.options in Proguard += ProguardOptions.keepMain("com.xueqiu.qa.appcrawler.AppCrawler") 52 | ProguardKeys.mergeStrategies in Proguard += ProguardMerge.first(".*".r) 53 | ProguardKeys.mergeStrategies in Proguard += ProguardMerge.discard("META-INF/.*".r) 54 | */ 55 | 56 | assemblyJarName in assembly := "appcrawler-"+version.value+".jar" 57 | test in assembly := {} 58 | mainClass in assembly := Some("com.testerhome.appcrawler.AppCrawler") 59 | scriptClasspath := Seq("*") 60 | assemblyMergeStrategy in assembly := { 61 | case PathList("META-INF", "MANIFEST.MF") => MergeStrategy.discard 62 | case PathList("META-INF", xs @ _*)=>{ 63 | (xs map {_.toLowerCase}) match { 64 | case ps @ (x :: xs) if ps.last.endsWith(".sf") || ps.last.endsWith(".dsa") => MergeStrategy.discard 65 | case _ => MergeStrategy.first 66 | } 67 | } 68 | case x if x.matches("com.testerhome.plugin.OCR.class") => MergeStrategy.discard 69 | case x if x.matches("com.testerhome.appcrawler.plugin.AndroidTrace.class") => MergeStrategy.discard 70 | case x => { 71 | //println(x) 72 | MergeStrategy.first 73 | } 74 | } 75 | 76 | //resolvers += "oschina" at "http://maven.oschina.net/content/groups/public/" 77 | 78 | resolvers += Classpaths.typesafeReleases 79 | resolvers += Classpaths.sbtPluginReleases 80 | resolvers += Classpaths.sbtIvySnapshots 81 | resolvers += Resolver.sonatypeRepo("public") 82 | resolvers += Resolver.mavenLocal 83 | resolvers += Resolver.url("bintray-sbt-plugins", url("http://dl.bintray.com/sbt/sbt-plugin-releases"))(Resolver.ivyStylePatterns) 84 | resolvers += "spring-snapshots" at "http://repo.spring.io/snapshot/" 85 | resolvers += "central" at "http://central.maven.org/maven2/" 86 | resolvers += "central2" at "http://central.maven.org/" 87 | resolvers += "elk" at "https://artifacts.elastic.co/maven" 88 | resolvers += "Artima Maven Repository" at "http://repo.artima.com/releases/" 89 | resolvers += "bintray" at "http://dl.bintray.com/xudafeng/maven/" 90 | resolvers += Resolver.sonatypeRepo("public") 91 | resolvers += Resolver.mavenLocal 92 | //externalResolvers := Resolver.withDefaultResolvers(resolvers.value, mavenCentral =false) 93 | 94 | 95 | parallelExecution in Test := false 96 | (testOptions in Test) += Tests.Argument(TestFrameworks.ScalaTest, "-o", "-u", "target/test-reports", "-h", "target/test-reports") 97 | (testOptions in Test) += Tests.Argument(TestFrameworks.ScalaTest, "-o") 98 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/chkbugreport-0.5-215.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmlanche/AppCrawler/94bf9d28006a2a9ac9e86dabd022f1184de06d3c/lib/chkbugreport-0.5-215.jar -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 1.0.3 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/main/resources/log4j2.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmlanche/AppCrawler/94bf9d28006a2a9ac9e86dabd022f1184de06d3c/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 | test("run steps") { 18 | log.info("testcase start") 19 | val conf = crawler.conf 20 | val driver = crawler.driver 21 | 22 | val cp = new scalatest.Checkpoints.Checkpoint 23 | 24 | conf.testcase.steps.foreach(step => { 25 | 26 | 27 | if(step.xpath!=null && step.action!=null){ 28 | step.when=When(step.xpath, step.action) 29 | } 30 | if(step.when!=null) { 31 | val when = step.when 32 | val xpath = when.xpath 33 | val action = when.action 34 | 35 | driver.getListFromXPath(xpath).headOption match { 36 | case Some(v) => { 37 | val ele = URIElement(v, "Steps") 38 | crawler.doElementAction(ele, action) 39 | } 40 | case None => { 41 | log.info("not found") 42 | //用于生成steps的用例 43 | val ele = URIElement("Steps", "", "", "NOT_FOUND", xpath) 44 | crawler.doElementAction(ele, "") 45 | } 46 | } 47 | } 48 | 49 | 50 | if(step.then!=null) { 51 | step.then.foreach(existAssert => { 52 | log.debug(existAssert) 53 | cp { 54 | withClue(s"${existAssert} 不存在\n") { 55 | driver.getListFromXPath(existAssert).size should be > 0 56 | } 57 | } 58 | 59 | }) 60 | } 61 | }) 62 | 63 | 64 | cp.reportAll() 65 | log.info("finish run steps") 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /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}.%M.%L] %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("alread 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/CrawlerConf.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler 2 | 3 | import java.io.File 4 | 5 | import com.fasterxml.jackson.databind.ObjectMapper 6 | import com.fasterxml.jackson.dataformat.yaml.YAMLFactory 7 | import com.fasterxml.jackson.module.scala.DefaultScalaModule 8 | 9 | import scala.collection.mutable 10 | import scala.collection.mutable.ListBuffer 11 | import scala.io.Source 12 | 13 | /** 14 | * Created by seveniruby on 16/1/6. 15 | */ 16 | class CrawlerConf { 17 | /** 插件列表 */ 18 | var pluginList = List("com.testerhome.appcrawler.plugin.TagLimitPlugin") 19 | var logLevel = "TRACE" 20 | /** 是否截图 */ 21 | var saveScreen = true 22 | var reportTitle = "" 23 | var screenshotTimeout = 20 24 | var currentDriver = "Android" 25 | var tagLimitMax = 3 26 | var tagLimit = ListBuffer[Map[String, Any]]() 27 | //var tagLimit=scala.collection.mutable.Map[String, Int]() 28 | var showCancel = false 29 | /** 最大运行时间 */ 30 | var maxTime = 3600 * 3 31 | /** 结果目录 */ 32 | var resultDir = "" 33 | /** appium的capability通用配置 */ 34 | var capability = Map[String, Any]( 35 | "app" -> "", 36 | "platformName" -> "", 37 | "platformVersion" -> "", 38 | "deviceName" -> "demo", 39 | "noReset" -> "false", 40 | "autoWebview" -> "false", 41 | "autoLaunch" -> "true" 42 | ) 43 | /** android专属配置 最后会和capability合并 */ 44 | var androidCapability = Map[String, Any]( 45 | "appPackage" -> "", 46 | "appActivity" -> "" 47 | ) 48 | var iosCapability = Map[String, Any]( 49 | "bundleId" -> "", 50 | "autoAcceptAlerts" -> "true", 51 | "platformVersion" -> "9.2", 52 | "deviceName" -> "iPhone 6" 53 | ) 54 | var xpathAttributes = List("name", "label", "value", "resource-id", "content-desc", "index", "text") 55 | /** 用来确定url的元素定位xpath 他的text会被取出当作url因素 */ 56 | var defineUrl = List[String]() 57 | /** 设置一个起始url和maxDepth, 用来在遍历时候指定初始状态和遍历深度 */ 58 | var baseUrl = List[String]() 59 | var appWhiteList = ListBuffer[String]() 60 | /** 默认的最大深度10, 结合baseUrl可很好的控制遍历的范围 */ 61 | var maxDepth = 6 62 | /** 是否是前向遍历或者后向遍历 */ 63 | var headFirst = true 64 | /** 是否遍历WebView控件 */ 65 | var enterWebView = true 66 | /** url黑名单.用于排除某些页面 */ 67 | var urlBlackList = ListBuffer[String]() 68 | var urlWhiteList = ListBuffer[String]() 69 | 70 | var defaultBackAction = ListBuffer[String]() 71 | /** 后退按钮标记, 主要用于iOS, xpath */ 72 | var backButton = ListBuffer[String]() 73 | 74 | /** 优先遍历元素 */ 75 | var firstList = ListBuffer[String]( 76 | ) 77 | /** 默认遍历列表 */ 78 | var selectedList = ListBuffer[String]( 79 | "//*[contains(name(), 'Text')]", 80 | "//*[contains(name(), 'Image')]", 81 | "//*[contains(name(), 'Button')]", 82 | "//*[contains(name(), 'CheckBox')]" 83 | ) 84 | /** 最后遍历列表 */ 85 | var lastList = ListBuffer[String]() 86 | 87 | //包括backButton 88 | //todo: 支持正则表达式 89 | /** 黑名单列表 matches风格, 默认排除内容是2个数字以上的控件. */ 90 | var blackList = ListBuffer[String]( 91 | ".*[0-9]{2}.*" 92 | ) 93 | /** 引导规则. name, value, times三个元素组成 */ 94 | var triggerActions = ListBuffer[scala.collection.mutable.Map[String, Any]]() 95 | //todo: 用watch代替triggerActions 96 | var autoCrawl: Boolean=true 97 | var asserts = ListBuffer[Map[String, Any]]() 98 | var testcase=TestCase( 99 | name="TesterHome AppCrawler", 100 | steps = List( 101 | Step(given = null, when = null, xpath="//*", action = "driver.swipe(0.9, 0.5, 0.1, 0.5)", then=null), 102 | Step(given = null, when = null, xpath="//*", action = "driver.swipe(0.9, 0.5, 0.1, 0.5)", then=null), 103 | Step(given = null, when = null, xpath="//*", action = "driver.swipe(0.9, 0.5, 0.1, 0.5)", then=null), 104 | Step(given = null, when = null, xpath="//*", action = "driver.swipe(0.9, 0.5, 0.1, 0.5)", then=null) 105 | ) 106 | ) 107 | 108 | var beforeElementAction = ListBuffer[Map[String, String]]() 109 | var afterElementAction = ListBuffer[String]() 110 | var afterUrlFinished = ListBuffer[String]() 111 | var monkeyEvents = ListBuffer[Int]() 112 | var monkeyRunTimeSeconds = 30 113 | 114 | 115 | def loadByJson4s(file: String): Option[this.type] = { 116 | if (new java.io.File(file).exists()) { 117 | println(s"load config from ${file}") 118 | println(Source.fromFile(file).mkString) 119 | Some(TData.fromYaml[this.type](Source.fromFile(file).mkString)) 120 | } else { 121 | println(s"conf file ${file} no exist ") 122 | None 123 | } 124 | } 125 | 126 | def save(path: String): Unit = { 127 | 128 | /* 129 | //这个方法不能正确的存储utf8编码的文字 130 | implicit val formats = DefaultFormats+ FieldSerializer[this.type]() 131 | val file = new java.io.File(path) 132 | val bw = new BufferedWriter(new FileWriter(file)) 133 | log.trace(writePretty(this)) 134 | log.trace(write(this)) 135 | bw.write(writePretty(this)) 136 | bw.close() 137 | */ 138 | 139 | val file = new java.io.File(path) 140 | val mapper = new ObjectMapper() 141 | mapper.registerModule(DefaultScalaModule) 142 | mapper.writerWithDefaultPrettyPrinter().writeValue(file, this) 143 | println(mapper.writeValueAsString(this)) 144 | } 145 | 146 | def toJson(): String = { 147 | val mapper = new ObjectMapper() 148 | mapper.registerModule(DefaultScalaModule) 149 | mapper.writerWithDefaultPrettyPrinter().writeValueAsString(this) 150 | 151 | } 152 | 153 | def toYaml(): String = { 154 | val mapper = new ObjectMapper(new YAMLFactory()) 155 | mapper.registerModule(DefaultScalaModule) 156 | mapper.writerWithDefaultPrettyPrinter().writeValueAsString(this) 157 | } 158 | 159 | def loadYaml(fileName: File): CrawlerConf = { 160 | val mapper = new ObjectMapper(new YAMLFactory()) 161 | mapper.registerModule(DefaultScalaModule) 162 | mapper.readValue(fileName, classOf[CrawlerConf]) 163 | } 164 | 165 | def loadYaml(content: String): Unit = { 166 | val mapper = new ObjectMapper(new YAMLFactory()) 167 | mapper.registerModule(DefaultScalaModule) 168 | mapper.readValue(content, classOf[CrawlerConf]) 169 | } 170 | 171 | 172 | def load(file: String): CrawlerConf = { 173 | load(new File(file)).get 174 | } 175 | 176 | def load(file: File): Option[CrawlerConf] = { 177 | val content = Source.fromFile(file, "UTF-8").getLines().mkString("\n") 178 | file.getName match { 179 | case json if json.endsWith(".json") => { 180 | Some(DataObject.fromJson[CrawlerConf](content)) 181 | } 182 | case yaml if yaml.endsWith(".yml") || yaml.endsWith(".yaml") => { 183 | Some(DataObject.fromYaml[CrawlerConf](content)) 184 | } 185 | case path => { 186 | println(s"${path} not support") 187 | None 188 | } 189 | } 190 | } 191 | 192 | 193 | } 194 | 195 | 196 | case class TestCase(name:String="", steps:List[Step]=List[Step]()) 197 | case class Step(given: List[String], var when: When, then:List[String], xpath:String, action:String) 198 | case class When(xpath:String, action:String) -------------------------------------------------------------------------------- /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 | 128 | object DataObject extends DataObject -------------------------------------------------------------------------------- /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 | //todo: 暂时只用2个就足够了 11 | private val size=10 12 | def append(any: Any): Unit ={ 13 | log.info(s"append ${any}") 14 | record.append(System.currentTimeMillis()->any) 15 | } 16 | def intervalMS(): Long ={ 17 | if(record.size<2){ 18 | return 0 19 | }else { 20 | val lastRecords = record.takeRight(2) 21 | lastRecords.last._1 - lastRecords.head._1 22 | } 23 | } 24 | def isDiff(): Boolean ={ 25 | if(record.size<2){ 26 | log.info("just only record return false") 27 | return false 28 | }else { 29 | val lastRecords = record.takeRight(2) 30 | lastRecords.last._2 != lastRecords.head._2 31 | } 32 | } 33 | def last(count: Int): List[Any] ={ 34 | record.takeRight(count).map(_._2).toList 35 | } 36 | def pre(): Any ={ 37 | record.takeRight(2).head._2 38 | } 39 | def last(): Any ={ 40 | record.last._2 41 | } 42 | def pop(): Unit ={ 43 | record.remove(record.size-1) 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /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.getListFromXPath(_, XPathUtil.toDocument(elementInfo.resDom))) 33 | .flatten.map(m=>{ 34 | val ele=URIElement(m, key) 35 | ele.loc->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.getListFromXPath(_, XPathUtil.toDocument(elementInfo.resDom))) 47 | .flatten.map(m=>{ 48 | val ele=URIElement(m, key) 49 | ele.loc->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.loc} 76 | | 77 | |master=${masterElement.loc} 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.loc should equal(masterElement.loc) 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/Plugin.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler 2 | 3 | /** 4 | * Created by seveniruby on 16/1/7. 5 | */ 6 | abstract class Plugin extends CommonLog{ 7 | private var crawler: Crawler=_ 8 | def getCrawler(): Crawler ={ 9 | this.crawler 10 | } 11 | def setCrawer(crawler:Crawler): Unit ={ 12 | this.crawler=crawler 13 | } 14 | def init(crawler: Crawler): Unit ={ 15 | this.crawler=crawler 16 | log.addAppender(crawler.fileAppender) 17 | log.info(this.getClass.getName+" init") 18 | } 19 | def start(): Unit ={ 20 | 21 | } 22 | def afterUrlRefresh(url:String): Unit ={ 23 | 24 | } 25 | 26 | def beforeBack(): Unit ={ 27 | 28 | } 29 | def beforeElementAction(element: URIElement): Unit ={ 30 | 31 | } 32 | def afterElementAction(element: URIElement): Unit ={ 33 | 34 | } 35 | 36 | /** 37 | * 如果实现了请设置返回值为true 38 | * @param path 39 | * @return 40 | */ 41 | def screenshot(path:String): Boolean ={ 42 | false 43 | } 44 | 45 | def getPageSource(): String ={ 46 | "" 47 | } 48 | def stop(): Unit ={ 49 | 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /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 | def runTestCase(namespace: String=""): Unit = { 41 | var cmdArgs = Array("-R", testcaseDir, 42 | "-oF", "-u", reportPath, "-h", reportPath) 43 | 44 | if(namespace.nonEmpty){ 45 | cmdArgs++=Array("-s", namespace) 46 | } 47 | 48 | /* 49 | val testcaseDirFile=new java.io.File(testcaseDir) 50 | FileUtils.listFiles(testcaseDirFile, Array(".class"), true).map(_.split(".class").head) 51 | val suites= testcaseDirFile.list().filter(_.endsWith(".class")).map(_.split(".class").head).toList 52 | suites.map(suite => Array("-s", s"${namespace}${suite}")).foreach(array => { 53 | cmdArgs = cmdArgs ++ array 54 | }) 55 | 56 | if (suites.size > 0) { 57 | log.info(s"run ${cmdArgs.toList}") 58 | Runner.run(cmdArgs) 59 | Runtimes.reset 60 | changeTitle 61 | } 62 | */ 63 | log.info(s"run ${cmdArgs.mkString(" ")}") 64 | Runner.run(cmdArgs) 65 | changeTitle() 66 | } 67 | 68 | def changeTitle(title:String=Report.title): Unit ={ 69 | val originTitle="ScalaTest Results" 70 | val indexFile=reportPath+"/index.html" 71 | val newContent=Source.fromFile(indexFile).mkString.replace(originTitle, title) 72 | scala.reflect.io.File(indexFile).writeAll(newContent) 73 | } 74 | 75 | } 76 | 77 | object Report extends Report{ 78 | var showCancel=false 79 | var title="AppCrawler" 80 | var master="" 81 | var candidate="" 82 | var reportDir="" 83 | var store=new URIElementStore 84 | 85 | 86 | def loadResult(elementsFile: String): URIElementStore ={ 87 | DataObject.fromYaml[URIElementStore](Source.fromFile(elementsFile).mkString) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /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 scala.reflect.internal.util.ScalaClassLoader.URLClassLoader 6 | import scala.tools.nsc.interpreter.IMain 7 | import scala.tools.nsc.{Global, Settings} 8 | 9 | /** 10 | * Created by seveniruby on 16/8/13. 11 | */ 12 | class Runtimes(val outputDir:String="") extends CommonLog{ 13 | private val settingsCompile=new Settings() 14 | 15 | if(outputDir.nonEmpty){ 16 | val tempDir=new File(outputDir) 17 | if(tempDir.exists()==false){ 18 | tempDir.mkdir() 19 | } 20 | settingsCompile.outputDirs.setSingleOutput(this.outputDir) 21 | } 22 | 23 | settingsCompile.deprecation.value = true // enable detailed deprecation warnings 24 | settingsCompile.unchecked.value = true // enable detailed unchecked warnings 25 | settingsCompile.usejavacp.value = true 26 | 27 | val global = new Global(settingsCompile) 28 | val run = new global.Run 29 | 30 | private val settingsEval=new Settings() 31 | settingsEval.deprecation.value = true // enable detailed deprecation warnings 32 | settingsEval.unchecked.value = true // enable detailed unchecked warnings 33 | settingsEval.usejavacp.value = true 34 | 35 | val interpreter = new IMain(settingsEval) 36 | 37 | def compile(fileNames:List[String]): Unit ={ 38 | run.compile(fileNames) 39 | } 40 | 41 | def eval(code:String): Unit ={ 42 | interpreter.interpret(code) 43 | } 44 | def reset(): Unit ={ 45 | 46 | } 47 | 48 | 49 | 50 | } 51 | 52 | object Runtimes extends CommonLog{ 53 | var instance=new Runtimes() 54 | var isLoaded=false 55 | def apply(): Unit ={ 56 | 57 | } 58 | def eval(code:String): Unit ={ 59 | if(isLoaded==false){ 60 | log.info("first import") 61 | instance.eval("val driver=com.testerhome.appcrawler.AppCrawler.crawler.driver") 62 | instance.eval("def crawl(depth:Int)=com.testerhome.appcrawler.AppCrawler.crawler.crawl(depth)") 63 | isLoaded=true 64 | } 65 | log.info(code) 66 | instance.eval(code) 67 | log.info("eval finish") 68 | } 69 | 70 | def compile(fileNames:List[String]): Unit ={ 71 | instance.compile(fileNames) 72 | isLoaded=false 73 | } 74 | def init(classDir:String=""): Unit ={ 75 | instance=new Runtimes(classDir) 76 | } 77 | def reset(): Unit ={ 78 | 79 | } 80 | def loadPlugins(pluginDir:String=""): List[Plugin] ={ 81 | val pluginDirFile=new java.io.File(pluginDir) 82 | if(pluginDirFile.exists()==false){ 83 | log.warn(s"no ${pluginDir} directory, skip") 84 | return Nil 85 | } 86 | val pluginFiles=pluginDirFile.list().filter(_.endsWith(".scala")).toList 87 | val pluginClassNames=pluginFiles.map(_.split(".scala").head) 88 | log.info(s"find plugins in ${pluginDir}") 89 | log.info(pluginFiles) 90 | log.info(pluginClassNames) 91 | val runtimes=new Runtimes(pluginDir) 92 | runtimes.compile(pluginFiles.map(pluginDirFile.getCanonicalPath+File.separator+_)) 93 | val urls=Seq(pluginDirFile.toURI.toURL, getClass.getProtectionDomain.getCodeSource.getLocation) 94 | val loader=new URLClassLoader(urls, Thread.currentThread().getContextClassLoader) 95 | pluginClassNames.map(loader.loadClass(_).newInstance().asInstanceOf[Plugin]) 96 | } 97 | } -------------------------------------------------------------------------------- /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 | /** 14 | * 生成用例对应的class文件,用于调用scalatest执行 15 | * 16 | */ 17 | def genTestCaseClass(className: String, superClassName: String, fields: Map[String, Any], directory: String): Unit = { 18 | val pool = ClassPool.getDefault 19 | //todo: 特殊字符处理 20 | val classNameFormat = className 21 | Try(pool.makeClass(classNameFormat)) match { 22 | case Success(classNew) => { 23 | classNew.setSuperclass(pool.get(superClassName)) 24 | val init = new CtConstructor(null, classNew) 25 | val body = fields.map(field => { 26 | s"${field._1}_$$eq(${'"' + field._2.toString.replace("\"", "\\\"").replace("\\", "\\\\") + '"'}); " 27 | }).mkString("\n") 28 | init.setBody(s"{ ${body}\naddTestCase(); }") 29 | classNew.addConstructor(init) 30 | classNew.writeFile(directory) 31 | } 32 | case Failure(e) => {} 33 | } 34 | } 35 | 36 | } 37 | 38 | -------------------------------------------------------------------------------- /src/main/scala/com/testerhome/appcrawler/TData.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler 2 | 3 | 4 | import java.io.{File, StringWriter} 5 | import java.nio.charset.{Charset, StandardCharsets} 6 | import java.util 7 | import java.util.Base64 8 | import javax.xml.parsers.DocumentBuilderFactory 9 | import javax.xml.xpath.{XPathConstants, XPathFactory} 10 | 11 | import com.fasterxml.jackson.databind.{DeserializationFeature, ObjectMapper, SerializationFeature} 12 | import com.fasterxml.jackson.dataformat.xml.XmlMapper 13 | import com.fasterxml.jackson.dataformat.yaml.YAMLFactory 14 | import com.fasterxml.jackson.module.scala.DefaultScalaModule 15 | import com.github.tototoshi.csv.CSVReader 16 | import com.jayway.jsonpath.{Configuration, JsonPath} 17 | import com.sun.org.apache.xml.internal.dtm.ref.DTMNodeList 18 | import net.lightbody.bmp.core.har.Har 19 | import net.minidev.json.JSONArray 20 | import org.apache.commons.io.IOUtils 21 | 22 | import scala.collection.mutable 23 | import scala.collection.mutable.ArrayBuffer 24 | import scala.reflect.ClassTag 25 | import scala.reflect.ClassTag 26 | import scala.reflect._ 27 | import org.jsoup.Jsoup 28 | import org.jsoup.nodes.{Document, Element} 29 | import org.jsoup.select.Elements 30 | import org.w3c.dom.NodeList 31 | import us.codecraft.xsoup.Xsoup 32 | 33 | import scala.collection.JavaConversions._ 34 | import scala.io.Source 35 | import collection.JavaConverters._ 36 | 37 | /** 38 | * Created by seveniruby on 16/8/13. 39 | */ 40 | object TData { 41 | 42 | private val factory=DocumentBuilderFactory.newInstance 43 | private val builder=factory.newDocumentBuilder() 44 | private val xpathObject=XPathFactory.newInstance().newXPath() 45 | 46 | private val defaultJsonConfig = Configuration.defaultConfiguration() 47 | defaultJsonConfig.addOptions(com.jayway.jsonpath.Option.DEFAULT_PATH_LEAF_TO_NULL) 48 | //defaultJsonConfig.addOptions(com.jayway.jsonpath.Option.ALWAYS_RETURN_LIST) 49 | //JsonPath.using(defaultJsonConfig) 50 | 51 | def toYaml(data: Any): String = { 52 | val mapper = new ObjectMapper(new YAMLFactory()) 53 | mapper.registerModule(DefaultScalaModule) 54 | mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) 55 | mapper.writerWithDefaultPrettyPrinter().writeValueAsString(data) 56 | } 57 | 58 | def fromYaml[T: ClassTag](data: String): T = { 59 | val mapper = new ObjectMapper(new YAMLFactory()) 60 | mapper.registerModule(DefaultScalaModule) 61 | mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); 62 | mapper.readValue(data, classTag[T].runtimeClass.asInstanceOf[Class[T]]) 63 | } 64 | 65 | 66 | def toJson(data: Any): String = { 67 | val mapper = new ObjectMapper() 68 | mapper.registerModule(DefaultScalaModule) 69 | mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) 70 | mapper.writerWithDefaultPrettyPrinter().writeValueAsString(data) 71 | } 72 | 73 | 74 | def fromJson[T: ClassTag](str: String): T = { 75 | val mapper = new ObjectMapper() 76 | mapper.registerModule(DefaultScalaModule) 77 | mapper.readValue(str, classTag[T].runtimeClass.asInstanceOf[Class[T]]) 78 | } 79 | def pretty(jsonString: String): String ={ 80 | val mapper = new ObjectMapper() 81 | mapper.registerModule(DefaultScalaModule) 82 | mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) 83 | 84 | val jsonObject=mapper.readValue(jsonString, classOf[java.lang.Object]) 85 | mapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonObject) 86 | } 87 | 88 | def toXML(data: Any, root:String="xml"): String = { 89 | val mapper = new XmlMapper() 90 | mapper.registerModule(com.fasterxml.jackson.module.scala.DefaultScalaModule) 91 | //mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) 92 | //mapper.registerModule(DefaultScalaModule) 93 | mapper.writerWithDefaultPrettyPrinter().withRootName(root).writeValueAsString(data) 94 | } 95 | 96 | 97 | def fromXML[T: ClassTag](str: String): T = { 98 | val mapper = new XmlMapper() 99 | mapper.registerModule(com.fasterxml.jackson.module.scala.DefaultScalaModule) 100 | mapper.readValue(str, classTag[T].runtimeClass.asInstanceOf[Class[T]]) 101 | } 102 | 103 | def fromHTML(str: String): Map[String, Any] = { 104 | val node = Jsoup.parse(str) 105 | 106 | def lift(node: Element): Map[String, Any] = node match { 107 | case doc: Document => 108 | Map[String, Any]( 109 | "head" -> lift(doc.head), 110 | "body" -> lift(doc.body) 111 | ) 112 | 113 | case doc: Element => { 114 | val children: Elements = doc.children 115 | val attributes = 116 | doc.attributes.asList map { attribute => 117 | attribute.getKey -> attribute.getValue 118 | } toMap 119 | 120 | Map( 121 | "tag" -> doc.tagName, 122 | "text" -> doc.ownText, 123 | "attributes" -> attributes, 124 | "children" -> children.map(element => lift(element)) 125 | ) 126 | 127 | } 128 | } 129 | 130 | lift(node) 131 | } 132 | 133 | 134 | //扁平化 135 | def flatten(data: Map[String, Any]): mutable.Map[String, Any] = { 136 | val stack = new mutable.Stack[String]() 137 | val result = mutable.Map[String, Any]() 138 | 139 | def loop(dataKV: scala.collection.Map[String, Any]): Unit = { 140 | 141 | dataKV.foreach(data => { 142 | stack.push(data._1) 143 | data match { 144 | case (key: String, valueMap: scala.collection.Map[String, _]) => { 145 | val tag = valueMap.getOrElse("tag", "").toString 146 | val key = tag.split('.').lastOption.getOrElse(tag) 147 | if (tag.nonEmpty) { 148 | stack.push(key) 149 | } 150 | 151 | valueMap.foreach(kv => { 152 | loop(scala.collection.Map(kv._1 -> kv._2)) 153 | }) 154 | 155 | if (tag.nonEmpty) { 156 | stack.pop() 157 | } 158 | 159 | } 160 | case (key: String, values: Seq[_]) => { 161 | var index = 0 162 | values.foreach(value => { 163 | loop(Map(index.toString -> value)) 164 | index += 1 165 | }) 166 | } 167 | case (key, value: Any) => { 168 | result(stack.reverse.mkString(".")) = value 169 | } 170 | case (key, null) => { 171 | result(stack.reverse.mkString(".")) = null 172 | } 173 | 174 | } 175 | stack.pop() 176 | }) 177 | } 178 | 179 | loop(data) 180 | result 181 | } 182 | 183 | def toSchema(content:String): mutable.Map[String, String]={ 184 | val map=flatten(from(content)) 185 | val mapNew=mutable.Map[String, String]() 186 | map.map{case (k, v)=>{ 187 | v match { 188 | case null => mapNew(k)="null" 189 | case _ => mapNew(k)=v.getClass.getSimpleName 190 | } 191 | 192 | } 193 | } 194 | mapNew 195 | } 196 | 197 | //从扁平化结构重新拼装为完整结构 198 | def pushToMap(origin: mutable.Map[String, Any], keys: Array[String], value: Any): Unit = { 199 | if (keys.length == 1) { 200 | origin(keys.head) = value 201 | } else { 202 | if (origin.contains(keys.head)) { 203 | if (origin(keys.head).isInstanceOf[mutable.Map[String, Any]] == false) { 204 | origin(keys.head) = mutable.Map[String, Any]() 205 | } else { 206 | } 207 | } else { 208 | origin(keys.head) = mutable.Map[String, Any]() 209 | } 210 | pushToMap(origin(keys.head).asInstanceOf[mutable.Map[String, Any]], keys.tail, value) 211 | } 212 | } 213 | 214 | 215 | //两个nested的结构合并 216 | def deepMerge[K](map: Map[K, _], that: Map[K, _]): Map[K, _] = { 217 | (for (k <- map.keys ++ that.keys) yield { 218 | val newValue = 219 | (map.get(k), that.get(k)) match { 220 | case (Some(v), None) => v 221 | case (None, Some(v)) => v 222 | case (Some(v1), Some(v2)) => { 223 | (v1, v2) match { 224 | case (v1: Map[K, _], v2: Map[K, _]) => deepMerge(v1, v2) 225 | case (v1: List[_], v2: List[_]) => v1 ++ v2 226 | case (v1: Array[_], v2: Array[_]) => v1 ++ v2 227 | case (v1, null) => v1 228 | case _ => v2 229 | } 230 | } 231 | } 232 | k -> newValue 233 | }).toMap 234 | } 235 | 236 | //从文本中给出结构化的解析结果 237 | def from(content:String): Map[String, Any] ={ 238 | content.trim.take(10) match { 239 | case json if json.contains("{") =>{ 240 | fromJson[Map[String, Any]](content) 241 | } 242 | case html if html.toLowerCase.contains("") => { 243 | fromHTML(content) 244 | } 245 | case xml if xml.contains("<") =>{ 246 | val root=builder.parse(IOUtils.toInputStream(content)).getDocumentElement.getTagName 247 | val elements=fromXML[Map[String,Any]](content) 248 | Map(root->elements) 249 | } 250 | case _ => { 251 | Map("raw"->content) 252 | } 253 | } 254 | } 255 | 256 | //todo: 支持单个值获取 257 | //给出jsonpath的结果 258 | def jsonPath(raw:String, path:String): Any ={ 259 | val res=JsonPath.using(defaultJsonConfig).parse(raw).read[Any](path) 260 | res match{ 261 | case array: JSONArray => { 262 | array.toList 263 | } 264 | case x: Any => { 265 | res 266 | } 267 | } 268 | //JsonPath.using(defaultJsonConfig).parse(raw).read[JSONArray](path).map(_.toString).toList 269 | } 270 | 271 | //解析xpath并给出结果 272 | private def xpath2(raw:String, path:String): Any ={ 273 | val doc=Jsoup.parse(raw) 274 | Xsoup.compile(path).evaluate(doc).get 275 | } 276 | 277 | private def xpathList(raw:String, path:String, encoding:String="UTF-8"): Any = { 278 | val doc=builder.parse(IOUtils.toInputStream(raw, encoding)) 279 | val array=xpathObject.compile(path).evaluate(doc, XPathConstants.NODESET).asInstanceOf[NodeList] 280 | 0.until(array.getLength).map(i=>{ 281 | val children=array.item(i).getChildNodes 282 | 0.until(children.getLength).map(j=>children.item(j).getNodeValue) 283 | }).flatten.toList 284 | } 285 | def xpathSingle(raw:String, path:String, encoding:String="UTF-8"): Any ={ 286 | val doc=builder.parse(IOUtils.toInputStream(raw, encoding)) 287 | xpathObject.compile(path).evaluate(doc, XPathConstants.STRING) 288 | } 289 | def xpath(raw:String, path:String):Any={ 290 | path match { 291 | case single if List("[", "(").exists(c=>single.contains(c)) => xpathSingle(raw, path) 292 | case list if List("//").exists(c=>list.contains(c)) => xpathList(raw, path) 293 | case _ => xpathSingle(raw, path) 294 | } 295 | } 296 | 297 | //用于windows上坑爹的编码问题 298 | def setEncoding(): Unit ={ 299 | System.setProperty("file.encoding", "UTF-8"); 300 | val charset = classOf[Charset].getDeclaredField("defaultCharset") 301 | charset.setAccessible(true) 302 | charset.set(null, null) 303 | } 304 | 305 | 306 | def har2string(har:Har): String ={ 307 | val writer=new StringWriter() 308 | har.writeTo(writer) 309 | writer.toString 310 | } 311 | 312 | 313 | def decodeBase64(raw: String): String = { 314 | return new String(Base64.getDecoder.decode(raw)) 315 | } 316 | 317 | def encodeBase64(raw: String): String = { 318 | val str = Base64.getEncoder.encodeToString(raw.getBytes(StandardCharsets.UTF_8)) 319 | return str 320 | } 321 | 322 | def fromCSV(file:String): Array[java.util.Map[String, String]] ={ 323 | CSVReader.open(new File(getClass.getResource(file).getPath)).allWithHeaders().map(_.asJava).toArray 324 | } 325 | def dataDriver(templateFile:String, dataFile:String, key:String="template"): Array[java.util.Map[String, String]] ={ 326 | val path=if(templateFile.head=='/'){ 327 | templateFile.takeRight(templateFile.size-1) 328 | }else{ 329 | templateFile 330 | } 331 | val content=Source.fromResource(path).mkString 332 | val csv=CSVReader.open(new File(getClass.getResource(dataFile).getPath)).allWithHeaders() 333 | csv.map(m=>{ 334 | var contentNew=content 335 | m.keys.foreach(key=>{ 336 | contentNew=contentNew.replace(s"$${${key}}", m(key)) 337 | }) 338 | (m++Map(key->contentNew)).asJava 339 | }).toArray 340 | } 341 | def dataDriver(templateFile:String, dataFile:String): Array[java.util.Map[String, String]] ={ 342 | dataDriver(templateFile, dataFile, "template") 343 | } 344 | 345 | //todo: 递归解析 346 | def toHashMap(someObject: Any): Any ={ 347 | someObject match { 348 | case kv: Map[String, _] => { 349 | val h=new java.util.HashMap[String, Any]() 350 | kv.foreach{ 351 | case (k,v)=> { 352 | h.put(k, toHashMap(v)) 353 | } 354 | } 355 | h 356 | } 357 | case list: List[_]=> { 358 | list.map(item=>{ 359 | toHashMap(item) 360 | }).toArray 361 | } 362 | case array: Array[_]=> { 363 | array.map(item=>{ 364 | toHashMap(item) 365 | }) 366 | } 367 | case _ => { 368 | someObject 369 | } 370 | } 371 | } 372 | 373 | } 374 | -------------------------------------------------------------------------------- /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=DataObject.fromJson[Map[String, String]](page).getOrElse("value", "") 23 | .asInstanceOf[Map[String, String]].getOrElse("tree", "") 24 | val doc=XPathUtil.toDocument(xml) 25 | elements("Demo")=ListBuffer[Map[String, Any]]() 26 | elements("Demo")++=XPathUtil.getListFromXPath("//*[]", doc) 27 | 28 | } 29 | def read(path:String): Unit = { 30 | 31 | //val path = "/Users/seveniruby/projects/AppCrawlerSuite/AppCrawler/android_20170109145102/elements.yml" 32 | val store = (DataObject.fromYaml[URIElementStore](Source.fromFile(path).mkString)).elementStore 33 | 34 | store.foreach(s => { 35 | val reqDom = s._2.reqDom 36 | val url = s._2.element.url 37 | if (reqDom.size != 0) { 38 | val doc = XPathUtil.toDocument(reqDom) 39 | 40 | if (elements.contains(url) == false) { 41 | elements.put(url, ListBuffer[Map[String, Any]]()) 42 | } 43 | elements(url) ++= XPathUtil.getListFromXPath("//*", doc) 44 | val tagsLimit=List("Image", "Button", "Text") 45 | elements(url) = elements(url) 46 | .filter(_.getOrElse("visible", "true")=="true") 47 | .filter(_.getOrElse("tag", "").toString.contains("StatusBar")==false) 48 | .filter(e=>tagsLimit.exists(t=>e.getOrElse("tag", "").toString.contains(t))) 49 | .distinct 50 | } 51 | 52 | }) 53 | } 54 | 55 | def write(template:String, dir:String) { 56 | val engine = new TemplateEngine 57 | elements.foreach(e => { 58 | val file:String = e._1 59 | println(s"file=${file}") 60 | e._2.foreach(m => { 61 | val name = m("name") 62 | val value = m("value") 63 | val label = m("label") 64 | val xpath = m("xpath") 65 | println(s"name=${name} label=${label} value=${value} xpath=${xpath}") 66 | }) 67 | 68 | val output = engine.layout(template, Map( 69 | "file" -> s"Template_${file.split('-').takeRight(1).head.toString}", 70 | "elements" -> elements(file)) 71 | ) 72 | println(output) 73 | 74 | val directory=new File(dir) 75 | if(directory.exists()==false){ 76 | FileUtils.forceMkdir(directory) 77 | } 78 | println(s"template source directory = ${dir}") 79 | val appdex=template.split('.').takeRight(2).head 80 | scala.reflect.io.File(s"${dir}/${file}.${appdex}").writeAll(output) 81 | 82 | }) 83 | 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /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.loc.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 | val req = XPathUtil.toDocument(ele.reqDom) 71 | val res = XPathUtil.toDocument(ele.resDom) 72 | log.debug(ele.reqDom) 73 | AppCrawler.crawler.conf.asserts.foreach(assert => { 74 | val given = assert.getOrElse("given", List[String]()).asInstanceOf[List[String]] 75 | log.info(given.map(g => XPathUtil.getListFromXPath(g, req).size)) 76 | if (given.forall(g => XPathUtil.getListFromXPath(g, req).size > 0) == true) { 77 | log.info(s"asserts match") 78 | val existAsserts = assert.getOrElse("then", List[String]()).asInstanceOf[List[String]] 79 | val cp = new scalatest.Checkpoints.Checkpoint 80 | existAsserts.foreach(existAssert => { 81 | log.debug(existAssert) 82 | cp { 83 | withClue(s"${existAssert} 不存在\n") { 84 | XPathUtil.getListFromXPath(existAssert, res).size should be > 0 85 | } 86 | } 87 | }) 88 | cp.reportAll() 89 | } else { 90 | log.info("not match") 91 | } 92 | }) 93 | 94 | AppCrawler.crawler.conf.testcase.steps.foreach(step => { 95 | if (XPathUtil.getListFromXPath(step.when.xpath, req) 96 | .map(_.getOrElse("xpath", "")) 97 | .headOption == Some(ele.element.loc) 98 | ) { 99 | log.info(s"match testcase ${ele.element.loc}") 100 | 101 | if(step.then!=null) { 102 | val cp = new scalatest.Checkpoints.Checkpoint 103 | step.then.foreach(existAssert => { 104 | log.debug(existAssert) 105 | cp { 106 | withClue(s"${existAssert} 不存在\n") { 107 | XPathUtil.getListFromXPath(existAssert, res).size should be > 0 108 | } 109 | } 110 | }) 111 | cp.reportAll() 112 | } 113 | } else { 114 | log.info("not match") 115 | } 116 | }) 117 | 118 | } 119 | case ElementStatus.Ready => { 120 | cancel(s"${ele.action} not click") 121 | } 122 | case ElementStatus.Skipped => { 123 | cancel(s"${ele.action} skipped") 124 | } 125 | } 126 | 127 | } 128 | }) 129 | } 130 | } 131 | 132 | object TemplateTestCase extends CommonLog { 133 | def saveTestCase(store: URIElementStore, resultDir: String): Unit = { 134 | log.info("save testcase") 135 | Report.reportPath = resultDir 136 | Report.testcaseDir = Report.reportPath + "/tmp/" 137 | //为了保持独立使用 138 | val path = new java.io.File(resultDir).getCanonicalPath 139 | 140 | val suites = store.elementStore.map(x => x._2.element.url).toList.distinct 141 | suites.foreach(suite => { 142 | log.info(s"gen testcase class ${suite}") 143 | //todo: 基于规则的多次点击事件只会被保存到一个状态中. 需要区分 144 | SuiteToClass.genTestCaseClass( 145 | suite, 146 | "com.testerhome.appcrawler.TemplateTestCase", 147 | Map("uri" -> suite, "name" -> suite), 148 | Report.testcaseDir 149 | ) 150 | }) 151 | } 152 | 153 | } 154 | -------------------------------------------------------------------------------- /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 org.apache.commons.lang3.StringUtils 4 | 5 | import scala.collection.immutable 6 | 7 | /** 8 | * Created by seveniruby on 15/12/18. 9 | */ 10 | case class URIElement(url: String="", tag: String="", id: String="", name: String="", loc:String="") { 11 | //用来代表唯一的控件, 每个特定的命名控件只被点击一次. 所以这个element的构造决定了控件是否可被点击多次. 12 | //比如某个输入框被命名为url=xueqiu id=input, 那么就只能被点击一次 13 | //如果url修改为url=xueqiu/xxxActivity id=input 就可以被点击多次 14 | //定义url是遍历的关键. 这是一门艺术 15 | 16 | /** 17 | * 可被当作文件名的唯一标记 18 | * @return 19 | */ 20 | def toFileName(): String ={ 21 | //url_[parent id]-tag-id 22 | s"${url}_${"\"([^/0-9][^\" =]*)\"".r.findAllMatchIn(loc).map(_.subgroups).toList.flatten. 23 | map(_.split("/").lastOption.getOrElse("")).mkString("-")}" 24 | .replaceAll("[\\\\/?\"*<>\\|\n ]", ".") 25 | .replace("android.widget.", "") 26 | .take(100) 27 | } 28 | 29 | /** 30 | * 唯一的定位标记 31 | * @return 32 | */ 33 | def toLoc(): String ={ 34 | s"${url}\t${loc}\t${tag}\t${id}\t${name}" 35 | } 36 | 37 | /** 38 | * 提取元素的tag组成的路径 39 | * @return 40 | */ 41 | def toTagPath(): String ={ 42 | //todo: 将来保存到URIElement中 43 | //相同url下的相同元素类型控制点击额度 44 | s"${url}_${loc.replaceAll("@index=\"[0-9]*\"", "")}" 45 | } 46 | 47 | override def toString: String = { 48 | s"${this.url}_${this.loc}" 49 | } 50 | 51 | def hash(s:String)={ 52 | val m = java.security.MessageDigest.getInstance("MD5") 53 | val b = s.getBytes("UTF-8") 54 | m.update(b,0,b.length) 55 | new java.math.BigInteger(1,m.digest()).toString(16) 56 | } 57 | 58 | } 59 | 60 | object URIElement { 61 | //def apply(url: String, tag: String, id: String, name: String, loc: String = ""): UrlElement = new UrlElement(url, tag, id, name, loc) 62 | 63 | def apply(x:scala.collection.Map[String, Any], uri:String): URIElement = { 64 | val tag = x.getOrElse("tag", "NoTag").toString 65 | 66 | //name为Android的description/text属性, 或者iOS的value属性 67 | //appium1.5已经废弃findElementByName 68 | val name = x.getOrElse("value", "").toString.replace("\n", "\\n").take(30) 69 | //name为id/name属性. 为空的时候为value属性 70 | 71 | //id表示android的resource-id或者iOS的name属性 72 | val id = x.getOrElse("name", "").toString.split('/').last 73 | val loc = x.getOrElse("xpath", "").toString 74 | URIElement(uri, tag, id, name, loc) 75 | } 76 | 77 | /* def apply(x: scala.collection.immutable.Map[String, Any], uri:String=""): UrlElement = { 78 | apply(scala.collection.mutable.Map[String, Any]()++x, uri) 79 | }*/ 80 | } -------------------------------------------------------------------------------- /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 | 38 | def saveElement(element: URIElement): Unit = { 39 | if(elementStore.contains(element.toString)==false){ 40 | elementStore(element.toString)=ElementInfo() 41 | elementStore(element.toString).element=element 42 | } 43 | if (elementStore.contains(element.toString) == false) { 44 | elementStore(element.toString).action=ElementStatus.Clicked 45 | AppCrawler.log.info(s"first found ${element}") 46 | } 47 | } 48 | 49 | 50 | def saveReqHash(hash: String = ""): Unit = { 51 | val head = clickedElementsList.last.toString 52 | if(elementStore(head).reqHash.isEmpty){ 53 | AppCrawler.log.info(s"save reqHash to ${clickedElementsList.size-1}") 54 | elementStore(head).reqHash=hash 55 | } 56 | } 57 | 58 | def saveResHash(hash: String = ""): Unit = { 59 | val head = clickedElementsList.last.toString 60 | if(elementStore(head).resHash.isEmpty){ 61 | AppCrawler.log.info(s"save resHash to ${clickedElementsList.size-1}") 62 | elementStore(head).resHash=hash 63 | } 64 | } 65 | 66 | 67 | def saveReqDom(dom: String = ""): Unit = { 68 | val head = clickedElementsList.last.toString 69 | if(elementStore(head).reqDom.isEmpty){ 70 | AppCrawler.log.info(s"save reqDom to ${clickedElementsList.size-1}") 71 | elementStore(head).reqDom=dom 72 | } 73 | } 74 | 75 | def saveResDom(dom: String = ""): Unit = { 76 | val head = clickedElementsList.last.toString 77 | if(elementStore(head).resDom.isEmpty){ 78 | AppCrawler.log.info(s"save resDom to ${clickedElementsList.size-1}") 79 | elementStore(head).resDom=dom 80 | } 81 | } 82 | 83 | 84 | 85 | def saveReqImg(imgName:String): Unit = { 86 | val head = clickedElementsList.last.toString 87 | if (elementStore(head).reqImg.isEmpty) { 88 | AppCrawler.log.info(s"save reqImg ${imgName} to ${clickedElementsList.size - 1}") 89 | elementStore(head.toString).reqImg = imgName 90 | } 91 | } 92 | 93 | 94 | def saveResImg(imgName:String): Unit = { 95 | val head = clickedElementsList.last.toString 96 | if (elementStore(head).resImg.isEmpty) { 97 | AppCrawler.log.info(s"save resImg ${imgName} to ${clickedElementsList.size - 1}") 98 | elementStore(head).resImg = imgName.split('.').dropRight(2).mkString(".")+".clicked.png" 99 | } 100 | } 101 | 102 | def getLastResponseImage(): Unit ={ 103 | 104 | } 105 | 106 | 107 | def isDiff(): Boolean = { 108 | val currentElement = clickedElementsList.last 109 | elementStore(currentElement.toString).reqHash!=elementStore(currentElement.toString).resHash 110 | } 111 | 112 | 113 | def isClicked(ele: URIElement): Boolean = { 114 | if (elementStore.contains(ele.toString)) { 115 | elementStore(ele.toString).action == ElementStatus.Clicked 116 | } else { 117 | AppCrawler.log.trace(s"element=${ele.toLoc()} first show, need click") 118 | false 119 | } 120 | } 121 | 122 | def isSkiped(ele: URIElement): Boolean = { 123 | if (elementStore.contains(ele.toString)) { 124 | elementStore(ele.toString).action == ElementStatus.Skipped 125 | } else { 126 | AppCrawler.log.trace(s"element=${ele.toLoc()} first show, need click") 127 | false 128 | } 129 | } 130 | 131 | 132 | } 133 | 134 | object ElementStatus extends Enumeration { 135 | val Ready, Clicked, Skipped = Value 136 | } 137 | 138 | case class ElementInfo( 139 | var reqDom: String = "", 140 | var resDom: String = "", 141 | var reqHash: String = "", 142 | var resHash: String = "", 143 | var reqImg:String="", 144 | var resImg:String="", 145 | var clickedIndex: Int = -1, 146 | var action: ElementStatus.Value = ElementStatus.Ready, 147 | var element: URIElement = URIElement("Init", "", "", "", "") 148 | ) -------------------------------------------------------------------------------- /src/main/scala/com/testerhome/appcrawler/XPathUtil.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler 2 | 3 | import java.io.{ByteArrayInputStream, StringWriter} 4 | import java.nio.charset.StandardCharsets 5 | import javax.xml.parsers.{DocumentBuilder, DocumentBuilderFactory} 6 | import javax.xml.xpath.{XPath, XPathConstants, XPathFactory} 7 | 8 | import com.sun.org.apache.xml.internal.serialize.{XMLSerializer, OutputFormat} 9 | 10 | //import org.apache.xml.serialize.{OutputFormat, XMLSerializer} 11 | import org.w3c.dom.{Attr, Document, Node, NodeList} 12 | 13 | import scala.collection.mutable 14 | import scala.collection.mutable.ListBuffer 15 | 16 | /** 17 | * Created by seveniruby on 16/3/26. 18 | */ 19 | object XPathUtil extends CommonLog { 20 | var xpathExpr=List("name", "label", "value", "resource-id", "content-desc", "class", "text", "index") 21 | 22 | def toDocument(raw: String): Document = { 23 | val builderFactory: DocumentBuilderFactory = DocumentBuilderFactory.newInstance() 24 | val builder: DocumentBuilder = builderFactory.newDocumentBuilder() 25 | //todo: appium有bug, 会返回&#非法字符. 需要给appium打补丁 26 | val document: Document = builder.parse( 27 | new ByteArrayInputStream( 28 | //todo: 替换可能存在问题 29 | raw.replaceAll("[\\x00-\\x1F]", "").replace("&#", xml.Utility.escape("&#")).getBytes(StandardCharsets.UTF_8))) 30 | document 31 | } 32 | 33 | def toPrettyXML(raw: String): String = { 34 | val document = toDocument(raw) 35 | val format = new OutputFormat(document); //document is an instance of org.w3c.dom.Document 36 | format.setLineWidth(65) 37 | format.setIndenting(true) 38 | format.setIndent(2) 39 | val out = new StringWriter() 40 | val serializer = new XMLSerializer(out, format) 41 | serializer.serialize(document) 42 | out.toString 43 | } 44 | 45 | def setXPathExpr(expr:List[String]): Unit ={ 46 | xpathExpr=expr 47 | } 48 | /** 49 | * 从属性中获取xpath的唯一表达式 50 | * 51 | * @param attributes 52 | * @return 53 | */ 54 | def getXPathFromAttributes(attributes: ListBuffer[Map[String, String]]): String = { 55 | var xpath = attributes.takeRight(4).map(attribute => { 56 | var newAttribute = attribute 57 | //如果有值就不需要path了, 基本上两层xpath定位即可唯一 58 | xpathExpr.foreach(key => { 59 | if (newAttribute.getOrElse(key, "").isEmpty) { 60 | newAttribute = newAttribute - key 61 | } else { 62 | newAttribute = newAttribute - "path" 63 | } 64 | }) 65 | 66 | //如果label和name相同且非空 取一个即可 67 | if (newAttribute.getOrElse("name", "") == newAttribute.getOrElse("label", "")) { 68 | newAttribute = newAttribute - "name" 69 | } 70 | if (newAttribute.getOrElse("content-desc", "") == newAttribute.getOrElse("resource-id", "")) { 71 | newAttribute = newAttribute - "content-desc" 72 | } 73 | if(newAttribute.getOrElse("resource-id", "").nonEmpty){ 74 | newAttribute=Map("resource-id"-> newAttribute.getOrElse("resource-id", "") ) 75 | } 76 | 77 | 78 | var xpathSingle = newAttribute.map(kv => { 79 | //todo: appium的bug. 如果控件内有换行getSource会自动去掉换行. 但是xpath表达式里面没换行会找不到元素 80 | //todo: 需要帮appium打补丁 81 | 82 | kv._1 match { 83 | case "tag" => "" 84 | //case "index" => "" 85 | case "name" if kv._2.size>50 => "" 86 | //todo: 优化长文本的展示 87 | case "text" if newAttribute("tag").contains("Button")==false && kv._2.length>10 => "" 88 | case key if xpathExpr.contains(key) && kv._2.nonEmpty => s"@${kv._1}=" + "\"" + kv._2.replace("\"", "\\\"") + "\"" 89 | case _ => "" 90 | } 91 | /* 92 | if (kv._1 != "tag") { 93 | if (kv._1 == "name" && kv._2.size > 50) { 94 | log.trace(s"name size too long ${kv._2.size}>20") 95 | "" 96 | } 97 | //只有按钮才需要记录文本, 文本框很容易变化, 不需要记录 98 | else if (kv._1 == "text" && kv._2.size > 10 && newAttribute("tag").contains("Button") ) { 99 | log.trace(s"text size too long ${kv._2.size}>10") 100 | s"contains(@text, '${kv._2.split("&")(0).take(10)}')" 101 | } 102 | else { 103 | s"@${kv._1}=" + "\"" + kv._2.replace("\"", "\\\"") + "\"" 104 | } 105 | } else { 106 | "" 107 | } 108 | */ 109 | }).filter(x => x.nonEmpty).mkString(" and ") 110 | 111 | //todo: macaca的source有问题 112 | xpathSingle = if (xpathSingle.isEmpty) { 113 | s"/${attribute.getOrElse("class", attribute.getOrElse("tag", "*"))}" 114 | } else { 115 | s"/*[${xpathSingle}]" 116 | } 117 | xpathSingle 118 | } 119 | ).mkString("") 120 | if (xpath.isEmpty) { 121 | log.trace(attributes) 122 | } else { 123 | xpath = "/" + xpath 124 | } 125 | return xpath 126 | } 127 | 128 | def getAttributesFromNode(node: Node): ListBuffer[Map[String, String]] ={ 129 | val path = ListBuffer[Map[String, String]]() 130 | //递归获取路径,生成可定位的xpath表达式 131 | def getParent(node: Node): Unit = { 132 | if (node.hasAttributes) { 133 | val attributes = node.getAttributes 134 | var attributeMap = Map[String, String]() 135 | 136 | 0 until attributes.getLength foreach (i => { 137 | val kv = attributes.item(i).asInstanceOf[Attr] 138 | attributeMap ++= Map(kv.getName -> kv.getValue) 139 | }) 140 | attributeMap ++= Map("tag" -> node.getNodeName) 141 | path += attributeMap 142 | } 143 | if (node.getParentNode != null) { 144 | getParent(node.getParentNode) 145 | } 146 | } 147 | getParent(node) 148 | //返回一个从root到leaf的属性列表 149 | return path.reverse 150 | 151 | } 152 | 153 | 154 | def getListFromXPath(xpath: String, pageDom: Document): List[Map[String, Any]] = { 155 | val nodesMap = ListBuffer[Map[String, Any]]() 156 | val xPath: XPath = XPathFactory.newInstance().newXPath() 157 | val compexp = xPath.compile(xpath) 158 | //val node=compexp.evaluate(pageDom) 159 | val node = if (xpath.matches("string(.*)") || xpath.matches(".*/@[^/]*")) { 160 | compexp.evaluate(pageDom, XPathConstants.STRING) 161 | } else { 162 | compexp.evaluate(pageDom, XPathConstants.NODESET) 163 | } 164 | 165 | node match { 166 | case nodeList: NodeList => { 167 | 0 until nodeList.getLength foreach (i => { 168 | val nodeMap = mutable.Map[String, Any]() 169 | //初始化必须的字段 170 | nodeMap("name") = "" 171 | nodeMap("value") = "" 172 | nodeMap("label") = "" 173 | 174 | val node = nodeList.item(i) 175 | //如果node为.可能会异常. 不过目前不会 176 | nodeMap("tag") = node.getNodeName 177 | val path=getAttributesFromNode(node) 178 | nodeMap("xpath") = getXPathFromAttributes(path) 179 | //支持导出单个字段 180 | nodeMap(node.getNodeName) = node.getNodeValue 181 | //获得所有节点属性 182 | val nodeAttributes = node.getAttributes 183 | if (nodeAttributes != null) { 184 | 0 until nodeAttributes.getLength foreach (a => { 185 | val attr = nodeAttributes.item(a).asInstanceOf[Attr] 186 | nodeMap(attr.getName) = attr.getValue 187 | }) 188 | } 189 | 190 | //todo: 支持selendroid 191 | //如果是android 转化为和iOS相同的结构 192 | //name=resource-id label=content-desc value=text 193 | if (nodeMap.contains("resource-id")) { 194 | //todo: /结尾的会被解释为/之前的内容 195 | val arr = nodeMap("resource-id").toString.split('/') 196 | if (arr.length == 1) { 197 | nodeMap("name") = "" 198 | } else { 199 | nodeMap("name") = nodeMap("resource-id").toString.split('/').last 200 | } 201 | } 202 | if (nodeMap.contains("text")) { 203 | nodeMap("value") = nodeMap("text") 204 | } 205 | if (nodeMap.contains("content-desc")) { 206 | nodeMap("label") = nodeMap("content-desc") 207 | } 208 | 209 | if (nodeMap("xpath").toString.nonEmpty && nodeMap("value").toString().size<50) { 210 | nodesMap += (nodeMap.toMap) 211 | } else { 212 | log.trace(s"xpath error skip ${nodeMap}") 213 | } 214 | } ) 215 | } 216 | case attr:String => { 217 | //如果是提取xpath的属性值, 就返回一个简单的结构 218 | nodesMap+=Map("attribute"->attr) 219 | } 220 | } 221 | nodesMap.toList 222 | } 223 | 224 | } -------------------------------------------------------------------------------- /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 CommonLog with WebBrowser with WebDriver{ 21 | Runtimes.init() 22 | var conf: CrawlerConf = _ 23 | 24 | implicit var driver: MacacaClient = _ 25 | var appiumProcess: Process = null 26 | var loc = "" 27 | var index = 0 28 | var currentElement: macaca.client.commands.Element =_ 29 | 30 | private var platformName = "" 31 | 32 | def this(url: String = "http://127.0.0.1:4723/wd/hub", configMap: Map[String, Any]=Map[String, Any]()) { 33 | this 34 | appium(url, configMap) 35 | } 36 | 37 | def setPlatformName(platform: String): Unit = { 38 | log.info(s"set platform ${platform}") 39 | platformName = platform 40 | } 41 | 42 | 43 | def shell(command:String): Unit ={ 44 | sys.props("os.name").toLowerCase match { 45 | case x if x contains "windows" => Seq("cmd", "/C") ++ command 46 | case _ => command 47 | } 48 | log.error("TODO") 49 | } 50 | 51 | //todo: 集成appium进程管理 52 | def start(port: Int = 4723): Unit = { 53 | appiumProcess = Process(s"appium --session-override -p ${port}").run() 54 | asyncTask(10){ 55 | appiumProcess.exitValue() 56 | } match { 57 | case None=>{log.info("appium start success")} 58 | case Some(code)=>{log.error(s"appium failed with code ${code}")} 59 | } 60 | 61 | } 62 | 63 | override def stop(): Unit = { 64 | appiumProcess.destroy() 65 | } 66 | 67 | override def hideKeyboard(): Unit = { 68 | //todo: 69 | } 70 | 71 | /** 72 | * 在5s的时间内确定元素存在并且位置是固定的 73 | * 74 | * @param key 75 | */ 76 | def wait(key: String): Unit = { 77 | var isFound = false 78 | 1 to 10 foreach (i => { 79 | if (isFound == false) { 80 | val elements = driver.elementsByXPath(key) 81 | if (elements.size() > 0) { 82 | isFound = true 83 | } else { 84 | sleep(0.5) 85 | } 86 | } 87 | }) 88 | 89 | } 90 | 91 | def see(key: String = "//*", index: Int = 0): this.type = { 92 | loc = key 93 | this.index = index 94 | wait(key) 95 | this 96 | } 97 | 98 | /* 99 | override def tap(): this.type = { 100 | click on (XPathQuery(tree(loc, index)("xpath").toString)) 101 | this 102 | } 103 | */ 104 | 105 | override def event(keycode: Int): Unit = { 106 | //todo: 107 | log.error("not implete") 108 | } 109 | 110 | def attribute(key: String): String = { 111 | nodes().head.get(key).get.toString 112 | } 113 | 114 | def apply(key: String): String = { 115 | attribute(key) 116 | } 117 | 118 | def nodes(): List[Map[String, Any]] = { 119 | getListFromXPath(loc) 120 | } 121 | 122 | 123 | def appium(url: String = "http://127.0.0.1:4723/wd/hub", configMap: Map[String, Any]=Map[String, Any]()): Unit = { 124 | driver=new MacacaClient() 125 | val porps = new JSONObject() 126 | configMap.foreach(m=>porps.put(m._1, m._2)) 127 | porps.put("package", configMap("appPackage")) 128 | porps.put("activity", configMap("appActivity")) 129 | 130 | val desiredCapabilities = new JSONObject() 131 | desiredCapabilities.put("desiredCapabilities", porps) 132 | driver.initDriver(desiredCapabilities) 133 | 134 | getDeviceInfo 135 | } 136 | 137 | /** 138 | * 解析给定的xpath表达式或者text的定位标记 把节点的信息转换为map 139 | * 140 | * @param key 141 | * @return 142 | */ 143 | def tree(key: String = "//*", index: Int = 0): Map[String, Any] = { 144 | log.info(s"find by key = ${key} index=${index}") 145 | val nodes = getListFromXPath(key) 146 | nodes.foreach(node => { 147 | log.debug(s"index=${nodes.indexOf(node)}") 148 | node.foreach(kv => { 149 | log.debug(kv) 150 | }) 151 | }) 152 | val ret = nodes.lift(index).getOrElse(Map[String, Any]()) 153 | log.info(s"ret = ${ret}") 154 | ret 155 | } 156 | 157 | //todo: not test 158 | def crawl(conf: String = "", resultDir: String = "", maxDepth: Int = 1): Unit = { 159 | var crawler: Crawler = new Crawler 160 | 161 | crawler = new Crawler 162 | if (conf.nonEmpty) { 163 | crawler.loadConf(conf) 164 | } 165 | if (resultDir.nonEmpty) { 166 | crawler.conf.resultDir = resultDir 167 | } 168 | crawler.log.setLevel(Level.TRACE) 169 | crawler.conf.maxDepth = maxDepth 170 | crawler.start(this) 171 | 172 | } 173 | 174 | override def getDeviceInfo(): Unit = { 175 | val size=driver.getWindowSize 176 | log.info(s"size=${size}") 177 | screenHeight = size.get("height").toString.toInt 178 | screenWidth = size.get("width").toString.toInt 179 | log.info(s"screenWidth=${screenWidth} screenHeight=${screenHeight}") 180 | } 181 | 182 | override def swipe(startX: Double = 0.9, endX: Double = 0.1, startY: Double = 0.9, endY: Double = 0.1): Option[_] = { 183 | retry(driver.swipe( 184 | (screenWidth * startX).toInt, (screenHeight * startY).toInt, 185 | (screenWidth * endX).toInt, (screenHeight * endY).toInt, 1000 186 | ) 187 | ) 188 | } 189 | 190 | 191 | override def screenshot(): File = { 192 | val location="/tmp/1.png" 193 | driver.saveScreenshot(location) 194 | new File(location) 195 | } 196 | 197 | //todo: 重构到独立的trait中 198 | override def mark(fileName: String, newImageName:String, x: Int, y: Int, w: Int, h: Int): Unit = { 199 | val file = new java.io.File(fileName) 200 | log.info(s"platformName=${platformName}") 201 | log.info("getScreenshot") 202 | val img = ImageIO.read(file) 203 | val graph = img.createGraphics() 204 | 205 | if (platformName.toLowerCase == "ios") { 206 | log.info("scale the origin image") 207 | graph.drawImage(img, 0, 0, screenWidth, screenHeight, null) 208 | } 209 | graph.setStroke(new BasicStroke(5)) 210 | graph.setColor(Color.RED) 211 | graph.drawRect(x, y, w, h) 212 | graph.dispose() 213 | 214 | log.info(s"write png ${fileName}") 215 | if (platformName.toLowerCase == "ios") { 216 | val subImg=img.getSubimage(0, 0, screenWidth, screenHeight) 217 | ImageIO.write(subImg, "png", new java.io.File(newImageName)) 218 | } else { 219 | ImageIO.write(img, "png", new java.io.File(newImageName)) 220 | } 221 | } 222 | 223 | 224 | def hello(action: String, number: Int = 0): Unit = { 225 | println(s"hello ${action} ${number}") 226 | } 227 | 228 | /* 229 | def tap(x: Int = screenWidth / 2, y: Int = screenHeight / 2): Unit = { 230 | log.info("tap") 231 | driver.tap(1, x, y, 100) 232 | //driver.findElementByXPath("//UIAWindow[@path='/0/2']").click() 233 | //new TouchAction(driver).tap(x, y).perform() 234 | }*/ 235 | 236 | override def tap(): this.type = { 237 | currentElement.click() 238 | this 239 | } 240 | 241 | override def longTap(): this.type = { 242 | currentElement.click() 243 | this 244 | } 245 | 246 | override def back(): Unit = { 247 | log.info("navigate back") 248 | driver.back() 249 | } 250 | 251 | override def backApp(): Unit = { 252 | /* 253 | sleep(10) 254 | event(AndroidKeyCode.BACK) 255 | sleep(2) 256 | event(AndroidKeyCode.ENTER) 257 | */ 258 | back() 259 | } 260 | 261 | override def getPageSource(): String = { 262 | //获取页面结构, 最多重试3次 263 | 1 to 3 foreach (i => { 264 | asyncTask(20)(driver.source()) match { 265 | case Some(v) => { 266 | log.trace("get page source success") 267 | //todo: wda返回的不是标准的xml 268 | val xmlStr=v match { 269 | case json if json.trim.charAt(0)=='{' => { 270 | log.info("json format maybe from wda") 271 | DataObject.fromJson[Map[String, String]](v).getOrElse("value", "") 272 | } 273 | case xml if xml.trim.charAt(0)=='<' =>{ 274 | log.info("xml format ") 275 | xml 276 | } 277 | } 278 | currentPageSource = XPathUtil.toPrettyXML(xmlStr) 279 | currentPageDom=XPathUtil.toDocument(currentPageSource) 280 | return currentPageSource 281 | } 282 | case None => { 283 | log.trace("get page source error") 284 | } 285 | } 286 | }) 287 | currentPageSource 288 | } 289 | 290 | 291 | def monkey(): Unit = { 292 | val crawl = AppCrawler.crawler 293 | val monkeyEvents = crawl.conf.monkeyEvents 294 | val count = monkeyEvents.size 295 | val limits = AppCrawler.crawler.conf.monkeyRunTimeSeconds 296 | val record = new DataRecord 297 | while (record.intervalMS() / 1000 < limits) { 298 | val number = util.Random.nextInt(count) 299 | val code = monkeyEvents(number) 300 | event(code) 301 | record.append(code) 302 | val element = URIElement(crawl.currentUrl + "_Monkey", "", "", "", s"monkey_${code}") 303 | crawl.store.setElementClicked(element) 304 | } 305 | } 306 | 307 | 308 | //todo:优化查找方法 309 | //找到统一的定位方法就在这里定义, 找不到就分别在子类中重载定义 310 | override def findElementByUrlElement(element: URIElement): Boolean = { 311 | //为了加速去掉id定位, 测试表明比xpath竟然还慢 312 | /* 313 | log.info(s"find element by uid ${element}") 314 | if (element.id != "") { 315 | log.info(s"find by id=${element.id}") 316 | MiniAppium.doAppium(driver.findElementsById(element.id)) match { 317 | case Some(v) => { 318 | val arr = v.toArray().distinct 319 | if (arr.length == 1) { 320 | log.trace("find by id success") 321 | return Some(arr.head.asInstanceOf[WebElement]) 322 | } else { 323 | //有些公司可能存在重名id 324 | arr.foreach(log.info) 325 | log.info(s"find count ${arr.size}, change to find by xpath") 326 | } 327 | } 328 | case None => { 329 | log.warn("find by id error") 330 | } 331 | } 332 | } 333 | */ 334 | //todo: 用其他定位方式优化 335 | log.info(s"find by xpath= ${element.loc}") 336 | retry{ 337 | val s=driver.elementsByXPath(element.loc) 338 | 0 until s.size() map(s.getIndex(_)) 339 | } match { 340 | case Some(v) => { 341 | val arr = v.toList.distinct 342 | arr.length match { 343 | case len if len == 1 => { 344 | log.info("find by xpath success") 345 | currentElement=arr.head 346 | return true 347 | } 348 | case len if len > 1 => { 349 | log.warn(s"find count ${v.size}, you should check your dom file") 350 | //有些公司可能存在重名id 351 | arr.foreach(log.info) 352 | log.warn("just use the first one") 353 | currentElement=arr.head 354 | return true 355 | } 356 | case len if len == 0 => { 357 | log.warn("find by xpath error no element found") 358 | } 359 | } 360 | } 361 | case None => { 362 | log.warn("find by xpath error") 363 | } 364 | } 365 | false 366 | } 367 | 368 | 369 | override def getAppName(): String = { 370 | val xpath="(//*[@package!=''])[1]" 371 | getListFromXPath(xpath).head.getOrElse("package", "").toString 372 | } 373 | 374 | override def getUrl(): String = { 375 | //todo: macaca的url没设定 376 | //driver.title() 377 | "" 378 | } 379 | 380 | override def getRect(): Rectangle ={ 381 | val rect=currentElement.getRect.asInstanceOf[JSONObject] 382 | new Rectangle(rect.getIntValue("x"), rect.getIntValue("y"), rect.getIntValue("height"), rect.getIntValue("width")) 383 | } 384 | 385 | 386 | override def sendKeys(content: String): Unit = { 387 | currentElement.sendKeys(content) 388 | } 389 | 390 | 391 | 392 | } 393 | 394 | -------------------------------------------------------------------------------- /src/main/scala/com/testerhome/appcrawler/driver/WebDriver.scala: -------------------------------------------------------------------------------- 1 | package com.testerhome.appcrawler.driver 2 | 3 | import java.io.File 4 | import java.util.concurrent.{Callable, Executors, TimeUnit, TimeoutException} 5 | 6 | import com.testerhome.appcrawler.{CommonLog, URIElement} 7 | import com.testerhome.appcrawler.{CommonLog, XPathUtil, Runtimes} 8 | import org.openqa.selenium.Rectangle 9 | import org.openqa.selenium.remote.DesiredCapabilities 10 | import org.w3c.dom.Document 11 | 12 | import scala.collection.mutable.ListBuffer 13 | import scala.util.{Failure, Success, Try} 14 | 15 | /** 16 | * Created by seveniruby on 2017/4/17. 17 | */ 18 | trait WebDriver extends CommonLog { 19 | 20 | val capabilities = new DesiredCapabilities() 21 | 22 | var screenWidth = 0 23 | var screenHeight = 0 24 | var currentPageDom: Document = null 25 | var currentPageSource: String="" 26 | val appiumExecResults=ListBuffer[String]() 27 | 28 | 29 | def config(key: String, value: Any): Unit = { 30 | capabilities.setCapability(key, value) 31 | } 32 | 33 | def stop(): Unit = { 34 | } 35 | 36 | def hideKeyboard(): Unit = { 37 | 38 | } 39 | 40 | def findElementByUrlElement(element: URIElement): Boolean= { 41 | false 42 | } 43 | 44 | def getDeviceInfo(): Unit = { 45 | } 46 | 47 | def screenshot(): File = { null } 48 | 49 | def back(): Unit = {} 50 | 51 | def backApp(): Unit = {} 52 | def launchApp(): Unit ={ 53 | 54 | } 55 | 56 | def getPageSource(): String = { "" } 57 | 58 | def tap(): this.type = { this } 59 | def longTap(): this.type = { this } 60 | def swipe(direction: String): Unit = { 61 | log.info(s"start swipe ${direction}") 62 | var startX = 0.0 63 | var startY = 0.0 64 | var endX = 0.0 65 | var endY = 0.0 66 | direction match { 67 | case "left" => { 68 | startX = 0.9 69 | startY = 0.5 70 | endX = 0.1 71 | endY = 0.5 72 | } 73 | case "right" => { 74 | startX = 0.1 75 | startY = 0.5 76 | endX = 0.9 77 | endY = 0.5 78 | } 79 | case "up" => { 80 | startX = 0.5 81 | startY = 0.9 82 | endX = 0.5 83 | endY = 0.1 84 | } 85 | case "down" => { 86 | startX = 0.5 87 | startY = 0.1 88 | endX = 0.5 89 | endY = 0.9 90 | } 91 | case _ => { 92 | startX = 0.9 93 | startY = 0.9 94 | endX = 0.1 95 | endY = 0.1 96 | } 97 | } 98 | swipe(startX, endX, startY, endY) 99 | sleep(1) 100 | } 101 | 102 | def swipe(startX: Double = 0.9, endX: Double = 0.1, startY: Double = 0.9, endY: Double = 0.1): Option[_] = { 103 | None 104 | } 105 | 106 | 107 | 108 | 109 | def dsl(command: String): Unit = { 110 | log.info(s"eval ${command}") 111 | Try(Runtimes.eval(command)) match { 112 | case Success(v) => log.info(v) 113 | case Failure(e) => log.warn(e.getMessage) 114 | } 115 | log.info("eval finish") 116 | //new Eval().inPlace(s"com.testerhome.appcrawler.MiniAppium.${command.trim}") 117 | } 118 | 119 | 120 | def getUrl(): String = { 121 | "" 122 | } 123 | 124 | def getAppName(): String ={ 125 | "" 126 | } 127 | 128 | def asyncTask[T](timeout: Int = 30, restart: Boolean = false)(callback: => T): Option[T] = { 129 | Try({ 130 | val task = Executors.newSingleThreadExecutor().submit(new Callable[T]() { 131 | def call(): T = { 132 | callback 133 | } 134 | }) 135 | if(timeout<0){ 136 | task.get() 137 | }else { 138 | task.get(timeout, TimeUnit.SECONDS) 139 | } 140 | 141 | }) match { 142 | case Success(v) => { 143 | appiumExecResults.append("success") 144 | Some(v) 145 | } 146 | case Failure(e) => { 147 | e match { 148 | case e: TimeoutException => { 149 | log.error(s"${timeout} seconds timeout") 150 | appiumExecResults.append("timeout") 151 | } 152 | case _ => { 153 | log.error("exception") 154 | log.error(e.getMessage) 155 | log.error(e.getStackTrace.mkString("\n")) 156 | appiumExecResults.append(e.getMessage) 157 | } 158 | } 159 | None 160 | } 161 | } 162 | } 163 | 164 | def retry[T](r: => T): Option[T] = { 165 | Try(r) match { 166 | case Success(v) => { 167 | log.info("retry execute success") 168 | Some(v) 169 | } 170 | case Failure(e) => { 171 | log.warn("message=" + e.getMessage) 172 | log.warn("cause=" + e.getCause) 173 | //log.trace(e.getStackTrace.mkString("\n")) 174 | None 175 | } 176 | } 177 | 178 | } 179 | 180 | 181 | def event(keycode: Int): Unit = {} 182 | def mark(fileName: String, newImageName:String, x: Int, y: Int, w: Int, h: Int): Unit = {} 183 | def getRect(): Rectangle ={ 184 | new Rectangle(0, 0, 0, 0) 185 | } 186 | 187 | def sendKeys(content:String): Unit ={ 188 | 189 | } 190 | 191 | def sleep(seconds: Double = 1.0F): Unit = { 192 | Thread.sleep((seconds * 1000).toInt) 193 | } 194 | 195 | //todo: xpath 2.0 support 196 | def getListFromXPath(key:String): List[Map[String, Any]] ={ 197 | key match { 198 | //xpath 199 | case xpath if Array('/', '(').contains(xpath.head) => { 200 | XPathUtil.getListFromXPath(xpath, currentPageDom) 201 | } 202 | case regex if key.contains(".*") || key.startsWith("^") => { 203 | XPathUtil.getListFromXPath("//*", currentPageDom).filter(m=>{ 204 | m("name").toString.matches(regex) || 205 | m("label").toString.matches(regex) || 206 | m("value").toString.matches(regex) 207 | }) 208 | } 209 | case str: String => { 210 | XPathUtil.getListFromXPath("//*", currentPageDom).filter(m=>{ 211 | m("name").toString.contains(str) || 212 | m("label").toString.contains(str) || 213 | m("value").toString.contains(str) 214 | }) 215 | } 216 | } 217 | } 218 | 219 | } -------------------------------------------------------------------------------- /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 | import com.testerhome.appcrawler.Plugin 5 | 6 | /** 7 | * Created by seveniruby on 16/1/21. 8 | */ 9 | class DemoPlugin extends Plugin{ 10 | override def beforeElementAction(element: URIElement): Unit ={ 11 | log.info("demo com.testerhome.appcrawler.plugin before element action") 12 | log.info(element) 13 | log.info("demo com.testerhome.appcrawler.plugin end") 14 | } 15 | override def afterUrlRefresh(url:String): Unit ={ 16 | getCrawler().currentUrl=url.split('|').last 17 | log.info(s"new url=${getCrawler().currentUrl}") 18 | if(getCrawler().currentUrl.contains("Browser")){ 19 | getCrawler().getBackButton() 20 | } 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /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, Plugin} 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, Plugin} 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.loc) 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 com.testerhome.appcrawler.Plugin 4 | import sys.process._ 5 | /** 6 | * Created by seveniruby on 16/4/26. 7 | */ 8 | class IDeviceScreenshot extends Plugin{ 9 | 10 | var use=false 11 | override def start(): Unit ={ 12 | getCrawler().conf.capability("udid") match { 13 | case null=> { 14 | use=false 15 | log.info("udid=null use simulator") 16 | } 17 | case ""=>{ 18 | use=false 19 | log.info("udid= use simulator") 20 | } 21 | case _ =>{ 22 | use=true 23 | log.info("use idevicescreenshot") 24 | } 25 | } 26 | } 27 | override def screenshot(path:String): Boolean ={ 28 | //非真机不使用 29 | if(use==false) return false 30 | val cmd=s"idevicescreenshot ${path}" 31 | log.info(s"cmd=${cmd}") 32 | cmd.! 33 | true 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /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.{Plugin, 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/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 com.testerhome.appcrawler.Plugin 8 | import net.lightbody.bmp.BrowserMobProxyServer 9 | import net.lightbody.bmp.proxy.CaptureType 10 | import org.apache.log4j.{BasicConfigurator, Level, Logger} 11 | 12 | import scala.util.Try 13 | 14 | /** 15 | * Created by seveniruby on 16/4/26. 16 | */ 17 | class ProxyPlugin extends Plugin { 18 | private var proxy: BrowserMobProxyServer = _ 19 | val port = 7777 20 | 21 | //todo: 支持代理 22 | override def start(): Unit = { 23 | BasicConfigurator.configure() 24 | Logger.getRootLogger.setLevel(Level.INFO) 25 | Logger.getLogger("ProxyServer").setLevel(Level.WARN) 26 | 27 | proxy = new BrowserMobProxyServer() 28 | proxy.setHarCaptureTypes(CaptureType.getNonBinaryContentCaptureTypes) 29 | proxy.setTrustAllServers(true) 30 | proxy.start(port) 31 | 32 | //proxy.setHarCaptureTypes(CaptureType.getAllContentCaptureTypes) 33 | //proxy.setHarCaptureTypes(CaptureType.getHeaderCaptureTypes) 34 | log.info(s"proxy server listen on ${port}") 35 | proxy.newHar("start") 36 | } 37 | 38 | override def beforeElementAction(element: URIElement): Unit = { 39 | log.info("clear har") 40 | proxy.endHar() 41 | //创建新的har 42 | val harFileName = getCrawler().getBasePathName() + ".har" 43 | proxy.newHar(harFileName) 44 | } 45 | 46 | override def afterElementAction(element: URIElement): Unit = { 47 | log.info("save har") 48 | val harFileName = getCrawler().getBasePathName() + ".har" 49 | val file = new File(harFileName) 50 | try { 51 | log.info(proxy.getHar) 52 | log.info(proxy.getHar.getLog) 53 | log.info(proxy.getHar.getLog.getEntries.size()) 54 | log.info(s"har entry size = ${proxy.getHar.getLog.getEntries.size()}") 55 | if (proxy.getHar.getLog.getEntries.size() > 0) { 56 | proxy.getHar.writeTo(file) 57 | } 58 | } catch { 59 | case e: Exception =>{ 60 | log.error("read har error") 61 | log.error(e.getCause) 62 | log.error(e.getMessage) 63 | e.getStackTrace.foreach(log.error) 64 | } 65 | } 66 | 67 | } 68 | 69 | override def stop(): Unit ={ 70 | log.info("prpxy stop") 71 | proxy.stop() 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /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 | generateReport() 31 | } 32 | 33 | override def afterElementAction(element: URIElement): Unit ={ 34 | val count=getCrawler().store.clickedElementsList.length 35 | log.info(s"clickedElementsList size = ${count}") 36 | val curSize=getCrawler().store.clickedElementsList.size 37 | if(curSize-lastSize > curSize/10+20 ){ 38 | log.info(s"${curSize}-${lastSize} > ${curSize}/10+10 ") 39 | log.info("generate test report ") 40 | generateReport() 41 | } 42 | } 43 | 44 | def generateReport(): Unit ={ 45 | Report.saveTestCase(getCrawler().store, getCrawler().conf.resultDir) 46 | Report.store=getCrawler().store 47 | Report.runTestCase() 48 | 49 | lastSize=getCrawler().store.clickedElementsList.size 50 | } 51 | 52 | 53 | } 54 | -------------------------------------------------------------------------------- /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, Plugin} 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 | tagLimitMax = getCrawler().conf.tagLimitMax 17 | } 18 | 19 | override def beforeElementAction(element: URIElement): Unit = { 20 | val key = element.toTagPath() 21 | log.trace(s"tag path = ${key}") 22 | if (!tagLimit.contains(key)) { 23 | //跳过具备selected=true的菜单栏 24 | getCrawler().driver.getListFromXPath("//*[@selected='true']").foreach(m=>{ 25 | val element=getCrawler().getUrlElementByMap(m) 26 | tagLimit(element.toTagPath())=20 27 | log.info(s"tagLimit[${element.toTagPath()}]=20") 28 | }) 29 | //应用定制化的规则 30 | getCrawler().getTagLimitFromElementActions(element) match { 31 | case Some(v)=> { 32 | tagLimit(key)=v 33 | log.info(s"tagLimit[${key}]=${tagLimit(key)} with conf.tagLimit") 34 | } 35 | case None => tagLimit(key)=tagLimitMax 36 | } 37 | } 38 | 39 | //如果达到限制次数就退出 40 | if (tagLimit(key) <= 0) { 41 | log.warn(s"tagLimit[${key}]=${tagLimit(key)}") 42 | getCrawler().setElementAction("skip") 43 | log.info(s"$element need skip") 44 | } 45 | } 46 | 47 | override def afterElementAction(element: URIElement): Unit = { 48 | val key = element.toTagPath() 49 | if (tagLimit.contains(key)) { 50 | tagLimit(key) -= 1 51 | log.info(s"tagLimit[${key}]=${tagLimit(key)}") 52 | } 53 | } 54 | 55 | 56 | override def afterUrlRefresh(url: String): Unit = { 57 | 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /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/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 | 5 | import com.sun.jdi.connect.spi.TransportService.Capabilities 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 | } 35 | } 36 | -------------------------------------------------------------------------------- /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 | 19 | test("ios测试"){ 20 | val capability=new DesiredCapabilities() 21 | capability.setCapability("app", 22 | "/Users/seveniruby/projects/ios-uicatalog/build/Debug-iphonesimulator/UICatalog.app") 23 | capability.setCapability("bundleId", "com.example.apple-samplecode.UICatalog") 24 | //capability.setCapability("appPackage", "com.example.apple-samplecode.UICatalog") 25 | //capability.setCapability("appActivity", "com.example.apple-samplecode.UICatalog") 26 | 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", 57 | app) 58 | capability.setCapability("bundleId", "com.example.apple-samplecode.UICatalog") 59 | //capability.setCapability("appPackage", "com.example.apple-samplecode.UICatalog") 60 | //capability.setCapability("appActivity", "com.example.apple-samplecode.UICatalog") 61 | 62 | 63 | capability.setCapability("fullReset", "false") 64 | capability.setCapability("noReset", "true") 65 | //capability.setCapability("udid", "4F05E384-FE32-43DE-8539-4DC3E2EBC117") 66 | capability.setCapability("automationName", "XCUITest") 67 | capability.setCapability("platformName", "ios") 68 | capability.setCapability("platformVersion", "10.2") 69 | capability.setCapability("deviceName", "iPhone 7") 70 | capability.setCapability("autoAcceptAlerts", true) 71 | 72 | 73 | //val url="http://192.168.100.65:7771" 74 | //val url="http://127.0.0.1:8100" 75 | val url="http://127.0.0.1:4723/wd/hub" 76 | val driver=new IOSDriver[IOSElement](new URL(url), capability) 77 | println(driver.getPageSource) 78 | driver.findElementsByXPath("//*[@label='OK']") match { 79 | case array if array.size()>0 => array.head.click() 80 | case _ => println("no OK alert") 81 | } 82 | driver.runAppInBackground(Duration.ofSeconds(3)) 83 | driver.findElementsByXPath("//*").foreach(x=>{ 84 | println(x) 85 | println(x.getText) 86 | }) 87 | 88 | } 89 | 90 | test("android"){ 91 | val capability=new DesiredCapabilities() 92 | capability.setCapability("app", "") 93 | capability.setCapability("appPackage", "com.gotokeep.keep") 94 | capability.setCapability("appActivity", ".activity.SplashActivity") 95 | //capability.setCapability("appWaitActivity", "MainActivity") 96 | 97 | capability.setCapability("fullReset", "false") 98 | capability.setCapability("noReset", "true") 99 | //capability.setCapability("udid", "4F05E384-FE32-43DE-8539-4DC3E2EBC117") 100 | //capability.setCapability("automationName", "uiautomator") 101 | capability.setCapability("automationName", "appium") 102 | capability.setCapability("platformName", "android") 103 | capability.setCapability("platformVersion", "") 104 | capability.setCapability("deviceName", "dddd") 105 | //capability.setCapability("autoAcceptAlerts", true) 106 | 107 | 108 | //val url="http://192.168.100.65:7771" 109 | //val url="http://127.0.0.1:8100" 110 | val url="http://127.0.0.1:4723/wd/hub" 111 | val driver=new RemoteWebDriver(new URL(url), capability) 112 | Thread.sleep(3000) 113 | 114 | driver.findElementsByXPath("//*").foreach(x=>{ 115 | println(x.getText) 116 | }) 117 | 118 | driver.findElementByXPath("//*[@text='跑步']").click() 119 | 120 | 121 | } 122 | 123 | test("appcrawler ios"){ 124 | AppCrawler.main(Array( 125 | "-c", "src/test/scala/com/testerhome/appcrawler/it/xueqiu_private.yml", 126 | "-a", app, 127 | "-p", "ios", 128 | "-o", s"/tmp/xueqiu/${System.currentTimeMillis()}", "--verbose" 129 | )) 130 | } 131 | 132 | 133 | 134 | 135 | } 136 | -------------------------------------------------------------------------------- /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 | package com.testerhome.appcrawler.ut 3 | 4 | import java.awt.BasicStroke 5 | import javax.imageio.ImageIO 6 | 7 | import net.sourceforge.tess4j.ITessAPI.TessPageIteratorLevel 8 | import org.scalatest.FunSuite 9 | 10 | import net.sourceforge.tess4j._ 11 | 12 | import scala.reflect.io.File 13 | import scala.collection.JavaConversions._ 14 | 15 | 16 | /** 17 | * Created by seveniruby on 16/9/20. 18 | */ 19 | class TestOCR extends FunSuite{ 20 | 21 | test("test ocr"){ 22 | val api=new Tesseract() 23 | api.setDatapath("/Users/seveniruby/Downloads/") 24 | api.setLanguage("eng+chi_sim") 25 | val img=new java.io.File("/Users/seveniruby/temp/google-test7.png") 26 | val imgFile=ImageIO.read(img) 27 | val graph=imgFile.createGraphics() 28 | graph.setStroke(new BasicStroke(5)) 29 | 30 | val result=api.doOCR(img) 31 | 32 | val words=api.getWords(imgFile, TessPageIteratorLevel.RIL_WORD).toList 33 | words.foreach(word=>{ 34 | 35 | val box=word.getBoundingBox 36 | val x=box.getX.toInt 37 | val y=box.getY.toInt 38 | val w=box.getWidth.toInt 39 | val h=box.getHeight.toInt 40 | 41 | graph.drawRect(x, y, w, h) 42 | graph.drawString(word.getText, x, y) 43 | 44 | println(word.getBoundingBox) 45 | println(word.getText) 46 | }) 47 | graph.dispose() 48 | ImageIO.write(imgFile, "png", new java.io.File(s"${img}.mark.png")) 49 | 50 | 51 | 52 | println(result) 53 | 54 | } 55 | 56 | } 57 | */ 58 | -------------------------------------------------------------------------------- /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 | 29 | } 30 | 31 | test("appcrawler"){ 32 | AppCrawler.main(Array("-c", "src/test/scala/com/testerhome/appcrawler/it/xueqiu_private.yml", 33 | "-o", s"/tmp/xueqiu/${System.currentTimeMillis()}", "--verbose" 34 | ) 35 | ) 36 | } 37 | 38 | test("appcrawler base example"){ 39 | AppCrawler.main(Array("-c", "src/test/scala/com/testerhome/appcrawler/it/xueqiu_base.yml", 40 | "-o", s"/tmp/xueqiu/${System.currentTimeMillis()}", "--verbose" 41 | ) 42 | ) 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /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: 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 | automationName: uiautomator2 29 | iosCapability: 30 | deviceName: "iPhone 6 Plus" 31 | bundleId: "com.xueqiu" 32 | screenshotWaitTimeout: "10" 33 | platformVersion: "9.3" 34 | autoAcceptAlerts: "true" 35 | app: "/Users/seveniruby/Library/Developer/Xcode/DerivedData/Snowball-ckpjegabufjxgxfeqyxgkmjuwmct/Build/Products/Debug-iphonesimulator/Snowball.app" 36 | appium: "http://127.0.0.1:4730/wd/hub" 37 | defineUrl: 38 | - "//*[@selected='true']/@text" 39 | - "//*[@selected='true']/@text" 40 | - "//*[contains(name(), 'NavigationBar')]/@label" 41 | #baseUrl: 42 | #- ".*MainActivity" 43 | #- ".*SNBHomeView.*" 44 | maxDepth: 1 45 | headFirst: true 46 | enterWebView: true 47 | urlBlackList: 48 | - ".*球友.*" 49 | - ".*png.*" 50 | - ".*Talk.*" 51 | - ".*Chat.*" 52 | - ".*Safari.*" 53 | - "WriteStatus.*" 54 | - "Browser.*" 55 | - "MyselfUser" 56 | - ".*MyselfUser.*" 57 | - ".*股市直播.*" 58 | #urlWhiteList: 59 | #- ".*Main.*" 60 | backButton: 61 | - //*[@resource-id='action_back'] 62 | - //*[@resource-id='android:id/up'] 63 | - //*[@resource-id='android:id/home'] 64 | - //*[@resource-id='android:id/action_bar_title'] 65 | - //*[@name='nav_icon_back'] 66 | - //*[@name='Back'] 67 | - //*[@name='返回'] 68 | - "//*[contains(name(), 'Button') and @name='取消']" 69 | - "//*[contains(name(), 'Button') and @label='返回']" 70 | - "//*[contains(name(), 'Button') and @name='关闭']" 71 | - "//*[contains(name(), 'Button') and @name='首页']" 72 | triggerActions: 73 | - xpath: "//*[contains(@resource-id, 'iv_close')]" 74 | - xpath: "//*[@resource-id='com.xueqiu.android:id/button_login']" 75 | times: 1 76 | - action: "15600534760" 77 | xpath: "//*[@resource-id='com.xueqiu.android:id/login_account']" 78 | times: 1 79 | - xpath: "//*[@resource-id='com.xueqiu.android:id/login_account']" 80 | times: 1 81 | - action: "hys2xueqiu" 82 | xpath: "//*[@resource-id='com.xueqiu.android:id/login_password']" 83 | times: 1 84 | - xpath: "button_next" 85 | times: 1 86 | - action: "15600534760" 87 | xpath: "//*[contains(name(), 'StaticText') and contains(@name, '登录')]" 88 | times: 1 89 | - action: "15600534760" 90 | xpath: "//*[contains(name(), 'TextField') and contains(@value, '手机')]" 91 | times: 1 92 | - action: "hys2xueqiu" 93 | xpath: "//*[contains(name(), 'SecureTextField')]" 94 | times: 1 95 | - xpath: "//*[contains(name(), 'Button') and contains(@name, '登 录')]" 96 | times: 1 97 | - xpath: ".*立即登录" 98 | times: 2 99 | - xpath: "//*[@name='登 录']" 100 | times: 2 101 | - xpath: "//*[@name='登录']" 102 | times: 2 103 | - action: "scroll left" 104 | xpath: "专题" 105 | times: 1 106 | - xpath: "点此.*" 107 | times: 3 108 | - xpath: "放弃" 109 | - xpath: "不保存" 110 | - xpath: "^确定$" 111 | - xpath: "^关闭$" 112 | - xpath: "取消" 113 | - xpath: "稍后再说" 114 | - xpath: "Cancel" 115 | - xpath: "这里可以.*" 116 | - xpath: ".*搬到这里.*" 117 | - xpath: "我要退出" 118 | - xpath: "tip_click_position" 119 | - xpath: "common guide icon ok" 120 | - xpath: "icon quotationinformation day" 121 | times: 1 122 | - xpath: "icon stock close" 123 | - xpath: "隐藏键盘" 124 | #一个神奇的符号 125 | - xpath: //*[@label='✕' and visible='true'] 126 | times: 10 127 | - action: 123 128 | xpath: //*[contains(name(), "EditText")] 129 | times: 10 130 | pri: 0 131 | - xpath: 我知道了 132 | autoCrawl: true 133 | testcase: 134 | name: demo1 135 | steps: 136 | - when: 137 | xpath: //* 138 | action: driver.swipe(0.5, 0.8, 0.5, 0.2) 139 | - when: 140 | xpath: //* 141 | action: driver.swipe(0.5, 0.2, 0.5, 0.8) 142 | - xpath: 自选 143 | action: click 144 | then: 145 | - //*[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: uiautomator2 31 | iosCapability: 32 | deviceName: "iPhone 7 Plus" 33 | bundleId: "com.xueqiu" 34 | screenshotWaitTimeout: "10" 35 | platformVersion: "10.2" 36 | autoAcceptAlerts: "true" 37 | automationName: xcuitest 38 | app: "/Users/seveniruby/Library/Developer/Xcode/DerivedData/Snowball-ckpjegabufjxgxfeqyxgkmjuwmct/Build/Products/Debug-iphonesimulator/Snowball.app" 39 | appium: "http://127.0.0.1:4723/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: 8 48 | headFirst: true 49 | enterWebView: true 50 | urlBlackList: 51 | - ".*球友.*" 52 | - ".*png.*" 53 | - ".*Talk.*" 54 | - ".*Chat.*" 55 | - ".*Safari.*" 56 | - "WriteStatus.*" 57 | - "Browser.*" 58 | - "MyselfUser" 59 | - ".*MyselfUser.*" 60 | - ".*股市直播.*" 61 | #urlWhiteList: 62 | #- ".*Main.*" 63 | backButton: 64 | - //*[@resource-id='action_back'] 65 | - //*[@resource-id='android:id/up'] 66 | - //*[@resource-id='android:id/home'] 67 | - //*[@resource-id='android:id/action_bar_title'] 68 | - //*[@name='nav_icon_back'] 69 | - //*[@name='Back'] 70 | - //*[@name='返回'] 71 | - "//*[contains(name(), 'Button') and @name='取消']" 72 | - "//*[contains(name(), 'Button') and @label='返回']" 73 | - "//*[contains(name(), 'Button') and @name='关闭']" 74 | - "//*[contains(name(), 'Button') and @name='首页']" 75 | firstList: 76 | - "//*[contains(name(), 'Popover')]//*" 77 | - "//*[contains(name(), 'Window')][3]//*" 78 | - "//*[contains(name(), 'Window')][2]//*" 79 | selectedList: 80 | #android非空标签 81 | - //*[@clickable="true"]//android.widget.TextView[string-length(@text)>0 and string-length(@text)<20] 82 | - //android.widget.EditText 83 | #ios 84 | - //*[contains(name(), 'Text') and string-length(@value)>0 and string-length(@value)<20 ] 85 | #通用的button和image 86 | - //*[contains(name(), 'Button')] 87 | - //*[contains(name(), 'Image')] 88 | #todo:如果多个规则都包含相同控件, 如何排序 89 | #处于选中状态的同级控件最后点击 90 | lastList: 91 | - //*[contains(@resource-id, 'header')]//* 92 | - //*[contains(@resource-id, 'indicator')]//* 93 | #股票 组合 94 | - //*[../*[@selected='true']] 95 | #港股 美股 96 | - //*[../../*/*[@selected='true'] and @resource-id=''] 97 | #tab标签 98 | - //*[../../*/*[@selected='true'] and contains(@resource-id, 'tab_')] 99 | #ios 沪深 港股等栏目 100 | - //*[../*[@value='1']] 101 | #ios 底层tab栏 102 | - //*[contains(name(), 'Button') and ../*[contains(name(), 'Button') and @value='1']] 103 | #tab低栏 104 | - //*[contains(@resource-id,'tabs')]//* 105 | blackList: 106 | #排除掉ios的状态栏 107 | - "//*[contains(name(), 'StatusBar')]//*" 108 | #股票分组编辑. 同一个imageview有2个图代表不同的状态. 没法区分, 只能设置为黑名单 109 | - //*[@resource-id='com.xueqiu.android:id/edit_group'] 110 | - ".*Safari" 111 | - ".*电话.*" 112 | - ".*Safari.*" 113 | - "发布" 114 | - "action_bar_title" 115 | - ".*浏览器.*" 116 | - "message" 117 | - ".*home" 118 | - "首页" 119 | - "Photos" 120 | - "地址" 121 | - "网址" 122 | - "拉黑" 123 | - "举报" 124 | - "camera" 125 | - "Camera" 126 | - "nav_icon_home" 127 | - "stock_item_.*" 128 | - ".*[0-9]{2}.*" 129 | - "发送" 130 | - "保存" 131 | - "确定" 132 | - "up" 133 | - "user_profile_icon" 134 | - "selectAll" 135 | - "cut" 136 | - "copy" 137 | - "send" 138 | - "买[0-9]*" 139 | - "卖[0-9]*" 140 | - "聊天.*" 141 | - "拍照.*" 142 | - "发表.*" 143 | - "回复.*" 144 | - "加入.*" 145 | - "赞助.*" 146 | - "微博.*" 147 | - "球友.*" 148 | - ".*开户.*" 149 | triggerActions: 150 | - xpath: "//*[contains(@resource-id, 'iv_close')]" 151 | - xpath: "//*[@resource-id='com.xueqiu.android:id/button_login']" 152 | times: 1 153 | - action: "15600534760" 154 | xpath: "//*[@resource-id='com.xueqiu.android:id/login_account']" 155 | times: 1 156 | - xpath: "//*[@resource-id='com.xueqiu.android:id/login_account']" 157 | times: 1 158 | - action: "hys2xueqiu" 159 | xpath: "//*[@resource-id='com.xueqiu.android:id/login_password']" 160 | times: 1 161 | - xpath: "button_next" 162 | times: 1 163 | - action: "15600534760" 164 | xpath: "//*[contains(name(), 'StaticText') and contains(@name, '登录')]" 165 | times: 1 166 | - action: "15600534760" 167 | xpath: "//*[contains(name(), 'TextField') and contains(@value, '手机')]" 168 | times: 1 169 | - action: "hys2xueqiu" 170 | xpath: "//*[contains(name(), 'SecureTextField')]" 171 | times: 1 172 | - xpath: "//*[contains(name(), 'Button') and contains(@name, '登 录')]" 173 | times: 1 174 | - xpath: ".*立即登录" 175 | times: 2 176 | - xpath: "//*[@name='登 录']" 177 | times: 2 178 | - xpath: "//*[@name='登录']" 179 | times: 2 180 | - action: "scroll left" 181 | xpath: "专题" 182 | times: 1 183 | - xpath: "点此.*" 184 | - xpath: "^放弃$" 185 | - xpath: "不保存" 186 | - xpath: "^确定$" 187 | - xpath: "^关闭$" 188 | - xpath: "^取消$" 189 | - xpath: "稍后再说" 190 | - xpath: "Cancel" 191 | - xpath: "这里可以.*" 192 | - xpath: ".*搬到这里.*" 193 | - xpath: "我要退出" 194 | - xpath: "tip_click_position" 195 | - xpath: "common guide icon ok" 196 | - xpath: "icon quotationinformation day" 197 | times: 1 198 | - xpath: "icon stock close" 199 | - xpath: "隐藏键盘" 200 | #一个神奇的符号 201 | - xpath: //*[@label='✕' and visible='true'] 202 | times: 10 203 | - action: 123 204 | xpath: //*[contains(name(), "EditText")] 205 | times: 10 206 | pri: 0 207 | - xpath: 我知道了 208 | tagLimit: 209 | - xpath: //*[../*[@selected='true']] 210 | count: 12 211 | - xpath: //*[../../*/*[@selected='true']] 212 | count: 12 -------------------------------------------------------------------------------- /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/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.screenshotTimeout=100 44 | val yaml=conf.toYaml() 45 | log.info(yaml) 46 | 47 | val conf2=new CrawlerConf 48 | conf2.loadYaml(yaml) 49 | conf2.screenshotTimeout should be equals(conf.screenshotTimeout) 50 | conf2.screenshotTimeout 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=DataObject.toYaml(store) 34 | log.info(str) 35 | val store2=DataObject.fromYaml[URIElementStore](str) 36 | log.info(store2) 37 | val str2=DataObject.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/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 | import java.io.File 5 | import java.util.jar.JarFile 6 | 7 | import com.testerhome.appcrawler.CommonLog 8 | import com.testerhome.appcrawler.plugin.DemoPlugin 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 TestRuntimes extends FunSuite with CommonLog{ 26 | 27 | val fileName="/Users/seveniruby/projects/LBSRefresh/iOS_20160813203343/AppCrawler_8.scala" 28 | test("MiniAppium dsl"){ 29 | AppiumClient.dsl("hello(\"seveniruby\", 30000)") 30 | AppiumClient.dsl("hello(\"ruby\", 30000)") 31 | AppiumClient.dsl(" hello(\"seveniruby\", 30000)") 32 | AppiumClient.dsl("hello(\"seveniruby\", 30000 ) ") 33 | AppiumClient.dsl("sleep(3)") 34 | AppiumClient.dsl("hello(\"xxxxx\")") 35 | AppiumClient.dsl("println(com.testerhome.appcrawler.AppCrawler.crawler.driver)") 36 | 37 | } 38 | 39 | test("compile by scala"){ 40 | Runtimes.init(new File(fileName).getParent) 41 | Runtimes.compile(List(fileName)) 42 | 43 | 44 | 45 | } 46 | 47 | test("native compile"){ 48 | 49 | 50 | val outputDir=new File(fileName).getParent 51 | 52 | val settings = new Settings() 53 | settings.deprecation.value = true // enable detailed deprecation warnings 54 | settings.unchecked.value = true // enable detailed unchecked warnings 55 | settings.outputDirs.setSingleOutput(outputDir) 56 | settings.usejavacp.value = true 57 | 58 | val global = new Global(settings) 59 | val run = new global.Run 60 | run.compile(List(fileName)) 61 | 62 | } 63 | 64 | 65 | test("imain"){ 66 | 67 | Runtimes.init() 68 | Runtimes.eval( 69 | """ 70 | |import com.testerhome.appcrawler.MiniAppium 71 | |println("xxx") 72 | |println("ddd") 73 | |MiniAppium.hello("222") 74 | """.stripMargin) 75 | 76 | 77 | } 78 | 79 | 80 | test("imain q"){ 81 | 82 | Runtimes.init() 83 | Runtimes.eval("import com.testerhome.appcrawler.MiniAppium") 84 | Runtimes.eval( 85 | """ 86 | |println("xxx") 87 | |println("ddd") 88 | |MiniAppium.hello("222") 89 | """.stripMargin) 90 | 91 | 92 | } 93 | 94 | 95 | test("imain with MiniAppium"){ 96 | 97 | Runtimes.init() 98 | Runtimes.eval("import com.testerhome.appcrawler.MiniAppium._") 99 | Runtimes.eval( 100 | """ 101 | |hello("222") 102 | |println(driver) 103 | """.stripMargin) 104 | } 105 | 106 | test("compile plugin"){ 107 | Runtimes.init() 108 | Runtimes.compile(List("src/universal/plugins/DynamicPlugin.scala")) 109 | val p=Class.forName("com.testerhome.appcrawler.plugin.DynamicPlugin").newInstance() 110 | log.info(p) 111 | 112 | 113 | } 114 | 115 | test("test classloader"){ 116 | val classPath="target/tmp/" 117 | Runtimes.init(classPath) 118 | Runtimes.compile(List("/Users/seveniruby/projects/LBSRefresh/src/universal/plugins/")) 119 | val urls=Seq(new java.io.File(classPath).toURI.toURL) 120 | val loader=new URLClassLoader(urls, ClassLoader.getSystemClassLoader) 121 | val x=loader.loadClass("AppCrawler_5").newInstance().asInstanceOf[FunSuite] 122 | log.info(x.testNames) 123 | log.info(getClass.getCanonicalName) 124 | 125 | log.info(getClass.getProtectionDomain.getCodeSource.getLocation.getPath) 126 | 127 | } 128 | 129 | test("load plugins"){ 130 | 131 | val a=new DemoPlugin() 132 | log.info(a.asInstanceOf[Plugin]) 133 | //getClass.getClassLoader.asInstanceOf[URLClassLoader].loadClass("DynamicPlugin") 134 | val plugins=Runtimes.loadPlugins("/Users/seveniruby/projects/LBSRefresh/src/universal/plugins/") 135 | plugins.foreach(log.info) 136 | 137 | } 138 | 139 | 140 | test("crawl keyword"){ 141 | Runtimes.eval("def crawl(depth:Int)=com.testerhome.appcrawler.AppCrawler.crawler.crawl(depth)") 142 | Runtimes.eval("crawl(1)") 143 | } 144 | 145 | 146 | } 147 | 148 | -------------------------------------------------------------------------------- /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.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.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.toFileName()) 13 | } 14 | 15 | test("tag path"){ 16 | 17 | val element=URIElement("", "", "", "", "//xxfxx[@index=\"11\" and @index=\"2\" and @text=\"fff>>dddff\"]") 18 | println(element.toTagPath()) 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/test/scala/com/testerhome/appcrawler/ut/scalate.ssp: -------------------------------------------------------------------------------- 1 |
    2 | #for (i <- 1 to 5) 3 |
  • ${i}
  • 4 |
  • ${unescape("<'\"")}
  • 5 | #end 6 | 7 |
--------------------------------------------------------------------------------