From 61b1f8509fcb57bde78776d601103cb9e2a2f8a2 Mon Sep 17 00:00:00 2001
From: srosse <none@none>
Date: Fri, 27 Oct 2017 20:26:13 +0200
Subject: [PATCH] OO-2969	: selenium which use complex conditional
 feedbacks for QTI 2.1 question

---
 .../ui/editor/FeedbacksEditorController.java  |   8 +-
 .../editor/_content/feedback_condition.html   |  33 +++---
 .../_content/feedback_condition_list.html     |   4 +
 .../java/org/olat/selenium/ImsQTI21Test.java  | 102 ++++++++++++++++++
 .../page/qti/QTI21FeedbacksEditorPage.java    |  31 ++++--
 .../page/qti/QTI21HotspotEditorPage.java      |  19 ++++
 .../org/olat/selenium/page/qti/QTI21Page.java |   6 ++
 7 files changed, 176 insertions(+), 27 deletions(-)
 create mode 100644 src/main/java/org/olat/ims/qti21/ui/editor/_content/feedback_condition_list.html

diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/FeedbacksEditorController.java b/src/main/java/org/olat/ims/qti21/ui/editor/FeedbacksEditorController.java
index a29b198d97a..e7f21d2b10c 100644
--- a/src/main/java/org/olat/ims/qti21/ui/editor/FeedbacksEditorController.java
+++ b/src/main/java/org/olat/ims/qti21/ui/editor/FeedbacksEditorController.java
@@ -388,7 +388,9 @@ public class FeedbacksEditorController extends FormBasicController implements Sy
 			titleEl.setEnabled(!restrictedEdit);
 			titleEl.setElementCssClass("o_sel_assessment_item_" + feedbackType.name() + "_feedback_title");
 			
-			conditionListContainer = FormLayoutContainer.createBareBoneFormLayout("cond_list_".concat(id), getTranslator());
+			String conditionListPage = velocity_root + "/feedback_condition_list.html";
+			conditionListContainer = FormLayoutContainer.createCustomFormLayout("cond_list_".concat(id),
+					getTranslator(), conditionListPage);
 			formLayout.add(conditionListContainer);
 			conditionListContainer.setRootForm(mainForm);
 			conditionListContainer.contextPut("conditions", conditions);
@@ -547,6 +549,10 @@ public class FeedbacksEditorController extends FormBasicController implements Sy
 				this.condition = condition;
 			}
 			
+			public FormLayoutContainer getRuleContainer() {
+				return ruleContainer;
+			}
+			
 			public void initForm(FormItemContainer feedbackFormLayout) {
 				String id = Integer.toString(counter.incrementAndGet());
 
diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/_content/feedback_condition.html b/src/main/java/org/olat/ims/qti21/ui/editor/_content/feedback_condition.html
index 4713052f87c..fd6c0917206 100644
--- a/src/main/java/org/olat/ims/qti21/ui/editor/_content/feedback_condition.html
+++ b/src/main/java/org/olat/ims/qti21/ui/editor/_content/feedback_condition.html
@@ -1,24 +1,21 @@
-<div class='form-inline o_condition'>
-	$r.render("var_$id")
-	$r.render("ope_$id")
-	#if($r.visible("txt_val_$id"))
-		$r.render("txt_val_$id")
-	#end
-	#if($r.visible("ans_$id"))
-		$r.render("ans_$id")
-	#end
-	#if($r.available("add_$id"))
-		$r.render("add_$id")
-	#end
-	#if($r.available("del_$id"))
-		$r.render("del_$id")
-	#end
-	
+$r.render("var_$id")
+$r.render("ope_$id")
+#if($r.visible("txt_val_$id"))
+	$r.render("txt_val_$id")
+#end
+#if($r.visible("ans_$id"))
+	$r.render("ans_$id")
+#end
+#if($r.available("add_$id"))
+	$r.render("add_$id")
+#end
+#if($r.available("del_$id"))
+	$r.render("del_$id")
+#end
+
 #if($f.hasError("var_$id"))
 	<br/>$r.render("var_${id}_ERROR")
 #end
 #if($f.hasError("txt_val_$id"))
 	<br/>$r.render("txt_val_${id}_ERROR")
 #end
-</div>
-
diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/_content/feedback_condition_list.html b/src/main/java/org/olat/ims/qti21/ui/editor/_content/feedback_condition_list.html
new file mode 100644
index 00000000000..d6e9b1f7b5c
--- /dev/null
+++ b/src/main/java/org/olat/ims/qti21/ui/editor/_content/feedback_condition_list.html
@@ -0,0 +1,4 @@
+#foreach($condition in $conditions)
+<div class='form-inline o_condition o_condition_${foreach.count}'>$r.render($condition.ruleContainer)</div>
+#end
+
diff --git a/src/test/java/org/olat/selenium/ImsQTI21Test.java b/src/test/java/org/olat/selenium/ImsQTI21Test.java
index 4aca73268ac..ab302ad0d46 100644
--- a/src/test/java/org/olat/selenium/ImsQTI21Test.java
+++ b/src/test/java/org/olat/selenium/ImsQTI21Test.java
@@ -1326,6 +1326,108 @@ public class ImsQTI21Test {
 			.assertOnAssessmentResults()
 			.assertOnAssessmentTestScore(6);// 3 points from the first question, 3 from the second
 	}
+	
+	/**
+	 * Test the conditional feedback with 3 conditions based
+	 * on attempts (and an incorrect feedback used as marker),
+	 * on score and on response. It's done with a multiple
+	 * choice with score per answer and a negative min. score.
+	 * 
+	 * @param authorLoginPage
+	 * @throws IOException
+	 * @throws URISyntaxException
+	 */
+	@Test
+	@RunAsClient
+	public void qti21EditorMultipleChoices_complexConditionalFeedback(@InitialPage LoginPage authorLoginPage)
+	throws IOException, URISyntaxException {
+		UserVO author = new UserRestClient(deploymentUrl).createAuthor();
+		authorLoginPage.loginAs(author.getLogin(), author.getPassword());
+		
+		String qtiTestTitle = "Choices QTI 2.1 " + UUID.randomUUID();
+		navBar
+			.openAuthoringEnvironment()
+			.createQTI21Test(qtiTestTitle)
+			.clickToolbarBack();
+		
+		QTI21Page qtiPage = QTI21Page
+				.getQTI12Page(browser);
+		QTI21EditorPage qtiEditor = qtiPage
+				.edit();
+		qtiEditor
+			.selectNode("Single choice")
+			.deleteNode();
+	
+		//add a single choice: all answers score
+		QTI21MultipleChoiceEditorPage mcEditor = qtiEditor
+			.addMultipleChoice();
+		mcEditor
+			.setAnswer(0, "Ok")
+			.setCorrect(0)
+			.addChoice(1)
+			.setCorrect(1)
+			.setAnswer(1, "Correct")
+			.addChoice(2)
+			.setAnswer(2, "Faux")
+			.addChoice(3)
+			.setAnswer(3, "Falsch")
+			.save();
+		
+		//add negative scores to play with
+		mcEditor
+			.selectScores()
+			.selectAssessmentMode(ScoreEvaluation.perAnswer)
+			.setScore("Ok", "3")
+			.setScore("Correct", "1")
+			.setScore("Faux", "-1")
+			.setScore("Falsch", "-1")
+			.setMaxScore("4")
+			.save();
+
+		// set a conditional feedback
+		mcEditor
+			.selectFeedbacks()
+			.setIncorrectFeedback("Incorrect", "Not the right response")
+			// attempts = 1 && score < 0
+			.addConditionalFeedback(1, "NegativeFirstAttempts", "Negative score")
+			.setCondition(1, 1, Variable.attempts, Operator.equals, "1")
+			.addCondition(1, 1)
+			.setCondition(1, 2, Variable.score, Operator.smaller, "0")
+			// response = 'Faux'
+			.addConditionalFeedback(2, "FauxAnswer", "You choose the 'Faux' answer")
+			.setCondition(2, 1, Variable.response, Operator.equals, "Faux")
+			// 0 < score < 3 
+			.addConditionalFeedback(3, "Positive", "Score between 0 and 3")
+			.setCondition(3, 1, Variable.score, Operator.biggerEquals, "0")
+			.addCondition(3, 1)
+			.setCondition(3, 2, Variable.score, Operator.smaller, "3")
+			.save();
+		
+		qtiPage
+			.clickToolbarBack()
+			.assertOnAssessmentItem()
+			//1 attempt, score -2.0
+			.answerMultipleChoice("Falsch", "Faux")
+			.saveAnswer()
+			.assertFeedback("Incorrect")
+			.assertFeedback("FauxAnswer")
+			.assertFeedback("NegativeFirstAttempts")
+			.assertNoFeedback("Positive")
+			//2 attempt, score 0.0
+			.deselectAnswerMultipleChoice("Faux", "Falsch")
+			.answerMultipleChoice("Faux", "Correct")
+			.saveAnswer()
+			.assertFeedback("Incorrect")
+			.assertFeedback("FauxAnswer")
+			.assertFeedback("Positive")
+			.assertNoFeedback("NegativeFirstAttempts")
+			//3 attempt
+			.deselectAnswerMultipleChoice("Faux")
+			.answerMultipleChoice("Ok")
+			.saveAnswer()
+			.assertNoFeedback()
+			.endTest();
+	}
 
 	/**
 	 * An author make a test with 2 kprims.<br>
diff --git a/src/test/java/org/olat/selenium/page/qti/QTI21FeedbacksEditorPage.java b/src/test/java/org/olat/selenium/page/qti/QTI21FeedbacksEditorPage.java
index 5792531c157..66e3a5fcc0a 100644
--- a/src/test/java/org/olat/selenium/page/qti/QTI21FeedbacksEditorPage.java
+++ b/src/test/java/org/olat/selenium/page/qti/QTI21FeedbacksEditorPage.java
@@ -126,29 +126,44 @@ public class QTI21FeedbacksEditorPage {
 	
 	public QTI21FeedbacksEditorPage setCondition(int feedbackPosition, int conditionPosition,
 			ModalFeedbackCondition.Variable variable, ModalFeedbackCondition.Operator operator, String value) {
+
+		String conditionPrefix = "//fieldset[contains(@class,'o_sel_assessment_item_additional_" + feedbackPosition + "')]"
+				+ "//div[contains(@class,'o_condition_" + conditionPosition + "')]";
 		
-		String feedbackPrefix = "//fieldset[contains(@class,'o_sel_assessment_item_additional_" + feedbackPosition + "')]";
-		String conditionPrefix = "//div[contains(@class,'o_condition')][" + conditionPosition + "]";
-		
-		By conditionBy = By.xpath(feedbackPrefix + conditionPrefix);
+		By conditionBy = By.xpath(conditionPrefix);
 		OOGraphene.waitElement(conditionBy, browser);
 		
-		By variableBy = By.xpath(feedbackPrefix + conditionPrefix + "//select[contains(@id,'o_fiovar_')]");
+		By variableBy = By.xpath(conditionPrefix + "//select[contains(@id,'o_fiovar_')]");
 		WebElement variableEl = browser.findElement(variableBy);
 		new Select(variableEl).selectByValue(variable.name());
 		OOGraphene.waitBusy(browser);
 		
-		By operatorBy = By.xpath(feedbackPrefix + conditionPrefix + "//select[contains(@id,'o_fioope_')]");
+		By operatorBy = By.xpath(conditionPrefix + "//select[contains(@id,'o_fioope_')]");
 		WebElement operatorEl = browser.findElement(operatorBy);
 		new Select(operatorEl).selectByValue(operator.name());
 		
 		if(variable == Variable.attempts || variable == Variable.score) {
-			By valueBy = By.xpath(feedbackPrefix + conditionPrefix + "//input[@type='text']");
+			By valueBy = By.xpath(conditionPrefix + "//input[@type='text']");
 			WebElement valueEl = browser.findElement(valueBy);
 			valueEl.clear();
 			valueEl.sendKeys(value);
+		} else if(variable == Variable.response) {
+			By answerBy = By.xpath(conditionPrefix + "//select[contains(@id,'o_fioans_')]");
+			WebElement answerEl = browser.findElement(answerBy);
+			new Select(answerEl).selectByVisibleText(value);
 		}
-		
+		return this;
+	}
+	
+	public QTI21FeedbacksEditorPage addCondition(int feedbackPosition, int conditionPosition) {
+		String conditionXpath = "//fieldset[contains(@class,'o_sel_assessment_item_additional_" + feedbackPosition + "')]"
+				+ "//div[contains(@class,'o_condition_" + conditionPosition + "')]"
+				+ "//a[contains(@class,'btn-default')][i[contains(@class,'o_icon_add')]]";
+		
+		By addConditionBy = By.xpath(conditionXpath);
+		OOGraphene.waitElement(addConditionBy, browser);
+		browser.findElement(addConditionBy).click();
+		OOGraphene.waitBusy(browser);
 		return this;
 	}
 	
diff --git a/src/test/java/org/olat/selenium/page/qti/QTI21HotspotEditorPage.java b/src/test/java/org/olat/selenium/page/qti/QTI21HotspotEditorPage.java
index 67bf32b8e9b..faed98382a9 100644
--- a/src/test/java/org/olat/selenium/page/qti/QTI21HotspotEditorPage.java
+++ b/src/test/java/org/olat/selenium/page/qti/QTI21HotspotEditorPage.java
@@ -1,3 +1,22 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
 package org.olat.selenium.page.qti;
 
 import java.io.File;
diff --git a/src/test/java/org/olat/selenium/page/qti/QTI21Page.java b/src/test/java/org/olat/selenium/page/qti/QTI21Page.java
index f29dcc31449..e4636d51693 100644
--- a/src/test/java/org/olat/selenium/page/qti/QTI21Page.java
+++ b/src/test/java/org/olat/selenium/page/qti/QTI21Page.java
@@ -285,6 +285,12 @@ public class QTI21Page {
 		return this;
 	}
 	
+	public QTI21Page assertNoFeedback(String title) {
+		By feedbackBy = By.xpath("//div[contains(@class,'modalFeedback')]/h4[contains(text(),'" + title + "')]");
+		OOGraphene.waitElementDisappears(feedbackBy, 5, browser);
+		return this;
+	}
+	
 	/**
 	 * Check that there are no feedbacks visible.
 	 * 
-- 
GitLab