diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 07c18ba8e..44c5f0c73 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,22 +1,12 @@ # Contribution Guidelines -First of all, thanks for thinking of contributing to this project. :smile: +First of all, thank you for your interest in contributing to this project ! -- Before sending a Pull Request, please make sure that you have had a discussion with the project admins. - - If a relevant issue already exists, discuss on the issue and make sure that the admins are okay with your approach - - If no relevant issue exists, open a new issue and discuss +* Before submitting a [Pull Request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests) (PR), please make sure that you have had a discussion with the project-leads +* If a [relevant issue](https://github.com/intuit/karate/issues) already exists, have a discussion within that issue (by commenting) - and make sure that the project-leads are okay with your approach +* If no relevant issue exists, please [open a new issue](https://github.com/intuit/karate/issues) to start a discussion +* Please proceed with a PR only *after* the project admins or owners are okay with your approach. We don't want you to spend time and effort working on something - only to find out later that it was not aligned with how the project developers were thinking about it ! +* You can refer to the [Developer Guide](https://github.com/intuit/karate/wiki/Developer-Guide) for information on how to build and test the project on your local / developer machine +* **IMPORTANT**: Submit your PR(s) against the [`develop`](https://github.com/intuit/karate/tree/develop) branch of this repository - Please proceed with a Pull Request only after the project admins or owners are okay with your approach. It'd be sad if your Pull Request (and your hard work) isn't accepted just because it isn't ideologically compatible. - -- Install the required dependencies. - - Install Git so that you can clone and later submit a PR for this project. - - Install Java JDK (>= 1.8.0_112) installed, from [this link](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html). - - Install Eclipse from [this link](http://www.eclipse.org/downloads/). - - (optional) Install Maven from [this link](http://maven.apache.org), if you need to build the project from the command-line. - -- Have some issue with setting up the project? - - [How to open the project in Eclipse as a Maven project?](https://stackoverflow.com/a/36242422/143475) - - [Maven is not able to install the dependencies behind proxy!]() - - Not listed here? Kindly search on Google / Stack Overflow. If you don't find a solution, feel free to open up a new issue in the issue tracker and maybe subsequently add it here. - -- Send in your Pull Request(s) to the `develop` branch of this repository. +If you are interested in project road-map items that you can potentially contribute to, please refer to the [Project Board](https://github.com/intuit/karate/projects/3). diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 8f1c8f661..4b5610910 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,6 @@ ### Description -Thanks for contributing this Pull Request. Make sure that you send in this Pull Request to the `develop` branch of this repository, add a brief description, and tag the relevant issue(s) and PR(s) below. +Thanks for contributing this Pull Request. Make sure that you submit this Pull Request against the `develop` branch of this repository, add a brief description, and tag the relevant issue(s) and PR(s) below. - Relevant Issues : (compulsory) - Relevant PRs : (optional) diff --git a/.gitignore b/.gitignore index 83239337e..f5fa0b340 100755 --- a/.gitignore +++ b/.gitignore @@ -4,13 +4,22 @@ target/ .project .settings .classpath +.vscode *.iml build/ +bin/ .gradle gradle gradlew gradlew.* dependency-reduced-pom.xml +examples/zip-release/*.jar karate-demo/activemq-data/ +karate-demo/*.pem +karate-demo/*.jks +karate-demo/*.der +karate-netty/*.pem +karate-netty/*.jks +karate-netty/*.der karate-junit4/src/test/java/com/intuit/karate/junit4/dev diff --git a/.travis.yml b/.travis.yml index aa6f1ac2a..a838b08b7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,7 @@ language: java +cache: + directories: + - "$HOME/.m2" jdk: - - oraclejdk8 -sudo: false -addons: - apt: - packages: - - oracle-java8-installer -install: true -script: mvn install -P pre-release -Dmaven.javadoc.skip=true -B -V - + - openjdk8 +script: mvn install -P pre-release -Dmaven.javadoc.skip=true -B -V -Djavacpp.platform=linux-x86_64 diff --git a/README.md b/README.md index 76b2f60c4..ed42c91dc 100755 --- a/README.md +++ b/README.md @@ -1,12 +1,10 @@ # Karate -## Web-Services Testing Made `Simple.` +## Test Automation Made `Simple.` [![Maven Central](https://img.shields.io/maven-central/v/com.intuit.karate/karate-core.svg)](https://mvnrepository.com/artifact/com.intuit.karate/karate-core) [![Build Status](https://travis-ci.org/intuit/karate.svg?branch=master)](https://travis-ci.org/intuit/karate) [![GitHub release](https://img.shields.io/github/release/intuit/karate.svg)](https://github.com/intuit/karate/releases) [![Support Slack](https://img.shields.io/badge/support-slack-red.svg)](https://github.com/intuit/karate/wiki/Support) [![Twitter Follow](https://img.shields.io/twitter/follow/KarateDSL.svg?style=social&label=Follow)](https://twitter.com/KarateDSL) -Karate is the only open-source tool to combine API test-automation, [mocks](karate-netty) and [performance-testing](karate-gatling) into a **single**, *unified* framework. The BDD syntax popularized by Cucumber is language-neutral, and easy for even non-programmers. Besides powerful JSON & XML assertions, you can run tests in parallel for speed - which is critical for HTTP API testing. +Karate is the only open-source tool to combine API test-automation, [mocks](karate-netty), [performance-testing](karate-gatling) and even [UI automation](karate-core) into a **single**, *unified* framework. The BDD syntax popularized by Cucumber is language-neutral, and easy for even non-programmers. Powerful JSON & XML assertions are built-in, and you can run tests in parallel for speed. -You can easily build (or re-use) complex request payloads, and dynamically construct more requests from response data. The payload and schema validation engine can perform a 'smart compare' (deep-equals) of two JSON or XML documents, and you can even ignore dynamic values where needed. - -Test execution and report generation feels like any standard Java project. But there's also a [stand-alone executable](karate-netty#standalone-jar) for teams not comfortable with Java. Just write tests in a **simple**, *readable* syntax - carefully designed for HTTP, JSON, GraphQL and XML. +Test execution and report generation feels like any standard Java project. But there's also a [stand-alone executable](karate-netty#standalone-jar) for teams not comfortable with Java. You don't have to compile code. Just write tests in a **simple**, *readable* syntax - carefully designed for HTTP, JSON, GraphQL and XML. And you can mix API and [UI test-automation](karate-core) within the same test script. ## Hello World @@ -141,6 +139,7 @@ And you don't need to create additional Java classes for any of the payloads tha | responseHeaders | responseCookies | responseTime + | responseType | requestTimeStamp @@ -183,6 +182,7 @@ And you don't need to create additional Java classes for any of the payloads tha | Header Manipulation | GraphQL | Websockets / Async + | call vs read() @@ -192,7 +192,8 @@ And you don't need to create additional Java classes for any of the payloads tha | Test Doubles | Performance Testing | UI Testing - | Karate UI + | Desktop Automation + | VS Code / Debug | Karate vs REST-assured | Karate vs Cucumber | Examples and Demos @@ -203,15 +204,15 @@ And you don't need to create additional Java classes for any of the payloads tha # Features * Java knowledge is not required and even non-programmers can write tests * Scripts are plain-text, require no compilation step or IDE, and teams can collaborate using Git / standard SCM -* Based on the popular Cucumber / Gherkin standard - with [IDE support](#running-in-eclipse-or-intellij) and syntax-coloring options +* Based on the popular Cucumber / Gherkin standard - with [IDE support](https://github.com/intuit/karate/wiki/IDE-Support) and syntax-coloring options * Elegant [DSL](https://en.wikipedia.org/wiki/Domain-specific_language) syntax 'natively' supports JSON and XML - including [JsonPath](#set) and [XPath](#xpath-functions) expressions * Eliminate the need for 'Java Beans' or 'helper code' to represent payloads and HTTP end-points, and [dramatically reduce the lines of code](https://twitter.com/KarateDSL/status/873035687817117696) needed for a test * Ideal for testing the highly dynamic responses from [GraphQL](http://graphql.org) API-s because of Karate's built-in [text-manipulation](#text) and [JsonPath](https://github.com/json-path/JsonPath#path-examples) capabilities * Tests are super-readable - as scenario data can be expressed in-line, in human-friendly [JSON](#json), [XML](#xml), Cucumber [Scenario](#the-cucumber-way) Outline [tables](#table), or a [payload builder](#set-multiple) approach [unique to Karate](https://gist.github.com/ptrthomas/d6beb17e92a43220d254af942e3ed3d9) * Express expected results as readable, well-formed JSON or XML, and [assert in a single step](#match) that the entire response payload (no matter how complex or deeply nested) - is as expected * Comprehensive [assertion capabilities](#fuzzy-matching) - and failures clearly report which data element (and path) is not as expected, for easy troubleshooting of even large payloads -* [Embedded UI](https://github.com/intuit/karate/wiki/Karate-UI) for stepping through a script in debug mode where you can even [re-play a step while editing it](https://twitter.com/ptrthomas/status/889356965461217281) - a huge time-saver -* Simpler and more [powerful alternative](https://twitter.com/KarateDSL/status/878984854012022784) to JSON-schema for [validating payload structure](#schema-validation) and format - that even supports cross-field / domain validation logic +* [Fully featured debugger](https://github.com/intuit/karate/wiki/IDE-Support#vs-code-karate-plugin) that can step *backwards* and even [re-play a step while editing it](https://twitter.com/KarateDSL/status/1167533484560142336) - a *huge* time-saver +* Simpler and more [powerful alternative](https://twitter.com/KarateDSL/status/878984854012022784) to JSON-schema for [validating payload structure](#schema-validation) and format - that even supports [cross-field](#referring-to-the-json-root) / domain validation logic * Scripts can [call other scripts](#calling-other-feature-files) - which means that you can easily re-use and maintain authentication and 'set up' flows efficiently, across multiple tests * Embedded JavaScript engine that allows you to build a library of [re-usable functions](#calling-javascript-functions) that suit your specific environment or organization * Re-use of payload-data and user-defined functions across tests is [so easy](#reading-files) - that it becomes a natural habit for the test-developer @@ -220,18 +221,20 @@ And you don't need to create additional Java classes for any of the payloads tha * Native support for reading [YAML](#yaml) and even [CSV](#csv-files) files - and you can use them for data-driven tests * Standard Java / Maven project structure, and [seamless integration](#command-line) into CI / CD pipelines - and support for [JUnit 5](#junit-5) * Option to use as a light-weight [stand-alone executable](https://github.com/intuit/karate/tree/master/karate-netty#standalone-jar) - convenient for teams not comfortable with Java -* Support for multi-threaded [parallel execution](#parallel-execution), which is a huge time-saver, especially for HTTP integration tests +* Multi-threaded [parallel execution](#parallel-execution), which is a huge time-saver, especially for integration and end-to-end tests * Built-in [test-reports](#test-reports) compatible with Cucumber so that you have the option of using third-party (open-source) maven-plugins for even [better-looking reports](karate-demo#example-report) -* Reports include HTTP request and response [logs in-line](#test-reports), which makes [troubleshooting](https://twitter.com/KarateDSL/status/899671441221623809) and [debugging a test](https://twitter.com/KarateDSL/status/935029435140489216) a lot easier +* Reports include HTTP request and response [logs *in-line*](#test-reports), which makes [troubleshooting](https://twitter.com/KarateDSL/status/899671441221623809) and [debugging](https://twitter.com/KarateDSL/status/935029435140489216) easier * Easily invoke JDK classes, Java libraries, or re-use custom Java code if needed, for [ultimate extensibility](#calling-java) * Simple plug-in system for [authentication](#http-basic-authentication-example) and HTTP [header management](#configure-headers) that will handle any complex, real-world scenario * Future-proof 'pluggable' HTTP client abstraction supports both Apache and Jersey so that you can [choose](#maven) what works best in your project, and not be blocked by library or dependency conflicts -* Option to invoke via a [Java API](#java-api), which means that you can easily [mix Karate into existing Selenium / WebDriver test-suites](https://stackoverflow.com/q/47795762/143475). -* [Cross-browser Web, Mobile and Desktop UI automation](karate-core) (experimental) so that you can test *all* layers of your application with the same framework -* [Save significant effort](https://twitter.com/ptrthomas/status/986463717465391104) by re-using Karate test-suites as [Gatling performance tests](karate-gatling) that deeply assert that server responses are accurate under load +* [Cross-browser Web UI automation](karate-core) so that you can test *all* layers of your application with the same framework +* Cross platform [Desktop Automation](karate-robot) (experimental) that can be [mixed into Web Automation flows](https://twitter.com/ptrthomas/status/1215534821234995200) if needed +* Option to invoke via a [Java API](#java-api), which means that you can easily [mix Karate into Java projects or legacy UI-automation suites](https://stackoverflow.com/q/47795762/143475) +* [Save significant effort](https://twitter.com/ptrthomas/status/986463717465391104) by re-using Karate test-suites as [Gatling performance tests](karate-gatling) that *deeply* assert that server responses are accurate under load * Gatling integration can hook into [*any* custom Java code](https://github.com/intuit/karate/tree/master/karate-gatling#custom) - which means that you can perf-test even non-HTTP protocols such as [gRPC](https://github.com/thinkerou/karate-grpc) +* Built-in [distributed-testing capability](https://github.com/intuit/karate/wiki/Distributed-Testing) that works for API, UI and even [load-testing](https://github.com/intuit/karate/wiki/Distributed-Testing#gatling) - without needing any complex "grid" infrastructure * [API mocks](karate-netty) or test-doubles that even [maintain CRUD 'state'](https://hackernoon.com/api-consumer-contract-tests-and-test-doubles-with-karate-72c30ea25c18) across multiple calls - enabling TDD for micro-services and [Consumer Driven Contracts](https://martinfowler.com/articles/consumerDrivenContracts.html) -* [Async](#async) support that allows you to seamlessly integrate listening to message-queues within a test +* [Async](#async) support that allows you to seamlessly integrate the handling of custom events or listening to message-queues * [Mock HTTP Servlet](karate-mock-servlet) that enables you to test __any__ controller servlet such as Spring Boot / MVC or Jersey / JAX-RS - without having to boot an app-server, and you can use your HTTP integration tests un-changed * Comprehensive support for different flavors of HTTP calls: * [SOAP](#soap-action) / XML requests @@ -243,7 +246,6 @@ And you don't need to create additional Java classes for any of the payloads tha * Full control over HTTP [headers](#header), [path](#path) and query [parameters](#param) * [Re-try](#retry-until) until condition * [Websocket](http://www.websocket.org) [support](#async) - * Intelligent defaults ## Real World Examples A set of real-life examples can be found here: [Karate Demos](karate-demo) @@ -252,19 +254,23 @@ A set of real-life examples can be found here: [Karate Demos](karate-demo) For teams familiar with or currently using [REST-assured](http://rest-assured.io), this detailed comparison of [Karate vs REST-assured](http://tinyurl.com/karatera) - can help you evaluate Karate. ## References -* [REST API Testing with Karate](http://www.baeldung.com/karate-rest-api-testing) - tutorial by [Baeldung](http://www.baeldung.com/author/baeldung/) -* [9 great open-source API testing tools: how to choose](https://techbeacon.com/9-great-open-source-api-testing-tools-how-choose) - [TechBeacon](https://techbeacon.com) article by [Joe Colantonio](https://twitter.com/jcolantonio) -* [Ceinture noire Karate en tests d’API REST](https://devfesttoulouse.fr/schedule/2018-11-08?sessionId=4128) - [Slides and Code](https://github.com/ncomet/karate-conf2018) - [DevFest Touluse 2018](https://devfesttoulouse.fr) talk by [Nicolas Comet](https://twitter.com/NicolasComet) and [Benoît Prioux](https://twitter.com/binout) -* [Karate, the black belt of HTTP API testing ? - Video / Slides](https://adapt.to/2018/en/schedule/karate-the-black-belt-of-http-api-testing.html) / [Photo](https://twitter.com/bdelacretaz/status/1039444256572751873) / [Code](http://tinyurl.com/potsdam2018) - [adaptTo() 2018](https://adapt.to/2018/en.html) talk by [Bertrand Delacretaz](https://twitter.com/bdelacretaz) of Adobe & the Apache Software Foundation ([Board of Directors](http://www.apache.org/foundation/#who-runs-the-asf)) +* [Karate entered the ThoughtWorks Tech Radar](https://twitter.com/KarateDSL/status/1120985060843249664) in April 2019 +* [11 top open-source API testing tools](https://techbeacon.com/app-dev-testing/11-top-open-source-api-testing-tools-what-your-team-needs-know) - [TechBeacon](https://techbeacon.com) article by [Joe Colantonio](https://twitter.com/jcolantonio) +* [Why the heck is not everyone using Karate for their automated API testing in 2019 ?](https://testing.richardd.nl/why-the-heck-is-not-everyone-using-karate-for-their-automated-api-testing-in-2019) - blog post by [Richard Duinmaijer](https://twitter.com/RichardTheQAguy) +* [マイクロサービスにおけるテスト自動化 with Karate](https://www.slideshare.net/takanorig/microservices-test-automation-with-karate/) - (*Microservices Test Automation with Karate*) presentation by [Takanori Suzuki](https://twitter.com/takanorig) * [Testing Web Services with Karate](https://automationpanda.com/2018/12/10/testing-web-services-with-karate/) - quick start guide and review by [Andrew Knight](https://twitter.com/automationpanda) at the *Automation Panda* blog -You can find a lot more references [in the wiki](https://github.com/intuit/karate/wiki/Community-News). Karate also has its own 'tag' and a very active and supportive community at [Stack Overflow](https://stackoverflow.com/questions/tagged/karate). +You can find a lot more references [in the wiki](https://github.com/intuit/karate/wiki/Community-News). Karate also has its own "tag" and a very active and supportive community at [Stack Overflow](https://stackoverflow.com/questions/tagged/karate). # Getting Started -Karate requires [Java](http://www.oracle.com/technetwork/java/javase/downloads/index.html) 8 (at least version 1.8.0_112 or greater) and then either [Maven](http://maven.apache.org), [Gradle](https://gradle.org), [Eclipse](#eclipse-quickstart) or [IntelliJ](https://github.com/intuit/karate/wiki/IDE-Support#intellij-community-edition) to be installed. +If you are a Java developer - Karate requires [Java](http://www.oracle.com/technetwork/java/javase/downloads/index.html) 8 (at least version 1.8.0_112 or greater) and then either [Maven](http://maven.apache.org), [Gradle](https://gradle.org), [Eclipse](#eclipse-quickstart) or [IntelliJ](https://github.com/intuit/karate/wiki/IDE-Support#intellij-community-edition) to be installed. Note that Karate works fine on OpenJDK. Any Java version from 8-12 is supported. + +If you are new to programming or test-automation, refer to this video for [getting started with just the (free) IntelliJ Community Edition](https://youtu.be/W-af7Cd8cMc). Other options are the [quickstart](#quickstart) or the [standalone executable](karate-netty#standalone-jar). + +If you *don't* want to use Java, you have the option of just downloading and extracting the [ZIP release](https://github.com/intuit/karate/wiki/ZIP-Release). Try this especially if you don't have much experience with programming or test-automation. We recommend that you use the [Karate extension for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=kirkslota.karate-runner) - and with that, JavaScript, .NET and Python programmers will feel right at home. -> If you are new to programming or test-automation, refer to this video for [getting started with just the (free) IntelliJ Community Edition](https://youtu.be/W-af7Cd8cMc). Other options are the [quickstart](#quickstart) or the [standalone executable](karate-netty#standalone-jar). +Visual Studio Code can be used for Java (or Maven) projects as well. One reason to use it is the excellent [*debug support* that we have for Karate](https://twitter.com/KarateDSL/status/1167533484560142336). ## Maven Karate is designed so that you can choose between the [Apache](https://hc.apache.org/index.html) or [Jersey](https://jersey.java.net) HTTP client implementations. @@ -275,31 +281,31 @@ So you need two ``: com.intuit.karate karate-apache - 0.9.4 + 0.9.5 test com.intuit.karate - karate-junit4 - 0.9.4 + karate-junit5 + 0.9.5 test ``` And if you run into class-loading conflicts, for example if an older version of the Apache libraries are being used within your project - then use `karate-jersey` instead of `karate-apache`. -If you want to use [JUnit 5](#junit-5), use `karate-junit5` instead of `karate-junit4`. - -> The [Karate UI](https://github.com/intuit/karate/wiki/Karate-UI) is no longer part of the core framework from 0.9.3 onwards, and is an optional dependency called `karate-ui`. +If you want to use [JUnit 4](#junit-4), use `karate-junit4` instead of `karate-junit5`. ## Gradle Alternatively for [Gradle](https://gradle.org) you need these two entries: ```yml - testCompile 'com.intuit.karate:karate-junit4:0.9.4' - testCompile 'com.intuit.karate:karate-apache:0.9.4' + testCompile 'com.intuit.karate:karate-junit5:0.9.5' + testCompile 'com.intuit.karate:karate-apache:0.9.5' ``` +Also refer to the wiki for using [Karate with Gradle](https://github.com/intuit/karate/wiki/Gradle). + ### Quickstart It may be easier for you to use the Karate Maven archetype to create a skeleton project with one command. You can then skip the next few sections, as the `pom.xml`, recommended directory structure, sample test and [JUnit 5](#junit-5) runners - will be created for you. @@ -311,15 +317,13 @@ You can replace the values of `com.mycompany` and `myproject` as per your needs. mvn archetype:generate \ -DarchetypeGroupId=com.intuit.karate \ -DarchetypeArtifactId=karate-archetype \ --DarchetypeVersion=0.9.4 \ +-DarchetypeVersion=0.9.5 \ -DgroupId=com.mycompany \ -DartifactId=myproject ``` This will create a folder called `myproject` (or whatever you set the name to). -> There is an issue with the `0.9.4` quickstart, please read this as well: [fix for 0.9.4 Maven archetype](https://github.com/intuit/karate/issues/823#issuecomment-509608205). - ### IntelliJ Quickstart Refer to this video for [getting started with the free IntelliJ Community Edition](https://youtu.be/W-af7Cd8cMc). It simplifies the above process, since you only need to install IntelliJ. For Eclipse, refer to the wiki on [IDE Support](https://github.com/intuit/karate/wiki/IDE-Support). @@ -364,6 +368,9 @@ With the above in place, you don't have to keep switching between your `src/test Once you get used to this, you may even start wondering why projects need a `src/test/resources` folder at all ! +### Spring Boot Example +[Soumendra Daas](https://twitter.com/sdaas) has created a nice example and guide that you can use as a reference here: [`hello-karate`](https://github.com/Sdaas/hello-karate). This demonstrates a Java Maven + JUnit4 project set up to test a [Spring Boot](http://projects.spring.io/spring-boot/) app. + ## Naming Conventions Since these are tests and not production Java code, you don't need to be bound by the `com.mycompany.foo.bar` convention and the un-necessary explosion of sub-folders that ensues. We suggest that you have a folder hierarchy only one or two levels deep - where the folder names clearly identify which 'resource', 'entity' or API is the web-service under test. @@ -423,6 +430,8 @@ In some cases, for large payloads and especially when the default system encodin ``` ## JUnit 4 +> If you want to use JUnit 4, use the [`karate-junit4` Maven dependency](#maven) instead of `karate-junit5`. + To run a script `*.feature` file from your Java IDE, you just need the following empty test-class in the same package. The name of the class doesn't matter, and it will automatically run any `*.feature` file in the same package. This comes in useful because depending on how you organize your files and folders - you can have multiple feature files executed by a single JUnit test-class. ```java @@ -442,7 +451,7 @@ Refer to your IDE documentation for how to run a JUnit class. Typically right-c > Karate will traverse sub-directories and look for `*.feature` files. For example if you have the JUnit class in the `com.mycompany` package, `*.feature` files in `com.mycompany.foo` and `com.mycompany.bar` will also be run. This is one reason why you may want to prefer a 'flat' directory structure as [explained above](#naming-conventions). ## JUnit 5 -Karate supports JUnit 5 and the advantage is that you can have multiple methods in a test-class. Only one `import` is needed, and instead of a class-level annotation, you use a nice [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) and [fluent-api](https://en.wikipedia.org/wiki/Fluent_interface) to express which tests and tags you want to use. +Karate supports JUnit 5 and the advantage is that you can have multiple methods in a test-class. Only 1 `import` is needed, and instead of a class-level annotation, you use a nice [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) and [fluent-api](https://en.wikipedia.org/wiki/Fluent_interface) to express which tests and tags you want to use. Note that the Java class does not need to be `public` and even the test methods do not need to be `public` - so tests end up being very concise. @@ -457,19 +466,17 @@ class SampleTest { @Karate.Test Karate testSample() { - return new Karate().feature("sample").relativeTo(getClass()); + return Karate.run("sample").relativeTo(getClass()); } @Karate.Test Karate testTags() { - return new Karate().feature("tags").tags("@second").relativeTo(getClass()); + return Karate.run("tags").tags("@second").relativeTo(getClass()); } @Karate.Test Karate testFullPath() { - return new Karate() - .feature("classpath:karate/tags.feature") - .tags("@first"); + return Karate.run("classpath:karate/tags.feature").tags("@first"); } } @@ -485,8 +492,6 @@ You should be able to right-click and run a single method using your IDE - which ``` -> There is an issue with the `0.9.4` JUnit 5 dependencies, you will need to manually add [`junit-jupiter-engine` as a dependency](https://github.com/intuit/karate/issues/823#issuecomment-509608205). - To run a single test method, for example the `testTags()` in the example above, you can do this: ``` @@ -509,7 +514,7 @@ You can easily select (double-click), copy and paste this `file:` URL into your ## Karate Options To run only a specific feature file from a JUnit 4 test even if there are multiple `*.feature` files in the same folder (or sub-folders), use the `@KarateOptions` annotation. -> The [JUnit 5 support](#junit-5) does not require a class-level annotation to specify the feature(s) and tags to use. +> > If you want to use JUnit 4, use the [`karate-junit4` Maven dependency](#maven) instead of `karate-junit5`. The [JUnit 5 support](#junit-5) does not require a class-level annotation to specify the feature(s) and tags to use. ```java package animals.cats; @@ -635,61 +640,68 @@ The big drawback of the approach above is that you cannot run tests in parallel. And most importantly - you can run tests in parallel without having to depend on third-party hacks that introduce code-generation and config 'bloat' into your `pom.xml` or `build.gradle`. ## Parallel Execution -Karate can run tests in parallel, and dramatically cut down execution time. This is a 'core' feature and does not depend on JUnit, Maven or Gradle. +Karate can run tests in parallel, and dramatically cut down execution time. This is a 'core' feature and does not depend on JUnit, Maven or Gradle. + +* You can easily "choose" features and tags to run and compose test-suites in a very flexible manner. +* You can use the returned `Results` object to check if any scenarios failed, and to even summarize the errors +* [JUnit XML](https://wiki.jenkins-ci.org/display/JENKINS/JUnit+Plugin) reports will be generated in the "`reportDir`" path you specify, and you can easily configure your CI to look for these files after a build (for e.g. in `**/*.xml` or `**/surefire-reports/*.xml`) +* [Cucumber JSON reports](https://relishapp.com/cucumber/cucumber/docs/formatters/json-output-formatter) will be generated side-by-side with the JUnit XML reports and with the same name, except that the extension will be `.json` instead of `.xml` ### JUnit 4 Parallel Execution -> Important: **do not** use the `@RunWith(Karate.class)` annotation. This is a *normal* JUnit 4 test class ! +> Important: **do not** use the `@RunWith(Karate.class)` annotation. This is a *normal* JUnit 4 test class ! If you want to use JUnit 4, use the [`karate-junit4` Maven dependency](#maven) instead of `karate-junit5`. ```java -import com.intuit.karate.KarateOptions; import com.intuit.karate.Results; import com.intuit.karate.Runner; import static org.junit.Assert.*; import org.junit.Test; -@KarateOptions(tags = {"~@ignore"}) public class TestParallel { @Test public void testParallel() { - Results results = Runner.parallel(getClass(), 5, "target/surefire-reports"); + Results results = Runner.path("classpath:some/package").tags("~@ignore").parallel(5); assertTrue(results.getErrorMessages(), results.getFailCount() == 0); } } ``` +* You don't use a JUnit runner (no `@RunWith` annotation), and you write a plain vanilla JUnit test (it could even be a normal Java class with a `main` method) +* The `Runner.path()` "builder" method in `karate-core` is how you refer to the package you want to execute, and all feature files within sub-directories will be picked up +* `Runner.path()` takes multiple string parameters, so you can refer to multiple packages or even individual `*.feature` files and easily "compose" a test-suite + * e.g. `Runner.path("classpath:animals", "classpath:some/other/package.feature")` +* To [choose tags](#tags), call the `tags()` API, note that in the example above, any `*.feature` file tagged as `@ignore` will be skipped - as the `~` prefix means a "NOT" operation. You can also specify tags on the [command-line](#test-suites). The `tags()` method also takes multiple arguments, for e.g. + * this is an "AND" operation: `tags("@customer", "@smoke")` + * and this is an "OR" operation: `tags("@customer,@smoke")` +* There is an optional `reportDir()` method if you want to customize the directory to which the [XML and JSON](#parallel-execution) will be output, it defaults to `target/surefire-reports` +* If you want to dynamically and programmatically determine the tags and features to be included - the API also accepts `List` as the `path()` and `tags()` methods arguments +* `parallel()` *has* to be the last method called, and you pass the number of parallel threads needed. It returns a `Results` object that has all the information you need - such as the number of passed or failed tests. + ### JUnit 5 Parallel Execution -For [JUnit 5](#junit-5) you can omit the `public` modifier for the class and method, and there are some changes to `import` package names. And the method signature of the `assertTrue` has flipped around a bit: +For [JUnit 5](#junit-5) you can omit the `public` modifier for the class and method, and there are some changes to `import` package names. The method signature of the `assertTrue` has flipped around a bit. Also note that you don't use `@Karate.Test` for the method, and you just use the *normal* JUnit 5 `@Test` annotation. + +Else the `Runner.path()` "builder" API is the same, refer the description above for [JUnit 4](#junit-4-parallel-execution). ```java -import com.intuit.karate.KarateOptions; import com.intuit.karate.Results; import com.intuit.karate.Runner; import static org.junit.jupiter.api.Assertions.*; import org.junit.jupiter.api.Test; -@KarateOptions(tags = {"~@ignore"}) class TestParallel { @Test void testParallel() { - Results results = Runner.parallel(getClass(), 5, "target/surefire-reports"); - assertTrue(results.getFailCount() == 0, results.getErrorMessages()); + Results results = Runner.path("classpath:animals").tags("~@ignore").parallel(5); + assertEquals(0, results.getFailCount(), results.getErrorMessages()); } } ``` -Things to note: -* For JUnit 4 - you don't use a JUnit runner (no `@RunWith` annotation), and you write a plain vanilla JUnit test (it could even be a normal Java class with a `main` method) using the `Runner.parallel()` static method in `karate-core`. -* You can use the returned `Results` object to check if any scenarios failed, and to even summarize the errors -* The first argument can be any class that marks the 'root package' in which `*.feature` files will be looked for, and sub-directories will be also scanned. As shown above you would typically refer to the enclosing test-class itself. If the class you refer to has a `@KarateOptions` annotation, it will be processed (see below). -* The second argument is the number of threads to use. -* [JUnit XML](https://wiki.jenkins-ci.org/display/JENKINS/JUnit+Plugin) reports will be generated in the path you specify as the third parameter, and you can easily configure your CI to look for these files after a build (for e.g. in `**/*.xml` or `**/surefire-reports/*.xml`). This argument is optional and will default to `target/surefire-reports`. -* [Cucumber JSON reports](https://relishapp.com/cucumber/cucumber/docs/formatters/json-output-formatter) will be generated side-by-side with the JUnit XML reports and with the same name, except that the extension will be `.json` instead of `.xml`. -* Options passed to `@KarateOptions` would work as expected, provided you point the `Runner` to the annotated class as the first argument. Note that in this example, any `*.feature` file tagged as `@ignore` will be skipped. You can also specify tags on the [command-line](#test-suites). -* For convenience, some stats are logged to the console when execution completes, which should look something like this: +### Parallel Stats +For convenience, some stats are logged to the console when execution completes, which should look something like this: ``` ====================================================== @@ -706,8 +718,6 @@ A `timeline.html` file will also be saved to the report output directory mention ### `@parallel=false` In rare cases you may want to suppress the default of `Scenario`-s executing in parallel and the special [`tag`](#tags) `@parallel=false` can be used. If you place it above the [`Feature`](#script-structure) keyword, it will apply to all `Scenario`-s. And if you just want one or two `Scenario`-s to NOT run in parallel, you can place this tag above only *those* `Scenario`-s. See [example](karate-demo/src/test/java/demo/encoding/encoding.feature). -> There is also an API to run a chosen set of features (and tags) which may be useful in cases where you dynamically want to select features at run time. Refer to this example [`DemoTestSelected.java`](karate-demo/src/test/java/demo/DemoTestSelected.java) - ## Test Reports As mentioned above, most CI tools would be able to process the JUnit XML output of the [parallel runner](#parallel-execution) and determine the status of the build as well as generate reports. @@ -721,7 +731,7 @@ This report is recommended especially because Karate's integration includes the -The demo also features [code-coverage using Jacoco](karate-demo#code-coverage-using-jacoco). Some third-party report-server solutions integrate with Karate such as [ReportPortal.io](https://github.com/reportportal/agent-java-karate). +The demo also features [code-coverage using Jacoco](karate-demo#code-coverage-using-jacoco), and some tips for even non-Java back-ends. Some third-party report-server solutions integrate with Karate such as [ReportPortal.io](https://github.com/reportportal/agent-java-karate). ## Logging > This is optional, and Karate will work without the logging config in place, but the default console logging may be too verbose for your needs. @@ -811,6 +821,41 @@ This approach is indeed slightly more complicated than traditional `*.properties And there is no more worrying about Maven profiles and whether the 'right' `*.properties` file has been copied to the proper place. +### Restrictions on Global Variables +Non-JSON values such as Java object references or JS functions are supported only if they are at the "root" of the JSON returned from [`karate-config.js`](#karate-configjs). So this below will *not* work: + +```javascript +function fn() { + var config = {}; + config.utils = {}; + config.utils.uuid = function(){ return java.util.UUID.randomUUID() + '' }; + // this is wrong, the "nested" uuid will be lost + return config; +} +``` + +The recommended best-practice is to move the `uuid` function into a common feature file following the pattern described [here](#multiple-functions-in-one-file): + +```javascript +function fn() { + var config = {}; + config.utils = karate.call('utils.feature') + return config; +} +``` + +But you can opt for using [`karate.toMap()`](#karate-tomap) which will "wrap" things so that the nested objects are not "lost": + +```javascript +function fn() { + var config = {}; + var utils = {}; + utils.uuid = function(){ return java.util.UUID.randomUUID() + '' }; + config.utils = karate.toMap(utils); + return config; +} +``` + ## Switching the Environment There is only one thing you need to do to switch the environment - which is to set a Java system property. @@ -883,7 +928,7 @@ Advanced users who build frameworks on top of Karate have the option to supply a ## Script Structure Karate scripts are technically in '[Gherkin](https://docs.cucumber.io/gherkin/reference/)' format - but all you need to grok as someone who needs to test web-services are the three sections: `Feature`, `Background` and `Scenario`. There can be multiple Scenario-s in a `*.feature` file, and at least one should be present. The `Background` is optional. -> Variables set using [`def`](#def) in the `Background` will be re-set before *every* `Scenario`. If you are looking for a way to do something only **once** per `Feature`, take a look at [`callonce`](#callonce). On the other hand, if you are expecting a variable in the `Background` to be modified by one `Scenario` so that later ones can see the updated value - that is *not* how you should think of them, and you should combine your 'flow' into one scenario. Keep in mind that you should be able to comment-out a `Scenario` or skip some via [`tags`](#tags) without impacting any others. Note that the [parallel runner](#parallel-execution) will run `Scenario`-s in parallel, which means they can run in *any* order. +> Variables set using [`def`](#def) in the `Background` will be re-set before *every* `Scenario`. If you are looking for a way to do something only **once** per `Feature`, take a look at [`callonce`](#callonce). On the other hand, if you are expecting a variable in the `Background` to be modified by one `Scenario` so that later ones can see the updated value - that is *not* how you should think of them, and you should combine your 'flow' into one scenario. Keep in mind that you should be able to comment-out a `Scenario` or skip some via [`tags`](#tags) without impacting any others. Note that the [parallel runner](#parallel-execution) will run `Scenario`-s in parallel, which means they can run in *any* order. If you are looking for ways to do something only *once* per feature or across *all* your tests, see [Hooks](#hooks). Lines that start with a `#` are comments. ```cucumber @@ -1068,7 +1113,7 @@ A few special built-in variables such as `$` (which is a [reference to the JSON A [special case](#remove-if-null) of embedded expressions can remove a JSON key (or XML element / attribute) if the expression evaluates to `null`. #### Rules for Embedded Expressions -They work only within JSON or XML. And the expression *has* to start with `"#(` and end with `)` - so note that string-concatenation may not work quite the way you expect: +* They work only within JSON or XML and when on the Right Hand Side of a `def` or `match` or when you [`read()`](#reading-files) a JSON or XML file. And the expression *has* to start with `"#(` and end with `)` - so note that string-concatenation may not work quite the way you expect: ```cucumber # wrong ! @@ -1106,7 +1151,7 @@ And def lang = 'en' > So how would you choose between the two approaches to create JSON ? [Embedded expressions](#embedded-expressions) are useful when you have complex JSON [`read`](#reading-files) from files, because you can auto-replace (or even [remove](#remove-if-null)) data-elements with values dynamically evaluated from variables. And the JSON will still be 'well-formed', and editable in your IDE or text-editor. Embedded expressions also make more sense in [validation](#ignore-or-validate) and [schema-like](#schema-validation) short-cut situations. It can also be argued that the `#` symbol is easy to spot when eyeballing your test scripts - which makes things more readable and clear. ### Multi-Line Expressions -The keywords [`def`](#def), [`set`](#set), [`match`](#match), [`request`](#request) and [`eval`](#eval) take multi-line input as the last argument. This is useful when you want to express a one-off lengthy snippet of text in-line, without having to split it out into a separate [file](#reading-files). Here are some examples: +The keywords [`def`](#def), [`set`](#set), [`match`](#match), [`request`](#request) and [`eval`](#eval) take multi-line input as the last argument. This is useful when you want to express a one-off lengthy snippet of text in-line, without having to split it out into a separate [file](#reading-files). Note how triple-quotes (`"""`) are used to enclose content. Here are some examples: ```cucumber # instead of: @@ -1289,7 +1334,7 @@ For those who may prefer [YAML](http://yaml.org) as a simpler way to represent d ``` ### `yaml` -A very rare need is to be able to convert a string which happens to be in YAML form into JSON, and this can be done via the `yaml` type cast keyword. For example - if a response data element or downloaded file is YAML and you need to use the data in subsequent steps. +A very rare need is to be able to convert a string which happens to be in YAML form into JSON, and this can be done via the `yaml` type cast keyword. For example - if a response data element or downloaded file is YAML and you need to use the data in subsequent steps. Also see [type conversion](#type-conversion). ```cucumber * text foo = @@ -1321,7 +1366,7 @@ Karate can read `*.csv` files and will auto-convert them to JSON. A header row i In rare cases you may want to use a csv-file as-is and *not* auto-convert it to JSON. A good example is when you want to use a CSV file as the [request-body](#request) for a file-upload. You could get by by renaming the file-extension to say `*.txt` but an alternative is to use the [`karate.readAsString()`](#read-file-as-string) API. ### `csv` -Just like [`yaml`](#yaml), you may occasionally need to convert a string which happens to be in CSV form into JSON, and this can be done via the `csv` keyword. +Just like [`yaml`](#yaml), you may occasionally need to [convert a string](#type-conversion) which happens to be in CSV form into JSON, and this can be done via the `csv` keyword. ```cucumber * text foo = @@ -1483,7 +1528,6 @@ Then status 202 ``` ## Type Conversion - > Best practice is to stick to using only [`def`](#def) unless there is a very good reason to do otherwise. Internally, Karate will auto-convert JSON (and even XML) to Java `Map` objects. And JSON arrays would become Java `List`-s. But you will never need to worry about this internal data-representation most of the time. @@ -1760,7 +1804,11 @@ Note that any cookies returned in the HTTP response would be automatically set f Also refer to the built-in variable [`responseCookies`](#responsecookies) for how you can access and perform assertions on cookie data values. ## `form field` -HTML form fields would be URL-encoded when the HTTP request is submitted (by the [`method`](#method) step). You would typically use these to simulate a user sign-in and then grab a security token from the [`response`](#response). For example: +HTML form fields would be URL-encoded when the HTTP request is submitted (by the [`method`](#method) step). You would typically use these to simulate a user sign-in and then grab a security token from the [`response`](#response). + +Note that the `Content-Type` header will be automatically set to: `application/x-www-form-urlencoded`. You just need to do a normal `POST` (or `GET`). + +For example: ```cucumber Given path 'login' @@ -1950,16 +1998,22 @@ You can adjust configuration settings for the HTTP client used by Karate using t `readTimeout` | integer | Set the read timeout (milliseconds). The default is 30000 (30 seconds). `proxy` | string | Set the URI of the HTTP proxy to use. `proxy` | JSON | For a proxy that requires authentication, set the `uri`, `username` and `password`, see example below. Also a `nonProxyHosts` key is supported which can take a list for e.g. `{ uri: 'http://my.proxy.host:8080', nonProxyHosts: ['host1', 'host2']}` +`localAddress` | string | see [`karate-gatling`](karate-gatling#configure-localaddress) `charset` | string | The charset that will be sent in the request `Content-Type` which defaults to `utf-8`. You typically never need to change this, and you can over-ride (or disable) this per-request if needed via the [`header`](#header) keyword ([example](karate-demo/src/test/java/demo/headers/content-type.feature)). `retry` | JSON | defaults to `{ count: 3, interval: 3000 }` - see [`retry until`](#retry-until) `outlineVariablesAuto` | boolean | defaults to `true`, whether each key-value pair in the `Scenario Outline` example-row is automatically injected into the context as a variable (and not just `__row`), see [`Scenario Outline` Enhancements](#scenario-outline-enhancements) `lowerCaseResponseHeaders` | boolean | Converts every key and value in the [`responseHeaders`](#responseheaders) to lower-case which makes it easier to validate for e.g. using [`match header`](#match-header) (default `false`) [(example)](karate-demo/src/test/java/demo/headers/content-type.feature). -`httpClientClass` | string | See [karate-mock-servlet](karate-mock-servlet) -`httpClientInstance` | Java Object | See [karate-mock-servlet](karate-mock-servlet) -`userDefined` | JSON | See [karate-mock-servlet](karate-mock-servlet) -`responseHeaders` | JSON / JS function | See [karate-netty](karate-netty#configure-responseheaders) -`cors` | boolean | See [karate-netty](karate-netty##configure-cors) - + `abortedStepsShouldPass` | boolean | defaults to `false`, whether steps after a [`karate.abort()`](#karate-abort) should be marked as `PASSED` instead of `SKIPPED` - this can impact the behavior of 3rd-party reports, see [this issue](https://github.com/intuit/karate/issues/755) for details +`logModifier` | Java Object | See [Log Masking](#log-masking) +`httpClientClass` | string | See [`karate-mock-servlet`](karate-mock-servlet) +`httpClientInstance` | Java Object | See [`karate-mock-servlet`](karate-mock-servlet) +`userDefined` | JSON | See [`karate-mock-servlet`](karate-mock-servlet) +`responseHeaders` | JSON / JS function | See [`karate-netty`](karate-netty#configure-responseheaders) +`cors` | boolean | See [`karate-netty`](karate-netty#configure-cors) +`driver` | JSON | See [UI Automation](karate-core) +`driverTarget` | JSON / Java Object | See [`configure driverTarget`](karate-core#configure-drivertarget) + +> If you are mixing Karate into an existing Java project via Maven or Gradle, it can happen that the version of the Apache HTTP client used by Karate conflicts with something in the existing classpath. This can cause `* configure ssl = true` to fail. Read [this answer on Stack Overflow](https://stackoverflow.com/a/52396293/143475) for more details. Examples: ```cucumber @@ -2011,6 +2065,33 @@ And this short-cut is also supported which will disable all logs: * configure report = false ``` +Since you can use `configure` any time within a test, you have control over which requests or steps you want to show / hide. + +### Log Masking +In cases where you want to "mask" values which are sensitive from a security point of view from the output files, logs and HTML reports, you can implement the [`HttpLogModifier`](karate-core/src/main/java/com/intuit/karate/http/HttpLogModifier.java) and tell Karate to use it via the [`configure`](#configure) keyword. Here is an [example](karate-demo/src/test/java/demo/headers/DemoLogModifier.java) of an implementation. For performance reasons, you can implement `enableForUri()` so that this "activates" only for some URL patterns. + +Instantiating a Java class and using this in a test is easy (see [example](karate-demo/src/test/java/demo/headers/headers-masking.feature)): + +```cucumber +# if this was in karate-config.js, it would apply "globally" +* def LM = Java.type('demo.headers.DemoLogModifier') +* configure logModifier = new LM() +``` + +Or globally in [`karate-config.js`](#karate-configjs) + +```js +var LM = Java.type('demo.headers.DemoLogModifier'); +karate.configure('logModifier', new LM()); +``` + +Since `karate-config.js` is processed for every `Scenario`, you can use a singleton instead of calling `new` every time. Something like this: + +```js +var LM = Java.type('demo.headers.DemoLogModifier'); +karate.configure('logModifier', LM.INSTANCE); +``` + ### System Properties for SSL and HTTP proxy For HTTPS / SSL, you can also specify a custom certificate or trust store by [setting Java system properties](https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/JSSERefGuide.html#InstallationAndCustomization). And similarly - for [specifying the HTTP proxy](https://docs.oracle.com/javase/8/docs/technotes/guides/net/proxies.html). @@ -2032,12 +2113,12 @@ Key | Type | Required? | Description Example: ``` # enable X509 certificate authentication with PKCS12 file 'certstore.pfx' and password 'certpassword' -* configure ssl = { keyStore: 'classpath:certstore.pfx', keyStorePassword: 'certpassword', keyStoreType: 'pkcs12' }; +* configure ssl = { keyStore: 'classpath:certstore.pfx', keyStorePassword: 'certpassword', keyStoreType: 'pkcs12' } ``` ``` # trust all server certificates, in the feature file -* configure ssl = { trustAll: true }; +* configure ssl = { trustAll: true } ``` ``` @@ -2045,6 +2126,8 @@ Example: karate.configure('ssl', { trustAll: true }); ``` +For end-to-end examples in the Karate demos, look at the files in [this folder](karate-demo/src/test/java/ssl). + # Payload Assertions ## Prepare, Mutate, Assert. Now it should be clear how Karate makes it easy to express JSON or XML. If you [read from a file](#reading-files), the advantage is that multiple scripts can re-use the same data. @@ -2120,7 +2203,7 @@ If you are wondering about the finer details of the `match` syntax, the left-han * variable name - e.g. `foo` * a 'named' JsonPath or XPath expression - e.g. `foo.bar` * any valid function or method call - e.g. `foo.bar()` or `foo.bar('hello').baz` -* or anything wrapped in parantheses which will be evaluated - e.g. `(foo + bar)` or `(42)` +* or anything wrapped in parentheses which will be evaluated - e.g. `(foo + bar)` or `(42)` And the right-hand-side can be any valid [Karate expression](#karate-expressions). Refer to the section on [JsonPath short-cuts](#jsonpath-short-cuts) for a deeper understanding of 'named' JsonPath expressions in Karate. @@ -2428,6 +2511,14 @@ In some cases where the response JSON is wildly dynamic, you may want to only ch # * match foo == { bar:1, baz: 'hello' } ``` +Note that `match contains` will "recurse", so any nested JSON chunks will also be matched using `match contains`: + +```cucumber +* def original = { a: 1, b: 2, c: 3, d: { a: 1, b: 2 } } +* def expected = { a: 1, c: 3, d: { b: 2 } } +* match original contains expected +``` + Also note that [`match contains any`](#match-contains-any) is possible for JSON objects as well as JSON arrays. ### (not) `!contains` @@ -2545,6 +2636,10 @@ The `match` keyword can be made to iterate over all elements in a JSON array usi * match each data.foo contains { baz: "#? _ != 'z'" } * def isAbc = function(x) { return x == 'a' || x == 'b' || x == 'c' } * match each data.foo contains { baz: '#? isAbc(_)' } + +# this is also possible, see the subtle difference from the above +* def isXabc = function(x) { return x.baz == 'a' || x.baz == 'b' || x.baz == 'c' } +* match each data.foo == '#? isXabc(_)' ``` Here is a contrived example that uses `match each`, [`contains`](#match-contains) and the [`#?`](#self-validation-expressions) 'predicate' marker to validate that the value of `totalPrice` is always equal to the `roomPrice` of the first item in the `roomInformation` array. @@ -3004,6 +3099,17 @@ Then status 201 And assert responseTime < 1000 ``` +## `responseType` +Karate will attempt to parse the raw HTTP response body as JSON or XML and make it available as the [`response`](#response) value. If parsing fails, Karate will log a warning and the value of `response` will then be a plain string. You can still perform string comparisons such as a [`match contains`](#match-text-or-binary) and look for error messages etc. In rare cases, you may want to check what the "type" of the `response` is and it can be one of 3 different values: `json`, `xml` and `string`. + +So if you really wanted to assert that the HTTP response body is well-formed JSON or XML you can do this: + +```cucumber +When method post +Then status 201 +And match responseType == 'json' +``` + ## `requestTimeStamp` Very rarely used - but you can get the Java system-time (for the current [`response`](#response)) at the point when the HTTP request was initiated (the value of `System.currentTimeMillis()`) which can be used for detailed logging or custom framework / stats calculations. @@ -3022,7 +3128,7 @@ This makes setting up of complex authentication schemes for your test-flows real Here is an example JavaScript function that uses some variables in the context (which have been possibly set as the result of a sign-in) to build the `Authorization` header. Note how even [calls to Java code](#calling-java) can be made if needed. -> In the example below, note the use of the [`karate.get()`](#karate-get) helper for getting the value of a dynamic variable. This is preferred because it takes care of situations such as if the value is `undefined` in JavaScript. In rare cases you may need to *set* a variable from this routine, and a good example is to make the generated UUID "visible" to the currently executing script or feature. You can easily do this via [`karate.set('someVarName', value)`](#karate-set). +> In the example below, note the use of the [`karate.get()`](#karate-get) helper for getting the value of a dynamic variable (which was *not set* at the time this JS `function` was *declared*). This is preferred because it takes care of situations such as if the value is `undefined` in JavaScript. In rare cases you may need to *set* a variable from this routine, and a good example is to make the generated UUID "visible" to the currently executing script or feature. You can easily do this via [`karate.set('someVarName', value)`](#karate-set). ```javascript function fn() { @@ -3057,9 +3163,9 @@ A JavaScript function or [Karate expression](#karate-expressions) at runtime has Operation | Description --------- | ----------- -karate.abort() | you can prematurely exit a `Scenario` by combining this with [conditional logic](#conditional-logic) like so: `* if (condition) karate.abort()` - please use [sparingly](https://martinfowler.com/articles/nonDeterminism.html) ! +karate.abort() | you can prematurely exit a `Scenario` by combining this with [conditional logic](#conditional-logic) like so: `* if (condition) karate.abort()` - please use [sparingly](https://martinfowler.com/articles/nonDeterminism.html) ! and also see [`configure abortedStepsShouldPass`](#configure) karate.append(... items) | useful to create lists out of items (which can be lists as well), see [JSON transforms](#json-transforms) -karate.appendTo(name, ... items) | useful to append to a list-like variable (that has to exist) in scope, see [JSON transforms](#json-transforms) +karate.appendTo(name, ... items) | useful to append to a list-like variable (that has to exist) in scope, see [JSON transforms](#json-transforms) - the first argument can be a reference to an array-like variable or even the name (string) of an existing variable which is list-like karate.call(fileName, [arg]) | invoke a [`*.feature` file](#calling-other-feature-files) or a [JavaScript function](#calling-javascript-functions) the same way that [`call`](#call) works (with an optional solitary argument) karate.callSingle(fileName, [arg]) | like the above, but guaranteed to run **only once** even across multiple features *and* parallel threads (recommended only for advanced users) - refer to this example: [`karate-config.js`](karate-demo/src/test/java/karate-config.js) / [`headers-single.feature`](karate-demo/src/test/java/demo/headers/headers-single.feature) karate.configure(key, value) | does the same thing as the [`configure`](#configure) keyword, and a very useful example is to do `karate.configure('connectTimeout', 5000);` in [`karate-config.js`](#configuration) - which has the 'global' effect of not wasting time if a connection cannot be established within 5 seconds @@ -3070,12 +3176,12 @@ Operation | Description karate.filter(list, predicate) | functional-style 'filter' operation useful to filter list-like objects (e.g. JSON arrays), see [example](karate-junit4/src/test/java/com/intuit/karate/junit4/demos/js-arrays.feature), the second argument has to be a JS function (item, [index]) that returns a `boolean` karate.filterKeys(map, keys) | extracts a sub-set of key-value pairs from the first argument, the second argument can be a list (or varargs) of keys - or even another JSON where only the keys would be used for extraction, [example](karate-junit4/src/test/java/com/intuit/karate/junit4/demos/js-arrays.feature) `karate.forEach(list, function)` | functional-style 'loop' operation useful to traverse list-like (or even map-like) objects (e.g. JSON / arrays), see [example](karate-junit4/src/test/java/com/intuit/karate/junit4/demos/js-arrays.feature), the second argument has to be a JS function (item, [index]) for lists and (key, [value], [index]) for JSON / maps -karate.get(name) | get the value of a variable by name (or JsonPath expression), if not found - this returns `null` which is easier to handle in JavaScript (than `undefined`) +karate.get(name, [default]) | get the value of a variable by name (or JsonPath expression), if not found - this returns `null` which is easier to handle in JavaScript (than `undefined`), and an optional second argument can be used to return a "default" value, very useful to set variables in called features that have not been pre-defined karate.info | within a test (or within the [`afterScenario`](#configure) function if configured) you can access metadata such as the `Scenario` name, refer to this example: [`hooks.feature`](karate-demo/src/test/java/demo/hooks/hooks.feature) karate.jsonPath(json, expression) | brings the power of [JsonPath](https://github.com/json-path/JsonPath) into JavaScript, and you can find an example [here](karate-junit4/src/test/java/com/intuit/karate/junit4/demos/js-arrays.feature). karate.keysOf(object) | returns only the keys of a map-like object karate.listen(timeout) | wait until [`karate.signal(result)`](#karate-signal) has been called or time-out after `timeout` milliseconds, see [async](#async) -karate.log(... args) | log to the same logger (and log file) being used by the parent process, logging can be suppressed with [`configure printEnabled`](#configure) set to `false` +karate.log(... args) | log to the same logger (and log file) being used by the parent process, logging can be suppressed with [`configure printEnabled`](#configure) set to `false`, and just like [`print`](#print) - use comma-separated values to "pretty print" JSON or XML karate.lowerCase(object) | useful to brute-force all keys and values in a JSON or XML payload to lower-case, useful in some cases, see [example](karate-junit4/src/test/java/com/intuit/karate/junit4/demos/lower-case.feature) karate.map(list, function) | functional-style 'map' operation useful to transform list-like objects (e.g. JSON arrays), see [example](karate-junit4/src/test/java/com/intuit/karate/junit4/demos/js-arrays.feature), the second argument has to be a JS function (item, [index]) karate.mapWithKey(list, string) | convenient for the common case of transforming an array of primitives into an array of objects, see [JSON transforms](#json-transforms) @@ -3096,13 +3202,17 @@ Operation | Description karate.setXml(name, xmlString) | rarely used, refer to the example above karate.signal(result) | trigger an event that [`karate.listen(timeout)`](#karate-listen) is waiting for, and pass the data, see [async](#async) karate.sizeOf(object) | returns the size of the map-like or list-like object +karate.stop() | will pause the test execution until a socket connection is made to the port logged to the console, useful for troubleshooting UI tests without using a [de-bugger](https://twitter.com/KarateDSL/status/1167533484560142336), of course - *NEVER* forget to remove this after use ! +karate.target(object) | currently for web-ui automation only, see [target lifecycle](karate-core#target-lifecycle) karate.tags | for advanced users - scripts can introspect the tags that apply to the current scope, refer to this example: [`tags.feature`](karate-junit4/src/test/java/com/intuit/karate/junit4/demos/tags.feature) karate.tagValues | for even more advanced users - Karate natively supports tags in a `@name=val1,val2` format, and there is an inheritance mechanism where `Scenario` level tags can over-ride `Feature` level tags, refer to this example: [`tags.feature`](karate-junit4/src/test/java/com/intuit/karate/junit4/demos/tags.feature) karate.toBean(json, className) | converts a JSON string or map-like object into a Java object, given the Java class name as the second argument, refer to this [file](karate-junit4/src/test/java/com/intuit/karate/junit4/demos/type-conv.feature) for an example karate.toJson(object) | converts a Java object into JSON, and `karate.toJson(object, true)` will strip all keys that have `null` values from the resulting JSON, convenient for unit-testing Java code, see [example](karate-demo/src/test/java/demo/unit/cat.feature) +karate.toList(object) | rarely used - when still within a JS block, you need to convert a JSON array into a Java `List` +karate.toMap(object) | rarely used - when still within a JS block, you need to convert a JSON object into a Java `Map`, or you are setting up [complex global variables](#restrictions-on-global-variables) karate.valuesOf(object) | returns only the values of a map-like object (or itself if a list-like object) karate.webSocket(url, handler) | see [websocket](#websocket) -karate.write(object, path) | writes the bytes of `object` to a path which will *always* be relative to the "build" directory (typically `target`), see this example: [`embed-pdf.js`](karate-demo/src/test/java/demo/embed/embed-pdf.js) - and this method returns a `java.io.File` reference to the file created / written to +karate.write(object, path) | *normally not recommended, please [read this first](https://stackoverflow.com/a/54593057/143475)* - writes the bytes of `object` to a path which will *always* be relative to the "build" directory (typically `target`), see this example: [`embed-pdf.js`](karate-demo/src/test/java/demo/embed/embed-pdf.js) - and this method returns a `java.io.File` reference to the file created / written to karate.xmlPath(xml, expression) | Just like [`karate.jsonPath()`](#karate-jsonpath) - but for XML, and allows you to use dynamic XPath if needed, see [example](karate-junit4/src/test/java/com/intuit/karate/junit4/xml/xml.feature). # Code Reuse / Common Routines @@ -3116,6 +3226,8 @@ When you have a sequence of HTTP calls that need to be repeated for multiple tes Here is an example of using the `call` keyword to invoke another feature file, loaded using the [`read`](#reading-files) function: +> If you find this hard to understand at first, try looking at this [set of examples](karate-demo/src/test/java/demo/callfeature/call-feature.feature). + ```cucumber Feature: which makes a 'call' to another re-usable feature @@ -3228,6 +3340,16 @@ Variable | Refers To Refer to this [demo feature](karate-demo) for an example: [`kitten-create.feature`](karate-demo/src/test/java/demo/calltable/kitten-create.feature) +### Default Values +Some users need "callable" features that are re-usable even when variables have not been defined by the calling feature. Normally an undefined variable results in nasty JavaScript errors. But there is an elegant way you can specify a default value using the [`karate.get()`](#karate-get) API: + +```cucumber +# if foo is not defined, it will default to 42 +* def foo = karate.get('foo', 42) +``` + +> A word of caution: we recommend that you should not over-use Karate's capability of being able to re-use features. Re-use can sometimes result in negative benefits - especially when applied to test-automation. Prefer readability over re-use. See this for an [example](https://stackoverflow.com/a/54126724/143475). + ### `copy` For a [`call`](#call) (or [`callonce`](#callonce)) - payload / data structures (JSON, XML, Map-like or List-like) variables are 'passed by reference' which means that steps within the 'called' feature can update or 'mutate' them, for e.g. using the [`set`](#set) keyword. This is actually the intent most of the time and is convenient. If you want to pass a 'clone' to a 'called' feature, you can do so using the rarely used `copy` keyword that works very similar to [type conversion](#type-conversion). This is best explained in the last scenario of this example: [`copy-caller.feature`](karate-junit4/src/test/java/com/intuit/karate/junit4/demos/copy-caller.feature) @@ -3294,6 +3416,24 @@ Shared | [`call-updates-config.feature`](karate-demo/src/test/java/demo/headers/ > Once you get comfortable with Karate, you can consider moving your authentication flow into a 'global' one-time flow using [`karate.callSingle()`](#karate-callsingle), think of it as '[`callonce`](#callonce) on steroids'. +#### `call` vs `read()` +Since this is a frequently asked question, the different ways of being able to re-use code (or data) are summarized below. + +Code | Description +---- | ----------- +`* def login = read('login.feature')`
`* call login` | [Shared Scope](#shared-scope), and the
`login` variable can be re-used +`* call read('login.feature')` | short-cut for the above
without needing a variable +`* def credentials = read('credentials.json')`
`* def login = read('login.feature')`
`* call login credentials` | Note how using [`read()`](#reading-files)
for a JSON file returns *data* -
not "callable" code, and here it is
used as the [`call`](#call) argument +`* call read('login.feature') read('credentials.json')` | You *can* do this in theory,
but it is not as readable as the above +`* karate.call('login.feature')` | The [JS API](#karate-call) allows you to do this,
but this will *not* be [Shared Scope](#shared-scope) +`* def result = call read('login.feature')` | [`call`](#call) result assigned to a variable
and *not* [Shared Scope](#shared-scope) +`* def result = karate.call('login.feature')` | exactly equivalent to the above ! +`* def credentials = read('credentials.json')`
`* def result = call read('login.feature') credentials` | like the above,
but with a [`call`](#call) argument +`* def credentials = read('credentials.json')`
`* def result = karate.call('login.feature', credentials)` | like the above, but in [JS API](#karate-call) form,
the advantage of the above form is
that using an in-line argument is less
"cluttered" (see next row) +`* def login = read('login.feature')`
`* def result = call login { user: 'john', password: 'secret' }` | using the `call` keyword makes
passing an in-line JSON argument
more "readable" +`* call read 'credentials.json'` | Since "`read`" happens to be a
[*function*](#calling-javascript-functions) (that takes a single
string argument), this has the effect
of loading *all* keys in the JSON file
into [Shared Scope](#shared-scope) as [variables](#def) !
This *can* be [sometimes handy](karate-core#locator-lookup). +`* call read ('credentials.json')` | A common mistake. First, there
is no meaning in `call` for JSON.
Second, the space after the "`read`"
makes this equal to the above. + ### Calling Java There are examples of calling JVM classes in the section on [Java Interop](#java-interop) and in the [file-upload demo](karate-demo). Also look at the section on [commonly needed utilities](#commonly-needed-utilities) for more ideas. @@ -3465,6 +3605,14 @@ In some rare cases you need to exit a `Scenario` based on some condition. You ca * if (responseStatus == 404) karate.abort() ``` +Using `karate.abort()` will *not* fail the test. Conditionally making a test fail is easy with JavaScript: + +```cucumber +* if (condition) throw 'a custom message' +``` + +But normally a [`match`](#match) statement is preferred unless you want a really descriptive error message. + Also refer to [polling](#polling) for more ideas. ## Commonly Needed Utilities @@ -3590,7 +3738,7 @@ A little-known capability of the Cucumber / Gherkin syntax is to be able to tag ```cucumber Scenario Outline: examples partitioned by tag * def vals = karate.tagValues -* match vals.region[0] == '' +* match vals.region[0] == expected @region=US Examples: @@ -3624,7 +3772,7 @@ Instead, Karate gives you all you need as part of the syntax. Here is a summary: To Run Some Code | How ---------------- | --- -Before *everything* (or 'globally' once) | Use [`karate.callSingle()`](#karate-callsingle) in [`karate-config.js`](#karate-configjs). Only recommended for advanced users, but this guarantees a routine is run only once, *even* when [running tests in parallel](#parallel-execution). +Before *everything* (or 'globally' once) | Use [`karate.callSingle()`](#karate-callsingle) in [`karate-config.js`](#karate-configjs). Only recommended for advanced users, but this guarantees a routine is run only once, *even* when [running tests in parallel](#parallel-execution). You *can* use this directly in a `*.feature` file, but it logically fits better in the global "bootstrap". Before every `Scenario` | Use the [`Background`](#script-structure). Note that [`karate-config.js`](#karate-configjs) is processed before *every* `Scenario` - so you can choose to put "global" config here, for example using [`karate.configure()`](#karate-configure). Once (or at the start of) every `Feature` | Use a [`callonce`](#callonce) in the [`Background`](#script-structure). The advantage is that you can set up variables (using [`def`](#def) if needed) which can be used in all `Scenario`-s within that `Feature`. After every `Scenario` | [`configure afterScenario`](#configure) (see [example](karate-demo/src/test/java/demo/hooks/hooks.feature)) @@ -3728,7 +3876,7 @@ The limitation of the Cucumber `Scenario Outline:` (seen above) is that the numb Also see the option below, where you can data-drive an `Examples:` table using JSON. ### Dynamic Scenario Outline -You can feed an `Examples` table from a JSON array, which is great for those situations where the table-content is dynamically resolved at run-time. This capability is triggered when the table consists of a single "cell", i.e. there is exactly one row and one column in the table. Here is an example: +You can feed an `Examples` table from a JSON array, which is great for those situations where the table-content is dynamically resolved at run-time. This capability is triggered when the table consists of a single "cell", i.e. there is exactly one row and one column in the table. Here is an example (also see [this video](https://twitter.com/KarateDSL/status/1051433711814627329)): ```cucumber Feature: scenario outline using a dynamic table diff --git a/examples/consumer-driven-contracts/.gitignore b/examples/consumer-driven-contracts/.gitignore new file mode 100755 index 000000000..240c72a74 --- /dev/null +++ b/examples/consumer-driven-contracts/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +target/ + diff --git a/examples/consumer-driven-contracts/README.md b/examples/consumer-driven-contracts/README.md new file mode 100755 index 000000000..f09f8c891 --- /dev/null +++ b/examples/consumer-driven-contracts/README.md @@ -0,0 +1,22 @@ +# Karate Consumer Driven Contracts Demo + +## References +This is a simplified version of the [example in the Karate test-doubles documentation](https://github.com/intuit/karate/tree/master/karate-netty#consumer-provider-example) - with JMS / queues removed and simplified to be a stand-alone maven project. + +Also see [The World's Smallest Microservice](https://www.linkedin.com/pulse/worlds-smallest-micro-service-peter-thomas/). + +## Instructions +* clone the project +* `mvn clean test` + +## Main Artifacts +| File | Description | Comment | +| ---- | ----------- | ------- | +| [PaymentService.java](payment-producer/src/main/java/payment/producer/PaymentService.java) | Producer | A very simple [Spring Boot](https://spring.io/projects/spring-boot) app / REST service | +| [payment-contract.feature](payment-producer/src/test/java/payment/producer/contract/payment-contract.feature) | Contract + Functional Test | [Karate](https://github.com/intuit/karate) API test | +| [PaymentContractTest.java](payment-producer/src/test/java/payment/producer/contract/PaymentContractTest.java) | Producer Integration Test | JUnit runner for the above | +| [payment-mock.feature](payment-producer/src/test/java/payment/producer/mock/payment-mock.feature) | Mock / Stub | [Karate mock](https://github.com/intuit/karate/tree/master/karate-netty) that *perfectly* simulates the Producer ! | +| [PaymentContractAgainstMockTest.java](payment-producer/src/test/java/payment/producer/mock/PaymentContractAgainstMockTest.java) | Verify that the Mock is as per Contract | JUnit runner that points `payment-contract.feature` --> `payment-mock.feature` | +| [Consumer.java](payment-consumer/src/main/java/payment/consumer/Consumer.java) | Consumer | A simple Java app that calls the Producer to do some work | +| [ConsumerIntegrationTest.java](payment-consumer/src/test/java/payment/consumer/ConsumerIntegrationTest.java) | Consumer Integration Test | A JUnit *full* integration test, using the *real* Consumer and Producer | +| [ConsumerIntegrationAgainstMockTest.java](payment-consumer/src/test/java/payment/consumer/ConsumerIntegrationAgainstMockTest.java) | Consumer Integration Test but using the Mock | Like the above but using the mock Producer | diff --git a/examples/consumer-driven-contracts/payment-consumer/pom.xml b/examples/consumer-driven-contracts/payment-consumer/pom.xml new file mode 100755 index 000000000..075d13190 --- /dev/null +++ b/examples/consumer-driven-contracts/payment-consumer/pom.xml @@ -0,0 +1,27 @@ + + 4.0.0 + + + com.intuit.karate.examples + examples-cdc + 1.0-SNAPSHOT + + + examples-cdc-consumer + jar + + + + com.intuit.karate.examples + examples-cdc-producer + ${project.version} + + + commons-io + commons-io + 2.5 + + + + diff --git a/examples/consumer-driven-contracts/payment-consumer/src/main/java/payment/consumer/Consumer.java b/examples/consumer-driven-contracts/payment-consumer/src/main/java/payment/consumer/Consumer.java new file mode 100755 index 000000000..4064d61eb --- /dev/null +++ b/examples/consumer-driven-contracts/payment-consumer/src/main/java/payment/consumer/Consumer.java @@ -0,0 +1,50 @@ +package payment.consumer; + +import com.intuit.karate.FileUtils; +import com.intuit.karate.JsonUtils; +import java.net.HttpURLConnection; +import java.net.URL; +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import payment.producer.Payment; + +/** + * + * @author pthomas3 + */ +public class Consumer { + + private static final Logger logger = LoggerFactory.getLogger(Consumer.class); + + private final String paymentServiceUrl; + + public Consumer(String paymentServiceUrl) { + this.paymentServiceUrl = paymentServiceUrl; + } + + private HttpURLConnection getConnection(String path) throws Exception { + URL url = new URL(paymentServiceUrl + path); + return (HttpURLConnection) url.openConnection(); + } + + public Payment create(Payment payment) { + try { + HttpURLConnection con = getConnection("/payments"); + con.setRequestMethod("POST"); + con.setDoOutput(true); + con.setRequestProperty("Content-Type", "application/json"); + String json = JsonUtils.toJson(payment); + IOUtils.write(json, con.getOutputStream(), "utf-8"); + int status = con.getResponseCode(); + if (status != 200) { + throw new RuntimeException("status code was " + status); + } + String content = FileUtils.toString(con.getInputStream()); + return JsonUtils.fromJson(content, Payment.class); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + +} diff --git a/examples/consumer-driven-contracts/payment-consumer/src/test/java/logback-test.xml b/examples/consumer-driven-contracts/payment-consumer/src/test/java/logback-test.xml new file mode 100755 index 000000000..4e250e7da --- /dev/null +++ b/examples/consumer-driven-contracts/payment-consumer/src/test/java/logback-test.xml @@ -0,0 +1,26 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + target/karate.log + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/consumer-driven-contracts/payment-consumer/src/test/java/payment/consumer/ConsumerIntegrationAgainstMockTest.java b/examples/consumer-driven-contracts/payment-consumer/src/test/java/payment/consumer/ConsumerIntegrationAgainstMockTest.java new file mode 100755 index 000000000..f369f81c9 --- /dev/null +++ b/examples/consumer-driven-contracts/payment-consumer/src/test/java/payment/consumer/ConsumerIntegrationAgainstMockTest.java @@ -0,0 +1,44 @@ +package payment.consumer; + +import com.intuit.karate.netty.FeatureServer; +import java.io.File; +import org.junit.AfterClass; +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.BeforeClass; +import payment.producer.Payment; + +/** + * + * @author pthomas3 + */ +public class ConsumerIntegrationAgainstMockTest { + + private static FeatureServer server; + private static Consumer consumer; + + @BeforeClass + public static void beforeClass() { + File file = new File("../payment-producer/src/test/java/payment/producer/mock/payment-mock.feature"); + server = FeatureServer.start(file, 0, false, null); + String paymentServiceUrl = "http://localhost:" + server.getPort(); + consumer = new Consumer(paymentServiceUrl); + } + + @Test + public void testPaymentCreate() throws Exception { + Payment payment = new Payment(); + payment.setAmount(5.67); + payment.setDescription("test one"); + payment = consumer.create(payment); + assertTrue(payment.getId() > 0); + assertEquals(payment.getAmount(), 5.67, 0); + assertEquals(payment.getDescription(), "test one"); + } + + @AfterClass + public static void afterClass() { + server.stop(); + } + +} diff --git a/examples/consumer-driven-contracts/payment-consumer/src/test/java/payment/consumer/ConsumerIntegrationTest.java b/examples/consumer-driven-contracts/payment-consumer/src/test/java/payment/consumer/ConsumerIntegrationTest.java new file mode 100755 index 000000000..eb88382cd --- /dev/null +++ b/examples/consumer-driven-contracts/payment-consumer/src/test/java/payment/consumer/ConsumerIntegrationTest.java @@ -0,0 +1,43 @@ +package payment.consumer; + +import org.junit.AfterClass; +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.BeforeClass; +import org.springframework.context.ConfigurableApplicationContext; +import payment.producer.Payment; +import payment.producer.PaymentService; + +/** + * + * @author pthomas3 + */ +public class ConsumerIntegrationTest { + + private static ConfigurableApplicationContext context; + private static Consumer consumer; + + @BeforeClass + public static void beforeClass() { + context = PaymentService.start(); + String paymentServiceUrl = "http://localhost:" + PaymentService.getPort(context); + consumer = new Consumer(paymentServiceUrl); + } + + @Test + public void testPaymentCreate() throws Exception { + Payment payment = new Payment(); + payment.setAmount(5.67); + payment.setDescription("test one"); + payment = consumer.create(payment); + assertTrue(payment.getId() > 0); + assertEquals(payment.getAmount(), 5.67, 0); + assertEquals(payment.getDescription(), "test one"); + } + + @AfterClass + public static void afterClass() { + PaymentService.stop(context); + } + +} diff --git a/examples/consumer-driven-contracts/payment-producer/pom.xml b/examples/consumer-driven-contracts/payment-producer/pom.xml new file mode 100755 index 000000000..a72e32632 --- /dev/null +++ b/examples/consumer-driven-contracts/payment-producer/pom.xml @@ -0,0 +1,29 @@ + + 4.0.0 + + + com.intuit.karate.examples + examples-cdc + 1.0-SNAPSHOT + + + examples-cdc-producer + jar + + + + org.springframework.boot + spring-boot-dependencies + ${spring.boot.version} + pom + import + + + org.springframework.boot + spring-boot-starter-web + ${spring.boot.version} + + + + diff --git a/examples/consumer-driven-contracts/payment-producer/src/main/java/payment/producer/Payment.java b/examples/consumer-driven-contracts/payment-producer/src/main/java/payment/producer/Payment.java new file mode 100755 index 000000000..17868bcb2 --- /dev/null +++ b/examples/consumer-driven-contracts/payment-producer/src/main/java/payment/producer/Payment.java @@ -0,0 +1,37 @@ +package payment.producer; + +/** + * + * @author pthomas3 + */ +public class Payment { + + private int id; + private double amount; + private String description; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public double getAmount() { + return amount; + } + + public void setAmount(double amount) { + this.amount = amount; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + +} diff --git a/examples/consumer-driven-contracts/payment-producer/src/main/java/payment/producer/PaymentService.java b/examples/consumer-driven-contracts/payment-producer/src/main/java/payment/producer/PaymentService.java new file mode 100755 index 000000000..5b9467d97 --- /dev/null +++ b/examples/consumer-driven-contracts/payment-producer/src/main/java/payment/producer/PaymentService.java @@ -0,0 +1,88 @@ +package payment.producer; + +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * + * @author pthomas3 + */ +@Configuration +@EnableAutoConfiguration +public class PaymentService { + + @RestController + @RequestMapping("/payments") + class PaymentController { + + private final AtomicInteger counter = new AtomicInteger(); + private final Map payments = new ConcurrentHashMap(); + + @PostMapping + public Payment create(@RequestBody Payment payment) { + int id = counter.incrementAndGet(); + payment.setId(id); + payments.put(id, payment); + return payment; + } + + @PutMapping("/{id:.+}") + public Payment update(@PathVariable int id, @RequestBody Payment payment) { + payments.put(id, payment); + return payment; + } + + @GetMapping + public Collection list() { + return payments.values(); + } + + @GetMapping("/{id:.+}") + public Payment get(@PathVariable int id) { + return payments.get(id); + } + + @DeleteMapping("/{id:.+}") + public void delete(@PathVariable int id) { + Payment payment = payments.remove(id); + if (payment == null) { + throw new RuntimeException("payment not found, id: " + id); + } + } + + } + + public static ConfigurableApplicationContext start() { + return SpringApplication.run(PaymentService.class, new String[]{"--server.port=0"}); + } + + public static void stop(ConfigurableApplicationContext context) { + SpringApplication.exit(context, () -> 0); + } + + public static int getPort(ConfigurableApplicationContext context) { + ServerStartedInitializingBean ss = context.getBean(ServerStartedInitializingBean.class); + return ss.getLocalPort(); + } + + @Bean + public ServerStartedInitializingBean getInitializingBean() { + return new ServerStartedInitializingBean(); + } + +} diff --git a/examples/consumer-driven-contracts/payment-producer/src/main/java/payment/producer/ServerStartedInitializingBean.java b/examples/consumer-driven-contracts/payment-producer/src/main/java/payment/producer/ServerStartedInitializingBean.java new file mode 100755 index 000000000..11554d15c --- /dev/null +++ b/examples/consumer-driven-contracts/payment-producer/src/main/java/payment/producer/ServerStartedInitializingBean.java @@ -0,0 +1,38 @@ +package payment.producer; + +import java.util.Arrays; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.boot.web.context.WebServerInitializedEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; + +/** + * + * @author pthomas3 + */ +@Component +public class ServerStartedInitializingBean implements ApplicationRunner, ApplicationListener { + + private static final Logger logger = LoggerFactory.getLogger(ServerStartedInitializingBean.class); + + private int localPort; + + public int getLocalPort() { + return localPort; + } + + @Override + public void run(ApplicationArguments aa) throws Exception { + logger.info("server started with args: {}", Arrays.toString(aa.getSourceArgs())); + } + + @Override + public void onApplicationEvent(WebServerInitializedEvent e) { + localPort = e.getWebServer().getPort(); + logger.info("after runtime init, local server port: {}", localPort); + } + +} \ No newline at end of file diff --git a/examples/consumer-driven-contracts/payment-producer/src/test/java/karate-config.js b/examples/consumer-driven-contracts/payment-producer/src/test/java/karate-config.js new file mode 100755 index 000000000..dbd68c49e --- /dev/null +++ b/examples/consumer-driven-contracts/payment-producer/src/test/java/karate-config.js @@ -0,0 +1,3 @@ +function() { + return { paymentServiceUrl: karate.properties['payment.service.url'] } +} diff --git a/examples/consumer-driven-contracts/payment-producer/src/test/java/logback-test.xml b/examples/consumer-driven-contracts/payment-producer/src/test/java/logback-test.xml new file mode 100755 index 000000000..4e250e7da --- /dev/null +++ b/examples/consumer-driven-contracts/payment-producer/src/test/java/logback-test.xml @@ -0,0 +1,26 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + target/karate.log + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/consumer-driven-contracts/payment-producer/src/test/java/payment/producer/contract/PaymentContractTest.java b/examples/consumer-driven-contracts/payment-producer/src/test/java/payment/producer/contract/PaymentContractTest.java new file mode 100755 index 000000000..575fd5605 --- /dev/null +++ b/examples/consumer-driven-contracts/payment-producer/src/test/java/payment/producer/contract/PaymentContractTest.java @@ -0,0 +1,33 @@ +package payment.producer.contract; + +import com.intuit.karate.KarateOptions; +import com.intuit.karate.junit4.Karate; +import payment.producer.PaymentService; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.runner.RunWith; +import org.springframework.context.ConfigurableApplicationContext; + +/** + * + * @author pthomas3 + */ +@RunWith(Karate.class) +@KarateOptions(features = "classpath:payment/producer/contract/payment-contract.feature") +public class PaymentContractTest { + + private static ConfigurableApplicationContext context; + + @BeforeClass + public static void beforeClass() { + context = PaymentService.start(); + String paymentServiceUrl = "http://localhost:" + PaymentService.getPort(context); + System.setProperty("payment.service.url", paymentServiceUrl); + } + + @AfterClass + public static void afterClass() { + PaymentService.stop(context); + } + +} diff --git a/examples/consumer-driven-contracts/payment-producer/src/test/java/payment/producer/contract/payment-contract.feature b/examples/consumer-driven-contracts/payment-producer/src/test/java/payment/producer/contract/payment-contract.feature new file mode 100755 index 000000000..2d4fa4cb2 --- /dev/null +++ b/examples/consumer-driven-contracts/payment-producer/src/test/java/payment/producer/contract/payment-contract.feature @@ -0,0 +1,35 @@ +Feature: payment service contract test + +Background: +* url paymentServiceUrl + '/payments' + +Scenario: create, get, update, list and delete payments + Given request { amount: 5.67, description: 'test one' } + When method post + Then status 200 + And match response == { id: '#number', amount: 5.67, description: 'test one' } + And def id = response.id + + Given path id + When method get + Then status 200 + And match response == { id: '#(id)', amount: 5.67, description: 'test one' } + + Given path id + And request { id: '#(id)', amount: 5.67, description: 'test two' } + When method put + Then status 200 + And match response == { id: '#(id)', amount: 5.67, description: 'test two' } + + When method get + Then status 200 + And match response contains { id: '#(id)', amount: 5.67, description: 'test two' } + + Given path id + When method delete + Then status 200 + + When method get + Then status 200 + And match response !contains { id: '#(id)', amount: '#number', description: '#string' } + \ No newline at end of file diff --git a/examples/consumer-driven-contracts/payment-producer/src/test/java/payment/producer/mock/PaymentContractAgainstMockTest.java b/examples/consumer-driven-contracts/payment-producer/src/test/java/payment/producer/mock/PaymentContractAgainstMockTest.java new file mode 100755 index 000000000..027602149 --- /dev/null +++ b/examples/consumer-driven-contracts/payment-producer/src/test/java/payment/producer/mock/PaymentContractAgainstMockTest.java @@ -0,0 +1,35 @@ +package payment.producer.mock; + +import com.intuit.karate.FileUtils; +import com.intuit.karate.KarateOptions; +import com.intuit.karate.junit4.Karate; +import com.intuit.karate.netty.FeatureServer; +import java.io.File; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.runner.RunWith; + +/** + * + * @author pthomas3 + */ +@RunWith(Karate.class) +@KarateOptions(features = "classpath:payment/producer/contract/payment-contract.feature") +public class PaymentContractAgainstMockTest { + + private static FeatureServer server; + + @BeforeClass + public static void beforeClass() { + File file = FileUtils.getFileRelativeTo(PaymentContractAgainstMockTest.class, "payment-mock.feature"); + server = FeatureServer.start(file, 0, false, null); + String paymentServiceUrl = "http://localhost:" + server.getPort(); + System.setProperty("payment.service.url", paymentServiceUrl); + } + + @AfterClass + public static void afterClass() { + server.stop(); + } + +} diff --git a/examples/consumer-driven-contracts/payment-producer/src/test/java/payment/producer/mock/payment-mock.feature b/examples/consumer-driven-contracts/payment-producer/src/test/java/payment/producer/mock/payment-mock.feature new file mode 100755 index 000000000..25d94690e --- /dev/null +++ b/examples/consumer-driven-contracts/payment-producer/src/test/java/payment/producer/mock/payment-mock.feature @@ -0,0 +1,26 @@ +Feature: payment service mock + +Background: +* def id = 0 +* def payments = {} + +Scenario: pathMatches('/payments') && methodIs('post') + * def payment = request + * def id = ~~(id + 1) + * payment.id = id + * payments[id + ''] = payment + * def response = payment + +Scenario: pathMatches('/payments') + * def response = $payments.* + +Scenario: pathMatches('/payments/{id}') && methodIs('put') + * payments[pathParams.id] = request + * def response = request + +Scenario: pathMatches('/payments/{id}') && methodIs('delete') + * karate.remove('payments', '$.' + pathParams.id) + * def response = '' + +Scenario: pathMatches('/payments/{id}') + * def response = payments[pathParams.id] diff --git a/examples/consumer-driven-contracts/pom.xml b/examples/consumer-driven-contracts/pom.xml new file mode 100755 index 000000000..5a7828ea8 --- /dev/null +++ b/examples/consumer-driven-contracts/pom.xml @@ -0,0 +1,61 @@ + + 4.0.0 + + com.intuit.karate.examples + examples-cdc + 1.0-SNAPSHOT + pom + + + payment-producer + payment-consumer + + + + UTF-8 + 1.8 + 3.6.0 + 2.2.0.RELEASE + 0.9.5 + + + + + com.intuit.karate + karate-apache + ${karate.version} + + + com.intuit.karate + karate-junit4 + ${karate.version} + test + + + + + + + src/test/java + + **/*.java + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven.compiler.version} + + UTF-8 + ${java.version} + ${java.version} + -Werror + + + + + + \ No newline at end of file diff --git a/examples/gatling/.gitignore b/examples/gatling/.gitignore new file mode 100755 index 000000000..0a345f6f3 --- /dev/null +++ b/examples/gatling/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +target/ +.idea/ + diff --git a/examples/gatling/README.md b/examples/gatling/README.md new file mode 100755 index 000000000..9200bbd28 --- /dev/null +++ b/examples/gatling/README.md @@ -0,0 +1,14 @@ +# karate-gatling-demo +demo sample project for karate [test-doubles](https://github.com/intuit/karate/tree/master/karate-netty) and [gatling integration](https://github.com/intuit/karate/tree/master/karate-gatling) + +## Instructions + +``` +mvn clean test +``` + +The above works because the `gatling-maven-plugin` has been configured to run as part of the Maven `test` phase automatically in the [`pom.xml`](pom.xml). + +The file location of the Gatling HTML report should appear towards the end of the console log. Copy and paste it into your browser address-bar. + +Here's a video of what to expect: https://twitter.com/ptrthomas/status/986463717465391104 diff --git a/examples/gatling/build.gradle b/examples/gatling/build.gradle new file mode 100644 index 000000000..93dd71309 --- /dev/null +++ b/examples/gatling/build.gradle @@ -0,0 +1,48 @@ +plugins { + id 'scala' +} + +ext { + karateVersion = '0.9.5' +} + +dependencies { + testCompile "com.intuit.karate:karate-apache:${karateVersion}" + testCompile "com.intuit.karate:karate-gatling:${karateVersion}" +} + +repositories { + mavenCentral() + // mavenLocal() +} + +test { + systemProperty "karate.options", System.properties.getProperty("karate.options") + systemProperty "karate.env", System.properties.getProperty("karate.env") + outputs.upToDateWhen { false } +} + +sourceSets { + test { + resources { + srcDir file('src/test/java') + exclude '**/*.java' + exclude '**/*.scala' + } + } +} + +// to run, type: "gradle gatling" +task gatlingRun(type: JavaExec) { + group = 'Web Tests' + description = 'Run Gatling Tests' + new File("${buildDir}/reports/gatling").mkdirs() + classpath = sourceSets.test.runtimeClasspath + main = "io.gatling.app.Gatling" + args = [ + // change this to suit your simulation entry-point + '-s', 'mock.CatsKarateSimulation', + '-rf', "${buildDir}/reports/gatling" + ] + systemProperties System.properties +} diff --git a/examples/gatling/pom.xml b/examples/gatling/pom.xml new file mode 100755 index 000000000..d7fcbcd69 --- /dev/null +++ b/examples/gatling/pom.xml @@ -0,0 +1,75 @@ + + 4.0.0 + + com.intuit.karate.examples + examples-gatling + 1.0-SNAPSHOT + jar + + + UTF-8 + 1.8 + 3.6.0 + 0.9.5 + 3.0.2 + + + + + com.intuit.karate + karate-apache + ${karate.version} + + + com.intuit.karate + karate-gatling + ${karate.version} + test + + + + + + + src/test/java + + **/*.java + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven.compiler.version} + + UTF-8 + ${java.version} + ${java.version} + -Werror + + + + io.gatling + gatling-maven-plugin + ${gatling.plugin.version} + + src/test/java + + mock.CatsKarateSimulation + + + + + test + + test + + + + + + + + \ No newline at end of file diff --git a/examples/gatling/src/test/java/jobtest/GatlingDockerJobRunner.java b/examples/gatling/src/test/java/jobtest/GatlingDockerJobRunner.java new file mode 100644 index 000000000..b1d4b0c23 --- /dev/null +++ b/examples/gatling/src/test/java/jobtest/GatlingDockerJobRunner.java @@ -0,0 +1,20 @@ +package jobtest; + +import com.intuit.karate.gatling.GatlingJobServer; +import com.intuit.karate.gatling.GatlingMavenJobConfig; + +/** + * + * @author pthomas3 + */ +public class GatlingDockerJobRunner { + + public static void main(String[] args) { + GatlingMavenJobConfig config = new GatlingMavenJobConfig(2, "host.docker.internal", 0); + GatlingJobServer server = new GatlingJobServer(config); + server.startExecutors(); + server.waitSync(); + io.gatling.app.Gatling.main(new String[]{"-ro", "reports", "-rf", "target"}); + } + +} diff --git a/examples/gatling/src/test/java/jobtest/GatlingRunner.java b/examples/gatling/src/test/java/jobtest/GatlingRunner.java new file mode 100644 index 000000000..aea5b1245 --- /dev/null +++ b/examples/gatling/src/test/java/jobtest/GatlingRunner.java @@ -0,0 +1,35 @@ +package jobtest; + +import com.intuit.karate.gatling.GatlingJobServer; +import com.intuit.karate.gatling.GatlingMavenJobConfig; +import com.intuit.karate.job.JobExecutor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +/** + * + * @author pthomas3 + */ +public class GatlingRunner { + + public static void main(String[] args) { + GatlingMavenJobConfig config = new GatlingMavenJobConfig(-1, "127.0.0.1", 0) { + @Override + public void startExecutors(String uniqueId, String serverUrl) throws Exception { + int executorCount = 2; + ExecutorService executor = Executors.newFixedThreadPool(executorCount); + for (int i = 0; i < executorCount; i++) { + executor.submit(() -> JobExecutor.run(serverUrl)); + } + executor.shutdown(); + executor.awaitTermination(0, TimeUnit.MINUTES); + } + }; + GatlingJobServer server = new GatlingJobServer(config); + server.startExecutors(); + server.waitSync(); + io.gatling.app.Gatling.main(new String[]{"-ro", "reports", "-rf", "target"}); + } + +} diff --git a/examples/gatling/src/test/java/karate-config.js b/examples/gatling/src/test/java/karate-config.js new file mode 100755 index 000000000..3d00e7c5a --- /dev/null +++ b/examples/gatling/src/test/java/karate-config.js @@ -0,0 +1,3 @@ +function(){ + return {}; +} \ No newline at end of file diff --git a/examples/gatling/src/test/java/logback-test.xml b/examples/gatling/src/test/java/logback-test.xml new file mode 100755 index 000000000..e7a67ace0 --- /dev/null +++ b/examples/gatling/src/test/java/logback-test.xml @@ -0,0 +1,26 @@ + + + + + false + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + false + target/karate.log + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + diff --git a/examples/gatling/src/test/java/mock/CatsGatlingSimulation.scala b/examples/gatling/src/test/java/mock/CatsGatlingSimulation.scala new file mode 100755 index 000000000..5524aa91a --- /dev/null +++ b/examples/gatling/src/test/java/mock/CatsGatlingSimulation.scala @@ -0,0 +1,73 @@ +package mock + +import io.gatling.core.Predef._ +import io.gatling.http.Predef._ + +import scala.concurrent.duration._ + +class CatsGatlingSimulation extends Simulation { + + MockUtils.startServer() + + val httpConf = http.baseUrl(System.getProperty("mock.cats.url")) + + val create = scenario("create") + .pause(25 milliseconds) + .exec(http("POST /cats") + .post("/") + .body(StringBody("""{ "name": "Billie" }""")) + .check(status.is(200)) + .check(jsonPath("$.name").is("Billie")) + .check(jsonPath("$.id") + .saveAs("id"))) + + .pause(10 milliseconds).exec( + http("GET /cats/{id}") + .get("/${id}") + .check(status.is(200)) + .check(jsonPath("$.id").is("${id}")) + // intentional assertion failure + .check(jsonPath("$.name").is("Billi"))) + .exitHereIfFailed + .exec( + http("PUT /cats/{id}") + .put("/${id}") + .body(StringBody("""{ "id":"${id}", "name": "Bob" }""")) + .check(status.is(200)) + .check(jsonPath("$.id").is("${id}")) + .check(jsonPath("$.name").is("Bob"))) + + .pause(10 milliseconds).exec( + http("GET /cats/{id}") + .get("/${id}") + .check(status.is(200))) + + val delete = scenario("delete") + .pause(15 milliseconds).exec( + http("GET /cats") + .get("/") + .check(status.is(200)) + .check(jsonPath("$[*].id").findAll.optional + .saveAs("ids"))) + + .doIf(_.contains("ids")) { + foreach("${ids}", "id") { + pause(20 milliseconds).exec( + http("DELETE /cats/{id}") + .delete("/${id}") + .check(status.is(200)) + .check(bodyString.is(""))) + + .pause(10 milliseconds).exec( + http("GET /cats/{id}") + .get("/${id}") + .check(status.is(404))) + } + } + + setUp( + create.inject(rampUsers(10) during (5 seconds)).protocols(httpConf), + delete.inject(rampUsers(5) during (5 seconds)).protocols(httpConf) + ) + +} diff --git a/examples/gatling/src/test/java/mock/CatsKarateSimulation.scala b/examples/gatling/src/test/java/mock/CatsKarateSimulation.scala new file mode 100755 index 000000000..3c143e629 --- /dev/null +++ b/examples/gatling/src/test/java/mock/CatsKarateSimulation.scala @@ -0,0 +1,30 @@ +package mock + +import com.intuit.karate.gatling.PreDef._ +import io.gatling.core.Predef._ +import scala.concurrent.duration._ + +class CatsKarateSimulation extends Simulation { + + MockUtils.startServer() + + val feeder = Iterator.continually(Map("catName" -> MockUtils.getNextCatName)) + + val protocol = karateProtocol( + "/cats/{id}" -> Nil, + "/cats" -> pauseFor("get" -> 15, "post" -> 25) + ) + + protocol.nameResolver = (req, ctx) => req.getHeader("karate-name") + + val create = scenario("create").feed(feeder).exec(karateFeature("classpath:mock/cats-create.feature")) + val delete = scenario("delete").exec(karateFeature("classpath:mock/cats-delete.feature@name=delete")) + val custom = scenario("custom").exec(karateFeature("classpath:mock/custom-rpc.feature")) + + setUp( + create.inject(rampUsers(10) during (5 seconds)).protocols(protocol), + delete.inject(rampUsers(5) during (5 seconds)).protocols(protocol), + custom.inject(rampUsers(10) during (5 seconds)).protocols(protocol) + ) + +} diff --git a/examples/gatling/src/test/java/mock/MockUtils.java b/examples/gatling/src/test/java/mock/MockUtils.java new file mode 100755 index 000000000..560642753 --- /dev/null +++ b/examples/gatling/src/test/java/mock/MockUtils.java @@ -0,0 +1,49 @@ +package mock; + +import com.intuit.karate.FileUtils; +import com.intuit.karate.PerfContext; +import com.intuit.karate.Runner; +import com.intuit.karate.netty.FeatureServer; + +import java.io.File; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * + * @author pthomas3 + */ +public class MockUtils { + + public static void startServer() { + File file = FileUtils.getFileRelativeTo(MockUtils.class, "mock.feature"); + FeatureServer server = FeatureServer.start(file, 0, false, null); + System.setProperty("mock.cats.url", "http://localhost:" + server.getPort() + "/cats"); + } + + private static final List catNames = (List) Runner.runFeature("classpath:mock/feeder.feature", null, false).get("names"); + + private static final AtomicInteger counter = new AtomicInteger(); + + public static String getNextCatName() { + return catNames.get(counter.getAndIncrement() % catNames.size()); + } + + public static Map myRpc(Map map, PerfContext context) { + long startTime = System.currentTimeMillis(); + // this is just an example, you can put any kind of code here + int sleepTime = (Integer) map.get("sleep"); + try { + Thread.sleep(sleepTime); + } catch (Exception e) { + throw new RuntimeException(e); + } + long endTime = System.currentTimeMillis(); + // and here is where you send the performance data to the reporting engine + context.capturePerfEvent("myRpc-" + sleepTime, startTime, endTime); + return Collections.singletonMap("success", true); + } + +} diff --git a/examples/gatling/src/test/java/mock/cats-create.feature b/examples/gatling/src/test/java/mock/cats-create.feature new file mode 100755 index 000000000..81ebfbd46 --- /dev/null +++ b/examples/gatling/src/test/java/mock/cats-create.feature @@ -0,0 +1,32 @@ +Feature: cats crud + + Background: + * url karate.properties['mock.cats.url'] + + Scenario: create, get and update cat + # example of using the gatling session / feeder data + # note how this can still work as a normal test, without gatling + * def name = karate.get('__gatling.catName', 'Billie') + Given request { name: '#(name)' } + When method post + Then status 200 + And match response == { id: '#uuid', name: '#(name)' } + * def id = response.id + + Given path id + When method get + # this step may randomly fail because another thread is doing deletes + Then status 200 + # intentional assertion failure + And match response == { id: '#(id)', name: 'Billi' } + + # since we failed above, these lines will not be executed + Given path id + When request { id: '#(id)', name: 'Bob' } + When method put + Then status 200 + And match response == { id: '#(id)', name: 'Bob' } + + When method get + Then status 200 + And match response contains { id: '#(id)', name: 'Bob' } diff --git a/examples/gatling/src/test/java/mock/cats-delete-one.feature b/examples/gatling/src/test/java/mock/cats-delete-one.feature new file mode 100755 index 000000000..d830794c9 --- /dev/null +++ b/examples/gatling/src/test/java/mock/cats-delete-one.feature @@ -0,0 +1,14 @@ +@ignore +Feature: delete cat by id and verify + + Scenario: + Given url karate.properties['mock.cats.url'] + And path id + When method delete + Then status 200 + And match response == '' + + Given path id + And header karate-name = 'cats-get-404' + When method get + Then status 404 diff --git a/examples/gatling/src/test/java/mock/cats-delete.feature b/examples/gatling/src/test/java/mock/cats-delete.feature new file mode 100755 index 000000000..6e6f3a703 --- /dev/null +++ b/examples/gatling/src/test/java/mock/cats-delete.feature @@ -0,0 +1,17 @@ +Feature: delete all cats found + + Background: + * url karate.properties['mock.cats.url'] + + Scenario: this scenario will be ignored because the gatling script looks for the tag @name=delete + * print 'this should not appear in the logs !' + When method get + Then status 400 + + @name=delete + Scenario: get all cats and then delete each by id + When method get + Then status 200 + + * def delete = read('cats-delete-one.feature') + * def result = call delete response diff --git a/examples/gatling/src/test/java/mock/custom-rpc.feature b/examples/gatling/src/test/java/mock/custom-rpc.feature new file mode 100755 index 000000000..ec14fa0d8 --- /dev/null +++ b/examples/gatling/src/test/java/mock/custom-rpc.feature @@ -0,0 +1,21 @@ +@ignore +Feature: even java interop performance test reports are possible + + Background: + * def Utils = Java.type('mock.MockUtils') + + Scenario: fifty + * def payload = { sleep: 50 } + * def response = Utils.myRpc(payload, karate) + * match response == { success: true } + + Scenario: seventy five + * def payload = { sleep: 75 } + * def response = Utils.myRpc(payload, karate) + # this is deliberately set up to fail + * match response == { success: false } + + Scenario: hundred + * def payload = { sleep: 100 } + * def response = Utils.myRpc(payload, karate) + * match response == { success: true } diff --git a/examples/gatling/src/test/java/mock/feeder.feature b/examples/gatling/src/test/java/mock/feeder.feature new file mode 100755 index 000000000..0b02813f9 --- /dev/null +++ b/examples/gatling/src/test/java/mock/feeder.feature @@ -0,0 +1,5 @@ +Feature: to generate a list of cat names + + Scenario: any variables defined can be retrieved when called via the java api + + * def names = ['Bob', 'Wild', 'Nyan', 'Ceiling'] \ No newline at end of file diff --git a/examples/gatling/src/test/java/mock/mock.feature b/examples/gatling/src/test/java/mock/mock.feature new file mode 100755 index 000000000..bc49e22af --- /dev/null +++ b/examples/gatling/src/test/java/mock/mock.feature @@ -0,0 +1,29 @@ +Feature: cats stateful crud + + Background: + * def uuid = function(){ return java.util.UUID.randomUUID() + '' } + * def cats = {} + * def delay = function(){ java.lang.Thread.sleep(850) } + + Scenario: pathMatches('/cats') && methodIs('post') + * def cat = request + * def id = uuid() + * cat.id = id + * cats[id] = cat + * def response = cat + + Scenario: pathMatches('/cats') + * def response = $cats.* + + Scenario: pathMatches('/cats/{id}') && methodIs('put') + * cats[pathParams.id] = request + * def response = request + + Scenario: pathMatches('/cats/{id}') && methodIs('delete') + * karate.remove('cats', '$.' + pathParams.id) + * def response = '' + * def afterScenario = delay + + Scenario: pathMatches('/cats/{id}') + * def response = cats[pathParams.id] + * def responseStatus = response ? 200 : 404 diff --git a/examples/jobserver/README.md b/examples/jobserver/README.md new file mode 100644 index 000000000..74dae5ec7 --- /dev/null +++ b/examples/jobserver/README.md @@ -0,0 +1,13 @@ +# Distributed Testing + +Please refer to the wiki: [Distributed Testing](https://github.com/intuit/karate/wiki/Distributed-Testing). + +# Docker + +How to run Web-UI tests using Docker without even Java or Maven installed. + +Please refer to the wiki: [Docker](https://github.com/intuit/karate/wiki/Docker). + +# Gradle + +This project also has a sample [`build.gradle`](build.gradle). Also see the wiki: [Gradle](https://github.com/intuit/karate/wiki/Gradle). \ No newline at end of file diff --git a/examples/jobserver/build.gradle b/examples/jobserver/build.gradle new file mode 100644 index 000000000..358f72c15 --- /dev/null +++ b/examples/jobserver/build.gradle @@ -0,0 +1,39 @@ +plugins { + id 'java' +} + +ext { + karateVersion = '0.9.5' +} + +dependencies { + testCompile "com.intuit.karate:karate-junit5:${karateVersion}" + testCompile "com.intuit.karate:karate-apache:${karateVersion}" + testCompile "net.masterthought:cucumber-reporting:3.8.0" +} + +sourceSets { + test { + resources { + srcDir file('src/test/java') + exclude '**/*.java' + } + } +} + +test { + useJUnitPlatform() + systemProperty "karate.options", System.properties.getProperty("karate.options") + systemProperty "karate.env", System.properties.getProperty("karate.env") + outputs.upToDateWhen { false } +} + +repositories { + mavenCentral() + // mavenLocal() +} + +task karateDebug(type: JavaExec) { + classpath = sourceSets.test.runtimeClasspath + main = 'com.intuit.karate.cli.Main' +} \ No newline at end of file diff --git a/examples/jobserver/pom.xml b/examples/jobserver/pom.xml new file mode 100644 index 000000000..cd4491a7a --- /dev/null +++ b/examples/jobserver/pom.xml @@ -0,0 +1,67 @@ + + 4.0.0 + + com.intuit.karate.examples + examples-jobserver + 1.0-SNAPSHOT + jar + + + UTF-8 + 1.8 + 3.6.0 + 0.9.5 + + + + + com.intuit.karate + karate-apache + ${karate.version} + test + + + com.intuit.karate + karate-junit5 + ${karate.version} + test + + + net.masterthought + cucumber-reporting + 3.8.0 + test + + + + + + + src/test/java + + **/*.java + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven.compiler.version} + + UTF-8 + ${java.version} + ${java.version} + -Werror + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.2 + + + + + \ No newline at end of file diff --git a/examples/jobserver/src/test/java/common/Main.java b/examples/jobserver/src/test/java/common/Main.java new file mode 100644 index 000000000..cad001308 --- /dev/null +++ b/examples/jobserver/src/test/java/common/Main.java @@ -0,0 +1,13 @@ +package common; + +/** + * + * @author pthomas3 + */ +public class Main { + + public static void main(String[] args) { + System.out.println("main ok"); + } + +} diff --git a/examples/jobserver/src/test/java/common/ReportUtils.java b/examples/jobserver/src/test/java/common/ReportUtils.java new file mode 100644 index 000000000..b96e2421d --- /dev/null +++ b/examples/jobserver/src/test/java/common/ReportUtils.java @@ -0,0 +1,26 @@ +package common; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import net.masterthought.cucumber.Configuration; +import net.masterthought.cucumber.ReportBuilder; +import org.apache.commons.io.FileUtils; + +/** + * + * @author pthomas3 + */ +public class ReportUtils { + + public static void generateReport(String karateOutputPath) { + Collection jsonFiles = FileUtils.listFiles(new File(karateOutputPath), new String[]{"json"}, true); + List jsonPaths = new ArrayList(jsonFiles.size()); + jsonFiles.forEach(file -> jsonPaths.add(file.getAbsolutePath())); + Configuration config = new Configuration(new File("target"), "demo"); + ReportBuilder reportBuilder = new ReportBuilder(jsonPaths, config); + reportBuilder.generateReports(); + } + +} diff --git a/examples/jobserver/src/test/java/jobtest/simple/SimpleDockerJobRunner.java b/examples/jobserver/src/test/java/jobtest/simple/SimpleDockerJobRunner.java new file mode 100644 index 000000000..92cbdf865 --- /dev/null +++ b/examples/jobserver/src/test/java/jobtest/simple/SimpleDockerJobRunner.java @@ -0,0 +1,22 @@ +package jobtest.simple; + +import common.ReportUtils; +import com.intuit.karate.Results; +import com.intuit.karate.Runner; +import com.intuit.karate.job.MavenJobConfig; +import org.junit.jupiter.api.Test; + +/** + * + * @author pthomas3 + */ +public class SimpleDockerJobRunner { + + @Test + void testJobManager() { + MavenJobConfig config = new MavenJobConfig(2, "host.docker.internal", 0); + Results results = Runner.path("classpath:jobtest/simple").startServerAndWait(config); + ReportUtils.generateReport(results.getReportDir()); + } + +} diff --git a/examples/jobserver/src/test/java/jobtest/simple/SimpleRunner.java b/examples/jobserver/src/test/java/jobtest/simple/SimpleRunner.java new file mode 100644 index 000000000..c7296d27f --- /dev/null +++ b/examples/jobserver/src/test/java/jobtest/simple/SimpleRunner.java @@ -0,0 +1,42 @@ +package jobtest.simple; + +import common.ReportUtils; +import com.intuit.karate.Results; +import com.intuit.karate.Runner; +import com.intuit.karate.job.JobExecutor; +import com.intuit.karate.job.MavenJobConfig; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; + +/** + * use this to troubleshoot the job-server-executor flow + * since this all runs locally and does not use a remote / docker instance + * you can debug and view all the logs in one place + * + * @author pthomas3 + */ +public class SimpleRunner { + + @Test + void testJobManager() { + MavenJobConfig config = new MavenJobConfig(-1, "127.0.0.1", 0) { + @Override + public void startExecutors(String uniqueId, String serverUrl) throws Exception { + int executorCount = 2; + ExecutorService executor = Executors.newFixedThreadPool(executorCount); + for (int i = 0; i < executorCount; i++) { + executor.submit(() -> JobExecutor.run(serverUrl)); + } + executor.shutdown(); + executor.awaitTermination(0, TimeUnit.MINUTES); + } + }; + // export KARATE_TEST="foo" + config.addEnvPropKey("KARATE_TEST"); + Results results = Runner.path("classpath:jobtest/simple").startServerAndWait(config); + ReportUtils.generateReport(results.getReportDir()); + } + +} diff --git a/examples/jobserver/src/test/java/jobtest/simple/simple1.feature b/examples/jobserver/src/test/java/jobtest/simple/simple1.feature new file mode 100644 index 000000000..93a3c8033 --- /dev/null +++ b/examples/jobserver/src/test/java/jobtest/simple/simple1.feature @@ -0,0 +1,17 @@ +Feature: simple 1 + +Background: +* print 'background: sleeping ...' +* java.lang.Thread.sleep(1000) +* print 'background: done' + +Scenario: 1-one +* print '1-one' +* def karateTest = java.lang.System.getenv('KARATE_TEST') +* print '*** KARATE_TEST: ', karateTest + +Scenario: 1-two +* print '1-two' + +Scenario: 1-three +* print '1-three' diff --git a/examples/jobserver/src/test/java/jobtest/simple/simple2.feature b/examples/jobserver/src/test/java/jobtest/simple/simple2.feature new file mode 100644 index 000000000..eb52bcd16 --- /dev/null +++ b/examples/jobserver/src/test/java/jobtest/simple/simple2.feature @@ -0,0 +1,10 @@ +Feature: simple 2 + +Scenario: 2-one +* print '2-one' + +Scenario: 2-two +* print '2-two' + +Scenario: 2-three +* print '2-three' diff --git a/examples/jobserver/src/test/java/jobtest/simple/simple3.feature b/examples/jobserver/src/test/java/jobtest/simple/simple3.feature new file mode 100644 index 000000000..b93486d9c --- /dev/null +++ b/examples/jobserver/src/test/java/jobtest/simple/simple3.feature @@ -0,0 +1,10 @@ +Feature: simple 3 + +Scenario: 3-one +* print '3-one' + +Scenario: 3-two +* print '3-two' + +Scenario: 3-three +* print '3-three' diff --git a/examples/jobserver/src/test/java/jobtest/web/WebDockerJobRunner.java b/examples/jobserver/src/test/java/jobtest/web/WebDockerJobRunner.java new file mode 100644 index 000000000..0f8e0b680 --- /dev/null +++ b/examples/jobserver/src/test/java/jobtest/web/WebDockerJobRunner.java @@ -0,0 +1,23 @@ +package jobtest.web; + +import common.ReportUtils; +import com.intuit.karate.Results; +import com.intuit.karate.Runner; +import com.intuit.karate.job.MavenChromeJobConfig; +import org.junit.jupiter.api.Test; + +/** + * + * @author pthomas3 + */ +public class WebDockerJobRunner { + + @Test + void test() { + MavenChromeJobConfig config = new MavenChromeJobConfig(2, "host.docker.internal", 0); + System.setProperty("karate.env", "jobserver"); + Results results = Runner.path("classpath:jobtest/web").startServerAndWait(config); + ReportUtils.generateReport(results.getReportDir()); + } + +} diff --git a/examples/jobserver/src/test/java/jobtest/web/WebDockerRunner.java b/examples/jobserver/src/test/java/jobtest/web/WebDockerRunner.java new file mode 100644 index 000000000..3af16718e --- /dev/null +++ b/examples/jobserver/src/test/java/jobtest/web/WebDockerRunner.java @@ -0,0 +1,26 @@ +package jobtest.web; + +import common.ReportUtils; +import com.intuit.karate.Results; +import com.intuit.karate.Runner; +import org.junit.jupiter.api.Test; + +/** + * + * @author pthomas3 + */ +public class WebDockerRunner { + + @Test + void test() { + // docker run --name karate --rm -p 5900:5900 --cap-add=SYS_ADMIN -v "$PWD":/src ptrthomas/karate-chrome + // open vnc://localhost:5900 + // docker exec -it -w /src karate mvn clean test -Dtest=jobtest.web.WebDockerRunner + // docker exec -it -w /src karate bash + // mvn clean test -Dtest=jobtest.web.WebDockerRunner + System.setProperty("karate.env", "docker"); + Results results = Runner.path("classpath:jobtest/web").parallel(1); + ReportUtils.generateReport(results.getReportDir()); + } + +} diff --git a/examples/jobserver/src/test/java/jobtest/web/WebRunner.java b/examples/jobserver/src/test/java/jobtest/web/WebRunner.java new file mode 100644 index 000000000..eb9a22b47 --- /dev/null +++ b/examples/jobserver/src/test/java/jobtest/web/WebRunner.java @@ -0,0 +1,22 @@ +package jobtest.web; + +import com.intuit.karate.Results; +import com.intuit.karate.Runner; +import common.ReportUtils; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; + +/** + * + * @author pthomas3 + */ +public class WebRunner { + + @Test + void test() { + Results results = Runner.path("classpath:jobtest/web").tags("~@ignore").parallel(1); + ReportUtils.generateReport(results.getReportDir()); + assertEquals(0, results.getFailCount(), results.getErrorMessages()); + } + +} diff --git a/examples/jobserver/src/test/java/jobtest/web/web1.feature b/examples/jobserver/src/test/java/jobtest/web/web1.feature new file mode 100644 index 000000000..8a77a9054 --- /dev/null +++ b/examples/jobserver/src/test/java/jobtest/web/web1.feature @@ -0,0 +1,40 @@ +Feature: web 1 + + Scenario: try to login to github + and then do a google search + + Given driver 'https://github.com/login' + And input('#login_field', 'dummy') + And input('#password', 'world') + When submit().click("input[name=commit]") + Then match html('#js-flash-container') contains 'Incorrect username or password.' + + Given driver 'https://google.com' + And input("input[name=q]", 'karate dsl') + When submit().click("input[name=btnI]") + Then waitForUrl('https://github.com/intuit/karate') + + Scenario: google search, land on the karate github page, and search for a file + + Given driver 'https://google.com' + And input('input[name=q]', 'karate dsl') + When click('input[name=btnI]') + Then waitForUrl('https://github.com/intuit/karate') + + When click('{a}Find file') + And def searchField = waitFor('input[name=query]') + Then match driver.url == 'https://github.com/intuit/karate/find/master' + + When searchField.input('karate-logo.png') + Then def searchResults = waitForResultCount('.js-tree-browser-result-path', 2, '_.innerText') + Then match searchResults contains 'karate-core/src/main/resources/karate-logo.png' + + Scenario: test-automation challenge + Given driver 'https://semantic-ui.com/modules/dropdown.html' + And def locator = "select[name=skills]" + Then scroll(locator) + And click(locator) + And click('div[data-value=css]') + And click('div[data-value=html]') + And click('div[data-value=ember]') + And delay(1000) \ No newline at end of file diff --git a/examples/jobserver/src/test/java/jobtest/web/web2.feature b/examples/jobserver/src/test/java/jobtest/web/web2.feature new file mode 100644 index 000000000..af7d1db5b --- /dev/null +++ b/examples/jobserver/src/test/java/jobtest/web/web2.feature @@ -0,0 +1,15 @@ +Feature: web 2 + +Scenario: try to login to github + and then do a google search + + Given driver 'https://github.com/login' + And input('#login_field', 'dummy') + And input('#password', 'world') + When submit().click("input[name=commit]") + Then match html('#js-flash-container') contains 'Incorrect username or password.' + + Given driver 'https://google.com' + And input("input[name=q]", 'karate dsl') + When submit().click("input[name=btnI]") + Then waitForUrl('https://github.com/intuit/karate') diff --git a/examples/jobserver/src/test/java/karate-config.js b/examples/jobserver/src/test/java/karate-config.js new file mode 100644 index 000000000..3cfd72c8c --- /dev/null +++ b/examples/jobserver/src/test/java/karate-config.js @@ -0,0 +1,18 @@ +function fn() { + if (karate.env === 'docker') { + var driverConfig = { + type: 'chrome', + showDriverLog: true, + start: false, + beforeStart: 'supervisorctl start ffmpeg', + afterStop: 'supervisorctl stop ffmpeg', + videoFile: '/tmp/karate.mp4' + }; + karate.configure('driver', driverConfig); + } else if (karate.env === 'jobserver') { + karate.configure('driver', {type: 'chrome', showDriverLog: true, start: false}); + } else { + karate.configure('driver', {type: 'chrome', showDriverLog: true}); + } + return {} +} \ No newline at end of file diff --git a/examples/jobserver/src/test/java/log4j2.properties b/examples/jobserver/src/test/java/log4j2.properties new file mode 100644 index 000000000..c7017fbf2 --- /dev/null +++ b/examples/jobserver/src/test/java/log4j2.properties @@ -0,0 +1,3 @@ +log4j.rootLogger = INFO, CONSOLE +log4j.appender.CONSOLE = org.apache.log4j.ConsoleAppender +log4j.appender.CONSOLE.layout = org.apache.log4j.PatternLayout diff --git a/examples/jobserver/src/test/java/logback-test.xml b/examples/jobserver/src/test/java/logback-test.xml new file mode 100644 index 000000000..fea195eb0 --- /dev/null +++ b/examples/jobserver/src/test/java/logback-test.xml @@ -0,0 +1,24 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + target/karate.log + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + \ No newline at end of file diff --git a/examples/ui-test/README.md b/examples/ui-test/README.md new file mode 100644 index 000000000..43bc084d8 --- /dev/null +++ b/examples/ui-test/README.md @@ -0,0 +1,15 @@ +# Karate UI Test +This project is designed to be the simplest way to replicate issues with the [Karate UI framework](https://github.com/intuit/karate/tree/master/karate-core) for web-browser testing. It includes an HTTP mock that serves HTML and JavaScript, which you can easily modify to simulate complex situations such as a slow-loading element. + +## Overview +To point to a specifc version of Karate, edit the `pom.xml`. If you are working with the source-code of Karate, follow the [developer guide](https://github.com/intuit/karate/wiki/Developer-Guide). + +You can double-click and view `page-01.html` to see how it works. It depends on `karate.js` which is very simple, so you can see how to add any JS (if required) along the same lines. + +The `mock.feature` is a Karate mock. Note how it is very simple - but able to serve both HTML and JS. If you need to include navigation to a second page, you can easily add a second HTML file and `Scenario`. To test the HTML being served manually, you can start the mock-server by running `MockRunner` as a JUnit test, and then opening [`http://localhost:8080/page-01`](http://localhost:8080/page-01) in a browser. + +## Running +The `test.feature` is a simple [Karate UI test](https://github.com/intuit/karate/tree/master/karate-core), and executing `UiRunner` as a JUnit test will run it. You will be able to open the HTML report (look towards the end of the console log) and refresh it after re-running. For convenience, this test is a `Scenario Outline` - set up so that you can add multiple browser targets or driver implementations. This makes it easy to validate cross-browser compatibility. + +## Debugging +You should be able to use the [Karate extension for Visual Studio Code](https://github.com/intuit/karate/wiki/IDE-Support#vs-code-karate-plugin) for stepping-through a test for troubleshooting. \ No newline at end of file diff --git a/examples/ui-test/pom.xml b/examples/ui-test/pom.xml new file mode 100644 index 000000000..935f37261 --- /dev/null +++ b/examples/ui-test/pom.xml @@ -0,0 +1,61 @@ + + 4.0.0 + + com.intuit.karate.examples + examples-ui-test + 1.0-SNAPSHOT + jar + + + UTF-8 + 1.8 + 3.6.0 + 0.9.5 + + + + + com.intuit.karate + karate-apache + ${karate.version} + test + + + com.intuit.karate + karate-junit5 + ${karate.version} + test + + + + + + + src/test/java + + **/*.java + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven.compiler.version} + + UTF-8 + ${java.version} + ${java.version} + -Werror + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.2 + + + + + \ No newline at end of file diff --git a/examples/ui-test/src/test/java/logback-test.xml b/examples/ui-test/src/test/java/logback-test.xml new file mode 100644 index 000000000..243a949bb --- /dev/null +++ b/examples/ui-test/src/test/java/logback-test.xml @@ -0,0 +1,25 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + target/karate.log + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/ui-test/src/test/java/ui/MockRunner.java b/examples/ui-test/src/test/java/ui/MockRunner.java new file mode 100644 index 000000000..a1f6dddd2 --- /dev/null +++ b/examples/ui-test/src/test/java/ui/MockRunner.java @@ -0,0 +1,22 @@ +package ui; + +import com.intuit.karate.FileUtils; +import com.intuit.karate.netty.FeatureServer; +import java.io.File; +import org.junit.jupiter.api.Test; + +/** + * run this as a junit test to start an http server at port 8080 + * the html page can be viewed at http://localhost:8080/page-01 + * kill / stop this process when done + */ +class MockRunner { + + @Test + public void testStart() { + File file = FileUtils.getFileRelativeTo(MockRunner.class, "mock.feature"); + FeatureServer server = FeatureServer.start(file, 8080, false, null); + server.waitSync(); + } + +} diff --git a/examples/ui-test/src/test/java/ui/UiRunner.java b/examples/ui-test/src/test/java/ui/UiRunner.java new file mode 100644 index 000000000..20122ff1f --- /dev/null +++ b/examples/ui-test/src/test/java/ui/UiRunner.java @@ -0,0 +1,23 @@ +package ui; + +import com.intuit.karate.FileUtils; +import com.intuit.karate.junit5.Karate; +import com.intuit.karate.netty.FeatureServer; +import java.io.File; +import org.junit.jupiter.api.BeforeAll; + +class UiRunner { + + @BeforeAll + public static void beforeAll() { + File file = FileUtils.getFileRelativeTo(UiRunner.class, "mock.feature"); + FeatureServer server = FeatureServer.start(file, 0, false, null); + System.setProperty("web.url.base", "http://localhost:" + server.getPort()); + } + + @Karate.Test + Karate testUi() { + return Karate.run("classpath:ui/test.feature"); + } + +} diff --git a/examples/ui-test/src/test/java/ui/karate.js b/examples/ui-test/src/test/java/ui/karate.js new file mode 100644 index 000000000..c4500c8d4 --- /dev/null +++ b/examples/ui-test/src/test/java/ui/karate.js @@ -0,0 +1,3 @@ +var karate = {}; +karate.get = function(id) { return document.getElementById(id) }; +karate.setHtml = function(id, value) { this.get(id).innerHTML = value }; diff --git a/examples/ui-test/src/test/java/ui/mock.feature b/examples/ui-test/src/test/java/ui/mock.feature new file mode 100644 index 000000000..f3d847f39 --- /dev/null +++ b/examples/ui-test/src/test/java/ui/mock.feature @@ -0,0 +1,12 @@ +@ignore +Feature: + +Background: + * configure responseHeaders = { 'Content-Type': 'text/html; charset=utf-8' } + +Scenario: pathMatches('/page-01') + * def response = read('page-01.html') + +Scenario: pathMatches('/karate.js') + * def responseHeaders = { 'Content-Type': 'text/javascript; charset=utf-8' } + * def response = karate.readAsString('karate.js') diff --git a/examples/ui-test/src/test/java/ui/page-01.html b/examples/ui-test/src/test/java/ui/page-01.html new file mode 100644 index 000000000..c5bbbcbe7 --- /dev/null +++ b/examples/ui-test/src/test/java/ui/page-01.html @@ -0,0 +1,20 @@ + + + + Page One + + + + + + +
Before
+
Waiting
+ + diff --git a/examples/ui-test/src/test/java/ui/test.feature b/examples/ui-test/src/test/java/ui/test.feature new file mode 100644 index 000000000..51821db94 --- /dev/null +++ b/examples/ui-test/src/test/java/ui/test.feature @@ -0,0 +1,17 @@ +Feature: ui test + +Scenario Outline: + * def webUrlBase = karate.properties['web.url.base'] + * configure driver = { type: '#(type)', showDriverLog: true } + + * driver webUrlBase + '/page-01' + * match text('#placeholder') == 'Before' + * click('{}Click Me') + * match text('#placeholder') == 'After' + +Examples: +| type | +| chrome | +#| chromedriver | +#| geckodriver | +#| safaridriver | diff --git a/examples/zip-release/karate b/examples/zip-release/karate new file mode 100755 index 000000000..6352c93e1 --- /dev/null +++ b/examples/zip-release/karate @@ -0,0 +1 @@ +java -cp karate.jar:. com.intuit.karate.Main $* diff --git a/examples/zip-release/karate.bat b/examples/zip-release/karate.bat new file mode 100644 index 000000000..14a955a1f --- /dev/null +++ b/examples/zip-release/karate.bat @@ -0,0 +1 @@ +java -cp karate.jar;. com.intuit.karate.Main %* diff --git a/examples/zip-release/src/demo/api/users.feature b/examples/zip-release/src/demo/api/users.feature new file mode 100644 index 000000000..0a5f6e5b8 --- /dev/null +++ b/examples/zip-release/src/demo/api/users.feature @@ -0,0 +1,46 @@ +Feature: sample karate api test script + for help, see: https://github.com/intuit/karate/wiki/ZIP-Release + + Background: + * url 'https://jsonplaceholder.typicode.com' + + Scenario: get all users and then get the first user by id + Given path 'users' + When method get + Then status 200 + + * def first = response[0] + + Given path 'users', first.id + When method get + Then status 200 + + Scenario: create a user and then get it by id + * def user = + """ + { + "name": "Test User", + "username": "testuser", + "email": "test@user.com", + "address": { + "street": "Has No Name", + "suite": "Apt. 123", + "city": "Electri", + "zipcode": "54321-6789" + } + } + """ + + Given url 'https://jsonplaceholder.typicode.com/users' + And request user + When method post + Then status 201 + + * def id = response.id + * print 'created id is: ', id + + Given path id + # When method get + # Then status 200 + # And match response contains user + \ No newline at end of file diff --git a/examples/zip-release/src/demo/mock/cats-mock.feature b/examples/zip-release/src/demo/mock/cats-mock.feature new file mode 100644 index 000000000..cc64bb9d2 --- /dev/null +++ b/examples/zip-release/src/demo/mock/cats-mock.feature @@ -0,0 +1,30 @@ +@ignore +Feature: stateful mock server + for help, see: https://github.com/intuit/karate/wiki/ZIP-Release + +Background: +* configure cors = true +* def uuid = function(){ return java.util.UUID.randomUUID() + '' } +* def cats = {} + +Scenario: pathMatches('/cats') && methodIs('post') + * def cat = request + * def id = uuid() + * cat.id = id + * cats[id] = cat + * def response = cat + +Scenario: pathMatches('/cats') + * def response = $cats.* + +Scenario: pathMatches('/cats/{id}') + * def response = cats[pathParams.id] + +Scenario: pathMatches('/hardcoded') + * def response = { hello: 'world' } + +Scenario: + # catch-all + * def responseStatus = 404 + * def responseHeaders = { 'Content-Type': 'text/html; charset=utf-8' } + * def response = Not Found diff --git a/examples/zip-release/src/demo/mock/cats-test.feature b/examples/zip-release/src/demo/mock/cats-test.feature new file mode 100644 index 000000000..ad3a0651f --- /dev/null +++ b/examples/zip-release/src/demo/mock/cats-test.feature @@ -0,0 +1,37 @@ +Feature: integration test for the mock + for help, see: https://github.com/intuit/karate/wiki/ZIP-Release + +Background: + * def port = karate.env == 'mock' ? karate.start('cats-mock.feature').port : 8080 + * url 'http://localhost:' + port + '/cats' + +Scenario: create cat + Given request { name: 'Billie' } + When method post + Then status 200 + And match response == { id: '#uuid', name: 'Billie' } + And def id = response.id + + Given path id + When method get + Then status 200 + And match response == { id: '#(id)', name: 'Billie' } + + When method get + Then status 200 + And match response contains [{ id: '#(id)', name: 'Billie' }] + + Given request { name: 'Bob' } + When method post + Then status 200 + And match response == { id: '#uuid', name: 'Bob' } + And def id = response.id + + Given path id + When method get + Then status 200 + And match response == { id: '#(id)', name: 'Bob' } + + When method get + Then status 200 + And match response contains [{ id: '#uuid', name: 'Billie' },{ id: '#(id)', name: 'Bob' }] diff --git a/examples/zip-release/src/demo/mock/cats.html b/examples/zip-release/src/demo/mock/cats.html new file mode 100644 index 000000000..7d0a49a20 --- /dev/null +++ b/examples/zip-release/src/demo/mock/cats.html @@ -0,0 +1,34 @@ + + + + + Cats + + + + +

+

+ + +
+

+

+ + + + diff --git a/examples/zip-release/src/demo/web/google.feature b/examples/zip-release/src/demo/web/google.feature new file mode 100644 index 000000000..66f1b6ae9 --- /dev/null +++ b/examples/zip-release/src/demo/web/google.feature @@ -0,0 +1,20 @@ +Feature: web-browser automation + for help, see: https://github.com/intuit/karate/wiki/ZIP-Release + +Background: + * configure driver = { type: 'chrome' } + +Scenario: try to login to github + and then do a google search + + Given driver 'https://github.com/login' + And input('#login_field', 'dummy') + And input('#password', 'world') + When submit().click("input[name=commit]") + Then match html('#js-flash-container') contains 'Incorrect username or password.' + + Given driver 'https://google.com' + And input("input[name=q]", 'karate dsl') + When submit().click("input[name=btnI]") + # this may fail depending on which part of the world you are in ! + Then waitForUrl('https://github.com/intuit/karate') diff --git a/karate-apache/pom.xml b/karate-apache/pom.xml index 6256a307c..371e5a98b 100755 --- a/karate-apache/pom.xml +++ b/karate-apache/pom.xml @@ -5,13 +5,13 @@ com.intuit.karate karate-parent - 1.0.0 + 0.9.5 karate-apache jar - 4.5.5 + 4.5.11 diff --git a/karate-apache/src/main/java/com/intuit/karate/http/apache/ApacheHttpClient.java b/karate-apache/src/main/java/com/intuit/karate/http/apache/ApacheHttpClient.java index 7fd0cb498..c1a94819a 100644 --- a/karate-apache/src/main/java/com/intuit/karate/http/apache/ApacheHttpClient.java +++ b/karate-apache/src/main/java/com/intuit/karate/http/apache/ApacheHttpClient.java @@ -39,6 +39,7 @@ import java.io.IOException; import java.io.InputStream; +import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Proxy; import java.net.ProxySelector; @@ -162,6 +163,14 @@ public void configure(Config config, ScenarioContext context) { .setCookieSpec(LenientCookieSpec.KARATE) .setConnectTimeout(config.getConnectTimeout()) .setSocketTimeout(config.getReadTimeout()); + if (config.getLocalAddress() != null) { + try { + InetAddress localAddress = InetAddress.getByName(config.getLocalAddress()); + configBuilder.setLocalAddress(localAddress); + } catch (Exception e) { + context.logger.warn("failed to resolve local address: {} - {}", config.getLocalAddress(), e.getMessage()); + } + } clientBuilder.setDefaultRequestConfig(configBuilder.build()); SocketConfig.Builder socketBuilder = SocketConfig.custom().setSoTimeout(config.getConnectTimeout()); clientBuilder.setDefaultSocketConfig(socketBuilder.build()); @@ -179,12 +188,14 @@ public void configure(Config config, ScenarioContext context) { if (config.getNonProxyHosts() != null) { ProxySelector proxySelector = new ProxySelector() { private final List proxyExceptions = config.getNonProxyHosts(); + @Override public List select(URI uri) { - return Collections.singletonList(proxyExceptions.contains(uri.getHost()) - ? Proxy.NO_PROXY + return Collections.singletonList(proxyExceptions.contains(uri.getHost()) + ? Proxy.NO_PROXY : new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyUri.getHost(), proxyUri.getPort()))); } + @Override public void connectFailed(URI uri, SocketAddress sa, IOException ioe) { context.logger.info("connect failed to uri: {}", uri, ioe); diff --git a/karate-apache/src/main/java/com/intuit/karate/http/apache/LoggingUtils.java b/karate-apache/src/main/java/com/intuit/karate/http/apache/LoggingUtils.java index abe5a65fc..7b6af608a 100644 --- a/karate-apache/src/main/java/com/intuit/karate/http/apache/LoggingUtils.java +++ b/karate-apache/src/main/java/com/intuit/karate/http/apache/LoggingUtils.java @@ -23,6 +23,7 @@ */ package com.intuit.karate.http.apache; +import com.intuit.karate.http.HttpLogModifier; import com.intuit.karate.http.HttpRequest; import com.intuit.karate.http.HttpUtils; import java.util.ArrayList; @@ -52,34 +53,42 @@ private static Collection sortKeys(Header[] headers) { return keys; } - private static void logHeaderLine(StringBuilder sb, int id, char prefix, String key, Header[] headers) { + private static void logHeaderLine(HttpLogModifier logModifier, StringBuilder sb, int id, char prefix, String key, Header[] headers) { sb.append(id).append(' ').append(prefix).append(' ').append(key).append(": "); if (headers.length == 1) { - sb.append(headers[0].getValue()); + if (logModifier == null) { + sb.append(headers[0].getValue()); + } else { + sb.append(logModifier.header(key, headers[0].getValue())); + } } else { List list = new ArrayList(headers.length); for (Header header : headers) { - list.add(header.getValue()); + if (logModifier == null) { + list.add(header.getValue()); + } else { + list.add(logModifier.header(key, header.getValue())); + } } sb.append(list); } sb.append('\n'); } - public static void logHeaders(StringBuilder sb, int id, char prefix, org.apache.http.HttpRequest request, HttpRequest actual) { + public static void logHeaders(HttpLogModifier logModifier, StringBuilder sb, int id, char prefix, org.apache.http.HttpRequest request, HttpRequest actual) { for (String key : sortKeys(request.getAllHeaders())) { Header[] headers = request.getHeaders(key); - logHeaderLine(sb, id, prefix, key, headers); + logHeaderLine(logModifier, sb, id, prefix, key, headers); for (Header header : headers) { actual.addHeader(header.getName(), header.getValue()); } } } - public static void logHeaders(StringBuilder sb, int id, char prefix, HttpResponse response) { + public static void logHeaders(HttpLogModifier logModifier, StringBuilder sb, int id, char prefix, HttpResponse response) { for (String key : sortKeys(response.getAllHeaders())) { Header[] headers = response.getHeaders(key); - logHeaderLine(sb, id, prefix, key, headers); + logHeaderLine(logModifier, sb, id, prefix, key, headers); } } diff --git a/karate-apache/src/main/java/com/intuit/karate/http/apache/RequestLoggingInterceptor.java b/karate-apache/src/main/java/com/intuit/karate/http/apache/RequestLoggingInterceptor.java index 6c89181d5..0faf298b1 100644 --- a/karate-apache/src/main/java/com/intuit/karate/http/apache/RequestLoggingInterceptor.java +++ b/karate-apache/src/main/java/com/intuit/karate/http/apache/RequestLoggingInterceptor.java @@ -25,6 +25,7 @@ import com.intuit.karate.FileUtils; import com.intuit.karate.core.ScenarioContext; +import com.intuit.karate.http.HttpLogModifier; import com.intuit.karate.http.HttpRequest; import java.io.IOException; import java.util.concurrent.atomic.AtomicInteger; @@ -41,10 +42,12 @@ public class RequestLoggingInterceptor implements HttpRequestInterceptor { private final ScenarioContext context; - private final AtomicInteger counter = new AtomicInteger(); + private final HttpLogModifier logModifier; + private final AtomicInteger counter = new AtomicInteger(); public RequestLoggingInterceptor(ScenarioContext context) { this.context = context; + logModifier = context.getConfig().getLogModifier(); } public AtomicInteger getCounter() { @@ -58,10 +61,11 @@ public void process(org.apache.http.HttpRequest request, HttpContext httpContext String uri = (String) httpContext.getAttribute(ApacheHttpClient.URI_CONTEXT_KEY); String method = request.getRequestLine().getMethod(); actual.setUri(uri); - actual.setMethod(method); + actual.setMethod(method); StringBuilder sb = new StringBuilder(); sb.append("request:\n").append(id).append(" > ").append(method).append(' ').append(uri).append('\n'); - LoggingUtils.logHeaders(sb, id, '>', request, actual); + HttpLogModifier requestModifier = logModifier == null ? null : logModifier.enableForUri(uri) ? logModifier : null; + LoggingUtils.logHeaders(requestModifier, sb, id, '>', request, actual); if (request instanceof HttpEntityEnclosingRequest) { HttpEntityEnclosingRequest entityRequest = (HttpEntityEnclosingRequest) request; HttpEntity entity = entityRequest.getEntity(); @@ -71,6 +75,9 @@ public void process(org.apache.http.HttpRequest request, HttpContext httpContext if (context.getConfig().isLogPrettyRequest()) { buffer = FileUtils.toPrettyString(buffer); } + if (requestModifier != null) { + buffer = requestModifier.request(uri, buffer); + } sb.append(buffer).append('\n'); actual.setBody(wrapper.getBytes()); entityRequest.setEntity(wrapper); diff --git a/karate-apache/src/main/java/com/intuit/karate/http/apache/ResponseLoggingInterceptor.java b/karate-apache/src/main/java/com/intuit/karate/http/apache/ResponseLoggingInterceptor.java index 7a6695a31..881cca84a 100644 --- a/karate-apache/src/main/java/com/intuit/karate/http/apache/ResponseLoggingInterceptor.java +++ b/karate-apache/src/main/java/com/intuit/karate/http/apache/ResponseLoggingInterceptor.java @@ -25,6 +25,7 @@ import com.intuit.karate.FileUtils; import com.intuit.karate.core.ScenarioContext; +import com.intuit.karate.http.HttpLogModifier; import com.intuit.karate.http.HttpRequest; import java.io.IOException; import org.apache.http.HttpEntity; @@ -40,11 +41,13 @@ public class ResponseLoggingInterceptor implements HttpResponseInterceptor { private final ScenarioContext context; + private final HttpLogModifier logModifier; private final RequestLoggingInterceptor requestInterceptor; public ResponseLoggingInterceptor(RequestLoggingInterceptor requestInterceptor, ScenarioContext context) { this.requestInterceptor = requestInterceptor; this.context = context; + logModifier = context.getConfig().getLogModifier(); } @Override @@ -55,7 +58,8 @@ public void process(HttpResponse response, HttpContext httpContext) throws HttpE StringBuilder sb = new StringBuilder(); sb.append("response time in milliseconds: ").append(actual.getResponseTimeFormatted()).append('\n'); sb.append(id).append(" < ").append(response.getStatusLine().getStatusCode()).append('\n'); - LoggingUtils.logHeaders(sb, id, '<', response); + HttpLogModifier responseModifier = logModifier == null ? null : logModifier.enableForUri(actual.getUri()) ? logModifier : null; + LoggingUtils.logHeaders(responseModifier, sb, id, '<', response); HttpEntity entity = response.getEntity(); if (LoggingUtils.isPrintable(entity)) { LoggingEntityWrapper wrapper = new LoggingEntityWrapper(entity); @@ -63,6 +67,9 @@ public void process(HttpResponse response, HttpContext httpContext) throws HttpE if (context.getConfig().isLogPrettyResponse()) { buffer = FileUtils.toPrettyString(buffer); } + if (responseModifier != null) { + buffer = responseModifier.response(actual.getUri(), buffer); + } sb.append(buffer).append('\n'); response.setEntity(wrapper); } diff --git a/karate-archetype/pom.xml b/karate-archetype/pom.xml index 986d72f19..ef0c21ee3 100755 --- a/karate-archetype/pom.xml +++ b/karate-archetype/pom.xml @@ -5,7 +5,7 @@ com.intuit.karate karate-parent - 1.0.0 + 0.9.5 karate-archetype jar diff --git a/karate-archetype/src/main/resources/archetype-resources/pom.xml b/karate-archetype/src/main/resources/archetype-resources/pom.xml index 853b6d784..a808a8536 100755 --- a/karate-archetype/src/main/resources/archetype-resources/pom.xml +++ b/karate-archetype/src/main/resources/archetype-resources/pom.xml @@ -11,7 +11,7 @@ UTF-8 1.8 3.6.0 - 0.9.4 + 0.9.5 diff --git a/karate-archetype/src/main/resources/archetype-resources/src/test/java/examples/ExamplesTest.java b/karate-archetype/src/main/resources/archetype-resources/src/test/java/examples/ExamplesTest.java index e9220d45c..923d91f99 100755 --- a/karate-archetype/src/main/resources/archetype-resources/src/test/java/examples/ExamplesTest.java +++ b/karate-archetype/src/main/resources/archetype-resources/src/test/java/examples/ExamplesTest.java @@ -8,7 +8,7 @@ class ExamplesTest { // see https://github.com/intuit/karate#naming-conventions @Karate.Test Karate testAll() { - return new Karate().relativeTo(getClass()); + return Karate.run().relativeTo(getClass()); } } diff --git a/karate-archetype/src/main/resources/archetype-resources/src/test/java/examples/users/UsersRunner.java b/karate-archetype/src/main/resources/archetype-resources/src/test/java/examples/users/UsersRunner.java index 6ed382ddc..6dc6bca86 100755 --- a/karate-archetype/src/main/resources/archetype-resources/src/test/java/examples/users/UsersRunner.java +++ b/karate-archetype/src/main/resources/archetype-resources/src/test/java/examples/users/UsersRunner.java @@ -6,7 +6,7 @@ class UsersRunner { @Karate.Test Karate testUsers() { - return new Karate().feature("users").relativeTo(getClass()); + return Karate.run("users").relativeTo(getClass()); } } diff --git a/karate-core/README.md b/karate-core/README.md index 085ab4457..f7fb8b1d6 100644 --- a/karate-core/README.md +++ b/karate-core/README.md @@ -1,75 +1,210 @@ -# Karate Driver +# Karate UI ## UI Test Automation Made `Simple.` -## Introduction -> This is new, and this first version 0.9.X should be considered experimental. - -Especially after [the Gherkin parser and execution engine were re-written from the ground-up](https://github.com/intuit/karate/issues/444#issuecomment-406877530), Karate is arguably a mature framework that elegantly solves quite a few test-automation engineering challenges - with capabilities such as [parallel execution](https://twitter.com/KarateDSL/status/1049321708241317888), [data-driven testing](https://github.com/intuit/karate#data-driven-tests), [environment-switching](https://github.com/intuit/karate#switching-the-environment), [powerful assertions](https://github.com/intuit/karate#contains-short-cuts), and an [innovative UI for debugging](https://twitter.com/KarateDSL/status/1065602097591156736). - -Which led us to think, what if we could add UI automation without disturbing the core HTTP API testing capabilities. So we gave it a go, and we are releasing the results so far as this experimental version. - -Please do note: this is work in progress and all actions needed for test-automation may not be in-place. But we hope that releasing this sooner would result in more users trying this in a variety of environments. And that they provide valuable feedback and even contribute code where possible. - -We know too well that UI automation is hard to get right and suffers from 2 big challenges, what we like to call the "*flaky test*" problem and the "*wait for UI element*" problem. - -With the help of the community, we would like to try valiantly - to see if we can get close to as ideal a state a possible. So wish us luck ! +### Hello World + + + +# Index + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Start + ZIP Release + | Java + | Maven Quickstart + | Karate - Main Index +
Config + driver + | configure driver + | configure driverTarget + | Docker / karate-chrome + | Driver Types +
Concepts + Syntax + | Special Keys + | Short Cuts + | Chaining + | Function Composition + | Browser JavaScript + | Debugging + | Retries + | Waits + | Distributed Testing + | Proxy +
Locators + Locator Types + | Wildcards + | Friendly Locators + | rightOf() + | leftOf() + | above() + | below() + | near() + | Locator Lookup +
Browser + driver.url + | driver.dimensions + | refresh() + | reload() + | back() + | forward() + | maximize() + | minimize() + | fullscreen() + | quit() +
Page + dialog() + | switchPage() + | switchFrame() + | close() + | driver.title + | screenshot() +
Actions + click() + | input() + | submit() + | focus() + | clear() + | value(set) + | select() + | scroll() + | mouse() + | highlight() + | highlightAll() +
State + html() + | text() + | value() + | attribute() + | enabled() + | exists() + | position() + | locate() + | locateAll() +
Wait / JS + retry() + | waitFor() + | waitForAny() + | waitForUrl() + | waitForText() + | waitForEnabled() + | waitForResultCount() + | waitUntil() + | delay() + | script() + | scriptAll() + | Karate vs the Browser +
Cookies + cookie() + cookie(set) + | driver.cookies + | deleteCookie() + | clearCookies() +
Chrome + Java API + | pdf() + | screenshotFull() +
Appium + Screen Recording + | hideKeyboard() +
## Capabilities -* Direct-to-Chrome automation using the [DevTools protocol](https://chromedevtools.github.io/devtools-protocol/) -* [W3C WebDriver](https://w3c.github.io/webdriver/) support +* Simple, clean syntax that is well suited for people new to programming or test-automation +* All-in-one framework that includes [parallel-execution](https://github.com/intuit/karate#parallel-execution), [HTML reports](https://github.com/intuit/karate#junit-html-report), [environment-switching](https://github.com/intuit/karate#switching-the-environment), and [CI integration](https://github.com/intuit/karate#test-reports) +* Cross-platform - with even the option to run as a programming-language *neutral* [stand-alone executable](https://github.com/intuit/karate/wiki/ZIP-Release) +* No need to learn complicated programming concepts such as "callbacks" "`await`" and "promises" +* Option to use [wildcard](#wildcard-locators) and ["friendly" locators](#friendly-locators) without needing to inspect the HTML-page source, CSS, or internal XPath structure +* Chrome-native automation using the [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/) (equivalent to [Puppeteer](https://pptr.dev)) +* [W3C WebDriver](https://w3c.github.io/webdriver/) support built-in, which can also use [remote / grid providers](https://twitter.com/ptrthomas/status/1222790566598991873) * [Cross-Browser support](https://twitter.com/ptrthomas/status/1048260573513666560) including [Microsoft Edge on Windows](https://twitter.com/ptrthomas/status/1046459965668388866) and [Safari on Mac](https://twitter.com/ptrthomas/status/1047152170468954112) -* WebDriver support without any intermediate server +* [Parallel execution on a single node](https://twitter.com/ptrthomas/status/1159295560794308609), cloud-CI environment or [Docker](#configure-drivertarget) - without needing a "master node" or "grid" +* You can even run tests in parallel across [different machines](#distributed-testing) - and Karate will aggregate the results +* Embed [video-recordings of tests](#karate-chrome) into the HTML report from a Docker container * Windows [Desktop application automation](https://twitter.com/KarateDSL/status/1052432964804640768) using the Microsoft [WinAppDriver](https://github.com/Microsoft/WinAppDriver) -* Android and iOS mobile support via [Appium](http://appium.io), see [details](https://github.com/intuit/karate/issues/743) -* Karate can start the executable (WebDriver / Chrome, WinAppDriver, Appium Server) automatically for you -* Seamlessly mix API and UI tests within the same script -* Use the power of Karate's [`match`](https://github.com/intuit/karate#prepare-mutate-assert) assertions and [core capabilities](https://github.com/intuit/karate#features) for UI element assertions - -### Chrome Java API -Karate also has a Java API to automate the Chrome browser directly, designed for common needs such as converting HTML to PDF or taking a screenshot of a page. You only need the [`karate-core`](https://search.maven.org/search?q=a:karate-core) Maven artifact. Here is an [example](../karate-demo/src/test/java/driver/screenshot/ChromePdfRunner.java): - -```java -import com.intuit.karate.FileUtils; -import com.intuit.karate.driver.chrome.Chrome; -import java.io.File; -import java.util.Collections; - -public class Test { - - public static void main(String[] args) { - Chrome chrome = Chrome.startHeadless(); - chrome.setLocation("https://github.com/login"); - byte[] bytes = chrome.pdf(Collections.EMPTY_MAP); - FileUtils.writeToFile(new File("target/github.pdf"), bytes); - bytes = chrome.screenshot(); - FileUtils.writeToFile(new File("target/github.png"), bytes); - chrome.quit(); - } - -} -``` - -The parameters that you can optionally customize via the `Map` argument to the `pdf()` method are documented here: [`Page.printToPDF -`](https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-printToPDF). - -If Chrome is not installed in the default location, you can pass a String argument like this: `Chrome.startHeadless(executable)` or `Chrome.start(executable)`. For more control or custom options, the `start()` method takes a `Map` argument where the following keys (all optional) are supported: -* `executable` - (String) path to the Chrome executable or batch file that starts Chrome -* `headless` - (Boolean) if headless -* `maxPayloadSize` - (Integer) defaults to 4194304 (bytes, around 4 MB), but you can override it if you deal with very large output / binary payloads - -# Syntax Guide - -## Examples -### Web Browser -* [Example 1](../karate-demo/src/test/java/driver/demo/demo-01.feature) -* [Example 2](../karate-demo/src/test/java/driver/core/test-01.feature) -### Windows -* [Example](../karate-demo/src/test/java/driver/windows/calc.feature) - -## Driver Configuration - -### `configure driver` +* [Android and iOS mobile support](https://github.com/intuit/karate/issues/743) via [Appium](http://appium.io) +* Seamlessly mix API and UI tests within the same script, for example [sign-in using an API](https://github.com/intuit/karate#http-basic-authentication-example) and speed-up your tests +* Use the power of Karate's [`match`](https://github.com/intuit/karate#prepare-mutate-assert) assertions and [core capabilities](https://github.com/intuit/karate#features) for UI assertions +* Simple [retry](#retry) and [wait](#wait-api) strategy, no need to graduate from any test-automation university to understand the difference between "implicit waits", "explicit waits" and "fluent waits" :) +* Simpler, [elegant, and *DRY* alternative](#locator-lookup) to the so-called "Page Object Model" pattern +* Carefully designed [fluent-API](#chaining) to handle common combinations such as a [`submit()` + `click()`](#submit) action +* Elegant syntax for typical web-automation challenges such as waiting for a [page-load](#waitforurl-instead-of-submit) or [element to appear](#waitfor) +* Execute JavaScript in the browser with [one-liners](#script) - for example to [get data out of an HTML table](#scriptall) +* [Compose re-usable functions](#function-composition) based on your specific environment or application needs +* Comprehensive support for user-input types including [key-combinations](#special-keys) and [`mouse()`](#mouse) actions +* Step-debug and even *"go back in time"* to edit and re-play steps - using the unique, innovative [Karate Extension for Visual Studio Code](https://twitter.com/KarateDSL/status/1167533484560142336) +* Traceability: detailed [wire-protocol logs](https://twitter.com/ptrthomas/status/1155958170335891467) can be enabled *in-line* with test-steps in the HTML report +* Convert HTML to PDF and capture the *entire* (scrollable) web-page as an image using the [Chrome Java API](#chrome-java-api) + +## Comparison +To understand how Karate compares to other UI automation frameworks, this article can be a good starting point: [The world needs an alternative to Selenium - *so we built one*](https://hackernoon.com/the-world-needs-an-alternative-to-selenium-so-we-built-one-zrk3j3nyr). + +# Examples +## Web Browser +* [Example 1](../karate-demo/src/test/java/driver/demo/demo-01.feature) - simple example that navigates to GitHub and Google Search +* [Example 2](../karate-demo/src/test/java/driver/demo/demo-02.feature) - simple but *very* relevant and meaty example ([see video](https://twitter.com/ptrthomas/status/1160680240781262851)) that shows how to + * wait for [page-navigation](#waitforurl-instead-of-submit) + * use a friendly [wildcard locator](#wildcard-locators) + * wait for an element to [be ready](#waitfor) + * [compose functions](#function-composition) for elegant *custom* "wait" logic + * assert on tabular [results in the HTML](#scriptall) +* [Example 3](../karate-demo/src/test/java/driver/core/test-01.feature) - which is a single script that exercises *all* capabilities of Karate Driver, so is a handy reference + +## Windows +* [Example](../karate-demo/src/test/java/driver/windows/calc.feature) - but also see the [`karate-robot`](https://github.com/intuit/karate/tree/master/karate-robot) for an alternative approach. + +# Driver Configuration + +## `configure driver` This below declares that the native (direct) Chrome integration should be used, on both Mac OS and Windows - from the default installed location. @@ -107,264 +242,1277 @@ key | description `executable` | if present, Karate will attempt to invoke this, if not in the system `PATH`, you can use a full-path instead of just the name of the executable. batch files should also work `start` | default `true`, Karate will attempt to start the `executable` - and if the `executable` is not defined, Karate will even try to assume the default for the OS in use `port` | optional, and Karate would choose the "traditional" port for the given `type` -`headless` | only applies to `type: 'chrome'` for now +`host` | optional, will default to `localhost` and you normally never need to change this +`pollAttempts` | optional, will default to `20`, you normally never need to change this (and changing `pollInterval` is preferred), and this is the number of attempts Karate will make to wait for the `port` to be ready and accepting connections before proceeding +`pollInterval` | optional, will default to `250` (milliseconds) and you normally never need to change this (see `pollAttempts`) unless the driver `executable` takes a *very* long time to start +`headless` | [headless mode](https://developers.google.com/web/updates/2017/04/headless-chrome) only applies to `{ type: 'chrome' }` for now, also see [`DockerTarget`](#dockertarget) and the `webDriverCapabilities` key `showDriverLog` | default `false`, will include webdriver HTTP traffic in Karate report, useful for troubleshooting or bug reports `showProcessLog` | default `false`, will include even executable (webdriver or browser) logs in the Karate report +`addOptions` | default `null`, has to be a list / JSON array that will be appended as additional CLI arguments to the `executable`, e.g. `['--no-sandbox', '--windows-size=1920,1080']` +`beforeStart` | default `null`, an OS command that will be executed before commencing a `Scenario` (and before the `executable` is invoked if applicable) typically used to start video-recording +`afterStart` | default `null`, an OS command that will be executed after a `Scenario` completes, typically used to stop video-recording and save the video file to an output folder +`videoFile` | default `null`, the path to the video file that will be added to the end of the test report, if it does not exist, it will be ignored +`httpConfig` | optional, and typically only used for remote WebDriver usage where the HTTP client [configuration](https://github.com/intuit/karate#configure) needs to be tweaked, e.g. `{ readTimeout: 120000 }` +`webDriverUrl` | see [`webDriverUrl`](#webdriverurl) +`webDriverSession` | see [`webDriverSession`](#webdriversession) +`webDriverPath` | optional, and rarely used only in case you need to append a path such as `/wd/hub` - typically needed for Appium (or a Selenium Grid) on `localhost`, where `host`, `port` / `executable` etc. are involved. + +For more advanced options such as for Docker, CI, headless, cloud-environments or custom needs, see [`configure driverTarget`](#configure-drivertarget). + +## webDriverUrl +Karate implements the [W3C WebDriver spec](https://w3c.github.io/webdriver), which means that you can point Karate to a remote "grid" such as [Zalenium](https://opensource.zalando.com/zalenium/) or a SaaS provider such as [the AWS Device Farm](https://docs.aws.amazon.com/devicefarm/latest/testgrid/what-is-testgrid.html). The `webDriverUrl` driver configuration key is optional, but if specified, will be used as the W3C WebDriver remote server. Note that you typically would set `start: false` as well, or use a [Custom `Target`](#custom-target). + +For example, once you run the [couple of Docker commands](https://opensource.zalando.com/zalenium/#try-it) to get Zalenium running, you can do this: + +```cucumber +* configure driver = { type: 'chromedriver', start: false, webDriverUrl: 'http://localhost:4444/wd/hub' } +``` + +Note that you can add `showDriverLog: true` to the above for troubleshooting if needed. You should be able to [run tests in parallel](https://github.com/intuit/karate#parallel-execution) with ease ! + +## `webDriverSession` +When targeting a W3C WebDriver implementation, either as a local executable or [Remote WebDriver](https://selenium.dev/documentation/en/remote_webdriver/remote_webdriver_client/), you can specify the JSON that will be passed as the payload to the [Create Session](https://w3c.github.io/webdriver/#new-session) API. The most important part of this payload is the [`capabilities`](https://w3c.github.io/webdriver/#capabilities). It will default to `{ browserName: '' }` for convenience where `` will be `chrome`, `firefox` etc. + +So most of the time this would be sufficient: + +```cucumber +* configure driver = { type: 'chromedriver' } +``` + +Since it will result in the following request to the WebDriver `/session`: + +```js +{"capabilities":{"alwaysMatch":{"browserName":"chrome"}}} +``` + +But in some cases, especially when you need to talk to remote driver instances, you need to pass specific "shapes" of JSON expected by the particular implementation - or you may need to pass custom data or "extension" properties. Use the `webDriverSession` property in those cases. For example: + +```cucumber +* def session = { capabilities: { browserName: 'chrome' }, desiredCapabilities: { browserName: 'chrome' } } +* configure driver = { type: 'chromedriver', webDriverSession: '#(session)', start: false, webDriverUrl: 'http://localhost:9515/wd/hub' } +``` + +Here are some of the things that you can customize, but note that these depend on the driver implementation. + +* [`proxy`](#proxy) +* [`acceptInsecureCerts`](https://w3c.github.io/webdriver/#dfn-insecure-tls-certificates) +* [`moz:firefoxOptions`](https://developer.mozilla.org/en-US/docs/Web/WebDriver/Capabilities/firefoxOptions#firefoxOptions) - e.g. for headless FireFox + +Note that some capabilities such as "headless" may be possible via the command-line to the local executable, so using [`addOptions`](#configure-driver) may work instead. + +## `configure driverTarget` +The [`configure driver`](#configure-driver) options are fine for testing on "`localhost`" and when not in `headless` mode. But when the time comes for running your web-UI automation tests on a continuous integration server, things get interesting. To support all the various options such as Docker, headless Chrome, cloud-providers etc., Karate introduces the concept of a pluggable [`Target`](src/main/java/com/intuit/karate/driver/Target.java) where you just have to implement two methods: + +```java +public interface Target { + + Map start(com.intuit.karate.Logger logger); + + Map stop(com.intuit.karate.Logger logger); + +} +``` + +* `start()`: The `Map` returned will be used as the generated [driver configuration](#driver-configuration). And the `start()` method will be invoked as soon as any `Scenario` requests for a web-browser instance (for the first time) via the [`driver`](#driver) keyword. + +* `stop()`: Karate will call this method at the end of every top-level `Scenario` (that has not been `call`-ed by another `Scenario`). + +If you use the provided `Logger` instance in your `Target` code, any logging you perform will nicely appear in-line with test-steps in the HTML report, which is great for troubleshooting or debugging tests. + +Combined with Docker, headless Chrome and Karate's [parallel-execution capabilities](https://github.com/intuit/karate#parallel-execution) - this simple `start()` and `stop()` lifecycle can effectively run web UI automation tests in parallel on a single node. + +### `DockerTarget` +Karate has a built-in implementation for Docker ([`DockerTarget`](src/main/java/com/intuit/karate/driver/DockerTarget.java)) that supports 2 existing Docker images out of the box: + +* [`justinribeiro/chrome-headless`](https://hub.docker.com/r/justinribeiro/chrome-headless/) - for Chrome "native" in [headless mode](https://developers.google.com/web/updates/2017/04/headless-chrome) +* [`ptrthomas/karate-chrome`](#karate-chrome) - for Chrome "native" but with an option to connect to the container and view via VNC, and with video-recording + +To use either of the above, you do this in a Karate test: + +```cucumber +* configure driverTarget = { docker: 'justinribeiro/chrome-headless', showDriverLog: true } +``` + +Or for more flexibility, you could do this in [`karate-config.js`](https://github.com/intuit/karate#configuration) and perform conditional logic based on [`karate.env`](https://github.com/intuit/karate#switching-the-environment). One very convenient aspect of `configure driverTarget` is that *if* in-scope, it will over-ride any `configure driver` directives that exist. This means that you can have the below snippet activate *only* for your CI build, and you can leave your feature files set to point to what you would use in "dev-local" mode. + +```javascript +function fn() { + var config = { + baseUrl: 'https://qa.mycompany.com' + }; + if (karate.env == 'ci') { + karate.configure('driverTarget', { docker: 'ptrthomas/karate-chrome' }); + } + return config; +} +``` + +To use the [recommended `--security-opt seccomp=chrome.json` Docker option](https://hub.docker.com/r/justinribeiro/chrome-headless/), add a `secComp` property to the `driverTarget` configuration. And if you need to view the container display via VNC, set the `vncPort` to map the port exposed by Docker. + +```javascript +karate.configure('driverTarget', { docker: 'ptrthomas/karate-chrome', secComp: 'src/test/java/chrome.json', vncPort: 5900 }); +``` + +### Custom `Target` +If you have a custom implementation of a `Target`, you can easily [construct any custom Java class](https://github.com/intuit/karate#calling-java) and pass it to `configure driverTarget`. Here below is the equivalent of the above, done the "hard way": + +```javascript +var DockerTarget = Java.type('com.intuit.karate.driver.DockerTarget'); +var options = { showDriverLog: true }; +var target = new DockerTarget(options); +target.command = function(port){ return 'docker run -d -p ' + + port + ':9222 --security-opt seccomp=./chrome.json justinribeiro/chrome-headless' }; +karate.configure('driverTarget', target); +``` + +The built-in [`DockerTarget`](src/main/java/com/intuit/karate/driver/DockerTarget.java) is a good example of how to: +* perform any pre-test set-up actions +* provision a free port and use it to shape the `start()` command dynamically +* execute the command to start the target process +* perform an HTTP health check to wait until we are ready to receive connections +* and when `stop()` is called, indicate if a video recording is present (after retrieving it from the stopped container) + +Controlling this flow from Java can take a lot of complexity out your build pipeline and keep things cross-platform. And you don't need to line-up an assortment of shell-scripts to do all these things. You can potentially include the steps of deploying (and un-deploying) the application-under-test using this approach - but probably the top-level [JUnit test-suite](https://github.com/intuit/karate#parallel-execution) would be the right place for those. + +### `karate-chrome` +The [`karate-chrome`](https://hub.docker.com/r/ptrthomas/karate-chrome) Docker is an image created from scratch, using a Java / Maven image as a base and with the following features: + +* Chrome in "full" mode (non-headless) +* [Chrome DevTools protocol](https://chromedevtools.github.io/devtools-protocol/) exposed on port 9222 +* VNC server exposed on port 5900 so that you can watch the browser in real-time +* a video of the entire test is saved to `/tmp/karate.mp4` +* after the test, when `stop()` is called, the [`DockerTarget`](src/main/java/com/intuit/karate/driver/DockerTarget.java) will embed the video into the HTML report (expand the last step in the `Scenario` to view) + +To try this or especially when you need to investigate why a test is not behaving properly when running within Docker, these are the steps: + +* start the container: + * `docker run --name karate --rm -p 9222:9222 -p 5900:5900 -e KARATE_SOCAT_START=true --cap-add=SYS_ADMIN ptrthomas/karate-chrome` + * it is recommended to use [`--security-opt seccomp=chrome.json`](https://hub.docker.com/r/justinribeiro/chrome-headless/) instead of `--cap-add=SYS_ADMIN` +* point your VNC client to `localhost:5900` (password: `karate`) + * for example on a Mac you can use this command: `open vnc://localhost:5900` +* run a test using the following [`driver` configuration](#configure-driver), and this is one of the few times you would ever need to set the [`start` flag](#configure-driver) to `false` + * `* configure driver = { type: 'chrome', start: false, showDriverLog: true }` +* you can even use the [Karate VS Code extension](https://github.com/intuit/karate/wiki/IDE-Support#vs-code-karate-plugin) to debug and step-through a test +* if you omit the `--rm` part in the start command, after stopping the container, you can dump the logs and video recording using this command (here `.` stands for the current working folder, change it if needed): + * `docker cp karate:/tmp .` + * this would include the `stderr` and `stdout` logs from Chrome, which can be helpful for troubleshooting + +For more information on the Docker containers for Karate and how to use them, refer to the wiki: [Docker](https://github.com/intuit/karate/wiki/Docker). ## Driver Types -type | default
port | default
executable | description ----- | ---------------- | ---------------------- | ----------- +The recommendation is that you prefer `chrome` for development, and once you have the tests running smoothly - you can switch to a different WebDriver implementation. + +type | default port | default executable | description +---- | ------------ | ------------------ | ----------- [`chrome`](https://chromedevtools.github.io/devtools-protocol/) | 9222 | mac: `/Applications/Google Chrome.app/Contents/MacOS/Google Chrome`
win: `C:/Program Files (x86)/Google/Chrome/Application/chrome.exe` | "native" Chrome automation via the [DevTools protocol](https://chromedevtools.github.io/devtools-protocol/) [`chromedriver`](https://sites.google.com/a/chromium.org/chromedriver/home) | 9515 | `chromedriver` | W3C Chrome Driver [`geckodriver`](https://github.com/mozilla/geckodriver) | 4444 | `geckodriver` | W3C Gecko Driver (Firefox) [`safaridriver`](https://webkit.org/blog/6900/webdriver-support-in-safari-10/) | 5555 | `safaridriver` | W3C Safari Driver [`mswebdriver`](https://docs.microsoft.com/en-us/microsoft-edge/webdriver) | 17556 | `MicrosoftWebDriver` | W3C Microsoft Edge WebDriver +[`iedriver`](https://github.com/SeleniumHQ/selenium/wiki/InternetExplorerDriver) | 5555 | `IEDriverServer` | IE (11 only) Driver [`msedge`](https://docs.microsoft.com/en-us/microsoft-edge/devtools-protocol/) | 9222 | `MicrosoftEdge` | *very* experimental - using the DevTools protocol [`winappdriver`](https://github.com/Microsoft/WinAppDriver) | 4727 | `C:/Program Files (x86)/Windows Application Driver/WinAppDriver` | Windows Desktop automation, similar to Appium [`android`](https://github.com/appium/appium/) | 4723 | `appium` | android automation via [Appium](https://github.com/appium/appium/) [`ios`](https://github.com/appium/appium/) | 4723 |`appium` | iOS automation via [Appium](https://github.com/appium/appium/) -## Locators +# Distributed Testing +Karate can split a test-suite across multiple machines or Docker containers for execution and aggregate the results. Please refer to the wiki: [Distributed Testing](https://github.com/intuit/karate/wiki/Distributed-Testing). + +# Locators The standard locator syntax is supported. For example for web-automation, a `/` prefix means XPath and else it would be evaluated as a "CSS selector". ```cucumber -And driver.input('input[name=someName]', 'test input') -When driver.submit("//input[@name='commit']") +And input('input[name=someName]', 'test input') +When submit().click("//input[@name='commit']") ``` platform | prefix | means | example ----- | ------ | ----- | ------- web | (none) | css selector | `input[name=someName]` web
android
ios | `/` | xpath | `//input[@name='commit']` -web | `^` | link text | `^Click Me` -web | `*` | partial link text | `*Click Me` +web | `{}` | [exact text content](#wildcard-locators) | `{a}Click Me` +web | `{^}` | [partial text content](#wildcard-locators) | `{^a}Click Me` win
android
ios| (none) | name | `Submit` win
android
ios | `@` | accessibility id | `@CalculatorResults` win
android
ios | `#` | id | `#MyButton` ios| `:` | -ios predicate string | `:name == 'OK' type == XCUIElementTypeButton` -ios| `^` | -ios class chain | ``^**/XCUIElementTypeTable[`name == 'dataTable'`]`` +ios| `^` | -ios class chain | `^**/XCUIElementTypeTable[name == 'dataTable']` android| `-` | -android uiautomator | `-input[name=someName]` -## Keywords -Only one keyword sets up UI automation in Karate, typically by specifying the URL to open in a browser. And then you would use the built-in [`driver`](#js-api) JS object for all other operations, combined with Karate's [`match`](https://github.com/intuit/karate#prepare-mutate-assert) syntax for assertions where needed. +## Wildcard Locators +The "`{}`" and "`{^}`" locator-prefixes are designed to make finding an HTML element by *text content* super-easy. You will typically also match against a specific HTML tag (which is preferred, and faster at run-time). But even if you use "`{*}`" (or "`{}`" which is the equivalent short-cut) to match *any* tag, you are selecting based on what the user *sees on the page*. + +When you use CSS and XPath, you need to understand the internal CSS class-names and XPath structure of the page. But when you use the visible text-content, for example the text within a ` +
Click Me
+ + + Click Me + + + +
+ + Click Me +
+ +
+
+
\ No newline at end of file diff --git a/karate-demo/src/test/java/driver/core/page-03.html b/karate-demo/src/test/java/driver/core/page-03.html index e7f7f362c..311a3792c 100644 --- a/karate-demo/src/test/java/driver/core/page-03.html +++ b/karate-demo/src/test/java/driver/core/page-03.html @@ -2,12 +2,14 @@ Page Three +
@@data1@@
@@data2@@
@@data3@@
+
@@data4@@
Go to Page One
@@ -17,12 +19,13 @@
- Check One
- Check Two
- Check Three

-
+ Check One
+ Check Two
+ Check Three
+
Input On Right
+
Input On Left

-
+ \ No newline at end of file diff --git a/karate-demo/src/test/java/driver/core/page-04.html b/karate-demo/src/test/java/driver/core/page-04.html new file mode 100644 index 000000000..cae4cdfa8 --- /dev/null +++ b/karate-demo/src/test/java/driver/core/page-04.html @@ -0,0 +1,33 @@ + + + + Page Four + + + + + +
+
this div is outside the iframe
+ +
+
+
+
+
+
+
+ + \ No newline at end of file diff --git a/karate-demo/src/test/java/driver/core/test-01.feature b/karate-demo/src/test/java/driver/core/test-01.feature index 87b11a07c..da81cf6c9 100644 --- a/karate-demo/src/test/java/driver/core/test-01.feature +++ b/karate-demo/src/test/java/driver/core/test-01.feature @@ -9,114 +9,286 @@ Scenario Outline: using * configure driver = config Given driver webUrlBase + '/page-01' + + # wait for very slow loading element + And waitFor('#eg01WaitId') + + # wait for text (is a string "contains" match for convenience) + And waitForText('#eg01WaitId', 'APPEARED') + And waitForText('body', 'APPEARED') + And waitForEnabled('#eg01WaitId') + + # powerful variants of the above, call any js on the element + And waitUntil('#eg01WaitId', "function(e){ return e.innerHTML == 'APPEARED!' }") + And waitUntil('#eg01WaitId', "_.innerHTML == 'APPEARED!'") + And waitUntil('#eg01WaitId', '!_.disabled') + + And match script('#eg01WaitId', "function(e){ return e.innerHTML }") == 'APPEARED!' + And match script('#eg01WaitId', '_.innerHTML') == 'APPEARED!' + And match script('#eg01WaitId', '!_.disabled') == true + + # key events and key combinations + And input('#eg02InputId', Key.CONTROL + 'a') + And def temp = text('#eg02DivId') + And match temp contains '17d' + And match temp contains '65u' + And script('#eg02DivId', "_.innerHTML = ''") + When input('#eg02InputId', 'aa') + Then def temp = text('#eg02DivId') + And match temp contains '65u' + # cookies * def cookie1 = { name: 'foo', value: 'bar' } And match driver.cookies contains '#(^cookie1)' - And match driver.cookie('foo') contains cookie1 + And match cookie('foo') contains cookie1 + # set window size And driver.dimensions = - And driver.input('#eg01InputId', 'hello world') - When driver.click('input[name=eg01SubmitName]') - Then match driver.text('#eg01DivId') == 'hello world' - And match driver.value('#eg01InputId') == 'hello world' - And match driver.attribute('#eg01SubmitId', 'type') == 'submit' - And match driver.name('#eg01SubmitId') == 'INPUT' - And match driver.enabled('#eg01InputId') == true - And match driver.enabled('#eg01DisabledId') == false - - When driver.input('#eg01InputId', 'something else', true) - And match driver.value('#eg01InputId') == 'something else' - When driver.value('#eg01InputId', 'something more') - And match driver.value('#eg01InputId') == 'something more' + # navigation and dom checks + And input('#eg01InputId', 'hello world') + When click('input[name=eg01SubmitName]') + And match value('#eg01InputId') == 'hello world' + Then match text('#eg01DivId') == 'hello world' + And match attribute('#eg01SubmitId', 'type') == 'submit' + And match script('#eg01SubmitId', '_.tagName') == 'INPUT' + And match enabled('#eg01InputId') == true + And match enabled('#eg01DisabledId') == false + + # clear before input + When clear('#eg01InputId') + And input('#eg01InputId', 'something else') + And match value('#eg01InputId') == 'something else' + When value('#eg01InputId', 'something more') + And match value('#eg01InputId') == 'something more' - When driver.refresh() - Then match driver.location == webUrlBase + '/page-01' - And match driver.text('#eg01DivId') == '' - And match driver.value('#eg01InputId') == '' + # refresh + When refresh() + Then match driver.url == webUrlBase + '/page-01' + And match text('#eg01DivId') == '' + And match value('#eg01InputId') == '' And match driver.title == 'Page One' + # navigate to page and css checks When driver webUrlBase + '/page-02' - Then match driver.text('.eg01Cls') == 'Class Locator Test' - And match driver.html('.eg01Cls') == 'Class Locator Test' + Then match text('.eg01Cls') == 'Class Locator Test' + And match html('.eg01Cls') == '
Class Locator Test
' And match driver.title == 'Page Two' - And match driver.location == webUrlBase + '/page-02' - And match driver.css('.eg01Cls', 'background-color') contains '(255, 255, 0' + And match driver.url == webUrlBase + '/page-02' + # set cookie Given def cookie2 = { name: 'hello', value: 'world' } - When driver.cookie = cookie2 + When cookie(cookie2) Then match driver.cookies contains '#(^cookie2)' - When driver.deleteCookie('foo') + # delete cookie + When deleteCookie('foo') Then match driver.cookies !contains '#(^cookie1)' - When driver.clearCookies() + # clear cookies + When clearCookies() Then match driver.cookies == '#[0]' - When driver.back() - Then match driver.location == webUrlBase + '/page-01' + # back and forward + When back() + Then match driver.url == webUrlBase + '/page-01' And match driver.title == 'Page One' - - When driver.forward() - Then match driver.location == webUrlBase + '/page-02' + When forward() + Then match driver.url == webUrlBase + '/page-02' And match driver.title == 'Page Two' - When driver.click('^Show Alert', true) + # wildcard locators + * click('{a}Click Me') + * match text('#eg03Result') == 'A' + * click('{^span}Me') + * match text('#eg03Result') == 'SPAN' + * click('{div}Click Me') + * match text('#eg03Result') == 'DIV' + * click('{^div:2}Click') + * match text('#eg03Result') == 'SECOND' + * click('{span/a}Click Me') + * match text('#eg03Result') == 'NESTED' + * click('{:4}Click Me') + * match text('#eg03Result') == 'BUTTON' + * click("{^button:2}Item") + * match text('#eg03Result') == 'ITEM2' + + # locate + * def element = locate('{}Click Me') + * assert element.exists + + # locate all + * def elements = locateAll('{}Click Me') + * match karate.sizeOf(elements) == 7 + * elements.get(6).click() + * match text('#eg03Result') == 'SECOND' + * match elements.get(3).script('_.tagName') == 'BUTTON' + + # dialog - alert + When click('{}Show Alert') Then match driver.dialog == 'this is an alert' - And driver.dialog(true) + And dialog(true) - When driver.click('^Show Confirm', true) + # dialog - confirm true + When click('{}Show Confirm') Then match driver.dialog == 'this is a confirm' - And driver.dialog(false) - And match driver.text('#eg02DivId') == 'Cancel' + And dialog(false) + And match text('#eg02DivId') == 'Cancel' - When driver.click('^Show Confirm', true) - And driver.dialog(true) - And match driver.text('#eg02DivId') == 'OK' + # dialog - confirm false + When click('{}Show Confirm') + And dialog(true) + And match text('#eg02DivId') == 'OK' - When driver.click('^Show Prompt', true) + # dialog - prompt + When click('{}Show Prompt') Then match driver.dialog == 'this is a prompt' - And driver.dialog(true, 'hello world') - And match driver.text('#eg02DivId') == 'hello world' + And dialog(true, 'hello world') + And match text('#eg02DivId') == 'hello world' + + # screenshot of selected element + * screenshot('#eg02DivId') - * def bytes = driver.screenshot('#eg02DivId') - * karate.write(bytes, 'partial-' + config.type + '.png') - * match driver.rect('#eg02DivId') == { x: '#number', y: '#number', height: '#number', width: '#number' } + # get element dimensions + * match position('#eg02DivId') contains { x: '#number', y: '#number', width: '#number', height: '#number' } - When driver.click('^New Tab') - And driver.waitUntil("document.readyState == 'complete'") + # new tab opens + When click('{}New Tab') - When driver.switchTo('Page Two') + # switch back to first tab + When switchPage('Page Two') Then match driver.title == 'Page Two' - And match driver.location contains webUrlBase + '/page-02' + And match driver.url contains webUrlBase + '/page-02' - When driver.submit('*Page Three') + # submit - action that waits for page navigation + When submit().click('{^}Page Three') And match driver.title == 'Page Three' - And match driver.location == webUrlBase + '/page-03' + And match driver.url == webUrlBase + '/page-03' + + # get html for all elements that match css selector + When def list = scriptAll('div#eg01 div', '_.innerHTML') + Then match list == '#[4]' + And match each list contains '@@data' + + # powerful wait designed for tabular results that take time to load + When def list = waitForResultCount('div#eg01 div', 4) + Then match list == '#[4]' + + When def list = waitForResultCount('div#eg01 div', 4, '_.innerHTML') + Then match list == '#[4]' + And match each list contains '@@data' + + # get html for all elements that match xpath selector + When def list = scriptAll('//option', '_.innerHTML') + Then match list == '#[3]' + And match each list contains 'Option' + + # get text for all elements that match css selector + When def list = scriptAll('div#eg01 div', '_.textContent') + Then match list == '#[4]' + And match each list contains '@@data' + + # get text for all but only containing given text + When def list = scriptAll('div#eg01 div', '_.textContent', function(x){ return x.contains('data2') }) + Then match list == ['@@data2@@'] + + # get text for all elements that match xpath selector + When def list = scriptAll('//option', '_.textContent') + Then match list == '#[3]' + And match each list contains 'Option' + + # get value for all elements that match css selector + When def list = scriptAll("input[name='data2']", '_.value') + Then match list == '#[3]' + And match each list contains 'check' + + # get value for all elements that match xpath selector + When def list = scriptAll("//input[@name='data2']", '_.value') + Then match list == '#[3]' + And match each list contains 'check' + + # select option with text + Given select('select[name=data1]', '{}Option Two') + And click('input[value=check2]') + When submit().click('#eg02SubmitId') + And match text('#eg01Data1') == 'option2' + And match text('#eg01Data2') == 'check2' + + # select option containing text + Given select('select[name=data1]', '{^}Two') + And click('[value=check2]') + And click('[value=check1]') + When submit().click('#eg02SubmitId') + And match text('#eg01Data1') == 'option2' + And match text('#eg01Data2') == '["check1","check2"]' + + # select option by value + Given select('select[name=data1]', 'option2') + When submit().click('#eg02SubmitId') + And match text('#eg01Data1') == 'option2' + + # friendly locators: leftOf / rightOf + * leftOf('{}Check Three').click() + * rightOf('{}Input On Right').input('input right') + * leftOf('{}Input On Left').clear().input('input left') + * submit().click('#eg02SubmitId') + * match text('#eg01Data2') == 'check3' + * match text('#eg01Data3') == 'Some Textinput right' + * match text('#eg01Data4') == 'input left' + + # friendly locators: above / below / near + * near('{}Go to Page One').click() + * below('{}Input On Right').input('input below') + * above('{}Input On Left').clear().input('input above') + * submit().click('#eg02SubmitId') + # TODO problem in safari + # * match text('#eg01Data2') == 'check1' + * match text('#eg01Data3') == 'input above' + * match text('#eg01Data4') == 'Some Textinput below' + + # friendly locator find by visible text + * above('{}Input On Right').find('{}Go to Page One').click() + * waitForUrl('/page-01') + + # switch to iframe by index + Given driver webUrlBase + '/page-04' + And match driver.url == webUrlBase + '/page-04' + # TODO problem with safari + And switchFrame(config.type == 'safaridriver' ? '#frame01' : 0) + When input('#eg01InputId', 'hello world') + And click('#eg01SubmitId') + Then match text('#eg01DivId') == 'hello world' - Given driver.select('select[name=data1]', '^Option Two') - And driver.click('input[value=check2]') - When driver.submit('#eg02SubmitId') - And match driver.text('#eg01Data1') == 'option2' - And match driver.text('#eg01Data2') == 'check2' + # switch back to parent frame + When switchFrame(null) + Then match text('#eg01DivId') == 'this div is outside the iframe' - Given driver.select('select[name=data1]', '*Two') - And driver.click('[value=check2]') - And driver.click('[value=check1]') - When driver.submit('#eg02SubmitId') - And match driver.text('#eg01Data1') == 'option2' - And match driver.text('#eg01Data2') == '["check1","check2"]' + # switch to iframe by locator + Given driver webUrlBase + '/page-04' + And match driver.url == webUrlBase + '/page-04' + And switchFrame('#frame01') + When input('#eg01InputId', 'hello world') + And click('#eg01SubmitId') + Then match text('#eg01DivId') == 'hello world' + And switchFrame(null) - Given driver.select('select[name=data1]', 'option2') - When driver.submit('#eg02SubmitId') - And match driver.text('#eg01Data1') == 'option2' + # mouse move and click + * mouse('#eg02LeftDivId').go() + * mouse('#eg02RightDivId').click() + * mouse().down().move('#eg02LeftDivId').up() + * def temp = text('#eg02ResultDivId') + # works only for chrome :( + # * match temp contains 'LEFT_HOVERED' + # * match temp contains 'RIGHT_CLICKED' + # * match temp !contains 'LEFT_DOWN' + # * match temp contains 'LEFT_UP' Examples: | config | dimensions | - # | { type: 'chrome' } | { left: 0, top: 0, width: 300, height: 800 } | - | { type: 'chromedriver' } | { left: 100, top: 0, width: 300, height: 800 } | - | { type: 'geckodriver' } | { left: 600, top: 0, width: 300, height: 800 } | - | { type: 'safaridriver' } | { left: 1000, top: 0, width: 300, height: 800 } | + | { type: 'chrome' } | { x: 0, y: 0, width: 300, height: 800 } | + | { type: 'chromedriver' } | { x: 50, y: 0, width: 250, height: 800 } | + | { type: 'geckodriver' } | { x: 600, y: 0, width: 300, height: 800 } | + | { type: 'safaridriver' } | { x: 1000, y: 0, width: 400, height: 800 } | # | { type: 'mswebdriver' } | # | { type: 'msedge' } | \ No newline at end of file diff --git a/karate-demo/src/test/java/driver/core/test-02-called.feature b/karate-demo/src/test/java/driver/core/test-02-called.feature index fdbcf9b2e..9d86182ed 100644 --- a/karate-demo/src/test/java/driver/core/test-02-called.feature +++ b/karate-demo/src/test/java/driver/core/test-02-called.feature @@ -2,9 +2,6 @@ Feature: common driver init code Scenario: - * configure driver = { type: 'chrome' } + * configure driver = { type: 'chrome', showDriverLog: true } * def webUrlBase = karate.properties['web.url.base'] * driver webUrlBase + '/page-01' - - - \ No newline at end of file diff --git a/karate-demo/src/test/java/driver/core/test-02.feature b/karate-demo/src/test/java/driver/core/test-02.feature index f38bccb5b..d94527bdf 100644 --- a/karate-demo/src/test/java/driver/core/test-02.feature +++ b/karate-demo/src/test/java/driver/core/test-02.feature @@ -2,9 +2,9 @@ Feature: calling a re-usable feature that inits the driver instance Scenario: * callonce read('test-02-called.feature') - Given driver.input('#eg01InputId', 'hello world') - When driver.click('input[name=eg01SubmitName]') - Then match driver.text('#eg01DivId') == 'hello world' - And match driver.value('#eg01InputId') == 'hello world' + Given input('#eg01InputId', 'hello world') + When click('input[name=eg01SubmitName]') + Then match text('#eg01DivId') == 'hello world' + And match value('#eg01InputId') == 'hello world' \ No newline at end of file diff --git a/karate-demo/src/test/java/driver/core/test-03.feature b/karate-demo/src/test/java/driver/core/test-03.feature index 3a613c7eb..f1970e804 100644 --- a/karate-demo/src/test/java/driver/core/test-03.feature +++ b/karate-demo/src/test/java/driver/core/test-03.feature @@ -1,10 +1,26 @@ -Feature: scratch pad - -Scenario: - * configure driver = { type: 'chrome', showDriverLog: true } - * def webUrlBase = karate.properties['web.url.base'] - * driver webUrlBase + '/page-03' - * assert driver.eval('1 + 2') == 3 - * match driver.eval("location.href") == webUrlBase + '/page-03' - * def getSubmitFn = function(formId){ return "document.getElementById('" + formId + "').submit()" } - * driver.eval(getSubmitFn('eg02FormId')) +Feature: parallel testing demo - single node using docker + + Background: + # * configure driverTarget = { docker: 'ptrthomas/karate-chrome' } + * configure driver = { type: 'chrome', start: false } + + Scenario: attempt github login + * driver 'https://github.com/login' + * input('#login_field', 'dummy') + * input('#password', 'world') + * submit().click("input[name=commit]") + * match html('#js-flash-container') contains 'Incorrect username or password.' + + Scenario: google search for karate + Given driver 'https://google.com' + And input("input[name=q]", 'karate dsl') + When submit().click("input[name=btnI]") + Then waitForUrl('https://github.com/intuit/karate') + + Scenario: test automation tool challenge + * driver 'https://semantic-ui.com/modules/dropdown.html' + * scroll('select[name=skills]').click() + * click('div[data-value=css]') + * click('div[data-value=html]') + * click('div[data-value=ember]') + * delay(1000).screenshot() diff --git a/karate-demo/src/test/java/driver/core/test-04.feature b/karate-demo/src/test/java/driver/core/test-04.feature new file mode 100644 index 000000000..929472eb0 --- /dev/null +++ b/karate-demo/src/test/java/driver/core/test-04.feature @@ -0,0 +1,20 @@ +Feature: scratch pad 2 + +Scenario Outline: + * def webUrlBase = karate.properties['web.url.base'] + * configure driver = { type: '#(type)', showDriverLog: true } + + Given driver webUrlBase + '/page-01' + + # key events and key combinations + And input('#eg02InputId', Key.CONTROL + 'a') + And def temp = text('#eg02DivId') + And match temp contains '17d' + And match temp contains '65u' + +Examples: +| type | +#| chrome | +| chromedriver | +#| geckodriver | +#| safaridriver | \ No newline at end of file diff --git a/karate-demo/src/test/java/driver/demo/Demo01JavaRunner.java b/karate-demo/src/test/java/driver/demo/Demo01JavaRunner.java index 99ca85a39..76cdc9337 100644 --- a/karate-demo/src/test/java/driver/demo/Demo01JavaRunner.java +++ b/karate-demo/src/test/java/driver/demo/Demo01JavaRunner.java @@ -1,7 +1,8 @@ package driver.demo; -import com.intuit.karate.driver.Driver; +import com.intuit.karate.FileUtils; import com.intuit.karate.driver.chrome.Chrome; +import java.io.File; import org.junit.Test; import static org.junit.Assert.*; import org.slf4j.Logger; @@ -17,17 +18,21 @@ public class Demo01JavaRunner { @Test public void testChrome() throws Exception { - Driver driver = Chrome.start(); - driver.setLocation("https://github.com/login"); - driver.input("#login_field", "hello"); + + Chrome driver = Chrome.start(); + driver.setUrl("https://github.com/login"); + driver.input("#login_field", "dummy"); driver.input("#password", "world"); - driver.submit("input[name=commit]"); + driver.submit().click("input[name=commit]"); String html = driver.html("#js-flash-container"); assertTrue(html.contains("Incorrect username or password.")); - driver.setLocation("https://google.com"); + driver.setUrl("https://google.com"); driver.input("input[name=q]", "karate dsl"); - driver.submit("input[name=btnI]"); - assertEquals("https://github.com/intuit/karate", driver.getLocation()); + driver.submit().click("input[name=btnI]"); + assertEquals("https://github.com/intuit/karate", driver.getUrl()); + byte[] bytes = driver.screenshot(); + // byte[] bytes = driver.screenshotFull(); + FileUtils.writeToFile(new File("target/screenshot.png"), bytes); driver.quit(); } diff --git a/karate-demo/src/test/java/driver/demo/Demo01UiRunner.java b/karate-demo/src/test/java/driver/demo/Demo01UiRunner.java deleted file mode 100644 index 2e8474310..000000000 --- a/karate-demo/src/test/java/driver/demo/Demo01UiRunner.java +++ /dev/null @@ -1,17 +0,0 @@ -package driver.demo; - -import com.intuit.karate.ui.App; -import org.junit.Test; - -/** - * - * @author pthomas3 - */ -public class Demo01UiRunner { - - @Test - public void testApp() { - App.run("src/test/java/driver/demo/demo-01.feature", "mock"); - } - -} diff --git a/karate-demo/src/test/java/driver/demo/Demo02Runner.java b/karate-demo/src/test/java/driver/demo/Demo02Runner.java new file mode 100644 index 000000000..78dbbe276 --- /dev/null +++ b/karate-demo/src/test/java/driver/demo/Demo02Runner.java @@ -0,0 +1,17 @@ +package driver.demo; + +import com.intuit.karate.KarateOptions; +import com.intuit.karate.junit4.Karate; +import org.junit.BeforeClass; +import org.junit.runner.RunWith; + +@RunWith(Karate.class) +@KarateOptions(features = "classpath:driver/demo/demo-02.feature") +public class Demo02Runner { + + @BeforeClass + public static void beforeClass() { + System.setProperty("karate.env", "mock"); + } + +} \ No newline at end of file diff --git a/karate-demo/src/test/java/driver/demo/Demo03ParallelRunner.java b/karate-demo/src/test/java/driver/demo/Demo03ParallelRunner.java new file mode 100644 index 000000000..028c9ac29 --- /dev/null +++ b/karate-demo/src/test/java/driver/demo/Demo03ParallelRunner.java @@ -0,0 +1,25 @@ +package driver.demo; + +import com.intuit.karate.Results; +import com.intuit.karate.Runner; +import demo.DemoTestParallel; +import org.junit.BeforeClass; +import org.junit.Test; + +import static org.junit.Assert.assertTrue; + +public class Demo03ParallelRunner { + + @BeforeClass + public static void beforeClass() { + System.setProperty("karate.env", "mock"); + } + + @Test + public void testParallel() { + Results results = Runner.path("classpath:driver/demo/demo-03.feature").reportDir("target/driver-demo").parallel(5); + DemoTestParallel.generateReport(results.getReportDir()); + assertTrue(results.getErrorMessages(), results.getFailCount() == 0); + } + +} \ No newline at end of file diff --git a/karate-demo/src/test/java/driver/demo/demo-01.feature b/karate-demo/src/test/java/driver/demo/demo-01.feature index 420b2aa08..be4cf1d79 100644 --- a/karate-demo/src/test/java/driver/demo/demo-01.feature +++ b/karate-demo/src/test/java/driver/demo/demo-01.feature @@ -1,26 +1,24 @@ -Feature: browser automation +Feature: browser automation 1 Background: - # * configure driver = { type: 'chrome' } + # * configure driver = { type: 'chrome', showDriverLog: true } + # * configure driverTarget = { docker: 'justinribeiro/chrome-headless', showDriverLog: true } + # * configure driverTarget = { docker: 'ptrthomas/karate-chrome', showDriverLog: true } # * configure driver = { type: 'chromedriver', showDriverLog: true } - * configure driver = { type: 'geckodriver', showDriverLog: true } - # * configure driver = { type: 'safaridriver' } - # * configure driver = { type: 'mswebdriver' } + # * configure driver = { type: 'geckodriver', showDriverLog: true } + # * configure driver = { type: 'safaridriver', showDriverLog: true } + * configure driver = { type: 'iedriver', showDriverLog: true, httpConfig: { readTimeout: 120000 } } Scenario: try to login to github and then do a google search Given driver 'https://github.com/login' - And driver.input('#login_field', 'hello') - And driver.input('#password', 'world') - When driver.submit("input[name=commit]") - Then match driver.html('#js-flash-container') contains 'Incorrect username or password.' + And input('#login_field', 'dummy') + And input('#password', 'world') + When submit().click("input[name=commit]") + Then match html('#js-flash-container') contains 'Incorrect username or password.' Given driver 'https://google.com' - And driver.input("input[name=q]", 'karate dsl') - When driver.submit("input[name=btnI]") - Then match driver.location == 'https://github.com/intuit/karate' - - * def bytes = driver.screenshot() - * karate.embed(bytes, 'image/png') - \ No newline at end of file + And input("input[name=q]", 'karate dsl') + When submit().click("input[name=btnI]") + Then waitForUrl('https://github.com/intuit/karate') diff --git a/karate-demo/src/test/java/driver/demo/demo-02.feature b/karate-demo/src/test/java/driver/demo/demo-02.feature new file mode 100644 index 000000000..9652e432e --- /dev/null +++ b/karate-demo/src/test/java/driver/demo/demo-02.feature @@ -0,0 +1,28 @@ +Feature: browser automation 2 + + Background: + * configure driver = { type: 'chrome' } + # * configure driverTarget = { docker: 'ptrthomas/karate-chrome', showDriverLog: true } + + Scenario: google search, land on the karate github page, and search for a file + + Given driver 'https://google.com' + And input('input[name=q]', 'karate dsl') + When click('input[name=btnI]') + Then waitForUrl('https://github.com/intuit/karate') + + When click('{a}Find file') + And def searchField = waitFor('input[name=query]') + Then match driver.url == 'https://github.com/intuit/karate/find/master' + + When searchField.input('karate-logo.png') + And def innerText = function(locator){ return scriptAll(locator, '_.innerText') } + And def searchFunction = + """ + function() { + var results = innerText('.js-tree-browser-result-path'); + return results.size() == 2 ? results : null; + } + """ + And def searchResults = waitUntil(searchFunction) + Then match searchResults contains 'karate-core/src/main/resources/karate-logo.png' diff --git a/karate-demo/src/test/java/driver/demo/demo-03.feature b/karate-demo/src/test/java/driver/demo/demo-03.feature new file mode 100644 index 000000000..44e016c80 --- /dev/null +++ b/karate-demo/src/test/java/driver/demo/demo-03.feature @@ -0,0 +1,44 @@ +Feature: 3 scenarios + + Background: + * configure driver = { type: 'chromedriver', showDriverLog: true } + # * configure driverTarget = { docker: 'ptrthomas/karate-chrome', showDriverLog: true } + + Scenario: try to login to github + and then do a google search + + Given driver 'https://github.com/login' + And input('#login_field', 'dummy') + And input('#password', 'world') + When submit().click("input[name=commit]") + Then match html('#js-flash-container') contains 'Incorrect username or password.' + + Given driver 'https://google.com' + And input("input[name=q]", 'karate dsl') + When submit().click("input[name=btnI]") + Then match driver.url == 'https://github.com/intuit/karate' + + Scenario: google search, land on the karate github page, and search for a file + + Given driver 'https://google.com' + And input('input[name=q]', 'karate dsl') + When click('input[name=btnI]') + Then waitForUrl('https://github.com/intuit/karate') + + When click('{a}Find file') + And def searchField = waitFor('input[name=query]') + Then match driver.url == 'https://github.com/intuit/karate/find/master' + + When searchField.input('karate-logo.png') + Then def searchResults = waitForResultCount('.js-tree-browser-result-path', 2, '_.innerText') + Then match searchResults contains 'karate-core/src/main/resources/karate-logo.png' + + Scenario: test-automation challenge + Given driver 'https://semantic-ui.com/modules/dropdown.html' + And def locator = "select[name=skills]" + Then scroll(locator) + And click(locator) + And click('div[data-value=css]') + And click('div[data-value=html]') + And click('div[data-value=ember]') + And delay(1000) \ No newline at end of file diff --git a/karate-demo/src/test/java/driver/screenshot/ChromeFullPageRunner.java b/karate-demo/src/test/java/driver/screenshot/ChromeFullPageRunner.java index cba194a65..f415d1365 100644 --- a/karate-demo/src/test/java/driver/screenshot/ChromeFullPageRunner.java +++ b/karate-demo/src/test/java/driver/screenshot/ChromeFullPageRunner.java @@ -19,7 +19,7 @@ public class ChromeFullPageRunner { @Test public void testChrome() throws Exception { Chrome chrome = Chrome.startHeadless(); - chrome.setLocation("https://github.com/intuit/karate/graphs/contributors"); + chrome.setUrl("https://github.com/intuit/karate/graphs/contributors"); byte[] bytes = chrome.pdf(Collections.EMPTY_MAP); FileUtils.writeToFile(new File("target/fullscreen.pdf"), bytes); bytes = chrome.screenshot(true); diff --git a/karate-demo/src/test/java/driver/screenshot/ChromePdfRunner.java b/karate-demo/src/test/java/driver/screenshot/ChromePdfRunner.java index b78b10ba0..4cfc210d4 100644 --- a/karate-demo/src/test/java/driver/screenshot/ChromePdfRunner.java +++ b/karate-demo/src/test/java/driver/screenshot/ChromePdfRunner.java @@ -13,7 +13,7 @@ public class ChromePdfRunner { public static void main(String[] args) { Chrome chrome = Chrome.startHeadless(); - chrome.setLocation("https://github.com/login"); + chrome.setUrl("https://github.com/login"); byte[] bytes = chrome.pdf(Collections.EMPTY_MAP); FileUtils.writeToFile(new File("target/github.pdf"), bytes); bytes = chrome.screenshot(); diff --git a/karate-demo/src/test/java/driver/windows/calc.feature b/karate-demo/src/test/java/driver/windows/calc.feature index 2600fd10b..1a534b01d 100644 --- a/karate-demo/src/test/java/driver/windows/calc.feature +++ b/karate-demo/src/test/java/driver/windows/calc.feature @@ -1,10 +1,10 @@ Feature: Background: - * configure driver = { type: 'winappdriver' } + * def session = { desiredCapabilities: { app: 'Microsoft.WindowsCalculator_8wekyb3d8bbwe!App' } } Scenario: - Given driver { app: 'Microsoft.WindowsCalculator_8wekyb3d8bbwe!App' } + Given driver { type: 'winappdriver', webDriverSession: '#(session)' } And driver.click('One') And driver.click('Plus') And driver.click('Seven') diff --git a/karate-demo/src/test/java/mock/contract/PaymentServiceMockSslMain.java b/karate-demo/src/test/java/mock/contract/PaymentServiceMockSslMain.java index c41656333..c0d9ca928 100644 --- a/karate-demo/src/test/java/mock/contract/PaymentServiceMockSslMain.java +++ b/karate-demo/src/test/java/mock/contract/PaymentServiceMockSslMain.java @@ -15,7 +15,7 @@ public static void main(String[] args) { File mockFeatureFile = FileUtils.getFileRelativeTo(PaymentServiceMockSslMain.class, "payment-service-mock.feature"); File certFile = new File("src/test/java/mock-cert.pem"); File privateKeyFile = new File("src/test/java/mock-key.pem"); - FeatureServer server = FeatureServer.start(mockFeatureFile, 8443, certFile, privateKeyFile, Collections.singletonMap("queueName", "DEMO.MOCK.8443")); + FeatureServer server = FeatureServer.start(mockFeatureFile, 8443, true, certFile, privateKeyFile, Collections.singletonMap("queueName", "DEMO.MOCK.8443")); server.waitSync(); } diff --git a/karate-demo/src/test/java/mock/proxy/DemoMockProxySslRunner.java b/karate-demo/src/test/java/mock/proxy/DemoMockProxySslRunner.java new file mode 100644 index 000000000..ede2525b6 --- /dev/null +++ b/karate-demo/src/test/java/mock/proxy/DemoMockProxySslRunner.java @@ -0,0 +1,54 @@ +package mock.proxy; + +import com.intuit.karate.FileUtils; +import com.intuit.karate.Match; +import com.intuit.karate.Runner; +import com.intuit.karate.Results; +import com.intuit.karate.netty.FeatureServer; +import com.intuit.karate.KarateOptions; +import demo.TestBase; +import java.io.File; +import java.util.Map; +import org.junit.AfterClass; +import static org.junit.Assert.assertTrue; +import org.junit.BeforeClass; +import org.junit.Test; + +/** + * + * @author pthomas3 + */ +@KarateOptions(tags = "~@ignore", features = { + "classpath:demo/cats", + "classpath:demo/greeting"}) +public class DemoMockProxySslRunner { + + private static FeatureServer server; + private static int demoServerPort; + + @BeforeClass + public static void beforeClass() throws Exception { + demoServerPort = TestBase.startServer(); + Map map = new Match().def("demoServerPort", null).allAsMap(); // don't rewrite url + File file = FileUtils.getFileRelativeTo(DemoMockProxySslRunner.class, "demo-mock-proceed.feature"); + server = FeatureServer.start(file, 0, false, map); + } + + @AfterClass + public static void afterClass() { + server.stop(); + } + + @Test + public void testParallel() { + System.setProperty("karate.env", "proxy"); + System.setProperty("demo.server.port", demoServerPort + ""); + System.setProperty("demo.proxy.port", server.getPort() + ""); + System.setProperty("demo.server.https", "true"); + String karateOutputPath = "target/mock-proxy-ssl"; + Results results = Runner.parallel(getClass(), 1, karateOutputPath); + // DemoMockUtils.generateReport(karateOutputPath); + assertTrue("there are scenario failures", results.getFailCount() == 0); + } + +} diff --git a/karate-demo/src/test/java/test/ServerStart.java b/karate-demo/src/test/java/test/ServerStart.java index 986ab73a7..bb932ad0b 100644 --- a/karate-demo/src/test/java/test/ServerStart.java +++ b/karate-demo/src/test/java/test/ServerStart.java @@ -58,12 +58,7 @@ public void start(String[] args, boolean wait) throws Exception { if (wait) { int stopPort = port + 1; logger.info("will use stop port as {}", stopPort); - monitor = new MonitorThread(stopPort, new Stoppable() { - @Override - public void stop() throws Exception { - context.close(); - } - }); + monitor = new MonitorThread(stopPort, () -> context.close()); monitor.start(); monitor.join(); } diff --git a/karate-docker/karate-chrome/Dockerfile b/karate-docker/karate-chrome/Dockerfile new file mode 100644 index 000000000..e2a76453e --- /dev/null +++ b/karate-docker/karate-chrome/Dockerfile @@ -0,0 +1,48 @@ +FROM maven:3-jdk-8 + +LABEL maintainer="Peter Thomas" +LABEL url="https://github.com/intuit/karate/tree/master/karate-docker/karate-chrome" + +RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \ + && echo "deb http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list \ + && apt-get update \ + && apt-get install -y --no-install-recommends \ + google-chrome-stable + +RUN useradd chrome --shell /bin/bash --create-home \ + && usermod -a -G sudo chrome \ + && echo 'ALL ALL = (ALL) NOPASSWD: ALL' >> /etc/sudoers \ + && echo 'chrome:karate' | chpasswd + +RUN apt-get install -y --no-install-recommends \ + xvfb \ + x11vnc \ + xterm \ + fluxbox \ + wmctrl \ + supervisor \ + socat \ + ffmpeg \ + locales \ + locales-all + +ENV LANG en_US.UTF-8 + +RUN apt-get clean \ + && rm -rf /var/cache/* /var/log/apt/* /var/lib/apt/lists/* /tmp/* \ + && mkdir ~/.vnc \ + && x11vnc -storepasswd karate ~/.vnc/passwd \ + && locale-gen ${LANG} \ + && dpkg-reconfigure --frontend noninteractive locales \ + && update-locale LANG=${LANG} + +COPY supervisord.conf /etc +COPY entrypoint.sh / +RUN chmod +x /entrypoint.sh + +EXPOSE 5900 9222 + +ADD target/karate.jar / +ADD target/repository /root/.m2/repository + +CMD ["/bin/bash", "/entrypoint.sh"] diff --git a/karate-docker/karate-chrome/build.sh b/karate-docker/karate-chrome/build.sh new file mode 100755 index 000000000..4228fbfdb --- /dev/null +++ b/karate-docker/karate-chrome/build.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -x -e +cd ../.. +docker run -it --rm -v "$(pwd)":/karate -w /karate -v "$(pwd)"/karate-docker/karate-chrome/target:/root/.m2 maven:3-jdk-8 bash karate-docker/karate-chrome/install.sh +cd karate-docker/karate-chrome +docker build -t karate-chrome . diff --git a/karate-docker/karate-chrome/entrypoint.sh b/karate-docker/karate-chrome/entrypoint.sh new file mode 100644 index 000000000..6136f8def --- /dev/null +++ b/karate-docker/karate-chrome/entrypoint.sh @@ -0,0 +1,21 @@ +#!/bin/bash +set -x -e +if [ -z "$KARATE_JOBURL" ] + then + export KARATE_OPTIONS="-h" + export KARATE_START="false" + else + export KARATE_START="true" + export KARATE_OPTIONS="-j $KARATE_JOBURL" +fi +if [ -z "$KARATE_SOCAT_START" ] + then + export KARATE_SOCAT_START="false" + export KARATE_CHROME_PORT="9222" + else + export KARATE_SOCAT_START="true" + export KARATE_CHROME_PORT="9223" +fi +[ -z "$KARATE_WIDTH" ] && export KARATE_WIDTH="1280" +[ -z "$KARATE_HEIGHT" ] && export KARATE_HEIGHT="720" +exec /usr/bin/supervisord diff --git a/karate-docker/karate-chrome/install.sh b/karate-docker/karate-chrome/install.sh new file mode 100755 index 000000000..b6954cb3a --- /dev/null +++ b/karate-docker/karate-chrome/install.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -x -e +mvn clean install -DskipTests -P pre-release -Djavacpp.platform=linux-x86_64 +KARATE_VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) +mvn -f karate-netty/pom.xml install -DskipTests -P fatjar +cp karate-netty/target/karate-${KARATE_VERSION}.jar /root/.m2/karate.jar +mvn -f examples/jobserver/pom.xml test-compile exec:java -Dexec.mainClass=common.Main -Dexec.classpathScope=test +mvn -f examples/gatling/pom.xml test diff --git a/karate-docker/karate-chrome/supervisord.conf b/karate-docker/karate-chrome/supervisord.conf new file mode 100644 index 000000000..939b8ee53 --- /dev/null +++ b/karate-docker/karate-chrome/supervisord.conf @@ -0,0 +1,70 @@ +[supervisord] +nodaemon=true + +[unix_http_server] +file=/tmp/supervisor.sock + +[rpcinterface:supervisor] +supervisor.rpcinterface_factory=supervisor.rpcinterface:make_main_rpcinterface + +[supervisorctl] +serverurl=unix:///tmp/supervisor.sock + +[program:xvfb] +command=/usr/bin/Xvfb :1 -screen 0 %(ENV_KARATE_WIDTH)sx%(ENV_KARATE_HEIGHT)sx24 +extension RANDR +autorestart=true +priority=100 + +[program:fluxbox] +environment=DISPLAY=":1" +command=/usr/bin/fluxbox -display :1 +autorestart=true +priority=200 + +[program:x11vnc] +command=/usr/bin/x11vnc -display :1 -usepw -shared -forever -xrandr +autorestart=true +priority=300 + +[program:chrome] +user=chrome +environment=HOME="/home/chrome",USER="chrome",DISPLAY=":1",DBUS_SESSION_BUS_ADDRESS="unix:path=/dev/null" +command=/usr/bin/google-chrome + --user-data-dir=/home/chrome + --no-first-run + --disable-translate + --disable-notifications + --disable-infobars + --disable-gpu + --mute-audio + --dbus-stub + --disable-dev-shm-usage + --enable-logging=stderr + --log-level=0 + --window-position=0,0 + --window-size=%(ENV_KARATE_WIDTH)s,%(ENV_KARATE_HEIGHT)s + --force-device-scale-factor=1 + --remote-debugging-port=%(ENV_KARATE_CHROME_PORT)s +autorestart=true +priority=400 + +[program:socat] +command=/usr/bin/socat tcp-listen:9222,fork tcp:localhost:9223 +autorestart=true +autostart=%(ENV_KARATE_SOCAT_START)s +priority=500 + +[program:ffmpeg] +command=/usr/bin/ffmpeg -y -f x11grab -r 16 -s %(ENV_KARATE_WIDTH)sx%(ENV_KARATE_HEIGHT)s -i :1 -vcodec libx264 -pix_fmt yuv420p -preset fast /tmp/karate.mp4 +autostart=%(ENV_KARATE_SOCAT_START)s +priority=600 + +[program:karate] +command=%(ENV_JAVA_HOME)s/bin/java -jar karate.jar %(ENV_KARATE_OPTIONS)s +autorestart=false +autostart=%(ENV_KARATE_START)s +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +priority=700 diff --git a/karate-gatling/README.md b/karate-gatling/README.md index dcdefdd02..3f5549599 100644 --- a/karate-gatling/README.md +++ b/karate-gatling/README.md @@ -7,6 +7,7 @@ * Leverage Karate's powerful assertion capabilities to check that server responses are as expected under load - which is much harder to do in Gatling and other performance testing tools * API invocation sequences that represent end-user workflows are much easier to express in Karate * [*Anything*](#custom) that can be written in Java can be performance tested ! +* Option to scale out by [distributing a test](#distributed-testing) over multiple hardware nodes or Docker containers ## Demo Video Refer: https://twitter.com/ptrthomas/status/986463717465391104 @@ -24,18 +25,49 @@ Refer: https://twitter.com/ptrthomas/status/986463717465391104 Since the above does *not* include the [`karate-apache` (or `karate-jersey`)]((https://github.com/intuit/karate#maven)) dependency you will need to include that as well. -You will also need the [Gatling Maven Plugin](https://github.com/gatling/gatling-maven-plugin), refer to the below [sample project](https://github.com/ptrthomas/karate-gatling-demo) for how to use this for a typical Karate project where feature files are in `src/test/java`. For convenience we recommend you keep even the Gatling simulation files in the same folder hierarchy, even though they are technically files with a `*.scala` extension. +You will also need the [Gatling Maven Plugin](https://github.com/gatling/gatling-maven-plugin), refer to the [sample project](../examples/gatling) for how to use this for a typical Karate project where feature files are in `src/test/java`. For convenience we recommend you keep even the Gatling simulation files in the same folder hierarchy, even though they are technically files with a `*.scala` extension. -### Gradle +```xml + + io.gatling + gatling-maven-plugin + ${gatling.plugin.version} + + src/test/java + + mock.CatsKarateSimulation + + + + + test + + test + + + + +``` -For those who use [Gradle](https://gradle.org), this sample [`build.gradle`](build.gradle) provides a `gatlingRun` task that executes the Gatling test of the `karate-netty` project - which you can use as a reference. The approach is fairly simple, and does not require the use of any Gradle Gatling plugins. +Because the `` phase is defined, just running `mvn clean test` will work. If you don't want to run Gatling tests as part of the normal Maven "test" lifecycle, you can avoid the `` section and instead manually invoke the Gatling plugin from the command-line. -Most problems when using Karate with Gradle occur when "test-resources" are not configured properly. So make sure that all your `*.js` and `*.feature` files are copied to the "resources" folder - when you build the project. +``` +mvn clean test-compile gatling:test +``` + +And in case you have multiple Gatling simulation files and you want to choose only one to run: -## Sample Project: -Refer: https://github.com/ptrthomas/karate-gatling-demo +``` +mvn clean test-compile gatling:test -Dgatling.simulationClass=mock.CatsKarateSimulation +``` -It is worth calling out that we are perf-testing [Karate test-doubles](https://hackernoon.com/api-consumer-contract-tests-and-test-doubles-with-karate-72c30ea25c18) here ! A truly self-contained demo. +It is worth calling out that in the sample project, we are perf-testing [Karate test-doubles](https://hackernoon.com/api-consumer-contract-tests-and-test-doubles-with-karate-72c30ea25c18) ! A truly self-contained demo. + +### Gradle + +For those who use [Gradle](https://gradle.org), this sample [`build.gradle`](../examples/gatling/build.gradle) provides a `gatlingRun` task that executes the Gatling test of the `karate-netty` project - which you can use as a reference. The approach is fairly simple, and does not require the use of any Gradle Gatling plugins. + +Most problems when using Karate with Gradle occur when "test-resources" are not configured properly. So make sure that all your `*.js` and `*.feature` files are copied to the "resources" folder - when you build the project. ## Limitations As of now the Gatling concept of ["throttle" and related syntax](https://gatling.io/docs/2.3/general/simulation_setup/#simulation-setup-throttling) is not supported. Most teams don't need this, but you can declare "pause" times in Karate, see [`pauseFor()`](#pausefor). @@ -94,10 +126,12 @@ When method get #### `pauseFor()` -You can also set pause times (in milliseconds) per URL pattern *and* HTTP method (`get`, `post` etc.) if needed (see [limitations](#limitations)). +You can also set pause times (in milliseconds) per URL pattern *and* HTTP method (`get`, `post` etc.) if needed (see [limitations](#limitations)). If non-zero, this pause will be applied *before* the invocation of the matching HTTP request. We recommend you set that to `0` for everything unless you really need to artifically limit the requests per second. Note how you can use `Nil` to default to `0` for all HTTP methods for a URL pattern. Make sure you wire up the `protocol` in the Gatling `setUp`. If you use a [`nameResolver`](#nameresolver), even those names can be used in the `pauseFor` lookup (instead of a URL pattern). +Also see how to [`pause()`](#think-time) without blocking threads if you really need to do it *within* a Karate feature, for e.g. to simulate user "think time" - in more detail. + ### `karateFeature()` This declares a whole Karate feature as a "flow". Note how you can have concurrent flows in the same Gatling simulation. @@ -108,6 +142,31 @@ If multiple `Scenario`-s have the tag on them, they will all be executed. The or > The tag does not need to be in the `@key=value` form and you can use the plain "`@foo`" form if you want to. But using the pattern `@name=someName` is arguably more readable when it comes to giving multiple `Scenario`-s meaningful names. +#### Ignore Tags +The above [Tag Selector](#tag-selector) approach is designed for simple cases where you have to pick and run only one `Scenario` out of many. Sometimes you will need the full flexibility of [tag combinations](https://github.com/intuit/karate#tags) and "ignore". The `karateFeature()` method takes an optional (vararg) set of Strings after the first feature-path argument. For example you can do this: + +```scala + val delete = scenario("delete").exec(karateFeature("classpath:mock/cats-delete.feature", "@name=delete")) +``` + +To exclude: + +```scala + val delete = scenario("delete").exec(karateFeature("classpath:mock/cats-delete.feature", "~@ignore")) +``` + +To run scenarios tagged `foo` OR `bar` + +```scala + val delete = scenario("delete").exec(karateFeature("classpath:mock/cats-delete.feature", "@foo,@bar")) +``` + +And to run scenarios tagged `foo` AND `bar` + +```scala + val delete = scenario("delete").exec(karateFeature("classpath:mock/cats-delete.feature", "@foo", "@bar")) +``` + ### Gatling Session The Gatling session attributes and `userId` would be available in a Karate variable under the name-space `__gatling`. So you can refer to the user-id for the thread as follows: @@ -126,7 +185,7 @@ val feeder = Iterator.continually(Map("catName" -> MockUtils.getNextCatName, "so val create = scenario("create").feed(feeder).exec(karateFeature("classpath:mock/cats-create.feature")) ``` -There is some [Java code behind the scenes](https://github.com/ptrthomas/karate-gatling-demo/blob/master/src/test/java/mock/MockUtils.java) that takes care of dispensing a new `catName` every time `getNextCatName()` is invoked: +There is some [Java code behind the scenes](../examples/gatling/src/test/java/mock/MockUtils.java) that takes care of dispensing a new `catName` every time `getNextCatName()` is invoked: ```java private static final AtomicInteger counter = new AtomicInteger(); @@ -148,13 +207,59 @@ And now in the feature file you can do this: * print __gatling.catName ``` -You would typically want your feature file to be usable when not being run via Gatling, so you can use this pattern, since [`karate.get()`](https://github.com/intuit/karate#karate-get) will gracefully return `null` if a variable does not exist or is not defined. +#### `karate.callSingle()` +A common need is to run a routine, typically a sign-in and setting up of an `Authorization` header only *once* - for all `Feature` invocations. Keep in mind that when you use Gatling, what used to be a single `Feature` in "normal" Karate will now be multiplied by the number of users you define. So [`callonce`](https://github.com/intuit/karate#callonce) won't be sufficient anymore. + +> IMPORTANT ! We have seen teams bring down their identity or SSO service because they did not realize that every `Feature` for every virtual-user was making a "sign in" call to get an `Authorization` header. Please use `karate.callSingle()` or Gatling "feeders" properly as explained below. + +You can use [`karate.callSingle()`](https://github.com/intuit/karate#hooks) in these situations and it will work as you expect. Ideally you should use [Feeders](#feeders) since `karate.callSingle()` will lock all threads - which may not play very well with Gatling. But when you want to quickly re-use existing Karate tests as performance tests, this will work nicely. + +Normally `karate.callSingle()` is used within the [`karate-config.js`](https://github.com/intuit/karate#karate-configjs) but it *can* be used at any point within a `Feature` if needed. Keep this in mind if you are trying to modify tests that depend on `callonce`. Also see the next section on how you can conditionally change the logic depending on whether the `Feature` is being run as a Gatling test or not. + +#### Detecting Gatling At Run Time +You would typically want your feature file to be usable when not being run via Gatling, so you can use this pattern, since [`karate.get()`](https://github.com/intuit/karate#karate-get) has an optional second argument to use as a "default" value if the variable does not exist or is `null`. + +```cucumber +* def name = karate.get('__gatling.catName', 'Billie') +``` + +For a full, working, stand-alone example, refer to the [`karate-gatling-demo`](../examples/gatling/src/test/java/mock). + +#### Think Time +Gatling provides a way to [`pause()`](https://gatling.io/docs/current/general/scenario/#scenario-pause) between HTTP requests, to simulate user "think time". But when you have all your requests in a Karate feature file, this can be difficult to simulate - and you may think that adding `java.lang.Thread.sleep()` here and there will do the trick. But no, what a `Thread.sleep()` will do is *block threads* - which is a very bad thing in a load simulation. This will get in the way of Gatling, which is specialized to generate load in a non-blocking fashion. + +For this - the [Gatling session](#gatling-session) mentioned above has a `pause(milliseconds)` function available. And following the pattern to [detect if the feature is being run by Gatling](#detecting-gatling-at-run-time) - you can do this: + +```cucumber +* def sleep = function(ms){ java.lang.Thread.sleep(ms) } +# or function(ms){ } for a no-op ! +* def pause = karate.get('__gatling.pause', sleep) +``` + +And now, whenever you need, you can add a pause between API invocations in a feature file: + +```cucumber +* pause(5000) +``` + +You can see how the `pause()` function can be a no-op when *not* a Gatling test, which is probably what you would do most of the time. You can have your "think-times" apply *only* when running as a load test. + +## `configure localAddress` +> This is implemented only for the `karate-apache` HTTP client. Note that the IP address needs to be [*physically assigned* to the local machine](https://www.blazemeter.com/blog/how-to-send-jmeter-requests-from-different-ips/). + +Gatling has a way to bind the HTTP "protocol" to [use a specific "local address"](https://gatling.io/docs/3.2/http/http_protocol/#local-address), which is useful when you want to use an IP range to avoid triggering rate-limiting on the server under test etc. But since Karate makes the HTTP requests, you can use the [`configure`](https://github.com/intuit/karate#configure) keyword, and this can actually be done *any* time within a Karate script or `*.feature` file. ```cucumber -* def name = karate.get('__gatling') ? __gatling.catName : 'Billie' +* configure localAddress = '123.45.67.89' ``` -For a full, working, stand-alone example, refer to the [`karate-gatling-demo`](https://github.com/ptrthomas/karate-gatling-demo/tree/master/src/test/java/mock). +One easy way to achieve a "round-robin" effect is to write a simple Java static method that will return a random IP out of a pool. See [feeders](#feeders) for example code. Note that you can "conditionally" perform a `configure` by using the JavaScript API on the `karate` object: + +```cucumber +* if (__gatling) karate.configure('localAddress', MyUtil.getIp()) +``` + +Since you can [use Java code](https://github.com/intuit/karate#calling-java), any kind of logic or strategy should be possible, and you can refer to [config or variables](https://github.com/intuit/karate#configuration) if needed. ## Custom You can even include any custom code you write in Java into a performance test, complete with full Gatling reporting. @@ -202,3 +307,6 @@ Scenario: fifty The `karate` object happens to implement the `PerfContext` interface and keeps your code simple. Note how the `myRpc` method has been implemented to accept a `Map` (auto-converted from JSON) and the `PerfContext` as arguments. Like the built-in HTTP support, any test failures are automatically linked to the previous "perf event" captured. + +## Distributed Testing +See wiki: [Distributed Testing](https://github.com/intuit/karate/wiki/Distributed-Testing#gatling) diff --git a/karate-gatling/build.gradle b/karate-gatling/build.gradle deleted file mode 100644 index bb4f8a7d6..000000000 --- a/karate-gatling/build.gradle +++ /dev/null @@ -1,65 +0,0 @@ -buildscript { - ext { - karateVersion = '0.9.4' - } -} - -plugins { - id 'scala' -} - -repositories { - mavenLocal() - jcenter() -} - -dependencies { - implementation 'org.scala-lang:scala-library:2.12.8' - - // remove these dependencies when you use this "build.gradle" file for your own gatling project - compile "com.intuit.karate:karate-apache:${karateVersion}" - compile 'io.gatling.highcharts:gatling-charts-highcharts:3.0.2' - - // add these dependencies when you use this "build.gradle" file for your own gatling project. - // testCompile "com.intuit.karate:karate-apache:${karateVersion}" - // testCompile("com.intuit.karate:karate-gatling:${karateVersion}") - // testCompile 'io.gatling.highcharts:gatling-charts-highcharts:3.0.2' - - testCompile "io.gatling:gatling-app:3.0.2" -} - -test { - // pull karate options into the runtime - systemProperty "karate.options", System.properties.getProperty("karate.options") - - // pull karate env into the runtime - systemProperty "karate.env", System.properties.getProperty("karate.env") - - // ensure tests are always run - outputs.upToDateWhen { false } -} - -sourceSets { - test { - resources { - // "*.feature" files in "src/test/scala" should be treated as resource files - srcDirs = ['src/test/resources', 'src/test/scala'] - } - } -} - -task gatlingRun(type: JavaExec) { - group = 'Web Tests' - description = 'Run Gatling Tests' - - new File("${buildDir}/reports/gatling").mkdirs() - - classpath = sourceSets.test.runtimeClasspath - - main = "io.gatling.app.Gatling" - args = [ - '-s', 'mock.CatsSimulation', - '-rf', "${buildDir}/reports/gatling" - ] - systemProperties System.properties -} \ No newline at end of file diff --git a/karate-gatling/pom.xml b/karate-gatling/pom.xml index 5938f437f..7e118c72c 100644 --- a/karate-gatling/pom.xml +++ b/karate-gatling/pom.xml @@ -5,7 +5,7 @@ com.intuit.karate karate-parent - 1.0.0 + 0.9.5 karate-gatling jar diff --git a/karate-gatling/src/main/scala/com/intuit/karate/gatling/Dummy.java b/karate-gatling/src/main/scala/com/intuit/karate/gatling/Dummy.java deleted file mode 100644 index a9738f665..000000000 --- a/karate-gatling/src/main/scala/com/intuit/karate/gatling/Dummy.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.intuit.karate.gatling; - -public class Dummy { - // just for the sake of javadoc else maven public release fails -} diff --git a/karate-gatling/src/main/scala/com/intuit/karate/gatling/GatlingJobServer.java b/karate-gatling/src/main/scala/com/intuit/karate/gatling/GatlingJobServer.java new file mode 100644 index 000000000..a2ecd41f3 --- /dev/null +++ b/karate-gatling/src/main/scala/com/intuit/karate/gatling/GatlingJobServer.java @@ -0,0 +1,89 @@ +/* + * The MIT License + * + * Copyright 2019 Intuit Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.intuit.karate.gatling; + +import com.intuit.karate.FileUtils; +import com.intuit.karate.core.ExecutionContext; +import com.intuit.karate.core.ScenarioExecutionUnit; +import com.intuit.karate.job.ChunkResult; +import com.intuit.karate.job.JobConfig; +import com.intuit.karate.job.JobServer; +import java.io.File; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * + * @author pthomas3 + */ +public class GatlingJobServer extends JobServer { + + private final Set executors = new HashSet(); + private final Set completed = new HashSet(); + + public GatlingJobServer(JobConfig config) { + super(config, "target/gatling"); + } + + @Override + public void addFeature(ExecutionContext exec, List units, Runnable onComplete) { + // TODO not applicable + } + + @Override + public synchronized ChunkResult getNextChunk(String executorId) { + if (executors.contains(executorId)) { + if (completed.size() >= executors.size()) { + stop(); + } + return null; + } + executors.add(executorId); + ChunkResult chunk = new ChunkResult(null, null); + chunk.setChunkId(executorId); + return chunk; + } + + @Override + public synchronized void handleUpload(File upload, String executorId, String chunkId) { + String karateLog = upload.getPath() + File.separator + "karate.log"; + File karateLogFile = new File(karateLog); + if (karateLogFile.exists()) { + karateLogFile.renameTo(new File(karateLog + ".txt")); + } + String gatlingReportDir = "target" + File.separator + "reports" + File.separator; + File[] dirs = upload.listFiles(); + for (File dir : dirs) { + if (dir.isDirectory()) { + File file = getFirstFileWithExtension(dir, "log"); + if (file != null) { + FileUtils.copy(file, new File(gatlingReportDir + "simulation_" + chunkId + ".log")); + } + } + } + completed.add(executorId); + } + +} diff --git a/karate-ui/src/main/java/com/intuit/karate/ui/FeatureOutlineCell.java b/karate-gatling/src/main/scala/com/intuit/karate/gatling/GatlingMavenJobConfig.java similarity index 52% rename from karate-ui/src/main/java/com/intuit/karate/ui/FeatureOutlineCell.java rename to karate-gatling/src/main/scala/com/intuit/karate/gatling/GatlingMavenJobConfig.java index 87215f69b..1ae12c995 100644 --- a/karate-ui/src/main/java/com/intuit/karate/ui/FeatureOutlineCell.java +++ b/karate-gatling/src/main/scala/com/intuit/karate/gatling/GatlingMavenJobConfig.java @@ -1,7 +1,7 @@ /* * The MIT License * - * Copyright 2018 Intuit Inc. + * Copyright 2019 Intuit Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,39 +21,41 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package com.intuit.karate.ui; +package com.intuit.karate.gatling; -import com.intuit.karate.core.ScenarioExecutionUnit; -import javafx.scene.control.ListCell; -import javafx.scene.control.Tooltip; +import com.intuit.karate.StringUtils; +import com.intuit.karate.job.JobCommand; +import com.intuit.karate.job.JobContext; +import com.intuit.karate.job.MavenJobConfig; +import java.util.Collections; +import java.util.List; /** * * @author pthomas3 */ -public class FeatureOutlineCell extends ListCell { - - private final Tooltip tooltip = new Tooltip(); +public class GatlingMavenJobConfig extends MavenJobConfig { + + private String mainCommand = "mvn gatling:test"; - private static final String STYLE_PASS = "-fx-control-inner-background: #A9F5A9"; - private static final String STYLE_FAIL = "-fx-control-inner-background: #F5A9A9"; + public GatlingMavenJobConfig(int executorCount, String host, int port) { + super(executorCount, host, port); + } + + public void setMainCommand(String mainCommand) { + this.mainCommand = mainCommand; + } @Override - public void updateItem(ScenarioExecutionUnit item, boolean empty) { - super.updateItem(item, empty); - if (empty) { - return; + public List getMainCommands(JobContext chunk) { + String temp = mainCommand; + for (String k : sysPropKeys) { + String v = StringUtils.trimToEmpty(System.getProperty(k)); + if (!v.isEmpty()) { + temp = temp + " -D" + k + "=" + v; + } } - setText(item.scenario.getDisplayMeta() + " " + item.scenario.getName()); - tooltip.setText(item.scenario.getName()); - setTooltip(tooltip); - if (item.result.isFailed()) { - setStyle(STYLE_FAIL); - } else if (!item.result.getStepResults().isEmpty() && item.isExecuted()) { - setStyle(STYLE_PASS); - } else { - setStyle(""); - } - } - + return Collections.singletonList(new JobCommand(temp)); + } + } diff --git a/karate-gatling/src/main/scala/com/intuit/karate/gatling/KarateAction.scala b/karate-gatling/src/main/scala/com/intuit/karate/gatling/KarateAction.scala index b090ac3fc..4b53e600c 100644 --- a/karate-gatling/src/main/scala/com/intuit/karate/gatling/KarateAction.scala +++ b/karate-gatling/src/main/scala/com/intuit/karate/gatling/KarateAction.scala @@ -4,7 +4,9 @@ import java.util.Collections import java.util.function.Consumer import akka.actor.{Actor, ActorRef, ActorSystem, Props} -import com.intuit.karate.Runner +import akka.pattern.ask +import akka.util.Timeout +import com.intuit.karate.{Results, Runner} import com.intuit.karate.core._ import com.intuit.karate.http.HttpRequestBuilder import io.gatling.commons.stats.{KO, OK} @@ -14,6 +16,9 @@ import io.gatling.core.session.Session import io.gatling.core.stats.StatsEngine import scala.collection.JavaConverters._ +import scala.concurrent.Await +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration.{Duration, FiniteDuration, MILLISECONDS} class KarateActor extends Actor { override def receive: Receive = { @@ -21,33 +26,54 @@ class KarateActor extends Actor { m.run() context.stop(self) } + case m: FiniteDuration => { + val waiter = sender() + val task: Runnable = () => waiter ! Nil + context.system.scheduler.scheduleOnce(m, self, task) + } } } -class KarateAction(val name: String, val protocol: KarateProtocol, val system: ActorSystem, val statsEngine: StatsEngine, val clock: Clock, val next: Action) extends ExitableAction { +class KarateAction(val name: String, val tags: Seq[String], val protocol: KarateProtocol, val system: ActorSystem, + val statsEngine: StatsEngine, val clock: Clock, val next: Action) extends ExitableAction { def getActor(): ActorRef = { val actorName = "karate-" + protocol.actorCount.incrementAndGet() system.actorOf(Props[KarateActor], actorName) } + def pause(time: Int) = { + val duration = Duration(time, MILLISECONDS) + implicit val timeout = Timeout(Duration(time + 5000, MILLISECONDS)) + val future = getActor() ? duration + Await.result(future, Duration.Inf) + } + override def execute(session: Session) = { val executionHook = new ExecutionHook { - override def beforeScenario(scenario: Scenario, ctx: ScenarioContext): Boolean = true + override def beforeScenario(scenario: Scenario, ctx: ScenarioContext) = true + + override def afterScenario(result: ScenarioResult, scenarioContext: ScenarioContext) = {} + + override def beforeFeature(feature: Feature, ctx: ExecutionContext) = true + + override def afterFeature(result: FeatureResult, ctx: ExecutionContext) = {} + + override def beforeAll(results: Results) = {} + + override def afterAll(results: Results) = {} + + override def beforeStep(step: Step, ctx: ScenarioContext) = true - override def afterScenario(scenarioResult: ScenarioResult, scenarioContext: ScenarioContext) = {} + override def afterStep(result: StepResult, ctx: ScenarioContext) = {} override def getPerfEventName(req: HttpRequestBuilder, ctx: ScenarioContext): String = { val customName = protocol.nameResolver.apply(req, ctx) val finalName = if (customName != null) customName else protocol.defaultNameResolver.apply(req, ctx) val pauseTime = protocol.pauseFor(finalName, req.getMethod) - if (pauseTime > 0) { - // this is probably bad scala / akka practice - // TODO proper throttling strategy - Thread.sleep(pauseTime) - } + if (pauseTime > 0) pause(pauseTime) return if (customName != null) customName else req.getMethod + " " + finalName } @@ -60,10 +86,12 @@ class KarateAction(val name: String, val protocol: KarateProtocol, val system: A } val asyncSystem: Consumer[Runnable] = r => getActor() ! r + val pauseFunction: Consumer[java.lang.Number] = t => pause(t.intValue()) val asyncNext: Runnable = () => next ! session - val attribs: Object = (session.attributes + ("userId" -> session.userId)).asInstanceOf[Map[String, AnyRef]].asJava + val attribs: Object = (session.attributes + ("userId" -> session.userId) + ("pause" -> pauseFunction)) + .asInstanceOf[Map[String, AnyRef]].asJava val arg = Collections.singletonMap("__gatling", attribs) - Runner.callAsync(name, arg, executionHook, asyncSystem, asyncNext) + Runner.callAsync(name, tags.asJava, arg, executionHook, asyncSystem, asyncNext) } diff --git a/karate-gatling/src/main/scala/com/intuit/karate/gatling/KarateActionBuilder.scala b/karate-gatling/src/main/scala/com/intuit/karate/gatling/KarateActionBuilder.scala index f61702a6c..3fa9ad85b 100644 --- a/karate-gatling/src/main/scala/com/intuit/karate/gatling/KarateActionBuilder.scala +++ b/karate-gatling/src/main/scala/com/intuit/karate/gatling/KarateActionBuilder.scala @@ -4,9 +4,9 @@ import io.gatling.core.action.Action import io.gatling.core.action.builder.ActionBuilder import io.gatling.core.structure.ScenarioContext -class KarateActionBuilder(name: String) extends ActionBuilder { +class KarateActionBuilder(name: String, tags: Seq[String]) extends ActionBuilder { override def build(ctx: ScenarioContext, next: Action): Action = { val karateComponents = ctx.protocolComponentsRegistry.components(KarateProtocol.KarateProtocolKey) - new KarateAction(name, karateComponents.protocol, karateComponents.system, ctx.coreComponents.statsEngine, ctx.coreComponents.clock, next) + new KarateAction(name, tags, karateComponents.protocol, karateComponents.system, ctx.coreComponents.statsEngine, ctx.coreComponents.clock, next) } } diff --git a/karate-gatling/src/main/scala/com/intuit/karate/gatling/KarateProtocol.scala b/karate-gatling/src/main/scala/com/intuit/karate/gatling/KarateProtocol.scala index 09f798263..e07a2576e 100644 --- a/karate-gatling/src/main/scala/com/intuit/karate/gatling/KarateProtocol.scala +++ b/karate-gatling/src/main/scala/com/intuit/karate/gatling/KarateProtocol.scala @@ -5,6 +5,7 @@ import java.util.concurrent.atomic.AtomicInteger import akka.actor.ActorSystem import com.intuit.karate.core.ScenarioContext import com.intuit.karate.http.{HttpRequestBuilder, HttpUtils} +import com.intuit.karate.netty.NettyUtils import io.gatling.core.CoreComponents import io.gatling.core.config.GatlingConfiguration import io.gatling.core.protocol.{Protocol, ProtocolComponents, ProtocolKey} @@ -20,7 +21,7 @@ class KarateProtocol(val uriPatterns: Map[String, Seq[MethodPause]]) extends Pro } val actorCount = new AtomicInteger() val defaultNameResolver = (req: HttpRequestBuilder, ctx: ScenarioContext) => { - val pathPair = HttpUtils.parseUriIntoUrlBaseAndPath(req.getUrlAndPath) + val pathPair = NettyUtils.parseUriIntoUrlBaseAndPath(req.getUrlAndPath) val matchedUri = pathMatches(pathPair.right) if (matchedUri.isDefined) matchedUri.get else pathPair.right } diff --git a/karate-gatling/src/main/scala/com/intuit/karate/gatling/PreDef.scala b/karate-gatling/src/main/scala/com/intuit/karate/gatling/PreDef.scala index f935ac0a5..020d08626 100644 --- a/karate-gatling/src/main/scala/com/intuit/karate/gatling/PreDef.scala +++ b/karate-gatling/src/main/scala/com/intuit/karate/gatling/PreDef.scala @@ -2,6 +2,6 @@ package com.intuit.karate.gatling object PreDef { def karateProtocol(uriPatterns: (String, Seq[MethodPause])*) = new KarateProtocol(uriPatterns.toMap) - def karateFeature(name: String) = new KarateActionBuilder(name) + def karateFeature(name: String, tags: String *) = new KarateActionBuilder(name, tags) def pauseFor(list: (String, Int)*) = list.map(mp => MethodPause(mp._1, mp._2)) } diff --git a/karate-gatling/src/test/scala/mock/CatsSimulation.scala b/karate-gatling/src/test/scala/mock/CatsSimulation.scala index 8bf59d27d..f9cdba306 100644 --- a/karate-gatling/src/test/scala/mock/CatsSimulation.scala +++ b/karate-gatling/src/test/scala/mock/CatsSimulation.scala @@ -10,7 +10,7 @@ class CatsSimulation extends Simulation { val protocol = karateProtocol( "/cats/{id}" -> Nil, - "/cats" -> Nil // pauseFor("get" -> 15, "post" -> 25) + "/cats" -> pauseFor("get" -> 15, "post" -> 25) ) protocol.nameResolver = (req, ctx) => req.getHeader("karate-name") diff --git a/karate-jersey/pom.xml b/karate-jersey/pom.xml index f880f6e65..861d3b519 100755 --- a/karate-jersey/pom.xml +++ b/karate-jersey/pom.xml @@ -5,13 +5,13 @@ com.intuit.karate karate-parent - 1.0.0 + 0.9.5 karate-jersey jar - 2.26 + 2.30 diff --git a/karate-jersey/src/main/java/com/intuit/karate/http/jersey/LoggingInterceptor.java b/karate-jersey/src/main/java/com/intuit/karate/http/jersey/LoggingInterceptor.java index a9d6060e5..2104aa02c 100644 --- a/karate-jersey/src/main/java/com/intuit/karate/http/jersey/LoggingInterceptor.java +++ b/karate-jersey/src/main/java/com/intuit/karate/http/jersey/LoggingInterceptor.java @@ -25,12 +25,14 @@ import com.intuit.karate.FileUtils; import com.intuit.karate.core.ScenarioContext; +import com.intuit.karate.http.HttpLogModifier; import com.intuit.karate.http.HttpRequest; import com.intuit.karate.http.HttpUtils; import com.intuit.karate.http.LoggingFilterOutputStream; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.TreeSet; @@ -49,13 +51,14 @@ public class LoggingInterceptor implements ClientRequestFilter, ClientResponseFilter { private final ScenarioContext context; + private final HttpLogModifier logModifier; + private final AtomicInteger counter = new AtomicInteger(); public LoggingInterceptor(ScenarioContext context) { this.context = context; + logModifier = context.getConfig().getLogModifier(); } - private final AtomicInteger counter = new AtomicInteger(); - private static boolean isPrintable(MediaType mediaType) { if (mediaType == null) { return false; @@ -63,12 +66,28 @@ private static boolean isPrintable(MediaType mediaType) { return HttpUtils.isPrintable(mediaType.toString()); } - private static void logHeaders(StringBuilder sb, int id, char prefix, MultivaluedMap headers, HttpRequest actual) { + private static void logHeaders(HttpLogModifier logModifier, StringBuilder sb, int id, char prefix, MultivaluedMap headers, HttpRequest actual) { Set keys = new TreeSet(headers.keySet()); for (String key : keys) { List entries = headers.get(key); - sb.append(id).append(' ').append(prefix).append(' ') - .append(key).append(": ").append(entries.size() == 1 ? entries.get(0) : entries).append('\n'); + sb.append(id).append(' ').append(prefix).append(' ').append(key).append(": "); + if (entries.size() == 1) { + String entry = entries.get(0); + if (logModifier != null) { + entry = logModifier.header(key, entry); + } + sb.append(entry).append('\n'); + } else { + if (logModifier == null) { + sb.append(entries).append('\n'); + } else { + List list = new ArrayList(entries.size()); + for (String entry : entries) { + list.add(logModifier.header(key, entry)); + } + sb.append(list).append('\n'); + } + } if (actual != null) { actual.putHeader(key, entries); } @@ -88,7 +107,7 @@ public void filter(ClientRequestContext request) throws IOException { } @Override - public void filter(ClientRequestContext request, ClientResponseContext response) throws IOException { + public void filter(ClientRequestContext request, ClientResponseContext response) throws IOException { HttpRequest actual = context.getPrevRequest(); actual.stopTimer(); int id = counter.incrementAndGet(); @@ -98,23 +117,27 @@ public void filter(ClientRequestContext request, ClientResponseContext response) actual.setUri(uri); StringBuilder sb = new StringBuilder(); sb.append("request\n").append(id).append(" > ").append(method).append(' ').append(uri).append('\n'); - logHeaders(sb, id, '>', request.getStringHeaders(), actual); + HttpLogModifier requestModifier = logModifier == null ? null : logModifier.enableForUri(uri) ? logModifier : null; + logHeaders(requestModifier, sb, id, '>', request.getStringHeaders(), actual); LoggingFilterOutputStream out = (LoggingFilterOutputStream) request.getProperty(LoggingFilterOutputStream.KEY); if (out != null) { byte[] bytes = out.getBytes().toByteArray(); - actual.setBody(bytes); + actual.setBody(bytes); String buffer = FileUtils.toString(bytes); if (context.getConfig().isLogPrettyRequest()) { buffer = FileUtils.toPrettyString(buffer); } + if (requestModifier != null) { + buffer = requestModifier.request(uri, buffer); + } sb.append(buffer).append('\n'); - } + } context.logger.debug(sb.toString()); // log request // response sb = new StringBuilder(); sb.append("response time in milliseconds: ").append(actual.getResponseTimeFormatted()).append('\n'); sb.append(id).append(" < ").append(response.getStatus()).append('\n'); - logHeaders(sb, id, '<', response.getHeaders(), null); + logHeaders(requestModifier, sb, id, '<', response.getHeaders(), null); if (response.hasEntity() && isPrintable(response.getMediaType())) { InputStream is = response.getEntityStream(); if (!is.markSupported()) { @@ -124,6 +147,9 @@ public void filter(ClientRequestContext request, ClientResponseContext response) String buffer = FileUtils.toString(is); if (context.getConfig().isLogPrettyResponse()) { buffer = FileUtils.toPrettyString(buffer); + } + if (requestModifier != null) { + buffer = requestModifier.request(uri, buffer); } sb.append(buffer).append('\n'); is.reset(); diff --git a/karate-junit4/pom.xml b/karate-junit4/pom.xml index f4a2050f7..1dfced5fc 100755 --- a/karate-junit4/pom.xml +++ b/karate-junit4/pom.xml @@ -5,7 +5,7 @@ com.intuit.karate karate-parent - 1.0.0 + 0.9.5 karate-junit4 jar @@ -26,19 +26,25 @@ karate-apache ${project.version} test - - - com.intuit.karate - karate-ui - ${project.version} - test - + net.masterthought cucumber-reporting ${cucumber.reporting.version} test - + + + org.springframework.boot + spring-boot-loader + ${spring.boot.version} + test + + + org.springframework + spring-core + ${spring.version} + test + diff --git a/karate-junit4/src/main/java/com/intuit/karate/junit4/FeatureInfo.java b/karate-junit4/src/main/java/com/intuit/karate/junit4/FeatureInfo.java index 924d8d6ed..2369bf9ce 100644 --- a/karate-junit4/src/main/java/com/intuit/karate/junit4/FeatureInfo.java +++ b/karate-junit4/src/main/java/com/intuit/karate/junit4/FeatureInfo.java @@ -24,16 +24,20 @@ package com.intuit.karate.junit4; import com.intuit.karate.CallContext; +import com.intuit.karate.Results; import com.intuit.karate.core.FeatureContext; import com.intuit.karate.core.ExecutionContext; import com.intuit.karate.core.ExecutionHook; import com.intuit.karate.core.Feature; import com.intuit.karate.core.FeatureExecutionUnit; +import com.intuit.karate.core.FeatureResult; import com.intuit.karate.core.PerfEvent; import com.intuit.karate.core.Scenario; import com.intuit.karate.core.ScenarioContext; import com.intuit.karate.core.ScenarioExecutionUnit; import com.intuit.karate.core.ScenarioResult; +import com.intuit.karate.core.Step; +import com.intuit.karate.core.StepResult; import com.intuit.karate.http.HttpRequestBuilder; import org.junit.runner.Description; import org.junit.runner.notification.Failure; @@ -56,23 +60,18 @@ public void setNotifier(RunNotifier notifier) { this.notifier = notifier; } - private static String getFeatureName(Feature feature) { - return "[" + feature.getResource().getFileNameWithoutExtension() + "]"; - } - public static Description getScenarioDescription(Scenario scenario) { - String featureName = getFeatureName(scenario.getFeature()); - return Description.createTestDescription(featureName, scenario.getDisplayMeta() + ' ' + scenario.getName()); + return Description.createTestDescription(scenario.getFeature().getNameForReport(), scenario.getNameForReport()); } public FeatureInfo(Feature feature, String tagSelector) { this.feature = feature; - description = Description.createSuiteDescription(getFeatureName(feature), feature.getResource().getPackageQualifiedName()); + description = Description.createSuiteDescription(feature.getNameForReport(), feature.getResource().getPackageQualifiedName()); FeatureContext featureContext = new FeatureContext(null, feature, tagSelector); CallContext callContext = new CallContext(null, true, this); - exec = new ExecutionContext(System.currentTimeMillis(), featureContext, callContext, null, null, null); + exec = new ExecutionContext(null, System.currentTimeMillis(), featureContext, callContext, null, null, null); unit = new FeatureExecutionUnit(exec); - unit.init(null); + unit.init(); for (ScenarioExecutionUnit u : unit.getScenarioExecutionUnits()) { Description scenarioDescription = getScenarioDescription(u.scenario); description.addChild(scenarioDescription); @@ -92,7 +91,7 @@ public boolean beforeScenario(Scenario scenario, ScenarioContext context) { @Override public void afterScenario(ScenarioResult result, ScenarioContext context) { // if dynamic scenario outline background or a call - if (notifier == null || context.callDepth > 0) { + if (notifier == null || context.callDepth > 0) { return; } Description scenarioDescription = getScenarioDescription(result.getScenario()); @@ -102,9 +101,40 @@ public void afterScenario(ScenarioResult result, ScenarioContext context) { // apparently this method should be always called // even if fireTestFailure was called notifier.fireTestFinished(scenarioDescription); - } + } + + @Override + public boolean beforeFeature(Feature feature, ExecutionContext context) { + return true; + } @Override + public void afterFeature(FeatureResult result, ExecutionContext context) { + + } + + @Override + public void beforeAll(Results results) { + + } + + @Override + public void afterAll(Results results) { + + } + + @Override + public boolean beforeStep(Step step, ScenarioContext context) { + return true; + } + + @Override + public void afterStep(StepResult result, ScenarioContext context) { + + } + + @Override + public String getPerfEventName(HttpRequestBuilder req, ScenarioContext context) { return null; } diff --git a/karate-junit4/src/main/java/com/intuit/karate/junit4/Karate.java b/karate-junit4/src/main/java/com/intuit/karate/junit4/Karate.java index d6bc5824d..3ddf70dcc 100644 --- a/karate-junit4/src/main/java/com/intuit/karate/junit4/Karate.java +++ b/karate-junit4/src/main/java/com/intuit/karate/junit4/Karate.java @@ -30,7 +30,6 @@ import com.intuit.karate.core.Feature; import com.intuit.karate.core.FeatureParser; import com.intuit.karate.core.FeatureResult; -import com.intuit.karate.core.ScenarioResult; import com.intuit.karate.core.Tags; import java.io.File; import java.io.IOException; @@ -40,7 +39,6 @@ import java.util.Map; import org.junit.Test; import org.junit.runner.Description; -import org.junit.runner.notification.Failure; import org.junit.runner.notification.RunNotifier; import org.junit.runners.ParentRunner; import org.junit.runners.model.FrameworkMethod; diff --git a/karate-junit4/src/test/java/com/intuit/karate/debug/DapServerRunner.java b/karate-junit4/src/test/java/com/intuit/karate/debug/DapServerRunner.java new file mode 100644 index 000000000..b67513866 --- /dev/null +++ b/karate-junit4/src/test/java/com/intuit/karate/debug/DapServerRunner.java @@ -0,0 +1,17 @@ +package com.intuit.karate.debug; + +import org.junit.Test; + +/** + * mvn exec:java -Dexec.mainClass="com.intuit.karate.cli.Main" -Dexec.args="-d 4711" -Dexec.classpathScope=test + * @author pthomas3 + */ +public class DapServerRunner { + + @Test + public void testDap() { + DapServer server = new DapServer(4711); + server.waitSync(); + } + +} diff --git a/karate-junit4/src/test/java/com/intuit/karate/job/JobUtilsRunner.java b/karate-junit4/src/test/java/com/intuit/karate/job/JobUtilsRunner.java new file mode 100644 index 000000000..542825364 --- /dev/null +++ b/karate-junit4/src/test/java/com/intuit/karate/job/JobUtilsRunner.java @@ -0,0 +1,24 @@ +package com.intuit.karate.job; + +import com.intuit.karate.shell.Command; +import java.io.File; +import org.junit.Test; +import org.slf4j.LoggerFactory; + +/** + * + * @author pthomas3 + */ +public class JobUtilsRunner { + + private static final org.slf4j.Logger logger = LoggerFactory.getLogger(Command.class); + + @Test + public void testZip() { + File src = new File("target/foo"); + File dest = new File("target/test.zip"); + JobUtils.zip(src, dest); + JobUtils.unzip(dest, new File("target/unzip")); + } + +} diff --git a/karate-junit4/src/test/java/com/intuit/karate/junit4/config/ConfigRunner.java b/karate-junit4/src/test/java/com/intuit/karate/junit4/config/ConfigRunner.java new file mode 100644 index 000000000..082dcb84d --- /dev/null +++ b/karate-junit4/src/test/java/com/intuit/karate/junit4/config/ConfigRunner.java @@ -0,0 +1,21 @@ +package com.intuit.karate.junit4.config; + +import com.intuit.karate.KarateOptions; +import com.intuit.karate.junit4.Karate; +import org.junit.BeforeClass; +import org.junit.runner.RunWith; + +/** + * + * @author pthomas3 + */ +@RunWith(Karate.class) +@KarateOptions(features = "classpath:com/intuit/karate/junit4/config/config-env.feature") +public class ConfigRunner { + + @BeforeClass + public static void beforeClass() { + System.setProperty("karate.env", "confenv"); + } + +} diff --git a/karate-junit4/src/test/java/com/intuit/karate/junit4/demos/UiRunner.java b/karate-junit4/src/test/java/com/intuit/karate/junit4/demos/UiRunner.java deleted file mode 100644 index f48c7427b..000000000 --- a/karate-junit4/src/test/java/com/intuit/karate/junit4/demos/UiRunner.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.intuit.karate.junit4.demos; - -import com.intuit.karate.ui.App; -import org.junit.Test; - -/** - * - * @author pthomas3 - */ -public class UiRunner { - - @Test - public void testUi() { - App.run(null, null); - } - -} diff --git a/karate-junit4/src/test/java/com/intuit/karate/junit4/demos/js-arrays.feature b/karate-junit4/src/test/java/com/intuit/karate/junit4/demos/js-arrays.feature index 2563621ff..54e2f2564 100644 --- a/karate-junit4/src/test/java/com/intuit/karate/junit4/demos/js-arrays.feature +++ b/karate-junit4/src/test/java/com/intuit/karate/junit4/demos/js-arrays.feature @@ -311,6 +311,16 @@ Scenario: comparing 2 payloads * def bar = { baz: 'ban', hello: 'world' } * match foo == bar +Scenario: contains will recurse + * def original = { a: 1, b: 2, c: 3, d: { a: 1, b: 2 } } + * def expected = { a: 1, c: 3, d: { b: 2 } } + * match original contains expected + +Scenario: contains will recurse in reverse ! + * def original = { "a": { "b": { "c": { "d":1, "e":2 } } } } + * def compared = { "a": { "b": { "c": { "d":1, "e":2, "f":3 } } } } + * match original !contains compared + Scenario: js eval * def temperature = { celsius: 100, fahrenheit: 212 } * string expression = 'temperature.celsius' diff --git a/karate-junit4/src/test/java/com/intuit/karate/junit4/demos/outline-dynamic.feature b/karate-junit4/src/test/java/com/intuit/karate/junit4/demos/outline-dynamic.feature index 762e37ece..7fe3e34e2 100644 --- a/karate-junit4/src/test/java/com/intuit/karate/junit4/demos/outline-dynamic.feature +++ b/karate-junit4/src/test/java/com/intuit/karate/junit4/demos/outline-dynamic.feature @@ -1,6 +1,8 @@ Feature: Scenario Outline: name is and age is +# ensure nested function is not lost in dynamic-scenario setup deep-copy +* match myUtils.hello() == 'hello world' * def name = '' * match name == "#? _ == 'Bob' || _ == 'Nyan'" * def title = karate.info.scenarioName diff --git a/karate-junit4/src/test/java/com/intuit/karate/junit4/demos/query2.txt b/karate-junit4/src/test/java/com/intuit/karate/junit4/demos/query2.txt new file mode 100644 index 000000000..1290db8e3 --- /dev/null +++ b/karate-junit4/src/test/java/com/intuit/karate/junit4/demos/query2.txt @@ -0,0 +1,4 @@ +{ +abcd +efgh +} \ No newline at end of file diff --git a/karate-junit4/src/test/java/com/intuit/karate/junit4/demos/scenario-variable-scope.feature b/karate-junit4/src/test/java/com/intuit/karate/junit4/demos/scenario-variable-scope.feature index b0c1b30ba..19e4d8b28 100644 --- a/karate-junit4/src/test/java/com/intuit/karate/junit4/demos/scenario-variable-scope.feature +++ b/karate-junit4/src/test/java/com/intuit/karate/junit4/demos/scenario-variable-scope.feature @@ -17,5 +17,8 @@ Scenario: Scenario: * assert a == 1 * assert typeof b == 'undefined' + # get else default value + * def b = karate.get('b', 42) + * match b == 42 * match c == {} \ No newline at end of file diff --git a/karate-junit4/src/test/java/com/intuit/karate/junit4/demos/schema-like.feature b/karate-junit4/src/test/java/com/intuit/karate/junit4/demos/schema-like.feature index c0caf75e6..7b30583fd 100644 --- a/karate-junit4/src/test/java/com/intuit/karate/junit4/demos/schema-like.feature +++ b/karate-junit4/src/test/java/com/intuit/karate/junit4/demos/schema-like.feature @@ -63,6 +63,9 @@ Then match response == # should be null or an array of strings * match foo == '##[] #string' +# each item of the array should match regex (with backslash involved) +* match foo == '#[] #regex \\w+' + # contains * def actual = [{ a: 1, b: 'x' }, { a: 2, b: 'y' }] diff --git a/karate-junit4/src/test/java/com/intuit/karate/junit4/demos/tags.feature b/karate-junit4/src/test/java/com/intuit/karate/junit4/demos/tags.feature index 5054f1b61..2f37f525b 100644 --- a/karate-junit4/src/test/java/com/intuit/karate/junit4/demos/tags.feature +++ b/karate-junit4/src/test/java/com/intuit/karate/junit4/demos/tags.feature @@ -38,7 +38,7 @@ Scenario: test scenario overrides tag @tagdemo Scenario Outline: examples partitioned by tag * def vals = karate.tagValues - * match vals.region[0] == '' + * match vals.region[0] == expected @region=US Examples: diff --git a/karate-junit4/src/test/java/com/intuit/karate/junit4/demos/type-conv.feature b/karate-junit4/src/test/java/com/intuit/karate/junit4/demos/type-conv.feature index 257d74091..40e237b7e 100644 --- a/karate-junit4/src/test/java/com/intuit/karate/junit4/demos/type-conv.feature +++ b/karate-junit4/src/test/java/com/intuit/karate/junit4/demos/type-conv.feature @@ -13,6 +13,16 @@ Scenario: multi-line text """ * match query == read('query.txt').replaceAll("\r", "") +Scenario: multi-line text with the starting line indented + * text query = + """ + { +abcd +efgh + } + """ + * match query == read('query2.txt').replaceAll("\r", "") + Scenario Outline: multi-line text in a scenario outline * text query = """ @@ -118,6 +128,10 @@ Scenario: json to java map - useful in some situations * def map = karate.toBean(response, 'java.util.LinkedHashMap') * def first = map.keySet().iterator().next() * match first == 'key1' + # short cut for the above + * def map = karate.toMap(response) + * def first = map.keySet().iterator().next() + * match first == 'key1' Scenario: java pojo to json * def className = 'com.intuit.karate.junit4.demos.SimplePojo' diff --git a/karate-junit4/src/test/java/com/intuit/karate/junit4/files/BootJarLoadingTest.java b/karate-junit4/src/test/java/com/intuit/karate/junit4/files/BootJarLoadingTest.java new file mode 100644 index 000000000..d097d7184 --- /dev/null +++ b/karate-junit4/src/test/java/com/intuit/karate/junit4/files/BootJarLoadingTest.java @@ -0,0 +1,118 @@ +package com.intuit.karate.junit4.files; + +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.Arrays; +import java.util.List; + +import com.intuit.karate.Resource; +import com.intuit.karate.Results; +import com.intuit.karate.Runner; +import org.junit.BeforeClass; +import org.junit.Test; +import org.springframework.boot.loader.JarLauncher; +import org.springframework.boot.loader.archive.Archive; +import org.springframework.boot.loader.archive.JarFileArchive; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; + +import static java.util.stream.Collectors.*; +import static org.junit.Assert.*; +import static org.springframework.core.io.support.ResourcePatternResolver.*; + +/** + * Tests on Karate's runner applied on resources bundled in a bootJar. + */ +public class BootJarLoadingTest { + + private static ClassLoader classLoader; + + @BeforeClass + public static void beforeAll() throws Exception { + classLoader = getJarClassLoader(); + } + + // @Test + public void testRunningFromBootJar() { + // mimics how a Spring Boot application sets its class loader into the thread's context + Thread.currentThread().setContextClassLoader(classLoader); + SpringBootResourceLoader springBootResourceLoader = new SpringBootResourceLoader(classLoader, "com/karate/jartest"); + List resources = springBootResourceLoader.asKarateResources(); + assertEquals(6, resources.size()); + Results results = Runner.parallel(resources, 1, "target/surefire-reports"); + assertEquals(6, results.getFeatureCount()); + assertEquals(6, results.getPassCount()); + } + + private static ClassLoader getJarClassLoader() throws Exception { + File jar = new File("../karate-core/src/test/resources/karate-bootjar-test.jar"); + assertTrue(jar.exists()); + return new IntegrationTestJarLauncher(new JarFileArchive(jar)).createClassLoader(); + } + + /** + * Custom {@link JarLauncher} used for retrieving a resource in a bootJar. + */ + static class IntegrationTestJarLauncher extends JarLauncher { + + IntegrationTestJarLauncher(Archive archive) { + super(archive); + } + + ClassLoader createClassLoader() throws Exception { + return createClassLoader(getClassPathArchives()); + } + } + + /** + * Loads feature files using the provided ClassLoader + */ + private static class SpringBootResourceLoader { + + private final org.springframework.core.io.Resource[] resources; + + SpringBootResourceLoader(ClassLoader cl, String packagePath) { + String locationPattern = CLASSPATH_ALL_URL_PREFIX + "**/" + packagePath + "/**/*.feature"; + ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(cl); + try { + resources = resolver.getResources(locationPattern); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + List asKarateResources() { + return Arrays.stream(resources) + .map(SpringBootResourceLoader::toSpringBootResource) + .collect(toList()); + } + + private static SpringBootResource toSpringBootResource(org.springframework.core.io.Resource resource) { + try { + return new SpringBootResource(resource); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } + + /** + * An extension of Karate's {@link Resource} handling a Spring Boot's resource. + */ + private static class SpringBootResource extends Resource { + + private static final String BOOT_INF_CLASS_DIRECTORY = "BOOT-INF/classes!/"; + + SpringBootResource(org.springframework.core.io.Resource resource) throws IOException { + super(resource.getURL()); + } + + private static String getBootClassSubstring(String path) { + // The filePath will always contain a Spring-Boot-specific directory structure here, + // since at this point we always expect to be inside a bundled (a bootJar) Spring Boot application + return path.substring(path.indexOf(BOOT_INF_CLASS_DIRECTORY) + BOOT_INF_CLASS_DIRECTORY.length()); + } + } + +} diff --git a/karate-junit4/src/test/java/com/intuit/karate/junit4/files/JarLoadingTest.java b/karate-junit4/src/test/java/com/intuit/karate/junit4/files/JarLoadingTest.java index 4e140108c..540a3a5cf 100644 --- a/karate-junit4/src/test/java/com/intuit/karate/junit4/files/JarLoadingTest.java +++ b/karate-junit4/src/test/java/com/intuit/karate/junit4/files/JarLoadingTest.java @@ -1,10 +1,13 @@ package com.intuit.karate.junit4.files; +import com.intuit.karate.CallContext; import com.intuit.karate.Resource; import com.intuit.karate.FileUtils; import com.intuit.karate.core.Feature; import com.intuit.karate.core.FeatureParser; import com.intuit.karate.Runner; +import com.intuit.karate.core.FeatureContext; +import com.intuit.karate.core.ScenarioContext; import java.io.File; import java.lang.reflect.Method; import java.net.URL; @@ -31,7 +34,7 @@ public class JarLoadingTest { private static final Logger logger = LoggerFactory.getLogger(JarLoadingTest.class); - private static ClassLoader getJarClassLoader() throws Exception { + private static ClassLoader getJarClassLoader1() throws Exception { File jar = new File("../karate-core/src/test/resources/karate-test.jar"); assertTrue(jar.exists()); return new URLClassLoader(new URL[]{jar.toURI().toURL()}); @@ -39,7 +42,7 @@ private static ClassLoader getJarClassLoader() throws Exception { @Test public void testRunningFromJarFile() throws Exception { - ClassLoader cl = getJarClassLoader(); + ClassLoader cl = getJarClassLoader1(); Class main = cl.loadClass("demo.jar1.Main"); Method meth = main.getMethod("hello"); Object result = meth.invoke(null); @@ -55,6 +58,7 @@ public void testRunningFromJarFile() throws Exception { String relativePath = FileUtils.toRelativeClassPath(path, cl); assertEquals("classpath:demo/jar1/caller.feature", relativePath); Feature feature = FeatureParser.parse(resource); + Thread.currentThread().setContextClassLoader(cl); Map map = Runner.runFeature(feature, null, false); assertEquals(true, map.get("success")); } @@ -63,7 +67,7 @@ public void testRunningFromJarFile() throws Exception { public void testFileUtilsForJarFile() throws Exception { File file = new File("src/test/java/common.feature"); assertTrue(!FileUtils.isJarPath(file.toPath().toUri())); - ClassLoader cl = getJarClassLoader(); + ClassLoader cl = getJarClassLoader1(); Class main = cl.loadClass("demo.jar1.Main"); Path path = FileUtils.getPathContaining(main); assertTrue(FileUtils.isJarPath(path.toUri())); @@ -72,11 +76,32 @@ public void testFileUtilsForJarFile() throws Exception { path = FileUtils.fromRelativeClassPath("classpath:demo/jar1", cl); assertEquals(path.toString(), "/demo/jar1"); } + + private static ClassLoader getJarClassLoader2() throws Exception { + File jar = new File("../karate-core/src/test/resources/karate-test2.jar"); + assertTrue(jar.exists()); + return new URLClassLoader(new URL[]{jar.toURI().toURL()}); + } + + private ScenarioContext getContext() throws Exception { + Path featureDir = FileUtils.getPathContaining(getClass()); + FeatureContext featureContext = FeatureContext.forWorkingDir("dev", featureDir.toFile()); + CallContext callContext = new CallContext(null, true); + return new ScenarioContext(featureContext, callContext, getJarClassLoader2(), null, null); + } + + @Test + public void testClassPathJarResource() throws Exception { + String relativePath = "classpath:example/dependency.feature"; + Resource resource = new Resource(getContext(), relativePath); + String temp = resource.getAsString(); + logger.debug("string: {}", temp); + } @Test public void testUsingKarateBase() throws Exception { String relativePath = "classpath:demo/jar1/caller.feature"; - ClassLoader cl = getJarClassLoader(); + ClassLoader cl = getJarClassLoader1(); ExecutorService executor = Executors.newFixedThreadPool(10); List> list = new ArrayList(); for (int i = 0; i < 10; i++) { diff --git a/karate-junit4/src/test/java/com/intuit/karate/junit4/http/JavaHttpTest.java b/karate-junit4/src/test/java/com/intuit/karate/junit4/http/JavaHttpTest.java index 70263f029..d05621859 100644 --- a/karate-junit4/src/test/java/com/intuit/karate/junit4/http/JavaHttpTest.java +++ b/karate-junit4/src/test/java/com/intuit/karate/junit4/http/JavaHttpTest.java @@ -2,6 +2,7 @@ import com.intuit.karate.FileUtils; import com.intuit.karate.Http; +import com.intuit.karate.LogAppender; import com.intuit.karate.netty.FeatureServer; import java.io.File; import org.junit.AfterClass; @@ -24,10 +25,10 @@ public static void beforeClass() { @Test public void testHttp() { - Http http = Http.forUrl(null, "http://localhost:" + server.getPort()); - http.path("echo").get().response().equals("{ uri: '/echo' }"); + Http http = Http.forUrl(LogAppender.NO_OP, "http://localhost:" + server.getPort()); + http.path("echo").get().body().equals("{ uri: '/echo' }"); String expected = "ws://127.0.0.1:9222/devtools/page/E54102F8004590484CC9FF85E2ECFCD0"; - http.path("chrome").get().response() + http.path("chrome").get().body() .equalsText("#[1]") .jsonPath("get[0] $..webSocketDebuggerUrl") .equalsText(expected); diff --git a/karate-junit4/src/test/java/com/intuit/karate/junit4/selenium/sample.feature b/karate-junit4/src/test/java/com/intuit/karate/junit4/selenium/sample.feature deleted file mode 100644 index bb6554ca3..000000000 --- a/karate-junit4/src/test/java/com/intuit/karate/junit4/selenium/sample.feature +++ /dev/null @@ -1,78 +0,0 @@ -@ignore -Feature: no-name-default-test-suite - url = https://github.com/ - with parameters - session.id, session.url - -Scenario: Test-1-of-Default-Suite-136573f0-2437-48aa-9bfe-457f0c88ee8b -# # TestCommand{id='a84bb8d4-b05a-4817-8cf6-457beaaba7cc', comment='', command='open', target='/', value=''} - -* url 'http://localhost:9515' - -* path 'session' -* request { desiredCapabilities: { browserName: 'Chrome' } } -* method post -* status 200 -* def sessionId = response.sessionId - -* def sessionPath = 'session/' + sessionId - -* path sessionPath, 'url' -* request { url: 'https://google.com' } -* method post -* status 200 - -Given path sessionPath, 'url' -And request {url:'https://github.com/'} -When method POST -Then status 200 -And assert response.status == 0 - -# # TestCommand{id='f4322a13-ab17-4961-b9b7-745dd42a2195', comment='', command='mouseOver', target='//div[@id='dashboard']/div/a', value=''} - -# # TestCommand{id='bed92dae-1ea6-458b-b051-4b3666fa5b80', comment='', command='clickAt', target='name=q', value='93,24'} -Given path sessionPath, 'element' -And request {using:'name', value:'q'} -When method POST -Then status 200 -And assert response.status == 0 -* def webdriverElementId = response.value.ELEMENT -* print 'Element ID is 'webdriverElementId - -Given path sessionPath, 'element', webdriverElementId, 'click' -And request {} -When method POST -Then status 200 -And assert response.status == 0 - -# # TestCommand{id='a2bb0a5e-6967-40e7-8a67-516bfe380dba', comment='', command='store', target='karate', value='searchFor'} - -# # TestCommand{id='641fc6bf-e1e1-4089-a6e7-884f849d0028', comment='', command='type', target='name=q', value='karate'} -Given path sessionPath, 'element' -And request {using:'name', value:'q'} -When method POST -Then status 200 -And assert response.status == 0 -* def webdriverElementId = response.value.ELEMENT -* print 'Element ID is 'webdriverElementId - -Given path sessionPath, 'element', webdriverElementId, 'value' -And request {value:['karate']} -When method POST -Then status 200 -And assert response.status == 0 - -# # TestCommand{id='81dbc283-9531-4005-8bd4-b4d3c95d6c81', comment='', command='sendKeys', target='name=q', value='${KEY_ENTER}'} -Given path sessionPath, 'element' -And request {using:'name', value:'q'} -When method POST -Then status 200 -And assert response.status == 0 -* def webdriverElementId = response.value.ELEMENT -* print 'Element ID is 'webdriverElementId - -Given path sessionPath, 'element', webdriverElementId, 'value' -And request {value:['${KEY_ENTER}']} -When method POST -Then status 200 -And assert response.status == 0 diff --git a/karate-junit4/src/test/java/com/intuit/karate/junit4/syntax/demo-json.json b/karate-junit4/src/test/java/com/intuit/karate/junit4/syntax/demo-json.json index 4c7651c45..ac02475b4 100755 --- a/karate-junit4/src/test/java/com/intuit/karate/junit4/syntax/demo-json.json +++ b/karate-junit4/src/test/java/com/intuit/karate/junit4/syntax/demo-json.json @@ -1 +1 @@ -{ from: 'file' } +{ "from": "file" } diff --git a/karate-junit4/src/test/java/com/intuit/karate/junit4/syntax/syntax.feature b/karate-junit4/src/test/java/com/intuit/karate/junit4/syntax/syntax.feature index 195e880f9..23dec4f6a 100755 --- a/karate-junit4/src/test/java/com/intuit/karate/junit4/syntax/syntax.feature +++ b/karate-junit4/src/test/java/com/intuit/karate/junit4/syntax/syntax.feature @@ -418,6 +418,9 @@ Then match pdf == read('test.pdf') * match each data.foo contains { baz: "#? _ != 'z'" } * def isAbc = function(x) { return x == 'a' || x == 'b' || x == 'c' } * match each data.foo contains { baz: '#? isAbc(_)' } +# this is also possible, see the subtle difference from the above +* def isXabc = function(x) { return x.baz == 'a' || x.baz == 'b' || x.baz == 'c' } +* match each data.foo == '#? isXabc(_)' # match each not contains * match each data.foo !contains { bar: 4 } diff --git a/karate-junit4/src/test/java/com/intuit/karate/mock/MalformedRunner.java b/karate-junit4/src/test/java/com/intuit/karate/mock/MalformedRunner.java new file mode 100644 index 000000000..1cf8c37cf --- /dev/null +++ b/karate-junit4/src/test/java/com/intuit/karate/mock/MalformedRunner.java @@ -0,0 +1,29 @@ +package com.intuit.karate.mock; + +import com.intuit.karate.FileUtils; +import com.intuit.karate.junit4.Karate; +import com.intuit.karate.netty.FeatureServer; +import com.intuit.karate.KarateOptions; +import java.io.File; +import org.junit.BeforeClass; +import org.junit.runner.RunWith; + +/** + * + * @author pthomas3 + */ +@RunWith(Karate.class) +@KarateOptions(features = "classpath:com/intuit/karate/mock/malformed.feature") +public class MalformedRunner { + + private static FeatureServer server; + + @BeforeClass + public static void beforeClass() { + File file = FileUtils.getFileRelativeTo(MockServerTest.class, "_mock.feature"); + server = FeatureServer.start(file, 0, false, null); + int port = server.getPort(); + System.setProperty("karate.server.port", port + ""); + } + +} diff --git a/karate-junit4/src/test/java/com/intuit/karate/mock/_mock.feature b/karate-junit4/src/test/java/com/intuit/karate/mock/_mock.feature index c30930d92..e12f2618a 100644 --- a/karate-junit4/src/test/java/com/intuit/karate/mock/_mock.feature +++ b/karate-junit4/src/test/java/com/intuit/karate/mock/_mock.feature @@ -79,5 +79,17 @@ Scenario: pathMatches('/v1/form') Scenario: pathMatches('/v1/headers') && karate.get('requestHeaders.val[0]') == 'foo' * def response = { val: 'foo' } -Scenario: pathMatches('/v1/headers') && karate.get('requestHeaders.val[0]') == 'bar' - * def response = { val: 'bar' } \ No newline at end of file +Scenario: pathMatches('/v1/headers') && headerContains('val', 'bar') + * def response = { val: 'bar' } + +Scenario: pathMatches('/v1/malformed') + * def response = read('malformed.txt') + +Scenario: pathMatches('/v1/jsonformed') + * def response = { hello: 'world' } + +Scenario: pathMatches('/v1/xmlformed') + * def response = world + +Scenario: pathMatches('/v1/stringformed') + * def response = 'hello world' \ No newline at end of file diff --git a/karate-junit4/src/test/java/com/intuit/karate/mock/malformed.feature b/karate-junit4/src/test/java/com/intuit/karate/mock/malformed.feature new file mode 100644 index 000000000..050e43995 --- /dev/null +++ b/karate-junit4/src/test/java/com/intuit/karate/mock/malformed.feature @@ -0,0 +1,25 @@ +Feature: malformed response json + +Background: +* url mockServerUrl + +Scenario: +Given path 'malformed' +When method get +Then status 200 +And match responseType == 'string' + +Given path 'jsonformed' +When method get +Then status 200 +And match responseType == 'json' + +Given path 'xmlformed' +When method get +Then status 200 +And match responseType == 'xml' + +Given path 'stringformed' +When method get +Then status 200 +And match responseType == 'string' \ No newline at end of file diff --git a/karate-junit4/src/test/java/com/intuit/karate/mock/malformed.txt b/karate-junit4/src/test/java/com/intuit/karate/mock/malformed.txt new file mode 100644 index 000000000..bd42dee72 --- /dev/null +++ b/karate-junit4/src/test/java/com/intuit/karate/mock/malformed.txt @@ -0,0 +1,12 @@ +{ + "errors":[ + { + "errCode":"TestCode1", + "errMsg":"TestCode1 Message" + }x + { + "errCode":"TestCode2", + "errMsg":"TestCode2 Message" + } + ] +} diff --git a/karate-junit4/src/test/java/karate-config-confenv.js b/karate-junit4/src/test/java/karate-config-confenv.js index 7acd1be95..cdabeb44a 100644 --- a/karate-junit4/src/test/java/karate-config-confenv.js +++ b/karate-junit4/src/test/java/karate-config-confenv.js @@ -1,3 +1,5 @@ function fn() { - return { confoverride: 'yes' }; + var temp = { confoverride: 'yes' }; + karate.log('temp:', temp); + return temp; } \ No newline at end of file diff --git a/karate-junit4/src/test/java/karate-config.js b/karate-junit4/src/test/java/karate-config.js index a1cb421b4..eb4748520 100644 --- a/karate-junit4/src/test/java/karate-config.js +++ b/karate-junit4/src/test/java/karate-config.js @@ -15,6 +15,7 @@ function fn() { } config.myObject = read('classpath:test.json'); config.myFunction = read('classpath:test.js'); + config.myUtils = karate.call('classpath:utils.feature'); var port = karate.properties['karate.server.port']; port = port || '8080'; config.mockServerUrl = 'http://localhost:' + port + '/v1/'; diff --git a/karate-junit4/src/test/java/logback-test.xml b/karate-junit4/src/test/java/logback-test.xml index 5b2eba236..29d112d7f 100644 --- a/karate-junit4/src/test/java/logback-test.xml +++ b/karate-junit4/src/test/java/logback-test.xml @@ -15,6 +15,7 @@ + diff --git a/karate-junit4/src/test/java/utils.feature b/karate-junit4/src/test/java/utils.feature new file mode 100644 index 000000000..92c9c4b4a --- /dev/null +++ b/karate-junit4/src/test/java/utils.feature @@ -0,0 +1,5 @@ +@ignore +Feature: + +Scenario: +* def hello = function(){ return 'hello world' } diff --git a/karate-junit5/pom.xml b/karate-junit5/pom.xml index ccb9d73fc..b5dd005ac 100755 --- a/karate-junit5/pom.xml +++ b/karate-junit5/pom.xml @@ -5,7 +5,7 @@ com.intuit.karate karate-parent - 1.0.0 + 0.9.5 karate-junit5 jar diff --git a/karate-junit5/src/main/java/com/intuit/karate/junit5/FeatureNode.java b/karate-junit5/src/main/java/com/intuit/karate/junit5/FeatureNode.java index 2757dd4d8..bbafdb08e 100644 --- a/karate-junit5/src/main/java/com/intuit/karate/junit5/FeatureNode.java +++ b/karate-junit5/src/main/java/com/intuit/karate/junit5/FeatureNode.java @@ -53,9 +53,9 @@ public FeatureNode(Feature feature, String tagSelector) { this.feature = feature; FeatureContext featureContext = new FeatureContext(null, feature, tagSelector); CallContext callContext = new CallContext(null, true); - exec = new ExecutionContext(System.currentTimeMillis(), featureContext, callContext, null, null, null); + exec = new ExecutionContext(null, System.currentTimeMillis(), featureContext, callContext, null, null, null); featureUnit = new FeatureExecutionUnit(exec); - featureUnit.init(null); + featureUnit.init(); List selected = new ArrayList(); for(ScenarioExecutionUnit unit : featureUnit.getScenarioExecutionUnits()) { if (featureUnit.isSelected(unit)) { // tag filtering @@ -76,17 +76,17 @@ public boolean hasNext() { @Override public DynamicTest next() { ScenarioExecutionUnit unit = iterator.next(); - String displayName = unit.scenario.getDisplayMeta() + " " + unit.scenario.getName(); - return DynamicTest.dynamicTest(displayName, () -> { + return DynamicTest.dynamicTest(unit.scenario.getNameForReport(), () -> { featureUnit.run(unit); - if (unit.result.isFailed()) { - Assertions.fail(unit.result.getError().getMessage()); - } - if (unit.isLast()) { + boolean failed = unit.result.isFailed(); + if (unit.isLast() || failed) { featureUnit.stop(); exec.result.printStats(null); Engine.saveResultHtml(FileUtils.getBuildDir() + File.separator + "surefire-reports", exec.result, null); } + if (failed) { + Assertions.fail(unit.result.getError().getMessage()); + } }); } diff --git a/karate-junit5/src/main/java/com/intuit/karate/junit5/Karate.java b/karate-junit5/src/main/java/com/intuit/karate/junit5/Karate.java index 0134d292b..3dde1cb44 100644 --- a/karate-junit5/src/main/java/com/intuit/karate/junit5/Karate.java +++ b/karate-junit5/src/main/java/com/intuit/karate/junit5/Karate.java @@ -37,6 +37,8 @@ import java.util.Arrays; import java.util.Iterator; import java.util.List; + +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DynamicContainer; import org.junit.jupiter.api.DynamicNode; import org.junit.jupiter.api.TestFactory; @@ -53,6 +55,11 @@ public class Karate implements Iterable { private final List tags = new ArrayList(); private final List paths = new ArrayList(); private Class clazz; + + // short cut for new Karate().feature() + public static Karate run(String... paths) { + return new Karate().feature(paths); + } public Karate relativeTo(Class clazz) { this.clazz = clazz; @@ -88,6 +95,9 @@ public Iterator iterator() { DynamicNode node = DynamicContainer.dynamicContainer(testName, featureNode); list.add(node); } + if (list.isEmpty()) { + Assertions.fail("no features or scenarios found: " + options.getFeatures()); + } return list.iterator(); } diff --git a/karate-junit5/src/test/java/karate/SampleTest.java b/karate-junit5/src/test/java/karate/SampleTest.java index 049880e9d..597c1217a 100644 --- a/karate-junit5/src/test/java/karate/SampleTest.java +++ b/karate-junit5/src/test/java/karate/SampleTest.java @@ -6,19 +6,22 @@ class SampleTest { @Karate.Test Karate testSample() { - return new Karate().feature("sample").relativeTo(getClass()); + return Karate.run("sample").relativeTo(getClass()); } @Karate.Test Karate testTags() { - return new Karate().feature("tags").tags("@second").relativeTo(getClass()); + return Karate.run("tags").tags("@second").relativeTo(getClass()); } @Karate.Test Karate testFullPath() { - return new Karate() - .feature("classpath:karate/tags.feature") - .tags("@first"); + return Karate.run("classpath:karate/tags.feature").tags("@first"); } + + @Karate.Test + Karate testAll() { + return Karate.run().relativeTo(getClass()); + } } diff --git a/karate-mock-servlet/README.md b/karate-mock-servlet/README.md index becf0d510..ebac9c02b 100644 --- a/karate-mock-servlet/README.md +++ b/karate-mock-servlet/README.md @@ -9,6 +9,18 @@ This can be a huge time-saver as you don't have to spend time waiting for your a So yes, you can test HTTP web-services with the same ease that you expect from traditional unit-tests. Especially for micro-services - when you combine this approach with Karate's data-driven and data-matching capabilities, you can lean towards having more integration tests without losing any of the benefits of unit-tests. +## Using +### Maven + +```xml + + com.intuit.karate + karate-mock-servlet + ${karate.version} + test + +``` + ## Switching the HTTP Client Karate actually allows you to switch the implementation of the Karate [`HttpClient`](../karate-core/src/main/java/com/intuit/karate/http/HttpClient.java) even *during* a test. For mocking a servlet container, you don't need to implement it from scratch and you just need to over-ride one or two methods of the mock-implementation that Karate provides. @@ -41,7 +53,6 @@ Use the test configuration for this `karate-mock-servlet` project as a reference ## Limitations Most teams would not run into these, but if you do, please [consider contributing](https://github.com/intuit/karate/projects/3#card-22529274) ! -* Servlet filters that may be "default" in "real" spring / boot apps etc will be missing, for e.g. encoding and error handling. Currently we lack a way to add custom filters to the "fake" servlet. * File Upload is not supported. * Other similar edge-cases (such as redirects) are not supported. diff --git a/karate-mock-servlet/pom.xml b/karate-mock-servlet/pom.xml index b5ba939f2..d17f9e909 100644 --- a/karate-mock-servlet/pom.xml +++ b/karate-mock-servlet/pom.xml @@ -5,7 +5,7 @@ com.intuit.karate karate-parent - 1.0.0 + 0.9.5 karate-mock-servlet diff --git a/karate-mock-servlet/src/main/java/com/intuit/karate/mock/servlet/MockMultiPart.java b/karate-mock-servlet/src/main/java/com/intuit/karate/mock/servlet/MockMultiPart.java index 5a5885ac3..b39c3e562 100644 --- a/karate-mock-servlet/src/main/java/com/intuit/karate/mock/servlet/MockMultiPart.java +++ b/karate-mock-servlet/src/main/java/com/intuit/karate/mock/servlet/MockMultiPart.java @@ -110,10 +110,14 @@ public void delete() throws IOException { } - @Override - public String getHeader(String string) { - return headers.get(string); - } + @Override + public String getHeader(String string) { + /** + * support spring boot 2 StandardMultipartHttpServletRequest implementation to + * give CONTENT_DISPOSITION header details. + */ + return headers.getOrDefault(string, headers.get(string.toLowerCase())); + } @Override public Collection getHeaders(String string) { diff --git a/karate-mock-servlet/src/test/java/com/intuit/karate/mock/servlet/test/MockMultiPartTest.java b/karate-mock-servlet/src/test/java/com/intuit/karate/mock/servlet/test/MockMultiPartTest.java new file mode 100644 index 000000000..1a7c46161 --- /dev/null +++ b/karate-mock-servlet/src/test/java/com/intuit/karate/mock/servlet/test/MockMultiPartTest.java @@ -0,0 +1,48 @@ +package com.intuit.karate.mock.servlet.test; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.springframework.http.HttpHeaders; + +import com.intuit.karate.ScriptValue; +import com.intuit.karate.http.MultiPartItem; +import com.intuit.karate.mock.servlet.MockMultiPart; + +/** + * @author nsehgal + * + * Test for different StandardMultipartHttpServletRequest implementation + * in spring. Below test checks both the implementation should return + * the CONTENT_DISPOSITION header details when asked via getHeader(). + * + */ +public class MockMultiPartTest { + + private MockMultiPart mockMultiPart = null; + + private static final String CONTENT_DISPOSITION = "content-disposition"; + + @Before + public void init() { + ScriptValue NULL = new ScriptValue(null); + MultiPartItem item = new MultiPartItem("file", NULL); + item.setContentType("text/csv"); + item.setFilename("test.csv"); + mockMultiPart = new MockMultiPart(item); + } + + @Test + public void testSpring2MultipartHeader() { + String headerValue = mockMultiPart.getHeader(HttpHeaders.CONTENT_DISPOSITION); + Assert.assertNotNull(headerValue); + Assert.assertEquals("form-data; filename=\"test.csv\"; name=\"file\"", headerValue); + } + + @Test + public void testSpring1MultipartHeader() { + String headerValue = mockMultiPart.getHeader(CONTENT_DISPOSITION); + Assert.assertNotNull(headerValue); + Assert.assertEquals("form-data; filename=\"test.csv\"; name=\"file\"", headerValue); + } +} diff --git a/karate-mock-servlet/src/test/java/demo/MockDemoConfig.java b/karate-mock-servlet/src/test/java/demo/MockDemoConfig.java index 40f1b94e6..bbc10500d 100644 --- a/karate-mock-servlet/src/test/java/demo/MockDemoConfig.java +++ b/karate-mock-servlet/src/test/java/demo/MockDemoConfig.java @@ -35,6 +35,8 @@ import com.intuit.karate.demo.controller.SignInController; import com.intuit.karate.demo.controller.SoapController; import com.intuit.karate.demo.controller.UploadController; +import com.intuit.karate.demo.exception.GlobalExceptionHandler; + import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -108,5 +110,10 @@ public SoapController soapController() { public EchoController echoController() { return new EchoController(); } + + @Bean + public GlobalExceptionHandler globalExceptionHandler() { + return new GlobalExceptionHandler(); + } } diff --git a/karate-mock-servlet/src/test/java/demo/MockSpringMvcServlet.java b/karate-mock-servlet/src/test/java/demo/MockSpringMvcServlet.java index 94baf6dd0..9f839e334 100644 --- a/karate-mock-servlet/src/test/java/demo/MockSpringMvcServlet.java +++ b/karate-mock-servlet/src/test/java/demo/MockSpringMvcServlet.java @@ -25,9 +25,12 @@ import com.intuit.karate.http.HttpRequestBuilder; import com.intuit.karate.mock.servlet.MockHttpClient; + import javax.servlet.Servlet; import javax.servlet.ServletConfig; import javax.servlet.ServletContext; + +import org.springframework.boot.autoconfigure.web.WebMvcProperties; import org.springframework.mock.web.MockServletConfig; import org.springframework.mock.web.MockServletContext; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; @@ -41,7 +44,7 @@ public class MockSpringMvcServlet extends MockHttpClient { private final Servlet servlet; private final ServletContext servletContext; - + public MockSpringMvcServlet(Servlet servlet, ServletContext servletContext) { this.servlet = servlet; this.servletContext = servletContext; @@ -56,14 +59,14 @@ protected Servlet getServlet(HttpRequestBuilder request) { protected ServletContext getServletContext() { return servletContext; } - + private static final ServletContext SERVLET_CONTEXT = new MockServletContext(); private static final Servlet SERVLET; - + static { SERVLET = initServlet(); } - + private static Servlet initServlet() { AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); context.register(MockDemoConfig.class); @@ -72,14 +75,33 @@ private static Servlet initServlet() { ServletConfig servletConfig = new MockServletConfig(); try { servlet.init(servletConfig); + customize(servlet); } catch (Exception e) { throw new RuntimeException(e); } return servlet; - } - + } + + /** + * Checks if servlet is Dispatcher servlet implementation and then fetches + * the WebMvcProperties from spring container and configure the dispatcher + * servlet. + * + * @param servlet input servlet implementation + */ + private static void customize(Servlet servlet) { + if (servlet instanceof DispatcherServlet) { + DispatcherServlet dispatcherServlet = (DispatcherServlet) servlet; + WebMvcProperties mvcProperties + = dispatcherServlet.getWebApplicationContext().getBean(WebMvcProperties.class); + dispatcherServlet.setThrowExceptionIfNoHandlerFound(mvcProperties.isThrowExceptionIfNoHandlerFound()); + dispatcherServlet.setDispatchOptionsRequest(mvcProperties.isDispatchOptionsRequest()); + dispatcherServlet.setDispatchTraceRequest(mvcProperties.isDispatchTraceRequest()); + } + } + public static MockSpringMvcServlet getMock() { return new MockSpringMvcServlet(SERVLET, SERVLET_CONTEXT); } - + } diff --git a/karate-netty/README.md b/karate-netty/README.md index 10d84d5c4..32317bdbf 100644 --- a/karate-netty/README.md +++ b/karate-netty/README.md @@ -4,9 +4,12 @@ And [Consumer Driven Contracts](https://martinfowler.com/articles/consumerDriven ### Capabilities * Everything on `localhost` or within your network, no need to worry about your data leaking into the cloud -* Super-easy 'hard-coded' mocks ([example](src/test/java/com/intuit/karate/mock/_mock.feature)) +* Super-easy 'hard-coded' mocks ([example](../karate-junit4/src/test/java/com/intuit/karate/mock/_mock.feature)) * Stateful mocks that can fully simulate CRUD for a micro-service ([example](../karate-demo/src/test/java/mock/proxy/demo-mock.feature)) * Not only JSON but first-class support for XML, plain-text, binary, etc. +* Convert JSON or XML into dynamic responses with ease +* Maintain and read large payloads from the file-system if needed +* Mocks are plain-text files - easily collaborate within or across teams using Git / SCM * Easy HTTP request matching by path, method, headers, body etc. * Use the full power of JavaScript expressions for HTTP request matching * SSL / HTTPS with built-in self-signed certificate @@ -41,9 +44,9 @@ The [Netty](https://netty.io) based capabilities are included when you use `kara We use a simplified example of a Java 'consumer' which makes HTTP calls to a Payment Service (provider) where `GET`, `POST`, `PUT` and `DELETE` have been implemented. The 'provider' implements CRUD for the [`Payment.java`](../karate-demo/src/test/java/mock/contract/Payment.java) 'POJO', and the `POST` (or create) results in a message ([`Shipment.java`](../karate-demo/src/test/java/mock/contract/Shipment.java) as JSON) being placed on a queue, which the consumer is listening to. -[ActiveMQ](http://activemq.apache.org) is being used for the sake of mixing an asynchronous flow into this example, and with the help of some [simple](../karate-demo/src/test/java/mock/contract/QueueUtils.java) [utilities](../karate-demo/src/test/java/mock/contract/QueueConsumer.java), we are able to mix asynchronous messaging into a Karate test *as well as* the test-double. +[ActiveMQ](http://activemq.apache.org) is being used for the sake of mixing an asynchronous flow into this example, and with the help of some [simple](../karate-demo/src/test/java/mock/contract/QueueUtils.java) [utilities](../karate-demo/src/test/java/mock/contract/QueueConsumer.java), we are able to mix asynchronous messaging into a Karate test *as well as* the test-double. Also refer to the documentation on [handling async flows in Karate](https://github.com/intuit/karate#async). -A simpler stand-alone example (without ActiveMQ / messaging) is also available here: [`payment-service`](https://github.com/ptrthomas/payment-service). You should be able to clone and run this project - and compare and contrast this with how other frameworks approach [Consumer Driven Contract](https://www.thoughtworks.com/radar/techniques/consumer-driven-contract-testing) testing. +A simpler stand-alone example (without ActiveMQ / messaging) is also available here: [`examples/consumer-driven-contracts`](../examples/consumer-driven-contracts). This is a stand-alone Maven project for convenience, and you just need to clone or download a ZIP of the Karate source code to get it. You can compare and contrast this example with how other frameworks approach [Consumer Driven Contract](https://www.thoughtworks.com/radar/techniques/consumer-driven-contract-testing) testing. | Key | Source Code | Description | | ------ | ----------- | ----------- | @@ -80,25 +83,12 @@ It is worth calling out *why* Karate on the 'other side of the fence' (*handling If you think about it, all the above are *sufficient* to implement *any* micro-service. Karate's DSL syntax is *focused* on exactly these aspects, thus opening up interesting possibilities. It may be hard to believe that you can spin-up a 'usable' micro-service in minutes with Karate - but do try it and see ! # Standalone JAR -*All* of Karate (core, parallel / HTML reports, the UI and mocks) is available as a single, executable JAR file, which includes even the [`karate-apache`](https://mvnrepository.com/artifact/com.intuit.karate/karate-apache) dependency. This is ideal for handing off to UI / web-dev teams for example, who don't want to mess around with a Java IDE. +*All* of Karate (core API testing, parallel-runner / HTML reports, the debugger-UI, mocks and web / UI automation) is available as a *single*, executable JAR file, which includes even the [`karate-apache`](https://mvnrepository.com/artifact/com.intuit.karate/karate-apache) dependency. This is ideal for handing off to UI / web-dev teams for example, who don't want to mess around with a Java IDE. And there is a [Visual Studio Code plugin](https://marketplace.visualstudio.com/items?itemName=kirkslota.karate-runner) that supports the Karate standalone JAR. The only pre-requisite is the [Java Runtime Environment](http://www.oracle.com/technetwork/java/javase/downloads/index.html). Note that the "lighter" JRE is sufficient, not the JDK / Java Development Kit. At least version 1.8.0_112 or greater is required, and there's a good chance you already have Java installed. Check by typing `java -version` on the command line. ## Quick Start -It will take you only 2 minutes to see Karate's mock-server capabilities in action ! And you can run tests as well. - -> Tip: Rename the file to `karate.jar` to make the commands below easier to type ! - -* Download the latest version of the JAR file from [Bintray](https://dl.bintray.com/ptrthomas/karate/), and it will have the name: `karate-.jar` -* Download this file: [`cats-mock.feature`](../karate-demo/src/test/java/mock/web/cats-mock.feature) (or copy the text) to a local file next to the above JAR file -* In the same directory, start the mock server with the command: - * `java -jar karate.jar -m cats-mock.feature -p 8080` -* To see how this is capable of backing an HTML front-end, download this file: [`cats.html`](../karate-demo/src/test/java/mock/web/cats.html). Open it in a browser and you will be able to `POST` data. Browse to [`http://localhost:8080/cats`](http://localhost:8080/cats) - to see the saved data (state). -* You can also run a "normal" Karate test using the stand-alone JAR. Download this file: [`cats-test.feature`](../karate-demo/src/test/java/mock/web/cats-test.feature) - and run the command (in a separate console / terminal): - * `java -jar karate.jar cats-test.feature` -* You will see HTML reports in the `target/cucumber-html-reports` directory - -Another (possibly simpler) version of the above example is included in this demo project: [`karate-sikulix-demo`](https://github.com/ptrthomas/karate-sikulix-demo) - and you can skip the step of downloading the "sikulix" JAR. This project is quite handy if you need to demo Karate (tests, mocks and UI) to others ! +Just use the [ZIP release](https://github.com/intuit/karate/wiki/ZIP-Release) and follow the insructions under the heading: [API Mocks](https://github.com/intuit/karate/wiki/ZIP-Release#api-mocks). Also try the ["World's Smallest MicroService"](#the-worlds-smallest-microservice-) ! @@ -116,6 +106,8 @@ To start a mock server, the 2 mandatory arguments are the path of the feature fi java -jar karate.jar -m my-mock.feature -p 8080 ``` +Note that this server will be able to act as an HTTPS proxy server if needed. If you need to specify a custom certificate and key combination, see below. + #### SSL For SSL, use the `-s` flag. If you don't provide a certificate and key (see next section), it will automatically create `cert.pem` and `key.pem` in the current working directory, and the next time you re-start the mock server - these will be re-used. This is convenient for web / UI developers because you then need to set the certificate 'exception' only once in the browser. @@ -123,12 +115,21 @@ For SSL, use the `-s` flag. If you don't provide a certificate and key (see next java -jar karate.jar -m my-mock.feature -p 8443 -s ``` -If you have a custom certificate and private-key (in PEM format) you can specify them, perhaps because these are your actual certificates or because they are trusted within your organization: +If you have a custom certificate and private-key (in PEM format) you can specify them, perhaps because these are your actual certificates or because they are trusted within your organization. + +``` +java -jar karate.jar -m my-mock.feature -p 8443 -s -c my-cert.crt -k my-key.key +``` + +If you *don't* enable SSL, the proxy server will still be able to tunnel HTTPS traffic - and will use the certificate / key combination you specify or auto-create `cert.pem` and `key.pem` as described above. ``` -java -jar karate.jar -m my-mock.feature -p 8443 -c my-cert.crt -k my-key.key +java -jar karate.jar -m my-mock.feature -p 8090 -c my-cert.crt -k my-key.key ``` +#### Hot Reload +You can hot-reload a mock feature file for changes by adding the `-w` or `--watch` option. + ### Running Tests Convenient to run standard [Karate](https://github.com/intuit/karate) tests on the command-line without needing to mess around with Java or the IDE ! Great for demos or exploratory testing. Even HTML reports are generated ! @@ -181,7 +182,19 @@ java -jar karate.jar -e e2e my-test.feature If [`karate-config.js`](https://github.com/intuit/karate#configuration) exists in the current working directory, it will be used. You can specify a full path by setting the system property `karate.config.dir`. Note that this is an easy way to set a bunch of variables, just return a JSON with the keys and values you need. ``` -java -jar -Dkarate.config.dir=parentdir/somedir karate.jar my-test.feature +java -Dkarate.config.dir=parentdir/somedir -jar karate.jar my-test.feature +``` + +If you want to pass any custom or environment variables, make sure they are *before* the `-jar` part else they will not be passed to the JVM. For example: + +```cucumber +java -Dfoo=bar -Dbaz=ban -jar karate.jar my-test.feature +``` + +And now you can get the value of `foo` from JavaScript or a [Karate expression](https://github.com/intuit/karate#karate-expressions) as follows: + +```javascript +var foo = karate.properties['foo'] ``` #### Parallel Execution @@ -198,17 +211,17 @@ The output directory where the `karate.log` file, JUnit XML and Cucumber report java -jar karate.jar -T 5 -t ~@ignore -o /my/custom/dir src/features ``` -#### UI -The 'default' command actually brings up the [Karate UI](https://github.com/intuit/karate/wiki/Karate-UI). So you can 'double-click' on the JAR or use this on the command-line: -``` -java -jar karate.jar -``` +#### Clean +The [output directory](#output-directory) will be deleted before the test runs if you use the `-C` or `--clean` option. -You can also open an existing Karate test in the UI via the command-line: ``` -java -jar karate.jar -u my-test.feature +java -jar karate.jar -T 5 -t ~@ignore -C src/features ``` +#### Debug Server +The `-d` or `--debug` option will start a debug server. See the [Debug Server wiki](https://github.com/intuit/karate/wiki/Debug-Server#standalone-jar) for more details. + + ## Logging A default [logback configuration file](https://logback.qos.ch/manual/configuration.html) (named [`logback-netty.xml`](src/main/resources/logback-netty.xml)) is present within the stand-alone JAR. If you need to customize logging, set the system property `logback.configurationFile` to point to your custom config: ``` @@ -260,6 +273,13 @@ And a `FeatureServer` instance has a `stop()` method that will [stop](#stopping) You can look at this demo example for reference: [ConsumerUsingMockTest.java](../karate-demo/src/test/java/mock/contract/ConsumerUsingMockTest.java) - note how the dynamic port number can be retrieved and passed to other elements in your test set-up. +## Continuous Integration +To include mocks into a test-suite that consists mostly of Karate tests, the easiest way is to use JUnit with the above approach, and ensure that the JUnit class is "included" in your test run. One way is to ensure that the JUnit "runner" follows the naming convention (`*Test.java`) or you can explicity include the mock "runners" in your Maven setup. + +You will also need to ensure that your mock feature is *not* picked up by the regular test-runners, and an `@ignore` [tag](https://github.com/intuit/karate#tags) typically does the job. + +For more details, refer to this [answer on Stack Overflow](https://stackoverflow.com/a/57746457/143475). + ## Within a Karate Test Teams that are using the [standalone JAR](#standalone-jar) and *don't* want to use Java at all can directly start a mock from within a Karate test script using the `karate.start()` API. The argument can be a string or JSON. If a string, it is processed as the path to the mock feature file, and behaves like the [`read()`](https://github.com/intuit/karate#reading-files) function. @@ -426,7 +446,7 @@ Scenario: pathMatches('/v1/headers') && headerContains('val', 'foo') ``` ## `bodyPath()` -A very powerful helper function that can run JsonPath or XPath expressions agains the request body or payload. +A very powerful helper function that can run JsonPath or XPath expressions against the request body or payload. JSON example: @@ -434,6 +454,12 @@ JSON example: Scenario: pathMatches('/v1/body/json') && bodyPath('$.name') == 'Scooby' ``` +It is worth mentioning that because of Karate's "native" support for JSON, you don't need it most of the time as the below is equivalent to the above. You just use the [`request`](#request) object directly: + +```cucumber +Scenario: pathMatches('/v1/body/json') && request.name == 'Scooby' +``` + XML example: ```cucumber Scenario: pathMatches('/v1/body/xml') && bodyPath('/dog/name') == 'Scooby' @@ -466,6 +492,22 @@ Scenario: pathMatches('/v1/cats') See the [`Background`](#background) example for how the `uuid` function can be defined. +One of the great things about Karate is how easy it is to [read JSON or XML from files](https://github.com/intuit/karate#reading-files). So when you have large complex responses, you can easily do this: + +```cucumber + * def response = read('get-cats-response.json') +``` + +Note that [embedded expressions](https://github.com/intuit/karate#embedded-expressions) work even for content loaded using `read()` ! + +To give you some interesting ideas, say you had a program written in a different language (e.g. Python) and it happened to be invoke-able on the command line. And if it returns a JSON string on the console output - then using the [`karate.exec()`](https://github.com/intuit/karate#karate-exec) API you can actually do this: + +```cucumber + * def response = karate.exec('some os command') +``` + +Because of Karate's [Java interop capabilities](https://github.com/intuit/karate#calling-java) there is no limit to what you can do. Need to call a database and return data ? No problem ! Of course at this point you may need to stop and think if you need to use a *real* app server. But that said, Karate gives you a way to create full fledged micro-services in minutes - far faster than how you would using traditional technologies such as Tomcat, Node / Express, Flask / Django and the like. + ## `responseHeaders` You can easily set multiple headers as JSON in one step as follows: @@ -482,6 +524,8 @@ Background: * configure responseHeaders = { 'Content-Type': 'application/json' } ``` +Note that `Scenario` level [`responseHeaders`](#responseheaders) can over-ride anything set by the "global" `configure responseHeaders`. This is convenient, as you may have a majority of end-points with the same `Content-Type`, and only one or two exceptions such as `text/html` or `text/javascript`. + ## `configure cors` This allows a wide range of browsers or HTTP clients to make requests to a Karate server without running into [CORS issues](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS). And this is perfect for UI / Front-End teams who can even work off an HTML file on the file-system. @@ -535,7 +579,7 @@ Scenario: pathMatches('/v1/abort') # Proxy Mode ## `karate.proceed()` -It is easy to set up a Karate server to "intercept" HTTP requests and then delegate them to a target server only if needed. Think of this as "[AOP](https://en.wikipedia.org/wiki/Aspect-oriented_programming)" for web services ! +It is easy to set up a Karate server to "intercept" HTTP and even HTTPS requests and then delegate them to a target server only if needed. Think of this as "[AOP](https://en.wikipedia.org/wiki/Aspect-oriented_programming)" for web services ! If you invoke the built in Karate function `karate.proceed(url)` - Karate will make an HTTP request to the URL using the current values of the [`request`](#request) and [`requestHeaders`](#requestheaders). Since the [request](#request-handling) is *mutable* this gives rise to some very interesting possibilities. For example, you can modify the request or decide to return a response without calling a downstream service. @@ -545,8 +589,6 @@ Refer to this example: [`payment-service-proxy.feature`](../karate-demo/src/test If not-null, the parameter has to be a URL that starts with `http` or `https`. -> Karate cannot act as an HTTPS proxy yet (do consider contributing !). But most teams are able to configure the "consumer" application to use HTTP and if you set the target URL for e.g. like this: `karate.proceed('https://myhost.com:8080')` Karate will proxy the current request to the server. For example, you can set up Karate to log all requests and responses - which is great for troubleshooting complex service interactions. - After the execution of `karate.proceed()` completes, the values of [`response`](#response) and [`responseHeaders`](#responseheaders) would be ready for returning to the consumer. And you again have the option of mutating the [response](#response-building). So you have control before and after the actual call, and you can modify the request or response - or introduce a time-delay using [`afterScenario`](#afterscenario). @@ -554,6 +596,8 @@ So you have control before and after the actual call, and you can modify the req # Stopping A simple HTTP `GET` to `/__admin/stop` is sufficient to stop a running server gracefully. So you don't need to resort to killing the process, which can lead to issues especially on Windows - such as the port not being released. +> Tip: for stopping HTTPS servers, you can use [curl](https://curl.haxx.se) like this: `curl -k https://localhost:8443/__admin/stop` + If you have started the server programmatically via Java, you can keep a reference to the `FeatureServer` instance and call the `stop()` method. Here is an example: [ConsumerUsingMockTest.java](../karate-demo/src/test/java/mock/contract/ConsumerUsingMockTest.java). # Other Examples diff --git a/karate-netty/pom.xml b/karate-netty/pom.xml index 52511a7e5..10a546fe5 100644 --- a/karate-netty/pom.xml +++ b/karate-netty/pom.xml @@ -5,17 +5,12 @@ com.intuit.karate karate-parent - 1.0.0 + 0.9.5 karate-netty jar - - - com.intuit.karate - karate-ui - ${project.version} - + com.intuit.karate karate-apache @@ -51,7 +46,7 @@ true - + @@ -85,7 +80,24 @@ - + + + maven-assembly-plugin + 2.5.3 + + src/assembly/bin.xml + karate-${project.version} + false + + + + package + + single + + + + diff --git a/karate-netty/src/assembly/bin.xml b/karate-netty/src/assembly/bin.xml new file mode 100644 index 000000000..85ff3443c --- /dev/null +++ b/karate-netty/src/assembly/bin.xml @@ -0,0 +1,26 @@ + + bin + + zip + + + + ../examples/zip-release + + + + + + target/karate-${project.version}.jar + + karate.jar + + + src/test/resources/gitignore.txt + + .gitignore + + + diff --git a/karate-netty/src/main/java/com/intuit/karate/Main.java b/karate-netty/src/main/java/com/intuit/karate/Main.java index 4c4da1e52..0da30d4d0 100644 --- a/karate-netty/src/main/java/com/intuit/karate/Main.java +++ b/karate-netty/src/main/java/com/intuit/karate/Main.java @@ -23,28 +23,28 @@ */ package com.intuit.karate; +import com.intuit.karate.cli.CliExecutionHook; +import com.intuit.karate.debug.DapServer; import com.intuit.karate.exception.KarateException; +import com.intuit.karate.formats.postman.PostmanConverter; +import com.intuit.karate.job.JobExecutor; import com.intuit.karate.netty.FeatureServer; -import com.intuit.karate.netty.NettyUtils; -import com.intuit.karate.ui.App; -import java.io.File; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Date; -import java.util.List; -import java.util.concurrent.Callable; -import java.util.stream.Collectors; +import com.intuit.karate.netty.FileChangedWatcher; import net.masterthought.cucumber.Configuration; import net.masterthought.cucumber.ReportBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import picocli.CommandLine; -import picocli.CommandLine.DefaultExceptionHandler; -import picocli.CommandLine.ExecutionException; import picocli.CommandLine.Option; import picocli.CommandLine.Parameters; -import picocli.CommandLine.ParseResult; -import picocli.CommandLine.RunLast; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.stream.Collectors; /** * @@ -54,8 +54,6 @@ public class Main implements Callable { private static final String DEFAULT_OUTPUT_DIR = "target"; private static final String LOGBACK_CONFIG = "logback.configurationFile"; - private static final String CERT_FILE = "cert.pem"; - private static final String KEY_FILE = "key.pem"; private static Logger logger; @@ -68,14 +66,18 @@ public class Main implements Callable { @Option(names = {"-p", "--port"}, description = "mock server port (required for --mock)") Integer port; + @Option(names = {"-w", "--watch"}, description = "watch (and hot-reload) mock server file for changes") + boolean watch; + @Option(names = {"-s", "--ssl"}, description = "use ssl / https, will use '" - + CERT_FILE + "' and '" + KEY_FILE + "' if they exist in the working directory, or generate them") + + FeatureServer.DEFAULT_CERT_NAME + "' and '" + FeatureServer.DEFAULT_KEY_NAME + + "' if they exist in the working directory, or generate them") boolean ssl; - @Option(names = {"-c", "--cert"}, description = "ssl certificate (default: " + CERT_FILE + ")") + @Option(names = {"-c", "--cert"}, description = "ssl certificate (default: " + FeatureServer.DEFAULT_CERT_NAME + ")") File cert; - @Option(names = {"-k", "--key"}, description = "ssl private key (default: " + KEY_FILE + ")") + @Option(names = {"-k", "--key"}, description = "ssl private key (default: " + FeatureServer.DEFAULT_KEY_NAME + ")") File key; @Option(names = {"-t", "--tags"}, description = "cucumber tags - e.g. '@smoke,~@ignore'") @@ -89,15 +91,25 @@ public class Main implements Callable { @Parameters(description = "one or more tests (features) or search-paths to run") List tests; - + @Option(names = {"-n", "--name"}, description = "scenario name") - String name; + String name; @Option(names = {"-e", "--env"}, description = "value of 'karate.env'") String env; - @Option(names = {"-u", "--ui"}, description = "show user interface") - boolean ui; + @Option(names = {"-C", "--clean"}, description = "clean output directory") + boolean clean; + + @Option(names = {"-d", "--debug"}, arity = "0..1", defaultValue = "-1", fallbackValue = "0", + description = "debug mode (optional port else dynamically chosen)") + int debugPort; + + @Option(names = {"-j", "--jobserver"}, description = "job server url") + String jobServerUrl; + + @Option(names = {"-i", "--import"}, description = "import and convert a file") + String importFile; public static void main(String[] args) { boolean isOutputArg = false; @@ -126,85 +138,86 @@ public static void main(String[] args) { logger = LoggerFactory.getLogger(Main.class); logger.info("Karate version: {}", FileUtils.getKarateVersion()); CommandLine cmd = new CommandLine(new Main()); - DefaultExceptionHandler> exceptionHandler = new DefaultExceptionHandler() { - @Override - public Object handleExecutionException(ExecutionException ex, ParseResult parseResult) { - if (ex.getCause() instanceof KarateException) { - throw new ExecutionException(cmd, ex.getCause().getMessage()); // minimum possible stack trace but exit code 1 - } else { - throw ex; - } - } - }; - cmd.parseWithHandlers(new RunLast(), exceptionHandler, args); - System.exit(0); + int returnCode = cmd.execute(args); + System.exit(returnCode); } @Override public Void call() throws Exception { + if (clean) { + org.apache.commons.io.FileUtils.deleteDirectory(new File(output)); + logger.info("deleted directory: {}", output); + } + if (jobServerUrl != null) { + JobExecutor.run(jobServerUrl); + return null; + } + if (debugPort != -1) { + DapServer server = new DapServer(debugPort); + server.waitSync(); + return null; + } if (tests != null) { - if (ui) { - App.main(new String[]{new File(tests.get(0)).getAbsolutePath(), env}); - } else { - if (env != null) { - System.setProperty(ScriptBindings.KARATE_ENV, env); - } - String configDir = System.getProperty(ScriptBindings.KARATE_CONFIG_DIR); - configDir = StringUtils.trimToNull(configDir); - if (configDir == null) { - System.setProperty(ScriptBindings.KARATE_CONFIG_DIR, new File("").getAbsolutePath()); - } - List fixed = tests.stream().map(f -> new File(f).getAbsolutePath()).collect(Collectors.toList()); - // this avoids mixing json created by other means which will break the cucumber report - String jsonOutputDir = output + File.separator + ScriptBindings.SUREFIRE_REPORTS; - Results results = Runner.parallel(tags, fixed, name, null, threads, jsonOutputDir); - Collection jsonFiles = org.apache.commons.io.FileUtils.listFiles(new File(jsonOutputDir), new String[]{"json"}, true); - List jsonPaths = new ArrayList(jsonFiles.size()); - jsonFiles.forEach(file -> jsonPaths.add(file.getAbsolutePath())); - Configuration config = new Configuration(new File(output), new Date() + ""); - ReportBuilder reportBuilder = new ReportBuilder(jsonPaths, config); - reportBuilder.generateReports(); - if (results.getFailCount() > 0) { - throw new KarateException("there are test failures"); - } + if (env != null) { + System.setProperty(ScriptBindings.KARATE_ENV, env); + } + String configDir = System.getProperty(ScriptBindings.KARATE_CONFIG_DIR); + configDir = StringUtils.trimToNull(configDir); + if (configDir == null) { + System.setProperty(ScriptBindings.KARATE_CONFIG_DIR, new File("").getAbsolutePath()); + } + List fixed = tests.stream().map(f -> new File(f).getAbsolutePath()).collect(Collectors.toList()); + // this avoids mixing json created by other means which will break the cucumber report + String jsonOutputDir = output + File.separator + ScriptBindings.SUREFIRE_REPORTS; + CliExecutionHook hook = new CliExecutionHook(false, jsonOutputDir, false); + Results results = Runner + .path(fixed).tags(tags).scenarioName(name) + .reportDir(jsonOutputDir).hook(hook).parallel(threads); + Collection jsonFiles = org.apache.commons.io.FileUtils.listFiles(new File(jsonOutputDir), new String[]{"json"}, true); + List jsonPaths = new ArrayList(jsonFiles.size()); + jsonFiles.forEach(file -> jsonPaths.add(file.getAbsolutePath())); + Configuration config = new Configuration(new File(output), new Date() + ""); + ReportBuilder reportBuilder = new ReportBuilder(jsonPaths, config); + reportBuilder.generateReports(); + if (results.getFailCount() > 0) { + Exception ke = new KarateException("there are test failures !"); + StackTraceElement[] newTrace = new StackTraceElement[]{ + new StackTraceElement(".", ".", ".", -1) + }; + ke.setStackTrace(newTrace); + throw ke; } return null; - } else if (ui || mock == null) { - App.main(new String[]{}); + } + if (importFile != null) { + new PostmanConverter().convert(importFile, output); return null; } - if (mock != null) { - if (port == null) { - System.err.println("--port required for --mock option"); - CommandLine.usage(this, System.err); - return null; - } + if (clean) { + return null; } - FeatureServer server; - if (cert != null) { - ssl = true; + if (mock == null) { + CommandLine.usage(this, System.err); + return null; } - if (ssl) { - if (cert == null) { - cert = new File(CERT_FILE); - key = new File(KEY_FILE); - } - if (!cert.exists() || !key.exists()) { - logger.warn("ssl requested, but " + CERT_FILE + " and/or " + KEY_FILE + " not found in working directory, will create"); - try { - NettyUtils.createSelfSignedCertificate(cert, key); - } catch (Exception e) { - throw new RuntimeException(e); - } - } else { - logger.info("ssl on, using existing files: {} and {}", CERT_FILE, KEY_FILE); - } - server = FeatureServer.start(mock, port, cert, key, null); - } else { - server = FeatureServer.start(mock, port, false, null); + if (port == null) { + System.err.println("--port required for --mock option"); + CommandLine.usage(this, System.err); + return null; + } + // these files will not be created, unless ssl or ssl proxying happens + // and then they will be lazy-initialized + if (cert == null || key == null) { + cert = new File(FeatureServer.DEFAULT_CERT_NAME); + key = new File(FeatureServer.DEFAULT_KEY_NAME); + } + FeatureServer server = FeatureServer.start(mock, port, ssl, cert, key, null); + if (watch) { + logger.info("--watch enabled, will hot-reload: {}", mock.getName()); + FileChangedWatcher watcher = new FileChangedWatcher(mock, server, port, ssl, cert, key); + watcher.watch(); } server.waitSync(); return null; } - } diff --git a/karate-netty/src/main/resources/log4j2.properties b/karate-netty/src/main/resources/log4j2.properties index d38f25dcd..e189e65b2 100644 --- a/karate-netty/src/main/resources/log4j2.properties +++ b/karate-netty/src/main/resources/log4j2.properties @@ -1,4 +1,4 @@ -# this is only for the net.masterthought.cucumber.ReportBuilder in DemoTestParallel.java +# this is only for the net.masterthought.cucumber.ReportBuilder log4j.rootLogger = INFO, CONSOLE log4j.appender.CONSOLE = org.apache.log4j.ConsoleAppender log4j.appender.CONSOLE.layout = org.apache.log4j.PatternLayout \ No newline at end of file diff --git a/karate-netty/src/test/java/com/intuit/karate/ClientUiRunner.java b/karate-netty/src/test/java/com/intuit/karate/ClientUiRunner.java deleted file mode 100644 index 8385c2a02..000000000 --- a/karate-netty/src/test/java/com/intuit/karate/ClientUiRunner.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.intuit.karate; - -import com.intuit.karate.ui.App; -import org.junit.Test; - -/** - * - * @author pthomas3 - */ -public class ClientUiRunner { - - @Test - public void testApp() { - App.run("src/test/java/com/intuit/karate/client.feature", null); - } - -} diff --git a/karate-netty/src/test/java/com/intuit/karate/FeatureProxyRunner.java b/karate-netty/src/test/java/com/intuit/karate/FeatureProxyRunner.java new file mode 100644 index 000000000..b470f95c0 --- /dev/null +++ b/karate-netty/src/test/java/com/intuit/karate/FeatureProxyRunner.java @@ -0,0 +1,20 @@ +package com.intuit.karate; + +import com.intuit.karate.netty.FeatureServer; +import java.io.File; +import org.junit.Test; + +/** + * + * @author pthomas3 + */ +public class FeatureProxyRunner { + + @Test + public void testServer() { + File file = FileUtils.getFileRelativeTo(FeatureProxyRunner.class, "proxy.feature"); + FeatureServer server = FeatureServer.start(file, 8090, false, null); + server.waitSync(); + } + +} diff --git a/karate-netty/src/test/java/com/intuit/karate/FeatureServerTest.java b/karate-netty/src/test/java/com/intuit/karate/FeatureServerTest.java index a2d8a5c07..ac9638c66 100644 --- a/karate-netty/src/test/java/com/intuit/karate/FeatureServerTest.java +++ b/karate-netty/src/test/java/com/intuit/karate/FeatureServerTest.java @@ -11,7 +11,7 @@ * @author pthomas3 */ public class FeatureServerTest { - + private static FeatureServer server; @BeforeClass @@ -19,14 +19,17 @@ public static void beforeClass() { File file = FileUtils.getFileRelativeTo(FeatureServerTest.class, "server.feature"); server = FeatureServer.start(file, 0, false, null); int port = server.getPort(); - System.setProperty("karate.server.port", port + ""); + System.setProperty("karate.server.port", port + ""); + // needed to ensure we undo what the other test does to the jvm else ci fails + System.setProperty("karate.server.ssl", ""); + System.setProperty("karate.server.proxy", ""); } - + @Test public void testClient() { Runner.runFeature("classpath:com/intuit/karate/client.feature", null, true); } - + @AfterClass public static void afterClass() { server.stop(); diff --git a/karate-netty/src/test/java/com/intuit/karate/ProxyServerRunner.java b/karate-netty/src/test/java/com/intuit/karate/ProxyServerRunner.java new file mode 100644 index 000000000..8d95680ca --- /dev/null +++ b/karate-netty/src/test/java/com/intuit/karate/ProxyServerRunner.java @@ -0,0 +1,18 @@ +package com.intuit.karate; + +import com.intuit.karate.netty.ProxyServer; +import org.junit.Test; + +/** + * + * @author pthomas3 + */ +public class ProxyServerRunner { + + @Test + public void testProxy() { + ProxyServer proxy = new ProxyServer(5000, req -> { System.out.println("*** " + req.uri()); return null; } , null); + proxy.waitSync(); + } + +} diff --git a/karate-netty/src/test/java/com/intuit/karate/ProxyServerSslMain.java b/karate-netty/src/test/java/com/intuit/karate/ProxyServerSslMain.java new file mode 100644 index 000000000..4753c43e7 --- /dev/null +++ b/karate-netty/src/test/java/com/intuit/karate/ProxyServerSslMain.java @@ -0,0 +1,36 @@ +package com.intuit.karate; + +import com.intuit.karate.netty.ProxyServer; +import java.io.File; +import org.junit.Test; +import org.slf4j.LoggerFactory; + +/** + * + * @author pthomas3 + */ +public class ProxyServerSslMain { + + private static final org.slf4j.Logger logger = LoggerFactory.getLogger(ProxyServerSslMain.class); + + private String html = FileUtils.toString(new File("src/test/java/com/intuit/karate/temp.html")); + + @Test + public void testProxy() { + ProxyServer server = new ProxyServer(8090, + req -> { + if ("httpbin.org".equals(req.context.host)) { + return req.fake(200, html).header("Content-Type", "text/html"); + } + return null; + }, + res -> { + if ("corte.si".equals(res.context.host) && res.uri().contains("/index.html")) { + return res.fake(200, html).header("Content-Type", "text/html"); + } + return null; + }); + server.waitSync(); + } + +} diff --git a/karate-netty/src/test/java/com/intuit/karate/ProxyServerSslTest.java b/karate-netty/src/test/java/com/intuit/karate/ProxyServerSslTest.java new file mode 100644 index 000000000..e1991ddc5 --- /dev/null +++ b/karate-netty/src/test/java/com/intuit/karate/ProxyServerSslTest.java @@ -0,0 +1,91 @@ +package com.intuit.karate; + +import com.google.common.base.Charsets; +import com.intuit.karate.http.LenientTrustManager; +import com.intuit.karate.netty.FeatureServer; +import com.intuit.karate.netty.ProxyServer; +import java.io.File; +import java.io.InputStream; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import org.apache.http.HttpEntity; +import org.apache.http.HttpHost; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.junit.AfterClass; +import static org.junit.Assert.*; +import org.junit.BeforeClass; +import org.junit.Test; +import org.slf4j.LoggerFactory; + +/** + * + * @author pthomas3 + */ +public class ProxyServerSslTest { + + private static final org.slf4j.Logger logger = LoggerFactory.getLogger(ProxyServerSslTest.class); + + private static ProxyServer proxy; + private static FeatureServer server; + + @BeforeClass + public static void beforeClass() { + proxy = new ProxyServer(0, null, null); + File file = FileUtils.getFileRelativeTo(ProxyServerSslTest.class, "server.feature"); + server = FeatureServer.start(file, 0, true, null); + int port = server.getPort(); + System.setProperty("karate.server.port", port + ""); + System.setProperty("karate.server.ssl", "true"); + System.setProperty("karate.server.proxy", "http://localhost:" + proxy.getPort()); + } + + @AfterClass + public static void afterClass() { + server.stop(); + proxy.stop(); + } + + @Test + public void testProxy() throws Exception { + String url = "https://localhost:" + server.getPort() + "/v1/cats"; + assertEquals(200, http(get(url))); + assertEquals(200, http(post(url, "{ \"name\": \"Billie\" }"))); + Runner.runFeature("classpath:com/intuit/karate/client.feature", null, true); + } + + private static HttpUriRequest get(String url) { + return new HttpGet(url); + } + + private static HttpUriRequest post(String url, String body) { + HttpPost post = new HttpPost(url); + HttpEntity entity = new StringEntity(body, ContentType.create("application/json", Charsets.UTF_8)); + post.setEntity(entity); + return post; + } + + private int http(HttpUriRequest request) throws Exception { + // System.setProperty("javax.net.debug", "all"); // -Djavax.net.debug=all + SSLContext sc = SSLContext.getInstance("SSL"); + sc.init(null, new TrustManager[]{LenientTrustManager.INSTANCE}, null); + CloseableHttpClient client = HttpClients.custom() + .setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE) + .setSSLContext(sc) + .setProxy(new HttpHost("localhost", proxy.getPort())) + .build(); + HttpResponse response = client.execute(request); + InputStream is = response.getEntity().getContent(); + String responseString = FileUtils.toString(is); + logger.debug("response: {}", responseString); + return response.getStatusLine().getStatusCode(); + } + +} diff --git a/karate-netty/src/test/java/com/intuit/karate/ProxyServerTest.java b/karate-netty/src/test/java/com/intuit/karate/ProxyServerTest.java new file mode 100644 index 000000000..5c09eac6a --- /dev/null +++ b/karate-netty/src/test/java/com/intuit/karate/ProxyServerTest.java @@ -0,0 +1,82 @@ +package com.intuit.karate; + +import com.google.common.base.Charsets; +import com.intuit.karate.netty.FeatureServer; +import com.intuit.karate.netty.ProxyServer; +import java.io.File; +import java.io.InputStream; +import org.apache.http.HttpEntity; +import org.apache.http.HttpHost; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.junit.AfterClass; +import static org.junit.Assert.*; +import org.junit.BeforeClass; +import org.junit.Test; +import org.slf4j.LoggerFactory; + +/** + * + * @author pthomas3 + */ +public class ProxyServerTest { + + private static final org.slf4j.Logger logger = LoggerFactory.getLogger(ProxyServerTest.class); + + private static ProxyServer proxy; + private static FeatureServer server; + + @BeforeClass + public static void beforeClass() { + proxy = new ProxyServer(0, null, null); + File file = FileUtils.getFileRelativeTo(ProxyServerTest.class, "server.feature"); + server = FeatureServer.start(file, 0, false, null); + int port = server.getPort(); + System.setProperty("karate.server.port", port + ""); + System.setProperty("karate.server.ssl", ""); // for ci + System.setProperty("karate.server.proxy", "http://localhost:" + proxy.getPort()); + } + + @AfterClass + public static void afterClass() { + server.stop(); + proxy.stop(); + } + + @Test + public void testProxy() throws Exception { + String url = "http://localhost:" + server.getPort() + "/v1/cats"; + assertEquals(200, http(get(url))); + assertEquals(200, http(post(url, "{ \"name\": \"Billie\" }"))); + Runner.runFeature("classpath:com/intuit/karate/client.feature", null, true); + } + + private static HttpUriRequest get(String url) { + return new HttpGet(url); + } + + private static HttpUriRequest post(String url, String body) { + HttpPost post = new HttpPost(url); + HttpEntity entity = new StringEntity(body, ContentType.create("application/json", Charsets.UTF_8)); + post.setEntity(entity); + return post; + } + + private static int http(HttpUriRequest request) throws Exception { + CloseableHttpClient client = HttpClients.custom() + .setProxy(new HttpHost("localhost", proxy.getPort())) + .build(); + HttpResponse response = client.execute(request); + InputStream is = response.getEntity().getContent(); + String responseString = FileUtils.toString(is); + logger.debug("response: {}", responseString); + return response.getStatusLine().getStatusCode(); + } + +} diff --git a/karate-netty/src/test/java/com/intuit/karate/proxy.feature b/karate-netty/src/test/java/com/intuit/karate/proxy.feature new file mode 100644 index 000000000..c0af1f4e9 --- /dev/null +++ b/karate-netty/src/test/java/com/intuit/karate/proxy.feature @@ -0,0 +1,7 @@ +@ignore +Feature: + +Scenario: +* print '>>', request +* karate.proceed() +* print '<<', response diff --git a/karate-netty/src/test/java/com/intuit/karate/server.feature b/karate-netty/src/test/java/com/intuit/karate/server.feature index 4ffa42228..c002fd868 100644 --- a/karate-netty/src/test/java/com/intuit/karate/server.feature +++ b/karate-netty/src/test/java/com/intuit/karate/server.feature @@ -9,7 +9,7 @@ Background: Scenario: pathMatches('/v1/cats') && methodIs('post') * def cat = request * def id = ~~(id + 1) - * set cat.id = id + * cat.id = id * cats[id + ''] = cat * def response = cat @@ -31,3 +31,8 @@ Scenario: pathMatches('/v1/abort') * if (response.success) karate.abort() # the next line will not be executed * def response = { success: false } + +Scenario: + * def responseStatus = 404 + * def responseHeaders = { 'Content-Type': 'text/html; charset=utf-8' } + * def response = Not Found diff --git a/karate-netty/src/test/java/com/intuit/karate/temp.html b/karate-netty/src/test/java/com/intuit/karate/temp.html new file mode 100644 index 000000000..9ca9f6a98 --- /dev/null +++ b/karate-netty/src/test/java/com/intuit/karate/temp.html @@ -0,0 +1,11 @@ + + + + + blah + + +

Hello World!

+ + + diff --git a/karate-netty/src/test/java/karate-config.js b/karate-netty/src/test/java/karate-config.js index 2b82e3ca1..63ccb153d 100644 --- a/karate-netty/src/test/java/karate-config.js +++ b/karate-netty/src/test/java/karate-config.js @@ -1,5 +1,15 @@ function fn() { var port = karate.properties['karate.server.port']; port = port || '8080'; - return { mockServerUrl: 'http://localhost:' + port + '/v1/' } + var ssl = karate.properties['karate.server.ssl']; + if (ssl) { + karate.log('using ssl:', ssl); + karate.configure('ssl', true); + } + var proxy = karate.properties['karate.server.proxy']; + if (proxy) { + karate.log('using proxy:', proxy); + karate.configure('proxy', proxy); + } + return { mockServerUrl: (ssl ? 'https' : 'http') + '://localhost:' + port + '/v1/' } } diff --git a/karate-netty/src/test/resources/gitignore.txt b/karate-netty/src/test/resources/gitignore.txt new file mode 100644 index 000000000..f2564bc44 --- /dev/null +++ b/karate-netty/src/test/resources/gitignore.txt @@ -0,0 +1,2 @@ +.DS_Store +target/ diff --git a/karate-robot/README.md b/karate-robot/README.md new file mode 100644 index 000000000..b15b7bd35 --- /dev/null +++ b/karate-robot/README.md @@ -0,0 +1,120 @@ +# Karate Robot + +## Desktop Automation Made `Simple.` +> Version 0.9.5 is the first release, and experimental. Please test and contribute if you can ! + +### Demo Videos +* Clicking the *native* "File Upload" button in a Web Page - [Link](https://twitter.com/ptrthomas/status/1215534821234995200) +* Clicking a button in an iOS Mobile Emulator - [Link](https://twitter.com/ptrthomas/status/1217479362666041344) + +### Capabilities +* Cross-Platform: MacOS, Windows, Linux - and should work on others as well via [Java CPP](https://github.com/bytedeco/javacpp) +* Native Mouse Events +* Native Keyboard Events +* Navigation via image detection +* Tightly integrated into Karate + +## Using +The `karate-robot` capabilities are not part of the `karate-core`, because they bring in a few extra dependencies. + +### Maven +Add this to the ``: + +```xml + + com.intuit.karate + karate-robot + ${karate.version} + test + +``` + +This may result in a few large JAR files getting downloaded by default because of the [`javacpp-presets`](https://github.com/bytedeco/javacpp-presets) dependency. But you can narrow down to what is sufficient for your OS by [following these instructions](https://github.com/bytedeco/javacpp-presets/wiki/Reducing-the-Number-of-Dependencies). + +## `robot` +Karate Robot is designed to only activate when you use the `robot` keyword, and if the `karate-robot` Java / JAR dependency is present in the project classpath. + +Here Karate will look for an application window called `Chrome` and will "focus" it so that it becomes the top-most window, and be visible. This will work on Mac, Windows and Linux (X Window System / X11). + +```cucumber +* robot { app: 'Chrome' } +``` + +In development mode, you can switch on a red highlight border around areas that Karate finds via image matching. Note that the `^` prefix means that Karate will look for a window where the name *contains* `Chrome`. + +```cucumber +* robot { app: '^Chrome', highlight: true } +``` + +Note that you can use [`karate.exec()`](https://github.com/intuit/karate#karate-exec) to run a console command to start an application if needed, before "activating" it. + +> If you want to do conditional logic depending on the OS, you can use [`karate.os`](https://github.com/intuit/karate#karate-os) - for e.g. `* if (karate.os.type == 'windows') karate.set('filename', 'start.bat')` + +The keys that the `robot` keyword supports are the following: + +key | description +--- | ----------- +`app` | the name of the window to bring to focus, and you can use a `^` prefix to do a string "contains" match +`basePath` | defaults to [`classpath:`](https://github.com/intuit/karate#classpath) which means `src/test/java` if you use the [recommended project structure](https://github.com/intuit/karate#folder-structure) +`highlight` | default `false` if an image match should be highlighted +`highlightDuration` | default `1000` - time to `highlight` in milliseconds +`retryCount` | default `3` number of times Karate will attempt to find an image +`retryInterval` | default `2000` time between retries when finding an image + +# API +Please refer to the available methods in [`Robot.java`](src/main/java/com/intuit/karate/robot/Robot.java). Most of them are "chainable". + +Here is a sample test: + +```cucumber +* robot { app: '^Chrome', highlight: true } +* robot.input(Key.META, 't') +* robot.input('karate dsl' + Key.ENTER) +* robot.click('tams.png') +``` + +The above flow performs the following operations: +* finds an already open window where the name contains `Chrome` +* enables "highlight" mode for ease of development / troubleshooting +* triggers keyboard events for [COMMAND + t] which will open a new browser tab +* triggers keyboard events for the input "karate dsl" and an ENTER key-press +* waits for a section of the screen defined by [`tams.png`](src/test/java/tams.png) to appear - and clicks in the center of that region + +## Images +Images have to be in PNG format, and with the extension `*.png`. Karate will attempt to find images that are smaller or larger to a certain extent. But for the best results, try to save images that are the same resolution as the application under test. + +## `Key` +Just [like Karate UI](https://github.com/intuit/karate/tree/master/karate-core#special-keys), the special keys are made available under the namespace `Key`. You can see all the available codes [here](https://github.com/intuit/karate/blob/master/karate-core/src/main/java/com/intuit/karate/driver/Key.java). + +```cucumber +* robot.input('karate dsl' + Key.ENTER) +``` + +## `robot.basePath` +Rarely used since `basePath` would typically be set by the [`robot` options](#robot). But you can do this any time during a test to "switch". Note that [`classpath:`](https://github.com/intuit/karate#classpath) would [typically resolve](https://github.com/intuit/karate#folder-structure) to `src/test/java`. + +```cucumber +* robot.basePath = 'classpath:some/package' +``` + +## `robot.click()` +Defaults to a "left-click", pass 1, 2 or 3 as the argument to specify left, middle or right mouse button. + +## `robot.move()` +Argument can be `x, y` co-ordinates or typically the name of an image, which will be looked for in the [`basePath`](#robot). Note that relative paths will work. + +## `robot.delay()` +Not recommended unless un-avoidable. Argument is time in milliseconds. + +## `robot.input()` +If there are 2 arguments, the first argument is for [modifier keys](#key) such as `Key.CTRL`, `Key.ALT`, etc. + +```cucumber +* robot.input(Key.META, 't') +``` + +Else, you pass a string of text which can include special characters such as a line-feed: + +```cucumber +* robot.input('karate dsl' + Key.ENTER) +``` diff --git a/karate-robot/pom.xml b/karate-robot/pom.xml new file mode 100644 index 000000000..a0ebf6221 --- /dev/null +++ b/karate-robot/pom.xml @@ -0,0 +1,64 @@ + + 4.0.0 + + + com.intuit.karate + karate-parent + 0.9.5 + + karate-robot + jar + + + 1.5.2 + 4.1.2 + + + + + com.intuit.karate + karate-core + ${project.version} + + + net.java.dev.jna + jna-platform + 5.5.0 + + + org.bytedeco + javacv + ${javacpp.version} + + + org.bytedeco + opencv-platform + ${opencv.version}-${javacpp.version} + + + com.intuit.karate + karate-apache + ${project.version} + test + + + com.intuit.karate + karate-junit4 + ${project.version} + test + + + + + + + src/test/java + + **/*.java + + + + + + \ No newline at end of file diff --git a/karate-robot/src/main/java/com/intuit/karate/robot/Location.java b/karate-robot/src/main/java/com/intuit/karate/robot/Location.java new file mode 100644 index 000000000..4aac1a5c4 --- /dev/null +++ b/karate-robot/src/main/java/com/intuit/karate/robot/Location.java @@ -0,0 +1,61 @@ +/* + * The MIT License + * + * Copyright 2020 Intuit Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.intuit.karate.robot; + +/** + * + * @author pthomas3 + */ +public class Location { + + public Robot robot; + + public final int x; + public final int y; + + public Location(int x, int y) { + this.x = x; + this.y = y; + } + + public Location with(Robot robot) { + this.robot = robot; + return this; + } + + public Location move() { + robot.move(x, y); + return this; + } + + public Location click() { + return click(1); + } + + public Location click(int num) { + robot.move(x, y).click(num); + return this; + } + +} diff --git a/karate-robot/src/main/java/com/intuit/karate/robot/Region.java b/karate-robot/src/main/java/com/intuit/karate/robot/Region.java new file mode 100644 index 000000000..67612302d --- /dev/null +++ b/karate-robot/src/main/java/com/intuit/karate/robot/Region.java @@ -0,0 +1,77 @@ +/* + * The MIT License + * + * Copyright 2020 Intuit Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.intuit.karate.robot; + +/** + * + * @author pthomas3 + */ +public class Region { + + private Robot robot; + + public final int x; + public final int y; + public final int width; + public final int height; + + public Region(int x, int y) { + this(x, y, 0, 0); + } + + public Region(int x, int y, int width, int height) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + + public Region with(Robot robot) { + this.robot = robot; + return this; + } + + public Location center() { + return new Location(x + width / 2, y + height / 2).with(robot); + } + + public void highlight(int millis) { + RobotUtils.highlight(x, y, width, height, millis); + } + + public Region click() { + return click(1); + } + + public Region click(int num) { + center().click(num); + return this; + } + + public Region move() { + center().move(); + return this; + } + +} diff --git a/karate-robot/src/main/java/com/intuit/karate/robot/Robot.java b/karate-robot/src/main/java/com/intuit/karate/robot/Robot.java new file mode 100644 index 000000000..d8917be80 --- /dev/null +++ b/karate-robot/src/main/java/com/intuit/karate/robot/Robot.java @@ -0,0 +1,292 @@ +/* + * The MIT License + * + * Copyright 2019 Intuit Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.intuit.karate.robot; + +import com.intuit.karate.FileUtils; +import com.intuit.karate.ScriptValue; +import com.intuit.karate.core.ScenarioContext; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Image; +import java.awt.Rectangle; +import java.awt.Toolkit; +import java.awt.event.InputEvent; +import java.awt.image.BufferedImage; +import java.io.File; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Predicate; +import java.util.function.Supplier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author pthomas3 + */ +public class Robot { + + private static final Logger logger = LoggerFactory.getLogger(Robot.class); + + public final ScenarioContext context; + public final java.awt.Robot robot; + public final Toolkit toolkit; + public final Dimension dimension; + public final Map options; + public final boolean highlight; + public final int highlightDuration; + public final int retryCount; + public final int retryInterval; + + public String basePath; + + private T get(String key, T defaultValue) { + T temp = (T) options.get(key); + return temp == null ? defaultValue : temp; + } + + public Robot(ScenarioContext context) { + this(context, Collections.EMPTY_MAP); + } + + public Robot(ScenarioContext context, Map options) { + this.context = context; + try { + this.options = options; + basePath = get("basePath", "classpath:"); + highlight = get("highlight", false); + highlightDuration = get("highlightDuration", 1000); + retryCount = get("retryCount", 3); + retryInterval = get("retryInterval", 2000); + toolkit = Toolkit.getDefaultToolkit(); + dimension = toolkit.getScreenSize(); + robot = new java.awt.Robot(); + robot.setAutoDelay(40); + robot.setAutoWaitForIdle(true); + String app = (String) options.get("app"); + if (app != null) { + switchTo(app); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public T retry(Supplier action, Predicate condition, String logDescription) { + long startTime = System.currentTimeMillis(); + int count = 0, max = retryCount; + T result; + boolean success; + do { + if (count > 0) { + logger.debug("{} - retry #{}", logDescription, count); + delay(retryInterval); + } + result = action.get(); + success = condition.test(result); + } while (!success && count++ < max); + if (!success) { + long elapsedTime = System.currentTimeMillis() - startTime; + logger.warn("failed after {} retries and {} milliseconds", (count - 1), elapsedTime); + } + return result; + } + + public void setBasePath(String basePath) { + this.basePath = basePath; + } + + public byte[] read(String path) { + if (basePath != null) { + String slash = basePath.endsWith(":") ? "" : "/"; + path = basePath + slash + path; + } + ScriptValue sv = FileUtils.readFile(path, context); + return sv.getAsByteArray(); + } + + public Robot delay(int millis) { + robot.delay(millis); + return this; + } + + private static int mask(int num) { + switch (num) { + case 2: return InputEvent.BUTTON2_DOWN_MASK; + case 3: return InputEvent.BUTTON3_DOWN_MASK; + default: return InputEvent.BUTTON1_DOWN_MASK; + } + } + + public Robot click() { + return click(1); + } + + public Robot click(int num) { + int mask = mask(num); + robot.mousePress(mask); + robot.mouseRelease(mask); + return this; + } + + public Robot input(char s) { + return input(Character.toString(s)); + } + + public Robot input(String mod, char s) { + return input(mod, Character.toString(s)); + } + + public Robot input(char mod, String s) { + return input(Character.toString(mod), s); + } + + public Robot input(char mod, char s) { + return input(Character.toString(mod), Character.toString(s)); + } + + public Robot input(String mod, String s) { // TODO refactor + for (char c : mod.toCharArray()) { + int[] codes = RobotUtils.KEY_CODES.get(c); + if (codes == null) { + logger.warn("cannot resolve char: {}", c); + robot.keyPress(c); + } else { + robot.keyPress(codes[0]); + } + } + input(s); + for (char c : mod.toCharArray()) { + int[] codes = RobotUtils.KEY_CODES.get(c); + if (codes == null) { + logger.warn("cannot resolve char: {}", c); + robot.keyRelease(c); + } else { + robot.keyRelease(codes[0]); + } + } + return this; + } + + public Robot input(String s) { + for (char c : s.toCharArray()) { + int[] codes = RobotUtils.KEY_CODES.get(c); + if (codes == null) { + logger.warn("cannot resolve char: {}", c); + robot.keyPress(c); + robot.keyRelease(c); + } else if (codes.length > 1) { + robot.keyPress(codes[0]); + robot.keyPress(codes[1]); + robot.keyRelease(codes[1]); + robot.keyRelease(codes[0]); + } else { + robot.keyPress(codes[0]); + robot.keyRelease(codes[0]); + } + } + return this; + } + + public BufferedImage capture() { + int width = dimension.width; + int height = dimension.height; + Image image = robot.createScreenCapture(new Rectangle(0, 0, width, height)); + BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY); + Graphics g = bi.createGraphics(); + g.drawImage(image, 0, 0, width, height, null); + return bi; + } + + public File captureAndSave(String path) { + BufferedImage image = capture(); + File file = new File(path); + RobotUtils.save(image, file); + return file; + } + + public Region move(int x, int y) { + return new Region(x, y).with(this).move(); + } + + public Region click(int x, int y) { + return move(x, y).click(); + } + + public Region move(String path) { + return find(path).move(); + } + + public Region click(String path) { + return find(path).click(); + } + + public Region find(String path) { + return find(read(path)).with(this); + } + + public Region find(byte[] bytes) { + AtomicBoolean resize = new AtomicBoolean(); + Region region = retry(() -> RobotUtils.find(capture(), bytes, resize.getAndSet(true)), r -> r != null, "find by image"); + if (highlight) { + region.highlight(highlightDuration); + } + return region; + } + + public boolean switchTo(String title) { + if (title.startsWith("^")) { + return switchTo(t -> t.contains(title.substring(1))); + } + FileUtils.OsType type = FileUtils.getOsType(); + switch (type) { + case LINUX: + return RobotUtils.switchToLinuxOs(title); + case MACOSX: + return RobotUtils.switchToMacOs(title); + case WINDOWS: + return RobotUtils.switchToWinOs(title); + default: + logger.warn("unsupported os: {}", type); + return false; + } + } + + public boolean switchTo(Predicate condition) { + FileUtils.OsType type = FileUtils.getOsType(); + switch (type) { + case LINUX: + return RobotUtils.switchToLinuxOs(condition); + case MACOSX: + return RobotUtils.switchToMacOs(condition); + case WINDOWS: + return RobotUtils.switchToWinOs(condition); + default: + logger.warn("unsupported os: {}", type); + return false; + } + } + +} diff --git a/karate-robot/src/main/java/com/intuit/karate/robot/RobotUtils.java b/karate-robot/src/main/java/com/intuit/karate/robot/RobotUtils.java new file mode 100644 index 000000000..72914d510 --- /dev/null +++ b/karate-robot/src/main/java/com/intuit/karate/robot/RobotUtils.java @@ -0,0 +1,499 @@ +/* + * The MIT License + * + * Copyright 2019 Intuit Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.intuit.karate.robot; + +import com.intuit.karate.StringUtils; +import com.intuit.karate.driver.Keys; +import com.intuit.karate.shell.Command; +import com.sun.jna.Native; +import com.sun.jna.platform.win32.User32; +import com.sun.jna.platform.win32.WinDef.HWND; +import java.awt.Color; +import java.awt.Image; +import java.awt.event.KeyEvent; +import java.awt.image.BufferedImage; +import java.io.File; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Predicate; +import javax.imageio.ImageIO; +import javax.swing.BorderFactory; +import javax.swing.JFrame; +import javax.swing.WindowConstants; +import org.bytedeco.javacpp.DoublePointer; +import org.bytedeco.javacv.CanvasFrame; +import org.bytedeco.javacv.Java2DFrameConverter; +import org.bytedeco.javacv.Java2DFrameUtils; +import org.bytedeco.javacv.OpenCVFrameConverter; +import static org.bytedeco.opencv.global.opencv_core.minMaxLoc; +import static org.bytedeco.opencv.global.opencv_imgcodecs.*; +import static org.bytedeco.opencv.global.opencv_imgproc.*; +import org.bytedeco.opencv.opencv_core.Mat; +import org.bytedeco.opencv.opencv_core.Point; +import org.bytedeco.opencv.opencv_core.Point2f; +import org.bytedeco.opencv.opencv_core.Point2fVector; +import org.bytedeco.opencv.opencv_core.Rect; +import org.bytedeco.opencv.opencv_core.Scalar; +import org.bytedeco.opencv.opencv_core.Size; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author pthomas3 + */ +public class RobotUtils { + + private static final Logger logger = LoggerFactory.getLogger(RobotUtils.class); + + public static Region find(File source, File target, boolean resize) { + return find(read(source), read(target), resize); + } + + public static Region find(BufferedImage source, byte[] bytes, boolean resize) { + Mat srcMat = Java2DFrameUtils.toMat(source); + return find(srcMat, read(bytes), resize); + } + + public static Region find(BufferedImage source, File target, boolean resize) { + Mat srcMat = Java2DFrameUtils.toMat(source); + return find(srcMat, read(target), resize); + } + + public static Mat rescale(Mat mat, double scale) { + Mat resized = new Mat(); + resize(mat, resized, new Size(), scale, scale, CV_INTER_AREA); + return resized; + } + + public static Region find(Mat source, Mat target, boolean resize) { + Double prevMinVal = null; + double prevRatio = -1; + Point prevMinPt = null; + double prevScore = -1; + //===================== + double step = 0.1; + int count = resize ? 5 : 0; + int targetScore = target.size().area() * 300; // magic number + for (int i = -count; i <= count; i++) { + double scale = 1 + step * i; + Mat resized = scale == 1 ? source : rescale(source, scale); + Size temp = resized.size(); + logger.debug("scale: {} - {}:{} - target: {}", scale, temp.width(), temp.height(), targetScore); + Mat result = new Mat(); + matchTemplate(resized, target, result, CV_TM_SQDIFF); + DoublePointer minVal = new DoublePointer(1); + DoublePointer maxVal = new DoublePointer(1); + Point minPt = new Point(); + Point maxPt = new Point(); + minMaxLoc(result, minVal, maxVal, minPt, maxPt, null); + double tempMinVal = minVal.get(); + double ratio = (double) 1 / scale; + double score = tempMinVal / targetScore; + String minValString = String.format("%.1f", tempMinVal); + if (prevMinVal == null || tempMinVal < prevMinVal) { + prevMinVal = tempMinVal; + prevRatio = ratio; + prevMinPt = minPt; + prevScore = score; + logger.debug("found minVal: {}, score: {}, ratio: {}", minValString, score, ratio); + } else { + logger.debug("ignore minVal: {}, score: {}, ratio: {}", minValString, score, ratio); + } + } + if (prevScore > 1.5) { + logger.debug("match quality insufficient: {}", prevScore); + return null; + } + int x = (int) Math.round(prevMinPt.x() * prevRatio); + int y = (int) Math.round(prevMinPt.y() * prevRatio); + int width = (int) Math.round(target.cols() * prevRatio); + int height = (int) Math.round(target.rows() * prevRatio); + return new Region(x, y, width, height); + } + + public static Mat loadAndShowOrExit(File file, int flags) { + Mat image = read(file, flags); + show(image, file.getName()); + return image; + } + + public static BufferedImage readImage(File file) { + Mat mat = read(file, IMREAD_GRAYSCALE); + return toBufferedImage(mat); + } + + public static Mat read(File file) { + return read(file, IMREAD_GRAYSCALE); + } + + public static Mat read(byte[] bytes) { + return read(bytes, IMREAD_GRAYSCALE); + } + + public static Mat read(byte[] bytes, int flags) { + Mat image = imdecode(new Mat(bytes), flags); + if (image.empty()) { + throw new RuntimeException("image decode failed"); + } + return image; + } + + public static Mat read(File file, int flags) { + Mat image = imread(file.getAbsolutePath(), flags); + if (image.empty()) { + throw new RuntimeException("image not found: " + file.getAbsolutePath()); + } + return image; + } + + public static File save(BufferedImage image, File file) { + try { + ImageIO.write(image, "png", file); + return file; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static void show(Mat mat, String title) { + OpenCVFrameConverter.ToMat converter = new OpenCVFrameConverter.ToMat(); + CanvasFrame canvas = new CanvasFrame(title, 1); + canvas.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); + canvas.showImage(converter.convert(mat)); + } + + public static void show(Image image, String title) { + CanvasFrame canvas = new CanvasFrame(title, 1); + canvas.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); + canvas.showImage(image); + } + + public static void save(Mat image, File file) { + imwrite(file.getAbsolutePath(), image); + } + + public static Mat drawOnImage(Mat image, Point2fVector points) { + Mat dest = image.clone(); + int radius = 5; + Scalar red = new Scalar(0, 0, 255, 0); + for (int i = 0; i < points.size(); i++) { + Point2f p = points.get(i); + circle(dest, new Point(Math.round(p.x()), Math.round(p.y())), radius, red); + } + return dest; + } + + public static Mat drawOnImage(Mat image, Rect overlay, Scalar color) { + Mat dest = image.clone(); + rectangle(dest, overlay, color); + return dest; + } + + public static BufferedImage toBufferedImage(Mat mat) { + OpenCVFrameConverter.ToMat openCVConverter = new OpenCVFrameConverter.ToMat(); + Java2DFrameConverter java2DConverter = new Java2DFrameConverter(); + return java2DConverter.convert(openCVConverter.convert(mat)); + } + + //========================================================================== + // + private static final String MAC_GET_PROCS + = " tell application \"System Events\"" + + "\n set procs to (processes whose background only is false)" + + "\n set results to {}" + + "\n repeat with n from 1 to the length of procs" + + "\n set p to item n of procs" + + "\n set entry to { name of p as text,\"|\"}" + + "\n set end of results to entry" + + "\n end repeat" + + "\n end tell" + + "\n results"; + + public static List getAppsMacOs() { + String res = Command.exec(true, null, "osascript", "-e", MAC_GET_PROCS); + res = res + ", "; + res = res.replace(", |, ", "\n"); + return StringUtils.split(res, '\n'); + } + + public static boolean switchToMacOs(Predicate condition) { + List list = getAppsMacOs(); + for (String s : list) { + if (condition.test(s)) { + Command.exec(true, null, "osascript", "-e", "tell app \"" + s + "\" to activate"); + return true; // TODO use command return code + } + } + return false; + } + + public static boolean switchToMacOs(String title) { + Command.exec(true, null, "osascript", "-e", "tell app \"" + title + "\" to activate"); + return true; // TODO use command return code + } + + public static boolean switchToWinOs(Predicate condition) { + final AtomicBoolean found = new AtomicBoolean(); + User32.INSTANCE.EnumWindows((HWND hwnd, com.sun.jna.Pointer p) -> { + char[] windowText = new char[512]; + User32.INSTANCE.GetWindowText(hwnd, windowText, 512); + String windowName = Native.toString(windowText); + logger.debug("scanning window: {}", windowName); + if (condition.test(windowName)) { + found.set(true); + focusWinOs(hwnd); + return false; + } + return true; + }, null); + return found.get(); + } + + private static void focusWinOs(HWND hwnd) { + User32.INSTANCE.ShowWindow(hwnd, 9); // SW_RESTORE + User32.INSTANCE.SetForegroundWindow(hwnd); + } + + public static boolean switchToWinOs(String title) { + HWND hwnd = User32.INSTANCE.FindWindow(null, title); + if (hwnd == null) { + return false; + } else { + focusWinOs(hwnd); + return true; + } + } + + public static boolean switchToLinuxOs(String title) { + Command.exec(true, null, "wmctrl", "-FR", title); + return true; // TODO ? + } + + public static boolean switchToLinuxOs(Predicate condition) { + String res = Command.exec(true, null, "wmctrl", "-l"); + List lines = StringUtils.split(res, '\n'); + for (String line : lines) { + List cols = StringUtils.split(line, ' '); + String id = cols.get(0); + String host = cols.get(2); + int pos = line.indexOf(host); + String name = line.substring(pos + host.length() + 1); + if (condition.test(name)) { + Command.exec(true, null, "wmctrl", "-iR", id); + return true; + } + } + return false; + } + + public static void highlight(int x, int y, int width, int height, int time) { + JFrame f = new JFrame(); + f.setUndecorated(true); + f.setBackground(new Color(0, 0, 0, 0)); + f.setAlwaysOnTop(true); + f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + f.setType(JFrame.Type.UTILITY); + f.setFocusableWindowState(false); + f.setAutoRequestFocus(false); + f.setLocation(x, y); + f.setSize(width, height); + f.getRootPane().setBorder(BorderFactory.createLineBorder(Color.RED, 3)); + f.setVisible(true); + delay(time); + f.dispose(); + } + + public static void delay(int millis) { + try { + Thread.sleep(millis); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + //========================================================================== + // + public static final Map KEY_CODES = new HashMap(); + + private static void key(char c, int... i) { + KEY_CODES.put(c, i); + } + + static { + key('a', KeyEvent.VK_A); + key('b', KeyEvent.VK_B); + key('c', KeyEvent.VK_C); + key('d', KeyEvent.VK_D); + key('e', KeyEvent.VK_E); + key('f', KeyEvent.VK_F); + key('g', KeyEvent.VK_G); + key('h', KeyEvent.VK_H); + key('i', KeyEvent.VK_I); + key('j', KeyEvent.VK_J); + key('k', KeyEvent.VK_K); + key('l', KeyEvent.VK_L); + key('m', KeyEvent.VK_M); + key('n', KeyEvent.VK_N); + key('o', KeyEvent.VK_O); + key('p', KeyEvent.VK_P); + key('q', KeyEvent.VK_Q); + key('r', KeyEvent.VK_R); + key('s', KeyEvent.VK_S); + key('t', KeyEvent.VK_T); + key('u', KeyEvent.VK_U); + key('v', KeyEvent.VK_V); + key('w', KeyEvent.VK_W); + key('x', KeyEvent.VK_X); + key('y', KeyEvent.VK_Y); + key('z', KeyEvent.VK_Z); + key('A', KeyEvent.VK_SHIFT, KeyEvent.VK_A); + key('B', KeyEvent.VK_SHIFT, KeyEvent.VK_B); + key('C', KeyEvent.VK_SHIFT, KeyEvent.VK_C); + key('D', KeyEvent.VK_SHIFT, KeyEvent.VK_D); + key('E', KeyEvent.VK_SHIFT, KeyEvent.VK_E); + key('F', KeyEvent.VK_SHIFT, KeyEvent.VK_F); + key('G', KeyEvent.VK_SHIFT, KeyEvent.VK_G); + key('H', KeyEvent.VK_SHIFT, KeyEvent.VK_H); + key('I', KeyEvent.VK_SHIFT, KeyEvent.VK_I); + key('J', KeyEvent.VK_SHIFT, KeyEvent.VK_J); + key('K', KeyEvent.VK_SHIFT, KeyEvent.VK_K); + key('L', KeyEvent.VK_SHIFT, KeyEvent.VK_L); + key('M', KeyEvent.VK_SHIFT, KeyEvent.VK_M); + key('N', KeyEvent.VK_SHIFT, KeyEvent.VK_N); + key('O', KeyEvent.VK_SHIFT, KeyEvent.VK_O); + key('P', KeyEvent.VK_SHIFT, KeyEvent.VK_P); + key('Q', KeyEvent.VK_SHIFT, KeyEvent.VK_Q); + key('R', KeyEvent.VK_SHIFT, KeyEvent.VK_R); + key('S', KeyEvent.VK_SHIFT, KeyEvent.VK_S); + key('T', KeyEvent.VK_SHIFT, KeyEvent.VK_T); + key('U', KeyEvent.VK_SHIFT, KeyEvent.VK_U); + key('V', KeyEvent.VK_SHIFT, KeyEvent.VK_V); + key('W', KeyEvent.VK_SHIFT, KeyEvent.VK_W); + key('X', KeyEvent.VK_SHIFT, KeyEvent.VK_X); + key('Y', KeyEvent.VK_SHIFT, KeyEvent.VK_Y); + key('Z', KeyEvent.VK_SHIFT, KeyEvent.VK_Z); + key('1', KeyEvent.VK_1); + key('2', KeyEvent.VK_2); + key('3', KeyEvent.VK_3); + key('4', KeyEvent.VK_4); + key('5', KeyEvent.VK_5); + key('6', KeyEvent.VK_6); + key('7', KeyEvent.VK_7); + key('8', KeyEvent.VK_8); + key('9', KeyEvent.VK_9); + key('0', KeyEvent.VK_0); + key('!', KeyEvent.VK_SHIFT, KeyEvent.VK_1); + key('@', KeyEvent.VK_SHIFT, KeyEvent.VK_2); + key('#', KeyEvent.VK_SHIFT, KeyEvent.VK_3); + key('$', KeyEvent.VK_SHIFT, KeyEvent.VK_4); + key('%', KeyEvent.VK_SHIFT, KeyEvent.VK_5); + key('^', KeyEvent.VK_SHIFT, KeyEvent.VK_6); + key('&', KeyEvent.VK_SHIFT, KeyEvent.VK_7); + key('*', KeyEvent.VK_SHIFT, KeyEvent.VK_8); + key('(', KeyEvent.VK_SHIFT, KeyEvent.VK_9); + key(')', KeyEvent.VK_SHIFT, KeyEvent.VK_0); + key('`', KeyEvent.VK_BACK_QUOTE); + key('~', KeyEvent.VK_SHIFT, KeyEvent.VK_BACK_QUOTE); + key('-', KeyEvent.VK_MINUS); + key('_', KeyEvent.VK_SHIFT, KeyEvent.VK_MINUS); + key('=', KeyEvent.VK_EQUALS); + key('+', KeyEvent.VK_SHIFT, KeyEvent.VK_EQUALS); + key('[', KeyEvent.VK_OPEN_BRACKET); + key('{', KeyEvent.VK_SHIFT, KeyEvent.VK_OPEN_BRACKET); + key(']', KeyEvent.VK_CLOSE_BRACKET); + key('}', KeyEvent.VK_SHIFT, KeyEvent.VK_CLOSE_BRACKET); + key('\\', KeyEvent.VK_BACK_SLASH); + key('|', KeyEvent.VK_SHIFT, KeyEvent.VK_BACK_SLASH); + key(';', KeyEvent.VK_SEMICOLON); + key(':', KeyEvent.VK_SHIFT, KeyEvent.VK_SEMICOLON); + key('\'', KeyEvent.VK_QUOTE); + key('"', KeyEvent.VK_SHIFT, KeyEvent.VK_QUOTE); + key(',', KeyEvent.VK_COMMA); + key('<', KeyEvent.VK_SHIFT, KeyEvent.VK_COMMA); + key('.', KeyEvent.VK_PERIOD); + key('|', KeyEvent.VK_SHIFT, KeyEvent.VK_PERIOD); + key('/', KeyEvent.VK_SLASH); + key('?', KeyEvent.VK_SHIFT, KeyEvent.VK_SLASH); + //====================================================================== + key('\b', KeyEvent.VK_BACK_SPACE); + key('\t', KeyEvent.VK_TAB); + key('\r', KeyEvent.VK_ENTER); + key('\n', KeyEvent.VK_ENTER); + key(' ', KeyEvent.VK_SPACE); + key(Keys.CONTROL, KeyEvent.VK_CONTROL); + key(Keys.ALT, KeyEvent.VK_ALT); + key(Keys.META, KeyEvent.VK_META); + key(Keys.SHIFT, KeyEvent.VK_SHIFT); + key(Keys.TAB, KeyEvent.VK_TAB); + key(Keys.ENTER, KeyEvent.VK_ENTER); + key(Keys.SPACE, KeyEvent.VK_SPACE); + key(Keys.BACK_SPACE, KeyEvent.VK_BACK_SPACE); + //====================================================================== + key(Keys.UP, KeyEvent.VK_UP); + key(Keys.RIGHT, KeyEvent.VK_RIGHT); + key(Keys.DOWN, KeyEvent.VK_DOWN); + key(Keys.LEFT, KeyEvent.VK_LEFT); + key(Keys.PAGE_UP, KeyEvent.VK_PAGE_UP); + key(Keys.PAGE_DOWN, KeyEvent.VK_PAGE_DOWN); + key(Keys.END, KeyEvent.VK_END); + key(Keys.HOME, KeyEvent.VK_HOME); + key(Keys.DELETE, KeyEvent.VK_DELETE); + key(Keys.ESCAPE, KeyEvent.VK_ESCAPE); + key(Keys.F1, KeyEvent.VK_F1); + key(Keys.F2, KeyEvent.VK_F2); + key(Keys.F3, KeyEvent.VK_F3); + key(Keys.F4, KeyEvent.VK_F4); + key(Keys.F5, KeyEvent.VK_F5); + key(Keys.F6, KeyEvent.VK_F6); + key(Keys.F7, KeyEvent.VK_F7); + key(Keys.F8, KeyEvent.VK_F8); + key(Keys.F9, KeyEvent.VK_F9); + key(Keys.F10, KeyEvent.VK_F10); + key(Keys.F11, KeyEvent.VK_F11); + key(Keys.F12, KeyEvent.VK_F12); + key(Keys.INSERT, KeyEvent.VK_INSERT); + key(Keys.PAUSE, KeyEvent.VK_PAUSE); + key(Keys.NUMPAD1, KeyEvent.VK_NUMPAD1); + key(Keys.NUMPAD2, KeyEvent.VK_NUMPAD2); + key(Keys.NUMPAD3, KeyEvent.VK_NUMPAD3); + key(Keys.NUMPAD4, KeyEvent.VK_NUMPAD4); + key(Keys.NUMPAD5, KeyEvent.VK_NUMPAD5); + key(Keys.NUMPAD6, KeyEvent.VK_NUMPAD6); + key(Keys.NUMPAD7, KeyEvent.VK_NUMPAD7); + key(Keys.NUMPAD8, KeyEvent.VK_NUMPAD8); + key(Keys.NUMPAD9, KeyEvent.VK_NUMPAD9); + key(Keys.NUMPAD0, KeyEvent.VK_NUMPAD0); + key(Keys.SEPARATOR, KeyEvent.VK_SEPARATOR); + key(Keys.ADD, KeyEvent.VK_ADD); + key(Keys.SUBTRACT, KeyEvent.VK_SUBTRACT); + key(Keys.MULTIPLY, KeyEvent.VK_MULTIPLY); + key(Keys.DIVIDE, KeyEvent.VK_DIVIDE); + key(Keys.DECIMAL, KeyEvent.VK_DECIMAL); + // TODO SCROLL_LOCK, NUM_LOCK, CAPS_LOCK, PRINTSCREEN, CONTEXT_MENU, WINDOWS + } + +} diff --git a/karate-robot/src/test/java/com/intuit/karate/robot/RobotUtilsTest.java b/karate-robot/src/test/java/com/intuit/karate/robot/RobotUtilsTest.java new file mode 100644 index 000000000..8620c98a2 --- /dev/null +++ b/karate-robot/src/test/java/com/intuit/karate/robot/RobotUtilsTest.java @@ -0,0 +1,31 @@ +package com.intuit.karate.robot; + +import java.io.File; +import org.junit.Test; +import static org.junit.Assert.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author pthomas3 + */ +public class RobotUtilsTest { + + private static final Logger logger = LoggerFactory.getLogger(RobotUtilsTest.class); + + @Test + public void testOpenCv() { + // System.setProperty("org.bytedeco.javacpp.logger.debug", "true"); + File target = new File("src/test/java/search.png"); + File source = new File("src/test/java/desktop01.png"); + Region region = RobotUtils.find(source, target, false); + assertEquals(1605, region.x); + assertEquals(1, region.y); + target = new File("src/test/java/search-1_5.png"); + region = RobotUtils.find(source, target, true); + assertEquals(1605, region.x); + assertEquals(1, region.y); + } + +} diff --git a/karate-robot/src/test/java/desktop01.png b/karate-robot/src/test/java/desktop01.png new file mode 100644 index 000000000..e5eec41e0 Binary files /dev/null and b/karate-robot/src/test/java/desktop01.png differ diff --git a/karate-robot/src/test/java/iphone-click.png b/karate-robot/src/test/java/iphone-click.png new file mode 100644 index 000000000..ff7b7b437 Binary files /dev/null and b/karate-robot/src/test/java/iphone-click.png differ diff --git a/karate-robot/src/test/java/robot/core/CaptureRunner.java b/karate-robot/src/test/java/robot/core/CaptureRunner.java new file mode 100644 index 000000000..557281fe8 --- /dev/null +++ b/karate-robot/src/test/java/robot/core/CaptureRunner.java @@ -0,0 +1,21 @@ +package robot.core; + +import com.intuit.karate.robot.Robot; +import org.junit.Test; + +/** + * + * @author pthomas3 + */ +public class CaptureRunner { + + @Test + public void testCapture() { + Robot bot = new Robot(ChromeJavaRunner.getContext()); + // make sure Chrome is open + bot.switchTo(t -> t.contains("Chrome")); + bot.delay(1000); + bot.captureAndSave("target/temp.png"); + } + +} diff --git a/karate-robot/src/test/java/robot/core/ChromeJavaRunner.java b/karate-robot/src/test/java/robot/core/ChromeJavaRunner.java new file mode 100755 index 000000000..d85f81bf5 --- /dev/null +++ b/karate-robot/src/test/java/robot/core/ChromeJavaRunner.java @@ -0,0 +1,38 @@ +package robot.core; + +import com.intuit.karate.CallContext; +import com.intuit.karate.FileUtils; +import com.intuit.karate.core.FeatureContext; +import com.intuit.karate.core.ScenarioContext; +import com.intuit.karate.driver.Keys; +import com.intuit.karate.robot.Region; +import com.intuit.karate.robot.Robot; +import java.nio.file.Path; +import org.junit.Test; + +/** + * + * @author pthomas3 + */ +public class ChromeJavaRunner { + + public static ScenarioContext getContext() { + Path featureDir = FileUtils.getPathContaining(ChromeJavaRunner.class); + FeatureContext featureContext = FeatureContext.forWorkingDir("dev", featureDir.toFile()); + CallContext callContext = new CallContext(null, true); + return new ScenarioContext(featureContext, callContext, null, null); + } + + @Test + public void testCalc() { + Robot bot = new Robot(getContext()); + // make sure Chrome is open + bot.switchTo(t -> t.contains("Chrome")); + bot.input(Keys.META, "t"); + bot.input("karate dsl" + Keys.ENTER); + Region region = bot.find("tams.png"); + region.highlight(2000); + region.click(); + } + +} diff --git a/karate-junit4/src/test/java/com/intuit/karate/junit4/selenium/SampleRunner.java b/karate-robot/src/test/java/robot/core/ChromeRunner.java similarity index 52% rename from karate-junit4/src/test/java/com/intuit/karate/junit4/selenium/SampleRunner.java rename to karate-robot/src/test/java/robot/core/ChromeRunner.java index 601fb518a..de72223aa 100644 --- a/karate-junit4/src/test/java/com/intuit/karate/junit4/selenium/SampleRunner.java +++ b/karate-robot/src/test/java/robot/core/ChromeRunner.java @@ -1,7 +1,7 @@ -package com.intuit.karate.junit4.selenium; +package robot.core; -import com.intuit.karate.junit4.Karate; import com.intuit.karate.KarateOptions; +import com.intuit.karate.junit4.Karate; import org.junit.runner.RunWith; /** @@ -9,7 +9,7 @@ * @author pthomas3 */ @RunWith(Karate.class) -@KarateOptions(features = "classpath:com/intuit/karate/junit4/selenium/sample.feature") -public class SampleRunner { +@KarateOptions(features = "classpath:robot/core/chrome.feature") +public class ChromeRunner { } diff --git a/karate-robot/src/test/java/robot/core/IphoneRunner.java b/karate-robot/src/test/java/robot/core/IphoneRunner.java new file mode 100644 index 000000000..75c561b01 --- /dev/null +++ b/karate-robot/src/test/java/robot/core/IphoneRunner.java @@ -0,0 +1,15 @@ +package robot.core; + +import com.intuit.karate.KarateOptions; +import com.intuit.karate.junit4.Karate; +import org.junit.runner.RunWith; + +/** + * + * @author pthomas3 + */ +@RunWith(Karate.class) +@KarateOptions(features = "classpath:robot/core/iphone.feature") +public class IphoneRunner { + +} diff --git a/karate-robot/src/test/java/robot/core/chrome.feature b/karate-robot/src/test/java/robot/core/chrome.feature new file mode 100644 index 000000000..f16cd7f3c --- /dev/null +++ b/karate-robot/src/test/java/robot/core/chrome.feature @@ -0,0 +1,8 @@ +Feature: browser + robot test + +Scenario: +# * karate.exec('Chrome') +* robot { app: '^Chrome', highlight: true } +* robot.input(Key.META, 't') +* robot.input('karate dsl' + Key.ENTER) +* robot.click('tams.png') diff --git a/karate-robot/src/test/java/robot/core/iphone.feature b/karate-robot/src/test/java/robot/core/iphone.feature new file mode 100644 index 000000000..a18fc2607 --- /dev/null +++ b/karate-robot/src/test/java/robot/core/iphone.feature @@ -0,0 +1,6 @@ +Feature: browser + robot test + +Scenario: +# * karate.exec('Chrome') +* robot { app: '^Simulator', highlight: true } +* robot.click('iphone-click.png') diff --git a/karate-robot/src/test/java/search-1_5.png b/karate-robot/src/test/java/search-1_5.png new file mode 100644 index 000000000..f41a0bf90 Binary files /dev/null and b/karate-robot/src/test/java/search-1_5.png differ diff --git a/karate-robot/src/test/java/search.png b/karate-robot/src/test/java/search.png new file mode 100644 index 000000000..1e61bd4d8 Binary files /dev/null and b/karate-robot/src/test/java/search.png differ diff --git a/karate-robot/src/test/java/tams.png b/karate-robot/src/test/java/tams.png new file mode 100644 index 000000000..c8923b17a Binary files /dev/null and b/karate-robot/src/test/java/tams.png differ diff --git a/karate-ui/README.md b/karate-ui/README.md deleted file mode 100644 index b35b5b8ec..000000000 --- a/karate-ui/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# Karate UI -Refer: [https://github.com/intuit/karate/wiki/Karate-UI](https://github.com/intuit/karate/wiki/Karate-UI) \ No newline at end of file diff --git a/karate-ui/pom.xml b/karate-ui/pom.xml deleted file mode 100644 index 4fe06e4de..000000000 --- a/karate-ui/pom.xml +++ /dev/null @@ -1,43 +0,0 @@ - - 4.0.0 - - - com.intuit.karate - karate-parent - 1.0.0 - - karate-ui - jar - - - - org.openjfx - javafx-controls - 12-ea+9 - - - com.intuit.karate - karate-core - ${project.version} - - - junit - junit - ${junit.version} - test - - - - - - - src/test/java - - **/*.java - - - - - - diff --git a/karate-ui/src/main/java/com/intuit/karate/ui/App.java b/karate-ui/src/main/java/com/intuit/karate/ui/App.java deleted file mode 100644 index cb2878a13..000000000 --- a/karate-ui/src/main/java/com/intuit/karate/ui/App.java +++ /dev/null @@ -1,184 +0,0 @@ -/* - * The MIT License - * - * Copyright 2018 Intuit Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package com.intuit.karate.ui; - -import com.intuit.karate.FileUtils; -import com.intuit.karate.ScriptBindings; -import com.intuit.karate.core.Feature; -import com.intuit.karate.core.FeatureParser; -import com.intuit.karate.formats.postman.PostmanItem; -import com.intuit.karate.formats.postman.PostmanUtils; -import java.io.File; -import java.util.List; -import javafx.application.Application; -import javafx.geometry.Insets; -import javafx.scene.Scene; -import javafx.scene.control.Menu; -import javafx.scene.control.MenuBar; -import javafx.scene.control.MenuItem; -import javafx.scene.image.Image; -import javafx.scene.layout.BorderPane; -import javafx.scene.text.Font; -import javafx.stage.FileChooser; -import javafx.stage.Stage; -import javax.swing.ImageIcon; - -/** - * - * @author pthomas3 - */ -public class App extends Application { - - private static final String KARATE_LOGO = "karate-logo.png"; - - public static final double PADDING = 3.0; - public static final Insets PADDING_ALL = new Insets(App.PADDING, App.PADDING, App.PADDING, App.PADDING); - public static final Insets PADDING_HOR = new Insets(0, App.PADDING, 0, App.PADDING); - public static final Insets PADDING_VER = new Insets(App.PADDING, 0, App.PADDING, 0); - public static final Insets PADDING_TOP = new Insets(App.PADDING, 0, 0, 0); - public static final Insets PADDING_BOT = new Insets(0, 0, App.PADDING, 0); - - private final FileChooser fileChooser = new FileChooser(); - - private File workingDir = new File("."); - private final BorderPane rootPane = new BorderPane(); - - private AppSession session; - private String featureName; - private Feature feature; - private String env; - - private File openFileChooser(Stage stage, String description, String extension) { - fileChooser.setTitle("Choose Feature File"); - fileChooser.setInitialDirectory(workingDir); - FileChooser.ExtensionFilter extFilter = new FileChooser.ExtensionFilter(description, extension); - fileChooser.getExtensionFilters().setAll(extFilter); - return fileChooser.showOpenDialog(stage); - } - - private File saveFileChooser(Stage stage, String description, String extension, String name) { - fileChooser.setTitle("Save Feature File"); - fileChooser.setInitialDirectory(workingDir); - FileChooser.ExtensionFilter extFilter = new FileChooser.ExtensionFilter(description, extension); - fileChooser.getExtensionFilters().setAll(extFilter); - fileChooser.setInitialFileName(name); - return fileChooser.showSaveDialog(stage); - } - - public static Font getDefaultFont() { - return Font.font("Courier"); - } - - private void initUi(Stage stage) { - if (feature != null) { - session = new AppSession(rootPane, workingDir, feature, env); - } - MenuBar menuBar = new MenuBar(); - Menu fileMenu = new Menu("File"); - MenuItem openFileMenuItem = new MenuItem("Open"); - fileMenu.getItems().addAll(openFileMenuItem); - openFileMenuItem.setOnAction(e -> { - File file = openFileChooser(stage, "*.feature files", "*.feature"); - if (file != null) { - feature = FeatureParser.parse(file); - workingDir = file.getParentFile(); - initUi(stage); - } - }); - MenuItem saveFileMenuItem = new MenuItem("Save"); - fileMenu.getItems().addAll(saveFileMenuItem); - saveFileMenuItem.setOnAction(e -> { - String fileName = featureName == null ? "noname" : featureName; - File file = saveFileChooser(stage, "*.feature files", "*.feature", fileName + ".feature"); - if (file != null) { - FileUtils.writeToFile(file, feature.getText()); - } - }); - Menu importMenu = new Menu("Import"); - MenuItem importMenuItem = new MenuItem("Open"); - importMenuItem.setOnAction(e -> { - File file = openFileChooser(stage, "*.postman_collection files", "*.postman_collection"); - if (file == null) { - return; - } - String json = FileUtils.toString(file); - List items = PostmanUtils.readPostmanJson(json); - featureName = FileUtils.removeFileExtension(file.getName()); - String text = PostmanUtils.toKarateFeature(featureName, items); - feature = FeatureParser.parseText(null, text); - initUi(stage); - }); - importMenu.getItems().addAll(importMenuItem); - menuBar.getMenus().addAll(fileMenu, importMenu); - rootPane.setTop(menuBar); - } - - @Override - public void start(Stage stage) throws Exception { - String fileName; - List params = getParameters().getUnnamed(); - env = System.getProperty(ScriptBindings.KARATE_ENV); - if (!params.isEmpty()) { - fileName = params.get(0); - if (params.size() > 1) { - env = params.get(1); - } - } else { - fileName = null; - } - if (fileName != null) { - File file = new File(fileName); - feature = FeatureParser.parse(file); - workingDir = file.getAbsoluteFile().getParentFile(); - } - initUi(stage); - Scene scene = new Scene(rootPane, 1080, 720); - stage.setScene(scene); - stage.setTitle("Karate UI"); - stage.getIcons().add(new Image(getClass().getClassLoader().getResourceAsStream(KARATE_LOGO))); - setDockIconForMac(); - stage.show(); - } - - private void setDockIconForMac() { - if (FileUtils.isOsMacOsX()) { - try { - ImageIcon icon = new ImageIcon(getClass().getClassLoader().getResource(KARATE_LOGO)); - // com.apple.eawt.Application.getApplication().setDockIconImage(icon.getImage()); - // TODO help - } catch (Exception e) { - // ignore - } - } - } - - public static void main(String[] args) { - App.launch(args); - } - - public static void run(String featurePath, String env) { - App.launch(new String[]{featurePath, env}); - } - -} diff --git a/karate-ui/src/main/java/com/intuit/karate/ui/AppSession.java b/karate-ui/src/main/java/com/intuit/karate/ui/AppSession.java deleted file mode 100644 index b07962b0c..000000000 --- a/karate-ui/src/main/java/com/intuit/karate/ui/AppSession.java +++ /dev/null @@ -1,155 +0,0 @@ -/* - * The MIT License - * - * Copyright 2018 Intuit Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package com.intuit.karate.ui; - -import com.intuit.karate.CallContext; -import com.intuit.karate.Logger; -import com.intuit.karate.core.ExecutionContext; -import com.intuit.karate.core.Feature; -import com.intuit.karate.core.FeatureContext; -import com.intuit.karate.core.FeatureExecutionUnit; -import com.intuit.karate.core.FeatureParser; -import com.intuit.karate.core.ScenarioExecutionUnit; - -import java.io.File; -import java.util.ArrayList; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.List; - -import javafx.concurrent.Task; -import javafx.scene.layout.BorderPane; - -/** - * - * @author pthomas3 - */ -public class AppSession { - - private final Logger logger = new Logger(); - private final ExecutionContext exec; - private final FeatureExecutionUnit featureUnit; - - private final BorderPane rootPane; - private final File workingDir; - private final FeatureOutlinePanel featureOutlinePanel; - private final LogPanel logPanel; - - private final List scenarioPanels; - - private ScenarioExecutionUnit currentlyExecutingScenario; - - public AppSession(BorderPane rootPane, File workingDir, File featureFile, String env) { - this(rootPane, workingDir, FeatureParser.parse(featureFile), env); - } - - public AppSession(BorderPane rootPane, File workingDir, String featureText, String env) { - this(rootPane, workingDir, FeatureParser.parseText(null, featureText), env); - } - - public AppSession(BorderPane rootPane, File workingDir, Feature feature, String envString) { - this(rootPane, workingDir, feature, envString, new CallContext(null, true)); - } - - public AppSession(BorderPane rootPane, File workingDir, Feature feature, String env, CallContext callContext) { - this.rootPane = rootPane; - this.workingDir = workingDir; - logPanel = new LogPanel(logger); - FeatureContext featureContext = FeatureContext.forFeatureAndWorkingDir(env, feature, workingDir); - exec = new ExecutionContext(System.currentTimeMillis(), featureContext, callContext, null, null, null); - featureUnit = new FeatureExecutionUnit(exec); - featureUnit.init(logger); - featureOutlinePanel = new FeatureOutlinePanel(this); - DragResizer.makeResizable(featureOutlinePanel, false, false, false, true); - List units = featureUnit.getScenarioExecutionUnits(); - scenarioPanels = new ArrayList(units.size()); - units.forEach(unit -> scenarioPanels.add(new ScenarioPanel(this, unit))); - rootPane.setLeft(featureOutlinePanel); - DragResizer.makeResizable(logPanel, false, false, true, false); - rootPane.setBottom(logPanel); - } - - public void resetAll() { - scenarioPanels.forEach(scenarioPanel -> scenarioPanel.reset()); - } - - public void runAll() { - ExecutorService scenarioExecutorService = Executors.newSingleThreadExecutor(); - Task runAllTask = new Task() { - @Override - protected Boolean call() throws Exception { - for (ScenarioPanel scenarioPanel : scenarioPanels) { - setCurrentlyExecutingScenario(scenarioPanel.getScenarioExecutionUnit()); - scenarioPanel.runAll(scenarioExecutorService); - } - return true; - } - }; - scenarioExecutorService.submit(runAllTask); - } - - public BorderPane getRootPane() { - return rootPane; - } - - public FeatureOutlinePanel getFeatureOutlinePanel() { - return featureOutlinePanel; - } - - public List getScenarioPanels() { - return scenarioPanels; - } - - public void setCurrentlyExecutingScenario(ScenarioExecutionUnit unit) { - this.currentlyExecutingScenario = unit; - } - - public ScenarioExecutionUnit getCurrentlyExecutingScenario() { - return currentlyExecutingScenario; - } - - public void setSelectedScenario(int index) { - if (index == -1 || scenarioPanels == null || index > scenarioPanels.size() || scenarioPanels.isEmpty()) { - return; - } - rootPane.setCenter(scenarioPanels.get(index)); - } - - public FeatureExecutionUnit getFeatureExecutionUnit() { - return featureUnit; - } - - public List getScenarioExecutionUnits() { - return featureUnit.getScenarioExecutionUnits(); - } - - public void logVar(Var var) { - logPanel.append(var.toString()); - } - - public File getWorkingDir() { - return workingDir; - } - -} diff --git a/karate-ui/src/main/java/com/intuit/karate/ui/ConsolePanel.java b/karate-ui/src/main/java/com/intuit/karate/ui/ConsolePanel.java deleted file mode 100644 index b93dca076..000000000 --- a/karate-ui/src/main/java/com/intuit/karate/ui/ConsolePanel.java +++ /dev/null @@ -1,131 +0,0 @@ -package com.intuit.karate.ui; - -import com.intuit.karate.core.FeatureParser; -import com.intuit.karate.core.Result; -import com.intuit.karate.core.ScenarioExecutionUnit; -import com.intuit.karate.core.Step; -import com.intuit.karate.core.StepResult; - -import javafx.geometry.Insets; -import javafx.geometry.Pos; -import javafx.scene.control.Button; -import javafx.scene.control.CheckBox; -import javafx.scene.paint.Color; -import javafx.scene.text.Font; -import javafx.scene.control.Label; -import javafx.scene.control.TextArea; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.HBox; - -/** - * - * @author babusekaran - */ -public class ConsolePanel extends BorderPane { - - private final AppSession session; - private final ScenarioPanel scenarioPanel; - private final ScenarioExecutionUnit unit; - private final TextArea textArea; - private final Label resultLabel; - private final Step step; - private final int index; - private final String consolePlaceHolder = "Enter your step here for debugging..."; - private final String idle = "●"; - private final String syntaxError = "● syntax error"; - private final String passed = "● passed"; - private final String failed = "● failed"; - private String text; - private boolean stepModified = false; - private boolean stepParseSuccess = false; - private final CheckBox preStepEnabled = new CheckBox("pre step"); - - public ConsolePanel(AppSession session, ScenarioPanel scenarioPanel) { - this.session = session; - this.unit = scenarioPanel.getScenarioExecutionUnit(); - // Creating a dummy step for console - this.index = unit.scenario.getIndex() + 1; - this.step = new Step(unit.scenario.getFeature(), unit.scenario, index); - this.scenarioPanel = scenarioPanel; - setPadding(App.PADDING_ALL); - Label consoleLabel = new Label("Console"); - consoleLabel.setStyle("-fx-font-weight: bold"); - consoleLabel.setPadding(new Insets(0, 0, 3.0, 3.0)); - setTop(consoleLabel); - setPadding(App.PADDING_ALL); - textArea = new TextArea(); - textArea.setFont(App.getDefaultFont()); - textArea.setWrapText(true); - textArea.setMinHeight(0); - textArea.setPromptText(consolePlaceHolder); - text = ""; - resultLabel = new Label(idle); - resultLabel.setTextFill(Color.web("#8c8c8c")); - resultLabel.setPadding(new Insets(3.0, 0, 0, 0)); - resultLabel.setFont(new Font(15)); - textArea.focusedProperty().addListener((val, before, after) -> { - if (!after) { // if we lost focus - String temp = textArea.getText(); - if (!text.equals(temp) && !temp.trim().equals("")) { - text = temp; - stepParseSuccess = FeatureParser.updateStepFromText(step, text); - if (!stepParseSuccess) { - resultLabel.setText(syntaxError); - resultLabel.setTextFill(Color.web("#D52B1E")); - } else { - resultLabel.setText(idle); - resultLabel.setTextFill(Color.web("#8c8c8c")); - stepModified = true; - } - } - } - }); - setCenter(textArea); - Button runButton = new Button("Run Code"); - runButton.setOnAction(e -> { - if (stepModified) { - if (!stepParseSuccess) { - resultLabel.setText(syntaxError); - resultLabel.setTextFill(Color.web("#D52B1E")); - } else { - if (run().getStatus().equals("passed")) { - resultLabel.setText(passed); - resultLabel.setTextFill(Color.web("#53B700")); - } else { - resultLabel.setText(failed); - resultLabel.setTextFill(Color.web("#D52B1E")); - } - } - } - }); - Button clearButton = new Button("Clear"); - clearButton.setOnAction(e -> refresh()); - HBox hbox = new HBox(App.PADDING); - hbox.setSpacing(5); - hbox.setAlignment(Pos.BASELINE_LEFT); - hbox.getChildren().addAll(runButton, resultLabel, clearButton, preStepEnabled); - setBottom(hbox); - setMargin(hbox, App.PADDING_TOP); - } - - public void runIfPreStepEnabled() { - if (preStepEnabled.isSelected()) { - run(); - } - } - - public Result run() { - StepResult sr = unit.execute(step); - unit.result.setStepResult(index, sr); - scenarioPanel.refreshVars(); - return sr.getResult(); - } - - public void refresh() { - textArea.clear(); - text = ""; - resultLabel.setText(idle); - resultLabel.setTextFill(Color.web("#8c8c8c")); - } - -} diff --git a/karate-ui/src/main/java/com/intuit/karate/ui/DragResizer.java b/karate-ui/src/main/java/com/intuit/karate/ui/DragResizer.java deleted file mode 100644 index b12fda1a3..000000000 --- a/karate-ui/src/main/java/com/intuit/karate/ui/DragResizer.java +++ /dev/null @@ -1,361 +0,0 @@ -package com.intuit.karate.ui; - -import javafx.event.EventHandler; -import javafx.scene.Cursor; -import javafx.scene.input.MouseEvent; -import javafx.scene.layout.Region; - -/** - * {@link DragResizer} can be used to add mouse listeners to a {@link Region} - * and make it moveable and resizable by the user by clicking and dragging the - * border in the same way as a window, or clicking within the window to move it - * around. - *

- * Height and width resizing is implemented, from the sides and corners. - * Dragging of the region is also optionally available, and the movement and - * resizing can be constrained within the bounds of the parent. - *

- * Usage: - *

DragResizer.makeResizable(myAnchorPane, true, true, true, true);
makes the - * region resizable for hight and width and moveable, but only within the bounds of the parent. - *

- * Builds on the modifications to the original version by - * Geoff Capper. - *

- * - */ -public class DragResizer { - - /** - * Enum containing the zones that we can drag around. - */ - enum Zone { - NONE, N, NE, E, SE, S, SW, W, NW, C - } - - /** - * The margin around the control that a user can click in to start resizing - * the region. - */ - private final int RESIZE_MARGIN = 5; - - /** - * How small can we go? - */ - private final int MIN_SIZE = 10; - - private Region region; - - private double y; - - private double x; - - private boolean initMinHeight; - - private boolean initMinWidth; - - private Zone zone; - - private boolean dragging; - - /** - * Whether the sizing and movement of the region is constrained within the - * bounds of the parent. - */ - private boolean constrainToParent; - - /** - * Whether dragging of the region is allowed. - */ - private boolean allowMove; - - /** - * Whether resizing of height is allowed. - */ - private boolean allowHeightResize; - - /** - * Whether resizing of width is allowed. - */ - private boolean allowWidthResize; - - private DragResizer(Region aRegion, boolean allowMove, boolean constrainToParent, boolean allowHeightResize, boolean allowWidthResize) { - region = aRegion; - this.constrainToParent = constrainToParent; - this.allowMove = allowMove; - this.allowHeightResize = allowHeightResize; - this.allowWidthResize = allowWidthResize; - } - - - /** - * Makes the region resizable, and optionally moveable, and constrained - * within the bounds of the parent. - * - * @param region - * @param allowMove Allow a click in the centre of the region to start - * dragging it around. - * @param constrainToParent Prevent movement and/or resizing outside the - * @param allowHeightResize if set to true makes component height resizeAble - * @param allowWidthResize if set to true makes component width resizeAble - * parent. - */ - public static void makeResizable(Region region, boolean allowMove, boolean constrainToParent, boolean allowHeightResize, boolean allowWidthResize) { - final DragResizer resizer = new DragResizer(region, allowMove, constrainToParent, allowHeightResize, allowWidthResize); - - region.setOnMousePressed(new EventHandler() { - @Override - public void handle(MouseEvent event) { - resizer.mousePressed(event); - } - }); - region.setOnMouseDragged(new EventHandler() { - @Override - public void handle(MouseEvent event) { - resizer.mouseDragged(event); - } - }); - region.setOnMouseMoved(new EventHandler() { - @Override - public void handle(MouseEvent event) { - resizer.mouseOver(event); - } - }); - region.setOnMouseReleased(new EventHandler() { - @Override - public void handle(MouseEvent event) { - resizer.mouseReleased(event); - } - }); - } - - protected void mouseReleased(MouseEvent event) { - dragging = false; - region.setCursor(Cursor.DEFAULT); - } - - protected void mouseOver(MouseEvent event) { - if (isInDraggableZone(event) || dragging) { - switch (zone) { - case N: { - region.setCursor(Cursor.N_RESIZE); - break; - } - case NE: { - region.setCursor(Cursor.NE_RESIZE); - break; - } - case E: { - region.setCursor(Cursor.E_RESIZE); - break; - } - case SE: { - region.setCursor(Cursor.SE_RESIZE); - break; - } - case S: { - region.setCursor(Cursor.S_RESIZE); - break; - } - case SW: { - region.setCursor(Cursor.SW_RESIZE); - break; - } - case W: { - region.setCursor(Cursor.W_RESIZE); - break; - } - case NW: { - region.setCursor(Cursor.NW_RESIZE); - break; - } - case C: { - region.setCursor(Cursor.MOVE); - break; - } - } - - } else { - region.setCursor(Cursor.DEFAULT); - } - } - - protected boolean isInDraggableZone(MouseEvent event) { - zone = Zone.NONE; - if(allowWidthResize) { - if ((event.getY() < RESIZE_MARGIN) && (event.getX() < RESIZE_MARGIN)) { - zone = Zone.NW; - } else if ((event.getY() < RESIZE_MARGIN) && (event.getX() > (region.getWidth() - RESIZE_MARGIN))) { - zone = Zone.NE; - } else if ((event.getY() > (region.getHeight() - RESIZE_MARGIN)) && (event.getX() > (region.getWidth() - RESIZE_MARGIN))) { - zone = Zone.SE; - } else if ((event.getY() > (region.getHeight() - RESIZE_MARGIN)) && (event.getX() < RESIZE_MARGIN)) { - zone = Zone.SW; - } else if (event.getX() < RESIZE_MARGIN) { - zone = Zone.W; - } else if (event.getX() > (region.getWidth() - RESIZE_MARGIN)) { - zone = Zone.E; - } - } else if (allowHeightResize) { - if (event.getY() > (region.getHeight() - RESIZE_MARGIN)) { - zone = Zone.S; - } else if (event.getY() < RESIZE_MARGIN) { - zone = Zone.N; - } - } else if (allowMove) { - zone = Zone.C; - } - return !Zone.NONE.equals(zone); - - } - - protected void mouseDragged(MouseEvent event) { - if (!dragging) { - return; - } - - double deltaY = allowHeightResize ? event.getSceneY() - y : 0; - double deltaX = allowWidthResize ? event.getSceneX() - x : 0; - - double originY = region.getLayoutY(); - double originX = region.getLayoutX(); - - double newHeight = region.getMinHeight(); - double newWidth = region.getMinWidth(); - - switch (zone) { - case N: { - originY += deltaY; - newHeight -= deltaY; - break; - } - case NE: { - originY += deltaY; - newHeight -= deltaY; - newWidth += deltaX; - break; - } - case E: { - newWidth += deltaX; - break; - } - case SE: { - newHeight += deltaY; - newWidth += deltaX; - break; - } - case S: { - newHeight += deltaY; - break; - } - case SW: { - originX += deltaX; - newHeight += deltaY; - newWidth -= deltaX; - break; - } - case W: { - originX += deltaX; - newWidth -= deltaX; - break; - } - case NW: { - originY += deltaY; - originX += deltaX; - newWidth -= deltaX; - newHeight -= deltaY; - break; - } - case C: { - originY += deltaY; - originX += deltaX; - break; - } - } - - if (constrainToParent) { - - if (originX < 0) { - if (!Zone.C.equals(zone)) { - newWidth -= Math.abs(originX); - } - originX = 0; - } - if (originY < 0) { - if (!Zone.C.equals(zone)) { - newHeight -= Math.abs(originY); - } - originY = 0; - } - - if (Zone.C.equals(zone)) { - if ((newHeight + originY) > region.getParent().getBoundsInLocal().getHeight()) { - originY = region.getParent().getBoundsInLocal().getHeight() - newHeight; - } - if ((newWidth + originX) > region.getParent().getBoundsInLocal().getWidth()) { - originX = region.getParent().getBoundsInLocal().getWidth() - newWidth; - } - } else { - if ((newHeight + originY) > region.getParent().getBoundsInLocal().getHeight()) { - newHeight = region.getParent().getBoundsInLocal().getHeight() - originY; - } - if ((newWidth + originX) > region.getParent().getBoundsInLocal().getWidth()) { - newWidth = region.getParent().getBoundsInLocal().getWidth() - originX; - } - } - } - if (newWidth < MIN_SIZE) { - newWidth = MIN_SIZE; - } - if (newHeight < MIN_SIZE) { - newHeight = MIN_SIZE; - } - - if (!Zone.C.equals(zone)) { - // need to set Pref Height/Width otherwise they act as minima. - if(allowHeightResize) { - region.setMinHeight(newHeight); - region.setPrefHeight(newHeight); - } - if(allowWidthResize) { - region.setMinWidth(newWidth); - region.setPrefWidth(newWidth); - } - } - if(allowMove) { - region.relocate(originX, originY); - } - - y = allowHeightResize ? event.getSceneY() : y; - x = allowWidthResize ? event.getSceneX() : x; - - } - - protected void mousePressed(MouseEvent event) { - - // ignore clicks outside of the draggable margin - if (!isInDraggableZone(event)) { - return; - } - - dragging = true; - - // make sure that the minimum height is set to the current height once, - // setting a min height that is smaller than the current height will - // have no effect - if (!initMinHeight) { - region.setMinHeight(region.getHeight()); - initMinHeight = true; - } - - y = event.getSceneY(); - - if (!initMinWidth) { - region.setMinWidth(region.getWidth()); - initMinWidth = true; - } - - x = event.getSceneX(); - } - -} \ No newline at end of file diff --git a/karate-ui/src/main/java/com/intuit/karate/ui/FeatureOutlinePanel.java b/karate-ui/src/main/java/com/intuit/karate/ui/FeatureOutlinePanel.java deleted file mode 100644 index a64247221..000000000 --- a/karate-ui/src/main/java/com/intuit/karate/ui/FeatureOutlinePanel.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * The MIT License - * - * Copyright 2018 Intuit Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package com.intuit.karate.ui; - -import com.intuit.karate.core.Feature; -import com.intuit.karate.core.ScenarioExecutionUnit; - -import java.nio.file.Path; -import java.util.List; - -import javafx.application.Platform; -import javafx.collections.FXCollections; -import javafx.scene.control.Button; -import javafx.scene.control.Label; -import javafx.scene.control.ListView; -import javafx.scene.control.ScrollPane; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.HBox; -import javafx.scene.layout.VBox; - -/** - * - * @author pthomas3 - */ -public class FeatureOutlinePanel extends BorderPane { - - private final AppSession session; - private ListView listView; - private final ScrollPane scrollPane; - private final List units; - - public FeatureOutlinePanel(AppSession session) { - this.session = session; - this.units = session.getScenarioExecutionUnits(); - setPadding(App.PADDING_HOR); - scrollPane = new ScrollPane(); - scrollPane.setFitToWidth(true); - scrollPane.setFitToHeight(true); - VBox header = new VBox(App.PADDING); - header.setPadding(App.PADDING_VER); - setTop(header); - Feature feature = session.getFeatureExecutionUnit().exec.featureContext.feature; - Path path = feature.getPath(); - Label featureLabel = new Label(path == null ? "" : path.getFileName().toString()); - header.getChildren().add(featureLabel); - HBox hbox = new HBox(App.PADDING); - header.getChildren().add(hbox); - Button resetButton = new Button("Reset"); - resetButton.setOnAction(e -> session.resetAll()); - Button runAllButton = new Button("Run All Scenarios"); - hbox.getChildren().add(resetButton); - hbox.getChildren().add(runAllButton); - setCenter(scrollPane); - refresh(); - Platform.runLater(() -> { - listView.getSelectionModel().select(0); - listView.requestFocus(); - }); - runAllButton.setOnAction(e -> Platform.runLater(() -> session.runAll())); - } - - public void refresh() { - // unless we do ALL of this - the custom cell rendering has problems in javafx - // and starts duplicating the last row for some reason, spent a lot of time on this :( - listView = new ListView(); - listView.setItems(FXCollections.observableArrayList(units)); - listView.setCellFactory(lv -> new FeatureOutlineCell()); - Platform.runLater(() -> { - scrollPane.setContent(listView); - }); - listView.getSelectionModel() - .selectedIndexProperty() - .addListener((o, prev, value) -> session.setSelectedScenario(value.intValue())); - } - -} diff --git a/karate-ui/src/main/java/com/intuit/karate/ui/ScenarioPanel.java b/karate-ui/src/main/java/com/intuit/karate/ui/ScenarioPanel.java deleted file mode 100644 index e4cf3c6d6..000000000 --- a/karate-ui/src/main/java/com/intuit/karate/ui/ScenarioPanel.java +++ /dev/null @@ -1,210 +0,0 @@ -/* - * The MIT License - * - * Copyright 2018 Intuit Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package com.intuit.karate.ui; - -import com.intuit.karate.core.ScenarioContext; -import com.intuit.karate.core.ScenarioExecutionUnit; -import com.intuit.karate.core.Step; -import com.intuit.karate.core.StepResult; - -import java.util.ArrayList; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.List; - -import javafx.application.Platform; -import javafx.concurrent.Task; -import javafx.scene.control.Button; -import javafx.scene.control.Label; -import javafx.scene.control.ScrollPane; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.HBox; -import javafx.scene.layout.VBox; - -/** - * - * @author pthomas3 - */ -public class ScenarioPanel extends BorderPane { - - private final AppSession session; - private final ScenarioExecutionUnit unit; - private final VBox content; - private final VarsPanel varsPanel; - protected final ConsolePanel consolePanel; - - private final List stepPanels; - private StepPanel lastStep; - - public ScenarioExecutionUnit getScenarioExecutionUnit() { - return unit; - } - - private final ScenarioContext initialContext; - private int index; - - public ScenarioPanel(AppSession session, ScenarioExecutionUnit unit) { - this.session = session; - this.unit = unit; - unit.init(); - initialContext = unit.getActions().context.copy(); - content = new VBox(App.PADDING); - ScrollPane scrollPane = new ScrollPane(content); - scrollPane.setFitToWidth(true); - setCenter(scrollPane); - VBox header = new VBox(App.PADDING); - header.setPadding(App.PADDING_VER); - setTop(header); - String headerText = "Scenario: " + unit.scenario.getDisplayMeta() + " " + unit.scenario.getName(); - Label headerLabel = new Label(headerText); - header.getChildren().add(headerLabel); - HBox hbox = new HBox(App.PADDING); - header.getChildren().add(hbox); - Button resetButton = new Button("Reset"); - resetButton.setOnAction(e -> reset()); - Button runAllButton = new Button("Run All Steps"); - runAllButton.setOnAction(e -> Platform.runLater(() -> runAll())); - hbox.getChildren().add(resetButton); - hbox.getChildren().add(runAllButton); - stepPanels = new ArrayList(); - unit.getSteps().forEach(step -> addStepPanel(step)); - if (lastStep != null) { - lastStep.setLast(true); - } - VBox vbox = new VBox(App.PADDING); - varsPanel = new VarsPanel(session, this); - vbox.getChildren().add(varsPanel); - consolePanel = new ConsolePanel(session, this); - vbox.getChildren().add(consolePanel); - setRight(vbox); - DragResizer.makeResizable(vbox, false, false, false, true); - DragResizer.makeResizable(consolePanel, false, false, true, false); - reset(); // clear any background results if dynamic scenario - } - - private void addStepPanel(Step step) { - lastStep = new StepPanel(session, this, step, index++); - content.getChildren().add(lastStep); - stepPanels.add(lastStep); - } - - public void refreshVars() { - varsPanel.refresh(); - } - - public void runAll() { - reset(); - ExecutorService scenarioExecutorService = Executors.newSingleThreadExecutor(); - Task runAllTask = new Task() { - @Override - protected Boolean call() throws Exception { - disableAllSteps(); - for (StepPanel step : stepPanels) { - if (step.run(true)) { - enableAllSteps(); - break; - } - } - unit.setExecuted(true); - return true; - } - }; - runAllTask.setOnSucceeded(onSuccess -> { - Platform.runLater(() -> { - session.getFeatureOutlinePanel().refresh(); - }); - }); - scenarioExecutorService.submit(runAllTask); - } - - public void runAll(ExecutorService stepExecutorService) { - reset(); - Task runAllTask = new Task() { - @Override - protected Boolean call() throws Exception { - disableAllSteps(); - for (StepPanel step : stepPanels) { - if (step.run(true)) { - enableAllSteps(); - break; - } - } - unit.setExecuted(true); - return true; - } - }; - runAllTask.setOnSucceeded(onSuccess -> { - Platform.runLater(() -> { - session.getFeatureOutlinePanel().refresh(); - }); - }); - stepExecutorService.submit(runAllTask); - } - - public void runUpto(int index) { - ExecutorService scenarioExecutorService = Executors.newSingleThreadExecutor(); - Task runUptoTask = new Task() { - @Override - protected Boolean call() throws Exception { - disableAllSteps(); - for (StepPanel stepPanel : stepPanels) { - int stepIndex = stepPanel.getIndex(); - StepResult sr = unit.result.getStepResult(stepPanel.getIndex()); - if (sr != null) { - continue; - } - if (stepPanel.run(true) || stepIndex == index) { - break; - } - } - enableAllSteps(); - return true; - } - }; - runUptoTask.setOnSucceeded(onSuccess -> { - Platform.runLater(() -> { - session.getFeatureOutlinePanel().refresh(); - }); - }); - scenarioExecutorService.submit(runUptoTask); - } - - public void reset() { - unit.reset(initialContext.copy()); - refreshVars(); - for (StepPanel stepPanel : stepPanels) { - stepPanel.initStyles(); - } - session.getFeatureOutlinePanel().refresh(); - } - - public void enableAllSteps() { - stepPanels.forEach(step -> {step.enableRun();}); - } - - public void disableAllSteps() { - stepPanels.forEach(step -> {step.disableRun();}); - } - -} diff --git a/karate-ui/src/main/java/com/intuit/karate/ui/StepPanel.java b/karate-ui/src/main/java/com/intuit/karate/ui/StepPanel.java deleted file mode 100644 index cf7c8ed11..000000000 --- a/karate-ui/src/main/java/com/intuit/karate/ui/StepPanel.java +++ /dev/null @@ -1,189 +0,0 @@ -/* - * The MIT License - * - * Copyright 2018 Intuit Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package com.intuit.karate.ui; - -import com.intuit.karate.StringUtils; -import com.intuit.karate.core.FeatureParser; -import com.intuit.karate.core.FeatureResult; -import com.intuit.karate.core.ScenarioExecutionUnit; -import com.intuit.karate.core.Step; -import com.intuit.karate.core.StepResult; -import javafx.application.Platform; -import javafx.scene.Scene; -import javafx.scene.control.MenuItem; -import javafx.scene.control.SplitMenuButton; -import javafx.scene.control.TextArea; -import javafx.scene.layout.AnchorPane; -import javafx.scene.layout.BorderPane; -import javafx.stage.Stage; - -/** - * - * @author pthomas3 - */ -public class StepPanel extends AnchorPane { - - private final AppSession session; - private final ScenarioPanel scenarioPanel; - private final ScenarioExecutionUnit unit; - private final Step step; - private final SplitMenuButton runButton; - private final int index; - - private String text; - private boolean last; - - private static final String STYLE_PASS = "-fx-base: #53B700"; - private static final String STYLE_FAIL = "-fx-base: #D52B1E"; - private static final String STYLE_METHOD = "-fx-base: #34BFFF"; - private static final String STYLE_DEFAULT = "-fx-base: #F0F0F0"; - private static final String STYLE_BACKGROUND = "-fx-text-fill: #8D9096"; - - public int getIndex() { - return index; - } - - public boolean isLast() { - return last; - } - - public void setLast(boolean last) { - this.last = last; - } - - private final MenuItem runMenuItem; - private final MenuItem calledMenuItem; - private boolean showCalled; - - private String getCalledMenuText() { - return showCalled ? "hide called" : "show called"; - } - - private String getRunButtonText() { - if (showCalled) { - return "►►"; - } else { - return "►"; - } - } - - public StepPanel(AppSession session, ScenarioPanel scenarioPanel, Step step, int index) { - this.session = session; - this.unit = scenarioPanel.getScenarioExecutionUnit(); - this.scenarioPanel = scenarioPanel; - this.step = step; - this.index = index; - TextArea textArea = new TextArea(); - textArea.setFont(App.getDefaultFont()); - textArea.setWrapText(true); - textArea.setMinHeight(0); - text = step.toString(); - int lines = StringUtils.wrappedLinesEstimate(text, 30); - textArea.setText(text); - textArea.setPrefRowCount(lines); - textArea.focusedProperty().addListener((val, before, after) -> { - if (!after) { // if we lost focus - String temp = textArea.getText(); - if (!text.equals(temp)) { - text = temp; - FeatureParser.updateStepFromText(step, text); - } - } - }); - runMenuItem = new MenuItem("run upto"); - calledMenuItem = new MenuItem(getCalledMenuText()); - runButton = new SplitMenuButton(runMenuItem, calledMenuItem); - runMenuItem.setOnAction(e -> Platform.runLater(() -> scenarioPanel.runUpto(index))); - calledMenuItem.setOnAction(e -> { - showCalled = !showCalled; - calledMenuItem.setText(getCalledMenuText()); - runButton.setText(getRunButtonText()); - }); - runButton.setText(getRunButtonText()); - runButton.setOnAction(e -> { - if (FeatureParser.updateStepFromText(step, text)) { - Platform.runLater(() -> run(false)); - } else { - runButton.setStyle(STYLE_FAIL); - } - }); - // layout - setLeftAnchor(textArea, 0.0); - setRightAnchor(textArea, 32.0); - setBottomAnchor(textArea, 0.0); - setRightAnchor(runButton, 3.0); - setTopAnchor(runButton, 0.0); - setBottomAnchor(runButton, 0.0); - // add - getChildren().addAll(textArea, runButton); - initStyles(); - } - - public void initStyles() { - StepResult sr = unit.result.getStepResult(index); - if (sr == null) { - runButton.setStyle(""); - } else if (sr.getResult().getStatus().equals("passed")) { - runButton.setStyle(STYLE_PASS); - } else { - runButton.setStyle(STYLE_FAIL); - } - enableRun(); - } - - public boolean run(boolean nonStop) { - if (!nonStop && showCalled) { - unit.getContext().setCallable(callContext -> { - AppSession calledSession = new AppSession(new BorderPane(), session.getWorkingDir(), callContext.feature, null, callContext); - Stage stage = new Stage(); - stage.setTitle(callContext.feature.getRelativePath()); - stage.setScene(new Scene(calledSession.getRootPane(), 700, 450)); - stage.showAndWait(); - FeatureResult result = calledSession.getFeatureExecutionUnit().exec.result; - result.setResultVars(calledSession.getCurrentlyExecutingScenario().getContext().vars); - return result; - }); - } else { - unit.getContext().setCallable(null); - } - if (!nonStop) { - scenarioPanel.consolePanel.runIfPreStepEnabled(); - } - StepResult stepResult = unit.execute(step); - unit.result.setStepResult(index, stepResult); - session.setCurrentlyExecutingScenario(unit); - initStyles(); - scenarioPanel.refreshVars(); - return stepResult.isStopped(); - } - - public void disableRun() { - this.runButton.setDisable(true); - } - - public void enableRun() { - this.runButton.setDisable(false); - } - -} diff --git a/karate-ui/src/main/java/com/intuit/karate/ui/VarsPanel.java b/karate-ui/src/main/java/com/intuit/karate/ui/VarsPanel.java deleted file mode 100644 index c46f15a3e..000000000 --- a/karate-ui/src/main/java/com/intuit/karate/ui/VarsPanel.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * The MIT License - * - * Copyright 2017 Intuit Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package com.intuit.karate.ui; - -import com.intuit.karate.ScriptValue; -import com.intuit.karate.ScriptValueMap; -import com.intuit.karate.core.ScenarioContext; -import javafx.beans.property.ReadOnlyObjectWrapper; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; -import javafx.scene.control.TableColumn; -import javafx.scene.control.TableRow; -import javafx.scene.control.TableView; -import javafx.scene.control.cell.PropertyValueFactory; -import javafx.scene.layout.BorderPane; - -import java.util.ArrayList; -import java.util.List; - -/** - * - * @author pthomas3 - */ -public class VarsPanel extends BorderPane { - - private final AppSession session; - private final TableView table; - private final ScenarioPanel scenarioPanel; - - public VarsPanel(AppSession session, ScenarioPanel scenarioPanel) { - this.session = session; - this.scenarioPanel = scenarioPanel; - this.setPadding(App.PADDING_HOR); - table = new TableView(); - table.setPrefWidth(280); - table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); - setCenter(table); - TableColumn nameCol = new TableColumn("Variable"); - nameCol.setCellValueFactory(new PropertyValueFactory("name")); - nameCol.setCellFactory(c -> new StringTooltipCell()); - TableColumn typeCol = new TableColumn("Type"); - typeCol.setMinWidth(45); - typeCol.setMaxWidth(60); - typeCol.setCellValueFactory(new PropertyValueFactory("type")); - TableColumn valueCol = new TableColumn("Value"); - valueCol.setCellValueFactory(c -> new ReadOnlyObjectWrapper(c.getValue().getValue())); - valueCol.setCellFactory(c -> new VarValueCell()); - table.getColumns().addAll(nameCol, typeCol, valueCol); - table.setItems(getVarList()); - table.setRowFactory(tv -> { - TableRow row = new TableRow<>(); - row.setOnMouseClicked(e -> { - if (e.getClickCount() == 2 && !row.isEmpty()) { - Var var = row.getItem(); - session.logVar(var); - } - }); - return row ; - }); - } - - private ObservableList getVarList() { - ScenarioContext context = scenarioPanel.getScenarioExecutionUnit().getActions().context; - ScriptValueMap vars = context.vars; - List list = new ArrayList(vars.size()); - context.vars.forEach((k, v) -> list.add(new Var(k, v))); - return FXCollections.observableList(list); - } - - public void refresh() { - table.setItems(getVarList()); - table.refresh(); - } - -} diff --git a/karate-ui/src/test/java/com/intuit/karate/ui/UiRunner.java b/karate-ui/src/test/java/com/intuit/karate/ui/UiRunner.java deleted file mode 100644 index d52645f55..000000000 --- a/karate-ui/src/test/java/com/intuit/karate/ui/UiRunner.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.intuit.karate.ui; - -import org.junit.Test; - -/** - * - * @author pthomas3 - */ -public class UiRunner { - - @Test - public void testDevUi() { - App.run("src/test/java/com/intuit/karate/ui/test.feature", null); - } - -} diff --git a/karate-ui/src/test/java/com/intuit/karate/ui/test.feature b/karate-ui/src/test/java/com/intuit/karate/ui/test.feature deleted file mode 100644 index c365ac32a..000000000 --- a/karate-ui/src/test/java/com/intuit/karate/ui/test.feature +++ /dev/null @@ -1,33 +0,0 @@ -Feature: test feature - -Background: -* def a = 1 -* def b = 2 - -Scenario: test scenario -* assert a + b == 3 - -Scenario: test multi line -* def foo = -""" -{ - hello: 'world' -} -""" -* match foo == { hello: '#string' } - -Scenario: test wrapping line -* def xml = succeed 8008 2017-04-03 20:29:58 CDT 2017-03-21 12:23:55 CDT Red Hat Enterprise Linux 6 2.6.32-573.12.1.el6.x86_64, 64 Bit, x86_64 R04M001170316 20170131131718 2017-04-03 20:25:00 CDT -* def count = get xml count(/response/records//record) -* assert count == 1 -* match xml/response/result == 'succeed' - -Scenario Outline: test outline -* def c = -* def d = 2 -* assert c + d == - -Examples: -| foo | bar | -| 1 | 3 | -| 2 | 4 | diff --git a/karate-ui/src/test/java/com/intuit/karate/ui/threadtest.feature b/karate-ui/src/test/java/com/intuit/karate/ui/threadtest.feature deleted file mode 100644 index 32c3a4cd1..000000000 --- a/karate-ui/src/test/java/com/intuit/karate/ui/threadtest.feature +++ /dev/null @@ -1,23 +0,0 @@ -@ignore -Feature: Thread Test - -Scenario: Scenario-1 -* print "Scenario-1 Started" -* string a = 'a' -* string b = 'b' -* string c = 'c' -* print "Scenario-1 Finished" - -Scenario: Scenario-2 -* print "Scenario-2 Started" -* string x = 'x' -* def Thread = Java.type('java.lang.Thread') -# def sleep = Thread.sleep(3000) -* def threadName = Thread.currentThread().getName() -* match threadName contains 'Karate-UI Run' -* print 'current thread is ' + threadName -* string y = 'y' -* string z = 'z' -# def sleep = Thread.sleep(3000) -* string a = 'a' -* print "Scenario-2 Finished" \ No newline at end of file diff --git a/karate-ui/src/test/java/karate-http.properties b/karate-ui/src/test/java/karate-http.properties deleted file mode 100644 index 5aae8c1a7..000000000 --- a/karate-ui/src/test/java/karate-http.properties +++ /dev/null @@ -1,2 +0,0 @@ -client.class=com.intuit.karate.http.DummyHttpClient - diff --git a/pom.xml b/pom.xml index 24e4f7b04..00e318728 100755 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.intuit.karate karate-parent - 1.0.0 + 0.9.5 pom ${project.artifactId} @@ -36,8 +36,9 @@ 1.8 3.6.0 2.22.2 - 3.1.1 + 3.2.1 4.12 + 13-ea+12 1.6.7 4.3.8.RELEASE 1.5.16.RELEASE @@ -46,14 +47,14 @@ - karate-core - karate-apache + karate-core + karate-apache karate-junit4 - karate-junit5 - karate-ui + karate-junit5 karate-netty karate-gatling karate-demo + karate-robot karate-mock-servlet karate-jersey karate-archetype