From 91ae1b8b1c067bdff2094ba00f4d1f53a5a577e1 Mon Sep 17 00:00:00 2001
From: srosse <none@none>
Date: Fri, 3 Nov 2017 12:09:30 +0100
Subject: [PATCH] no-jira: add a selenium test for the FIB editor

---
 .../interactions/FIBEditorController.java     |   2 +
 .../interactions/FIBScoreController.java      |   2 +
 .../FIBTextEntrySettingsController.java       |   4 +
 .../interactions/_content/fib_score.html      |   2 +-
 .../ui/LectureSettingsAdminController.java    |   8 +-
 .../java/org/olat/selenium/ImsQTI21Test.java  | 172 ++++++++++++++++++
 .../page/core/AdministrationPage.java         |  10 +
 .../selenium/page/graphene/OOGraphene.java    |  21 ++-
 .../lecture/LectureAdminSettingsPage.java     |  68 +++++++
 .../page/lecture/TeacherRollCallPage.java     |  14 +-
 .../selenium/page/qti/QTI21EditorPage.java    |   5 +
 .../page/qti/QTI21GapEntriesEditorPage.java   | 140 ++++++++++++++
 .../qti/QTI21GapEntriesScoreEditorPage.java   |  86 +++++++++
 .../org/olat/selenium/page/qti/QTI21Page.java |  21 +++
 14 files changed, 550 insertions(+), 5 deletions(-)
 create mode 100644 src/test/java/org/olat/selenium/page/lecture/LectureAdminSettingsPage.java
 create mode 100644 src/test/java/org/olat/selenium/page/qti/QTI21GapEntriesEditorPage.java
 create mode 100644 src/test/java/org/olat/selenium/page/qti/QTI21GapEntriesScoreEditorPage.java

diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/interactions/FIBEditorController.java b/src/main/java/org/olat/ims/qti21/ui/editor/interactions/FIBEditorController.java
index 6e7eef7aaa1..8a93302e1cb 100644
--- a/src/main/java/org/olat/ims/qti21/ui/editor/interactions/FIBEditorController.java
+++ b/src/main/java/org/olat/ims/qti21/ui/editor/interactions/FIBEditorController.java
@@ -103,6 +103,7 @@ public class FIBEditorController extends FormBasicController {
 		textEl = uifactory.addRichTextElementForQTI21("desc", "form.imd.descr", question, 16, -1, itemContainer,
 				formLayout, ureq.getUserSession(),  getWindowControl());
 		textEl.addActionListener(FormEvent.ONCLICK);
+		textEl.setElementCssClass("o_sel_assessment_item_fib_text");
 		RichTextConfiguration richTextConfig = textEl.getEditorConfiguration();
 		richTextConfig.setReadOnly(restrictedEdit);
 		
@@ -129,6 +130,7 @@ public class FIBEditorController extends FormBasicController {
 		
 		// Submit Button
 		FormLayoutContainer buttonsContainer = FormLayoutContainer.createButtonLayout("buttons", getTranslator());
+		buttonsContainer.setElementCssClass("o_sel_fib_save");
 		buttonsContainer.setRootForm(mainForm);
 		formLayout.add(buttonsContainer);
 		uifactory.addFormSubmitButton("submit", buttonsContainer);
diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/interactions/FIBScoreController.java b/src/main/java/org/olat/ims/qti21/ui/editor/interactions/FIBScoreController.java
index d8574bd6dc4..9e9722e604d 100644
--- a/src/main/java/org/olat/ims/qti21/ui/editor/interactions/FIBScoreController.java
+++ b/src/main/java/org/olat/ims/qti21/ui/editor/interactions/FIBScoreController.java
@@ -84,11 +84,13 @@ public class FIBScoreController extends AssessmentItemRefEditorController implem
 		setFormContextHelp("Test editor QTI 2.1 in detail#details_testeditor_score");
 		super.initForm(formLayout, listener, ureq);
 		minScoreEl = uifactory.addTextElement("min.score", "min.score", 8, "0.0", formLayout);
+		minScoreEl.setElementCssClass("o_sel_assessment_item_min_score");
 		minScoreEl.setEnabled(false);
 		
 		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/FIBTextEntrySettingsController.java b/src/main/java/org/olat/ims/qti21/ui/editor/interactions/FIBTextEntrySettingsController.java
index 09552b59fb1..315ac739d4f 100644
--- a/src/main/java/org/olat/ims/qti21/ui/editor/interactions/FIBTextEntrySettingsController.java
+++ b/src/main/java/org/olat/ims/qti21/ui/editor/interactions/FIBTextEntrySettingsController.java
@@ -75,8 +75,11 @@ public class FIBTextEntrySettingsController extends FormBasicController {
 
 	@Override
 	protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) {
+		formLayout.setElementCssClass("o_sel_gap_entry_form");
+		
 		String solution = interaction.getSolution();
 		solutionEl = uifactory.addTextElement("fib.solution", "fib.solution", 256, solution, formLayout);
+		solutionEl.setElementCssClass("o_sel_gap_entry_solution");
 		solutionEl.setEnabled(!restrictedEdit);
 		if(!StringHelper.containsNonWhitespace(solution)) {
 			solutionEl.setFocus(true);
@@ -84,6 +87,7 @@ public class FIBTextEntrySettingsController extends FormBasicController {
 		
 		String placeholder = interaction.getPlaceholder();
 		placeholderEl = uifactory.addTextElement("fib.placeholder", "fib.placeholder", 256, placeholder, formLayout);
+		placeholderEl.setElementCssClass("o_sel_gap_entry_placeholder");
 		placeholderEl.setEnabled(!restrictedEdit);
 		
 		String alternativesPage = velocity_root + "/fib_alternatives.html";
diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/interactions/_content/fib_score.html b/src/main/java/org/olat/ims/qti21/ui/editor/interactions/_content/fib_score.html
index 78277a2a7af..a4979cad25e 100644
--- a/src/main/java/org/olat/ims/qti21/ui/editor/interactions/_content/fib_score.html
+++ b/src/main/java/org/olat/ims/qti21/ui/editor/interactions/_content/fib_score.html
@@ -1,4 +1,4 @@
-<table class="table">
+<table class="table o_sel_gap_entries_scores">
 	<thead><tr>
 		<th>$r.translate("form.score.answer.summary")</th>
 		<th>$r.translate("form.score.answer.points")</th>
diff --git a/src/main/java/org/olat/modules/lecture/ui/LectureSettingsAdminController.java b/src/main/java/org/olat/modules/lecture/ui/LectureSettingsAdminController.java
index 5ab057b48c3..6ad9a4b2e27 100644
--- a/src/main/java/org/olat/modules/lecture/ui/LectureSettingsAdminController.java
+++ b/src/main/java/org/olat/modules/lecture/ui/LectureSettingsAdminController.java
@@ -112,11 +112,13 @@ public class LectureSettingsAdminController extends FormBasicController {
 		formLayout.add("global", globalCont);
 
 		partiallyDoneEnabledEl = uifactory.addCheckboxesVertical("lecture.status.partially.done.enabled", globalCont, onKeys, onValues, 1);
-
+		partiallyDoneEnabledEl.setElementCssClass("o_sel_lecture_status_partially_done");
+		
 		String[] statusKeys = new String[]{ LectureBlockStatus.cancelled.name() };
 		String[] statusValues = new String[]{ translate(LectureBlockStatus.cancelled.name()) };
 		statusEnabledEl = uifactory.addCheckboxesVertical("lecture.status.enabled", globalCont, statusKeys, statusValues, 1);
-
+		statusEnabledEl.setElementCssClass("o_sel_lecture_status_cancelled");
+		
 		// reminder enabled
 		reminderEnableEl = uifactory.addCheckboxesHorizontal("lecture.reminder.enabled", globalCont, onKeys, onValues);
 		reminderEnableEl.addActionListener(FormEvent.ONCHANGE);
@@ -129,6 +131,7 @@ public class LectureSettingsAdminController extends FormBasicController {
 		autoClosePeriodEl.setMandatory(true);
 
 		authorizedAbsenceEnableEl = uifactory.addCheckboxesHorizontal("lecture.authorized.absence.enabled", globalCont, onKeys, onValues);
+		authorizedAbsenceEnableEl.setElementCssClass("o_sel_lecture_autorized_absence");
 		authorizedAbsenceEnableEl.addActionListener(FormEvent.ONCHANGE);
 		countAuthorizedAbsenceAsAttendantEl = uifactory.addCheckboxesHorizontal("lecture.count.authorized.absence.attendant", globalCont, onKeys, onValues);
 		absenceDefaultAuthorizedEl = uifactory.addCheckboxesHorizontal("lecture.absence.default.authorized", globalCont, onKeys, onValues);
@@ -146,6 +149,7 @@ public class LectureSettingsAdminController extends FormBasicController {
 		formLayout.add("buttonsWrapper", buttonsWrapperCont);
 		FormLayoutContainer buttonsCont = FormLayoutContainer.createButtonLayout("buttons", getTranslator());
 		buttonsWrapperCont.add(buttonsCont);
+		buttonsCont.setElementCssClass("o_sel_lecture_save_settings");
 		uifactory.addFormSubmitButton("save", buttonsCont);
 	}
 	
diff --git a/src/test/java/org/olat/selenium/ImsQTI21Test.java b/src/test/java/org/olat/selenium/ImsQTI21Test.java
index ab302ad0d46..3755cdfca5a 100644
--- a/src/test/java/org/olat/selenium/ImsQTI21Test.java
+++ b/src/test/java/org/olat/selenium/ImsQTI21Test.java
@@ -47,6 +47,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.QTI21GapEntriesEditorPage;
 import org.olat.selenium.page.qti.QTI21HotspotEditorPage;
 import org.olat.selenium.page.qti.QTI21KprimEditorPage;
 import org.olat.selenium.page.qti.QTI21LobEditorPage;
@@ -1945,6 +1946,177 @@ public class ImsQTI21Test {
 			.assertOnAssessmentTestScore(6);// 3 points from the first question, 3 from the second
 	}
 	
+	/**
+	 * An author make a test with 2 questions using fill-in-blank,
+	 * 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 qti21EditorFib_text(@InitialPage LoginPage authorLoginPage,
+			@Drone @User WebDriver participantBrowser)
+	throws IOException, URISyntaxException {
+
+		UserVO author = new UserRestClient(deploymentUrl).createAuthor();
+		UserVO ryomou = new UserRestClient(deploymentUrl).createRandomUser("Ryomou");
+		UserVO rei = new UserRestClient(deploymentUrl).createRandomUser("Rei");
+		authorLoginPage.loginAs(author.getLogin(), author.getPassword());
+
+		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 a gap entry: all answers score
+		QTI21GapEntriesEditorPage fibEditor = qtiEditor
+			.addFib()
+			.appendContent("Usefull for circles ")
+			.addGapEntry("Pi", "314")
+			.saveGapEntry()
+			.editGapEntry("Ln", "lognat", 2)
+			.saveGapEntry()
+			.save();
+		//set max score
+		fibEditor
+			.selectScores()
+			.selectAssessmentMode(ScoreEvaluation.allCorrectAnswers)
+			.setMaxScore("2")
+			.save();
+		// set feedbacks
+		fibEditor
+			.selectFeedbacks()
+			.setHint("Hint", "This is a usefull hint")
+			.setCorrectSolution("Correct solution", "This is an information about the correct solution")
+			.setCorrectFeedback("Correct feedback", "Your answer is correct")
+			.setIncorrectFeedback("Incorrect", "Your answer is not correct")
+			.save();
+		
+		//add a gap entry: score per anser
+		fibEditor = qtiEditor
+			.addFib()
+			.appendContent("European rocket ")
+			.addGapEntry("Ariane", "ari")
+			.saveGapEntry()
+			.editGapEntry("Falcon9", "falc", 2)
+			.saveGapEntry()
+			.save();
+		//set max score
+		fibEditor
+			.selectScores()
+			.selectAssessmentMode(ScoreEvaluation.perAnswer)
+			.setMaxScore("4")
+			.setScore("Ariane", "3")
+			.setScore("Falcon9", "1")
+			.save();
+		// set feedbacks
+		fibEditor
+			.selectFeedbacks()
+			.setHint("Hint", "Think to space")
+			.setCorrectSolution("Correct solution", "This is an information about the correct solution")
+			.setCorrectFeedback("Correct feedback", "Your answer 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();
+		
+		// first user make the test
+		QTI21Page ryomouQtiPage = QTI21Page
+				.getQTI12Page(participantBrowser);
+		ryomouQtiPage
+			.assertOnAssessmentItem()
+			.answerGapTextWithPlaceholder("Log", "314")
+			.answerGapTextWithPlaceholder("Sin", "lognat")
+			.saveAnswer()
+			.assertFeedback("Incorrect")
+			.assertCorrectSolution("Correct solution")
+			.hint()
+			.assertFeedback("Hint")
+			.answerGapTextWithPlaceholder("Pi", "314")
+			.answerGapTextWithPlaceholder("Ln", "lognat")
+			.saveAnswer()
+			.assertFeedback("Correct feedback")
+			.nextAnswer()
+			.answerGapTextWithPlaceholder("Saturn 5", "ari")
+			.answerGapTextWithPlaceholder("Falcon9", "falc")
+			.saveAnswer()
+			.assertCorrectSolution("Correct solution")
+			.assertFeedback("Incorrect")
+			.endTest()
+			.assertOnAssessmentResults()
+			.assertOnAssessmentTestScore(3);// 2 points from the first question, 1 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 with all the correct answers
+		QTI21Page
+			.getQTI12Page(participantBrowser)
+			.assertOnAssessmentItem()
+			.answerGapTextWithPlaceholder("Pi", "314")
+			.answerGapTextWithPlaceholder("Ln", "lognat")
+			.saveAnswer()
+			.assertFeedback("Correct feedback")
+			.nextAnswer()
+			.answerGapTextWithPlaceholder("Ariane", "ari")
+			.answerGapTextWithPlaceholder("Falcon9", "falc")
+			.saveAnswer()
+			.endTest()
+			.assertOnAssessmentResults()
+			.assertOnAssessmentTestScore(6);// 2 points from the first question, 4 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/core/AdministrationPage.java b/src/test/java/org/olat/selenium/page/core/AdministrationPage.java
index 02363a69a6b..02da1bd003e 100644
--- a/src/test/java/org/olat/selenium/page/core/AdministrationPage.java
+++ b/src/test/java/org/olat/selenium/page/core/AdministrationPage.java
@@ -23,6 +23,7 @@ import java.util.List;
 
 import org.junit.Assert;
 import org.olat.selenium.page.graphene.OOGraphene;
+import org.olat.selenium.page.lecture.LectureAdminSettingsPage;
 import org.openqa.selenium.By;
 import org.openqa.selenium.WebDriver;
 import org.openqa.selenium.WebElement;
@@ -91,6 +92,15 @@ public class AdministrationPage {
 		return this;
 	}
 	
+	public LectureAdminSettingsPage openLecturesSettings() {
+		selectModules();
+		
+		By lecturesBy = By.cssSelector(".o_sel_lectures span.o_tree_level_label_leaf>a");
+		browser.findElement(lecturesBy).click();
+		OOGraphene.waitBusy(browser);
+		return new LectureAdminSettingsPage(browser);
+	}
+	
 	public AdministrationPage openGroupSettings() {
 		selectModules();
 		
diff --git a/src/test/java/org/olat/selenium/page/graphene/OOGraphene.java b/src/test/java/org/olat/selenium/page/graphene/OOGraphene.java
index ec91e6a3512..4a058b65bcb 100644
--- a/src/test/java/org/olat/selenium/page/graphene/OOGraphene.java
+++ b/src/test/java/org/olat/selenium/page/graphene/OOGraphene.java
@@ -213,7 +213,7 @@ public class OOGraphene {
 	/**
 	 * Scroll to the top anchor.
 	 * 
-	 * @param browser
+	 * @param browser The browser
 	 */
 	public static void scrollTop(WebDriver browser) {
 		WebElement el = browser.findElement(By.id("o_top"));
@@ -242,6 +242,25 @@ public class OOGraphene {
 		((JavascriptExecutor)browser).executeScript("top.tinymce.editors['" + tinyId + "'].setContent('" + content + "')");
 	}
 	
+	/**
+	 * Insert a piece of text in TinyMCE where is the caret.
+	 * 
+	 * @param content The text to add
+	 * @param containerCssSelector A selector to point where the rich text editor is
+	 * @param browser The browser
+	 */
+	public static final void tinymceInsert(String content, String containerCssSelector, WebDriver browser) {
+		By tinyIdBy = By.cssSelector(containerCssSelector + " div.o_richtext_mce");
+		waitElement(tinyIdBy, 5, browser);
+		WebElement tinyIdEl = browser.findElement(tinyIdBy);
+		String tinyId = tinyIdEl.getAttribute("id").replace("_diw", "");
+
+		Graphene.waitModel(browser).withTimeout(waitTinyDuration, TimeUnit.SECONDS)
+			.pollingEvery(poolingDuration, TimeUnit.MILLISECONDS)
+			.until(new TinyMCELoadedByIdPredicate(tinyId));
+		((JavascriptExecutor)browser).executeScript("top.tinymce.editors['" + tinyId + "'].insertContent('" + content + "')");
+	}
+	
 	/**
 	 * 
 	 * @param tabsBy The selector for the tabs bar
diff --git a/src/test/java/org/olat/selenium/page/lecture/LectureAdminSettingsPage.java b/src/test/java/org/olat/selenium/page/lecture/LectureAdminSettingsPage.java
new file mode 100644
index 00000000000..311036c558e
--- /dev/null
+++ b/src/test/java/org/olat/selenium/page/lecture/LectureAdminSettingsPage.java
@@ -0,0 +1,68 @@
+/**
+ * <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.lecture;
+
+import org.olat.selenium.page.graphene.OOGraphene;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+/**
+ * 
+ * Initial date: 31 oct. 2017<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class LectureAdminSettingsPage {
+	
+	private WebDriver browser;
+	
+	public LectureAdminSettingsPage(WebDriver browser) {
+		this.browser = browser;
+	}
+	
+	/**
+	 * Set some configurations of the lecture module.
+	 * 
+	 * @param authorizedAbsence Enable/disable authorized absence
+	 * @return Itself
+	 */
+	public LectureAdminSettingsPage set(boolean authorizedAbsence) {		
+		By authorizedAbsenceLabelBy = By.xpath("//label[input[@name='lecture.authorized.absence.enabled' and @value='on']]");
+		By authorizedAbsenceCheckBy = By.xpath("//label/input[@name='lecture.authorized.absence.enabled' and @value='on']");
+		
+		OOGraphene.waitElement(authorizedAbsenceLabelBy, browser);
+		OOGraphene.scrollTo(authorizedAbsenceLabelBy, browser);
+		
+		WebElement authorizedAbsenceLabelEl = browser.findElement(authorizedAbsenceLabelBy);
+		WebElement authorizedAbsenceCheckEl = browser.findElement(authorizedAbsenceCheckBy);
+		OOGraphene.check(authorizedAbsenceLabelEl, authorizedAbsenceCheckEl, new Boolean(authorizedAbsence));
+		OOGraphene.waitBusy(browser);
+		return this;
+	}
+	
+	public LectureAdminSettingsPage save() {
+		By saveBy = By.cssSelector("div.o_sel_lecture_save_settings button.btn-primary");
+		browser.findElement(saveBy).click();
+		OOGraphene.waitBusy(browser);
+		return this;
+	}
+
+}
diff --git a/src/test/java/org/olat/selenium/page/lecture/TeacherRollCallPage.java b/src/test/java/org/olat/selenium/page/lecture/TeacherRollCallPage.java
index 986215d55a4..f7818426d79 100644
--- a/src/test/java/org/olat/selenium/page/lecture/TeacherRollCallPage.java
+++ b/src/test/java/org/olat/selenium/page/lecture/TeacherRollCallPage.java
@@ -19,11 +19,14 @@
  */
 package org.olat.selenium.page.lecture;
 
+import java.util.List;
+
 import org.olat.selenium.page.graphene.OOGraphene;
 import org.olat.user.restapi.UserVO;
 import org.openqa.selenium.By;
 import org.openqa.selenium.WebDriver;
 import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.ui.Select;
 
 /**
  * 
@@ -79,10 +82,19 @@ public class TeacherRollCallPage {
 	}
 	
 	/**
-	 * Simply confirm the clsoing of the roll call.
+	 * Simply confirm the closing of the roll call and choose
+	 * a reason if there is one.
+	 * 
 	 * @return Itself
 	 */
 	public TeacherRollCallPage confirmCloseRollCall() {
+		//check reasons
+		By reasonsBy = By.id("o_fioeffective_reason_SELBOX");
+		List<WebElement> reasonsEls = browser.findElements(reasonsBy);
+		if(reasonsEls.size() > 0) {
+			new Select(reasonsEls.get(0)).selectByIndex(1);
+		}
+		
 		By confirmCloseBy = By.cssSelector("fieldset.o_sel_lecture_confirm_close_form button.btn-primary");
 		browser.findElement(confirmCloseBy).click();
 		OOGraphene.waitBusy(browser);
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 70a1e3d7207..da88b562678 100644
--- a/src/test/java/org/olat/selenium/page/qti/QTI21EditorPage.java
+++ b/src/test/java/org/olat/selenium/page/qti/QTI21EditorPage.java
@@ -142,6 +142,11 @@ public class QTI21EditorPage {
 		return new QTI21HotspotEditorPage(browser);
 	}
 	
+	public QTI21GapEntriesEditorPage addFib() {
+		addQuestion(QTI21QuestionType.fib);
+		return new QTI21GapEntriesEditorPage(browser);
+	}
+	
 	private QTI21EditorPage addQuestion(QTI21QuestionType type) {
 		openElementsMenu();
 		
diff --git a/src/test/java/org/olat/selenium/page/qti/QTI21GapEntriesEditorPage.java b/src/test/java/org/olat/selenium/page/qti/QTI21GapEntriesEditorPage.java
new file mode 100644
index 00000000000..8213ce301d1
--- /dev/null
+++ b/src/test/java/org/olat/selenium/page/qti/QTI21GapEntriesEditorPage.java
@@ -0,0 +1,140 @@
+/**
+ * <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.selenium.page.graphene.OOGraphene;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+/**
+ * 
+ * Initial date: 2 nov. 2017<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class QTI21GapEntriesEditorPage extends QTI21AssessmentItemEditorPage {
+
+	public QTI21GapEntriesEditorPage(WebDriver browser) {
+		super(browser);
+	}
+	
+	public QTI21GapEntriesEditorPage appendContent(String text) {
+		String textSelector = ".o_sel_assessment_item_fib_text";
+		OOGraphene.tinymceInsert(text, textSelector, browser);
+		return this;
+	}
+	
+	/**
+	 * Add a new gap entry of type text. Use the placeholder to locate
+	 * the gap during the test.
+	 * 
+	 * @param solution The solution
+	 * @param placeholder The placeholder
+	 * @return Itself
+	 */
+	public QTI21GapEntriesEditorPage addGapEntry(String solution, String placeholder) {
+		By addGapBy = By.xpath("//div[contains(@class,'o_sel_assessment_item_fib_text')]//button[i[contains(@class,'mce-i-gaptext')]]");
+		browser.findElement(addGapBy).click();
+		OOGraphene.waitModalDialog(browser);
+		
+		By solutionBy = By.cssSelector("fieldset.o_sel_gap_entry_form div.o_sel_gap_entry_solution input[type=text]");
+		OOGraphene.waitElement(solutionBy, browser);
+		browser.findElement(solutionBy).sendKeys(solution);
+		
+		By placeholderBy = By.cssSelector("fieldset.o_sel_gap_entry_form div.o_sel_gap_entry_placeholder input[type=text]");
+		browser.findElement(placeholderBy).sendKeys(placeholder);
+		return this;
+	}
+	
+	/**
+	 * Edit an existing gap entry of type text. Use the placeholder to locate
+	 * the gap during the test.
+	 * 
+	 * @param solution The solution
+	 * @param placeholder The placeholder
+	 * @param index The index of the entry in the paragraph
+	 * @return Itself
+	 */
+	public QTI21GapEntriesEditorPage editGapEntry(String solution, String placeholder, int index) {
+		By frameBy = By.cssSelector("div.o_sel_assessment_item_fib_text div.mce-edit-area iframe");
+		WebElement frameEl = browser.findElement(frameBy);
+		browser.switchTo().frame(frameEl);
+		
+		By gapEntryBy = By.xpath("//p/span[@class='textentryinteraction'][" + index + "]/a");
+		browser.findElement(gapEntryBy).click();
+		
+		browser.switchTo().defaultContent();
+		OOGraphene.waitModalDialog(browser);
+		
+		By solutionBy = By.cssSelector("fieldset.o_sel_gap_entry_form div.o_sel_gap_entry_solution input[type=text]");
+		WebElement solutionEl = browser.findElement(solutionBy);
+		solutionEl.clear();
+		solutionEl.sendKeys(solution);
+		
+		By placeholderBy = By.cssSelector("fieldset.o_sel_gap_entry_form div.o_sel_gap_entry_placeholder input[type=text]");
+		browser.findElement(placeholderBy).sendKeys(placeholder);
+		return this;
+	}
+	
+	/**
+	 * Save and close the modal dialog to edit the gap entry.
+	 * 
+	 * @return Itself
+	 */
+	public QTI21GapEntriesEditorPage saveGapEntry() {
+		By saveBy = By.cssSelector(".o_sel_gap_entry_form button.btn-primary");
+		browser.findElement(saveBy).click();
+		OOGraphene.waitBusy(browser);
+		return this;
+	}
+	
+	/**
+	 * Save the whole interaction.
+	 * 
+	 * @return Itself
+	 */
+	public QTI21GapEntriesEditorPage save() {
+		By saveBy = By.cssSelector("div.o_sel_fib_save button.btn.btn-primary");
+		OOGraphene.click(saveBy, browser);
+		OOGraphene.waitBusy(browser);
+		return this;
+	}
+	
+	/**
+	 * Select the tab to edit the scores
+	 * 
+	 * @return The score page
+	 */
+	public QTI21GapEntriesScoreEditorPage selectScores() {
+		selectTab(By.className("o_sel_assessment_item_options"));
+		return new QTI21GapEntriesScoreEditorPage(browser);
+	}
+	
+	/**
+	 * Select the tab to edit the feedbacks
+	 * 
+	 * @return the feedback page
+	 */
+	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/QTI21GapEntriesScoreEditorPage.java b/src/test/java/org/olat/selenium/page/qti/QTI21GapEntriesScoreEditorPage.java
new file mode 100644
index 00000000000..d534738e9bb
--- /dev/null
+++ b/src/test/java/org/olat/selenium/page/qti/QTI21GapEntriesScoreEditorPage.java
@@ -0,0 +1,86 @@
+/**
+ * <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: 3 nov. 2017<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class QTI21GapEntriesScoreEditorPage {
+	
+	private static final By choiceScoreTable = By.className("o_sel_gap_entries_scores");
+	
+	private final WebDriver browser;
+	
+	public QTI21GapEntriesScoreEditorPage(WebDriver browser) {
+		this.browser = browser;
+	}
+	
+	public QTI21GapEntriesScoreEditorPage 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(choiceScoreTable, 5, browser);
+		} else if (mode == ScoreEvaluation.perAnswer) {
+			OOGraphene.waitElement(choiceScoreTable, 5, browser);
+		}
+		return this;
+	}
+	
+	public QTI21GapEntriesScoreEditorPage setScore(String answer, String score) {
+		By scoreBy = By.xpath("//table[contains(@class,'o_sel_gap_entries_scores')]//tr[td[contains(text(),'" + answer + "')]]/td/div/input[@type='text']");
+		WebElement scoreEl = browser.findElement(scoreBy);
+		scoreEl.clear();
+		scoreEl.sendKeys(score);
+		return this;
+	}
+	
+	public QTI21GapEntriesScoreEditorPage 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 QTI21GapEntriesScoreEditorPage setMinScore(String minScore) {
+		By minScoreBy = By.cssSelector("div.o_sel_assessment_item_min_score input[type='text']");
+		WebElement minScoreEl = browser.findElement(minScoreBy);
+		minScoreEl.clear();
+		minScoreEl.sendKeys(minScore);
+		return this;
+	}
+	
+	public QTI21GapEntriesScoreEditorPage save() {
+		By saveBy = By.cssSelector("fieldset.o_sel_assessment_item_options button.btn.btn-primary");
+		browser.findElement(saveBy).click();
+		OOGraphene.waitBusy(browser);
+		return this;
+	}
+}
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 e4636d51693..95b0b83efe9 100644
--- a/src/test/java/org/olat/selenium/page/qti/QTI21Page.java
+++ b/src/test/java/org/olat/selenium/page/qti/QTI21Page.java
@@ -160,6 +160,13 @@ public class QTI21Page {
 		return this;
 	}
 	
+	/**
+	 * Fill the gap entry based on its response id.
+	 * 
+	 * @param text The answer
+	 * @param responseId The identifier of the text entry
+	 * @return Itself
+	 */
 	public QTI21Page answerGapText(String text, String responseId) {
 		By gapBy = By.xpath("//span[contains(@class,'textEntryInteraction')]/input[@type='text'][contains(@name,'" + responseId + "')]");
 		WebElement gapEl = browser.findElement(gapBy);
@@ -168,6 +175,20 @@ public class QTI21Page {
 		return this;
 	}
 	
+	/**
+	 * 
+	 * @param text The answer
+	 * @param placeholder The placeholder to found the right gap
+	 * @return Itself
+	 */
+	public QTI21Page answerGapTextWithPlaceholder(String text, String placeholder) {
+		By gapBy = By.xpath("//span[contains(@class,'textEntryInteraction')]/input[@type='text'][@placeholder='" + placeholder + "']");
+		WebElement gapEl = browser.findElement(gapBy);
+		gapEl.clear();
+		gapEl.sendKeys(text);
+		return this;
+	}
+	
 	public QTI21Page answerMatch(String source, String target, boolean match) {
 		By matchBy = By.xpath("//div[contains(@class,'matchInteraction')]/table//tr[th/p[contains(text(),'" + source + "')]]/td[count(//div[contains(@class,'matchInteraction')]/table//tr/th[p[contains(text(),'" + target + "')]]/preceding-sibling::th)]/input");
 		WebElement matchEl = browser.findElement(matchBy);
-- 
GitLab