From f079b7f1acea4b60a426ffafc50fd8703ff7a50a Mon Sep 17 00:00:00 2001
From: srosse <none@none>
Date: Tue, 24 Oct 2017 19:49:00 +0200
Subject: [PATCH] OO-2926: selenium test for the editor of hotspot questions

---
 .../HotspotChoiceScoreController.java         |   2 +
 .../interactions/HotspotEditorController.java |   3 +
 .../_content/hotspot_choices_score.html       |   2 +-
 .../java/org/olat/selenium/ImsQTI21Test.java  | 350 ++++++++++++++++++
 .../selenium/page/qti/QTI21EditorPage.java    |   5 +
 .../page/qti/QTI21HotspotEditorPage.java      | 135 +++++++
 .../page/qti/QTI21HotspotScoreEditorPage.java |  79 ++++
 7 files changed, 575 insertions(+), 1 deletion(-)
 create mode 100644 src/test/java/org/olat/selenium/page/qti/QTI21HotspotEditorPage.java
 create mode 100644 src/test/java/org/olat/selenium/page/qti/QTI21HotspotScoreEditorPage.java

diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/interactions/HotspotChoiceScoreController.java b/src/main/java/org/olat/ims/qti21/ui/editor/interactions/HotspotChoiceScoreController.java
index e44715f8ef8..a152af2d6bf 100644
--- a/src/main/java/org/olat/ims/qti21/ui/editor/interactions/HotspotChoiceScoreController.java
+++ b/src/main/java/org/olat/ims/qti21/ui/editor/interactions/HotspotChoiceScoreController.java
@@ -108,11 +108,13 @@ public class HotspotChoiceScoreController extends AssessmentItemRefEditorControl
 		ScoreBuilder minScore = itemBuilder.getMinScoreBuilder();
 		String minValue = minScore == null ? "" : (minScore.getScore() == null ? "" : minScore.getScore().toString());
 		minScoreEl = uifactory.addTextElement("min.score", "min.score", 8, minValue, formLayout);
+		minScoreEl.setElementCssClass("o_sel_assessment_item_min_score");
 		minScoreEl.setEnabled(!restrictedEdit);
 		
 		ScoreBuilder maxScore = itemBuilder.getMaxScoreBuilder();
 		String maxValue = maxScore == null ? "" : (maxScore.getScore() == null ? "" : maxScore.getScore().toString());
 		maxScoreEl = uifactory.addTextElement("max.score", "max.score", 8, maxValue, formLayout);
+		maxScoreEl.setElementCssClass("o_sel_assessment_item_max_score");
 		maxScoreEl.setEnabled(!restrictedEdit);
 		
 		String[] modeValues = new String[]{
diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/interactions/HotspotEditorController.java b/src/main/java/org/olat/ims/qti21/ui/editor/interactions/HotspotEditorController.java
index 30fe3a191e4..ef525fcf76b 100644
--- a/src/main/java/org/olat/ims/qti21/ui/editor/interactions/HotspotEditorController.java
+++ b/src/main/java/org/olat/ims/qti21/ui/editor/interactions/HotspotEditorController.java
@@ -152,6 +152,7 @@ public class HotspotEditorController extends FormBasicController {
 		String[] cardinalityKeys = new String[] { Cardinality.SINGLE.name(), Cardinality.MULTIPLE.name() };
 		String[] cardinalityValues = new String[] { translate(Cardinality.SINGLE.name()), translate(Cardinality.MULTIPLE.name()) };
 		cardinalityEl = uifactory.addRadiosHorizontal("form.imd.cardinality", formLayout, cardinalityKeys, cardinalityValues);
+		cardinalityEl.setElementCssClass("o_sel_assessment_item_cardinality");
 		if(itemBuilder.isSingleChoice()) {
 			cardinalityEl.select(cardinalityKeys[0], true);
 		} else {
@@ -206,6 +207,7 @@ public class HotspotEditorController extends FormBasicController {
 
 		String[] emptyKeys = new String[0];
 		correctHotspotsEl = uifactory.addCheckboxesHorizontal("form.imd.correct.spots", formLayout, emptyKeys, emptyKeys);
+		correctHotspotsEl.setElementCssClass("o_sel_assessment_item_correct_spots");
 		correctHotspotsEl.setEnabled(!restrictedEdit);
 		correctHotspotsEl.addActionListener(FormEvent.ONCHANGE);
 		rebuildWrappersAndCorrectSelection();
@@ -237,6 +239,7 @@ public class HotspotEditorController extends FormBasicController {
 
 		// Submit Button
 		FormLayoutContainer buttonsContainer = FormLayoutContainer.createButtonLayout("buttons", getTranslator());
+		buttonsContainer.setElementCssClass("o_sel_hotspots_save");
 		buttonsContainer.setRootForm(mainForm);
 		formLayout.add(buttonsContainer);
 		uifactory.addFormSubmitButton("submit", buttonsContainer);
diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/interactions/_content/hotspot_choices_score.html b/src/main/java/org/olat/ims/qti21/ui/editor/interactions/_content/hotspot_choices_score.html
index 4b017f19f23..92166d3ceb2 100644
--- a/src/main/java/org/olat/ims/qti21/ui/editor/interactions/_content/hotspot_choices_score.html
+++ b/src/main/java/org/olat/ims/qti21/ui/editor/interactions/_content/hotspot_choices_score.html
@@ -1,4 +1,4 @@
-<table class="table">
+<table class="table o_sel_hotspots_scores">
 	<thead><tr>
 		<th>&nbsp;</th>
 		<th>$r.translate("form.score.answer.correct")</th>
diff --git a/src/test/java/org/olat/selenium/ImsQTI21Test.java b/src/test/java/org/olat/selenium/ImsQTI21Test.java
index 5848685ecda..8aa7b1f2e51 100644
--- a/src/test/java/org/olat/selenium/ImsQTI21Test.java
+++ b/src/test/java/org/olat/selenium/ImsQTI21Test.java
@@ -45,6 +45,7 @@ import org.olat.selenium.page.course.CourseEditorPageFragment;
 import org.olat.selenium.page.course.CoursePageFragment;
 import org.olat.selenium.page.qti.QTI21ConfigurationCEPage;
 import org.olat.selenium.page.qti.QTI21EditorPage;
+import org.olat.selenium.page.qti.QTI21HotspotEditorPage;
 import org.olat.selenium.page.qti.QTI21KprimEditorPage;
 import org.olat.selenium.page.qti.QTI21LobEditorPage;
 import org.olat.selenium.page.qti.QTI21MatchEditorPage;
@@ -61,6 +62,8 @@ import org.openqa.selenium.By;
 import org.openqa.selenium.WebDriver;
 import org.openqa.selenium.WebElement;
 
+import uk.ac.ed.ph.jqtiplus.value.Cardinality;
+
 /**
  * 
  * Initial date: 03.05.2016<br>
@@ -1419,6 +1422,353 @@ public class ImsQTI21Test {
 			.assertOnAssessmentTestScore(6);// 3 points from the first question, 3 from the second
 	}
 	
+	
+	/**
+	 * An author make a test with 2 hotspots with the single choice cardinality,
+	 * the first with the score set if all answers are correct, the second
+	 * with scoring per answers.<br>
+	 * A first user make the test, but doesn't answer all questions
+	 * correctly, log out and a second user make the perfect test.
+	 * 
+	 * @param authorLoginPage
+	 * @param participantBrowser
+	 * @throws IOException
+	 * @throws URISyntaxException
+	 */
+	@Test
+	@RunAsClient
+	public void qti21EditorHotspot_singleChoice(@InitialPage LoginPage authorLoginPage,
+			@Drone @User WebDriver participantBrowser)
+	throws IOException, URISyntaxException {
+		UserVO author = new UserRestClient(deploymentUrl).createAuthor();
+		authorLoginPage.loginAs(author.getLogin(), author.getPassword());
+		UserVO ryomou = new UserRestClient(deploymentUrl).createRandomUser("Ryomou");
+		UserVO rei = new UserRestClient(deploymentUrl).createRandomUser("Rei");
+		
+		String qtiTestTitle = "Hotspot QTI 2.1 " + UUID.randomUUID();
+		navBar
+			.openAuthoringEnvironment()
+			.createQTI21Test(qtiTestTitle)
+			.clickToolbarBack();
+		
+		QTI21Page qtiPage = QTI21Page
+				.getQTI12Page(browser);
+		QTI21EditorPage qtiEditor = qtiPage
+				.edit();
+		//start a blank test
+		qtiEditor
+			.selectNode("Single choice")
+			.deleteNode();
+		
+		//add an hotspot: all answers score
+		QTI21HotspotEditorPage hotspotEditor = qtiEditor
+			.addHotspot();
+		// 2 spots
+		URL backgroundImageUrl = JunitTestHelper.class.getResource("file_resources/house.jpg");
+		File backgroundImageFile = new File(backgroundImageUrl.toURI());
+		hotspotEditor
+			.updloadBackground(backgroundImageFile)
+			.resizeCircle()
+			.moveCircle(300, 120)
+			.addRectangle()
+			.moveRectangle(150, 150)
+			.setCardinality(Cardinality.SINGLE)
+			.save();
+		// change max score
+		hotspotEditor
+			.selectScores()
+			.setMaxScore("3")
+			.save();
+		// some feedbacks
+		hotspotEditor
+			.selectFeedbacks()
+			.setHint("Hint", "This is only an hint")
+			.setCorrectSolution("Correct solution", "This is the correct solution")
+			.setCorrectFeedback("Correct feedback", "This is correct")
+			.setIncorrectFeedback("Incorrect", "Your answer is not correct")
+			.save();
+		
+		//add a second hotspot: score per answer
+		hotspotEditor = qtiEditor
+			.addHotspot();
+		hotspotEditor
+			.updloadBackground(backgroundImageFile)
+			.resizeCircle()
+			.moveCircle(310, 125)
+			.addRectangle()
+			.moveRectangle(145, 155)
+			.setCardinality(Cardinality.SINGLE)
+			.save();
+		// change scoring
+		hotspotEditor
+			.selectScores()
+			.setMaxScore("2")
+			.selectAssessmentMode(ScoreEvaluation.perAnswer)
+			.setScore("1.", "2")
+			.setScore("2.", "0")
+			.save();
+		hotspotEditor
+			.selectFeedbacks()
+			.setHint("Hint", "The hint")
+			.setCorrectSolution("Correct solution", "This is the correct solution")
+			.setCorrectFeedback("Correct feedback", "This is correct")
+			.setIncorrectFeedback("Incorrect", "Your answer is not correct")
+			.save();
+		
+		qtiPage
+			.clickToolbarBack();
+		// access to all
+		qtiPage
+			.accessConfiguration()
+			.setUserAccess(UserAccess.guest)
+			.clickToolbarBack();
+		// show results
+		qtiPage
+			.options()
+			.showResults(Boolean.TRUE, QTI21AssessmentResultsOptions.allOptions())
+			.save();
+
+
+		//a user search the content package
+		LoginPage userLoginPage = LoginPage.getLoginPage(participantBrowser, deploymentUrl);
+		userLoginPage
+			.loginAs(ryomou.getLogin(), ryomou.getPassword())
+			.resume();
+		NavigationPage userNavBar = new NavigationPage(participantBrowser);
+		userNavBar
+			.openMyCourses()
+			.openSearch()
+			.extendedSearch(qtiTestTitle)
+			.select(qtiTestTitle)
+			.start();
+		
+		// make the test
+		QTI21Page ryomouQtiPage = QTI21Page
+				.getQTI12Page(participantBrowser);
+		ryomouQtiPage
+			.assertOnAssessmentItem()
+			.answerHotspot("rect")
+			.saveAnswer()
+			.assertFeedback("Incorrect")
+			.assertCorrectSolution("Correct solution")
+			.hint()
+			.assertFeedback("Hint")
+			.answerHotspot("circle")
+			.saveAnswer()
+			.assertFeedback("Correct feedback")
+			.nextAnswer()
+			.answerHotspot("rect")
+			.saveAnswer()
+			.assertCorrectSolution("Correct solution")
+			.assertFeedback("Incorrect")
+			.endTest()
+			.assertOnAssessmentResults()
+			.assertOnAssessmentTestScore(3);// 3 points from the first question, 0 from the second
+		
+
+		//a second user search the content package
+		LoginPage reiLoginPage = LoginPage.getLoginPage(participantBrowser, deploymentUrl);
+		reiLoginPage
+			.loginAs(rei.getLogin(), rei.getPassword())
+			.resume();
+		NavigationPage reiNavBar = new NavigationPage(participantBrowser);
+		reiNavBar
+			.openMyCourses()
+			.openSearch()
+			.extendedSearch(qtiTestTitle)
+			.select(qtiTestTitle)
+			.start();
+		
+		// make the test
+		QTI21Page
+			.getQTI12Page(participantBrowser)
+			.assertOnAssessmentItem()
+			.answerHotspot("circle")
+			.saveAnswer()
+			.assertFeedback("Correct feedback")
+			.nextAnswer()
+			.answerHotspot("circle")
+			.saveAnswer()
+			.endTest()
+			.assertOnAssessmentResults()
+			.assertOnAssessmentTestScore(5);// 3 points from the first question, 2 from the second
+	}
+	
+	/**
+	 * An author make a test with 2 hotspots with the multiple choice cardinality,
+	 * the first with the score set if all answers are correct, the second
+	 * with scoring per answers.<br>
+	 * A first user make the test, but doesn't answer all questions
+	 * correctly, log out and a second user make the perfect test.
+	 * 
+	 * @param authorLoginPage
+	 * @param participantBrowser
+	 * @throws IOException
+	 * @throws URISyntaxException
+	 */
+	@Test
+	@RunAsClient
+	public void qti21EditorHotspot_multipleChoice(@InitialPage LoginPage authorLoginPage,
+			@Drone @User WebDriver participantBrowser)
+	throws IOException, URISyntaxException {
+		UserVO author = new UserRestClient(deploymentUrl).createAuthor();
+		authorLoginPage.loginAs(author.getLogin(), author.getPassword());
+		UserVO ryomou = new UserRestClient(deploymentUrl).createRandomUser("Ryomou");
+		UserVO rei = new UserRestClient(deploymentUrl).createRandomUser("Rei");
+		
+		String qtiTestTitle = "Hotspot QTI 2.1 " + UUID.randomUUID();
+		navBar
+			.openAuthoringEnvironment()
+			.createQTI21Test(qtiTestTitle)
+			.clickToolbarBack();
+		
+		QTI21Page qtiPage = QTI21Page
+				.getQTI12Page(browser);
+		QTI21EditorPage qtiEditor = qtiPage
+				.edit();
+		//start a blank test
+		qtiEditor
+			.selectNode("Single choice")
+			.deleteNode();
+		
+		//add an hotspot: all answers score
+		QTI21HotspotEditorPage hotspotEditor = qtiEditor
+			.addHotspot();
+		// 2 spots
+		URL backgroundImageUrl = JunitTestHelper.class.getResource("file_resources/house.jpg");
+		File backgroundImageFile = new File(backgroundImageUrl.toURI());
+		hotspotEditor
+			.updloadBackground(backgroundImageFile)
+			.resizeCircle()
+			.moveCircle(300, 120)
+			.addRectangle()
+			.moveRectangle(150, 150)
+			.setCardinality(Cardinality.MULTIPLE)
+			.setCorrect("2.", true)
+			.save();
+		// change max score
+		hotspotEditor
+			.selectScores()
+			.setMaxScore("3")
+			.save();
+		// some feedbacks
+		hotspotEditor
+			.selectFeedbacks()
+			.setHint("Hint", "This is only an hint")
+			.setCorrectSolution("Correct solution", "This is the correct solution")
+			.setCorrectFeedback("Correct feedback", "This is correct")
+			.setIncorrectFeedback("Incorrect", "Your answer is not correct")
+			.save();
+		
+		//add a second hotspot: score per answer
+		hotspotEditor = qtiEditor
+			.addHotspot();
+		hotspotEditor
+			.updloadBackground(backgroundImageFile)
+			.resizeCircle()
+			.moveCircle(310, 125)
+			.addRectangle()
+			.moveRectangle(145, 155)
+			.setCardinality(Cardinality.MULTIPLE)
+			.setCorrect("2.", true)
+			.save();
+		// change scoring
+		hotspotEditor
+			.selectScores()
+			.setMaxScore("3")
+			.selectAssessmentMode(ScoreEvaluation.perAnswer)
+			.setScore("1.", "2")
+			.setScore("2.", "1")
+			.save();
+		hotspotEditor
+			.selectFeedbacks()
+			.setHint("Hint", "The hint")
+			.setCorrectSolution("Correct solution", "This is the correct solution")
+			.setCorrectFeedback("Correct feedback", "This is correct")
+			.setIncorrectFeedback("Incorrect", "Your answer is not correct")
+			.save();
+		
+		qtiPage
+			.clickToolbarBack();
+		// access to all
+		qtiPage
+			.accessConfiguration()
+			.setUserAccess(UserAccess.guest)
+			.clickToolbarBack();
+		// show results
+		qtiPage
+			.options()
+			.showResults(Boolean.TRUE, QTI21AssessmentResultsOptions.allOptions())
+			.save();
+
+
+		//a user search the content package
+		LoginPage userLoginPage = LoginPage.getLoginPage(participantBrowser, deploymentUrl);
+		userLoginPage
+			.loginAs(ryomou.getLogin(), ryomou.getPassword())
+			.resume();
+		NavigationPage userNavBar = new NavigationPage(participantBrowser);
+		userNavBar
+			.openMyCourses()
+			.openSearch()
+			.extendedSearch(qtiTestTitle)
+			.select(qtiTestTitle)
+			.start();
+		
+		// make the test
+		QTI21Page ryomouQtiPage = QTI21Page
+				.getQTI12Page(participantBrowser);
+		ryomouQtiPage
+			.assertOnAssessmentItem()
+			.answerHotspot("rect")
+			.saveAnswer()
+			.assertFeedback("Incorrect")
+			.assertCorrectSolution("Correct solution")
+			.hint()
+			.assertFeedback("Hint")
+			.answerHotspot("circle")
+			.saveAnswer()
+			.assertFeedback("Correct feedback")
+			.nextAnswer()
+			.answerHotspot("circle")
+			.saveAnswer()
+			.assertCorrectSolution("Correct solution")
+			.assertFeedback("Incorrect")
+			.endTest()
+			.assertOnAssessmentResults()
+			.assertOnAssessmentTestScore(5);// 3 points from the first question, 2 from the second
+		
+
+		//a second user search the content package
+		LoginPage reiLoginPage = LoginPage.getLoginPage(participantBrowser, deploymentUrl);
+		reiLoginPage
+			.loginAs(rei.getLogin(), rei.getPassword())
+			.resume();
+		NavigationPage reiNavBar = new NavigationPage(participantBrowser);
+		reiNavBar
+			.openMyCourses()
+			.openSearch()
+			.extendedSearch(qtiTestTitle)
+			.select(qtiTestTitle)
+			.start();
+		
+		// make the test
+		QTI21Page
+			.getQTI12Page(participantBrowser)
+			.assertOnAssessmentItem()
+			.answerHotspot("circle")
+			.answerHotspot("rect")
+			.saveAnswer()
+			.assertFeedback("Correct feedback")
+			.nextAnswer()
+			.answerHotspot("circle")
+			.answerHotspot("rect")
+			.saveAnswer()
+			.endTest()
+			.assertOnAssessmentResults()
+			.assertOnAssessmentTestScore(6);// 3 points from the first question, 3 from the second
+	}
+	
 	/**
 	 * An author make a test with 2 matches. A match with "multiple selection"
 	 * and score "all answers", a second with "single selection" and score
diff --git a/src/test/java/org/olat/selenium/page/qti/QTI21EditorPage.java b/src/test/java/org/olat/selenium/page/qti/QTI21EditorPage.java
index 922e0f27244..70a1e3d7207 100644
--- a/src/test/java/org/olat/selenium/page/qti/QTI21EditorPage.java
+++ b/src/test/java/org/olat/selenium/page/qti/QTI21EditorPage.java
@@ -137,6 +137,11 @@ public class QTI21EditorPage {
 		return new QTI21LobEditorPage(browser);
 	}
 	
+	public QTI21HotspotEditorPage addHotspot() {
+		addQuestion(QTI21QuestionType.hotspot);
+		return new QTI21HotspotEditorPage(browser);
+	}
+	
 	private QTI21EditorPage addQuestion(QTI21QuestionType type) {
 		openElementsMenu();
 		
diff --git a/src/test/java/org/olat/selenium/page/qti/QTI21HotspotEditorPage.java b/src/test/java/org/olat/selenium/page/qti/QTI21HotspotEditorPage.java
new file mode 100644
index 00000000000..a3575ed9b72
--- /dev/null
+++ b/src/test/java/org/olat/selenium/page/qti/QTI21HotspotEditorPage.java
@@ -0,0 +1,135 @@
+package org.olat.selenium.page.qti;
+
+import java.io.File;
+
+import org.olat.selenium.page.graphene.OOGraphene;
+import org.openqa.selenium.By;
+import org.openqa.selenium.Dimension;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.interactions.Actions;
+
+import uk.ac.ed.ph.jqtiplus.value.Cardinality;
+
+/**
+ * 
+ * Initial date: 23 oct. 2017<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class QTI21HotspotEditorPage extends QTI21AssessmentItemEditorPage {
+	
+	public QTI21HotspotEditorPage(WebDriver browser) {
+		super(browser);
+	}
+	
+	public QTI21HotspotEditorPage updloadBackground(File file) {
+		By inputBy = By.cssSelector(".o_fileinput input[type='file']");
+		OOGraphene.uploadFile(inputBy, file, browser);
+		OOGraphene.waitBusy(browser);
+		return this;
+	}
+	
+	public QTI21HotspotEditorPage setCardinality(Cardinality cardinality) {
+		By cardinalityBy = By.xpath("//div[contains(@class,'o_sel_assessment_item_cardinality')]//input[@value='" + cardinality.name() + "']");
+		browser.findElement(cardinalityBy).click();
+		return this;
+	}
+	
+	public QTI21HotspotEditorPage setCorrect(String indexName, boolean correct) {
+		By correctCheckboxBy = By.xpath("//div[contains(@class,'o_sel_assessment_item_correct_spots')]//label[contains(text(),'" + indexName+ "')]/input[@name='form.imd.correct.spots']");
+		WebElement correctCheckboxEl = browser.findElement(correctCheckboxBy);
+		OOGraphene.check(correctCheckboxEl, new Boolean(correct));
+		OOGraphene.waitBusy(browser);
+		return this;
+	}
+	
+	/**
+	 * Add a rectangle hotspot.
+	 * 
+	 * @return Itself
+	 */
+	public QTI21HotspotEditorPage addRectangle() {
+		By addRectBy = By.xpath("//a[contains(@class,'btn-default')][i[contains(@class,'o_icon_rectangle')]]");
+		browser.findElement(addRectBy).click();
+		OOGraphene.waitBusy(browser);
+		By rectangleBy = By.cssSelector("div.o_draw_rectangle");
+		OOGraphene.waitElement(rectangleBy, browser);
+		return this;
+	}
+	
+	/**
+	 * Resize the default circle
+	 * @return Itself
+	 */
+	public QTI21HotspotEditorPage resizeCircle() {
+		By circleBy = By.cssSelector("div.o_draw_circle");
+		OOGraphene.waitElement(circleBy, browser);
+		WebElement circleEl = browser.findElement(circleBy);
+		new Actions(browser)
+			.moveToElement(circleEl, 10, 10)
+			.clickAndHold()
+			.moveByOffset(60, 60)
+			.release()
+			.build()
+			.perform();
+		return this;
+	}
+	
+	/**
+	 * Move a circle hotspot by the specified offsets.
+	 * 
+	 * @param xOffset
+	 * @param yOffset
+	 * @return
+	 */
+	public QTI21HotspotEditorPage moveCircle(int xOffset, int yOffset) {
+		By circleBy = By.cssSelector("div.o_draw_circle");
+		return moveElement(circleBy, xOffset, yOffset);
+	}
+	
+	/**
+	 * Move a rectangle hotspot by the specified offsets.
+	 * 
+	 * @param xOffset
+	 * @param yOffset
+	 * @return Itself
+	 */
+	public QTI21HotspotEditorPage moveRectangle(int xOffset, int yOffset) {
+		By rectangleBy = By.cssSelector("div.o_draw_rectangle");
+		return moveElement(rectangleBy, xOffset, yOffset);
+	}
+	
+	private QTI21HotspotEditorPage moveElement(By elementBy, int xOffset, int yOffset) {
+		OOGraphene.waitElement(elementBy, browser);
+		WebElement rectangleEl = browser.findElement(elementBy);
+		Dimension size = rectangleEl.getSize();
+		int centerX = size.getWidth() / 2;
+		int centerY = size.getHeight() / 2;
+		
+		new Actions(browser)
+			.moveToElement(rectangleEl, centerX, centerY)
+			.clickAndHold()
+			.moveByOffset(xOffset, yOffset)
+			.release()
+			.build()
+			.perform();
+		return this;
+	}
+
+	public QTI21HotspotEditorPage save() {
+		By saveBy = By.cssSelector("div.o_sel_hotspots_save button.btn.btn-primary");
+		OOGraphene.click(saveBy, browser);
+		return this;
+	}
+	
+	public QTI21HotspotScoreEditorPage selectScores() {
+		selectTab(By.className("o_sel_assessment_item_options"));
+		return new QTI21HotspotScoreEditorPage(browser);
+	}
+	
+	public QTI21FeedbacksEditorPage selectFeedbacks() {
+		selectTab(By.className("o_sel_assessment_item_feedbacks"));
+		return new QTI21FeedbacksEditorPage(browser);
+	}
+}
diff --git a/src/test/java/org/olat/selenium/page/qti/QTI21HotspotScoreEditorPage.java b/src/test/java/org/olat/selenium/page/qti/QTI21HotspotScoreEditorPage.java
new file mode 100644
index 00000000000..bc4c3dd2dd5
--- /dev/null
+++ b/src/test/java/org/olat/selenium/page/qti/QTI21HotspotScoreEditorPage.java
@@ -0,0 +1,79 @@
+/**
+ * <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 org.olat.ims.qti21.model.xml.interactions.SimpleChoiceAssessmentItemBuilder.ScoreEvaluation;
+import org.olat.selenium.page.graphene.OOGraphene;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+/**
+ * 
+ * Initial date: 24 oct. 2017<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class QTI21HotspotScoreEditorPage {
+	
+	private static final By hotspotsScoreTable = By.className("o_sel_hotspots_scores");
+	
+	private final WebDriver browser;
+	
+	public QTI21HotspotScoreEditorPage(WebDriver browser) {
+		this.browser = browser;
+	}
+	
+	public QTI21HotspotScoreEditorPage selectAssessmentMode(ScoreEvaluation mode) {
+		By modeBy = By.cssSelector("#o_coassessment_mode input[value='" + mode.name() + "']");
+		browser.findElement(modeBy).click();
+		OOGraphene.waitBusy(browser);
+		if(mode == ScoreEvaluation.allCorrectAnswers) {
+			OOGraphene.waitElementDisappears(hotspotsScoreTable, 5, browser);
+		} else if (mode == ScoreEvaluation.perAnswer) {
+			OOGraphene.waitElement(hotspotsScoreTable, 5, browser);
+		}
+		return this;
+	}
+	
+	public QTI21HotspotScoreEditorPage setScore(String indexName, String score) {
+		By scoreBy = By.xpath("//table[contains(@class,'o_sel_hotspots_scores')]//tr[td[contains(text(),'" + indexName + "')]]/td/div/input[@type='text']");
+		WebElement scoreEl = browser.findElement(scoreBy);
+		scoreEl.clear();
+		scoreEl.sendKeys(score);
+		return this;
+	}
+	
+	public QTI21HotspotScoreEditorPage setMaxScore(String maxScore) {
+		By maxScoreBy = By.cssSelector("div.o_sel_assessment_item_max_score input[type='text']");
+		WebElement maxScoreEl = browser.findElement(maxScoreBy);
+		maxScoreEl.clear();
+		maxScoreEl.sendKeys(maxScore);
+		return this;
+	}
+	
+	public QTI21HotspotScoreEditorPage save() {
+		By saveBy = By.cssSelector("fieldset.o_sel_assessment_item_options button.btn.btn-primary");
+		browser.findElement(saveBy).click();
+		OOGraphene.waitBusy(browser);
+		return this;
+	}
+
+}
-- 
GitLab