├── src ├── test │ ├── resources │ │ ├── de │ │ │ └── retest │ │ │ │ └── web │ │ │ │ ├── AttributesConfigDeserializerTest │ │ │ │ ├── empty.yaml │ │ │ │ ├── all-html.yaml │ │ │ │ ├── invalid-html.yaml │ │ │ │ └── selected.yaml │ │ │ │ ├── selenium │ │ │ │ └── UnbreakableDriverIT.html │ │ │ │ ├── it │ │ │ │ ├── tab.html │ │ │ │ ├── page.html │ │ │ │ ├── pseudo-elements.css │ │ │ │ ├── pseudo-element.html │ │ │ │ ├── pseudo-element-changed.html │ │ │ │ ├── pseudo-elements-changed.css │ │ │ │ ├── RootElementPeerNullTest.html │ │ │ │ └── TestHealerCssSelectorIT.html │ │ │ │ └── AutoHealingTest.html │ │ ├── retest │ │ │ ├── recheck │ │ │ │ ├── de.retest.web.it.TabIT │ │ │ │ │ └── recheck.ignore │ │ │ │ ├── de.retest.web.it.CenterIT │ │ │ │ │ ├── center-ChromeDriver.open.recheck │ │ │ │ │ │ └── screenshot │ │ │ │ │ │ │ └── html.png │ │ │ │ │ └── center-FirefoxDriver.open.recheck │ │ │ │ │ │ └── screenshot │ │ │ │ │ │ └── html.png │ │ │ │ ├── de.retest.web.it.TestHealerCssSelectorIT │ │ │ │ │ ├── setup.00.recheck │ │ │ │ │ │ └── screenshot │ │ │ │ │ │ │ └── root.png │ │ │ │ │ └── setup.01.recheck │ │ │ │ │ │ └── screenshot │ │ │ │ │ │ └── root.png │ │ │ │ ├── de.retest.web.it.ShowcaseIT │ │ │ │ │ └── showcase-FirefoxDriver.open.recheck │ │ │ │ │ │ └── screenshot │ │ │ │ │ │ ├── html.png │ │ │ │ │ │ └── html_006f66c7412bad4d92aaec4856af74d3f506bc2df3645f9f1ea953c444b028af.png │ │ │ │ ├── de.retest.web.it.FormPageIT │ │ │ │ │ └── form-page-FirefoxDriver.open.recheck │ │ │ │ │ │ └── screenshot │ │ │ │ │ │ └── html.png │ │ │ │ ├── de.retest.web.it.FramePageIT │ │ │ │ │ ├── frame-page-ChromeDriver.open.recheck │ │ │ │ │ │ └── screenshot │ │ │ │ │ │ │ └── html.png │ │ │ │ │ └── frame-page-FirefoxDriver.open.recheck │ │ │ │ │ │ └── screenshot │ │ │ │ │ │ └── html.png │ │ │ │ ├── de.retest.web.it.SimplePageIT │ │ │ │ │ ├── simple-page-FirefoxDriver.open.recheck │ │ │ │ │ │ └── screenshot │ │ │ │ │ │ │ ├── html.png │ │ │ │ │ │ │ └── html_ccc95f1c9c6803a22f080f151bfacd67a737c35b1cb4f5a2f07b16d30105e204.png │ │ │ │ │ └── simple-page-ChromeDriver.open.recheck │ │ │ │ │ │ └── screenshot │ │ │ │ │ │ └── html_43ab85c02685dcd082f87fef200b9586fd3945be3eac4b16d06d75fa3836136b.png │ │ │ │ ├── de.retest.web.it.SpecialContainerIT │ │ │ │ │ ├── special-container-FirefoxDriver.open.recheck │ │ │ │ │ │ └── screenshot │ │ │ │ │ │ │ ├── html.png │ │ │ │ │ │ │ └── html_7b43151124a12deb468c88e608ba52949773935aec87fd9c24a4d88609b0155c.png │ │ │ │ │ ├── special-container-FirefoxDriver.click.recheck │ │ │ │ │ │ └── screenshot │ │ │ │ │ │ │ ├── html.png │ │ │ │ │ │ │ └── html_7b43151124a12deb468c88e608ba52949773935aec87fd9c24a4d88609b0155c.png │ │ │ │ │ ├── special-container-ChromeDriver.click.recheck │ │ │ │ │ │ └── screenshot │ │ │ │ │ │ │ └── html_f5603547723e5f5c3b9979f8db49ad6b91389981770f6666898b686e5d882b53.png │ │ │ │ │ └── special-container-ChromeDriver.open.recheck │ │ │ │ │ │ └── screenshot │ │ │ │ │ │ └── html_f5603547723e5f5c3b9979f8db49ad6b91389981770f6666898b686e5d882b53.png │ │ │ │ ├── de.retest.web.it.SimpleUnbreakableSeleniumShowcaseIT │ │ │ │ │ └── simple-showcase.index.recheck │ │ │ │ │ │ └── screenshot │ │ │ │ │ │ └── html.png │ │ │ │ ├── de.retest.web.it.WikipediaIT │ │ │ │ │ └── myWikipediaTest.characterization-testing-page.recheck │ │ │ │ │ │ └── screenshot │ │ │ │ │ │ └── html.png │ │ │ │ ├── de.retest.web.it.RecheckRemoteWebElementIT │ │ │ │ │ ├── simple-webelement-FirefoxDriver.open.recheck │ │ │ │ │ │ └── screenshot │ │ │ │ │ │ │ ├── email.png │ │ │ │ │ │ │ └── input_55bf18063eed7a040d95559c1a8689df5118c2d589738e9d491872f3b9153f2d.png │ │ │ │ │ ├── empty-article-ChromeDriver.open.recheck │ │ │ │ │ │ └── screenshot │ │ │ │ │ │ │ └── article_b0398bd8a34b4b9128404e8c7b6ecb99d35238103cddf4bd8bb30363dbca8c84.png │ │ │ │ │ ├── empty-article-FirefoxDriver.open.recheck │ │ │ │ │ │ ├── screenshot │ │ │ │ │ │ │ └── article_55bf18063eed7a040d95559c1a8689df5118c2d589738e9d491872f3b9153f2d.png │ │ │ │ │ │ └── retest.xml │ │ │ │ │ ├── simple-webelement-ChromeDriver.open.recheck │ │ │ │ │ │ └── screenshot │ │ │ │ │ │ │ └── input_b0398bd8a34b4b9128404e8c7b6ecb99d35238103cddf4bd8bb30363dbca8c84.png │ │ │ │ │ └── findElement-equals-driver-FirefoxDriver.open.recheck │ │ │ │ │ │ └── screenshot │ │ │ │ │ │ └── html_55bf18063eed7a040d95559c1a8689df5118c2d589738e9d491872f3b9153f2d.png │ │ │ │ ├── de.retest.web.it.RecheckRemoteWebElementFailingIT │ │ │ │ │ ├── select-element-FirefoxDriver.open.recheck │ │ │ │ │ │ └── screenshot │ │ │ │ │ │ │ ├── multi.png │ │ │ │ │ │ │ └── select_55bf18063eed7a040d95559c1a8689df5118c2d589738e9d491872f3b9153f2d.png │ │ │ │ │ └── select-element-ChromeDriver.open.recheck │ │ │ │ │ │ └── screenshot │ │ │ │ │ │ └── select_b0398bd8a34b4b9128404e8c7b6ecb99d35238103cddf4bd8bb30363dbca8c84.png │ │ │ │ ├── de.retest.web.it.SimplePageDiffIT │ │ │ │ │ └── testSimpleChange.open.recheck │ │ │ │ │ │ └── screenshot │ │ │ │ │ │ └── html_ec47e175cc5a136fbd93d9daadba64a38dd208426a378a091dfa892e70aa5180.png │ │ │ │ └── de.retest.web.it.SimpleRetestIdShowcaseIT │ │ │ │ │ └── simple-showcase.index.recheck │ │ │ │ │ └── screenshot │ │ │ │ │ └── html_c529e46f176dacc7a258ef25e8241cbc9dbcfd9e659538e5b9a023c973801af2.png │ │ │ └── retest.properties │ │ └── pages │ │ │ ├── frame.html │ │ │ ├── frame-diff.html │ │ │ ├── showcase │ │ │ └── assets │ │ │ │ ├── de.png │ │ │ │ ├── us.png │ │ │ │ ├── logo.png │ │ │ │ ├── tick.png │ │ │ │ ├── one-click.png │ │ │ │ ├── EXIST-Logo.png │ │ │ │ ├── 1-on-1-assignment.png │ │ │ │ ├── redundant-infos.png │ │ │ │ ├── monkey_clever_laptop.png │ │ │ │ ├── innovationsgutschein-logo.png │ │ │ │ ├── BestOf_IT-Service_2016_170px.png │ │ │ │ ├── emojione.min.css │ │ │ │ ├── doubletaptogo.js │ │ │ │ ├── init.js │ │ │ │ ├── jquery.waypoints.inview.min.js │ │ │ │ └── jquery.freeow.min.js │ │ │ ├── wikipedia │ │ │ ├── actual │ │ │ │ └── assets │ │ │ │ │ ├── enwiki-2x.png │ │ │ │ │ ├── wikimedia-button.png │ │ │ │ │ ├── 32px-Green_bug_and_broom.png │ │ │ │ │ ├── poweredby_mediawiki_88x31.png │ │ │ │ │ └── load_002.css │ │ │ └── expected │ │ │ │ └── assets │ │ │ │ ├── enwiki-2x.png │ │ │ │ ├── wikimedia-button.png │ │ │ │ ├── 32px-Green_bug_and_broom.png │ │ │ │ ├── poweredby_mediawiki_88x31.png │ │ │ │ └── load_002.css │ │ │ ├── single-element-diff.html │ │ │ ├── frame-outer.html │ │ │ ├── special-container.html │ │ │ ├── frame-page.html │ │ │ ├── centered.html │ │ │ ├── covered-page.html │ │ │ ├── simple-page.html │ │ │ └── simple-page-diff.html │ └── java │ │ └── de │ │ └── retest │ │ └── web │ │ ├── testutils │ │ ├── SystemProperty.java │ │ ├── SystemPropertyExtension.java │ │ ├── PageFactory.java │ │ └── WebDriverFactory.java │ │ ├── selenium │ │ ├── CounterCheckNamingStrategyTest.java │ │ ├── AutocheckingRecheckDriverTest.java │ │ ├── UnbreakableDriverTest.java │ │ ├── AutocheckingWebElementIT.java │ │ ├── UnbreakableDriverIT.java │ │ ├── AutocheckingTargetLocatorTest.java │ │ ├── css │ │ │ └── RegexTransformerTest.java │ │ ├── ByWhispererTest.java │ │ └── WriteToResultWarningConsumerTest.java │ │ ├── util │ │ └── TextAttributeUtilTest.java │ │ ├── meta │ │ ├── driver │ │ │ ├── capabilities │ │ │ │ ├── BrowserMetadataProviderTest.java │ │ │ │ ├── PlatformMetadataProviderTest.java │ │ │ │ ├── CapabilityMetadataProviderTest.java │ │ │ │ └── AllCapabilities.java │ │ │ └── WebDriverMetadataProviderTest.java │ │ ├── element │ │ │ └── WebElementMetadataProviderTest.java │ │ └── SeleniumMetadataProviderTest.java │ │ ├── ScreenshotProviderTest.java │ │ ├── mapping │ │ ├── PathsToWebDataMappingTest.java │ │ └── WebDataFilterTest.java │ │ ├── screenshot │ │ └── ViewportOnlyMinimalScreenshotTest.java │ │ ├── it │ │ ├── CenterIT.java │ │ ├── FramePageIT.java │ │ ├── SimplePageIT.java │ │ ├── ShowcaseIT.java │ │ ├── FormPageIT.java │ │ ├── SpecialContainerIT.java │ │ ├── WikipediaIT.java │ │ ├── SimplePageDiffIT.java │ │ ├── TabIT.java │ │ ├── SimpleAutocheckingDriverShowcaseIT.java │ │ ├── SingleElementIT.java │ │ ├── RootElementPeerNullIT.java │ │ ├── SimpleRecheckShowcaseIT.java │ │ ├── RecheckRemoteWebElementIT.java │ │ ├── RecheckRemoteWebElementFailingIT.java │ │ └── SimpleRetestIdShowcaseIT.java │ │ ├── RecheckWebImplIT.java │ │ └── PeerConverterTest.java ├── main │ ├── resources │ │ └── META-INF │ │ │ └── services │ │ │ └── de.retest.recheck.RecheckAdapter │ └── java │ │ └── de │ │ └── retest │ │ └── web │ │ ├── selenium │ │ ├── css │ │ │ ├── Transformer.java │ │ │ ├── PredicateFactory.java │ │ │ ├── Selector.java │ │ │ ├── RegexTransformer.java │ │ │ ├── PredicateBuilder.java │ │ │ └── DefaultSelectors.java │ │ ├── QualifiedElementWarning.java │ │ ├── ImplicitDriverWrapper.java │ │ ├── NoElementWithReTestIdFoundException.java │ │ ├── NoElementWithHighEnoughMatchFoundException.java │ │ ├── RecheckDriver.java │ │ ├── CounterCheckNamingStrategy.java │ │ ├── AutocheckingNavigationWrapper.java │ │ ├── AutocheckingCheckNamingStrategy.java │ │ ├── AutocheckingTargetLocator.java │ │ ├── WriteToResultWarningConsumer.java │ │ └── By.java │ │ ├── ConversionException.java │ │ ├── screenshot │ │ ├── NoScreenshot.java │ │ ├── ViewportOnlyScreenshot.java │ │ ├── ScreenshotProvider.java │ │ ├── FullPageScreenshot.java │ │ ├── ViewportOnlyMinimalScreenshot.java │ │ └── ScreenshotProviders.java │ │ ├── util │ │ └── TextAttributeUtil.java │ │ ├── meta │ │ ├── SeleniumMetadata.java │ │ ├── driver │ │ │ ├── capabilities │ │ │ │ ├── BrowserMetadataProvider.java │ │ │ │ ├── PlatformMetadataProvider.java │ │ │ │ └── CapabilityMetadataProvider.java │ │ │ └── WebDriverMetadataProvider.java │ │ ├── element │ │ │ └── WebElementMetadataProvider.java │ │ └── SeleniumMetadataProvider.java │ │ ├── mapping │ │ ├── WebDataFilter.java │ │ └── PathsToWebDataMapping.java │ │ ├── AttributesUtil.java │ │ ├── RecheckWebProperties.java │ │ ├── RootElementPeer.java │ │ └── RecheckWebImpl.java └── eclipse │ └── retest GUI recheck-web Java 1.8.launch ├── .retest ├── retest.properties ├── recheck.ignore.js ├── filter │ └── recheck-pseudo-elements.filter └── recheck.ignore ├── CONTRIBUTING.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── .github ├── dependabot.yml ├── pull_request_template.md ├── ISSUE_TEMPLATE │ ├── feature.md │ └── bug.md └── workflows │ ├── deploy-release.yml │ ├── release-hotfix-start.yml │ ├── release-feature-start.yml │ ├── release-hotfix-finish.yml │ ├── release-feature-finish.yml │ ├── build-project.yml │ └── release-beta.yml ├── .gitattributes └── .mergify.yml /src/test/resources/de/retest/web/AttributesConfigDeserializerTest/empty.yaml: -------------------------------------------------------------------------------- 1 | htmlAttributes: 2 | -------------------------------------------------------------------------------- /src/test/resources/de/retest/web/AttributesConfigDeserializerTest/all-html.yaml: -------------------------------------------------------------------------------- 1 | htmlAttributes: all 2 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/de.retest.recheck.RecheckAdapter: -------------------------------------------------------------------------------- 1 | de.retest.web.RecheckSeleniumAdapter -------------------------------------------------------------------------------- /src/test/resources/de/retest/web/AttributesConfigDeserializerTest/invalid-html.yaml: -------------------------------------------------------------------------------- 1 | htmlAttributes: invalid 2 | -------------------------------------------------------------------------------- /src/test/resources/retest/recheck/de.retest.web.it.TabIT/recheck.ignore: -------------------------------------------------------------------------------- 1 | change=inserted 2 | change=deleted 3 | -------------------------------------------------------------------------------- /src/test/resources/pages/frame.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/pages/frame.html -------------------------------------------------------------------------------- /.retest/retest.properties: -------------------------------------------------------------------------------- 1 | de.retest.recheck.ignore.attributes=absolute-outline 2 | de.retest.recheck.web.screenshot.provider=none 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | The ***retest*** Contributing Guidelines can be found at https://docs.retest.de/contributing. 4 | -------------------------------------------------------------------------------- /src/test/resources/de/retest/web/AttributesConfigDeserializerTest/selected.yaml: -------------------------------------------------------------------------------- 1 | htmlAttributes: 2 | - tic 3 | - tac 4 | - toe 5 | -------------------------------------------------------------------------------- /src/test/resources/pages/frame-diff.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/pages/frame-diff.html -------------------------------------------------------------------------------- /src/test/resources/pages/showcase/assets/de.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/pages/showcase/assets/de.png -------------------------------------------------------------------------------- /src/test/resources/pages/showcase/assets/us.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/pages/showcase/assets/us.png -------------------------------------------------------------------------------- /src/test/resources/pages/showcase/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/pages/showcase/assets/logo.png -------------------------------------------------------------------------------- /src/test/resources/pages/showcase/assets/tick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/pages/showcase/assets/tick.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Eclipse. 2 | .classpath 3 | .project 4 | .settings/ 5 | 6 | # IntelliJ. 7 | .idea/ 8 | *.iml 9 | 10 | # Maven. 11 | /target/ 12 | 13 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | The ***retest*** Code of Conduct can be found at https://docs.retest.de/code-of-conduct. 4 | -------------------------------------------------------------------------------- /src/test/resources/pages/showcase/assets/one-click.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/pages/showcase/assets/one-click.png -------------------------------------------------------------------------------- /src/test/resources/pages/showcase/assets/EXIST-Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/pages/showcase/assets/EXIST-Logo.png -------------------------------------------------------------------------------- /src/test/resources/pages/showcase/assets/1-on-1-assignment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/pages/showcase/assets/1-on-1-assignment.png -------------------------------------------------------------------------------- /src/test/resources/pages/showcase/assets/redundant-infos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/pages/showcase/assets/redundant-infos.png -------------------------------------------------------------------------------- /src/test/resources/pages/wikipedia/actual/assets/enwiki-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/pages/wikipedia/actual/assets/enwiki-2x.png -------------------------------------------------------------------------------- /src/test/resources/pages/wikipedia/expected/assets/enwiki-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/pages/wikipedia/expected/assets/enwiki-2x.png -------------------------------------------------------------------------------- /src/test/resources/pages/showcase/assets/monkey_clever_laptop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/pages/showcase/assets/monkey_clever_laptop.png -------------------------------------------------------------------------------- /src/test/resources/pages/wikipedia/actual/assets/wikimedia-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/pages/wikipedia/actual/assets/wikimedia-button.png -------------------------------------------------------------------------------- /src/test/resources/pages/showcase/assets/innovationsgutschein-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/pages/showcase/assets/innovationsgutschein-logo.png -------------------------------------------------------------------------------- /src/test/resources/pages/wikipedia/expected/assets/wikimedia-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/pages/wikipedia/expected/assets/wikimedia-button.png -------------------------------------------------------------------------------- /src/test/resources/pages/showcase/assets/BestOf_IT-Service_2016_170px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/pages/showcase/assets/BestOf_IT-Service_2016_170px.png -------------------------------------------------------------------------------- /src/test/resources/pages/wikipedia/actual/assets/32px-Green_bug_and_broom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/pages/wikipedia/actual/assets/32px-Green_bug_and_broom.png -------------------------------------------------------------------------------- /src/test/resources/pages/wikipedia/actual/assets/poweredby_mediawiki_88x31.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/pages/wikipedia/actual/assets/poweredby_mediawiki_88x31.png -------------------------------------------------------------------------------- /src/test/resources/pages/wikipedia/expected/assets/32px-Green_bug_and_broom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/pages/wikipedia/expected/assets/32px-Green_bug_and_broom.png -------------------------------------------------------------------------------- /src/main/java/de/retest/web/selenium/css/Transformer.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.selenium.css; 2 | 3 | @FunctionalInterface 4 | interface Transformer { 5 | 6 | Selector transform( String selector ); 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/test/resources/pages/wikipedia/expected/assets/poweredby_mediawiki_88x31.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/pages/wikipedia/expected/assets/poweredby_mediawiki_88x31.png -------------------------------------------------------------------------------- /src/test/resources/pages/single-element-diff.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Check Single Elements 4 | 5 | 6 |

This text is red

7 | 8 | 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: maven 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "04:00" 8 | timezone: Europe/Berlin 9 | open-pull-requests-limit: 10 10 | -------------------------------------------------------------------------------- /src/test/resources/de/retest/web/selenium/UnbreakableDriverIT.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/test/resources/pages/showcase/assets/emojione.min.css: -------------------------------------------------------------------------------- 1 | .emojione{font-size:inherit;height:3ex;width:3.1ex;min-height:20px;min-width:20px;display:inline-block;margin:-.2ex .15em .2ex;line-height:normal;vertical-align:middle}img.emojione{width:auto} -------------------------------------------------------------------------------- /src/test/resources/retest/recheck/de.retest.web.it.CenterIT/center-ChromeDriver.open.recheck/screenshot/html.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/retest/recheck/de.retest.web.it.CenterIT/center-ChromeDriver.open.recheck/screenshot/html.png -------------------------------------------------------------------------------- /src/test/resources/retest/recheck/de.retest.web.it.TestHealerCssSelectorIT/setup.00.recheck/screenshot/root.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/retest/recheck/de.retest.web.it.TestHealerCssSelectorIT/setup.00.recheck/screenshot/root.png -------------------------------------------------------------------------------- /src/test/resources/retest/recheck/de.retest.web.it.TestHealerCssSelectorIT/setup.01.recheck/screenshot/root.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/retest/recheck/de.retest.web.it.TestHealerCssSelectorIT/setup.01.recheck/screenshot/root.png -------------------------------------------------------------------------------- /src/test/resources/retest/recheck/de.retest.web.it.CenterIT/center-FirefoxDriver.open.recheck/screenshot/html.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/retest/recheck/de.retest.web.it.CenterIT/center-FirefoxDriver.open.recheck/screenshot/html.png -------------------------------------------------------------------------------- /src/test/resources/de/retest/web/it/tab.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Tab 6 | 7 | 8 | 9 |

Title

10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/test/resources/retest/recheck/de.retest.web.it.ShowcaseIT/showcase-FirefoxDriver.open.recheck/screenshot/html.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/retest/recheck/de.retest.web.it.ShowcaseIT/showcase-FirefoxDriver.open.recheck/screenshot/html.png -------------------------------------------------------------------------------- /src/test/resources/retest/recheck/de.retest.web.it.FormPageIT/form-page-FirefoxDriver.open.recheck/screenshot/html.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/retest/recheck/de.retest.web.it.FormPageIT/form-page-FirefoxDriver.open.recheck/screenshot/html.png -------------------------------------------------------------------------------- /src/test/resources/retest/recheck/de.retest.web.it.FramePageIT/frame-page-ChromeDriver.open.recheck/screenshot/html.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/retest/recheck/de.retest.web.it.FramePageIT/frame-page-ChromeDriver.open.recheck/screenshot/html.png -------------------------------------------------------------------------------- /src/test/resources/retest/recheck/de.retest.web.it.FramePageIT/frame-page-FirefoxDriver.open.recheck/screenshot/html.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/retest/recheck/de.retest.web.it.FramePageIT/frame-page-FirefoxDriver.open.recheck/screenshot/html.png -------------------------------------------------------------------------------- /src/test/resources/retest/recheck/de.retest.web.it.SimplePageIT/simple-page-FirefoxDriver.open.recheck/screenshot/html.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/retest/recheck/de.retest.web.it.SimplePageIT/simple-page-FirefoxDriver.open.recheck/screenshot/html.png -------------------------------------------------------------------------------- /src/test/resources/retest/recheck/de.retest.web.it.SpecialContainerIT/special-container-FirefoxDriver.open.recheck/screenshot/html.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/retest/recheck/de.retest.web.it.SpecialContainerIT/special-container-FirefoxDriver.open.recheck/screenshot/html.png -------------------------------------------------------------------------------- /src/test/resources/retest/recheck/de.retest.web.it.SimpleUnbreakableSeleniumShowcaseIT/simple-showcase.index.recheck/screenshot/html.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/retest/recheck/de.retest.web.it.SimpleUnbreakableSeleniumShowcaseIT/simple-showcase.index.recheck/screenshot/html.png -------------------------------------------------------------------------------- /src/test/resources/retest/recheck/de.retest.web.it.SpecialContainerIT/special-container-FirefoxDriver.click.recheck/screenshot/html.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/retest/recheck/de.retest.web.it.SpecialContainerIT/special-container-FirefoxDriver.click.recheck/screenshot/html.png -------------------------------------------------------------------------------- /src/test/resources/retest/recheck/de.retest.web.it.WikipediaIT/myWikipediaTest.characterization-testing-page.recheck/screenshot/html.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/retest/recheck/de.retest.web.it.WikipediaIT/myWikipediaTest.characterization-testing-page.recheck/screenshot/html.png -------------------------------------------------------------------------------- /src/test/resources/pages/wikipedia/actual/assets/load_002.css: -------------------------------------------------------------------------------- 1 | div#editpage-specialchars{display:block;border:1px solid #c0c0c0;padding:.5em 1em}#editpage-specialchars a{background-color:#f9f9f9;border:1px solid #ddd;padding:1px 4px}textarea#wpTextbox1 + #editpage-specialchars,.wikiEditor-ui-clear + #editpage-specialchars{border-top:none} -------------------------------------------------------------------------------- /src/test/resources/retest/recheck/de.retest.web.it.RecheckRemoteWebElementIT/simple-webelement-FirefoxDriver.open.recheck/screenshot/email.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/retest/recheck/de.retest.web.it.RecheckRemoteWebElementIT/simple-webelement-FirefoxDriver.open.recheck/screenshot/email.png -------------------------------------------------------------------------------- /src/test/resources/pages/wikipedia/expected/assets/load_002.css: -------------------------------------------------------------------------------- 1 | div#editpage-specialchars{display:block;border:1px solid #c0c0c0;padding:.5em 1em}#editpage-specialchars a{background-color:#f9f9f9;border:1px solid #ddd;padding:1px 4px}textarea#wpTextbox1 + #editpage-specialchars,.wikiEditor-ui-clear + #editpage-specialchars{border-top:none} -------------------------------------------------------------------------------- /src/test/resources/retest/recheck/de.retest.web.it.RecheckRemoteWebElementFailingIT/select-element-FirefoxDriver.open.recheck/screenshot/multi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/retest/recheck/de.retest.web.it.RecheckRemoteWebElementFailingIT/select-element-FirefoxDriver.open.recheck/screenshot/multi.png -------------------------------------------------------------------------------- /src/test/resources/retest/retest.properties: -------------------------------------------------------------------------------- 1 | de.retest.output.Format=PLAIN 2 | de.retest.sut.applicationPath=target/ 3 | de.retest.sut.applicationClassPath=${de.retest.sut.applicationPath}/classes/de/retest/web/ 4 | de.retest.testresults.reportsDir=${de.retest.sut.applicationPath}/test-classes/retest/recheck/ 5 | de.retest.feedback.disabled=true 6 | -------------------------------------------------------------------------------- /src/main/java/de/retest/web/selenium/css/PredicateFactory.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.selenium.css; 2 | 3 | import java.util.function.Predicate; 4 | 5 | import de.retest.recheck.ui.descriptors.Element; 6 | 7 | @FunctionalInterface 8 | public interface PredicateFactory { 9 | 10 | Predicate create( String attribute ); 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/de/retest/web/selenium/QualifiedElementWarning.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.selenium; 2 | 3 | import de.retest.recheck.ui.diff.ElementIdentificationWarning; 4 | import lombok.Value; 5 | 6 | @Value 7 | public class QualifiedElementWarning { 8 | 9 | private String retestId; 10 | private String attributeKey; 11 | private ElementIdentificationWarning warning; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/test/resources/de/retest/web/it/page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Page 6 | 7 | 8 | New Tab 9 | New Window 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/test/resources/retest/recheck/de.retest.web.it.ShowcaseIT/showcase-FirefoxDriver.open.recheck/screenshot/html_006f66c7412bad4d92aaec4856af74d3f506bc2df3645f9f1ea953c444b028af.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/retest/recheck/de.retest.web.it.ShowcaseIT/showcase-FirefoxDriver.open.recheck/screenshot/html_006f66c7412bad4d92aaec4856af74d3f506bc2df3645f9f1ea953c444b028af.png -------------------------------------------------------------------------------- /src/test/resources/retest/recheck/de.retest.web.it.SimplePageDiffIT/testSimpleChange.open.recheck/screenshot/html_ec47e175cc5a136fbd93d9daadba64a38dd208426a378a091dfa892e70aa5180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/retest/recheck/de.retest.web.it.SimplePageDiffIT/testSimpleChange.open.recheck/screenshot/html_ec47e175cc5a136fbd93d9daadba64a38dd208426a378a091dfa892e70aa5180.png -------------------------------------------------------------------------------- /src/main/java/de/retest/web/ConversionException.java: -------------------------------------------------------------------------------- 1 | package de.retest.web; 2 | 3 | public class ConversionException extends RuntimeException { 4 | 5 | private static final long serialVersionUID = 1L; 6 | 7 | public ConversionException( final String msg ) { 8 | super( msg ); 9 | } 10 | 11 | public ConversionException( final String msg, final Exception e ) { 12 | super( msg, e ); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/test/resources/retest/recheck/de.retest.web.it.SimplePageIT/simple-page-ChromeDriver.open.recheck/screenshot/html_43ab85c02685dcd082f87fef200b9586fd3945be3eac4b16d06d75fa3836136b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/retest/recheck/de.retest.web.it.SimplePageIT/simple-page-ChromeDriver.open.recheck/screenshot/html_43ab85c02685dcd082f87fef200b9586fd3945be3eac4b16d06d75fa3836136b.png -------------------------------------------------------------------------------- /src/test/resources/retest/recheck/de.retest.web.it.SimplePageIT/simple-page-FirefoxDriver.open.recheck/screenshot/html_ccc95f1c9c6803a22f080f151bfacd67a737c35b1cb4f5a2f07b16d30105e204.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/retest/recheck/de.retest.web.it.SimplePageIT/simple-page-FirefoxDriver.open.recheck/screenshot/html_ccc95f1c9c6803a22f080f151bfacd67a737c35b1cb4f5a2f07b16d30105e204.png -------------------------------------------------------------------------------- /src/test/resources/retest/recheck/de.retest.web.it.SimpleRetestIdShowcaseIT/simple-showcase.index.recheck/screenshot/html_c529e46f176dacc7a258ef25e8241cbc9dbcfd9e659538e5b9a023c973801af2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/retest/recheck/de.retest.web.it.SimpleRetestIdShowcaseIT/simple-showcase.index.recheck/screenshot/html_c529e46f176dacc7a258ef25e8241cbc9dbcfd9e659538e5b9a023c973801af2.png -------------------------------------------------------------------------------- /src/test/resources/retest/recheck/de.retest.web.it.SpecialContainerIT/special-container-ChromeDriver.click.recheck/screenshot/html_f5603547723e5f5c3b9979f8db49ad6b91389981770f6666898b686e5d882b53.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/retest/recheck/de.retest.web.it.SpecialContainerIT/special-container-ChromeDriver.click.recheck/screenshot/html_f5603547723e5f5c3b9979f8db49ad6b91389981770f6666898b686e5d882b53.png -------------------------------------------------------------------------------- /src/test/resources/retest/recheck/de.retest.web.it.SpecialContainerIT/special-container-ChromeDriver.open.recheck/screenshot/html_f5603547723e5f5c3b9979f8db49ad6b91389981770f6666898b686e5d882b53.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/retest/recheck/de.retest.web.it.SpecialContainerIT/special-container-ChromeDriver.open.recheck/screenshot/html_f5603547723e5f5c3b9979f8db49ad6b91389981770f6666898b686e5d882b53.png -------------------------------------------------------------------------------- /src/test/resources/retest/recheck/de.retest.web.it.SpecialContainerIT/special-container-FirefoxDriver.open.recheck/screenshot/html_7b43151124a12deb468c88e608ba52949773935aec87fd9c24a4d88609b0155c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/retest/recheck/de.retest.web.it.SpecialContainerIT/special-container-FirefoxDriver.open.recheck/screenshot/html_7b43151124a12deb468c88e608ba52949773935aec87fd9c24a4d88609b0155c.png -------------------------------------------------------------------------------- /src/test/resources/retest/recheck/de.retest.web.it.SpecialContainerIT/special-container-FirefoxDriver.click.recheck/screenshot/html_7b43151124a12deb468c88e608ba52949773935aec87fd9c24a4d88609b0155c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/retest/recheck/de.retest.web.it.SpecialContainerIT/special-container-FirefoxDriver.click.recheck/screenshot/html_7b43151124a12deb468c88e608ba52949773935aec87fd9c24a4d88609b0155c.png -------------------------------------------------------------------------------- /src/test/resources/retest/recheck/de.retest.web.it.RecheckRemoteWebElementIT/empty-article-ChromeDriver.open.recheck/screenshot/article_b0398bd8a34b4b9128404e8c7b6ecb99d35238103cddf4bd8bb30363dbca8c84.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/retest/recheck/de.retest.web.it.RecheckRemoteWebElementIT/empty-article-ChromeDriver.open.recheck/screenshot/article_b0398bd8a34b4b9128404e8c7b6ecb99d35238103cddf4bd8bb30363dbca8c84.png -------------------------------------------------------------------------------- /src/test/resources/retest/recheck/de.retest.web.it.RecheckRemoteWebElementIT/empty-article-FirefoxDriver.open.recheck/screenshot/article_55bf18063eed7a040d95559c1a8689df5118c2d589738e9d491872f3b9153f2d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/retest/recheck/de.retest.web.it.RecheckRemoteWebElementIT/empty-article-FirefoxDriver.open.recheck/screenshot/article_55bf18063eed7a040d95559c1a8689df5118c2d589738e9d491872f3b9153f2d.png -------------------------------------------------------------------------------- /src/test/resources/retest/recheck/de.retest.web.it.RecheckRemoteWebElementIT/simple-webelement-ChromeDriver.open.recheck/screenshot/input_b0398bd8a34b4b9128404e8c7b6ecb99d35238103cddf4bd8bb30363dbca8c84.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/retest/recheck/de.retest.web.it.RecheckRemoteWebElementIT/simple-webelement-ChromeDriver.open.recheck/screenshot/input_b0398bd8a34b4b9128404e8c7b6ecb99d35238103cddf4bd8bb30363dbca8c84.png -------------------------------------------------------------------------------- /src/test/resources/retest/recheck/de.retest.web.it.RecheckRemoteWebElementIT/simple-webelement-FirefoxDriver.open.recheck/screenshot/input_55bf18063eed7a040d95559c1a8689df5118c2d589738e9d491872f3b9153f2d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/retest/recheck/de.retest.web.it.RecheckRemoteWebElementIT/simple-webelement-FirefoxDriver.open.recheck/screenshot/input_55bf18063eed7a040d95559c1a8689df5118c2d589738e9d491872f3b9153f2d.png -------------------------------------------------------------------------------- /src/test/resources/retest/recheck/de.retest.web.it.RecheckRemoteWebElementFailingIT/select-element-ChromeDriver.open.recheck/screenshot/select_b0398bd8a34b4b9128404e8c7b6ecb99d35238103cddf4bd8bb30363dbca8c84.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/retest/recheck/de.retest.web.it.RecheckRemoteWebElementFailingIT/select-element-ChromeDriver.open.recheck/screenshot/select_b0398bd8a34b4b9128404e8c7b6ecb99d35238103cddf4bd8bb30363dbca8c84.png -------------------------------------------------------------------------------- /src/test/resources/retest/recheck/de.retest.web.it.RecheckRemoteWebElementFailingIT/select-element-FirefoxDriver.open.recheck/screenshot/select_55bf18063eed7a040d95559c1a8689df5118c2d589738e9d491872f3b9153f2d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/retest/recheck/de.retest.web.it.RecheckRemoteWebElementFailingIT/select-element-FirefoxDriver.open.recheck/screenshot/select_55bf18063eed7a040d95559c1a8689df5118c2d589738e9d491872f3b9153f2d.png -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.css text encoding=utf-8 eol=lf 2 | *.html text encoding=utf-8 eol=lf 3 | *.ignore text encoding=utf-8 eol=lf 4 | *.java text encoding=utf-8 eol=lf 5 | *.js text encoding=utf-8 eol=lf 6 | *.md text encoding=utf-8 eol=lf 7 | *.properties text encoding=utf-8 eol=lf 8 | *.sh text encoding=utf-8 eol=lf 9 | *.xml text encoding=utf-8 eol=lf 10 | *.yaml text encoding=utf-8 eol=lf 11 | *.yml text encoding=utf-8 eol=lf 12 | *.cmd text encoding=utf-8 eol=crlf 13 | -------------------------------------------------------------------------------- /src/test/resources/retest/recheck/de.retest.web.it.RecheckRemoteWebElementIT/findElement-equals-driver-FirefoxDriver.open.recheck/screenshot/html_55bf18063eed7a040d95559c1a8689df5118c2d589738e9d491872f3b9153f2d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retest/recheck-web/HEAD/src/test/resources/retest/recheck/de.retest.web.it.RecheckRemoteWebElementIT/findElement-equals-driver-FirefoxDriver.open.recheck/screenshot/html_55bf18063eed7a040d95559c1a8689df5118c2d589738e9d491872f3b9153f2d.png -------------------------------------------------------------------------------- /src/main/java/de/retest/web/selenium/ImplicitDriverWrapper.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.selenium; 2 | 3 | import org.openqa.selenium.WebDriver; 4 | import org.openqa.selenium.WrapsDriver; 5 | 6 | import lombok.AccessLevel; 7 | import lombok.RequiredArgsConstructor; 8 | 9 | @RequiredArgsConstructor( access = AccessLevel.PACKAGE, staticName = "of" ) 10 | public class ImplicitDriverWrapper implements WrapsDriver { 11 | 12 | private final WebDriver wrapped; 13 | 14 | @Override 15 | public WebDriver getWrappedDriver() { 16 | return wrapped; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/de/retest/web/selenium/NoElementWithReTestIdFoundException.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.selenium; 2 | 3 | public class NoElementWithReTestIdFoundException extends RuntimeException { 4 | 5 | private static final long serialVersionUID = 1L; 6 | 7 | private final String retestId; 8 | 9 | public NoElementWithReTestIdFoundException( final String retestId ) { 10 | super( "No element with retest ID '" + retestId + "' found!" ); 11 | this.retestId = retestId; 12 | } 13 | 14 | public String getRetestId() { 15 | return retestId; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/test/java/de/retest/web/testutils/SystemProperty.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.testutils; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | import org.junit.jupiter.api.extension.ExtendWith; 9 | 10 | @Retention( RetentionPolicy.RUNTIME ) 11 | @Target( ElementType.METHOD ) 12 | @ExtendWith( SystemPropertyExtension.class ) 13 | public @interface SystemProperty { 14 | 15 | String key(); 16 | 17 | String value() default ""; 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/test/resources/de/retest/web/it/pseudo-elements.css: -------------------------------------------------------------------------------- 1 | h1::before { 2 | content: "This is an invisible text before!"; 3 | color: red; 4 | background-color: green; 5 | } 6 | 7 | h1::after { 8 | content: "This is an invisible text after!"; 9 | color: red; 10 | background-color: green; 11 | } 12 | 13 | h1 { 14 | color: green; 15 | background-color: red; 16 | } 17 | 18 | h1#hidden { 19 | display: none; 20 | } 21 | 22 | li::marker { 23 | content: "marker > "; 24 | color: purple; 25 | } 26 | 27 | p::first-line { 28 | color: #abc; 29 | } 30 | 31 | p::first-letter { 32 | color: #bca; 33 | } -------------------------------------------------------------------------------- /src/test/resources/pages/frame-outer.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | Multi Frame 8 | 9 | 10 | 11 | 12 | 13 | <body> 14 | <p>Diese Seite verwendet Frames. Frames werden von Ihrem Browser aber nicht unterstützt.</p> 15 | </body> 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/test/resources/de/retest/web/it/pseudo-element.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

You can use the ::first-line pseudo-element to add a special effect to the first line of a text. Some more text. And even more, and more, and more, and more, and more, and more, and more, and more, and more, and more, and more, and more.

9 | 10 |

This is a heading

11 | 12 |

This is a hidden heading

13 | 14 | 18 | 19 |
20 |
21 | 22 | -------------------------------------------------------------------------------- /src/main/java/de/retest/web/screenshot/NoScreenshot.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.screenshot; 2 | 3 | import java.awt.image.BufferedImage; 4 | 5 | import org.openqa.selenium.WebDriver; 6 | import org.openqa.selenium.WebElement; 7 | 8 | /** 9 | * Does not take any screenshot at all. Increases the performance. Cannot be set globally. 10 | */ 11 | public class NoScreenshot implements ScreenshotProvider { 12 | 13 | @Override 14 | public BufferedImage shoot( final WebDriver driver ) { 15 | return null; 16 | } 17 | 18 | @Override 19 | public BufferedImage shoot( final WebDriver driver, final WebElement element ) { 20 | return null; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/test/resources/de/retest/web/it/pseudo-element-changed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

You can use the ::first-line pseudo-element to add a special effect to the first line of a text. Some more text. And even more, and more, and more, and more, and more, and more, and more, and more, and more, and more, and more, and more.

9 | 10 |

This is a heading

11 | 12 |

This is a hidden heading

13 | 14 | 18 | 19 |
20 |
21 | 22 | -------------------------------------------------------------------------------- /src/test/resources/de/retest/web/it/pseudo-elements-changed.css: -------------------------------------------------------------------------------- 1 | h1::before { 2 | content: "This is a changed invisible text using a before pseudo element!"; 3 | color: orange; 4 | background-color: blue; 5 | } 6 | 7 | h1::after { 8 | content: "This is a changed invisible text using an after pseudo element!"; 9 | color: orange; 10 | background-color: blue; 11 | } 12 | 13 | h1 { 14 | color: blue; 15 | background-color: orange; 16 | } 17 | 18 | h1#hidden { 19 | 20 | } 21 | 22 | li::marker { 23 | content: "changed marker > "; 24 | color: cyan; 25 | } 26 | 27 | p::first-line { 28 | color: #def; 29 | } 30 | 31 | p::first-letter { 32 | color: #efd; 33 | } 34 | -------------------------------------------------------------------------------- /src/test/java/de/retest/web/selenium/CounterCheckNamingStrategyTest.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.selenium; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | class CounterCheckNamingStrategyTest { 8 | 9 | private final CounterCheckNamingStrategy cut = new CounterCheckNamingStrategy(); 10 | 11 | @Test 12 | void makeUnique_should_append_unique_prefix() { 13 | assertThat( cut.getUniqueCheckName( "someString", null ) ).isEqualTo( "00" ); 14 | assertThat( cut.getUniqueCheckName( "someOtherString", null ) ).isEqualTo( "01" ); 15 | assertThat( cut.getUniqueCheckName( "someString", null ) ).isEqualTo( "02" ); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/test/java/de/retest/web/selenium/AutocheckingRecheckDriverTest.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.selenium; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.mockito.Mockito.mock; 5 | 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.Test; 8 | import org.openqa.selenium.remote.RemoteWebDriver; 9 | 10 | class AutocheckingRecheckDriverTest { 11 | 12 | AutocheckingRecheckDriver cut; 13 | 14 | @BeforeEach 15 | void setUp() { 16 | cut = new AutocheckingRecheckDriver( mock( RemoteWebDriver.class ) ); 17 | } 18 | 19 | @Test 20 | void switchTo_should_create_autochecking_locator() { 21 | assertThat( cut.switchTo() ).isInstanceOf( AutocheckingTargetLocator.class ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/test/java/de/retest/web/util/TextAttributeUtilTest.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.util; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | class TextAttributeUtilTest { 8 | 9 | @Test 10 | void create_should_return_a_text_attribute_without_formatting() { 11 | final String text = "foo \n bar"; 12 | final String expected = text.replaceAll( "\\s+", " " ); 13 | assertThat( TextAttributeUtil.createTextAttribute( "", text ).getValue() ).isEqualTo( expected ); 14 | } 15 | 16 | @Test 17 | void create_should_return_text_attribute_with_formatting() { 18 | final String text = "foo \n bar"; 19 | assertThat( TextAttributeUtil.createTextAttribute( "/" + TextAttributeUtil.PRE_ELEMENT, text ).getValue() ) 20 | .isEqualTo( text ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/de/retest/web/selenium/css/Selector.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.selenium.css; 2 | 3 | import java.util.function.Predicate; 4 | 5 | import de.retest.recheck.ui.descriptors.Element; 6 | import lombok.AccessLevel; 7 | import lombok.Getter; 8 | import lombok.RequiredArgsConstructor; 9 | 10 | @Getter 11 | @RequiredArgsConstructor( access = AccessLevel.PRIVATE ) 12 | class Selector { 13 | 14 | private final String remainingSelector; 15 | private final Predicate predicate; 16 | 17 | static Selector supported( final String remainingSelector, final Predicate predicate ) { 18 | return new Selector( remainingSelector, predicate ); 19 | } 20 | 21 | static Selector unsupported( final String remainingSelector ) { 22 | return new Selector( remainingSelector, e -> false ); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/test/resources/pages/special-container.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Welcome to the Test of Recheck-web 6 | 7 | 8 | 9 |
10 |

11 | Das ist ein Text! 12 |

13 | Und das auch. 14 |
15 | 16 | 17 | 18 | 24 | 25 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/main/java/de/retest/web/selenium/NoElementWithHighEnoughMatchFoundException.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.selenium; 2 | 3 | import de.retest.recheck.RecheckProperties; 4 | import de.retest.recheck.ui.descriptors.Element; 5 | 6 | public class NoElementWithHighEnoughMatchFoundException extends RuntimeException { 7 | 8 | private static final long serialVersionUID = 1L; 9 | 10 | public NoElementWithHighEnoughMatchFoundException( final Element resultFromExpected ) { 11 | super( "The element " + resultFromExpected + " from the Golden Master could not be matched with confidence > " 12 | + RecheckProperties.getInstance().elementMatchThreshold() 13 | + ". To change the required minimal confidence for a match, please set property '" 14 | + RecheckProperties.ELEMENT_MATCH_THRESHOLD_PROPERTY_KEY + "'." ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/de/retest/web/util/TextAttributeUtil.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.util; 2 | 3 | import static de.retest.web.AttributesUtil.TEXT; 4 | 5 | import de.retest.recheck.ui.descriptors.TextAttribute; 6 | 7 | public class TextAttributeUtil { 8 | 9 | private TextAttributeUtil() {} 10 | 11 | public static final String PRE_ELEMENT = "/pre["; 12 | 13 | public static TextAttribute createTextAttribute( final String path, final String text ) { 14 | return isPreContained( path ) ? new TextAttribute( TEXT, text ) 15 | : new TextAttribute( TEXT, removeFormatting( text ) ); 16 | } 17 | 18 | private static boolean isPreContained( final String path ) { 19 | return path.toLowerCase().contains( PRE_ELEMENT ); 20 | } 21 | 22 | private static String removeFormatting( final String text ) { 23 | return text.replaceAll( "\\s+", " " ).trim(); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/de/retest/web/meta/SeleniumMetadata.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.meta; 2 | 3 | import de.retest.recheck.meta.global.OSMetadataProvider; 4 | 5 | public final class SeleniumMetadata { 6 | 7 | // Generic 8 | 9 | public static final String CHECK_TYPE = "check.type"; 10 | 11 | // Driver 12 | 13 | public static final String BROWSER_NAME = "browser.name"; 14 | public static final String BROWSER_VERSION = "browser.version"; 15 | 16 | public static final String OS_NAME = OSMetadataProvider.OS_NAME; 17 | public static final String OS_VERSION = OSMetadataProvider.OS_VERSION; 18 | public static final String OS_ARCH = OSMetadataProvider.OS_ARCH; 19 | 20 | public static final String DRIVER_TYPE = "driver.type"; 21 | 22 | public static final String WINDOW_WIDTH = "window.width"; 23 | public static final String WINDOW_HEIGHT = "window.height"; 24 | 25 | public static final String URL = "url"; 26 | } 27 | -------------------------------------------------------------------------------- /src/test/java/de/retest/web/meta/driver/capabilities/BrowserMetadataProviderTest.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.meta.driver.capabilities; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import org.junit.jupiter.params.ParameterizedTest; 6 | import org.junit.jupiter.params.provider.MethodSource; 7 | import org.openqa.selenium.Capabilities; 8 | 9 | import de.retest.web.meta.SeleniumMetadata; 10 | 11 | class BrowserMetadataProviderTest { 12 | 13 | @ParameterizedTest 14 | @MethodSource( "de.retest.web.meta.driver.capabilities.AllCapabilities#capabilities" ) 15 | void retrieve_should_gather_browser_name_and_version( final Capabilities capabilities ) throws Exception { 16 | final BrowserMetadataProvider cut = new BrowserMetadataProvider( capabilities ); 17 | 18 | assertThat( cut.retrieve() ) // 19 | .containsKeys( SeleniumMetadata.BROWSER_NAME, SeleniumMetadata.BROWSER_VERSION ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/de/retest/web/meta/driver/capabilities/BrowserMetadataProvider.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.meta.driver.capabilities; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | import org.openqa.selenium.Capabilities; 7 | 8 | import de.retest.recheck.meta.MetadataProvider; 9 | import de.retest.web.meta.SeleniumMetadata; 10 | import lombok.AccessLevel; 11 | import lombok.RequiredArgsConstructor; 12 | 13 | @RequiredArgsConstructor( access = AccessLevel.PACKAGE ) 14 | public final class BrowserMetadataProvider implements MetadataProvider { 15 | 16 | private final Capabilities capabilities; 17 | 18 | @Override 19 | public Map retrieve() { 20 | final Map map = new HashMap<>(); 21 | 22 | map.put( SeleniumMetadata.BROWSER_NAME, capabilities.getBrowserName() ); 23 | map.put( SeleniumMetadata.BROWSER_VERSION, capabilities.getVersion() ); 24 | 25 | return map; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/test/java/de/retest/web/meta/driver/capabilities/PlatformMetadataProviderTest.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.meta.driver.capabilities; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import org.junit.jupiter.params.ParameterizedTest; 6 | import org.junit.jupiter.params.provider.MethodSource; 7 | import org.openqa.selenium.Capabilities; 8 | 9 | import de.retest.web.meta.SeleniumMetadata; 10 | import lombok.extern.slf4j.Slf4j; 11 | 12 | @Slf4j 13 | class PlatformMetadataProviderTest { 14 | 15 | @ParameterizedTest 16 | @MethodSource( "de.retest.web.meta.driver.capabilities.AllCapabilities#capabilities" ) 17 | void retrieve_should_properly_resolve_platform( final Capabilities capabilities ) throws Exception { 18 | final PlatformMetadataProvider cut = new PlatformMetadataProvider( capabilities ); 19 | 20 | assertThat( cut.retrieve() ) // 21 | .containsKeys( SeleniumMetadata.OS_NAME ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/de/retest/web/selenium/RecheckDriver.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.selenium; 2 | 3 | import org.openqa.selenium.remote.RemoteWebDriver; 4 | 5 | import de.retest.recheck.RecheckOptions; 6 | import de.retest.web.RecheckWebOptions; 7 | 8 | /** 9 | * Extends both {@link AutocheckingRecheckDriver} and {@link UnbreakableDriver} to combine all recheck-web features. Use 10 | * this class if you automatically want to incorporate new features without changing your recheck-web driver 11 | * implementation explicitly. 12 | */ 13 | public class RecheckDriver extends AutocheckingRecheckDriver { 14 | 15 | public RecheckDriver( final RemoteWebDriver wrapped ) { 16 | super( wrapped ); 17 | } 18 | 19 | public RecheckDriver( final RemoteWebDriver wrapped, final RecheckOptions options ) { 20 | super( wrapped, options ); 21 | } 22 | 23 | public RecheckDriver( final RemoteWebDriver wrapped, final RecheckWebOptions options ) { 24 | super( wrapped, options ); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/test/resources/pages/frame-page.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 18 | 19 | 20 | 21 | 22 | 23 | <body> 24 | <p>Diese Seite verwendet Frames. Frames werden von Ihrem Browser aber nicht unterstützt.</p> 25 | </body> 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/test/resources/de/retest/web/AutoHealingTest.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | AutoHealingTest 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/main/java/de/retest/web/selenium/CounterCheckNamingStrategy.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.selenium; 2 | 3 | import org.openqa.selenium.WebElement; 4 | 5 | /** 6 | * This naming strategy simply counts the checks and returns a two-digit string of the current check, starting with 0. 7 | * This is robust to changes of the target and params, but fails miserably if steps are inserted, as this will affect 8 | * all subsequent checks. Also, this is somewhat humanly unreadable. 9 | */ 10 | public class CounterCheckNamingStrategy implements AutocheckingCheckNamingStrategy { 11 | 12 | private int stepCounter = 0; 13 | 14 | @Override 15 | public String getUniqueCheckName( final String action, final WebElement target, final Object... params ) { 16 | return getUniqueCheckName( action ); 17 | } 18 | 19 | @Override 20 | public String getUniqueCheckName( final String action ) { 21 | return String.format( "%02d", stepCounter++ ); 22 | } 23 | 24 | @Override 25 | public void nextTest() { 26 | stepCounter = 0; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/de/retest/web/screenshot/ViewportOnlyScreenshot.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.screenshot; 2 | 3 | import java.awt.image.BufferedImage; 4 | 5 | import org.openqa.selenium.WebDriver; 6 | import org.openqa.selenium.WebElement; 7 | 8 | import com.assertthat.selenium_shutterbug.core.Shutterbug; 9 | 10 | /** 11 | * The default implementation that takes a screenshot of only the viewport visible to the user from the top page. In 12 | * contrast to {@link ViewportOnlyMinimalScreenshot}, images are not resized. 13 | */ 14 | public class ViewportOnlyScreenshot implements ScreenshotProvider { 15 | 16 | private static final boolean USE_DEVICE_PIXEL_RATIO = true; 17 | 18 | @Override 19 | public BufferedImage shoot( final WebDriver driver ) { 20 | return Shutterbug.shootPage( driver, USE_DEVICE_PIXEL_RATIO ).getImage(); 21 | } 22 | 23 | @Override 24 | public BufferedImage shoot( final WebDriver driver, final WebElement element ) { 25 | return Shutterbug.shootElement( driver, element, USE_DEVICE_PIXEL_RATIO ).getImage(); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /.retest/recheck.ignore.js: -------------------------------------------------------------------------------- 1 | var baseUrl = /http[s]?:\/\/[\w.:\d\-]*/; 2 | var fontFamilies = [ [ "system-ui", "Arial" ], [ "-apple-system", "sans-serif" ] ]; 3 | 4 | function contains(array, key) { 5 | for (var i = 0; i < array.length; i++) { 6 | if (key == array[i]) { 7 | return true; 8 | } 9 | } 10 | return false; 11 | } 12 | 13 | function matches(element, diff) { 14 | if (diff == null) { 15 | return false; 16 | } 17 | if (diff.key == "opacity") { 18 | return (Math.abs(diff.expected - diff.actual) <= 10); 19 | } 20 | if (diff.key == "font-family") { 21 | for (var i = 0; i < fontFamilies.length; i++) { 22 | if (contains(fontFamilies[i], diff.expected)) { 23 | return contains(fontFamilies[i], diff.actual); 24 | } 25 | } 26 | } 27 | if (diff.expected != null && diff.actual != null) { 28 | expected = new String(diff.expected); 29 | actual = new String(diff.actual); 30 | cleanExpected = expected.replace(baseUrl, ''); 31 | cleanActual = actual.replace(baseUrl, ''); 32 | return cleanExpected === cleanActual; 33 | } 34 | return false; 35 | } 36 | -------------------------------------------------------------------------------- /src/test/resources/pages/centered.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 33 | 34 | 35 |
36 | text 37 |
38 | 39 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | *Before submission, please check that ...* 2 | 3 | - [ ] the code follows the [Clean Code](https://clean-code-developer.com/) guidelines. 4 | - [ ] the necessary tests are either created or updated. 5 | - [ ] all status checks (GitHub Actions, SonarCloud, etc.) pass. 6 | - [ ] your commit history is cleaned up. 7 | - [ ] you updated the changelog. 8 | - [ ] you updated necessary documentation within [retest/docs](https://github.com/retest/docs). 9 | 10 | 11 | 12 | ## Description 13 | 14 | > TL;DR 15 | 16 | 17 | 18 | ### State of this PR 19 | 20 | 21 | 22 | ### Additional Context 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/test/java/de/retest/web/ScreenshotProviderTest.java: -------------------------------------------------------------------------------- 1 | package de.retest.web; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.mockito.Mockito.mock; 5 | import static org.mockito.Mockito.when; 6 | 7 | import org.junit.jupiter.api.Test; 8 | import org.mockito.Mockito; 9 | import org.openqa.selenium.chrome.ChromeDriver; 10 | import org.openqa.selenium.remote.CommandExecutor; 11 | import org.openqa.selenium.remote.RemoteWebDriver; 12 | 13 | import de.retest.web.screenshot.NoScreenshot; 14 | import de.retest.web.screenshot.ScreenshotProviders; 15 | 16 | class ScreenshotProviderTest { 17 | 18 | @Test 19 | void failingDriver_should_return_null() { 20 | final RemoteWebDriver exceptionCausingDriver = mock( ChromeDriver.class ); 21 | when( exceptionCausingDriver.executeScript( Mockito.anyString() ) ).thenReturn( 1.0 ); 22 | when( exceptionCausingDriver.getCommandExecutor() ).thenReturn( mock( CommandExecutor.class ) ); 23 | 24 | assertThat( ScreenshotProviders.shoot( exceptionCausingDriver, null, new NoScreenshot() ) ).isNull(); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/test/java/de/retest/web/mapping/PathsToWebDataMappingTest.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.mapping; 2 | 3 | import static java.util.Arrays.asList; 4 | import static org.assertj.core.api.Assertions.assertThat; 5 | 6 | import java.util.ArrayList; 7 | import java.util.HashMap; 8 | import java.util.List; 9 | 10 | import org.junit.jupiter.api.Test; 11 | 12 | class PathsToWebDataMappingTest { 13 | 14 | @Test 15 | void should_have_shortest_xpath_as_root() { 16 | final List> mapping = new ArrayList<>(); 17 | mapping.add( asList( "//html[1]", new HashMap<>() ) ); 18 | mapping.add( asList( "//html[1]/body[1]", new HashMap<>() ) ); 19 | mapping.add( asList( "//html[1]/body[1]/div[1]", new HashMap<>() ) ); 20 | 21 | final PathsToWebDataMapping cut1 = new PathsToWebDataMapping( mapping ); 22 | assertThat( cut1.getRootPath() ).isEqualTo( "//html[1]" ); 23 | 24 | final PathsToWebDataMapping cut2 = new PathsToWebDataMapping( "//html[1]/body[1]/div[1]/iframe[1]", mapping ); 25 | assertThat( cut2.getRootPath() ).isEqualTo( "//html[1]/body[1]/div[1]/iframe[1]/html[1]" ); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/test/resources/pages/showcase/assets/doubletaptogo.js: -------------------------------------------------------------------------------- 1 | /* 2 | AUTHOR: Osvaldas Valutis, www.osvaldas.info 3 | */ 4 | 5 | 6 | 7 | ;(function( $, window, document, undefined ) 8 | { 9 | $.fn.doubleTapToGo = function( params ) 10 | { 11 | if( !( 'ontouchstart' in window ) && 12 | !navigator.msMaxTouchPoints && 13 | !navigator.userAgent.toLowerCase().match( /windows phone os 7/i ) ) return false; 14 | 15 | this.each( function() 16 | { 17 | var curItem = false; 18 | 19 | $( this ).on( 'click', function( e ) 20 | { 21 | var item = $( this ); 22 | if( item[ 0 ] != curItem[ 0 ] ) 23 | { 24 | e.preventDefault(); 25 | curItem = item; 26 | } 27 | }); 28 | 29 | $( document ).on( 'click touchstart MSPointerDown', function( e ) 30 | { 31 | var resetItem = true, 32 | parents = $( e.target ).parents(); 33 | 34 | for( var i = 0; i < parents.length; i++ ) 35 | if( parents[ i ] == curItem[ 0 ] ) 36 | resetItem = false; 37 | 38 | if( resetItem ) 39 | curItem = false; 40 | }); 41 | }); 42 | return this; 43 | }; 44 | })( jQuery, window, document ); -------------------------------------------------------------------------------- /src/main/java/de/retest/web/mapping/WebDataFilter.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.mapping; 2 | 3 | import java.util.Arrays; 4 | import java.util.HashSet; 5 | import java.util.Set; 6 | 7 | // TODO This should be part of recheck and somehow use RecheckAdapter#convert to filter the result. 8 | public class WebDataFilter { 9 | 10 | /** 11 | * Tags that will not be ignored even if they are not shown. 12 | */ 13 | static final Set specialTags = new HashSet<>( Arrays.asList( "meta", "option", "title" ) ); 14 | 15 | private WebDataFilter() {} 16 | 17 | public static boolean shouldIgnore( final WebData webData ) { 18 | return isNotShown( webData ) && isNotPseudo( webData ) && isNotSpecialTag( webData ); 19 | } 20 | 21 | private static boolean isNotShown( final WebData webData ) { 22 | return !webData.isShown(); 23 | } 24 | 25 | private static boolean isNotPseudo( final WebData webData ) { 26 | return !webData.isPseudo(); 27 | } 28 | 29 | private static boolean isNotSpecialTag( final WebData webData ) { 30 | final String lowerCaseTag = webData.getTag().toLowerCase(); 31 | return !specialTags.contains( lowerCaseTag ); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/test/java/de/retest/web/screenshot/ViewportOnlyMinimalScreenshotTest.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.screenshot; 2 | 3 | import static de.retest.web.screenshot.ViewportOnlyMinimalScreenshot.DEFAULT_WANTED_WIDTH_PX; 4 | import static org.assertj.core.api.Assertions.assertThat; 5 | 6 | import java.awt.image.BufferedImage; 7 | 8 | import org.junit.jupiter.api.Test; 9 | 10 | class ViewportOnlyMinimalScreenshotTest { 11 | 12 | @Test 13 | void image_should_be_downscaled_to_maxwidth() { 14 | final BufferedImage big = new BufferedImage( 3000, 3000, BufferedImage.TYPE_INT_RGB ); 15 | final BufferedImage result = ViewportOnlyMinimalScreenshot.resizeImage( big ); 16 | 17 | assertThat( result.getWidth() ).isEqualTo( DEFAULT_WANTED_WIDTH_PX ); 18 | assertThat( result.getHeight() ).isEqualTo( DEFAULT_WANTED_WIDTH_PX ); 19 | } 20 | 21 | @Test 22 | void small_image_should_be_unaffected() { 23 | final BufferedImage small = new BufferedImage( 300, 300, BufferedImage.TYPE_INT_RGB ); 24 | final BufferedImage result = ViewportOnlyMinimalScreenshot.resizeImage( small ); 25 | 26 | assertThat( result.getWidth() ).isEqualTo( 300 ); 27 | assertThat( result.getHeight() ).isEqualTo( 300 ); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/test/java/de/retest/web/it/CenterIT.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.it; 2 | 3 | import static de.retest.web.testutils.PageFactory.page; 4 | import static de.retest.web.testutils.PageFactory.Page.CENTER; 5 | 6 | import org.junit.jupiter.api.AfterEach; 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.extension.ExtendWith; 9 | import org.junit.jupiter.params.ParameterizedTest; 10 | import org.junit.jupiter.params.provider.MethodSource; 11 | import org.openqa.selenium.WebDriver; 12 | 13 | import de.retest.recheck.Recheck; 14 | import de.retest.recheck.RecheckImpl; 15 | import de.retest.recheck.junit.jupiter.RecheckExtension; 16 | 17 | @ExtendWith( RecheckExtension.class ) 18 | class CenterIT { 19 | WebDriver driver; 20 | Recheck re; 21 | 22 | @BeforeEach 23 | void before() { 24 | re = new RecheckImpl(); 25 | } 26 | 27 | @ParameterizedTest( name = "center-{1}" ) 28 | @MethodSource( "de.retest.web.testutils.WebDriverFactory#drivers" ) 29 | void testCenter( final WebDriver driver, final String name ) throws Exception { 30 | this.driver = driver; 31 | driver.get( page( CENTER ) ); 32 | 33 | Thread.sleep( 1000 ); 34 | 35 | re.check( driver, "open" ); 36 | } 37 | 38 | @AfterEach 39 | void tearDown() { 40 | driver.quit(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature 3 | about: Create a issue that suggests or improves a feature 4 | labels: enhancement 5 | --- 6 | 7 | *Before submission, please check that ...* 8 | 9 | - [ ] this is related specifically to recheck-web and [recheck](https://github.com/retest/recheck) itself. 10 | - [ ] the [documentation](https://docs.retest.de/) does not mention a similar solution (e.g. possible configurations). 11 | - [ ] there are no open or closed issues that are related to this idea. 12 | - [ ] this, to my best knowledge, will not break something else. 13 | 14 | 15 | 16 | ## Problem 17 | 18 | 19 | 20 | ### Solution 21 | 22 | 23 | 24 | ### Additional Context 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/main/java/de/retest/web/selenium/AutocheckingNavigationWrapper.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.selenium; 2 | 3 | import java.net.URL; 4 | 5 | import org.openqa.selenium.WebDriver.Navigation; 6 | 7 | public class AutocheckingNavigationWrapper implements Navigation { 8 | 9 | private final Navigation delegate; 10 | private final AutocheckingRecheckDriver autocheckingDriver; 11 | 12 | public AutocheckingNavigationWrapper( final Navigation delegate, 13 | final AutocheckingRecheckDriver autocheckingDriver ) { 14 | this.delegate = delegate; 15 | this.autocheckingDriver = autocheckingDriver; 16 | } 17 | 18 | @Override 19 | public void back() { 20 | delegate.back(); 21 | autocheckingDriver.check( "navigate-back" ); 22 | } 23 | 24 | @Override 25 | public void forward() { 26 | delegate.forward(); 27 | autocheckingDriver.check( "navigate-backforward" ); 28 | } 29 | 30 | @Override 31 | public void to( final String url ) { 32 | delegate.to( url ); 33 | autocheckingDriver.check( "navigate-to" ); 34 | } 35 | 36 | @Override 37 | public void to( final URL url ) { 38 | delegate.to( url ); 39 | autocheckingDriver.check( "navigate-to" ); 40 | } 41 | 42 | @Override 43 | public void refresh() { 44 | delegate.refresh(); 45 | autocheckingDriver.check( "refresh" ); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/test/java/de/retest/web/it/FramePageIT.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.it; 2 | 3 | import static de.retest.web.testutils.PageFactory.page; 4 | import static de.retest.web.testutils.PageFactory.Page.FRAME_PAGE; 5 | 6 | import org.junit.jupiter.api.AfterEach; 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.extension.ExtendWith; 9 | import org.junit.jupiter.params.ParameterizedTest; 10 | import org.junit.jupiter.params.provider.MethodSource; 11 | import org.openqa.selenium.WebDriver; 12 | 13 | import de.retest.recheck.Recheck; 14 | import de.retest.recheck.RecheckImpl; 15 | import de.retest.recheck.junit.jupiter.RecheckExtension; 16 | 17 | @ExtendWith( RecheckExtension.class ) 18 | class FramePageIT { 19 | 20 | WebDriver driver; 21 | Recheck re; 22 | 23 | @BeforeEach 24 | void setUp() { 25 | re = new RecheckImpl(); 26 | } 27 | 28 | @ParameterizedTest( name = "frame-page-{1}" ) 29 | @MethodSource( "de.retest.web.testutils.WebDriverFactory#drivers" ) 30 | void frame_page_html_should_be_checked( final WebDriver driver, final String name ) throws Exception { 31 | this.driver = driver; 32 | driver.get( page( FRAME_PAGE ) ); 33 | 34 | Thread.sleep( 1000 ); 35 | re.check( driver, "open" ); 36 | } 37 | 38 | @AfterEach 39 | void tearDown() { 40 | driver.quit(); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/test/java/de/retest/web/it/SimplePageIT.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.it; 2 | 3 | import static de.retest.web.testutils.PageFactory.page; 4 | import static de.retest.web.testutils.PageFactory.Page.SIMPLE_PAGE; 5 | 6 | import org.junit.jupiter.api.AfterEach; 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.extension.ExtendWith; 9 | import org.junit.jupiter.params.ParameterizedTest; 10 | import org.junit.jupiter.params.provider.MethodSource; 11 | import org.openqa.selenium.WebDriver; 12 | 13 | import de.retest.recheck.Recheck; 14 | import de.retest.recheck.RecheckImpl; 15 | import de.retest.recheck.junit.jupiter.RecheckExtension; 16 | 17 | @ExtendWith( RecheckExtension.class ) 18 | class SimplePageIT { 19 | 20 | WebDriver driver; 21 | Recheck re; 22 | 23 | @BeforeEach 24 | void setUp() { 25 | re = new RecheckImpl(); 26 | } 27 | 28 | @ParameterizedTest( name = "simple-page-{1}" ) 29 | @MethodSource( "de.retest.web.testutils.WebDriverFactory#drivers" ) 30 | void simple_html_page_should_be_checked( final WebDriver driver, final String name ) throws Exception { 31 | this.driver = driver; 32 | driver.get( page( SIMPLE_PAGE ) ); 33 | 34 | Thread.sleep( 1000 ); 35 | 36 | re.check( driver, "open" ); 37 | } 38 | 39 | @AfterEach 40 | void tearDown() { 41 | driver.quit(); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/test/java/de/retest/web/it/ShowcaseIT.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.it; 2 | 3 | import static de.retest.web.testutils.PageFactory.page; 4 | import static de.retest.web.testutils.PageFactory.Page.SHOWCASE; 5 | 6 | import org.junit.jupiter.api.AfterEach; 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.extension.ExtendWith; 9 | import org.junit.jupiter.params.ParameterizedTest; 10 | import org.junit.jupiter.params.provider.MethodSource; 11 | import org.openqa.selenium.WebDriver; 12 | 13 | import de.retest.recheck.Recheck; 14 | import de.retest.recheck.RecheckImpl; 15 | import de.retest.recheck.junit.jupiter.RecheckExtension; 16 | 17 | // Use extension to administer lifecycle of test 18 | @ExtendWith( RecheckExtension.class ) 19 | class ShowcaseIT { 20 | 21 | WebDriver driver; 22 | Recheck re; 23 | 24 | @BeforeEach 25 | void setUp() { 26 | re = new RecheckImpl(); 27 | } 28 | 29 | @ParameterizedTest( name = "showcase-{1}" ) 30 | @MethodSource( "de.retest.web.testutils.WebDriverFactory#drivers" ) 31 | void showcase_html_should_be_checked( final WebDriver driver, final String name ) throws Exception { 32 | this.driver = driver; 33 | driver.get( page( SHOWCASE ) ); 34 | 35 | Thread.sleep( 1000 ); 36 | 37 | re.check( driver, "open" ); 38 | } 39 | 40 | @AfterEach 41 | void tearDown() { 42 | driver.quit(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.github/workflows/deploy-release.yml: -------------------------------------------------------------------------------- 1 | name: Deploy releases 2 | 3 | on: 4 | create: 5 | tags: v* 6 | 7 | env: 8 | MVN_ARGS: --batch-mode --errors --fail-fast --no-transfer-progress 9 | 10 | jobs: 11 | default: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | 18 | - uses: actions/setup-java@v1 19 | with: 20 | java-version: 8 21 | server-id: ossrh 22 | server-username: MAVEN_USERNAME 23 | server-password: MAVEN_PASSWORD 24 | 25 | - name: Cache local Maven repository 26 | uses: actions/cache@v2 27 | with: 28 | path: ~/.m2/repository 29 | key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} 30 | restore-keys: ${{ runner.os }}-maven- 31 | 32 | - name: Install gpg secret key 33 | run: | 34 | gpg --batch --import <(echo -e "${{ secrets.OSSRH_GPG_SECRET_KEY }}") 35 | gpg --list-secret-keys --keyid-format LONG 36 | 37 | - id: publish-to-central 38 | name: Publish to Central Repository 39 | run: | 40 | mvn ${MVN_ARGS} -DskipTests -Psign clean deploy 41 | env: 42 | GPG_PASSPHRASE: ${{ secrets.OSSRH_GPG_PASSPHRASE }} 43 | MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }} 44 | MAVEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }} 45 | -------------------------------------------------------------------------------- /.github/workflows/release-hotfix-start.yml: -------------------------------------------------------------------------------- 1 | name: Release hotfix start 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | approval: 7 | description: 'Do you really want to start a HOTFIX release from MAIN branch?' 8 | required: true 9 | default: 'NO' 10 | 11 | env: 12 | MVN_ARGS: --batch-mode --errors --fail-fast --no-transfer-progress 13 | 14 | jobs: 15 | default: 16 | 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Verify approval 21 | run: "[[ $(echo ${{ github.event.inputs.approval }} | tr 'a-z' 'A-Z') == 'YES' ]]" 22 | 23 | - uses: actions/checkout@v2 24 | with: 25 | token: ${{ secrets.TRIGGER_ACTIONS_GITHUB_TOKEN }} 26 | 27 | - uses: actions/setup-java@v1 28 | with: 29 | java-version: 8 30 | 31 | - name: Cache local Maven repository 32 | uses: actions/cache@v2 33 | with: 34 | path: ~/.m2/repository 35 | key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} 36 | restore-keys: ${{ runner.os }}-maven- 37 | 38 | - name: Configure Git user 39 | run: | 40 | git config user.email "ops+githubactions@retest.de" 41 | git config user.name "retest release github action" 42 | 43 | 44 | - name: Start release branch 45 | run: mvn ${MVN_ARGS} gitflow:hotfix-start 46 | -------------------------------------------------------------------------------- /src/test/java/de/retest/web/selenium/UnbreakableDriverTest.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.selenium; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.mockito.Mockito.mock; 5 | import static org.mockito.Mockito.when; 6 | import static org.mockito.Mockito.withSettings; 7 | 8 | import org.junit.jupiter.api.Test; 9 | import org.openqa.selenium.WrapsDriver; 10 | import org.openqa.selenium.remote.RemoteWebDriver; 11 | 12 | class UnbreakableDriverTest { 13 | 14 | @Test 15 | void should_unwrap_wrapped_driver_if_possible() throws Exception { 16 | final RemoteWebDriver inner = mock( RemoteWebDriver.class ); 17 | 18 | final RemoteWebDriver oldOuter = mock( RemoteWebDriver.class, 19 | withSettings().extraInterfaces( org.openqa.selenium.WrapsDriver.class ) ); 20 | when( ((WrapsDriver) oldOuter).getWrappedDriver() ).thenReturn( inner ); 21 | 22 | final RemoteWebDriver newOuter = 23 | mock( RemoteWebDriver.class, withSettings().extraInterfaces( WrapsDriver.class ) ); 24 | when( ((WrapsDriver) newOuter).getWrappedDriver() ).thenReturn( inner ); 25 | 26 | assertThat( new UnbreakableDriver( inner ).getWrappedDriver() ).isSameAs( inner ); 27 | assertThat( new UnbreakableDriver( oldOuter ).getWrappedDriver() ).isSameAs( inner ); 28 | assertThat( new UnbreakableDriver( newOuter ).getWrappedDriver() ).isSameAs( inner ); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/release-feature-start.yml: -------------------------------------------------------------------------------- 1 | name: Release feature start 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | approval: 7 | description: 'Do you really want to start a FEATURE release from DEVELOP branch?' 8 | required: true 9 | default: 'NO' 10 | 11 | env: 12 | MVN_ARGS: --batch-mode --errors --fail-fast --no-transfer-progress 13 | 14 | jobs: 15 | default: 16 | 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Verify approval 21 | run: "[[ $(echo ${{ github.event.inputs.approval }} | tr 'a-z' 'A-Z') == 'YES' ]]" 22 | 23 | - uses: actions/checkout@v2 24 | with: 25 | token: ${{ secrets.TRIGGER_ACTIONS_GITHUB_TOKEN }} 26 | 27 | - uses: actions/setup-java@v1 28 | with: 29 | java-version: 8 30 | 31 | - name: Cache local Maven repository 32 | uses: actions/cache@v2 33 | with: 34 | path: ~/.m2/repository 35 | key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} 36 | restore-keys: ${{ runner.os }}-maven- 37 | 38 | - name: Configure Git user 39 | run: | 40 | git config user.email "ops+githubactions@retest.de" 41 | git config user.name "retest release github action" 42 | 43 | 44 | - name: Start release branch 45 | run: mvn ${MVN_ARGS} gitflow:release-start 46 | -------------------------------------------------------------------------------- /.github/workflows/release-hotfix-finish.yml: -------------------------------------------------------------------------------- 1 | name: Release hotfix finish 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | approval: 7 | description: 'Do you really want to finish the current HOTFIX release?' 8 | required: true 9 | default: 'NO' 10 | 11 | env: 12 | MVN_ARGS: --batch-mode --errors --fail-fast --no-transfer-progress 13 | 14 | jobs: 15 | default: 16 | 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Verify approval 21 | run: "[[ $(echo ${{ github.event.inputs.approval }} | tr 'a-z' 'A-Z') == 'YES' ]]" 22 | 23 | - uses: actions/checkout@v2 24 | with: 25 | fetch-depth: 0 26 | token: ${{ secrets.TRIGGER_ACTIONS_GITHUB_TOKEN }} 27 | 28 | - uses: actions/setup-java@v1 29 | with: 30 | java-version: 8 31 | 32 | - name: Cache local Maven repository 33 | uses: actions/cache@v2 34 | with: 35 | path: ~/.m2/repository 36 | key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} 37 | restore-keys: ${{ runner.os }}-maven- 38 | 39 | - name: Configure Git user 40 | run: | 41 | git config user.email "ops+githubactions@retest.de" 42 | git config user.name "retest release github action" 43 | 44 | - name: Tag and finish release branch 45 | run: mvn ${MVN_ARGS} gitflow:hotfix-finish 46 | -------------------------------------------------------------------------------- /.github/workflows/release-feature-finish.yml: -------------------------------------------------------------------------------- 1 | name: Release feature finish 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | approval: 7 | description: 'Do you really want to finish the current FEATURE release?' 8 | required: true 9 | default: 'NO' 10 | 11 | env: 12 | MVN_ARGS: --batch-mode --errors --fail-fast --no-transfer-progress 13 | 14 | jobs: 15 | default: 16 | 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Verify approval 21 | run: "[[ $(echo ${{ github.event.inputs.approval }} | tr 'a-z' 'A-Z') == 'YES' ]]" 22 | 23 | - uses: actions/checkout@v2 24 | with: 25 | fetch-depth: 0 26 | token: ${{ secrets.TRIGGER_ACTIONS_GITHUB_TOKEN }} 27 | 28 | - uses: actions/setup-java@v1 29 | with: 30 | java-version: 8 31 | 32 | - name: Cache local Maven repository 33 | uses: actions/cache@v2 34 | with: 35 | path: ~/.m2/repository 36 | key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} 37 | restore-keys: ${{ runner.os }}-maven- 38 | 39 | - name: Configure Git user 40 | run: | 41 | git config user.email "ops+githubactions@retest.de" 42 | git config user.name "retest release github action" 43 | 44 | - name: Tag and finish release branch 45 | run: mvn ${MVN_ARGS} gitflow:release-finish 46 | -------------------------------------------------------------------------------- /src/main/java/de/retest/web/selenium/AutocheckingCheckNamingStrategy.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.selenium; 2 | 3 | import org.openqa.selenium.WebElement; 4 | 5 | /** 6 | * Interface to create names for checks for the {@link AutocheckingRecheckDriver}. These names must be unique within a 7 | * test, but can repeat in between tests (i.e. after calling {@link #nextTest()}). 8 | */ 9 | public interface AutocheckingCheckNamingStrategy { 10 | 11 | /** 12 | * Return a string that is a unique identifier in the current test. 13 | * 14 | * @param action 15 | * The action to use when creating a semantic name (e.g. "click"). 16 | * @param target 17 | * The target of the action or null. 18 | * @param params 19 | * Optional params of the action, like the URL for a "get" or the text for a "enter". 20 | * @return The unique identifier. 21 | */ 22 | String getUniqueCheckName( String action, WebElement target, Object... params ); 23 | 24 | /** 25 | * Return a string that is a unique identifier in the current test. 26 | * 27 | * @param action 28 | * The action to use when creating a semantic name (e.g. "navigate-back"). 29 | * @return The unique identifier. 30 | */ 31 | String getUniqueCheckName( String action ); 32 | 33 | /** 34 | * Reset this naming strategy, such that strings can be reused in different tests. 35 | */ 36 | void nextTest(); 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/de/retest/web/it/FormPageIT.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.it; 2 | 3 | import static de.retest.web.testutils.PageFactory.page; 4 | import static de.retest.web.testutils.PageFactory.Page.FORM_PAGE; 5 | 6 | import org.junit.jupiter.api.AfterEach; 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.extension.ExtendWith; 9 | import org.junit.jupiter.params.ParameterizedTest; 10 | import org.junit.jupiter.params.provider.MethodSource; 11 | import org.openqa.selenium.WebDriver; 12 | 13 | import de.retest.recheck.Recheck; 14 | import de.retest.recheck.RecheckImpl; 15 | import de.retest.recheck.junit.jupiter.RecheckExtension; 16 | import de.retest.web.selenium.By; 17 | 18 | @ExtendWith( RecheckExtension.class ) 19 | class FormPageIT { 20 | 21 | WebDriver driver; 22 | Recheck re; 23 | 24 | @BeforeEach 25 | void setUp() { 26 | re = new RecheckImpl(); 27 | } 28 | 29 | @ParameterizedTest( name = "form-page-{1}" ) 30 | @MethodSource( "de.retest.web.testutils.WebDriverFactory#drivers" ) 31 | void form_page_html_should_be_checked( final WebDriver driver, final String name ) throws Exception { 32 | this.driver = driver; 33 | driver.get( page( FORM_PAGE ) ); 34 | 35 | driver.findElement( By.id( "email" ) ).sendKeys( "Max" ); 36 | 37 | Thread.sleep( 1000 ); 38 | re.check( driver, "open" ); 39 | } 40 | 41 | @AfterEach 42 | void tearDown() { 43 | driver.quit(); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/de/retest/web/AttributesUtil.java: -------------------------------------------------------------------------------- 1 | package de.retest.web; 2 | 3 | import java.util.Arrays; 4 | import java.util.HashSet; 5 | import java.util.Set; 6 | 7 | public class AttributesUtil { 8 | 9 | public static final String ABSOLUTE_X = "absolute-x"; 10 | public static final String ABSOLUTE_Y = "absolute-y"; 11 | public static final String ABSOLUTE_WIDTH = "absolute-width"; 12 | public static final String ABSOLUTE_HEIGHT = "absolute-height"; 13 | 14 | public static final String X = "x"; 15 | public static final String Y = "y"; 16 | public static final String WIDTH = "width"; 17 | public static final String HEIGHT = "height"; 18 | 19 | public static final String TEXT = "text"; 20 | 21 | public static final String CLASS = "class"; 22 | public static final String ID = "id"; 23 | public static final String NAME = "name"; 24 | 25 | // Mapped to our "type" attribute in WebElementPeer to avoid conflicts with HTML "type" attribute. 26 | public static final String TAG_NAME = "tagName"; 27 | 28 | // Keys used in getAllElementsByPath.js. 29 | private static final Set identifyingAttributes = new HashSet<>( Arrays.asList( ABSOLUTE_X, ABSOLUTE_Y, 30 | ABSOLUTE_WIDTH, ABSOLUTE_HEIGHT, X, Y, WIDTH, HEIGHT, TEXT, CLASS, ID, NAME, TAG_NAME ) ); 31 | 32 | private AttributesUtil() {} 33 | 34 | public static boolean isIdentifyingAttribute( final String key ) { 35 | return identifyingAttributes.contains( key ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/de/retest/web/it/SpecialContainerIT.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.it; 2 | 3 | import org.junit.jupiter.api.AfterEach; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.extension.ExtendWith; 6 | import org.junit.jupiter.params.ParameterizedTest; 7 | import org.junit.jupiter.params.provider.MethodSource; 8 | import org.openqa.selenium.WebDriver; 9 | 10 | import de.retest.recheck.Recheck; 11 | import de.retest.recheck.RecheckImpl; 12 | import de.retest.recheck.junit.jupiter.RecheckExtension; 13 | import de.retest.web.testutils.PageFactory; 14 | 15 | @ExtendWith( RecheckExtension.class ) 16 | class SpecialContainerIT { 17 | WebDriver driver; 18 | Recheck re; 19 | 20 | @BeforeEach 21 | void setUp() { 22 | re = new RecheckImpl(); 23 | } 24 | 25 | @ParameterizedTest( name = "special-container-{1}" ) 26 | @MethodSource( "de.retest.web.testutils.WebDriverFactory#drivers" ) 27 | void testCenter( final WebDriver driver, final String name ) throws Exception { 28 | this.driver = driver; 29 | driver.get( PageFactory.toPageUrlString( "special-container.html" ) ); 30 | 31 | re.check( driver, "open" ); 32 | 33 | // Comment in to create golden master with the expected change 34 | // driver.findElement( org.openqa.selenium.By.id( "change-for-div" ) ).click(); 35 | 36 | // This test should then have exactly one difference 37 | re.check( driver, "click" ); 38 | } 39 | 40 | @AfterEach 41 | void tearDown() { 42 | driver.quit(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/de/retest/web/meta/driver/capabilities/PlatformMetadataProvider.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.meta.driver.capabilities; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | import java.util.Objects; 6 | 7 | import org.openqa.selenium.Capabilities; 8 | import org.openqa.selenium.Platform; 9 | import org.openqa.selenium.WebDriverException; 10 | 11 | import de.retest.recheck.meta.MetadataProvider; 12 | import de.retest.web.meta.SeleniumMetadata; 13 | import lombok.AccessLevel; 14 | import lombok.RequiredArgsConstructor; 15 | 16 | @RequiredArgsConstructor( access = AccessLevel.PACKAGE ) 17 | public final class PlatformMetadataProvider implements MetadataProvider { 18 | 19 | private final Capabilities capabilities; 20 | 21 | @Override 22 | public Map retrieve() { 23 | final Map map = new HashMap<>(); 24 | 25 | final Platform platform = retrievePlatformSilently(); 26 | if ( platform != null ) { 27 | map.put( SeleniumMetadata.OS_NAME, platform.toString() ); 28 | final Object version = capabilities.getCapability( "platformVersion" ); 29 | map.put( SeleniumMetadata.OS_VERSION, Objects.toString( version, "" ) ); 30 | map.put( SeleniumMetadata.OS_ARCH, "" ); 31 | } 32 | 33 | return map; 34 | } 35 | 36 | private Platform retrievePlatformSilently() { 37 | try { 38 | return capabilities.getPlatform(); 39 | } catch ( final IllegalArgumentException | WebDriverException e ) { 40 | // failed 41 | return null; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/de/retest/web/meta/element/WebElementMetadataProvider.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.meta.element; 2 | 3 | import org.openqa.selenium.WebDriver; 4 | import org.openqa.selenium.WebElement; 5 | import org.openqa.selenium.remote.RemoteWebElement; 6 | 7 | import de.retest.recheck.meta.MetadataProvider; 8 | import de.retest.web.meta.driver.WebDriverMetadataProvider; 9 | import de.retest.web.util.SeleniumWrapperUtil; 10 | import de.retest.web.util.SeleniumWrapperUtil.WrapperOf; 11 | import lombok.extern.slf4j.Slf4j; 12 | 13 | @Slf4j 14 | public final class WebElementMetadataProvider { 15 | 16 | public static MetadataProvider of( final WebElement element ) { 17 | return extractDriverMetadataProvider( element ); 18 | } 19 | 20 | private static MetadataProvider extractDriverMetadataProvider( final WebElement element ) { 21 | if ( SeleniumWrapperUtil.isWrapper( WrapperOf.DRIVER, element ) ) { 22 | final Object wrapped = SeleniumWrapperUtil.getWrapped( WrapperOf.DRIVER, element ); 23 | return WebDriverMetadataProvider.of( (WebDriver) wrapped ); 24 | } 25 | if ( SeleniumWrapperUtil.isWrapper( WrapperOf.ELEMENT, element ) ) { 26 | final Object wrapped = SeleniumWrapperUtil.getWrapped( WrapperOf.ELEMENT, element ); 27 | return extractDriverMetadataProvider( (WebElement) wrapped ); 28 | } 29 | log.debug( "Cannot retrieve driver from element {}. Element must be of '{}'. Returning empty metadata.", 30 | element, RemoteWebElement.class ); 31 | return MetadataProvider.empty(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/de/retest/web/meta/driver/WebDriverMetadataProvider.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.meta.driver; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | import org.openqa.selenium.Dimension; 7 | import org.openqa.selenium.WebDriver; 8 | 9 | import de.retest.recheck.meta.MetadataProvider; 10 | import de.retest.recheck.meta.MultiMetadataProvider; 11 | import de.retest.web.meta.SeleniumMetadata; 12 | import de.retest.web.meta.driver.capabilities.CapabilityMetadataProvider; 13 | import lombok.AccessLevel; 14 | import lombok.RequiredArgsConstructor; 15 | 16 | @RequiredArgsConstructor( access = AccessLevel.PACKAGE ) 17 | public final class WebDriverMetadataProvider implements MetadataProvider { 18 | 19 | private final WebDriver driver; 20 | 21 | public static MetadataProvider of( final WebDriver driver ) { 22 | return MultiMetadataProvider.of( // 23 | CapabilityMetadataProvider.of( driver ), // 24 | new WebDriverMetadataProvider( driver ) // 25 | ); 26 | } 27 | 28 | @Override 29 | public Map retrieve() { 30 | final Map map = new HashMap<>(); 31 | 32 | map.put( SeleniumMetadata.DRIVER_TYPE, driver.getClass().getSimpleName() ); 33 | map.put( SeleniumMetadata.URL, driver.getCurrentUrl() ); 34 | 35 | final Dimension size = driver.manage().window().getSize(); 36 | map.put( SeleniumMetadata.WINDOW_WIDTH, String.valueOf( size.getWidth() ) ); 37 | map.put( SeleniumMetadata.WINDOW_HEIGHT, String.valueOf( size.getHeight() ) ); 38 | 39 | return map; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/test/resources/pages/showcase/assets/init.js: -------------------------------------------------------------------------------- 1 | /*----------------------------------------------------------------------------------- 2 | /* 3 | /* Init JS 4 | /* 5 | -----------------------------------------------------------------------------------*/ 6 | 7 | jQuery(document).ready(function() { 8 | 9 | /*----------------------------------------------------*/ 10 | /* Navigation - Double Tap to Go 11 | ------------------------------------------------------*/ 12 | 13 | $( '#nav li:has(ul)' ).doubleTapToGo(); 14 | 15 | /*----------------------------------------------------*/ 16 | /* Back To Top Button 17 | /*----------------------------------------------------*/ 18 | var pxShow = 300; //height on which the button will show 19 | var fadeInTime = 400; //how slow/fast you want the button to show 20 | var fadeOutTime = 400; //how slow/fast you want the button to hide 21 | var scrollSpeed = 300; //how slow/fast you want the button to scroll to top. can be a value, 'slow', 'normal' or 'fast' 22 | 23 | // Show or hide the sticky footer button 24 | jQuery(window).scroll(function() { 25 | 26 | if (jQuery(window).scrollTop() >= pxShow) { 27 | jQuery("#go-top").fadeIn(fadeInTime); 28 | } else { 29 | jQuery("#go-top").fadeOut(fadeOutTime); 30 | } 31 | 32 | }); 33 | 34 | // Animate the scroll to top 35 | jQuery("#go-top a").click(function() { 36 | jQuery("html, body").animate({scrollTop:0}, scrollSpeed); 37 | return false; 38 | }); 39 | }); 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/main/java/de/retest/web/screenshot/ScreenshotProvider.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.screenshot; 2 | 3 | import java.awt.image.BufferedImage; 4 | 5 | import org.openqa.selenium.WebDriver; 6 | import org.openqa.selenium.WebElement; 7 | 8 | /** 9 | * The screenshot provider for way screenshots are taken. 10 | * 11 | * The ScreenshotProvider can choose whether screenshots are taken for the full page, just the viewport or none at all. 12 | * The Default implementation is {@link ViewportOnlyMinimalScreenshot}. 13 | */ 14 | public interface ScreenshotProvider { 15 | 16 | /** 17 | * Takes the screenshot from the {@link WebDriver}. 18 | * 19 | * @param driver 20 | * Webdriver for taking the screenshot. 21 | * @return screenshot image of the tested page. May return {@code null} if screenshots are not supported. 22 | * @throws Exception 23 | * If an exception occurred during screenshot creation. 24 | */ 25 | BufferedImage shoot( WebDriver driver ) throws Exception; 26 | 27 | /** 28 | * Takes the screenshot from {@link WebElement}. 29 | * 30 | * @param driver 31 | * Webdriver for taking the screenshot. 32 | * @param element 33 | * WebElement where screenshot is taken from. 34 | * @return screenshot image of tested web element. May return {@code null} if screenshots are not supported. 35 | * @throws Exception 36 | * If an exception occurred during screenshot creation. 37 | */ 38 | BufferedImage shoot( WebDriver driver, WebElement element ) throws Exception; 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/test/java/de/retest/web/testutils/SystemPropertyExtension.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.testutils; 2 | 3 | import java.util.Properties; 4 | 5 | import org.junit.jupiter.api.extension.AfterEachCallback; 6 | import org.junit.jupiter.api.extension.BeforeEachCallback; 7 | import org.junit.jupiter.api.extension.ExtensionContext; 8 | import org.junit.jupiter.api.extension.ExtensionContext.Namespace; 9 | import org.junit.jupiter.api.extension.ExtensionContext.Store; 10 | 11 | public class SystemPropertyExtension implements BeforeEachCallback, AfterEachCallback { 12 | 13 | private static final String BACKUP_STORE_KEY = "backup"; 14 | 15 | @Override 16 | public void beforeEach( final ExtensionContext context ) throws Exception { 17 | final Properties backup = new Properties(); 18 | backup.putAll( System.getProperties() ); 19 | final Store store = context.getStore( Namespace.create( getClass(), context.getRequiredTestMethod() ) ); 20 | store.put( BACKUP_STORE_KEY, backup ); 21 | final SystemProperty prop = context.getTestMethod().get().getAnnotation( SystemProperty.class ); 22 | if ( prop.value().isEmpty() ) { 23 | System.clearProperty( prop.key() ); 24 | } else { 25 | System.setProperty( prop.key(), prop.value() ); 26 | } 27 | } 28 | 29 | @Override 30 | public void afterEach( final ExtensionContext context ) throws Exception { 31 | final Store store = context.getStore( Namespace.create( getClass(), context.getRequiredTestMethod() ) ); 32 | final Properties backup = store.get( BACKUP_STORE_KEY, Properties.class ); 33 | System.setProperties( backup ); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/de/retest/web/screenshot/FullPageScreenshot.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.screenshot; 2 | 3 | import static de.retest.recheck.ui.image.ImageUtils.resizeImage; 4 | import static de.retest.web.screenshot.ScreenshotProviders.SCALE; 5 | 6 | import java.awt.image.BufferedImage; 7 | 8 | import org.openqa.selenium.WebDriver; 9 | import org.openqa.selenium.WebElement; 10 | import org.openqa.selenium.chrome.ChromeDriver; 11 | 12 | import com.assertthat.selenium_shutterbug.core.Capture; 13 | import com.assertthat.selenium_shutterbug.core.Shutterbug; 14 | 15 | /** 16 | * Scrolls to the bottom of the page and takes a screenshot of the entire page. 17 | */ 18 | public class FullPageScreenshot implements ScreenshotProvider { 19 | 20 | private static final int SCROLL_TIMEOUT_MS = 100; 21 | private static final boolean USE_DEVICE_PIXEL_RATIO = true; 22 | 23 | @Override 24 | public BufferedImage shoot( final WebDriver driver ) { 25 | if ( driver instanceof ChromeDriver ) { 26 | final BufferedImage image = Shutterbug // 27 | .shootPage( driver, Capture.FULL, SCROLL_TIMEOUT_MS, USE_DEVICE_PIXEL_RATIO ) // 28 | .getImage(); 29 | return resizeImage( image, image.getWidth() / SCALE, image.getHeight() / SCALE ); 30 | } 31 | return Shutterbug // 32 | .shootPage( driver, Capture.FULL, SCROLL_TIMEOUT_MS, USE_DEVICE_PIXEL_RATIO ) // 33 | .getImage(); 34 | } 35 | 36 | @Override 37 | public BufferedImage shoot( final WebDriver driver, final WebElement element ) { 38 | return Shutterbug.shootElement( driver, element, USE_DEVICE_PIXEL_RATIO ).getImage(); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug 3 | about: Create a bug report to help us improve 4 | labels: bug 5 | --- 6 | 7 | *Before submission, please check that ...* 8 | 9 | - [ ] this is related specifically to recheck-web and [recheck](https://github.com/retest/recheck) itself. 10 | - [ ] the [documentation](https://docs.retest.de/) does not mention a fix (e.g. wrong API usage, ...). 11 | - [ ] the bug is not related to a customization (e.g. custom implementations). 12 | - [ ] there are no open or closed issues that are related to this problem. 13 | 14 | 15 | 16 | ## Describe the Bug 17 | 18 | 19 | 20 | ### How to Reproduce? 21 | 22 | 23 | 24 | Steps to reproduce the behavior: 25 | 1. ... 26 | 2. ... 27 | 3. ... 28 | 29 | ### Setup 30 | 31 | 32 | 33 | - recheck-web: [e.g. 1.6.0] 34 | - Selenium version: [e.g. 3.141.59] 35 | - Testing framework: [e.g. JUnit5 5.5.2, TestNG 7.0.0] 36 | - OS: [e.g. MacOS 14, Windows 10] 37 | - Java version: [e.g. OpenJDK 11] 38 | 39 | ### Additional Context 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/main/java/de/retest/web/selenium/css/RegexTransformer.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.selenium.css; 2 | 3 | import java.util.function.Predicate; 4 | import java.util.regex.Matcher; 5 | import java.util.regex.Pattern; 6 | 7 | import de.retest.recheck.ui.descriptors.Element; 8 | 9 | class RegexTransformer implements Transformer { 10 | 11 | private static final String REMAINING_GROUP = "remaining"; 12 | private static final String REMAINING = "(?<" + REMAINING_GROUP + ">.*)$"; 13 | private static final String START_OF_LINE = "^"; 14 | 15 | private final Pattern cssPattern; 16 | private final PredicateFactory factory; 17 | 18 | private RegexTransformer( final String pattern, final PredicateFactory factory ) { 19 | cssPattern = Pattern.compile( START_OF_LINE + pattern + REMAINING ); 20 | this.factory = factory; 21 | } 22 | 23 | static Transformer of( final String pattern, final PredicateFactory factory ) { 24 | return new RegexTransformer( pattern, factory ); 25 | } 26 | 27 | @Override 28 | public Selector transform( final String selector ) { 29 | final Matcher matcher = cssPattern.matcher( selector ); 30 | if ( matcher.find() ) { 31 | return newSelector( matcher ); 32 | } 33 | return Selector.unsupported( selector ); 34 | } 35 | 36 | private Selector newSelector( final Matcher matcher ) { 37 | final String cssAttribute = matcher.group( 1 ); 38 | final String remainingSelector = matcher.group( REMAINING_GROUP ).trim(); 39 | final Predicate predicate = factory.create( cssAttribute ); 40 | return Selector.supported( remainingSelector, predicate ); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/test/java/de/retest/web/meta/driver/WebDriverMetadataProviderTest.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.meta.driver; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.mockito.Mockito.RETURNS_MOCKS; 5 | import static org.mockito.Mockito.mock; 6 | import static org.mockito.Mockito.when; 7 | 8 | import org.apache.commons.lang3.tuple.Pair; 9 | import org.junit.jupiter.api.Test; 10 | import org.openqa.selenium.WebDriver; 11 | import org.openqa.selenium.remote.RemoteWebDriver; 12 | 13 | import de.retest.recheck.meta.MetadataProvider; 14 | import de.retest.web.meta.SeleniumMetadata; 15 | import de.retest.web.selenium.UnbreakableDriver; 16 | 17 | class WebDriverMetadataProviderTest { 18 | 19 | @Test 20 | void retrieve_should_directly_take_driver_type() throws Exception { 21 | final WebDriver driver = mock( WebDriver.class, RETURNS_MOCKS ); 22 | 23 | final MetadataProvider cut = WebDriverMetadataProvider.of( driver ); 24 | 25 | assertThat( cut.retrieve() ) 26 | .contains( Pair.of( SeleniumMetadata.DRIVER_TYPE, driver.getClass().getSimpleName() ) ); 27 | } 28 | 29 | @Test 30 | void retrieve_should_directly_take_driver_type_if_wrapped() throws Exception { 31 | final UnbreakableDriver driver = mock( UnbreakableDriver.class, RETURNS_MOCKS ); 32 | when( driver.getWrappedDriver() ).thenReturn( mock( RemoteWebDriver.class, RETURNS_MOCKS ) ); 33 | 34 | final MetadataProvider cut = WebDriverMetadataProvider.of( driver ); 35 | 36 | assertThat( cut.retrieve() ) 37 | .contains( Pair.of( SeleniumMetadata.DRIVER_TYPE, driver.getClass().getSimpleName() ) ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/test/java/de/retest/web/it/WikipediaIT.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.it; 2 | 3 | import org.junit.jupiter.api.AfterEach; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.Disabled; 6 | import org.junit.jupiter.api.Test; 7 | import org.junit.jupiter.api.extension.ExtendWith; 8 | import org.openqa.selenium.WebDriver; 9 | 10 | import de.retest.recheck.Recheck; 11 | import de.retest.recheck.RecheckImpl; 12 | import de.retest.recheck.RecheckOptions; 13 | import de.retest.recheck.junit.jupiter.RecheckExtension; 14 | import de.retest.web.RecheckWebOptions; 15 | import de.retest.web.testutils.PageFactory; 16 | import de.retest.web.testutils.WebDriverFactory; 17 | import de.retest.web.testutils.WebDriverFactory.Driver; 18 | 19 | @Disabled( "We only use this to create an example.report file for review." ) 20 | @ExtendWith( RecheckExtension.class ) 21 | class WikipediaIT { 22 | 23 | WebDriver driver; 24 | Recheck re; 25 | 26 | @BeforeEach 27 | void setUp() throws Exception { 28 | driver = WebDriverFactory.driver( Driver.CHROME ); 29 | final RecheckOptions opts = RecheckWebOptions.builder() // 30 | .enableScreenshots() // 31 | .build(); 32 | re = new RecheckImpl( opts ); 33 | } 34 | 35 | @Test 36 | void myWikipediaTest() throws Exception { 37 | // Switch to "expected" subfolder to restore original page. 38 | driver.get( PageFactory.toPageUrlString( "wikipedia/actual/wikipedia-characterization-test.html" ) ); 39 | re.check( driver, "characterization-testing-page" ); 40 | } 41 | 42 | @AfterEach 43 | void tearDown() throws Exception { 44 | driver.close(); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/test/java/de/retest/web/testutils/PageFactory.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.testutils; 2 | 3 | import java.net.MalformedURLException; 4 | import java.nio.file.Paths; 5 | import java.util.Arrays; 6 | import java.util.stream.Stream; 7 | 8 | public class PageFactory { 9 | 10 | private static final String BASE_PATH = "src/test/resources/pages/"; 11 | 12 | public enum Page { 13 | SHOWCASE, 14 | SIMPLE_PAGE, 15 | FORM_PAGE, 16 | FRAME_PAGE, 17 | CENTER, 18 | COVERED_PAGE 19 | } 20 | 21 | public static String page( final Page page ) { 22 | switch ( page ) { 23 | case SHOWCASE: { 24 | return toPageUrlString( "showcase/retest.html" ); 25 | } 26 | case SIMPLE_PAGE: { 27 | return toPageUrlString( "simple-page.html" ); 28 | } 29 | case FORM_PAGE: { 30 | return toPageUrlString( "form-page.html" ); 31 | } 32 | case CENTER: { 33 | return toPageUrlString( "centered.html" ); 34 | } 35 | case FRAME_PAGE: { 36 | return toPageUrlString( "frame-outer.html" ); 37 | } 38 | case COVERED_PAGE: { 39 | return toPageUrlString( "covered-page.html" ); 40 | } 41 | default: 42 | throw new IllegalArgumentException( "No \"" + page + "\" page available." ); 43 | } 44 | } 45 | 46 | public static final String toPageUrlString( final String relativePath ) { 47 | try { 48 | return Paths.get( BASE_PATH, relativePath ).toUri().toURL().toString(); 49 | } catch ( final MalformedURLException e ) { 50 | return null; 51 | } 52 | } 53 | 54 | public static Stream pages() { 55 | return Arrays.stream( Page.values() ).map( PageFactory::page ); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/test/java/de/retest/web/meta/driver/capabilities/CapabilityMetadataProviderTest.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.meta.driver.capabilities; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.mockito.Mockito.RETURNS_MOCKS; 5 | import static org.mockito.Mockito.mock; 6 | import static org.mockito.Mockito.when; 7 | 8 | import org.junit.jupiter.api.Test; 9 | import org.openqa.selenium.WebDriver; 10 | import org.openqa.selenium.remote.RemoteWebDriver; 11 | 12 | import de.retest.recheck.meta.MetadataProvider; 13 | import de.retest.web.selenium.UnbreakableDriver; 14 | 15 | class CapabilityMetadataProviderTest { 16 | 17 | @Test 18 | void of_should_identify_driver() throws Exception { 19 | final WebDriver driver = mock( RemoteWebDriver.class, RETURNS_MOCKS ); 20 | 21 | final MetadataProvider cut = CapabilityMetadataProvider.of( driver ); 22 | 23 | assertThat( cut.retrieve() ).isNotEmpty(); 24 | } 25 | 26 | @Test 27 | void of_should_identify_wrapped_driver() throws Exception { 28 | final UnbreakableDriver wrapsDriver = mock( UnbreakableDriver.class, RETURNS_MOCKS ); 29 | when( wrapsDriver.getWrappedDriver() ).thenReturn( mock( RemoteWebDriver.class, RETURNS_MOCKS ) ); 30 | 31 | final MetadataProvider cut = CapabilityMetadataProvider.of( wrapsDriver ); 32 | 33 | assertThat( cut.retrieve() ).isNotEmpty(); 34 | } 35 | 36 | @Test 37 | void of_should_not_use_plain_web_driver_and_return_empty_provider() throws Exception { 38 | final WebDriver driver = mock( WebDriver.class ); 39 | 40 | final MetadataProvider cut = CapabilityMetadataProvider.of( driver ); 41 | 42 | assertThat( cut.retrieve() ).isEmpty(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/de/retest/web/selenium/AutocheckingTargetLocator.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.selenium; 2 | 3 | import org.openqa.selenium.Alert; 4 | import org.openqa.selenium.WebDriver; 5 | import org.openqa.selenium.WebElement; 6 | 7 | import lombok.RequiredArgsConstructor; 8 | import org.openqa.selenium.WindowType; 9 | 10 | @RequiredArgsConstructor 11 | public class AutocheckingTargetLocator implements WebDriver.TargetLocator { 12 | 13 | private final WebDriver.TargetLocator delegate; 14 | private final AutocheckingRecheckDriver driver; 15 | 16 | @Override 17 | public WebDriver frame( final int index ) { 18 | return delegate.frame( index ); 19 | } 20 | 21 | @Override 22 | public WebDriver frame( final String nameOrId ) { 23 | return delegate.frame( nameOrId ); 24 | } 25 | 26 | @Override 27 | public WebDriver frame( final WebElement frameElement ) { 28 | return delegate.frame( frameElement ); 29 | } 30 | 31 | @Override 32 | public WebDriver parentFrame() { 33 | return delegate.parentFrame(); 34 | } 35 | 36 | @Override 37 | public WebDriver window( final String nameOrHandle ) { 38 | final WebDriver driver = delegate.window( nameOrHandle ); 39 | this.driver.check( "switch-window" ); 40 | return driver; 41 | } 42 | 43 | @Override 44 | public WebDriver newWindow(WindowType typeHint) { 45 | return delegate.newWindow(typeHint); 46 | } 47 | 48 | @Override 49 | public WebDriver defaultContent() { 50 | return delegate.defaultContent(); 51 | } 52 | 53 | @Override 54 | public WebElement activeElement() { 55 | return driver.wrap( delegate.activeElement() ) ; 56 | } 57 | 58 | @Override 59 | public Alert alert() { 60 | return delegate.alert(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | queue_rules: 2 | - name: default 3 | conditions: 4 | - "#approved-reviews-by>=1" 5 | - "#changes-requested-reviews-by=0" 6 | - "-draft" 7 | 8 | pull_request_rules: 9 | - name: "rebase non-release PRs" 10 | conditions: 11 | - "head~=^(?!(release|hotfix)).*$" 12 | actions: 13 | rebase: 14 | 15 | - name: "merge non-release SonarCloud-checked PRs with strict rebase" 16 | conditions: 17 | - "head~=^(?!(release|hotfix)).*$" 18 | - "#approved-reviews-by>=1" 19 | - "#changes-requested-reviews-by=0" 20 | - "#commits-behind=0" 21 | - "-draft" 22 | - "check-success=SonarCloud Code Analysis" 23 | actions: 24 | queue: 25 | name: default 26 | update_method: rebase 27 | 28 | - name: "merge non-release without-SonarCloud-but-from-dependabot PRs with strict rebase" 29 | conditions: 30 | - "head~=^(?!(release|hotfix)).*$" 31 | - "#approved-reviews-by>=1" 32 | - "#changes-requested-reviews-by=0" 33 | - "#commits-behind=0" 34 | - "-draft" 35 | - "author=dependabot[bot]" 36 | actions: 37 | queue: 38 | name: default 39 | update_method: rebase 40 | 41 | - name: "merge release PRs with strict merge" 42 | conditions: 43 | - "head~=^(release|hotfix).*$" 44 | - "#approved-reviews-by>=1" 45 | - "#changes-requested-reviews-by=0" 46 | - "#commits-behind=0" 47 | - "-draft" 48 | actions: 49 | queue: 50 | name: default 51 | update_method: rebase 52 | 53 | - name: "delete PR branches after merge" 54 | conditions: 55 | - merged 56 | actions: 57 | delete_head_branch: {} 58 | -------------------------------------------------------------------------------- /src/main/java/de/retest/web/RecheckWebProperties.java: -------------------------------------------------------------------------------- 1 | package de.retest.web; 2 | 3 | import org.aeonbits.owner.Config.LoadPolicy; 4 | import org.aeonbits.owner.Config.LoadType; 5 | import org.aeonbits.owner.Config.Sources; 6 | import org.aeonbits.owner.ConfigCache; 7 | import org.aeonbits.owner.Reloadable; 8 | 9 | import de.retest.web.screenshot.ScreenshotProvider; 10 | import de.retest.web.screenshot.ScreenshotProviders.ScreenshotProviderConverter; 11 | 12 | /** 13 | * Interface for additional recheck-web properties, determined via system properties (first) or 14 | * .retest/retest.properties (second). For more information, please have a look at the 15 | * documentation. 16 | */ 17 | @LoadPolicy( LoadType.MERGE ) 18 | @Sources( { "system:properties", "file:${projectroot}/.retest/retest.properties" } ) 19 | public interface RecheckWebProperties extends Reloadable { 20 | 21 | /* 22 | * Basic usage. 23 | */ 24 | 25 | static RecheckWebProperties getInstance() { 26 | final RecheckWebProperties instance = ConfigCache.getOrCreate( RecheckWebProperties.class ); 27 | instance.reload(); 28 | return instance; 29 | } 30 | 31 | /* 32 | * Properties, their key constants and related functionality. 33 | */ 34 | 35 | static final String SCREENSHOT_PROVIDER_PROPERTY_KEY = "de.retest.recheck.web.screenshot.provider"; 36 | 37 | /** 38 | * @return The {@link ScreenshotProvider} to be used. 39 | */ 40 | @Key( SCREENSHOT_PROVIDER_PROPERTY_KEY ) 41 | @DefaultValue( "viewportOnlyMinimal" ) 42 | @ConverterClass( ScreenshotProviderConverter.class ) 43 | ScreenshotProvider screenshotProvider(); 44 | 45 | } 46 | -------------------------------------------------------------------------------- /.retest/filter/recheck-pseudo-elements.filter: -------------------------------------------------------------------------------- 1 | # Ignore file for recheck. Please do not delete, even if it is empty. 2 | 3 | # You can ignore specific attributes like so: 4 | # matcher: retestid=div-b4f23, attribute: font.* 5 | 6 | # To ignore whole subtrees, use e.g. XPath: 7 | # matcher: xpath=HTML[1]/BODY[1]/DIV[1]/DIV[1]/DIV[1] 8 | 9 | # To ignore attributes globally, use: 10 | # attribute=class 11 | 12 | # More details and examples can be found here: 13 | # https://retest.github.io/docs/recheck/how-ignore-works-in-recheck/ 14 | 15 | # Ignore non-visible attributes: 16 | attribute=class 17 | attribute=ping 18 | attribute-regex=data-.* 19 | attribute=jsdata 20 | attribute=jsaction 21 | attribute=cd_frame_id_ 22 | attribute=font-family 23 | attribute=font-size 24 | attribute=margin-bottom 25 | attribute=margin-top 26 | attribute=display 27 | attribute=box-sizing 28 | attribute=background-size 29 | attribute=left 30 | attribute=right 31 | attribute=min-height 32 | attribute=min-width 33 | attribute-regex=border-.*-width 34 | attribute-regex=border-.*-style 35 | attribute-regex=padding-.* 36 | attribute=line-height 37 | attribute=quotes 38 | matcher: type=meta, attribute-regex=.* 39 | 40 | # Have the tests pass locally: 41 | attribute=outline 42 | attribute=outline-width 43 | 44 | # So we can have a different ID in SimpleAutocheckingDriverShowcaseIT: 45 | matcher: xpath=html[1]/body[1]/form[1]/input[1], attribute=id 46 | 47 | # Ignore pixel diffs up to 5 pixels: 48 | pixel-diff=5px 49 | 50 | # Ignore OS-specific differences 51 | matcher: retestid=firefox_cant, attribute=text 52 | matcher: retestid=to_fully_automa, attribute=covered 53 | matcher: retestid=wallace, attribute=covered 54 | matcher: type=option, attribute=covered 55 | -------------------------------------------------------------------------------- /src/test/java/de/retest/web/testutils/WebDriverFactory.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.testutils; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | import java.util.stream.Stream; 6 | 7 | import org.junit.jupiter.params.provider.Arguments; 8 | import org.openqa.selenium.WebDriver; 9 | import org.openqa.selenium.chrome.ChromeDriver; 10 | import org.openqa.selenium.chrome.ChromeOptions; 11 | import org.openqa.selenium.firefox.FirefoxDriver; 12 | import org.openqa.selenium.firefox.FirefoxOptions; 13 | import org.openqa.selenium.remote.RemoteWebDriver; 14 | 15 | public class WebDriverFactory { 16 | 17 | public enum Driver { 18 | CHROME, 19 | FIREFOX 20 | } 21 | 22 | public static RemoteWebDriver driver( final Driver driver ) { 23 | switch ( driver ) { 24 | case CHROME: { 25 | return new ChromeDriver( new ChromeOptions().addArguments( commonArguments() ) ); 26 | } 27 | case FIREFOX: { 28 | return new FirefoxDriver( new FirefoxOptions().addArguments( commonArguments() ) ); 29 | } 30 | default: 31 | throw new IllegalArgumentException( "No \"" + driver + "\" driver available." ); 32 | } 33 | } 34 | 35 | public static Stream drivers() { 36 | return Arrays.stream( Driver.values() ).map( WebDriverFactory::driver ).map( WebDriverFactory::toArguments ); 37 | } 38 | 39 | private static Arguments toArguments( final WebDriver driver ) { 40 | return Arguments.of( driver, driver.getClass().getSimpleName() ); 41 | } 42 | 43 | public static List commonArguments() { 44 | return Arrays.asList( 45 | // Enable headless mode for faster execution. 46 | "--headless", 47 | // Fix window size for stable results. 48 | "--window-size=1200,800" ); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/test/java/de/retest/web/meta/element/WebElementMetadataProviderTest.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.meta.element; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.mockito.Mockito.RETURNS_MOCKS; 5 | import static org.mockito.Mockito.mock; 6 | import static org.mockito.Mockito.when; 7 | 8 | import org.junit.jupiter.api.Test; 9 | import org.openqa.selenium.WebElement; 10 | import org.openqa.selenium.remote.RemoteWebElement; 11 | 12 | import de.retest.recheck.meta.MetadataProvider; 13 | import de.retest.web.selenium.AutocheckingRecheckDriver; 14 | import de.retest.web.selenium.AutocheckingWebElement; 15 | 16 | class WebElementMetadataProviderTest { 17 | 18 | @Test 19 | void of_should_extract_driver_from_element() throws Exception { 20 | final RemoteWebElement element = mock( RemoteWebElement.class, RETURNS_MOCKS ); 21 | 22 | final MetadataProvider cut = WebElementMetadataProvider.of( element ); 23 | 24 | assertThat( cut.retrieve() ).isNotEmpty(); 25 | } 26 | 27 | @Test 28 | void of_should_extract_driver_from_wrapped() throws Exception { 29 | final AutocheckingWebElement element = mock( AutocheckingWebElement.class ); 30 | when( element.getWrappedDriver() ).thenReturn( mock( AutocheckingRecheckDriver.class, RETURNS_MOCKS ) ); 31 | 32 | final MetadataProvider cut = WebElementMetadataProvider.of( element ); 33 | 34 | assertThat( cut.retrieve() ).isNotEmpty(); 35 | } 36 | 37 | @Test 38 | void of_should_not_use_plain_web_element_and_return_empty_provider() throws Exception { 39 | final WebElement element = mock( WebElement.class ); 40 | 41 | final MetadataProvider cut = WebElementMetadataProvider.of( element ); 42 | 43 | assertThat( cut.retrieve() ).isEmpty(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/resources/pages/showcase/assets/jquery.waypoints.inview.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | Waypoints Inview Shortcut - 4.0.0 3 | Copyright © 2011-2015 Caleb Troughton 4 | Licensed under the MIT license. 5 | https://github.com/imakewebthings/waypoints/blog/master/licenses.txt 6 | */ 7 | !function(){"use strict";function t(){}function e(t){this.options=i.Adapter.extend({},e.defaults,t),this.axis=this.options.horizontal?"horizontal":"vertical",this.waypoints=[],this.element=this.options.element,this.createWaypoints()}var i=window.Waypoint;e.prototype.createWaypoints=function(){for(var t={vertical:[{down:"enter",up:"exited",offset:"100%"},{down:"entered",up:"exit",offset:"bottom-in-view"},{down:"exit",up:"entered",offset:0},{down:"exited",up:"enter",offset:function(){return-this.adapter.outerHeight()}}],horizontal:[{right:"enter",left:"exited",offset:"100%"},{right:"entered",left:"exit",offset:"right-in-view"},{right:"exit",left:"entered",offset:0},{right:"exited",left:"enter",offset:function(){return-this.adapter.outerWidth()}}]},e=0,i=t[this.axis].length;i>e;e++){var n=t[this.axis][e];this.createWaypoint(n)}},e.prototype.createWaypoint=function(t){var e=this;this.waypoints.push(new i({context:this.options.context,element:this.options.element,enabled:this.options.enabled,handler:function(t){return function(i){e.options[t[i]].call(e,i)}}(t),offset:t.offset,horizontal:this.options.horizontal}))},e.prototype.destroy=function(){for(var t=0,e=this.waypoints.length;e>t;t++)this.waypoints[t].destroy();this.waypoints=[]},e.prototype.disable=function(){for(var t=0,e=this.waypoints.length;e>t;t++)this.waypoints[t].disable()},e.prototype.enable=function(){for(var t=0,e=this.waypoints.length;e>t;t++)this.waypoints[t].enable()},e.defaults={context:window,enabled:!0,enter:t,entered:t,exit:t,exited:t},i.Inview=e}(); -------------------------------------------------------------------------------- /src/main/java/de/retest/web/selenium/WriteToResultWarningConsumer.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.selenium; 2 | 3 | import java.util.Collection; 4 | import java.util.function.Consumer; 5 | import java.util.function.Predicate; 6 | import java.util.stream.Stream; 7 | 8 | import de.retest.recheck.ui.diff.AttributeDifference; 9 | import de.retest.recheck.ui.diff.ElementDifference; 10 | import de.retest.recheck.ui.diff.ElementIdentificationWarning; 11 | import lombok.RequiredArgsConstructor; 12 | 13 | @RequiredArgsConstructor 14 | public class WriteToResultWarningConsumer implements Consumer { 15 | 16 | private final ElementDifferenceRetriever retriever; 17 | 18 | @Override 19 | public void accept( final QualifiedElementWarning warning ) { 20 | retriever.getDifferences() // 21 | .filter( matchesRetestId( warning.getRetestId() ) ) // 22 | .map( ElementDifference::getAttributeDifferences ) // 23 | .flatMap( Collection::stream ) // 24 | .filter( matchesAttributeKey( warning.getAttributeKey() ) ) // Should only be one difference 25 | .forEach( addWarning( warning.getWarning() ) ); 26 | } 27 | 28 | private Predicate matchesRetestId( final String retestId ) { 29 | return difference -> difference.getRetestId().equals( retestId ); 30 | } 31 | 32 | private Predicate matchesAttributeKey( final String attributeKey ) { 33 | return difference -> difference.getKey().equals( attributeKey ); 34 | } 35 | 36 | private Consumer addWarning( final ElementIdentificationWarning warning ) { 37 | return difference -> difference.addElementIdentificationWarning( warning ); 38 | } 39 | 40 | @FunctionalInterface 41 | public interface ElementDifferenceRetriever { 42 | 43 | Stream getDifferences(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /.retest/recheck.ignore: -------------------------------------------------------------------------------- 1 | # Ignore file for recheck. Please do not delete, even if it is empty. 2 | 3 | import: volatile-metadata.filter 4 | 5 | # You can ignore specific attributes like so: 6 | # matcher: retestid=div-b4f23, attribute: font.* 7 | 8 | # To ignore whole subtrees, use e.g. XPath: 9 | # matcher: xpath=HTML[1]/BODY[1]/DIV[1]/DIV[1]/DIV[1] 10 | 11 | # To ignore attributes globally, use: 12 | # attribute=class 13 | 14 | # More details and examples can be found here: 15 | # https://retest.github.io/docs/recheck/how-ignore-works-in-recheck/ 16 | 17 | # Ignore non-visible attributes: 18 | attribute=class 19 | attribute=ping 20 | attribute-regex=data-.* 21 | attribute=jsdata 22 | attribute=jsaction 23 | attribute=cd_frame_id_ 24 | attribute=color 25 | attribute=font-family 26 | attribute=font-size 27 | attribute=margin-bottom 28 | attribute=margin-top 29 | attribute=display 30 | attribute=box-sizing 31 | attribute=background-size 32 | attribute=left 33 | attribute=right 34 | attribute=min-height 35 | attribute=min-width 36 | attribute-regex=border-.*-width 37 | attribute-regex=.*-color 38 | attribute-regex=border-.*-style 39 | attribute-regex=padding-.* 40 | attribute=line-height 41 | attribute=quotes 42 | attribute=outline-offset 43 | attribute=unicode-bidi 44 | matcher: type=meta, attribute-regex=.* 45 | 46 | # Have the tests pass locally: 47 | attribute=outline 48 | attribute=outline-width 49 | 50 | # So we can have a different ID in SimpleAutocheckingDriverShowcaseIT: 51 | matcher: xpath=html[1]/body[1]/form[1]/input[1], attribute=id 52 | 53 | # Ignore pixel diffs up to 5 pixels: 54 | pixel-diff=5px 55 | 56 | # Ignore OS-specific differences 57 | matcher: retestid=firefox_cant, attribute=text 58 | matcher: retestid=to_fully_automa, attribute=covered 59 | matcher: retestid=wallace, attribute=covered 60 | matcher: type=option, attribute=covered 61 | -------------------------------------------------------------------------------- /src/main/java/de/retest/web/screenshot/ViewportOnlyMinimalScreenshot.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.screenshot; 2 | 3 | import java.awt.image.BufferedImage; 4 | 5 | import org.openqa.selenium.WebDriver; 6 | import org.openqa.selenium.WebElement; 7 | 8 | import com.assertthat.selenium_shutterbug.core.Shutterbug; 9 | 10 | import de.retest.recheck.ui.image.ImageUtils; 11 | 12 | /** 13 | * The default implementation that takes a screenshot of only the viewport visible to the user from the top page. In 14 | * contrast to {@link ViewportOnlyScreenshot}, images are resized to maximum 800px (see 15 | * {@link #DEFAULT_WANTED_WIDTH_PX}) if they exceed this width, while keeping the aspect ratio. 16 | */ 17 | public class ViewportOnlyMinimalScreenshot implements ScreenshotProvider { 18 | 19 | static final int DEFAULT_WANTED_WIDTH_PX = 800; 20 | private static final String RESIZE_MAX_WIDTH_PX = "de.retest.recheck.web.screenshot.maxWidthPx"; 21 | private static final int WANTED_WIDTH = Integer.getInteger( RESIZE_MAX_WIDTH_PX, DEFAULT_WANTED_WIDTH_PX ); 22 | private static final boolean USE_DEVICE_PIXEL_RATIO = true; 23 | 24 | @Override 25 | public BufferedImage shoot( final WebDriver driver ) { 26 | return resizeImage( Shutterbug.shootPage( driver, USE_DEVICE_PIXEL_RATIO ).getImage() ); 27 | } 28 | 29 | @Override 30 | public BufferedImage shoot( final WebDriver driver, final WebElement element ) { 31 | return resizeImage( Shutterbug.shootElement( driver, element, USE_DEVICE_PIXEL_RATIO ).getImage() ); 32 | } 33 | 34 | public static BufferedImage resizeImage( final BufferedImage image ) { 35 | if ( image.getWidth() <= WANTED_WIDTH ) { 36 | return image; 37 | } 38 | final double height = image.getHeight() * ((double) WANTED_WIDTH / image.getWidth()); 39 | return ImageUtils.resizeImage( image, WANTED_WIDTH, (int) height ); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/test/resources/pages/covered-page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 53 | 54 | 55 | 56 |
57 | 58 |
59 | foo 60 |
61 | 62 |
63 | bar baz 64 | 65 |
66 | bar baz 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/test/java/de/retest/web/selenium/AutocheckingWebElementIT.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.selenium; 2 | 3 | import static org.assertj.core.api.Assertions.assertThatCode; 4 | import static org.mockito.Mockito.spy; 5 | 6 | import java.nio.file.Path; 7 | 8 | import org.junit.jupiter.api.BeforeEach; 9 | import org.junit.jupiter.api.Test; 10 | import org.junit.jupiter.api.extension.ExtendWith; 11 | import org.junit.jupiter.api.io.TempDir; 12 | import org.openqa.selenium.interactions.Actions; 13 | 14 | import de.retest.recheck.RecheckOptions; 15 | import de.retest.recheck.junit.jupiter.RecheckExtension; 16 | import de.retest.recheck.persistence.SeparatePathsProjectLayout; 17 | import de.retest.web.testutils.PageFactory; 18 | import de.retest.web.testutils.WebDriverFactory; 19 | import de.retest.web.testutils.WebDriverFactory.Driver; 20 | 21 | @ExtendWith( RecheckExtension.class ) 22 | class AutocheckingWebElementIT { 23 | 24 | AutocheckingRecheckDriver driver; 25 | AutocheckingWebElement cut; 26 | 27 | @BeforeEach 28 | void setUp( @TempDir Path temp ) { 29 | driver = new AutocheckingRecheckDriver( WebDriverFactory.driver( Driver.CHROME ), RecheckOptions.builder() // 30 | // No Golden Master should be created with this test, thus it will never fail in Recheck#capTest 31 | .projectLayout( new SeparatePathsProjectLayout( temp.resolve( "states" ), temp.resolve( "reports" ) ) ) // 32 | .build() ); 33 | 34 | driver.skipCheck().get( PageFactory.page( PageFactory.Page.CENTER ) ); 35 | 36 | cut = spy( driver.findElement( By.id( "center" ) ) ); 37 | } 38 | 39 | @Test 40 | void actions_should_be_able_to_move_to_element() { 41 | final Actions actions = new Actions( driver ) // 42 | .moveToElement( cut ); 43 | 44 | // Apparently, this does not call Location#getCoordinates 45 | assertThatCode( actions::perform ).doesNotThrowAnyException(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/de/retest/web/meta/driver/capabilities/CapabilityMetadataProvider.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.meta.driver.capabilities; 2 | 3 | import java.util.Map; 4 | 5 | import org.openqa.selenium.Capabilities; 6 | import org.openqa.selenium.WebDriver; 7 | import org.openqa.selenium.remote.RemoteWebDriver; 8 | 9 | import de.retest.recheck.meta.MetadataProvider; 10 | import de.retest.recheck.meta.MultiMetadataProvider; 11 | import de.retest.web.util.SeleniumWrapperUtil; 12 | import de.retest.web.util.SeleniumWrapperUtil.WrapperOf; 13 | import lombok.extern.slf4j.Slf4j; 14 | 15 | @Slf4j 16 | public final class CapabilityMetadataProvider implements MetadataProvider { 17 | 18 | private final MetadataProvider provider; 19 | 20 | CapabilityMetadataProvider( final Capabilities capabilities ) { 21 | provider = MultiMetadataProvider.of( // 22 | new BrowserMetadataProvider( capabilities ), // 23 | new PlatformMetadataProvider( capabilities ) // 24 | ); 25 | } 26 | 27 | public static MetadataProvider of( final WebDriver driver ) { 28 | if ( SeleniumWrapperUtil.isWrapper( WrapperOf.DRIVER, driver ) ) { 29 | return of( (WebDriver) SeleniumWrapperUtil.getWrapped( WrapperOf.DRIVER, driver ) ); 30 | } 31 | if ( driver instanceof RemoteWebDriver ) { 32 | return of( (RemoteWebDriver) driver ); 33 | } 34 | log.debug( "Cannot retrieve capabilities from driver {}. Driver must be of '{}'. Returning empty metadata.", 35 | driver, RemoteWebDriver.class ); 36 | return MetadataProvider.empty(); 37 | } 38 | 39 | public static CapabilityMetadataProvider of( final RemoteWebDriver driver ) { 40 | return of( driver.getCapabilities() ); 41 | } 42 | 43 | private static CapabilityMetadataProvider of( final Capabilities capabilities ) { 44 | return new CapabilityMetadataProvider( capabilities ); 45 | } 46 | 47 | @Override 48 | public Map retrieve() { 49 | return provider.retrieve(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/test/java/de/retest/web/selenium/UnbreakableDriverIT.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.selenium; 2 | 3 | import static org.assertj.core.api.Assertions.assertThatCode; 4 | 5 | import java.nio.file.Path; 6 | 7 | import org.junit.jupiter.api.AfterEach; 8 | import org.junit.jupiter.api.BeforeEach; 9 | import org.junit.jupiter.api.Test; 10 | import org.junit.jupiter.api.io.TempDir; 11 | import org.openqa.selenium.WebDriver; 12 | 13 | import de.retest.recheck.RecheckOptions; 14 | import de.retest.recheck.persistence.SeparatePathsProjectLayout; 15 | import de.retest.web.RecheckWebImpl; 16 | import de.retest.web.RecheckWebOptions; 17 | import de.retest.web.testutils.WebDriverFactory; 18 | 19 | class UnbreakableDriverIT { 20 | 21 | RecheckWebImpl re; 22 | WebDriver driver; 23 | 24 | @BeforeEach 25 | void setUp( @TempDir final Path project ) { 26 | final Path goldenMasterDirectory = project.resolve( "state" ); 27 | final Path reportDirectory = project.resolve( "report" ); 28 | final RecheckOptions options = RecheckWebOptions.builder() 29 | .projectLayout( new SeparatePathsProjectLayout( goldenMasterDirectory, reportDirectory ) ) // 30 | .build(); 31 | 32 | re = new RecheckWebImpl( options ); 33 | driver = new UnbreakableDriver( WebDriverFactory.driver( WebDriverFactory.Driver.CHROME ) ); 34 | } 35 | 36 | @AfterEach 37 | void tearDown() { 38 | driver.quit(); 39 | re.cap(); 40 | } 41 | 42 | @Test 43 | void check_with_web_element_should_properly_identify_the_unbreakable_driver() { 44 | re.startTest(); 45 | 46 | driver.get( getClass().getResource( "UnbreakableDriverIT.html" ).toExternalForm() ); 47 | 48 | assertThatCode( () -> re.check( driver.findElement( By.id( "html" ) ), "element" ) ).doesNotThrowAnyException(); 49 | 50 | capTestSilently(); 51 | } 52 | 53 | private void capTestSilently() { 54 | try { 55 | re.capTest(); 56 | } catch ( final AssertionError e ) { 57 | // expected 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/test/java/de/retest/web/it/SimplePageDiffIT.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.it; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.junit.jupiter.api.Assertions.fail; 5 | 6 | import org.junit.After; 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | import org.openqa.selenium.WebDriver; 10 | 11 | import de.retest.recheck.Recheck; 12 | import de.retest.recheck.RecheckImpl; 13 | import de.retest.web.testutils.PageFactory; 14 | import de.retest.web.testutils.WebDriverFactory; 15 | import de.retest.web.testutils.WebDriverFactory.Driver; 16 | 17 | public class SimplePageDiffIT { 18 | 19 | WebDriver driver; 20 | Recheck re; 21 | 22 | @Before 23 | public void setup() { 24 | re = new RecheckImpl(); 25 | driver = WebDriverFactory.driver( Driver.CHROME ); 26 | } 27 | 28 | @Test 29 | public void testSimpleChange() throws Exception { 30 | re.startTest(); 31 | 32 | driver.get( PageFactory.toPageUrlString( "simple-page-diff.html" ) ); 33 | 34 | re.check( driver, "open" ); 35 | try { 36 | re.capTest(); 37 | } catch ( final AssertionError e ) { 38 | assertThat( e ).hasMessageContaining( "Test 'testSimpleChange' has 5 difference(s) in 1 state(s):" ) // 39 | .hasMessageEndingWith( "div (twoblocks) at 'html[1]/body[1]/div[3]':\n" // 40 | + "\t\tid: expected=\"twoblocks\", actual=\"changedblock\"\n" // 41 | + "\tp (some_text) at 'html[1]/body[1]/div[3]/p[1]':\n" // 42 | + "\t\ttext: expected=\"Some text\", actual=\"Some changed text\"\n" // 43 | + "\tp (some_more_text) at 'html[1]/body[1]/div[3]/p[2]':\n" // 44 | + "\t\twas deleted\n" // 45 | + "\tspan (span-2) at 'html[1]/body[1]/div[5]/span[1]':\n" // 46 | + "\t\tcovered: expected=\"true\", actual=\"null\"\n" // 47 | + "\th2 (subheading) at 'html[1]/body[1]/h2[1]':\n" // 48 | + "\t\twas inserted" ); 49 | } 50 | } 51 | 52 | @After 53 | public void tearDown() { 54 | driver.quit(); 55 | re.cap(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/test/java/de/retest/web/it/TabIT.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.it; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import org.junit.jupiter.api.AfterEach; 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.Test; 9 | import org.junit.jupiter.api.extension.ExtendWith; 10 | import org.openqa.selenium.By; 11 | import org.openqa.selenium.WebDriver; 12 | 13 | import de.retest.recheck.junit.jupiter.RecheckExtension; 14 | import de.retest.web.selenium.RecheckDriver; 15 | import de.retest.web.testutils.WebDriverFactory; 16 | 17 | @ExtendWith( RecheckExtension.class ) 18 | class TabIT { 19 | 20 | WebDriver driver; 21 | 22 | @BeforeEach 23 | void setUp() { 24 | driver = new RecheckDriver( WebDriverFactory.driver( WebDriverFactory.Driver.CHROME ) ); 25 | } 26 | 27 | @AfterEach 28 | void tearDown() { 29 | driver.quit(); 30 | } 31 | 32 | @Test 33 | void tab() { 34 | driver.get( TabIT.class.getResource( "page.html" ).toExternalForm() ); 35 | 36 | driver.findElement( By.id( "tab" ) ).click(); 37 | 38 | selectWindow( driver, 1 ); 39 | driver.findElement( By.id( "hello" ) ).click(); 40 | closeWindow( driver, 1 ); 41 | } 42 | 43 | @Test 44 | void window() { 45 | driver.get( TabIT.class.getResource( "page.html" ).toExternalForm() ); 46 | 47 | driver.findElement( By.id( "window" ) ).click(); 48 | 49 | selectWindow( driver, 1 ); 50 | driver.findElement( By.id( "hello" ) ).click(); 51 | closeWindow( driver, 1 ); 52 | } 53 | 54 | void selectWindow( final WebDriver driver, final int index ) { 55 | driver.switchTo().window( getWindowHandle( driver, index ) ); 56 | } 57 | 58 | String getWindowHandle( final WebDriver driver, final int index ) { 59 | final List tabs = new ArrayList<>( driver.getWindowHandles() ); 60 | return tabs.get( index ); 61 | } 62 | 63 | void closeWindow( final WebDriver driver, final int index ) { 64 | driver.close(); 65 | if ( index > 0 ) { 66 | selectWindow( driver, index - 1 ); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/test/java/de/retest/web/selenium/AutocheckingTargetLocatorTest.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.selenium; 2 | 3 | import static org.mockito.Mockito.mock; 4 | import static org.mockito.Mockito.only; 5 | import static org.mockito.Mockito.verify; 6 | 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.Test; 9 | import org.openqa.selenium.WebDriver; 10 | 11 | class AutocheckingTargetLocatorTest { 12 | 13 | WebDriver.TargetLocator delegate; 14 | AutocheckingRecheckDriver driver; 15 | 16 | AutocheckingTargetLocator cut; 17 | 18 | @BeforeEach 19 | void setUp() { 20 | delegate = mock( WebDriver.TargetLocator.class ); 21 | driver = mock( AutocheckingRecheckDriver.class ); 22 | 23 | cut = new AutocheckingTargetLocator( delegate, driver ); 24 | } 25 | 26 | @Test 27 | void frame_int_should_call_delegate_method() { 28 | cut.frame( 0 ); 29 | 30 | verify( delegate, only() ).frame( 0 ); 31 | } 32 | 33 | @Test 34 | void frame_string_should_call_delegate_method() { 35 | cut.frame( "name" ); 36 | 37 | verify( delegate, only() ).frame( "name" ); 38 | } 39 | 40 | @Test 41 | void parentFrame_should_call_delegate_method() { 42 | cut.parentFrame(); 43 | 44 | verify( delegate, only() ).parentFrame(); 45 | } 46 | 47 | @Test 48 | void window_should_call_delegate_method() { 49 | cut.window( "name" ); 50 | 51 | verify( delegate, only() ).window( "name" ); 52 | } 53 | 54 | @Test 55 | void defaultContent_should_call_delegate_method() { 56 | cut.defaultContent(); 57 | 58 | verify( delegate, only() ).defaultContent(); 59 | } 60 | 61 | @Test 62 | void activeElement_should_call_delegate_method() { 63 | cut.activeElement(); 64 | 65 | verify( delegate, only() ).activeElement(); 66 | } 67 | 68 | @Test 69 | void alert_should_call_delegate_method() { 70 | cut.alert(); 71 | 72 | verify( delegate, only() ).alert(); 73 | } 74 | 75 | @Test 76 | void window_should_perform_check() { 77 | cut.window( "name" ); 78 | 79 | verify( driver, only() ).check( "switch-window" ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/test/java/de/retest/web/mapping/WebDataFilterTest.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.mapping; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.mockito.Mockito.mock; 5 | import static org.mockito.Mockito.when; 6 | 7 | import java.util.stream.Stream; 8 | 9 | import org.junit.jupiter.api.Test; 10 | import org.junit.jupiter.params.ParameterizedTest; 11 | import org.junit.jupiter.params.provider.MethodSource; 12 | 13 | import de.retest.web.mapping.WebData; 14 | import de.retest.web.mapping.WebDataFilter; 15 | 16 | class WebDataFilterTest { 17 | 18 | @Test 19 | void should_ignore_invisible_elements() { 20 | final WebData webData = createInvisibleWebDataForTag( "a" ); 21 | assertThat( WebDataFilter.shouldIgnore( webData ) ).isTrue(); 22 | } 23 | 24 | @Test 25 | void should_not_ignore_visible_elements() throws Exception { 26 | final WebData webData = createVisibleWebDataForTag( "a" ); 27 | assertThat( WebDataFilter.shouldIgnore( webData ) ).isFalse(); 28 | } 29 | 30 | @ParameterizedTest 31 | @MethodSource( "specialTags" ) 32 | void testName( final String specialTag ) throws Exception { 33 | final WebData webData = createVisibleWebDataForTag( specialTag ); 34 | assertThat( WebDataFilter.shouldIgnore( webData ) ).isFalse(); 35 | } 36 | 37 | private WebData createInvisibleWebDataForTag( final String tagName ) { 38 | final WebData webData = createWebDataMock( tagName ); 39 | when( webData.isShown() ).thenReturn( false ); 40 | return webData; 41 | } 42 | 43 | private WebData createVisibleWebDataForTag( final String tagName ) { 44 | final WebData webData = createWebDataMock( tagName ); 45 | when( webData.isShown() ).thenReturn( true ); 46 | return webData; 47 | } 48 | 49 | private WebData createWebDataMock( final String tagName ) { 50 | final WebData webData = mock( WebData.class ); 51 | when( webData.getTag() ).thenReturn( tagName ); 52 | return webData; 53 | } 54 | 55 | static Stream specialTags() { 56 | return WebDataFilter.specialTags.stream(); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/test/java/de/retest/web/selenium/css/RegexTransformerTest.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.selenium.css; 2 | 3 | import static org.mockito.Mockito.when; 4 | 5 | import org.assertj.core.api.SoftAssertions; 6 | import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension; 7 | import org.junit.jupiter.api.Test; 8 | import org.junit.jupiter.api.extension.ExtendWith; 9 | import org.mockito.Mock; 10 | import org.mockito.junit.jupiter.MockitoExtension; 11 | 12 | import de.retest.recheck.ui.descriptors.Element; 13 | 14 | @ExtendWith( MockitoExtension.class ) 15 | @ExtendWith( SoftAssertionsExtension.class ) 16 | class RegexTransformerTest { 17 | 18 | private static final String SELECTOR_PATTERN = "([a-z;]+;)"; 19 | private static final String SELECTED_PART = "abc;"; 20 | private static final String REMAINING_PART = "b l u b"; 21 | private static final String TRIMMED_REMAINING_PART = REMAINING_PART.trim(); 22 | 23 | @Mock 24 | private PredicateFactory factory; 25 | @Mock 26 | private Element element; 27 | 28 | @Test 29 | void should_not_match_regex( final SoftAssertions softly ) throws Exception { 30 | final Transformer transformer = RegexTransformer.of( SELECTOR_PATTERN, factory ); 31 | 32 | final String selectorString = "002;blub"; 33 | final Selector cssSelector = transformer.transform( selectorString ); 34 | 35 | softly.assertThat( cssSelector.getPredicate().test( element ) ).isFalse(); 36 | softly.assertThat( cssSelector.getRemainingSelector() ).isEqualTo( selectorString ); 37 | } 38 | 39 | @Test 40 | void should_match_regex( final SoftAssertions softly ) throws Exception { 41 | when( factory.create( SELECTED_PART ) ).thenReturn( e -> true ); 42 | final Transformer transformer = RegexTransformer.of( SELECTOR_PATTERN, factory ); 43 | 44 | final Selector cssSelector = transformer.transform( SELECTED_PART + REMAINING_PART ); 45 | 46 | softly.assertThat( cssSelector.getPredicate().test( element ) ).isTrue(); 47 | softly.assertThat( cssSelector.getRemainingSelector() ).isEqualTo( TRIMMED_REMAINING_PART ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /.github/workflows/build-project.yml: -------------------------------------------------------------------------------- 1 | name: Build project 2 | 3 | on: [push, pull_request] 4 | 5 | 6 | env: 7 | MVN_ARGS: --batch-mode --errors --fail-fast --no-transfer-progress 8 | HAS_SONAR_TOKEN: ${{ secrets.SONAR_TOKEN != '' }} 9 | 10 | 11 | jobs: 12 | default: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | 19 | - name: Fetch unshallow to enable blame for Sonar 20 | run: git fetch --prune --unshallow 21 | 22 | - uses: actions/setup-java@v1 23 | with: 24 | java-version: 8 25 | 26 | - name: Cache local Maven repository 27 | uses: actions/cache@v2 28 | with: 29 | path: ~/.m2/repository 30 | key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} 31 | restore-keys: ${{ runner.os }}-maven- 32 | 33 | - name: Cache Sonar 34 | uses: actions/cache@v1 35 | with: 36 | path: ~/.sonar/cache/ 37 | key: ${{ runner.os }}-sonar 38 | 39 | - name: Compile 40 | run: mvn ${MVN_ARGS} clean package -DskipTests 41 | 42 | - uses: actions/setup-java@v1 43 | with: 44 | java-version: 15 45 | 46 | - name: Test with SonarCloud 47 | id: test-with-sonar 48 | if: ${{ env.HAS_SONAR_TOKEN == 'true' }} 49 | run: > 50 | mvn ${MVN_ARGS} verify sonar:sonar -Pcoverage 51 | -Dsonar.host.url=https://sonarcloud.io 52 | -Dsonar.organization=retest 53 | env: 54 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 55 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 56 | 57 | - name: Test without SonarCloud 58 | id: test-without-sonar 59 | if: ${{ steps.test-with-sonar.conclusion == 'skipped' }} 60 | run: mvn ${MVN_ARGS} verify 61 | 62 | - name: Archive recheck tests.report 63 | if: always() 64 | uses: actions/upload-artifact@v2 65 | with: 66 | name: tests.report 67 | path: target/test-classes/retest/recheck/tests.report 68 | retention-days: 10 69 | -------------------------------------------------------------------------------- /src/test/java/de/retest/web/it/SimpleAutocheckingDriverShowcaseIT.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.it; 2 | 3 | import org.junit.After; 4 | import org.junit.Before; 5 | import org.junit.Test; 6 | import org.openqa.selenium.chrome.ChromeDriver; 7 | import org.openqa.selenium.chrome.ChromeOptions; 8 | 9 | import de.retest.web.selenium.AutocheckingRecheckDriver; 10 | import de.retest.web.selenium.By; 11 | import de.retest.web.testutils.PageFactory; 12 | import de.retest.web.testutils.PageFactory.Page; 13 | 14 | /* 15 | * Simple recheck-web showcase for a Chrome-based integration test. See other *IT classes for more examples. 16 | */ 17 | public class SimpleAutocheckingDriverShowcaseIT { 18 | 19 | private AutocheckingRecheckDriver driver; 20 | 21 | @Before 22 | public void setup() { 23 | // If ChromeDriver (http://chromedriver.chromium.org/downloads/) is not in your PATH, uncomment this and point to your installation. 24 | // System.setProperty( "webdriver.chrome.driver", "path/to/chromedriver" ); 25 | 26 | final ChromeOptions opts = new ChromeOptions(); 27 | opts.addArguments( 28 | // Enable headless mode for faster execution. 29 | "--headless", 30 | // Use Chrome in container-based Travis CI enviroment (see https://docs.travis-ci.com/user/chrome#Sandboxing). 31 | "--no-sandbox", 32 | // Fix window size for stable results. 33 | "--window-size=1200,800" ); 34 | driver = new AutocheckingRecheckDriver( new ChromeDriver( opts ) ); 35 | } 36 | 37 | @Test 38 | public void index() throws Exception { 39 | driver.navigate().to( PageFactory.page( Page.FORM_PAGE ) ); 40 | 41 | driver.findElement( By.id( "old-email-id" ) ).sendKeys( "me@retest.de" ); 42 | 43 | driver.findElement( By.tagName( "html" ) ).findElement( By.className( "inputLabel" ) ).sendKeys( "typed" ); 44 | 45 | driver.findElement( By.name( "checky" ) ).click(); 46 | 47 | driver.findElement( By.linkText( "Contact" ) ).click(); 48 | 49 | driver.findElement( By.retestId( "contact" ) ).click(); 50 | 51 | driver.capTest(); 52 | } 53 | 54 | @After 55 | public void tearDown() { 56 | driver.quit(); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/de/retest/web/RootElementPeer.java: -------------------------------------------------------------------------------- 1 | package de.retest.web; 2 | 3 | import java.awt.image.BufferedImage; 4 | 5 | import de.retest.recheck.ui.DefaultValueFinder; 6 | import de.retest.recheck.ui.descriptors.Element; 7 | import de.retest.recheck.ui.descriptors.IdentifyingAttributes; 8 | import de.retest.recheck.ui.descriptors.MutableAttributes; 9 | import de.retest.recheck.ui.descriptors.RootElement; 10 | import de.retest.recheck.ui.descriptors.idproviders.RetestIdProvider; 11 | import de.retest.recheck.ui.image.ImageUtils; 12 | import de.retest.recheck.ui.image.Screenshot; 13 | import de.retest.web.mapping.WebData; 14 | 15 | public class RootElementPeer extends WebElementPeer { 16 | 17 | private final String title; 18 | private final BufferedImage screenshot; 19 | 20 | public RootElementPeer( final RetestIdProvider retestIdProvider, final WebData webData, final String path, 21 | final String title, final BufferedImage screenshot, final DefaultValueFinder defaultValueFinder ) { 22 | super( retestIdProvider, webData, path, defaultValueFinder ); 23 | this.screenshot = screenshot; 24 | this.title = title; 25 | } 26 | 27 | @Override 28 | public RootElement toElement( final Element parent ) { 29 | if ( webData == null ) { 30 | throw new IllegalStateException( "RootElement was not properly initialized!" ); 31 | } 32 | final IdentifyingAttributes identifyingAttributes = retrieveIdentifyingAttributes(); 33 | 34 | // If this is a WebElement 35 | String screen = title; 36 | String title = this.title; 37 | if ( !identifyingAttributes.getType().equals( "html" ) ) { 38 | screen = "WebElement"; 39 | title = identifyingAttributes.getPath(); 40 | } 41 | 42 | final MutableAttributes stateAttributes = retrieveStateAttributes( identifyingAttributes ); 43 | final String retestId = retestIdProvider.getRetestId( identifyingAttributes ); 44 | final Screenshot ss = ImageUtils.image2Screenshot( retestId, screenshot ); 45 | final RootElement rootElement = 46 | new RootElement( retestId, identifyingAttributes, stateAttributes.immutable(), ss, screen, 1, title ); 47 | rootElement.addChildren( convertChildren( rootElement ) ); 48 | return rootElement; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/test/java/de/retest/web/it/SingleElementIT.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.it; 2 | 3 | import java.nio.file.Paths; 4 | 5 | import org.apache.commons.io.FileUtils; 6 | import org.assertj.core.api.Assertions; 7 | import org.junit.jupiter.api.AfterEach; 8 | import org.junit.jupiter.api.BeforeEach; 9 | import org.junit.jupiter.api.Test; 10 | import org.openqa.selenium.By; 11 | import org.openqa.selenium.JavascriptExecutor; 12 | import org.openqa.selenium.WebDriver; 13 | 14 | import de.retest.recheck.Recheck; 15 | import de.retest.recheck.RecheckImpl; 16 | import de.retest.recheck.RecheckOptions; 17 | import de.retest.web.testutils.PageFactory; 18 | import de.retest.web.testutils.WebDriverFactory; 19 | import de.retest.web.testutils.WebDriverFactory.Driver; 20 | 21 | class SingleElementIT { 22 | 23 | WebDriver driver; 24 | Recheck re; 25 | 26 | @BeforeEach 27 | public void setup() { 28 | final RecheckOptions reopts = RecheckOptions.builder().ignoreNothing().build(); 29 | re = new RecheckImpl( reopts ); 30 | driver = WebDriverFactory.driver( Driver.CHROME ); 31 | } 32 | 33 | @Test 34 | public void testParentScope() throws Exception { 35 | // create GM 36 | re.startTest( "testParentScope" ); 37 | driver.get( PageFactory.toPageUrlString( "single-element-diff.html" ) ); 38 | re.check( driver.findElement( By.id( "redtext" ) ), "same-color" ); 39 | Assertions.assertThatThrownBy( () -> re.capTest() ).hasMessageContaining( // 40 | "No Golden Master found. First time test was run?" ); 41 | 42 | // Check GM has text with red 43 | re.startTest( "testParentScope" ); 44 | driver.get( PageFactory.toPageUrlString( "single-element-diff.html" ) ); 45 | ((JavascriptExecutor) driver).executeScript( "document.body.style.color = 'green';" ); 46 | re.check( driver.findElement( By.id( "redtext" ) ), "same-color" ); 47 | Assertions.assertThatThrownBy( () -> re.capTest() ).hasMessageContaining( // 48 | "color: expected=\"rgb(255, 0, 0)\", actual=\"rgb(0, 128, 0)\"" ); 49 | } 50 | 51 | @AfterEach 52 | public void tearDown() { 53 | driver.quit(); 54 | re.cap(); 55 | 56 | // Not standard! Ensure we delete the GM that was created 57 | FileUtils.deleteQuietly( 58 | Paths.get( "src/test/resources/retest/recheck", SingleElementIT.class.getName() ).toFile() ); 59 | 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/test/java/de/retest/web/it/RootElementPeerNullIT.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.it; 2 | 3 | import static org.assertj.core.api.Assertions.assertThatCode; 4 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 5 | import static org.mockito.Mockito.RETURNS_MOCKS; 6 | import static org.mockito.Mockito.any; 7 | import static org.mockito.Mockito.doNothing; 8 | import static org.mockito.Mockito.doReturn; 9 | import static org.mockito.Mockito.mock; 10 | import static org.mockito.Mockito.spy; 11 | 12 | import org.junit.jupiter.api.AfterEach; 13 | import org.junit.jupiter.api.BeforeEach; 14 | import org.junit.jupiter.api.Test; 15 | import org.openqa.selenium.By; 16 | import org.openqa.selenium.WebDriver; 17 | import org.openqa.selenium.WebElement; 18 | import org.openqa.selenium.chrome.ChromeDriver; 19 | import org.openqa.selenium.chrome.ChromeOptions; 20 | 21 | import de.retest.recheck.RecheckImpl; 22 | import de.retest.recheck.ui.descriptors.SutState; 23 | 24 | class RootElementPeerNullIT { 25 | 26 | RecheckImpl re; 27 | WebDriver driver; 28 | 29 | @BeforeEach 30 | void setUp() { 31 | re = spy( new RecheckImpl() ); 32 | // Pretend that there is a valid and empty sut state, so it does not get created 33 | doReturn( mock( SutState.class, RETURNS_MOCKS ) ).when( re ).loadExpected( any() ); 34 | // Do nothing on cap, i.e do not save the report 35 | doNothing().when( re ).cap(); 36 | 37 | final ChromeOptions opts = new ChromeOptions(); 38 | opts.setHeadless( true ); 39 | opts.addArguments( "--window-size=480,800" ); 40 | driver = new ChromeDriver( opts ); 41 | } 42 | 43 | @AfterEach 44 | void tearDown() { 45 | driver.quit(); 46 | re.cap(); 47 | } 48 | 49 | @Test 50 | void root_element_invisible_should_not_cause_npe() throws Exception { 51 | re.startTest(); 52 | 53 | driver.get( getClass().getResource( "RootElementPeerNullTest.html" ).toExternalForm() ); 54 | 55 | final WebElement invisibleElement = driver.findElement( By.className( "md-tabs" ) ); 56 | 57 | assertThatCode( () -> re.check( invisibleElement, "tabs" ) ).doesNotThrowAnyException(); 58 | 59 | assertThatThrownBy( () -> re.capTest() ) // 60 | .isInstanceOf( AssertionError.class ) // 61 | .hasMessageContaining( "nav (html[1]/body[1]/div[1]/nav[1]) at 'html[1]/body[1]/div[1]/nav[1]':\n" // 62 | + "\t\twas inserted" ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/test/java/de/retest/web/it/SimpleRecheckShowcaseIT.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.it; 2 | 3 | import java.nio.file.Path; 4 | import java.nio.file.Paths; 5 | 6 | import org.junit.After; 7 | import org.junit.Before; 8 | import org.junit.Rule; 9 | import org.junit.Test; 10 | import org.openqa.selenium.WebDriver; 11 | import org.openqa.selenium.chrome.ChromeDriver; 12 | import org.openqa.selenium.chrome.ChromeOptions; 13 | 14 | import de.retest.recheck.Recheck; 15 | import de.retest.recheck.RecheckImpl; 16 | import de.retest.recheck.junit.vintage.RecheckRule; 17 | 18 | /* 19 | * Simple recheck-web showcase for a Chrome-based integration test. See other *IT classes for more examples. 20 | */ 21 | public class SimpleRecheckShowcaseIT { 22 | // Connects recheck to your test's life cycle, taking care of starting and finishing tests, naming them, etc. 23 | @Rule 24 | public RecheckRule rule = new RecheckRule(); 25 | 26 | private WebDriver driver; 27 | private Recheck re; 28 | 29 | @Before 30 | public void setup() { 31 | // If ChromeDriver (http://chromedriver.chromium.org/downloads/) is not in your PATH, uncomment this and point to your installation. 32 | // System.setProperty( "webdriver.chrome.driver", "path/to/chromedriver" ); 33 | 34 | final ChromeOptions opts = new ChromeOptions(); 35 | opts.addArguments( 36 | // Enable headless mode for faster execution. 37 | "--headless", 38 | // Use Chrome in container-based Travis CI enviroment (see https://docs.travis-ci.com/user/chrome#Sandboxing). 39 | "--no-sandbox", 40 | // Fix window size for stable results. 41 | "--window-size=1200,800" ); 42 | driver = new ChromeDriver( opts ); 43 | 44 | // Use the default implementation. 45 | re = new RecheckImpl(); 46 | // Let the recheck rule know which implementation it shall use. 47 | rule.use( re ); 48 | } 49 | 50 | @Test 51 | public void simpleShowcase() throws Exception { 52 | // Do your Selenium stuff. 53 | final Path showcasePath = Paths.get( "src/test/resources/pages/showcase/retest.html" ); 54 | driver.get( showcasePath.toUri().toURL().toString() ); 55 | 56 | Thread.sleep( 1000 ); 57 | 58 | // Single call instead of multiple assertions (doesn't fail on differences). 59 | re.check( driver, "index" ); 60 | } 61 | 62 | @After 63 | public void tearDown() { 64 | driver.quit(); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/test/resources/pages/showcase/assets/jquery.freeow.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Freeow! 3 | * Stylish, Growl-like message boxes 4 | * 5 | * Copyright (c) 2012 PJ Dietz 6 | * Version: 1.0.2 7 | * Modified: 2012-05-03 8 | * Licensed under the MIT license: 9 | * http://www.opensource.org/licenses/mit-license.php 10 | * 11 | * http://pjdietz.com/jquery-plugins/freeow/ 12 | */ 13 | (function($){"use strict";var Freeow;Freeow=function(title,message,options){var startStyle,i,u;this.options=$.extend({},$.fn.freeow.defaults,options);this.element=$(this.options.template(title,message));if(this.options.startStyle){startStyle=this.options.startStyle;} 14 | else{startStyle=this.options.hideStyle;} 15 | this.element.css(startStyle);this.element.data("freeow",this);for(i=0,u=this.options.classes.length;i0){this.autoHide=true;self=this;delay=this.options.autoHideDelay;fn=function(){if(self.autoHide){self.hide();}};opts.complete=function(){setTimeout(fn,delay);};} 18 | this.element.animate(this.options.showStyle,opts);},hide:function(){var self=this;this.element.animate(this.options.hideStyle,{duration:this.options.hideDuration,complete:function(){self.destroy();}});},destroy:function(){this.element.data("freeow",undefined);this.element.remove();}};if(typeof $.fn.freeow==="undefined"){$.fn.extend({freeow:function(title,message,options){return this.each(function(){var f;f=new Freeow(title,message,options);f.attach(this);});}});$.fn.freeow.defaults={autoHide:true,autoHideDelay:3000,classes:[],prepend:true,startStyle:null,showStyle:{opacity:1.0},showDuration:250,hideStyle:{opacity:0.0},hideDuration:500,onClick:function(event){$(this).data("freeow").hide();},onHover:function(event){$(this).data("freeow").autoHide=false;},template:function(title,message){var e;e=['
','
','
','

'+title+'

','

'+message+'

','
','
','','','
'].join("");return e;}};}}(jQuery)); -------------------------------------------------------------------------------- /src/main/java/de/retest/web/meta/SeleniumMetadataProvider.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.meta; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | import org.openqa.selenium.WebDriver; 7 | import org.openqa.selenium.WebElement; 8 | 9 | import de.retest.recheck.meta.MetadataProvider; 10 | import de.retest.recheck.meta.MultiMetadataProvider; 11 | import de.retest.web.meta.driver.WebDriverMetadataProvider; 12 | import de.retest.web.meta.element.WebElementMetadataProvider; 13 | import de.retest.web.util.SeleniumWrapperUtil; 14 | import de.retest.web.util.SeleniumWrapperUtil.WrapperOf; 15 | import lombok.AccessLevel; 16 | import lombok.RequiredArgsConstructor; 17 | 18 | @RequiredArgsConstructor( access = AccessLevel.PACKAGE ) 19 | public final class SeleniumMetadataProvider implements MetadataProvider { 20 | 21 | public static final String TYPE_DRIVER = "driver"; 22 | public static final String TYPE_ELEMENT = "element"; 23 | 24 | private final String type; 25 | 26 | public static MetadataProvider of( final Object object ) { 27 | if ( object instanceof WebElement ) { 28 | return of( (WebElement) object ); 29 | } 30 | if ( SeleniumWrapperUtil.isWrapper( WrapperOf.ELEMENT, object ) ) { 31 | return of( SeleniumWrapperUtil.getWrapped( WrapperOf.ELEMENT, object ) ); 32 | } 33 | if ( object instanceof WebDriver ) { 34 | return of( (WebDriver) object ); 35 | } 36 | if ( SeleniumWrapperUtil.isWrapper( WrapperOf.DRIVER, object ) ) { 37 | return of( SeleniumWrapperUtil.getWrapped( WrapperOf.DRIVER, object ) ); 38 | } 39 | throw new IllegalArgumentException( 40 | String.format( "Cannot retrieve metadata from objects of type '%s'.", object.getClass().getName() ) ); 41 | } 42 | 43 | private static MetadataProvider of( final WebDriver object ) { 44 | return MultiMetadataProvider.of( // 45 | WebDriverMetadataProvider.of( object ), // 46 | new SeleniumMetadataProvider( TYPE_DRIVER ) // 47 | ); 48 | } 49 | 50 | private static MetadataProvider of( final WebElement object ) { 51 | return MultiMetadataProvider.of( // 52 | WebElementMetadataProvider.of( object ), // 53 | new SeleniumMetadataProvider( TYPE_ELEMENT ) // 54 | ); 55 | } 56 | 57 | @Override 58 | public Map retrieve() { 59 | final Map map = new HashMap<>(); 60 | 61 | map.put( SeleniumMetadata.CHECK_TYPE, type ); 62 | 63 | return map; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/test/java/de/retest/web/RecheckWebImplIT.java: -------------------------------------------------------------------------------- 1 | package de.retest.web; 2 | 3 | import static org.assertj.core.api.Assertions.assertThatCode; 4 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 5 | 6 | import java.nio.file.Path; 7 | 8 | import org.junit.jupiter.api.AfterEach; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.Test; 11 | import org.junit.jupiter.api.io.TempDir; 12 | 13 | import de.retest.recheck.RecheckLifecycle; 14 | import de.retest.recheck.RecheckOptions; 15 | import de.retest.recheck.persistence.SeparatePathsProjectLayout; 16 | import de.retest.web.selenium.RecheckDriver; 17 | import de.retest.web.testutils.WebDriverFactory; 18 | import de.retest.web.testutils.WebDriverFactory.Driver; 19 | 20 | class RecheckWebImplIT { 21 | 22 | RecheckWebImpl re; 23 | 24 | RecheckDriver driver; 25 | 26 | @BeforeEach 27 | void setUp( @TempDir final Path project ) { 28 | final Path goldenMasterDirectory = project.resolve( "state" ); 29 | final Path reportDirectory = project.resolve( "report" ); 30 | final RecheckOptions options = RecheckWebOptions.builder() 31 | .projectLayout( new SeparatePathsProjectLayout( goldenMasterDirectory, reportDirectory ) ) // 32 | .build(); 33 | 34 | re = new RecheckWebImpl( options ); 35 | driver = new RecheckDriver( WebDriverFactory.driver( Driver.CHROME ), options ); 36 | } 37 | 38 | @AfterEach 39 | void tearDown() { 40 | driver.quit(); 41 | re.cap(); 42 | } 43 | 44 | @Test 45 | void check_should_throw_if_explicit_checking() throws Exception { 46 | re.startTest(); 47 | driver.startTest(); 48 | 49 | assertThatThrownBy( () -> re.check( driver, "explicit-implicit-mixed" ) ) // 50 | .isInstanceOf( UnsupportedOperationException.class ) // 51 | .hasMessageStartingWith( "The 'RecheckDriver' does implicit checking after each action, " 52 | + "therefore no explicit check with 'Recheck#check' is needed" ); 53 | 54 | capTestSilently( re ); 55 | capTestSilently( driver ); 56 | } 57 | 58 | @Test 59 | void check_should_not_throw_if_implicit_checking() throws Exception { 60 | driver.startTest(); 61 | 62 | assertThatCode( () -> driver.get( "https://retest.de" ) ).doesNotThrowAnyException(); 63 | 64 | capTestSilently( driver ); 65 | } 66 | 67 | private void capTestSilently( final RecheckLifecycle re ) { 68 | try { 69 | re.capTest(); 70 | } catch ( final AssertionError e ) { 71 | // expected 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/de/retest/web/selenium/css/PredicateBuilder.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.selenium.css; 2 | 3 | import java.util.LinkedList; 4 | import java.util.List; 5 | import java.util.Optional; 6 | import java.util.function.Predicate; 7 | 8 | import de.retest.recheck.ui.descriptors.Element; 9 | import lombok.extern.slf4j.Slf4j; 10 | 11 | @Slf4j 12 | public class PredicateBuilder { 13 | 14 | private final List selectors; 15 | private final List> predicates; 16 | private final String origSelector; 17 | 18 | public PredicateBuilder( final List selectors, final String origSelector ) { 19 | this.selectors = selectors; 20 | this.origSelector = origSelector; 21 | predicates = new LinkedList<>(); 22 | } 23 | 24 | public Optional> build() { 25 | final String remainingSelector = parse( origSelector ); 26 | 27 | if ( isPartAvailable( remainingSelector ) ) { 28 | logUnkownSelector( remainingSelector ); 29 | return Optional.empty(); 30 | 31 | } 32 | return Optional.of( combinePredicates() ); 33 | } 34 | 35 | private String parse( final String selector ) { 36 | String oldSelector = ""; 37 | String remainingSelector = selector; 38 | while ( isPartAvailable( remainingSelector ) && !oldSelector.equals( remainingSelector ) ) { 39 | oldSelector = remainingSelector; 40 | remainingSelector = transform( remainingSelector ); 41 | } 42 | return remainingSelector; 43 | } 44 | 45 | private String transform( final String selector ) { 46 | String remainingSelector = selector; 47 | for ( final Transformer function : selectors ) { 48 | final Selector cssSelector = function.transform( remainingSelector ); 49 | remainingSelector = cssSelector.getRemainingSelector(); 50 | if ( !selector.equals( remainingSelector ) ) { 51 | predicates.add( cssSelector.getPredicate() ); 52 | return remainingSelector; 53 | } 54 | } 55 | return remainingSelector; 56 | } 57 | 58 | private boolean isPartAvailable( final String selector ) { 59 | return !selector.isEmpty(); 60 | } 61 | 62 | private void logUnkownSelector( final String selector ) { 63 | log.warn( 64 | "Unbreakable tests are not implemented for all CSS selectors. Please report your chosen selector ('{}') at https://github.com/retest/recheck-web/issues.", 65 | selector ); 66 | } 67 | 68 | private Predicate combinePredicates() { 69 | return predicates.stream().reduce( e -> true, Predicate::and ); 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/de/retest/web/selenium/css/DefaultSelectors.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.selenium.css; 2 | 3 | import java.util.LinkedList; 4 | import java.util.List; 5 | 6 | public class DefaultSelectors { 7 | 8 | private static final String CHARACTERSET = "a-zA-Z0-9\\-_"; 9 | private static final String ALLOWED_CHARACTERS = "[" + CHARACTERSET + "]+"; 10 | private static final String TAG_PATTERN = "(" + ALLOWED_CHARACTERS + ")"; 11 | private static final String ID_PATTERN = "\\#(" + ALLOWED_CHARACTERS + ")"; 12 | private static final String CLASS_PATTERN = "\\.(" + ALLOWED_CHARACTERS + ")"; 13 | private static final String ATTRIBUTE_PATTERN = attributePattern( "" ); 14 | private static final String ATTRIBUTE_CONTAINING_PATTERN = attributePattern( "~" ); 15 | private static final String ATTRIBUTE_STARTING_PATTERN = attributePattern( "\\|" ); 16 | private static final String ATTRIBUTE_BEGINNING_PATTERN = attributePattern( "\\^" ); 17 | private static final String ATTRIBUTE_ENDING_PATTERN = attributePattern( "\\$" ); 18 | private static final String ATTRIBUTE_CONTAINING_SUBSTRING_PATTERN = attributePattern( "\\*" ); 19 | private static final String PSEUDO_CLASS_PATTERN = ":((?!not\\()" + ALLOWED_CHARACTERS + ")"; 20 | 21 | private static String attributePattern( final String selectorChar ) { 22 | return "\\[([" + CHARACTERSET + selectorChar + " =\"']+)\\]"; 23 | } 24 | 25 | private DefaultSelectors() {} 26 | 27 | public static List all() { 28 | final List transformers = new LinkedList<>(); 29 | transformers.add( RegexTransformer.of( TAG_PATTERN, Has::cssTag ) ); 30 | transformers.add( RegexTransformer.of( ID_PATTERN, Has::cssId ) ); 31 | transformers.add( RegexTransformer.of( CLASS_PATTERN, Has::cssClass ) ); 32 | transformers.add( RegexTransformer.of( ATTRIBUTE_PATTERN, Has::attribute ) ); 33 | transformers.add( RegexTransformer.of( ATTRIBUTE_CONTAINING_PATTERN, Has::attributeContaining ) ); 34 | transformers.add( RegexTransformer.of( ATTRIBUTE_STARTING_PATTERN, Has::attributeStarting ) ); 35 | transformers.add( RegexTransformer.of( ATTRIBUTE_BEGINNING_PATTERN, Has::attributeBeginning ) ); 36 | transformers.add( RegexTransformer.of( ATTRIBUTE_ENDING_PATTERN, Has::attributeEnding ) ); 37 | transformers.add( RegexTransformer.of( ATTRIBUTE_CONTAINING_SUBSTRING_PATTERN, Has::attributeContainingSubstring ) ); 38 | transformers.add( RegexTransformer.of( PSEUDO_CLASS_PATTERN, Has::cssPseudoClass ) ); 39 | return transformers; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/test/java/de/retest/web/meta/SeleniumMetadataProviderTest.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.meta; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.mockito.Mockito.RETURNS_MOCKS; 5 | import static org.mockito.Mockito.mock; 6 | import static org.mockito.Mockito.when; 7 | 8 | import org.apache.commons.lang3.tuple.Pair; 9 | import org.junit.jupiter.api.Test; 10 | import org.openqa.selenium.WebDriver; 11 | import org.openqa.selenium.WebElement; 12 | import org.openqa.selenium.WrapsDriver; 13 | import org.openqa.selenium.WrapsElement; 14 | import org.openqa.selenium.remote.RemoteWebDriver; 15 | import org.openqa.selenium.remote.RemoteWebElement; 16 | 17 | import de.retest.recheck.meta.MetadataProvider; 18 | 19 | class SeleniumMetadataProviderTest { 20 | 21 | @Test 22 | void retrieve_should_identify_driver() throws Exception { 23 | final WebDriver driver = mock( RemoteWebDriver.class, RETURNS_MOCKS ); 24 | 25 | final MetadataProvider cut = SeleniumMetadataProvider.of( driver ); 26 | 27 | assertThat( cut.retrieve() ) // 28 | .contains( Pair.of( SeleniumMetadata.CHECK_TYPE, SeleniumMetadataProvider.TYPE_DRIVER ) ); 29 | } 30 | 31 | @Test 32 | void retrieve_should_identify_wrapped_driver() throws Exception { 33 | final WrapsDriver wrapsDriver = mock( WrapsDriver.class ); 34 | when( wrapsDriver.getWrappedDriver() ).thenReturn( mock( RemoteWebDriver.class, RETURNS_MOCKS ) ); 35 | 36 | final MetadataProvider cut = SeleniumMetadataProvider.of( wrapsDriver ); 37 | 38 | assertThat( cut.retrieve() ) // 39 | .contains( Pair.of( SeleniumMetadata.CHECK_TYPE, SeleniumMetadataProvider.TYPE_DRIVER ) ); 40 | } 41 | 42 | @Test 43 | void retrieve_should_identify_element() throws Exception { 44 | final WebElement element = mock( WebElement.class, RETURNS_MOCKS ); 45 | 46 | final MetadataProvider cut = SeleniumMetadataProvider.of( element ); 47 | 48 | assertThat( cut.retrieve() ) // 49 | .contains( Pair.of( SeleniumMetadata.CHECK_TYPE, SeleniumMetadataProvider.TYPE_ELEMENT ) ); 50 | } 51 | 52 | @Test 53 | void retrieve_should_identify_wrapped_element() throws Exception { 54 | final WrapsElement wrapsElement = mock( WrapsElement.class ); 55 | when( wrapsElement.getWrappedElement() ).thenReturn( mock( RemoteWebElement.class, RETURNS_MOCKS ) ); 56 | 57 | final MetadataProvider cut = SeleniumMetadataProvider.of( wrapsElement ); 58 | 59 | assertThat( cut.retrieve() ) // 60 | .contains( Pair.of( SeleniumMetadata.CHECK_TYPE, SeleniumMetadataProvider.TYPE_ELEMENT ) ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/test/java/de/retest/web/it/RecheckRemoteWebElementIT.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.it; 2 | 3 | import org.junit.jupiter.api.AfterEach; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.extension.ExtendWith; 6 | import org.junit.jupiter.params.ParameterizedTest; 7 | import org.junit.jupiter.params.provider.MethodSource; 8 | import org.openqa.selenium.JavascriptExecutor; 9 | import org.openqa.selenium.WebDriver; 10 | 11 | import de.retest.recheck.Recheck; 12 | import de.retest.recheck.RecheckImpl; 13 | import de.retest.recheck.junit.jupiter.RecheckExtension; 14 | import de.retest.web.selenium.By; 15 | import de.retest.web.testutils.PageFactory; 16 | import de.retest.web.testutils.PageFactory.Page; 17 | 18 | @ExtendWith( RecheckExtension.class ) 19 | class RecheckRemoteWebElementIT { 20 | 21 | WebDriver driver; 22 | Recheck re; 23 | 24 | @BeforeEach 25 | void setUp() { 26 | re = new RecheckImpl(); 27 | } 28 | 29 | @ParameterizedTest( name = "findElement-equals-driver-{1}" ) 30 | @MethodSource( "de.retest.web.testutils.WebDriverFactory#drivers" ) 31 | void html_via_findElement_should_equal_driver_html( final WebDriver driver, final String name ) throws Exception { 32 | this.driver = driver; 33 | driver.get( PageFactory.page( Page.FORM_PAGE ) ); 34 | 35 | Thread.sleep( 1000 ); 36 | 37 | re.check( driver, "open" ); 38 | re.check( driver.findElement( By.tagName( "html" ) ), "open" ); 39 | } 40 | 41 | @ParameterizedTest( name = "simple-webelement-{1}" ) 42 | @MethodSource( "de.retest.web.testutils.WebDriverFactory#drivers" ) 43 | void no_children_webelement_should_be_checked( final WebDriver driver, final String name ) throws Exception { 44 | this.driver = driver; 45 | driver.get( PageFactory.page( Page.FORM_PAGE ) ); 46 | 47 | Thread.sleep( 1000 ); 48 | 49 | re.check( driver.findElement( By.id( "email" ) ), "open" ); 50 | } 51 | 52 | @ParameterizedTest( name = "empty-article-{1}" ) 53 | @MethodSource( "de.retest.web.testutils.WebDriverFactory#drivers" ) 54 | void empty_article_should_be_checked( final WebDriver driver, final String name ) throws Exception { 55 | this.driver = driver; 56 | driver.get( PageFactory.page( Page.FORM_PAGE ) ); 57 | 58 | ((JavascriptExecutor) driver).executeScript( 59 | "document.getElementsByTagName('body')[0].appendChild(document.createElement('article'))" ); 60 | 61 | Thread.sleep( 1000 ); 62 | 63 | re.check( driver.findElement( By.tagName( "article" ) ), "open" ); 64 | } 65 | 66 | @AfterEach 67 | void tearDown() { 68 | driver.quit(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /.github/workflows/release-beta.yml: -------------------------------------------------------------------------------- 1 | name: Release beta 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | approval: 7 | description: 'Do you really want to release a BETA from configured BRANCH?' 8 | required: true 9 | default: 'NO' 10 | 11 | env: 12 | MVN_ARGS: --batch-mode --errors --fail-fast --no-transfer-progress 13 | 14 | jobs: 15 | default: 16 | 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Verify approval 21 | run: "[[ $(echo ${{ github.event.inputs.approval }} | tr 'a-z' 'A-Z') == 'YES' ]]" 22 | 23 | - uses: actions/checkout@v2 24 | with: 25 | fetch-depth: 0 26 | token: ${{ secrets.TRIGGER_ACTIONS_GITHUB_TOKEN }} 27 | 28 | - name: Verify for release or hotfix branch 29 | run: "[[ $( git branch --show-current ) =~ ^release.*|^hotfix.* ]]" 30 | 31 | - uses: actions/setup-java@v1 32 | with: 33 | java-version: 8 34 | 35 | - name: Cache local Maven repository 36 | uses: actions/cache@v2 37 | with: 38 | path: ~/.m2/repository 39 | key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} 40 | restore-keys: ${{ runner.os }}-maven- 41 | 42 | - name: Configure Git user 43 | run: | 44 | git config user.email "ops+githubactions@retest.de" 45 | git config user.name "retest release github action" 46 | 47 | - id: next_beta 48 | name: Find next beta version 49 | run: | 50 | # get next stable release version from pom.xml 51 | RELEASE_VERSION=$( mvn help:evaluate -Dexpression=project.version -q -DforceStdout | sed 's/-SNAPSHOT//' ) 52 | # find last beta for this version using git tags. Prefix with 0 als default for next line 53 | LAST_BETA_TAG=0$( git for-each-ref --sort=-taggerdate --count=1 --format '%(refname:short)' "refs/tags/v${RELEASE_VERSION}-beta.*" ) 54 | # Create string for beta version 55 | NEXT_BETA="$RELEASE_VERSION-beta.$((1+"${LAST_BETA_TAG/v$RELEASE_VERSION-beta./}"))" 56 | echo "::set-output name=version::$NEXT_BETA" 57 | 58 | - name: Create beta release tag 59 | run: | 60 | mvn versions:set -DnewVersion=${{ steps.next_beta.outputs.version }} -DgenerateBackupPoms=false 61 | git commit -a -m "ci: release ${{ steps.next_beta.outputs.version }}" 62 | git tag -a v${{ steps.next_beta.outputs.version }} -m "ci: tag beta release ${{ steps.next_beta.outputs.version }}" 63 | git push --tags 64 | -------------------------------------------------------------------------------- /src/main/java/de/retest/web/RecheckWebImpl.java: -------------------------------------------------------------------------------- 1 | package de.retest.web; 2 | 3 | import java.io.File; 4 | 5 | import de.retest.recheck.RecheckAdapter; 6 | import de.retest.recheck.RecheckImpl; 7 | import de.retest.recheck.RecheckOptions; 8 | import de.retest.recheck.ui.descriptors.SutState; 9 | import de.retest.web.selenium.ImplicitDriverWrapper; 10 | import de.retest.web.selenium.UnbreakableDriver; 11 | import de.retest.web.util.SeleniumWrapperUtil; 12 | import de.retest.web.util.SeleniumWrapperUtil.WrapperOf; 13 | import lombok.AccessLevel; 14 | import lombok.Getter; 15 | 16 | /** 17 | * This class is specifically needed in conjunction with the {@link UnbreakableDriver}. For simple explicit calls to 18 | * {@link #check(Object, String)}, a {@link RecheckImpl} suffices. 19 | */ 20 | public class RecheckWebImpl extends RecheckImpl { 21 | 22 | @Getter( AccessLevel.PACKAGE ) 23 | private UnbreakableDriver driver; 24 | 25 | public RecheckWebImpl() { 26 | super(); 27 | } 28 | 29 | public RecheckWebImpl( final RecheckOptions opts ) { 30 | super( opts ); 31 | } 32 | 33 | @Override 34 | public void check( final Object driver, final RecheckAdapter seleniumAdapter, final String currentStep ) { 35 | this.driver = retrieveUnbreakableDriver( driver ); 36 | super.check( driver, seleniumAdapter, currentStep ); 37 | } 38 | 39 | @Override 40 | public void check( final Object driver, final String currentStep ) { 41 | this.driver = retrieveUnbreakableDriver( driver ); 42 | super.check( driver, currentStep ); 43 | } 44 | 45 | private UnbreakableDriver retrieveUnbreakableDriver( final Object driver ) { 46 | if ( driver instanceof ImplicitDriverWrapper ) { 47 | return retrieveUnbreakableDriver( ((ImplicitDriverWrapper) driver).getWrappedDriver() ); 48 | } 49 | if ( driver instanceof UnbreakableDriver ) { 50 | return (UnbreakableDriver) driver; 51 | } 52 | if ( SeleniumWrapperUtil.isWrapper( WrapperOf.DRIVER, driver ) ) { 53 | return retrieveUnbreakableDriver( SeleniumWrapperUtil.getWrapped( WrapperOf.DRIVER, driver ) ); 54 | } 55 | return null; 56 | } 57 | 58 | @Override 59 | public SutState loadExpected( final File file ) { 60 | final SutState result = super.loadExpected( file ); 61 | if ( driver == null ) { 62 | throw new IllegalStateException( 63 | "Must first call a check-method with an UnbreakableDriver before being able to load a Golden Master (needed for unbreakable tests)!" ); 64 | } 65 | if ( result != null ) { 66 | driver.setLastExpectedState( result.getRootElements().get( 0 ) ); 67 | } 68 | return result; 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/test/resources/de/retest/web/it/RootElementPeerNullTest.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Welcome - retest docs 11 | 12 | 13 | 14 | 15 | 16 |
17 | 39 |
40 | 41 |
42 | 68 |
69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/main/java/de/retest/web/screenshot/ScreenshotProviders.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.screenshot; 2 | 3 | import static de.retest.recheck.ui.image.ImageUtils.extractScale; 4 | 5 | import java.awt.image.BufferedImage; 6 | import java.lang.reflect.Method; 7 | 8 | import org.aeonbits.owner.Converter; 9 | import org.openqa.selenium.WebDriver; 10 | import org.openqa.selenium.WebElement; 11 | 12 | import de.retest.web.RecheckWebOptions; 13 | import de.retest.web.RecheckWebProperties; 14 | import lombok.extern.slf4j.Slf4j; 15 | 16 | @Slf4j 17 | public class ScreenshotProviders { 18 | 19 | /** 20 | * The default screenshot provider: {@link ViewportOnlyMinimalScreenshot}. 21 | */ 22 | public static final ScreenshotProvider DEFAULT = new ViewportOnlyMinimalScreenshot(); 23 | /** 24 | * The none screenshot provider: {@link NoScreenshot}. 25 | */ 26 | public static final ScreenshotProvider NONE = new NoScreenshot(); 27 | 28 | public static class ScreenshotProviderConverter implements Converter { 29 | 30 | @Override 31 | public ScreenshotProvider convert( final Method method, final String input ) { 32 | switch ( input ) { 33 | case "fullPage": 34 | return new FullPageScreenshot(); 35 | case "viewportOnly": 36 | return new ViewportOnlyScreenshot(); 37 | case "viewportOnlyMinimal": 38 | return new ViewportOnlyMinimalScreenshot(); 39 | case "none": 40 | log.info( "ScreenshotProvider has been set to 'none' either via property " 41 | + RecheckWebProperties.SCREENSHOT_PROVIDER_PROPERTY_KEY + " or via " 42 | + RecheckWebOptions.class.getSimpleName() + ", will create NO screenshots." ); 43 | return NONE; 44 | default: 45 | log.warn( "Unknown configured screenshot provider '{}'. Using default value 'viewportOnlyMinimal'.", 46 | input ); 47 | return DEFAULT; 48 | } 49 | } 50 | 51 | } 52 | 53 | public static final int SCALE = extractScale(); 54 | 55 | private ScreenshotProviders() {} 56 | 57 | public static BufferedImage shoot( final WebDriver driver, final WebElement element, 58 | final ScreenshotProvider screenshotProvider ) { 59 | try { 60 | final long startTime = System.currentTimeMillis(); 61 | if ( element != null ) { 62 | return screenshotProvider.shoot( driver, element ); 63 | } 64 | final BufferedImage result = screenshotProvider.shoot( driver ); 65 | log.info( "Took {}ms to create the screenshot.", System.currentTimeMillis() - startTime ); 66 | return result; 67 | } catch ( final Exception e ) { 68 | log.error( "Exception creating screenshot for check.", e ); 69 | return null; 70 | } 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/test/java/de/retest/web/selenium/ByWhispererTest.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.selenium; 2 | 3 | import static de.retest.web.selenium.ByWhisperer.retrieveCssClassName; 4 | import static de.retest.web.selenium.ByWhisperer.retrieveCssSelector; 5 | import static de.retest.web.selenium.ByWhisperer.retrieveId; 6 | import static de.retest.web.selenium.ByWhisperer.retrieveLinkText; 7 | import static de.retest.web.selenium.ByWhisperer.retrieveName; 8 | import static de.retest.web.selenium.ByWhisperer.retrievePartialLinkText; 9 | import static de.retest.web.selenium.ByWhisperer.retrieveTag; 10 | import static de.retest.web.selenium.ByWhisperer.retrieveXPath; 11 | import static org.assertj.core.api.Assertions.assertThat; 12 | 13 | import org.junit.jupiter.api.Test; 14 | import org.openqa.selenium.By.ByClassName; 15 | import org.openqa.selenium.By.ByCssSelector; 16 | import org.openqa.selenium.By.ById; 17 | import org.openqa.selenium.By.ByLinkText; 18 | import org.openqa.selenium.By.ByName; 19 | import org.openqa.selenium.By.ByPartialLinkText; 20 | import org.openqa.selenium.By.ByTagName; 21 | import org.openqa.selenium.By.ByXPath; 22 | 23 | class ByWhispererTest { 24 | 25 | @Test 26 | void retrieveId_should_return_id() { 27 | final String id = "someId"; 28 | assertThat( retrieveId( (ById) By.id( id ) ) ).isEqualTo( id ); 29 | } 30 | 31 | @Test 32 | void retrieveCSSClassName_should_return_ClassName() { 33 | final String cssClass = "someClass"; 34 | assertThat( retrieveCssClassName( (ByClassName) By.className( cssClass ) ) ).isEqualTo( cssClass ); 35 | } 36 | 37 | @Test 38 | void retrieveName_should_return_Name() { 39 | final String name = "someName"; 40 | assertThat( retrieveName( (ByName) By.name( name ) ) ).isEqualTo( name ); 41 | } 42 | 43 | @Test 44 | void retrieveLinkText_should_return_LinkText() { 45 | final String linkText = "someLinkText"; 46 | assertThat( retrieveLinkText( (ByLinkText) By.linkText( linkText ) ) ).isEqualTo( linkText ); 47 | } 48 | 49 | @Test 50 | void retrievePartialLinkText_should_return_PartialLinkText() { 51 | final String linkText = "someLinkText"; 52 | assertThat( retrievePartialLinkText( (ByPartialLinkText) By.partialLinkText( linkText ) ) ) 53 | .isEqualTo( linkText ); 54 | } 55 | 56 | @Test 57 | void retrieveCssSelector_should_return_Selector() { 58 | final String selector = "selector"; 59 | assertThat( retrieveCssSelector( (ByCssSelector) By.cssSelector( selector ) ) ).isEqualTo( selector ); 60 | } 61 | 62 | @Test 63 | void retrieveXPath_should_return_xpath() { 64 | final String xpath = "HTML[1]/DIV[1]"; 65 | assertThat( retrieveXPath( (ByXPath) By.xpath( xpath ) ) ).isEqualTo( xpath ); 66 | } 67 | 68 | @Test 69 | void retrieveTagName_should_return_tagName() { 70 | final String tag = "div"; 71 | assertThat( retrieveTag( (ByTagName) By.tagName( tag ) ) ).isEqualTo( tag ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/de/retest/web/mapping/PathsToWebDataMapping.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.mapping; 2 | 3 | import static org.apache.commons.lang3.StringUtils.countMatches; 4 | 5 | import java.util.Iterator; 6 | import java.util.LinkedHashMap; 7 | import java.util.List; 8 | import java.util.Map; 9 | import java.util.Map.Entry; 10 | 11 | import org.openqa.selenium.JavascriptExecutor; 12 | 13 | /** 14 | * Raw paths to web data mapping that is received via {@link JavascriptExecutor#executeScript(String, Object...)}. 15 | * Internally, it is a map of paths to maps of attributes. Example: 16 | * 17 | *
18 |  * {
19 |  *   /foo[1]        = { attribute0 = value0, attribute1 = value1, ... }
20 |  *   /foo[1]/bar[1] = { attribute0 = value0, attribute1 = value1, ... }
21 |  *   ...
22 |  * }
23 |  * 
24 | */ 25 | public class PathsToWebDataMapping implements Iterable> { 26 | 27 | private final LinkedHashMap mapping; 28 | private final String rootPath; 29 | 30 | public PathsToWebDataMapping( final List> mapping ) { 31 | this( "/", mapping ); 32 | } 33 | 34 | /** 35 | * @param frameParentPath 36 | * The parent path of the frame. 37 | * @param mapping 38 | * The raw map of paths to maps of attributes. 39 | */ 40 | public PathsToWebDataMapping( final String frameParentPath, final List> mapping ) { 41 | rootPath = frameParentPath + extractRootPath( mapping ); 42 | this.mapping = convertMapping( frameParentPath, mapping ); 43 | } 44 | 45 | @SuppressWarnings( "unchecked" ) 46 | private LinkedHashMap convertMapping( final String frameParentPath, 47 | final List> mapping ) { 48 | final LinkedHashMap result = new LinkedHashMap<>(); 49 | for ( final List list : mapping ) { 50 | result.put( frameParentPath + list.get( 0 ).toString().replace( "//", "/" ), 51 | new WebData( (Map) list.get( 1 ) ) ); 52 | } 53 | return result; 54 | } 55 | 56 | private String extractRootPath( final List> mapping ) { 57 | String rootPath = null; 58 | for ( final List entry : mapping ) { 59 | if ( rootPath == null ) { 60 | rootPath = entry.get( 0 ).toString(); 61 | continue; 62 | } 63 | if ( countMatches( entry.get( 0 ).toString(), "/" ) < countMatches( rootPath, "/" ) ) { 64 | rootPath = entry.get( 0 ).toString(); 65 | } 66 | } 67 | return rootPath != null ? rootPath.replace( "//", "/" ) : ""; 68 | } 69 | 70 | public int size() { 71 | return mapping.size(); 72 | } 73 | 74 | public WebData getWebData( final String path ) { 75 | return mapping.get( path ); 76 | } 77 | 78 | @Override 79 | public Iterator> iterator() { 80 | return mapping.entrySet().iterator(); 81 | } 82 | 83 | public String getRootPath() { 84 | return rootPath; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/eclipse/retest GUI recheck-web Java 1.8.launch: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/test/java/de/retest/web/it/RecheckRemoteWebElementFailingIT.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.it; 2 | 3 | import static org.junit.jupiter.api.Assertions.fail; 4 | 5 | import org.assertj.core.api.Assertions; 6 | import org.junit.jupiter.api.AfterEach; 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.Disabled; 9 | import org.junit.jupiter.params.ParameterizedTest; 10 | import org.junit.jupiter.params.provider.MethodSource; 11 | import org.openqa.selenium.JavascriptExecutor; 12 | import org.openqa.selenium.WebDriver; 13 | 14 | import de.retest.recheck.Recheck; 15 | import de.retest.recheck.RecheckImpl; 16 | import de.retest.web.selenium.By; 17 | import de.retest.web.testutils.PageFactory; 18 | import de.retest.web.testutils.PageFactory.Page; 19 | 20 | class RecheckRemoteWebElementFailingIT { 21 | 22 | private static final String MISSING_ASSERTION_MSG = "An assertion error should have been produced"; 23 | private static final String SELECT_ELEMENT = "select-element-"; 24 | WebDriver driver; 25 | Recheck re; 26 | 27 | @BeforeEach 28 | void setUp() { 29 | re = new RecheckImpl(); 30 | } 31 | 32 | @ParameterizedTest 33 | @MethodSource( "de.retest.web.testutils.WebDriverFactory#drivers" ) 34 | @Disabled("Temporarily disabled due to bug") 35 | void inserted_child_in_webelement_should_be_checked( final WebDriver driver ) throws Exception { 36 | this.driver = driver; 37 | driver.get( PageFactory.page( Page.FORM_PAGE ) ); 38 | 39 | ((JavascriptExecutor) driver) 40 | .executeScript( "document.getElementById('multi').add(document.createElement('option'))" ); 41 | 42 | re.startTest( SELECT_ELEMENT + driver.getClass().getSimpleName() ); 43 | 44 | Thread.sleep( 1000 ); 45 | 46 | re.check( driver.findElement( By.id( "multi" ) ), "open" ); 47 | 48 | try { 49 | re.capTest(); 50 | fail( MISSING_ASSERTION_MSG ); 51 | } catch ( final AssertionError e ) { 52 | Assertions.assertThat( e ) 53 | .hasMessageContaining( "option (option) at 'html[1]/body[1]/form[3]/select[2]/option[5]':\n" // 54 | + "\t\twas inserted" ); 55 | } 56 | } 57 | 58 | @ParameterizedTest 59 | @MethodSource( "de.retest.web.testutils.WebDriverFactory#drivers" ) 60 | @Disabled("Temporarily disabled due to bug") 61 | void deleted_child_in_webelement_should_be_checked( final WebDriver driver ) throws Exception { 62 | this.driver = driver; 63 | driver.get( PageFactory.page( Page.FORM_PAGE ) ); 64 | 65 | ((JavascriptExecutor) driver).executeScript( "document.getElementById('multi').remove(3)" ); 66 | 67 | re.startTest( SELECT_ELEMENT + driver.getClass().getSimpleName() ); 68 | 69 | Thread.sleep( 1000 ); 70 | 71 | re.check( driver.findElement( By.id( "multi" ) ), "open" ); 72 | 73 | try { 74 | re.capTest(); 75 | fail( MISSING_ASSERTION_MSG ); 76 | } catch ( final AssertionError e ) { 77 | Assertions.assertThat( e ).hasMessageContaining( 78 | "option (onion_gravy) at 'html[1]/body[1]/form[3]/select[2]/option[4]':\n was deleted" ); 79 | } 80 | } 81 | 82 | @AfterEach 83 | void tearDown() { 84 | driver.quit(); 85 | re.cap(); 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/test/java/de/retest/web/meta/driver/capabilities/AllCapabilities.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.meta.driver.capabilities; 2 | 3 | import java.util.logging.Logger; 4 | import java.util.stream.Stream; 5 | 6 | import org.openqa.selenium.Capabilities; 7 | import org.openqa.selenium.Platform; 8 | import org.openqa.selenium.remote.BrowserType; 9 | import org.openqa.selenium.remote.CapabilityType; 10 | import org.openqa.selenium.remote.DesiredCapabilities; 11 | 12 | public class AllCapabilities { 13 | 14 | private static final Logger LOG = Logger.getLogger(Capabilities.class.getName()); 15 | 16 | public static Stream capabilities() { 17 | return Stream.of( // 18 | android(), // 19 | iphone(), // 20 | ipad(), // 21 | htmlUnit(), // 22 | 23 | chrome(), // 24 | firefox(), // 25 | 26 | edge(), // 27 | internetExplorer(), // 28 | 29 | safari(), // 30 | 31 | operaBlink() // 32 | ); 33 | } 34 | 35 | private static Capabilities iphone() { 36 | return new DesiredCapabilities(BrowserType.IPHONE, "", Platform.MAC); 37 | } 38 | 39 | private static Capabilities ipad() { 40 | return new DesiredCapabilities(BrowserType.IPAD, "", Platform.MAC); 41 | } 42 | 43 | private static DesiredCapabilities android() { 44 | return new DesiredCapabilities(BrowserType.ANDROID, "", Platform.ANDROID); 45 | } 46 | 47 | public static DesiredCapabilities chrome() { 48 | LOG.info("Using `new ChromeOptions()` is preferred to `DesiredCapabilities.chrome()`"); 49 | return new DesiredCapabilities(BrowserType.CHROME, "", Platform.ANY); 50 | } 51 | 52 | public static DesiredCapabilities firefox() { 53 | LOG.info("Using `new FirefoxOptions()` is preferred to `DesiredCapabilities.firefox()`"); 54 | DesiredCapabilities capabilities = new DesiredCapabilities( 55 | BrowserType.FIREFOX, 56 | "", 57 | Platform.ANY); 58 | capabilities.setCapability("acceptInsecureCerts", true); 59 | 60 | return capabilities; 61 | } 62 | 63 | public static DesiredCapabilities htmlUnit() { 64 | return new DesiredCapabilities(BrowserType.HTMLUNIT, "", Platform.ANY); 65 | } 66 | 67 | public static DesiredCapabilities edge() { 68 | LOG.info("Using `new EdgeOptions()` is preferred to `DesiredCapabilities.edge()`"); 69 | return new DesiredCapabilities(BrowserType.EDGE, "", Platform.WINDOWS); 70 | } 71 | public static DesiredCapabilities internetExplorer() { 72 | DesiredCapabilities capabilities = new DesiredCapabilities( 73 | BrowserType.IE, "", Platform.WINDOWS); 74 | capabilities.setCapability(CapabilityType.ForSeleniumServer.ENSURING_CLEAN_SESSION, true); 75 | return capabilities; 76 | } 77 | 78 | public static DesiredCapabilities operaBlink() { 79 | LOG.info("Using `new OperaOptions()` is preferred to `DesiredCapabilities.operaBlink()`"); 80 | return new DesiredCapabilities(BrowserType.OPERA, "", Platform.ANY); 81 | } 82 | 83 | public static DesiredCapabilities safari() { 84 | LOG.info("Using `new SafariOptions()` is preferred to `DesiredCapabilities.safari()`"); 85 | return new DesiredCapabilities(BrowserType.SAFARI, "", Platform.MAC); 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/test/java/de/retest/web/it/SimpleRetestIdShowcaseIT.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.it; 2 | 3 | import java.nio.file.Path; 4 | import java.nio.file.Paths; 5 | 6 | import org.apache.commons.io.FileUtils; 7 | import org.assertj.core.api.Assertions; 8 | import org.junit.After; 9 | import org.junit.Before; 10 | import org.junit.Test; 11 | import org.openqa.selenium.chrome.ChromeDriver; 12 | import org.openqa.selenium.chrome.ChromeOptions; 13 | 14 | import de.retest.recheck.Recheck; 15 | import de.retest.web.RecheckWebImpl; 16 | import de.retest.web.selenium.By; 17 | import de.retest.web.selenium.UnbreakableDriver; 18 | 19 | /* 20 | * Simple recheck-web showcase for a Chrome-based integration test. See other *IT classes for more examples. 21 | */ 22 | public class SimpleRetestIdShowcaseIT { 23 | 24 | private UnbreakableDriver driver; 25 | private Recheck re; 26 | 27 | @Before 28 | public void setup() { 29 | // If ChromeDriver (http://chromedriver.chromium.org/downloads/) is not in your PATH, uncomment this and point to your installation. 30 | // System.setProperty( "webdriver.chrome.driver", "path/to/chromedriver" ); 31 | 32 | final ChromeOptions opts = new ChromeOptions(); 33 | opts.addArguments( 34 | // Enable headless mode for faster execution. 35 | "--headless", 36 | // Use Chrome in container-based Travis CI enviroment (see https://docs.travis-ci.com/user/chrome#Sandboxing). 37 | "--no-sandbox", 38 | // Fix window size for stable results. 39 | "--window-size=1200,800" ); 40 | driver = new UnbreakableDriver( new ChromeDriver( opts ) ); 41 | 42 | // Use the default implementation. 43 | re = new RecheckWebImpl(); 44 | } 45 | 46 | @Test 47 | public void index() throws Exception { 48 | // Set the file name of the Golden Master. 49 | re.startTest( "simple-showcase" ); 50 | 51 | // Do your Selenium stuff. 52 | final Path showcasePath = Paths.get( "src/test/resources/pages/showcase/retest-changed.html" ); 53 | driver.get( showcasePath.toUri().toURL().toString() ); 54 | 55 | // Single call instead of multiple assertions (doesn't fail on differences). 56 | re.check( driver, "index" ); 57 | 58 | driver.findElement( By.retestId( "contact" ) ).click(); 59 | 60 | // Works even if the GM is not yet created, if retestId is deterministic 61 | re.check( driver, "new_state" ); 62 | driver.findElement( By.retestId( "for_testers" ) ).click(); 63 | 64 | // Usually, we conclude the test case with re.capTest(), 65 | // but since we expect differences, we check these. 66 | Assertions.assertThatThrownBy( () -> re.capTest() ).hasMessageContainingAll( // 67 | "text: expected=\"Contact\", actual=\"Support\"", // 68 | "No Golden Master found. First time test was run?" ); 69 | } 70 | 71 | @After 72 | public void tearDown() { 73 | driver.quit(); 74 | 75 | // Produce the result file. 76 | // re.cap(); 77 | 78 | // Not standard! Ensure we delete the GM that was created 79 | FileUtils.deleteQuietly( Paths.get( "src/test/resources/retest/recheck", 80 | "de.retest.web.it.SimpleRetestIdShowcaseIT", "simple-showcase.new_state.recheck" ).toFile() ); 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/test/resources/pages/simple-page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hello WebDriver 4 | 5 | 6 |

Heading

7 | 8 |

A single line of text

9 | 10 | 11 |
12 |

A div containing

13 | More than one line of text
14 | 15 |
and block level elements
16 |
17 | 18 | An inline element 19 | 20 |

This line has lots 21 | 22 | of spaces. 23 |

24 | 25 |

This line has a non-breaking space

26 | 27 |

This line has a   non-breaking space and spaces

28 | 29 |

These lines  
  have leading and trailing NBSPs  

30 | 31 |

This line has text within elements that are meant to be displayed 32 | inline

33 | 34 |
35 |

before pre

36 |
   This section has a preformatted
37 |     text block    
38 |   split in four lines
39 |          
40 |

after pre

41 |
42 | 43 |

Some text

Some more text

44 | 45 |
Cheese

Some text

Some more text

and also

Brie
46 | 47 |
Hello, world
48 | 49 |
50 | 54 |
55 | 56 |
57 |

58 | 59 | 60 |

61 |
62 | 63 | 72 | 73 | 74 | 75 |
Top level
76 |
77 |
Nested
78 |
79 | 80 | 81 | 82 | 83 | 84 |
beforeSpace afterSpace
85 | 86 | 87 | 88 | 89 | 90 | a link to an icon 91 | 92 | {a="b", c=1, d=true} 93 | {a="\\b\\\"'\'"} 94 | 95 |            ​‌‍  ⁠ test            ​‌‍  ⁠  96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /src/test/resources/pages/simple-page-diff.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hello WebDriver 4 | 5 | 6 |

Heading

7 |

Subheading

8 |

A single line of text

9 | 10 | 11 |
12 |

A div containing

13 | More than one line of text
14 | 15 |
and block level elements
16 |
17 | 18 | An inline element 19 | 20 |

This line has lots 21 | 22 | of spaces. 23 |

24 | 25 |

This line has a non-breaking space

26 | 27 |

This line has a   non-breaking space and spaces

28 | 29 |

These lines  
  have leading and trailing NBSPs  

30 | 31 |

This line has text within elements that are meant to be displayed 32 | inline

33 | 34 |
35 |

before pre

36 |
   This section has a preformatted
37 |     text block    
38 |   split in four lines
39 |          
40 |

after pre

41 |
42 | 43 |

Some changed text

44 | 45 |
Cheese

Some text

Some more text

and also

Brie
46 | 47 |
Hello, world
48 | 49 |
50 | 54 |
55 | 56 |
57 |

58 | 59 | 60 |

61 |
62 | 63 | 72 | 73 | 74 | 75 |
Top level
76 |
77 |
Nested
78 |
79 | 80 | 81 | 82 | 83 | 84 |
beforeSpace afterSpace
85 | 86 | 87 | 88 | 89 | 90 | a link to an icon 91 | 92 | {a="b", c=1, d=true} 93 | {a="\\b\\\"'\'"} 94 | 95 |            ​‌‍  ⁠ test            ​‌‍  ⁠  96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /src/main/java/de/retest/web/selenium/By.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.selenium; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.function.Predicate; 6 | 7 | import de.retest.recheck.ui.descriptors.Element; 8 | import de.retest.recheck.ui.descriptors.RootElement; 9 | import de.retest.recheck.ui.diff.Alignment; 10 | 11 | public abstract class By extends org.openqa.selenium.By { 12 | 13 | public static ByBestMatchToRetestId retestId( final String retestId ) { 14 | return new ByBestMatchToRetestId( retestId ); 15 | } 16 | 17 | public static Element findElement( final RootElement lastExpectedState, final RootElement lastActualState, 18 | final Predicate predicate ) { 19 | if ( lastExpectedState == null ) { 20 | throw new IllegalArgumentException( "Cannot find element in null state." ); 21 | } 22 | final Element resultFromExpected = findElement( lastExpectedState.getContainedElements(), predicate ); 23 | if ( resultFromExpected == null ) { 24 | return null; 25 | } 26 | final Alignment alignment = Alignment.createAlignment( lastExpectedState, lastActualState ); 27 | final Element resultFromActual = alignment.getActual( resultFromExpected ); 28 | if ( resultFromActual == null ) { 29 | throw new NoElementWithHighEnoughMatchFoundException( resultFromExpected ); 30 | } 31 | return resultFromActual.applyRetestId( resultFromExpected.getRetestId() ); 32 | } 33 | 34 | public static Element findElement( final List children, final Predicate predicate ) { 35 | for ( final Element element : children ) { 36 | if ( predicate.test( element ) ) { 37 | return element; 38 | } 39 | final Element result = findElement( element.getContainedElements(), predicate ); 40 | if ( result != null ) { 41 | return result; 42 | } 43 | } 44 | return null; 45 | } 46 | 47 | public static List findElements( final List children, final Predicate predicate ) { 48 | final List result = new ArrayList<>(); 49 | for ( final Element element : children ) { 50 | if ( predicate.test( element ) ) { 51 | result.add( element ); 52 | } 53 | result.addAll( findElements( element.getContainedElements(), predicate ) ); 54 | } 55 | return result; 56 | } 57 | 58 | public static Element findElementByAttribute( final RootElement lastExpectedState, 59 | final RootElement lastActualState, final String attributeName, final Predicate condition ) { 60 | return findElement( lastExpectedState, lastActualState, element -> { 61 | if ( element.getIdentifyingAttributes().get( attributeName ) != null ) { 62 | return condition.test( element.getIdentifyingAttributes().getAttribute( attributeName ).getValue() ); 63 | } else if ( element.getAttributes().get( attributeName ) != null ) { 64 | return condition.test( element.getAttributes().get( attributeName ) ); 65 | } else { 66 | return false; 67 | } 68 | } ); 69 | } 70 | 71 | public static Element findElementByAttribute( final RootElement lastExpectedState, 72 | final RootElement lastActualState, final String attributeName, final Object attributeValue ) { 73 | return findElementByAttribute( lastExpectedState, lastActualState, attributeName, attributeValue::equals ); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/test/resources/de/retest/web/it/TestHealerCssSelectorIT.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | retest - Java Swing GUI Testing 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | 21 | 23 | 24 | 26 | 27 | 28 | 29 |

30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | Unvisited 40 | 41 | Visited 42 | 43 | 44 | 45 | other 47 | default 49 | 50 | 51 | 52 | 57 | 58 | -------------------------------------------------------------------------------- /src/test/java/de/retest/web/selenium/WriteToResultWarningConsumerTest.java: -------------------------------------------------------------------------------- 1 | package de.retest.web.selenium; 2 | 3 | import static org.mockito.Mockito.mock; 4 | import static org.mockito.Mockito.never; 5 | import static org.mockito.Mockito.verify; 6 | import static org.mockito.Mockito.when; 7 | 8 | import java.util.Collections; 9 | import java.util.stream.Stream; 10 | 11 | import org.junit.jupiter.api.BeforeEach; 12 | import org.junit.jupiter.api.Test; 13 | 14 | import de.retest.recheck.ui.diff.AttributeDifference; 15 | import de.retest.recheck.ui.diff.ElementDifference; 16 | import de.retest.recheck.ui.diff.ElementIdentificationWarning; 17 | import de.retest.web.selenium.WriteToResultWarningConsumer.ElementDifferenceRetriever; 18 | 19 | class WriteToResultWarningConsumerTest { 20 | 21 | ElementDifferenceRetriever retriever; 22 | WriteToResultWarningConsumer cut; 23 | 24 | ElementIdentificationWarning warning; 25 | QualifiedElementWarning qualifiedWarning; 26 | 27 | @BeforeEach 28 | void setUp() { 29 | retriever = mock( ElementDifferenceRetriever.class ); 30 | 31 | cut = new WriteToResultWarningConsumer( retriever ); 32 | 33 | warning = new ElementIdentificationWarning( "file.java", 0, "id", "File" ); 34 | qualifiedWarning = new QualifiedElementWarning( "id", "id", warning ); 35 | } 36 | 37 | @Test 38 | void accept_should_find_warning_and_add_to_result() throws Exception { 39 | final AttributeDifference attribute = mock( AttributeDifference.class ); 40 | when( attribute.getKey() ).thenReturn( "id" ); 41 | 42 | final ElementDifference difference = mock( ElementDifference.class ); 43 | when( difference.getRetestId() ).thenReturn( "id" ); 44 | when( difference.getAttributeDifferences() ).thenReturn( Collections.singletonList( attribute ) ); 45 | 46 | when( retriever.getDifferences() ).thenReturn( Stream.of( difference ) ); 47 | 48 | cut.accept( qualifiedWarning ); 49 | 50 | verify( attribute ).addElementIdentificationWarning( warning ); 51 | } 52 | 53 | @Test 54 | void accept_should_not_add_if_attribute_does_not_match() throws Exception { 55 | final AttributeDifference attribute = mock( AttributeDifference.class ); 56 | when( attribute.getKey() ).thenReturn( "foo" ); 57 | 58 | final ElementDifference difference = mock( ElementDifference.class ); 59 | when( difference.getRetestId() ).thenReturn( "id" ); 60 | when( difference.getAttributeDifferences() ).thenReturn( Collections.singletonList( attribute ) ); 61 | 62 | when( retriever.getDifferences() ).thenReturn( Stream.of( difference ) ); 63 | 64 | cut.accept( qualifiedWarning ); 65 | 66 | verify( attribute, never() ).addElementIdentificationWarning( warning ); 67 | } 68 | 69 | @Test 70 | void accept_should_not_add_if_retest_id_does_not_match() throws Exception { 71 | final AttributeDifference attribute = mock( AttributeDifference.class ); 72 | when( attribute.getKey() ).thenReturn( "id" ); 73 | 74 | final ElementDifference difference = mock( ElementDifference.class ); 75 | when( difference.getRetestId() ).thenReturn( "foo" ); 76 | when( difference.getAttributeDifferences() ).thenReturn( Collections.singletonList( attribute ) ); 77 | 78 | when( retriever.getDifferences() ).thenReturn( Stream.of( difference ) ); 79 | 80 | cut.accept( qualifiedWarning ); 81 | 82 | verify( attribute, never() ).addElementIdentificationWarning( warning ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/test/resources/retest/recheck/de.retest.web.it.RecheckRemoteWebElementIT/empty-article-FirefoxDriver.open.recheck/retest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 8 9 | 900 10 | 0 11 | 1340 12 | 13 | 14 | 0 15 | 892 16 | -892 17 | 0 18 | 19 | html[1]/body[1]/article[1] 20 | 1 21 | article 22 | 23 | 24 | 25 | 26 | 27 | shown 28 | true 29 | 30 | 31 | 32 | 33 | 34 | 35 | driver.type 36 | FirefoxDriver 37 | 38 | 39 | time.offset 40 | +01:00 41 | 42 | 43 | os.arch 44 | amd64 45 | 46 | 47 | check.type 48 | element 49 | 50 | 51 | vcs.commit 52 | 6494eed61cc748f758146dbd619b7e05660caa65 53 | 54 | 55 | window.height 56 | 768 57 | 58 | 59 | vcs.branch 60 | feature/check-clickability 61 | 62 | 63 | url 64 | file:///home/travis/build/retest/recheck-web/src/test/resources/pages/form-page.html 65 | 66 | 67 | os.version 68 | 5.3.0-40-generic 69 | 70 | 71 | browser.name 72 | firefox 73 | 74 | 75 | browser.version 76 | 73.0.1 77 | 78 | 79 | time.time 80 | 09:04:34.28 81 | 82 | 83 | time.zone 84 | Europe/Berlin 85 | 86 | 87 | window.width 88 | 1366 89 | 90 | 91 | os.name 92 | LINUX 93 | 94 | 95 | time.date 96 | 2020-03-17 97 | 98 | 99 | vcs.name 100 | git 101 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /src/test/java/de/retest/web/PeerConverterTest.java: -------------------------------------------------------------------------------- 1 | package de.retest.web; 2 | 3 | import static java.util.Arrays.asList; 4 | import static java.util.Collections.singletonList; 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | 7 | import java.util.ArrayList; 8 | import java.util.HashMap; 9 | import java.util.List; 10 | import java.util.Map; 11 | 12 | import org.junit.jupiter.api.Test; 13 | 14 | import de.retest.recheck.ui.DefaultValueFinder; 15 | import de.retest.recheck.ui.descriptors.RootElement; 16 | import de.retest.recheck.ui.diff.RootElementDifference; 17 | import de.retest.recheck.ui.diff.RootElementDifferenceFinder; 18 | import de.retest.recheck.util.RetestIdProviderUtil; 19 | import de.retest.web.mapping.PathsToWebDataMapping; 20 | 21 | class PeerConverterTest { 22 | 23 | @Test 24 | void getParentPath_should_return_null_for_toplevel() { 25 | assertThat( PeerConverter.getParentPath( "//HTML[1]" ) ).isNull(); 26 | } 27 | 28 | @Test 29 | void getParentPath_should_return_parent_path() { 30 | assertThat( PeerConverter.getParentPath( "//HTML[1]/BODY[1]" ) ).isEqualTo( "//HTML[1]" ); 31 | } 32 | 33 | @Test 34 | void convertToPeers_should_result_in_valid_tree() throws Exception { 35 | final List> data = new ArrayList<>(); 36 | data.add( asList( "//HTML[1]/BODY[1]", toHashMap( "BODY" ) ) ); 37 | data.add( asList( "//HTML[1]/BODY[1]/FOOTER[1]/DIV[1]", toHashMap( "DIV" ) ) ); 38 | data.add( asList( "//HTML[1]/BODY[1]/FOOTER[1]/DIV[1]/DIV[1]", toHashMap( "DIV" ) ) ); 39 | data.add( asList( "//HTML[1]/BODY[1]/FOOTER[1]/DIV[1]/DIV[1]/UL[1]/LI[1]", toHashMap( "LI" ) ) ); 40 | data.add( asList( "//HTML[1]/BODY[1]/FOOTER[1]/DIV[1]/DIV[1]/UL[1]/LI[1]/A[1]", toHashMap( "A" ) ) ); 41 | data.add( asList( "//HTML[1]", toHashMap( "HTML" ) ) ); 42 | data.add( asList( "//HTML[1]/BODY[1]/FOOTER[1]", toHashMap( "FOOTER" ) ) ); 43 | data.add( asList( "//HTML[1]/BODY[1]/FOOTER[1]/DIV[1]/DIV[1]/UL[1]/LI[2]", toHashMap( "LI" ) ) ); 44 | data.add( asList( "//HTML[1]/BODY[1]/FOOTER[1]/DIV[1]/DIV[1]/UL[1]/LI[2]/A[1]", toHashMap( "A" ) ) ); 45 | data.add( asList( "//HTML[1]/BODY[1]/FOOTER[1]/DIV[1]/DIV[1]/UL[1]/LI[3]", toHashMap( "LI" ) ) ); 46 | data.add( asList( "//HTML[1]/BODY[1]/FOOTER[1]/DIV[1]/DIV[1]/UL[1]", toHashMap( "UL" ) ) ); 47 | data.add( asList( "//HTML[1]/BODY[1]/FOOTER[1]/DIV[1]/DIV[1]/UL[1]/LI[3]/A[1]", toHashMap( "A" ) ) ); 48 | final PathsToWebDataMapping mapping = new PathsToWebDataMapping( data ); 49 | 50 | final DefaultValueFinder defaultValueFinder = ( identifyingAttributes, attributeKey, attributeValue ) -> false; 51 | final PeerConverter cut = new PeerConverter( RetestIdProviderUtil.getConfiguredRetestIdProvider(), mapping, 52 | "title", null, defaultValueFinder, null ); 53 | final RootElement root = cut.convertToPeers(); 54 | 55 | final RootElementDifferenceFinder diffFinder = new RootElementDifferenceFinder( defaultValueFinder ); 56 | final List diffs = 57 | diffFinder.findDifferences( singletonList( root ), singletonList( root ) ); 58 | assertThat( diffs ).isEmpty(); 59 | } 60 | 61 | private Map toHashMap( final String tagName ) { 62 | final Map result = new HashMap<>(); 63 | result.put( "tagName", tagName ); 64 | result.put( "x", "10" ); 65 | result.put( "y", "10" ); 66 | result.put( "height", "100" ); 67 | result.put( "width", "100" ); 68 | return result; 69 | } 70 | } 71 | --------------------------------------------------------------------------------