From 5eaf462569d780c2f07ba4bca2b999158b6fb245 Mon Sep 17 00:00:00 2001
From: srosse <stephane.rosse@frentix.com>
Date: Tue, 9 Jun 2020 07:20:55 +0200
Subject: [PATCH] no-jira: add selenium tests for expert rules, learn path

---
 .../org/olat/selenium/AssessmentTest.java     | 255 +++++++++------
 .../olat/selenium/CourseLearnPathTest.java    | 295 ++++++++++++++++++
 .../java/org/olat/selenium/CourseTest.java    |   1 +
 .../page/course/CoursePageFragment.java       |  47 ++-
 .../org/olat/selenium/page/qti/QTI21Page.java |  17 +-
 .../course_certificates_exrules.zip           | Bin 0 -> 27733 bytes
 6 files changed, 509 insertions(+), 106 deletions(-)
 create mode 100644 src/test/java/org/olat/selenium/CourseLearnPathTest.java
 create mode 100644 src/test/java/org/olat/test/file_resources/course_certificates_exrules.zip

diff --git a/src/test/java/org/olat/selenium/AssessmentTest.java b/src/test/java/org/olat/selenium/AssessmentTest.java
index 8ba2b269c9e..38ffea5ff45 100644
--- a/src/test/java/org/olat/selenium/AssessmentTest.java
+++ b/src/test/java/org/olat/selenium/AssessmentTest.java
@@ -34,7 +34,7 @@ import org.jboss.arquillian.test.api.ArquillianResource;
 import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.olat.course.learningpath.FullyAssessedTrigger;
+import org.olat.repository.RepositoryEntryStatusEnum;
 import org.olat.repository.model.SingleRoleRepositoryEntrySecurity.Role;
 import org.olat.selenium.page.LoginPage;
 import org.olat.selenium.page.NavigationPage;
@@ -55,6 +55,8 @@ import org.olat.selenium.page.course.MembersPage;
 import org.olat.selenium.page.graphene.OOGraphene;
 import org.olat.selenium.page.group.GroupPage;
 import org.olat.selenium.page.qti.QTI12Page;
+import org.olat.selenium.page.qti.QTI21Page;
+import org.olat.selenium.page.repository.RepositoryEditDescriptionPage;
 import org.olat.selenium.page.repository.ScormPage;
 import org.olat.selenium.page.repository.UserAccess;
 import org.olat.selenium.page.user.UserToolsPage;
@@ -807,6 +809,154 @@ public class AssessmentTest extends Deployments {
 			.selectStatementSegment()
 			.assertOnCourseDetails(testNodeTitle, true);
 	}
+
+	/**
+	 * This tests a course with cascading rules and expert rules
+	 * to calculate if the course is passed and generate a
+	 * certificate. 
+	 * 
+	 * @param loginPage
+	 */
+	@Test
+	@RunAsClient
+	public void certificatesGeneratedWithCascadingRules()
+	throws IOException, URISyntaxException {
+		
+		UserVO author = new UserRestClient(deploymentUrl).createRandomAuthor();
+		UserVO participant1 = new UserRestClient(deploymentUrl).createRandomUser("Ryomou");
+		UserVO participant2 = new UserRestClient(deploymentUrl).createRandomUser("Rei");
+
+		LoginPage loginPage = LoginPage.load(browser, deploymentUrl);
+		loginPage.loginAs(author.getLogin(), author.getPassword());
+		
+		URL zipUrl = JunitTestHelper.class.getResource("file_resources/course_certificates_exrules.zip");
+		File zipFile = new File(zipUrl.toURI());
+		//go the authoring environment to import our course
+		String zipTitle = "Certif - " + UUID.randomUUID();
+		NavigationPage navBar = NavigationPage.load(browser);
+		navBar
+			.openAuthoringEnvironment()
+			.uploadResource(zipTitle, zipFile);
+		
+		// publish the course
+		new RepositoryEditDescriptionPage(browser)
+			.clickToolbarBack();
+		CoursePageFragment course = CoursePageFragment.getCourse(browser)
+				.edit()
+				.autoPublish();
+		
+		// add a participant
+		MembersPage members = course
+			.members();
+		members
+			.importMembers()
+			.setMembers(participant1, participant2)
+			.nextUsers()
+			.nextOverview()
+			.nextPermissions()
+			.finish();
+		members
+			.clickToolbarBack();
+		
+		course
+			.settings()
+			.accessConfiguration()
+			.setUserAccess(UserAccess.registred)
+			.save()
+			.clickToolbarBack();
+		
+		course
+			.changeStatus(RepositoryEntryStatusEnum.published);
+	
+		//log out
+		new UserToolsPage(browser)
+			.logout();
+		
+		// participant log in and go directly to the first test
+		LoginPage participantLoginPage = LoginPage.load(browser, deploymentUrl);
+		
+		participantLoginPage
+			.loginAs(participant1.getLogin(), participant1.getPassword())
+			.resume();
+		
+		//open the course
+		NavigationPage participantNavBar = NavigationPage.load(browser);
+		participantNavBar
+			.openMyCourses()
+			.select(zipTitle);
+		
+		//go to the test
+		CoursePageFragment certificationCourse = new CoursePageFragment(browser);
+		certificationCourse
+			.clickTree()
+			.assertWithTitleSelected("Test 1");
+		//pass the test
+		QTI21Page.getQTI21Page(browser)
+			.passE4()
+			.assertOnCourseAssessmentTestScore(4);
+		
+		OOGraphene.waitingALittleLonger();
+		
+		//open the efficiency statements
+		String certificateTitle = "Certificates" + zipTitle;
+		UserToolsPage participantUserTools = new UserToolsPage(browser);
+		participantUserTools
+			.openUserToolsMenu()
+			.openMyEfficiencyStatement()
+			.assertOnEfficiencyStatmentPage()
+			.assertOnCertificateAndStatements(certificateTitle)
+			.selectStatement(certificateTitle)
+			.selectStatementSegment()
+			.assertOnCourseDetails("CertificatesCert", true)
+			.assertOnCourseDetails("Struktur 1", true)
+			.assertOnCourseDetails("Test 1", true);
+		
+		//log out
+		new UserToolsPage(browser)
+			.logout();
+		
+		
+		// participant 2 log in and go directly to the second test
+		LoginPage participant2LoginPage = LoginPage.load(browser, deploymentUrl);
+		
+		participant2LoginPage
+			.loginAs(participant2.getLogin(), participant2.getPassword())
+			.resume();
+		
+		//open the course
+		NavigationPage participant2NavBar = NavigationPage.load(browser);
+		participant2NavBar
+			.openMyCourses()
+			.select(zipTitle);
+		
+		//go to the test
+		CoursePageFragment certification2Course = new CoursePageFragment(browser);
+		certification2Course
+			.clickTree()
+			.selectWithTitle("Struktur 3")
+			.assertWithTitleSelected("Struktur 3")
+			.assertWithTitle("Test 3")
+			.selectWithTitle("Test 3");
+		//pass the test
+		QTI21Page.getQTI21Page(browser)
+			.passE4()
+			.assertOnCourseAssessmentTestScore(4);
+		
+		OOGraphene.waitingALittleLonger();
+		
+		//open the efficiency statements
+		UserToolsPage participant2UserTools = new UserToolsPage(browser);
+		participant2UserTools
+			.openUserToolsMenu()
+			.openMyEfficiencyStatement()
+			.assertOnEfficiencyStatmentPage()
+			.assertOnCertificateAndStatements(certificateTitle)
+			.selectStatement(certificateTitle)
+			.selectStatementSegment()
+			.assertOnCourseDetails("CertificatesCert", true)
+			.assertOnCourseDetails("Struktur 3", true)
+			.assertOnCourseDetails("Test 3", true);
+	}
 	
 	/**
 	 * An author create a course with an assessment course element with
@@ -2114,107 +2264,4 @@ public class AssessmentTest extends Deployments {
 			.openSolutions()
 			.assertSolution("solution_1.txt");
 	}
-	
-	/**
-	 * This is a degenerated form of task but the case exists.
-	 * An author creates a course (learn path) with a task course
-	 * element. The task is configured to only show the solution,
-	 * and the course element to be passed if the task is done. In
-	 * this case, the participant only need to see the solution to
-	 * get the node done.
-	 * 
-	 * @param participantBrowser Browser of the participant
-	 * @throws IOException
-	 * @throws URISyntaxException
-	 */
-	@Test
-	@RunAsClient
-	public void taskLearnPathSolutionsOnly(@Drone @User WebDriver participantBrowser)
-	throws IOException, URISyntaxException {
-		
-		UserVO author = new UserRestClient(deploymentUrl).createRandomAuthor();
-		UserVO participant = new UserRestClient(deploymentUrl).createRandomUser("kanu");
-		
-		LoginPage authorLoginPage = LoginPage.load(browser, deploymentUrl);
-		authorLoginPage.loginAs(author.getLogin(), author.getPassword());
-		
-		//create a course
-		String courseTitle = "Course-with-auto-task-" + UUID.randomUUID();
-		NavigationPage navBar = NavigationPage.load(browser);
-		navBar
-			.openAuthoringEnvironment()
-			.createCourse(courseTitle, true)
-			.clickToolbarBack();
-
-		//create a course element of type Test with the test that we create above
-		String gtaNodeTitle = "Solution 1";
-		CourseEditorPageFragment courseEditor = CoursePageFragment.getCourse(browser)
-			.edit();
-		courseEditor
-			.createNode("ita")
-			.nodeTitle(gtaNodeTitle);
-		
-		courseEditor
-			.selectTabLearnPath()
-			.setCompletionCriterion(FullyAssessedTrigger.statusDone)
-			.save();
-		
-		GroupTaskConfigurationPage gtaConfig = new GroupTaskConfigurationPage(browser);
-		gtaConfig
-			.selectWorkflow()
-			.enableAssignment(false)
-			.enableSubmission(false)
-			.enableReview(false)
-			.enableGrading(false)
-			.saveWorkflow();
-		
-		URL solutionUrl = JunitTestHelper.class.getResource("file_resources/solution_1.txt");
-		File solutionFile = new File(solutionUrl.toURI());
-		gtaConfig
-			.selectSolution()
-			.uploadSolution("A possible solution", solutionFile);
-		
-		courseEditor
-			.publish()
-			.quickPublish(UserAccess.membersOnly);
-		
-
-		MembersPage membersPage = courseEditor
-			.clickToolbarBack()
-			.members();
-		
-		membersPage
-			.importMembers()
-			.setMembers(participant)
-			.nextUsers()
-			.nextOverview()
-			.nextPermissions()
-			.finish();
-		
-		//Participant log in
-		LoginPage participantLoginPage = LoginPage.load(participantBrowser, deploymentUrl);
-		participantLoginPage
-			.loginAs(participant)
-			.resume();
-		
-		//open the course
-		NavigationPage participantNavBar = NavigationPage.load(participantBrowser);
-		participantNavBar
-			.openMyCourses()
-			.select(courseTitle);
-		
-		//go to the group task
-		CoursePageFragment participantCourse = new CoursePageFragment(participantBrowser);
-		participantCourse
-			.clickTree()
-			.selectWithTitle(gtaNodeTitle);
-		
-		GroupTaskPage participantTask = new GroupTaskPage(participantBrowser);
-		participantTask
-			.openSolutions()
-			.assertSolution("solution_1.txt");
-		// seeing the solution got the job done
-		participantCourse
-			.assertOnLearnPathNodeDone(gtaNodeTitle);
-	}
 }
diff --git a/src/test/java/org/olat/selenium/CourseLearnPathTest.java b/src/test/java/org/olat/selenium/CourseLearnPathTest.java
new file mode 100644
index 00000000000..adcc2e7b104
--- /dev/null
+++ b/src/test/java/org/olat/selenium/CourseLearnPathTest.java
@@ -0,0 +1,295 @@
+/**
+ * <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;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.UUID;
+
+import org.jboss.arquillian.container.test.api.RunAsClient;
+import org.jboss.arquillian.drone.api.annotation.Drone;
+import org.jboss.arquillian.junit.Arquillian;
+import org.jboss.arquillian.test.api.ArquillianResource;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.olat.course.learningpath.FullyAssessedTrigger;
+import org.olat.selenium.page.LoginPage;
+import org.olat.selenium.page.NavigationPage;
+import org.olat.selenium.page.User;
+import org.olat.selenium.page.core.MenuTreePageFragment;
+import org.olat.selenium.page.course.CourseEditorPageFragment;
+import org.olat.selenium.page.course.CoursePageFragment;
+import org.olat.selenium.page.course.GroupTaskConfigurationPage;
+import org.olat.selenium.page.course.GroupTaskPage;
+import org.olat.selenium.page.course.MembersPage;
+import org.olat.selenium.page.course.SinglePageConfigurationPage;
+import org.olat.selenium.page.repository.UserAccess;
+import org.olat.test.JunitTestHelper;
+import org.olat.test.rest.UserRestClient;
+import org.olat.user.restapi.UserVO;
+import org.openqa.selenium.WebDriver;
+
+/**
+ * Test specifically features of the course in learn path mode.
+ * 
+ * Initial date: 8 juin 2020<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+@RunWith(Arquillian.class)
+public class CourseLearnPathTest extends Deployments {
+	
+	@Drone
+	private WebDriver browser;
+	@ArquillianResource
+	private URL deploymentUrl;
+	
+
+	/**
+	 * This is a course with learn path, three course elements,
+	 * two to confirm, one to visit. The author creates the course
+	 * and a user play it until 100% done.
+	 * 
+	 * @param participantBrowser Browser of the participant
+	 * @throws IOException
+	 * @throws URISyntaxException
+	 */
+	@Test
+	@RunAsClient
+	public void learnPathConfirmationAndVisitedFlow(@Drone @User WebDriver participantBrowser)
+	throws IOException, URISyntaxException {
+		
+		UserVO author = new UserRestClient(deploymentUrl).createRandomAuthor();
+		UserVO participant = new UserRestClient(deploymentUrl).createRandomUser("kanu");
+		
+		LoginPage authorLoginPage = LoginPage.load(browser, deploymentUrl);
+		authorLoginPage.loginAs(author.getLogin(), author.getPassword());
+		
+		//create a course
+		String courseTitle = "Course-with-auto-task-" + UUID.randomUUID();
+		NavigationPage navBar = NavigationPage.load(browser);
+		navBar
+			.openAuthoringEnvironment()
+			.createCourse(courseTitle, true)
+			.clickToolbarBack();
+
+		//create a course element of type single page
+		String firstNodeTitle = "First page";
+		CourseEditorPageFragment courseEditor = CoursePageFragment.getCourse(browser)
+			.edit();
+		courseEditor
+			.createNode("sp")
+			.nodeTitle(firstNodeTitle);
+		
+		String content = "A new single page with some content";
+		SinglePageConfigurationPage spConfiguration = new SinglePageConfigurationPage(browser);
+		spConfiguration
+			.selectConfiguration()
+			.newDefaultPage(content)
+			.assertOnPreview();
+		
+		courseEditor
+			.selectTabLearnPath()
+			.setCompletionCriterion(FullyAssessedTrigger.confirmed)
+			.save();
+		
+		// create a second element, a forum
+		String forumNodeTitle = "Forum discussion";
+		courseEditor
+			.createNode("fo")
+			.nodeTitle(forumNodeTitle)
+			.selectTabLearnPath()
+			.setCompletionCriterion(FullyAssessedTrigger.confirmed)
+			.save();
+		
+		// create a third element, an info message
+		String infosNodeTitle = "Informations";
+		courseEditor
+			.createNode("info")
+			.nodeTitle(infosNodeTitle)
+			.selectTabLearnPath()
+			.setCompletionCriterion(FullyAssessedTrigger.nodeVisited)
+			.save();
+		
+		courseEditor
+			.autoPublish()
+			.publish()
+			.members()
+			.importMembers()
+			.setMembers(participant)
+			.nextUsers()
+			.nextOverview()
+			.nextPermissions()
+			.finish();
+		
+		//Participant log in
+		LoginPage participantLoginPage = LoginPage.load(participantBrowser, deploymentUrl);
+		participantLoginPage
+			.loginAs(participant)
+			.resume();
+		
+		//open the course
+		NavigationPage participantNavBar = NavigationPage.load(participantBrowser);
+		participantNavBar
+			.openMyCourses()
+			.select(courseTitle);
+		
+		//go to the group task
+		CoursePageFragment participantCourse = new CoursePageFragment(participantBrowser);
+		MenuTreePageFragment menuTree = participantCourse
+			.clickTree()
+			.selectWithTitle(firstNodeTitle);
+		participantCourse
+			.assertOnLearnPathNodeReady(firstNodeTitle)
+			.confirmNode()
+			.assertOnLearnPathNodeDone(firstNodeTitle)
+			.assertOnLearnPathNodeInProgress(courseTitle)
+			.assertOnLearnPathNodeReady(forumNodeTitle)
+			.assertOnLearnPathNodeNotAccessible(infosNodeTitle);
+		
+		// confirm second node
+		menuTree
+			.selectWithTitle(forumNodeTitle)
+			.assertWithTitleSelected(forumNodeTitle);
+		participantCourse
+			.confirmNode()
+			.assertOnLearnPathNodeDone(forumNodeTitle)
+			.assertOnLearnPathNodeInProgress(courseTitle)
+			.assertOnLearnPathNodeReady(infosNodeTitle);
+
+		// see the third node
+		menuTree
+			.selectWithTitle(infosNodeTitle)
+			.assertWithTitleSelected(infosNodeTitle);
+		participantCourse
+			.assertOnLearnPathNodeDone(infosNodeTitle)
+			.assertOnLearnPathNodeDone(firstNodeTitle)
+			.assertOnLearnPathNodeDone(forumNodeTitle)
+			.assertOnLearnPathNodeDone(courseTitle)
+			.assertOnLearnPathPercent(100);
+	}
+	
+
+	/**
+	 * This is a degenerated form of task but the case exists.
+	 * An author creates a course (learn path) with a task course
+	 * element. The task is configured to only show the solution,
+	 * and the course element to be passed if the task is done. In
+	 * this case, the participant only need to see the solution to
+	 * get the node done.
+	 * 
+	 * @param participantBrowser Browser of the participant
+	 * @throws IOException
+	 * @throws URISyntaxException
+	 */
+	@Test
+	@RunAsClient
+	public void taskLearnPathSolutionsOnly(@Drone @User WebDriver participantBrowser)
+	throws IOException, URISyntaxException {
+		
+		UserVO author = new UserRestClient(deploymentUrl).createRandomAuthor();
+		UserVO participant = new UserRestClient(deploymentUrl).createRandomUser("kanu");
+		
+		LoginPage authorLoginPage = LoginPage.load(browser, deploymentUrl);
+		authorLoginPage.loginAs(author.getLogin(), author.getPassword());
+		
+		//create a course
+		String courseTitle = "Course-with-auto-task-" + UUID.randomUUID();
+		NavigationPage navBar = NavigationPage.load(browser);
+		navBar
+			.openAuthoringEnvironment()
+			.createCourse(courseTitle, true)
+			.clickToolbarBack();
+
+		//create a course element of type Test with the test that we create above
+		String gtaNodeTitle = "Solution 1";
+		CourseEditorPageFragment courseEditor = CoursePageFragment.getCourse(browser)
+			.edit();
+		courseEditor
+			.createNode("ita")
+			.nodeTitle(gtaNodeTitle);
+		
+		courseEditor
+			.selectTabLearnPath()
+			.setCompletionCriterion(FullyAssessedTrigger.statusDone)
+			.save();
+		
+		GroupTaskConfigurationPage gtaConfig = new GroupTaskConfigurationPage(browser);
+		gtaConfig
+			.selectWorkflow()
+			.enableAssignment(false)
+			.enableSubmission(false)
+			.enableReview(false)
+			.enableGrading(false)
+			.saveWorkflow();
+		
+		URL solutionUrl = JunitTestHelper.class.getResource("file_resources/solution_1.txt");
+		File solutionFile = new File(solutionUrl.toURI());
+		gtaConfig
+			.selectSolution()
+			.uploadSolution("A possible solution", solutionFile);
+		
+		courseEditor
+			.publish()
+			.quickPublish(UserAccess.membersOnly);
+		
+
+		MembersPage membersPage = courseEditor
+			.clickToolbarBack()
+			.members();
+		
+		membersPage
+			.importMembers()
+			.setMembers(participant)
+			.nextUsers()
+			.nextOverview()
+			.nextPermissions()
+			.finish();
+		
+		//Participant log in
+		LoginPage participantLoginPage = LoginPage.load(participantBrowser, deploymentUrl);
+		participantLoginPage
+			.loginAs(participant)
+			.resume();
+		
+		//open the course
+		NavigationPage participantNavBar = NavigationPage.load(participantBrowser);
+		participantNavBar
+			.openMyCourses()
+			.select(courseTitle);
+		
+		//go to the group task
+		CoursePageFragment participantCourse = new CoursePageFragment(participantBrowser);
+		participantCourse
+			.clickTree()
+			.selectWithTitle(gtaNodeTitle);
+		
+		GroupTaskPage participantTask = new GroupTaskPage(participantBrowser);
+		participantTask
+			.openSolutions()
+			.assertSolution("solution_1.txt");
+		// seeing the solution got the job done
+		participantCourse
+			.assertOnLearnPathNodeDone(gtaNodeTitle);
+	}
+
+}
diff --git a/src/test/java/org/olat/selenium/CourseTest.java b/src/test/java/org/olat/selenium/CourseTest.java
index d2d6404c62d..b107f1c54c6 100644
--- a/src/test/java/org/olat/selenium/CourseTest.java
+++ b/src/test/java/org/olat/selenium/CourseTest.java
@@ -1738,4 +1738,5 @@ public class CourseTest extends Deployments {
 			.uploadResource(zipTitle, zipFile)
 			.assertOnResourceType();
 	}
+	
 }
diff --git a/src/test/java/org/olat/selenium/page/course/CoursePageFragment.java b/src/test/java/org/olat/selenium/page/course/CoursePageFragment.java
index 5700988b382..d229a6416a0 100644
--- a/src/test/java/org/olat/selenium/page/course/CoursePageFragment.java
+++ b/src/test/java/org/olat/selenium/page/course/CoursePageFragment.java
@@ -93,11 +93,36 @@ public class CoursePageFragment {
 	}
 	
 	public CoursePageFragment assertOnLearnPathNodeDone(String nodeTitle) {
-		By nodeDoneBy = By.xpath("//div[contains(@class,'o_lp_tree')]//span[contains(@class,'o_tree_l1')]/a[i[contains(@class,'o_lp_done')]][span[text()[contains(.,'" + nodeTitle + "')]]]");
+		return assertOnLearnPathNodeStatus(nodeTitle, "o_lp_done");
+	}
+	
+	public CoursePageFragment assertOnLearnPathNodeReady(String nodeTitle) {
+		return assertOnLearnPathNodeStatus(nodeTitle, "o_lp_ready");
+	}
+	
+	public CoursePageFragment assertOnLearnPathNodeInProgress(String nodeTitle) {
+		return assertOnLearnPathNodeStatus(nodeTitle, "o_lp_in_progress");
+	}
+	
+	public CoursePageFragment assertOnLearnPathNodeNotAccessible(String nodeTitle) {
+		return assertOnLearnPathNodeStatus(nodeTitle, "o_lp_not_accessible");
+	}
+	
+	private CoursePageFragment assertOnLearnPathNodeStatus(String nodeTitle, String statusCssClass) {
+		if(nodeTitle.length() > 20) {
+			nodeTitle = nodeTitle.substring(0, 20);
+		}
+		By nodeDoneBy = By.xpath("//div[contains(@class,'o_lp_tree')]//span[contains(@class,'o_tree_l')]/a[i[contains(@class,'" + statusCssClass + "')]][span[text()[contains(.,'" + nodeTitle + "')]]]");
 		OOGraphene.waitElement(nodeDoneBy, browser);
 		return this;
 	}
 	
+	public CoursePageFragment assertOnLearnPathPercent(int percent) {
+		By percentageBy = By.xpath("//span[contains(@class,'o_progress')]//span[contains(@class,'percentage')]//span[text()[contains(.,'" + percent + "%')]]");
+		OOGraphene.waitElement(percentageBy, browser);
+		return this;
+	}
+	
 	/**
 	 * Assert if the password field is displayed.
 	 * @return
@@ -148,6 +173,21 @@ public class CoursePageFragment {
 		return menuTree.selectRoot();
 	}
 	
+	/**
+	 * 
+	 * @return
+	 */
+	public CoursePageFragment confirmNode() {
+		By confirmationBy = By.cssSelector("div.o_course_pagination div.o_confirm a.btn");
+		OOGraphene.waitElement(confirmationBy, browser);
+		browser.findElement(confirmationBy).click();
+		OOGraphene.waitBusy(browser);
+
+		By confirmedBy = By.cssSelector("div.o_course_pagination div.o_confirm a.btn.o_course_pagination_status_done");
+		OOGraphene.waitElement(confirmedBy, browser);
+		return this;
+	}
+	
 	/**
 	 * Open the tools drop-down
 	 * @return
@@ -276,6 +316,11 @@ public class CoursePageFragment {
 		return new BookingPage(browser);
 	}
 	
+	/**
+	 * Set the course status to published.
+	 * 
+	 * @return Itself
+	 */
 	public CoursePageFragment publish() {
 		return changeStatus(RepositoryEntryStatusEnum.published);
 	}
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 56be8baa524..906042a87b1 100644
--- a/src/test/java/org/olat/selenium/page/qti/QTI21Page.java
+++ b/src/test/java/org/olat/selenium/page/qti/QTI21Page.java
@@ -117,6 +117,21 @@ public class QTI21Page {
 		return this;
 	}
 	
+	public QTI21Page passE4() {
+		start()
+			.answerSingleChoiceWithParagraph("Correct answer")
+			.saveAnswer()
+			.answerMultipleChoice("Correct answer", "The answer is correct")
+			.saveAnswer()
+			.answerCorrectKPrim("This answer", "Plus answer")
+			.answerIncorrectKPrim("Not answer", "Minus answer")
+			.saveAnswer()
+			.answerGapText("not", "qtiworks_response_oofibc4c14bfe94a41861fe19c70091182_5_1_RESPONSE_1")
+			.saveAnswer()
+			.endTest();
+		return this;
+	}
+	
 	/**
 	 * Check the answer of a single choice.
 	 * @param answer The answer
@@ -807,7 +822,7 @@ public class QTI21Page {
 	
 	public QTI21Page closeTest() {
 		By closeBy = By.cssSelector("a.o_sel_close_test");
-		OOGraphene.waitElement(closeBy, 5, browser);
+		OOGraphene.waitElement(closeBy, browser);
 		browser.findElement(closeBy).click();
 		OOGraphene.waitBusy(browser);
 		confirm();
diff --git a/src/test/java/org/olat/test/file_resources/course_certificates_exrules.zip b/src/test/java/org/olat/test/file_resources/course_certificates_exrules.zip
new file mode 100644
index 0000000000000000000000000000000000000000..988bde8a23373ac1b09c2bf06d3730f68769d507
GIT binary patch
literal 27733
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

literal 0
HcmV?d00001

-- 
GitLab