diff --git a/client-java/test-utils-java/src/main/java/org/evomaster/test/utils/SeleniumEMUtils.java b/client-java/test-utils-java/src/main/java/org/evomaster/test/utils/SeleniumEMUtils.java index d69504d1b5..322a997887 100644 --- a/client-java/test-utils-java/src/main/java/org/evomaster/test/utils/SeleniumEMUtils.java +++ b/client-java/test-utils-java/src/main/java/org/evomaster/test/utils/SeleniumEMUtils.java @@ -2,6 +2,7 @@ import org.openqa.selenium.*; import org.openqa.selenium.support.ui.ExpectedCondition; +import org.openqa.selenium.support.ui.Select; import org.openqa.selenium.support.ui.WebDriverWait; import org.testcontainers.Testcontainers; @@ -127,6 +128,29 @@ public static void clickAndWaitPageLoad(WebDriver driver, String cssSelector){ //TODO will need to check if JS executing in background } + public static void selectAndWaitPageLoad(WebDriver driver, String cssSelector, String value) { + Select element; + try { + WebElement el = driver.findElement(By.cssSelector(cssSelector)); + element = new Select(el); + } catch (NoSuchElementException e) { + throw new RuntimeException("Cannot locate element with '" + cssSelector + "'." + + "\nCurrent URL is: " + driver.getCurrentUrl() + + "\nCurrent page is: " + driver.getPageSource()); + } + try { + element.selectByValue(value); + } catch (NoSuchElementException e){ + element.selectByVisibleText(value); + } + + try { + Thread.sleep(50); + } catch (Exception e) { + } + waitForPageToLoad(driver, 2); + } + public static void goToPage(WebDriver driver, String pageURL, int timeoutSeconds){ driver.get(pageURL); waitForPageToLoad(driver, timeoutSeconds); diff --git a/core-it/src/test/kotlin/org/evomaster/core/problem/webfrontend/service/BrowserControllerDockerTest.kt b/core-it/src/test/kotlin/org/evomaster/core/problem/webfrontend/service/BrowserControllerDockerTest.kt index 27ae64125b..ee67708e19 100644 --- a/core-it/src/test/kotlin/org/evomaster/core/problem/webfrontend/service/BrowserControllerDockerTest.kt +++ b/core-it/src/test/kotlin/org/evomaster/core/problem/webfrontend/service/BrowserControllerDockerTest.kt @@ -83,4 +83,49 @@ internal class BrowserControllerDockerTest{ browser.goBack() assertEquals(aPage, browser.getCurrentUrl()) } + + //TODO @IVa add test for Select + @Test + fun testSingleSelect(){ + browser.initUrlOfStartingPage("http://localhost:8080/",true) + //browser.initUrlOfStartingPage("http://localhost:${getPort()}",true)// to double check + browser.goToStartingPage() + + var actions = browser.computePossibleUserInteractions() + assertEquals(1, actions.size) + val homePage = browser.getCurrentUrl() + assertTrue(homePage.endsWith("/"), homePage) + val a = actions[0] // first action is the dropdown list + assertEquals(UserActionType.SELECT_SINGLE, a.userActionType) + + browser.selectAndWaitPageLoad(a.cssSelector, listOf(a.inputs[1].toString())) + val page1 = browser.getCurrentUrl() + assertTrue(page1.endsWith("1?"), page1)// ? to be fixed + + actions = browser.computePossibleUserInteractions() + assertEquals(1, actions.size) + var backHome = actions.first { it.cssSelector.contains("a") } + browser.clickAndWaitPageLoad(backHome.cssSelector) + assertEquals(homePage, browser.getCurrentUrl()) + + browser.selectAndWaitPageLoad(a.cssSelector, listOf(a.inputs[2].toString())) + val page2 = browser.getCurrentUrl() + assertTrue(page2.endsWith("2?"), page2) + actions = browser.computePossibleUserInteractions() + assertEquals(1, actions.size) + backHome = actions.first { it.cssSelector.contains("a") } + browser.clickAndWaitPageLoad(backHome.cssSelector) + assertEquals(homePage, browser.getCurrentUrl()) + + browser.selectAndWaitPageLoad(a.cssSelector, listOf(a.inputs[3].toString())) + val page3 = browser.getCurrentUrl() + assertTrue(page3.endsWith("3?"), page3) + actions = browser.computePossibleUserInteractions() + assertEquals(1, actions.size) + backHome = actions.first { it.cssSelector.contains("a") } + browser.clickAndWaitPageLoad(backHome.cssSelector) + assertEquals(homePage, browser.getCurrentUrl()) + + } + } \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/output/service/WebTestCaseWriter.kt b/core/src/main/kotlin/org/evomaster/core/output/service/WebTestCaseWriter.kt index 4fad8836ba..6ba5079497 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/service/WebTestCaseWriter.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/service/WebTestCaseWriter.kt @@ -34,7 +34,12 @@ class WebTestCaseWriter : TestCaseWriter() { lines.addStatement("goToPage($driver, $baseUrlOfSut, 5)") if(ind.individual is WebIndividual){ - ind.evaluatedMainActions().forEachIndexed { index, a -> + val all = ind.evaluatedMainActions() + for(index in all.indices) { + val a = all[index] + if(a.result.stopping){ + break + } addActionLines(a.action, index, testCaseName, lines, a.result, testSuitePath, baseUrlOfSut) } val lastEvaluated = ind.evaluatedMainActions().last() @@ -44,8 +49,10 @@ class WebTestCaseWriter : TestCaseWriter() { lastResult.getUrlPageEnd()!! } else { //if stopping, it means nothing could be done, and no info on where it went. - //it also implies that such entry itself was printed out in the test - assert(lastAction.userInteractions.isEmpty()) + //it also implies that such entry itself was printed out in the test. + //however, it might be that a mutated test ends up on a page in which action is not applicable + //TODO should check lastAction.isApplicableInGivenPage(pageBeforeExecutingAction) + //assert(lastAction.userInteractions.isEmpty()) lastResult.getUrlPageStart()!! } lines.add(getCommentOnPage("ended on page", url,null,lastResult.getValidHtml())) @@ -77,6 +84,11 @@ class WebTestCaseWriter : TestCaseWriter() { lines.addStatement("clickAndWaitPageLoad($driver, \"${it.cssSelector}\")") lines.append(getCommentOnPage("on page", r.getUrlPageStart()!!, r.getUrlPageEnd(), r.getValidHtml())) } + UserActionType.SELECT_SINGLE-> { + val selectedValue = a.singleSelection[it.cssSelector]?.getValueAsRawString() // TODO what if missing? + lines.addStatement("selectAndWaitPageLoad($driver, \"${it.cssSelector}\", \"$selectedValue\")") + lines.append(getCommentOnPage("on page", r.getUrlPageStart()!!, r.getUrlPageEnd(), r.getValidHtml())) + } //TODO all other cases else -> throw IllegalStateException("Not handled action type: ${it.userActionType}") } diff --git a/core/src/main/kotlin/org/evomaster/core/problem/webfrontend/BrowserActionBuilder.kt b/core/src/main/kotlin/org/evomaster/core/problem/webfrontend/BrowserActionBuilder.kt index 8d4dcab9cd..f5643612c9 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/webfrontend/BrowserActionBuilder.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/webfrontend/BrowserActionBuilder.kt @@ -1,18 +1,27 @@ package org.evomaster.core.problem.webfrontend +import org.evomaster.core.search.gene.collection.EnumGene +import org.evomaster.core.search.gene.numeric.IntegerGene import org.jsoup.Jsoup +import org.jsoup.nodes.Document +import org.openqa.selenium.By +import org.openqa.selenium.remote.RemoteWebDriver +import org.openqa.selenium.support.ui.Select import java.net.URI import java.net.URISyntaxException -import java.net.URL +import kotlin.math.min object BrowserActionBuilder { - fun computePossibleUserInteractions(html: String) : List{ + /** + * Given the current page we need to see what can we do in the page, what kind of interactions we can have. + */ + fun computePossibleUserInteractions(driver: RemoteWebDriver) : List{ val document = try{ - Jsoup.parse(html) + Jsoup.parse(driver.pageSource) }catch (e: Exception){ //TODO double-check return listOf() @@ -22,10 +31,52 @@ object BrowserActionBuilder { //TODO all cases + handleALinks(document, driver, list) + handleDropDowns(document, driver, list) + + return list + } + + private fun handleDropDowns( + document: Document, + driver: RemoteWebDriver, + list: MutableList + ) { + + document.getElementsByTag("select") + .forEach { jsoup -> + val dropdown = Select(driver.findElement(By.cssSelector(jsoup.cssSelector()))) + val type = if(dropdown.isMultiple) { + UserActionType.SELECT_MULTI + } else { + UserActionType.SELECT_SINGLE + } + val options = dropdown.options + .filter{it.isEnabled} + .map{ + /* + Look at "value" attribute. unfortunately, it is not mandatory. + in such case, we rather use the visible text. + note: prefer avoiding indices, as they are not reliable, eg, if changes, it would not + break the test, unless out of bounds + */ + it.getAttribute("value") ?: it.text + } + list.add(WebUserInteraction(jsoup.cssSelector(), type, options)) + } + } + + +/* Analyze anchor tags to identify clickable elements*/ + private fun handleALinks( + document: Document, + driver: RemoteWebDriver, + list: MutableList + ) { document.getElementsByTag("a") .forEach { val href = it.attr("href") - val canClick = if(!href.isNullOrBlank()) { + val canClick = if (!href.isNullOrBlank()) { val uri = try { URI(href) } catch (e: URISyntaxException) { @@ -38,25 +89,36 @@ object BrowserActionBuilder { val onclick = it.attr("onclick") !onclick.isNullOrBlank() } - if(canClick){ + if (canClick) { list.add(WebUserInteraction(it.cssSelector(), UserActionType.CLICK)) } } - - return list } - fun createPossibleActions(html: String) : List{ + fun createPossibleActions(driver: RemoteWebDriver) : List{ - val interactions = computePossibleUserInteractions(html) + val interactions = computePossibleUserInteractions(driver) val inputs = interactions.filter { it.userActionType == UserActionType.FILL_TEXT } val others = interactions.filter { it.userActionType != UserActionType.FILL_TEXT } //TODO genes for inputs - return others.map { WebAction(mutableListOf(it)) } + return others.map { + when(it.userActionType) { + UserActionType.CLICK -> WebAction(mutableListOf(it)) + UserActionType.SELECT_SINGLE -> { + val selection = EnumGene(it.cssSelector, it.inputs) + WebAction(mutableListOf(it), singleSelection = mutableMapOf(it.cssSelector to selection)) + } + //TODO multi + else -> { + //TODO log warn + WebAction(mutableListOf(it)) + } + } + } } } \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/problem/webfrontend/UserActionType.kt b/core/src/main/kotlin/org/evomaster/core/problem/webfrontend/UserActionType.kt index b209079254..02b26711b2 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/webfrontend/UserActionType.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/webfrontend/UserActionType.kt @@ -4,5 +4,28 @@ enum class UserActionType { CLICK, - FILL_TEXT + FILL_TEXT, + + /** + * Select only a single element in a dropdown elements might allow to select more than one element + */ + SELECT_MULTI, + + DOUBLE_CLICK, + RIGHT_CLICK, + HOVER_OVER_ELEMENT, + VERTICAL_SCROLLING, + HORIZONTAL_SCROLLING, + DRAG_AND_DROP, + + CHECK_CHECKBOX, + UNCHECK_CHECKBOX, + CHOOSE_RADIO_BUTTON, + SELECT, + UPLOAD_FILE, } \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/problem/webfrontend/WebAction.kt b/core/src/main/kotlin/org/evomaster/core/problem/webfrontend/WebAction.kt index dd46ef791c..0c59b9e541 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/webfrontend/WebAction.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/webfrontend/WebAction.kt @@ -2,35 +2,90 @@ package org.evomaster.core.problem.webfrontend import org.evomaster.core.problem.gui.GuiAction import org.evomaster.core.search.StructuralElement +import org.evomaster.core.search.gene.BooleanGene import org.evomaster.core.search.gene.Gene +import org.evomaster.core.search.gene.collection.ArrayGene +import org.evomaster.core.search.gene.collection.EnumGene +import org.evomaster.core.search.gene.numeric.IntegerGene import org.evomaster.core.search.gene.string.StringGene import org.jsoup.Jsoup class WebAction( /** - * Different interactions could be squizzed into a single action, like filling all text inputs - * of a form and then submit it + * Different interactions could be squeezed into a single action, like filling all text inputs + * of a form and then submit it - + * ie. filling a form and submitting - would be considered one action */ val userInteractions: MutableList = mutableListOf(), /** * Map from cssLocator (coming from [userInteractions]) for text input to StringGene representing its value + * MutableMap */ - val textData : MutableMap = mutableMapOf() -) : GuiAction(textData.values.map { it }) { + val textData: MutableMap = mutableMapOf(), + /** + * For a dropdown menu, where only one selection is possible, specify which one to choose. + * This is based on the "value" attribute, or the visible text if not available. + */ + val singleSelection: MutableMap> = mutableMapOf(), + /** + * TODO explanation + * TODO might change ArrayGene + */ + val multiSelection: MutableMap> = mutableMapOf(), +) : GuiAction( + textData.values.map { it } + .plus(singleSelection.values.map { it }) + .plus(multiSelection.values.map { it }) +) { init { val nFillText = userInteractions.count { it.userActionType == UserActionType.FILL_TEXT } - if(nFillText != textData.size){ + if (nFillText != textData.size) { throw IllegalArgumentException("Mismatch between $nFillText fill text actions and ${textData.size} genes for it") } - for(key in textData.keys){ - if(! userInteractions.any { it.cssSelector == key }){ + for (key in textData.keys) { + if (!userInteractions.any { it.cssSelector == key }) { throw IllegalArgumentException("Missing info for input: $key") } } + + //TODO constraint checks on singleSelection and multiSelection + //SingleSelection + if (singleSelection.isNotEmpty()) { + for ((key, value) in singleSelection) { + val selectedValue = value.getValueAsRawString() + val allowedValues = value.values + + //check for duplicated option values? + val duplicates = allowedValues + .groupingBy { it } + .eachCount() + .filter { it.value > 1 } + .keys + + //Constraint: Empty option list + if (allowedValues.isNullOrEmpty()) { + throw Exception("List is Empty") + } + //Constraint: duplicated values + if (duplicates.isNotEmpty()) { + throw Exception("Selection contains duplicate values") + } + + // Constraint: Selection Shouldnt be empty? the first option can be- to be removed + /*if (selectedValue.isNullOrEmpty()) { + throw Exception("Selection for '$selectedValue' is missing.") + }*/ + + // Constraint: Selection must be in the allowed values -> for autocomplete cases when the user can type the option + if (!allowedValues.contains(selectedValue.toString())) { + throw Exception("Invalid selection for '$selectedValue': '$selectedValue' is not among allowed values $allowedValues.") + } + } + } } - override fun isDefined() : Boolean { + override fun isDefined(): Boolean { return userInteractions.isNotEmpty() } @@ -38,19 +93,19 @@ class WebAction( TODO("Not yet implemented") } - fun isApplicableInGivenPage(page: String) : Boolean{ + fun isApplicableInGivenPage(page: String): Boolean { - val document = try{ + val document = try { Jsoup.parse(page) - }catch (e: Exception){ + } catch (e: Exception) { return false } - return userInteractions.all { document.select(it.cssSelector).size > 0 } + return userInteractions.all { document.select(it.cssSelector).size > 0 } } override fun getName(): String { - if(userInteractions.isEmpty()){ + if (userInteractions.isEmpty()) { return "Undefined" } val x = userInteractions.last() @@ -58,25 +113,40 @@ class WebAction( } override fun seeTopGenes(): List { - return children as List + return children as List } override fun copyContent(): StructuralElement { return WebAction( userInteractions.map { it.copy() }.toMutableList(), - textData.entries.associate { it.key to it.value.copy() as StringGene }.toMutableMap() + textData.entries.associate { it.key to it.value.copy() as StringGene }.toMutableMap(), + singleSelection.entries.associate { it.key to it.value.copy() as EnumGene }.toMutableMap(), + multiSelection.entries.associate { it.key to it.value.copy() as ArrayGene }.toMutableMap(), ) } - fun copyValueFrom(other: WebAction){ + /** + * Given another WebAction, copy its entire genotype into this. + */ + fun copyValueFrom(other: WebAction) { + + killAllChildren() + userInteractions.clear() userInteractions.addAll(other.userInteractions) //immutable elements textData.clear() textData.putAll(other.textData.entries.associate { it.key to it.value.copy() as StringGene }) + singleSelection.clear() + singleSelection.putAll(other.singleSelection.entries.associate { it.key to it.value.copy() as EnumGene }) + //TODO multiSelection + + addChildren(textData.values.toList()) + addChildren(singleSelection.values.toList()) + addChildren(multiSelection.values.toList()) } - fun getIdentifier() : String { - return "A:" + userInteractions.joinToString(","){"${it.userActionType}:${it.cssSelector}"} + fun getIdentifier(): String { + return "A:" + userInteractions.joinToString(",") { "${it.userActionType}:${it.cssSelector}" } } } \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/problem/webfrontend/WebIndividual.kt b/core/src/main/kotlin/org/evomaster/core/problem/webfrontend/WebIndividual.kt index 0eddacd0a9..98567909af 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/webfrontend/WebIndividual.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/webfrontend/WebIndividual.kt @@ -11,6 +11,7 @@ import org.evomaster.core.search.Individual import org.evomaster.core.search.StructuralElement import org.evomaster.core.search.gene.Gene +//A test case or test suite (?) class WebIndividual( sampleType: SampleType, children: MutableList, @@ -25,7 +26,7 @@ class WebIndividual( childTypeVerifier = EnterpriseChildTypeVerifier(WebAction::class.java), groups = groups ) { - + // to support genetic operation (?) override fun copyContent(): Individual { return WebIndividual( sampleType, diff --git a/core/src/main/kotlin/org/evomaster/core/problem/webfrontend/WebUserInteraction.kt b/core/src/main/kotlin/org/evomaster/core/problem/webfrontend/WebUserInteraction.kt index 2254755e5a..ba16764f67 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/webfrontend/WebUserInteraction.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/webfrontend/WebUserInteraction.kt @@ -3,5 +3,6 @@ package org.evomaster.core.problem.webfrontend data class WebUserInteraction( val cssSelector : String, - val userActionType : UserActionType + val userActionType : UserActionType, + val inputs : List = listOf() ) \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/problem/webfrontend/service/BrowserController.kt b/core/src/main/kotlin/org/evomaster/core/problem/webfrontend/service/BrowserController.kt index 2e28dcb3f8..9ced1ba3ce 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/webfrontend/service/BrowserController.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/webfrontend/service/BrowserController.kt @@ -51,6 +51,8 @@ class BrowserController { fun cleanBrowser(){ //TODO clean cookies + driver.manage().deleteAllCookies(); + //maybe also clean cache? } fun goToStartingPage(){ @@ -67,7 +69,7 @@ class BrowserController { } fun computePossibleUserInteractions() : List{ - return BrowserActionBuilder.computePossibleUserInteractions(getCurrentPageSource()) + return BrowserActionBuilder.computePossibleUserInteractions(driver) } fun getCurrentPageSource(): String { @@ -78,10 +80,23 @@ class BrowserController { return driver.currentUrl } + fun getDriver(): RemoteWebDriver{ + return driver + } + fun clickAndWaitPageLoad(cssSelector: String){ SeleniumEMUtils.clickAndWaitPageLoad(driver, cssSelector) } + // + fun selectAndWaitPageLoad(cssSelector: String, values: List){ + //TODO + if(values.size == 1) { // if single select + SeleniumEMUtils.selectAndWaitPageLoad(driver, cssSelector, values[0]) + } + //TODO for multi + } + fun goBack(){ driver.navigate().back() SeleniumEMUtils.waitForPageToLoad(driver,2) diff --git a/core/src/main/kotlin/org/evomaster/core/problem/webfrontend/service/WebFitness.kt b/core/src/main/kotlin/org/evomaster/core/problem/webfrontend/service/WebFitness.kt index 35644de7bc..e58b9e9dec 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/webfrontend/service/WebFitness.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/webfrontend/service/WebFitness.kt @@ -10,7 +10,9 @@ import org.evomaster.core.problem.webfrontend.* import org.evomaster.core.search.action.ActionResult import org.evomaster.core.search.EvaluatedIndividual import org.evomaster.core.search.FitnessValue +import org.evomaster.core.search.gene.numeric.IntegerGene import org.evomaster.core.taint.TaintAnalysis +import org.openqa.selenium.WebDriver import org.slf4j.Logger import org.slf4j.LoggerFactory import java.net.MalformedURLException @@ -18,7 +20,11 @@ import java.net.URI import java.net.URISyntaxException import java.net.URL import javax.inject.Inject +import javax.net.ssl.HttpsURLConnection +/* ftiness fuction - given an input text case- tell me how good it is. 0- not covered, 1 - covered. +* +* */ class WebFitness : EnterpriseFitness() { @@ -47,15 +53,22 @@ class WebFitness : EnterpriseFitness() { rc.resetSUT() browserController.cleanBrowser() //TODO these 2 calls could be made in parallel - val actionResults: MutableList = mutableListOf() + val actionResults: MutableList = + mutableListOf() // keep track of results - for each action we execute we need to know the result of the action. ex: the response of an http call - doDbCalls(individual.seeInitializingActions().filterIsInstance(), actionResults = actionResults) + //initialization + doDbCalls( + individual.seeInitializingActions().filterIsInstance(), + actionResults = actionResults + )// to think about it later, next year - it is setting up the environment + //data structure representing the fitness of the individual val fv = FitnessValue(individual.size().toDouble()) val actions = individual.seeMainExecutableActions() browserController.goToStartingPage() + //read what this functions does - checks if URLs are malformed checkHtmlGlobalOracle(browserController.getCurrentPageSource(), browserController.getCurrentUrl(), fv) //if starting page is invalid, not much we can do at all... //TODO maybe should have explicit check at the beginning of the search @@ -65,7 +78,7 @@ class WebFitness : EnterpriseFitness() { val a = actions[i] - registerNewAction(a, i) + registerNewAction(a, i) // needed to tell the driver about the action to be taken val ok = handleWebAction(a, actionResults, fv) actionResults.filterIsInstance()[i].stopping = !ok @@ -81,7 +94,7 @@ class WebFitness : EnterpriseFitness() { handleExtra(dto, fv) val webResults = actionResults.filterIsInstance() - handleResponseTargets(fv, actions, webResults, dto.additionalInfoList) + handleResponseTargets(fv, actions, webResults, dto.additionalInfoList)// handles black box interaction if (config.isEnabledTaintAnalysis()) { @@ -99,40 +112,45 @@ class WebFitness : EnterpriseFitness() { ) } - private fun handleWebAction(a: WebAction, actionResults: MutableList, fv: FitnessValue): Boolean { + //the actual execution + private fun handleWebAction(wa: WebAction, actionResults: MutableList, fv: FitnessValue): Boolean { //TODO should check if current "page" is not html, eg an image val pageBeforeExecutingAction = browserController.getCurrentPageSource() val urlBeforeExecutingAction = browserController.getCurrentUrl() - val possibilities = BrowserActionBuilder.createPossibleActions(pageBeforeExecutingAction) + val possibilities = BrowserActionBuilder.createPossibleActions(browserController.getDriver()) + + if (isNotHtmlPage(browserController.getDriver(), pageBeforeExecutingAction, urlBeforeExecutingAction)) { + log.error("Not an HTML page"); // to be amended + } var blocking = false - if(!a.isDefined() || - !a.isApplicableInGivenPage(pageBeforeExecutingAction)){ + if (!wa.isDefined() || !wa.isApplicableInGivenPage(pageBeforeExecutingAction)) { //not applicable might happen if mutation in previous action led to different page //TODO possibly add "back" and "refresh" actions, but with a probability //TODO if page is invalid, should always return with "back" - if(possibilities.isEmpty()){ + if (possibilities.isEmpty()) { blocking = true } else { //TODO check archive for missing targets when choosing possibilities val chosen = randomness.choose(possibilities) assert(chosen.isDefined()) - a.copyValueFrom(chosen) + chosen.doInitialize(randomness) + wa.copyValueFrom(chosen) } } - assert(blocking || (a.isDefined() && a.isApplicableInGivenPage(pageBeforeExecutingAction))) + assert(blocking || (wa.isDefined() && wa.isApplicableInGivenPage(pageBeforeExecutingAction))) - if(!blocking) { - val inputs = a.userInteractions.filter { it.userActionType == UserActionType.FILL_TEXT } + if (!blocking) { + val inputs = wa.userInteractions.filter { it.userActionType == UserActionType.FILL_TEXT } //TODO first fill all inputs - val interactions = a.userInteractions.filter { it.userActionType != UserActionType.FILL_TEXT } + val interactions = wa.userInteractions.filter { it.userActionType != UserActionType.FILL_TEXT } interactions.forEach { when (it.userActionType) { @@ -141,6 +159,33 @@ class WebFitness : EnterpriseFitness() { browserController.clickAndWaitPageLoad(it.cssSelector) //TODO better wait } + + UserActionType.SELECT_SINGLE ->{ + for(select in wa.singleSelection){ + val css = select.key + val valueAttributeOrText = select.value.getValueAsRawString() + browserController.selectAndWaitPageLoad(css, listOf(valueAttributeOrText)) + } + } + UserActionType.SELECT_MULTI -> { + //TODO +// /* +// not just clicking, but deciding which options to select. +// this is based on values in the genes +// */ +// wa.seeTopGenes().size // size 0 ? No genes in webaction +// +// +// // val selectedValues = listOf("") // select options coming from genes +// +// // from webaction, check if it is a single selector or multi, +// // then extract the gene, then from the browser controller extract the value +// +// +// browserController.selectAndWaitPageLoad(it.cssSelector, selectedValues) +// + } + else -> { log.error("Not handled action type ${it.userActionType}") } @@ -149,22 +194,22 @@ class WebFitness : EnterpriseFitness() { } - val result = WebResult(a.getLocalId(), blocking) + val result = WebResult(wa.getLocalId(), blocking) val start = pageIdentifier.registerShape(HtmlUtils.computeIdentifyingShape(pageBeforeExecutingAction)) result.setIdentifyingPageIdStart(start) result.setUrlPageStart(urlBeforeExecutingAction) result.setPossibleActionIds(possibilities.map { it.getIdentifier() }) - if(!blocking) { + if (!blocking) { //TODO all needed info val endPageSource = browserController.getCurrentPageSource() - val end = pageIdentifier.registerShape(HtmlUtils.computeIdentifyingShape(endPageSource)) + val end = pageIdentifier.registerShape(HtmlUtils.computeIdentifyingShape(endPageSource)) result.setIdentifyingPageIdEnd(end) val endUrl = browserController.getCurrentUrl() result.setUrlPageEnd(endUrl) - if(start != end){ + if (start != end) { // navigation occurred val valid = checkHtmlGlobalOracle(endPageSource, endUrl, fv) result.setValidHtml(valid) } @@ -174,7 +219,7 @@ class WebFitness : EnterpriseFitness() { return !blocking } - private fun checkHtmlGlobalOracle(html: String, urlOfHtmlPage: String, fv: FitnessValue) : Boolean{ + private fun checkHtmlGlobalOracle(html: String, urlOfHtmlPage: String, fv: FitnessValue): Boolean { var issues = false @@ -194,9 +239,9 @@ class WebFitness : EnterpriseFitness() { */ HtmlUtils.getUrlInALinks(html).forEach { - try{ + try { URI(it) - } catch (e: URISyntaxException){ + } catch (e: URISyntaxException) { //FIXME URI should not be used in Java, as implementing deprecated specs webGlobalState.addMalformedUri(it, urlOfHtmlPage) issues = true @@ -209,26 +254,26 @@ class WebFitness : EnterpriseFitness() { } //external links should be valid URL - val url = try{ + val url = try { URL(it) - } catch (e: MalformedURLException){ + } catch (e: MalformedURLException) { return@forEach } val external = !url.host.isNullOrBlank() - if(external){ - if(! webGlobalState.hasAlreadySeenExternalLink(url)){ + if (external) { + if (!webGlobalState.hasAlreadySeenExternalLink(url)) { val found = HtmlUtils.checkLink(url) - if(!found){ + if (!found) { issues = true val id = idMapper.handleLocalTarget(idMapper.getFaultDescriptiveId(ExperimentalFaultCategory.WEB_BROKEN_LINK,it)) fv.updateTarget(id, 1.0) } webGlobalState.addExternalLink(url, found, urlOfHtmlPage) - } else { + } else { webGlobalState.updateExternalLink(url, urlOfHtmlPage) - if(webGlobalState.isBrokenLink(url)){ + if (webGlobalState.isBrokenLink(url)) { issues = true } } @@ -249,11 +294,11 @@ class WebFitness : EnterpriseFitness() { fv.updateTarget(idMapper.handleLocalTarget("WEB_HOME_PAGE"), 1.0) - for(i in actions.indices){ + for (i in actions.indices) { val a = actions[i] val r = actionResults[i] - if(r.stopping){ + if (r.stopping) { return } @@ -267,7 +312,19 @@ class WebFitness : EnterpriseFitness() { val executedActionId = a.getIdentifier() r.getPossibleActionIds().forEach { - val actionInPageId = idMapper.handleLocalTarget("WEB_ACTION:${r.getIdentifyingPageIdStart()}@$it") + val prefix = "WEB_ACTION:${r.getIdentifyingPageIdStart()}@$it" + val actionInPageId = idMapper.handleLocalTarget( + if(a.singleSelection.isEmpty()){ + prefix + } else { + prefix + a.singleSelection.values.joinToString(",") { g -> g.getValueAsRawString() } + } + ) + /* + on a page, there could be several interesting actions to do... we don't want to lose info on them. + we give as such a non-zero score. + of course, for action we actually made, it is covered (ie score 1) + */ val h = if(it == executedActionId) 1.0 else 0.5 fv.updateTarget(actionInPageId, h, i) } @@ -280,4 +337,34 @@ class WebFitness : EnterpriseFitness() { } } -} \ No newline at end of file + + private fun isNotHtmlPage(driver: WebDriver, pageSource: String, pageURL: String): Boolean { + return when { + //check if it contains html or body tags + !pageSource.contains(" true + //check headers + else -> { + try { + val url = URL(pageURL) + val connection = url.openConnection() as HttpsURLConnection + connection.requestMethod = "HEAD" + connection.connect() + val contentType = connection.contentType?.lowercase() ?: "" + connection.disconnect() + + contentType.isNotEmpty() && contentType.startsWith("application/") && !contentType.contains("html") //to be revised + } catch (e: Exception) { + false + } + } + } + } + +} +/* Common content-type values +HTML - text/html +JPEG - image/jpeg +PNG - image/png +PDF - application/pdf +JSON - application/json +* */ \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/problem/webfrontend/service/WebPageIdentifier.kt b/core/src/main/kotlin/org/evomaster/core/problem/webfrontend/service/WebPageIdentifier.kt index ccf9089642..b650073ef3 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/webfrontend/service/WebPageIdentifier.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/webfrontend/service/WebPageIdentifier.kt @@ -18,14 +18,16 @@ class WebPageIdentifier { private var counter = 0 - fun registerShape(shape: String) : String{ + fun registerShape(html: String) : String{ + val shape = html + //normalizeHtml(html); -> if needed, should go to HtmlUtils.computeIdentifyingShape var id = shapeToId[shape] if(id != null){ //already registered return id } - + // shape is new -> generate id and update both maps id = "P$counter" counter++ shapeToId[shape] = id @@ -33,4 +35,11 @@ class WebPageIdentifier { return id } + + // normalize html -> remove white spaces and dynamic ids + private fun normalizeHtml(html: String): String { + return html + .replace(Regex("\\s+"), " ") // Collapse whitespace + .replace(Regex("id=\"[^\"]+\""), "id=\"*\"") // Remove dynamic IDs + } } \ No newline at end of file diff --git a/core/src/main/kotlin/org/evomaster/core/problem/webfrontend/service/WebSampler.kt b/core/src/main/kotlin/org/evomaster/core/problem/webfrontend/service/WebSampler.kt index 27137ac709..47117174a7 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/webfrontend/service/WebSampler.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/webfrontend/service/WebSampler.kt @@ -14,6 +14,11 @@ import org.slf4j.LoggerFactory import javax.annotation.PostConstruct import javax.inject.Inject +//gives a random test case - +//problem - the actions we are sampling are undefined -> +//different from rest or graphql where there is a schema that defines all possible cases and interactions +//in front-end you would have to visit a page to know what u can do there. +//exploring pages and different inputs class WebSampler : EnterpriseSampler() { companion object { @@ -74,12 +79,13 @@ class WebSampler : EnterpriseSampler() { } + //Create a random test case override fun sampleAtRandom(): WebIndividual { val actions = mutableListOf>() - val n = randomness.nextInt(1, getMaxTestSizeDuringSampler()) - + val n = randomness.nextInt(1, getMaxTestSizeDuringSampler()) // random test length ? + //sample n random actions (0 until n).forEach { - val a = sampleUndefinedAction() + val a = sampleUndefinedAction()// when we sample it will be a list of undefined actions actions.add(EnterpriseActionGroup(mutableListOf(a), WebAction::class.java)) } val ind = WebIndividual(SampleType.RANDOM, actions) diff --git a/e2e-tests/spring-web/pom.xml b/e2e-tests/spring-web/pom.xml index 37f15e5561..ee562d7a87 100644 --- a/e2e-tests/spring-web/pom.xml +++ b/e2e-tests/spring-web/pom.xml @@ -24,6 +24,13 @@ + + javax.servlet + javax.servlet-api + 4.0.1 + provided + + org.evomaster evomaster-e2e-tests-utils diff --git a/e2e-tests/spring-web/src/main/java/com/foo/web/examples/spring/dropdown/DropDownController.java b/e2e-tests/spring-web/src/main/java/com/foo/web/examples/spring/dropdown/DropDownController.java index 6c3090f46a..3d748a2f70 100644 --- a/e2e-tests/spring-web/src/main/java/com/foo/web/examples/spring/dropdown/DropDownController.java +++ b/e2e-tests/spring-web/src/main/java/com/foo/web/examples/spring/dropdown/DropDownController.java @@ -11,6 +11,8 @@ public class DropDownController { @GetMapping("/") public String index0() { + System.out.println("Servlet API Version: " + + javax.servlet.ServletRequest.class.getPackage().getSpecificationVersion()); return "/dropdown/index.html"; } diff --git a/e2e-tests/spring-web/src/main/java/com/foo/web/examples/spring/dropdown/DropDownWebApplication.java b/e2e-tests/spring-web/src/main/java/com/foo/web/examples/spring/dropdown/DropDownWebApplication.java index 310f5e638f..dde33a761e 100644 --- a/e2e-tests/spring-web/src/main/java/com/foo/web/examples/spring/dropdown/DropDownWebApplication.java +++ b/e2e-tests/spring-web/src/main/java/com/foo/web/examples/spring/dropdown/DropDownWebApplication.java @@ -3,7 +3,6 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; -import org.springframework.web.bind.annotation.GetMapping; @SpringBootApplication(exclude = SecurityAutoConfiguration.class) public class DropDownWebApplication { diff --git a/e2e-tests/spring-web/src/test/java/com/foo/web/examples/spring/dropdownselector/DropDownController.java b/e2e-tests/spring-web/src/test/java/com/foo/web/examples/spring/dropdownselector/DropDownController.java new file mode 100644 index 0000000000..d1d9f72d55 --- /dev/null +++ b/e2e-tests/spring-web/src/test/java/com/foo/web/examples/spring/dropdownselector/DropDownController.java @@ -0,0 +1,11 @@ +package com.foo.web.examples.spring.dropdownselector; + +import com.foo.web.examples.spring.SpringController; +import com.foo.web.examples.spring.base.BaseWebApplication; + +public class DropDownController extends SpringController { + public DropDownController() { + super(BaseWebApplication.class, "/dropdown/index.html"); + } + +} diff --git a/e2e-tests/spring-web/src/test/java/org/evomaster/e2etests/spring/web/dropdownselector/DropDownEMTest.java b/e2e-tests/spring-web/src/test/java/org/evomaster/e2etests/spring/web/dropdownselector/DropDownEMTest.java new file mode 100644 index 0000000000..84ad430df8 --- /dev/null +++ b/e2e-tests/spring-web/src/test/java/org/evomaster/e2etests/spring/web/dropdownselector/DropDownEMTest.java @@ -0,0 +1,43 @@ +package org.evomaster.e2etests.spring.web.dropdownselector; + +import com.foo.web.examples.spring.dropdownselector.DropDownController; +import org.evomaster.core.problem.webfrontend.WebIndividual; +import org.evomaster.core.search.Solution; +import org.evomaster.e2etests.spring.web.SpringTestBase; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class DropDownEMTest extends SpringTestBase { + @BeforeAll + public static void initClass() throws Exception { + SpringTestBase.initClass(new DropDownController()); + } + + //@Disabled + @Test + public void testRunEM() throws Throwable { + + runTestHandlingFlakyAndCompilation( + "DropDownEM", + "org.DropDownEM", + 50, + (args) -> { + + //TODO need to ask Man + setOption(args, "adaptiveGeneSelectionMethod", "NONE"); + + Solution solution = initAndRun(args); + + assertTrue(solution.getIndividuals().size() > 0); + +// assertHasVisitedUrlPath(solution, "/dropdown/index.html", "/dropdown/page1.html", "/dropdown/page2.html", "/dropdown/page3.html"); + assertHasVisitedUrlPath(solution, "/dropdown/index.html", "/navigate/1", "/navigate/2", "/navigate/3"); + assertNoHtmlErrors(solution); // statement ok - gives no errors + } + ); + } + +}