From 1137ee556c70224f50d6fb65698e21db3c5479a8 Mon Sep 17 00:00:00 2001
From: srosse <none@none>
Date: Thu, 9 Mar 2017 11:12:57 +0100
Subject: [PATCH] OO-2602: skip close step if there are no review and no
 feedbacks, implement some selenium tests to secure the different flows for
 different settings

---
 pom.xml                                       |   2 +-
 .../olat/ims/qti21/AssessmentTestSession.java |   2 +
 .../ui/AssessmentTestDisplayController.java   | 125 ++++++-
 .../olat/ims/qti21/ui/_content/suspended.html |  15 +-
 .../AssessmentObjectComponentRenderer.java    |   2 +-
 .../components/AssessmentRenderFunctions.java |  16 +
 .../components/AssessmentTestComponent.java   |   5 +-
 .../AssessmentTestComponentRenderer.java      |  25 +-
 .../org/olat/selenium/AssessmentTest.java     |  23 +-
 .../java/org/olat/selenium/ImsQTI21Test.java  | 344 +++++++++++++++++-
 src/test/java/org/olat/selenium/UserTest.java |   7 +-
 .../org/olat/selenium/page/qti/QTI21Page.java |  88 ++++-
 .../qti21/test_parts_without_feedbacks.zip    | Bin 0 -> 5996 bytes
 .../file_resources/qti21/test_time_limits.zip | Bin 0 -> 5406 bytes
 .../qti21/test_with_feedbacks.zip             | Bin 0 -> 4401 bytes
 .../test_with_parts_and_test_feedbacks.zip    | Bin 0 -> 6221 bytes
 .../qti21/test_without_feedbacks.zip          | Bin 0 -> 3599 bytes
 17 files changed, 580 insertions(+), 74 deletions(-)
 create mode 100644 src/test/java/org/olat/test/file_resources/qti21/test_parts_without_feedbacks.zip
 create mode 100644 src/test/java/org/olat/test/file_resources/qti21/test_time_limits.zip
 create mode 100644 src/test/java/org/olat/test/file_resources/qti21/test_with_feedbacks.zip
 create mode 100644 src/test/java/org/olat/test/file_resources/qti21/test_with_parts_and_test_feedbacks.zip
 create mode 100644 src/test/java/org/olat/test/file_resources/qti21/test_without_feedbacks.zip

diff --git a/pom.xml b/pom.xml
index 187f8a37c3f..1e1fc74575d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1997,7 +1997,7 @@
 		<dependency>
 			<groupId>org.openolat.imscp</groupId>
 			<artifactId>manifest</artifactId>
-			<version>1.3-SNAPSHOT</version>
+			<version>1.3.0</version>
 		</dependency>
 		<dependency>
 			<groupId>rome</groupId>
diff --git a/src/main/java/org/olat/ims/qti21/AssessmentTestSession.java b/src/main/java/org/olat/ims/qti21/AssessmentTestSession.java
index 5cd84eb67c3..c88f11fb2d9 100644
--- a/src/main/java/org/olat/ims/qti21/AssessmentTestSession.java
+++ b/src/main/java/org/olat/ims/qti21/AssessmentTestSession.java
@@ -72,6 +72,8 @@ public interface AssessmentTestSession extends CreateInfo, ModifiedInfo {
 	
 	public boolean isExploded();
 	
+	public void setExploded(boolean exploded);
+	
 	public String getStorage();
 	
 	
diff --git a/src/main/java/org/olat/ims/qti21/ui/AssessmentTestDisplayController.java b/src/main/java/org/olat/ims/qti21/ui/AssessmentTestDisplayController.java
index d2617ff2973..f3366585906 100644
--- a/src/main/java/org/olat/ims/qti21/ui/AssessmentTestDisplayController.java
+++ b/src/main/java/org/olat/ims/qti21/ui/AssessmentTestDisplayController.java
@@ -19,6 +19,8 @@
  */
 package org.olat.ims.qti21.ui;
 
+import static org.olat.ims.qti21.ui.components.AssessmentRenderFunctions.testFeedbackVisible;
+
 import java.io.File;
 import java.math.BigDecimal;
 import java.net.URI;
@@ -109,6 +111,8 @@ import uk.ac.ed.ph.jqtiplus.node.result.TestResult;
 import uk.ac.ed.ph.jqtiplus.node.test.AssessmentTest;
 import uk.ac.ed.ph.jqtiplus.node.test.NavigationMode;
 import uk.ac.ed.ph.jqtiplus.node.test.SubmissionMode;
+import uk.ac.ed.ph.jqtiplus.node.test.TestFeedback;
+import uk.ac.ed.ph.jqtiplus.node.test.TestFeedbackAccess;
 import uk.ac.ed.ph.jqtiplus.node.test.TestPart;
 import uk.ac.ed.ph.jqtiplus.notification.NotificationLevel;
 import uk.ac.ed.ph.jqtiplus.notification.NotificationRecorder;
@@ -658,7 +662,7 @@ public class AssessmentTestDisplayController extends BasicController implements
             TestPlanNodeKey itemNodeKey = testSessionState.getCurrentItemKey();
             if(itemNodeKey != null) {
 				TestPlanNode currentItemNode = testSessionState.getTestPlan().getNode(itemNodeKey);
-	        	boolean hasFeedbacks = qtiWorksCtrl.willShowSomeFeedbacks(currentItemNode);
+	        	boolean hasFeedbacks = qtiWorksCtrl.willShowSomeAssessmentItemFeedbacks(currentItemNode);
 	        	//allow skipping
 	        	if(!hasFeedbacks) {
 	        		processNextItem(ureq);
@@ -999,6 +1003,40 @@ public class AssessmentTestDisplayController extends BasicController implements
         if(nextTestPart == null) {
         	candidateSession = qtiService.finishTestSession(candidateSession, testSessionState, assessmentResult,
         			requestTimestamp, getDigitalSignatureOptions(), getIdentity());
+        	if(!qtiWorksCtrl.willShowSomeAssessmentTestFeedbacks()) {
+        		//need feedback, no more parts, quickly exit
+        		try {
+        			//end current test part
+                    testSessionController.enterNextAvailableTestPart(requestTimestamp);
+                } catch (final QtiCandidateStateException e) {
+                    candidateAuditLogger.logAndThrowCandidateException(candidateSession, CandidateExceptionReason.CANNOT_ADVANCE_TEST_PART, e);
+                    logError("CANNOT_ADVANCE_TEST_PART", e);
+                    return;
+                } catch (final RuntimeException e) {
+                    candidateAuditLogger.logAndThrowCandidateException(candidateSession, CandidateExceptionReason.CANNOT_ADVANCE_TEST_PART, e);
+                    logError("RuntimeException", e);
+                    return;// handleExplosion(e, candidateSession);
+                }
+
+        		//exit the test
+                NotificationRecorder notificationRecorder = new NotificationRecorder(NotificationLevel.INFO);
+                CandidateTestEventType eventType = CandidateTestEventType.EXIT_TEST;
+                testSessionController.exitTest(requestTimestamp);
+                candidateSession.setTerminationTime(requestTimestamp);
+                candidateSession = qtiService.updateAssessmentTestSession(candidateSession);
+        		
+        		/* Record and log event */
+                final CandidateEvent candidateTestEvent = qtiService.recordCandidateTestEvent(candidateSession, testEntry, entry,
+                       eventType, testSessionState, notificationRecorder);
+                candidateAuditLogger.logCandidateEvent(candidateTestEvent);
+                this.lastEvent = candidateTestEvent;
+
+                qtiWorksCtrl.updateStatusAndResults(ureq);
+        		doExitTest(ureq);
+        	}
+        } else if(!qtiWorksCtrl.willShowSomeTestPartFeedbacks()) {
+        	//no feedback, go to the next part
+        	processAdvanceTestPart(ureq);
         }
 	}
 	
@@ -1366,10 +1404,14 @@ public class AssessmentTestDisplayController extends BasicController implements
 
 		BadResourceException ex = resolvedAssessmentTest.getTestLookup().getBadResourceException();
 		if(ex instanceof QtiXmlInterpretationException) {
-			QtiXmlInterpretationException exml = (QtiXmlInterpretationException)ex;
-			System.out.println(exml.getInterpretationFailureReason());
-			for(QtiModelBuildingError err :exml.getQtiModelBuildingErrors()) {
-				System.out.println(err);
+			try {//try to log some informations
+				QtiXmlInterpretationException exml = (QtiXmlInterpretationException)ex;
+				logError(exml.getInterpretationFailureReason().toString(), null);
+				for(QtiModelBuildingError err :exml.getQtiModelBuildingErrors()) {
+					logError(err.toString(), null);
+				}
+			} catch (Exception e) {
+				logError("", e);
 			}
 		}
 		
@@ -1525,7 +1567,7 @@ public class AssessmentTestDisplayController extends BasicController implements
 			updateStatusAndResults(ureq);
 		}
 		
-		public boolean willShowSomeFeedbacks(TestPlanNode itemNode) {
+		public boolean willShowSomeAssessmentItemFeedbacks(TestPlanNode itemNode) {
 			if(itemNode == null || testSessionController == null
 					|| testSessionController.getTestSessionState().isExited()
 					|| testSessionController.getTestSessionState().isEnded()) {
@@ -1534,6 +1576,77 @@ public class AssessmentTestDisplayController extends BasicController implements
 			
 			return qtiEl.getComponent().willShowFeedbacks(itemNode);
 		}
+		
+		/**
+		 * 
+		 * @return
+		 */
+		public boolean willShowSomeAssessmentTestFeedbacks() {
+			if(testSessionController == null
+					|| testSessionController.getTestSessionState().isExited()
+					|| testSessionController.getTestSessionState().isEnded()) {
+				return true;
+			}
+			
+			TestSessionState testSessionState = testSessionController.getTestSessionState();
+			TestPlanNodeKey currentTestPartNodeKey = testSessionState.getCurrentTestPartKey();
+			TestPlanNode currentTestPlanNode = testSessionState.getTestPlan().getNode(currentTestPartNodeKey);
+			boolean hasReviewableItems = currentTestPlanNode.searchDescendants(TestNodeType.ASSESSMENT_ITEM_REF)
+					.stream().anyMatch(itemNode
+							-> itemNode.getEffectiveItemSessionControl().isAllowReview()
+							|| itemNode.getEffectiveItemSessionControl().isShowFeedback());
+			if(hasReviewableItems) {
+				return true;
+			}
+			
+			//Show 'atEnd' test feedback f there's only 1 testPart
+			List<TestFeedback> testFeedbacks = qtiEl.getComponent().getAssessmentTest().getTestFeedbacks();
+			for(TestFeedback testFeedback:testFeedbacks) {
+				if(testFeedback.getTestFeedbackAccess() == TestFeedbackAccess.AT_END
+						&& testFeedbackVisible(testFeedback, testSessionController.getTestSessionState())) {
+					return true;
+				}
+			}
+			
+			return false;
+		}
+		
+		/**
+		 * 
+		 * Check if the current test part will show some test part feedback,
+		 * item feedback or item reviews.
+		 * 
+		 * @return
+		 */
+		public boolean willShowSomeTestPartFeedbacks() {
+			if(testSessionController == null
+					|| testSessionController.getTestSessionState().isExited()
+					|| testSessionController.getTestSessionState().isEnded()) {
+				return true;
+			}
+			
+			TestSessionState testSessionState = testSessionController.getTestSessionState();
+			TestPlanNodeKey currentTestPartNodeKey = testSessionState.getCurrentTestPartKey();
+			TestPlanNode currentTestPlanNode = testSessionState.getTestPlan().getNode(currentTestPartNodeKey);
+			boolean hasReviewableItems = currentTestPlanNode.searchDescendants(TestNodeType.ASSESSMENT_ITEM_REF)
+					.stream().anyMatch(itemNode
+							-> itemNode.getEffectiveItemSessionControl().isAllowReview()
+							|| itemNode.getEffectiveItemSessionControl().isShowFeedback());
+			if(hasReviewableItems) {
+				return true;
+			}
+
+			TestPart currentTestPart = testSessionController.getCurrentTestPart();
+			List<TestFeedback> testFeedbacks = currentTestPart.getTestFeedbacks();
+			for(TestFeedback testFeedback:testFeedbacks) {
+				if(testFeedback.getTestFeedbackAccess() == TestFeedbackAccess.AT_END
+						&& testFeedbackVisible(testFeedback, testSessionController.getTestSessionState())) {
+					return true;
+				}
+			}
+			
+			return false;
+		}
 
 		@Override
 		protected Identifier getResponseIdentifierFromUniqueId(String uniqueId) {
diff --git a/src/main/java/org/olat/ims/qti21/ui/_content/suspended.html b/src/main/java/org/olat/ims/qti21/ui/_content/suspended.html
index c0282a63625..bcd24f92b66 100644
--- a/src/main/java/org/olat/ims/qti21/ui/_content/suspended.html
+++ b/src/main/java/org/olat/ims/qti21/ui/_content/suspended.html
@@ -1,3 +1,16 @@
 <div id="o_qti_container">
 	<div class="o_important">$r.translate("assessment.test.suspended")</div>
-</div>
\ No newline at end of file
+</div>
+<script type="text/javascript">
+/* <![CDATA[ */
+jQuery(function() {
+	try {
+		if(!(typeof window.qti21TestTimer === "undefined")) {
+			window.qti21TestTimer.cancel();
+		}
+	} catch(e) {
+		if(window.console) console.log(e);
+	}
+});
+/* ]]> */
+</script>
\ No newline at end of file
diff --git a/src/main/java/org/olat/ims/qti21/ui/components/AssessmentObjectComponentRenderer.java b/src/main/java/org/olat/ims/qti21/ui/components/AssessmentObjectComponentRenderer.java
index c2124c674dd..dba3dc47be2 100644
--- a/src/main/java/org/olat/ims/qti21/ui/components/AssessmentObjectComponentRenderer.java
+++ b/src/main/java/org/olat/ims/qti21/ui/components/AssessmentObjectComponentRenderer.java
@@ -197,7 +197,7 @@ public abstract class AssessmentObjectComponentRenderer extends DefaultComponent
     }
 
     protected void renderTerminated(StringOutput sb, Translator translator) {
-		sb.append("<div class='o_info'>").append(translator.translate("terminated.msg")).append("</div>");
+		sb.append("<div class='o_info o_sel_assessment_test_terminated'>").append(translator.translate("terminated.msg")).append("</div>");
     }
 	
 	protected void renderItemStatus(StringOutput sb, ItemSessionState itemSessionState, RenderingRequest options, Translator translator) {
diff --git a/src/main/java/org/olat/ims/qti21/ui/components/AssessmentRenderFunctions.java b/src/main/java/org/olat/ims/qti21/ui/components/AssessmentRenderFunctions.java
index d6928f5fde1..fa42cc925c4 100644
--- a/src/main/java/org/olat/ims/qti21/ui/components/AssessmentRenderFunctions.java
+++ b/src/main/java/org/olat/ims/qti21/ui/components/AssessmentRenderFunctions.java
@@ -49,9 +49,11 @@ import uk.ac.ed.ph.jqtiplus.node.item.CorrectResponse;
 import uk.ac.ed.ph.jqtiplus.node.item.interaction.choice.Choice;
 import uk.ac.ed.ph.jqtiplus.node.item.response.declaration.ResponseDeclaration;
 import uk.ac.ed.ph.jqtiplus.node.item.template.declaration.TemplateDeclaration;
+import uk.ac.ed.ph.jqtiplus.node.test.TestFeedback;
 import uk.ac.ed.ph.jqtiplus.node.test.VisibilityMode;
 import uk.ac.ed.ph.jqtiplus.resolution.ResolvedAssessmentItem;
 import uk.ac.ed.ph.jqtiplus.state.ItemSessionState;
+import uk.ac.ed.ph.jqtiplus.state.TestSessionState;
 import uk.ac.ed.ph.jqtiplus.types.Identifier;
 import uk.ac.ed.ph.jqtiplus.types.ResponseData;
 import uk.ac.ed.ph.jqtiplus.types.StringResponseData;
@@ -623,4 +625,18 @@ public class AssessmentRenderFunctions {
 		String relativePath = component.relativePathTo(resolvedAssessmentItem);
 		return component.getMapperUri() + "/file?href=" + relativePath + (uri == null ? "" : uri);
 	}
+	
+	public static final boolean testFeedbackVisible(TestFeedback testFeedback, TestSessionState testSessionState) {
+		//<xsl:variable name="identifierMatch" select="boolean(qw:value-contains(qw:get-test-outcome-value(@outcomeIdentifier), @identifier))" as="xs:boolean"/>
+		Identifier outcomeIdentifier = testFeedback.getOutcomeIdentifier();
+		Value outcomeValue = testSessionState.getOutcomeValue(outcomeIdentifier);
+		boolean identifierMatch = valueContains(outcomeValue, testFeedback.getOutcomeValue());
+		//<xsl:if test="($identifierMatch and @showHide='show') or (not($identifierMatch) and @showHide='hide')">
+		if((identifierMatch && testFeedback.getVisibilityMode() == VisibilityMode.SHOW_IF_MATCH)
+				|| (!identifierMatch && testFeedback.getVisibilityMode() == VisibilityMode.HIDE_IF_MATCH)) {
+			return true;
+		}
+		return false;
+	}
+	
 }
\ No newline at end of file
diff --git a/src/main/java/org/olat/ims/qti21/ui/components/AssessmentTestComponent.java b/src/main/java/org/olat/ims/qti21/ui/components/AssessmentTestComponent.java
index 3e6fc43326a..cc0f9371546 100644
--- a/src/main/java/org/olat/ims/qti21/ui/components/AssessmentTestComponent.java
+++ b/src/main/java/org/olat/ims/qti21/ui/components/AssessmentTestComponent.java
@@ -163,9 +163,7 @@ public class AssessmentTestComponent extends AssessmentObjectComponent  {
 			if(!itemSessionState.isResponded()) {
 				return true;
 			}
-			if(!itemNode.getEffectiveItemSessionControl().isAllowSkipping()) {
-				return true;
-			}
+
 
 			List<Interaction> interactions = assessmentItem.getItemBody().findInteractions();
 			for(Interaction interaction:interactions) {
@@ -178,7 +176,6 @@ public class AssessmentTestComponent extends AssessmentObjectComponent  {
 			}
 
 			ItemProcessingContext itemContext = getTestSessionController().getItemProcessingContext(itemNode);
-			
 			if(assessmentItem.getItemBody().willShowFeedback(itemContext)) {
 				return true;
 			}
diff --git a/src/main/java/org/olat/ims/qti21/ui/components/AssessmentTestComponentRenderer.java b/src/main/java/org/olat/ims/qti21/ui/components/AssessmentTestComponentRenderer.java
index a4e3d8565de..7b582e7278b 100644
--- a/src/main/java/org/olat/ims/qti21/ui/components/AssessmentTestComponentRenderer.java
+++ b/src/main/java/org/olat/ims/qti21/ui/components/AssessmentTestComponentRenderer.java
@@ -20,7 +20,7 @@
 package org.olat.ims.qti21.ui.components;
 
 import static org.olat.ims.qti21.ui.components.AssessmentRenderFunctions.contentAsString;
-import static org.olat.ims.qti21.ui.components.AssessmentRenderFunctions.valueContains;
+import static org.olat.ims.qti21.ui.components.AssessmentRenderFunctions.testFeedbackVisible;
 
 import java.io.OutputStream;
 import java.io.OutputStreamWriter;
@@ -72,7 +72,6 @@ import uk.ac.ed.ph.jqtiplus.node.test.NavigationMode;
 import uk.ac.ed.ph.jqtiplus.node.test.TestFeedback;
 import uk.ac.ed.ph.jqtiplus.node.test.TestFeedbackAccess;
 import uk.ac.ed.ph.jqtiplus.node.test.TestPart;
-import uk.ac.ed.ph.jqtiplus.node.test.VisibilityMode;
 import uk.ac.ed.ph.jqtiplus.resolution.ResolvedAssessmentItem;
 import uk.ac.ed.ph.jqtiplus.running.TestSessionController;
 import uk.ac.ed.ph.jqtiplus.state.AssessmentSectionSessionState;
@@ -455,9 +454,9 @@ public class AssessmentTestComponentRenderer extends AssessmentObjectComponentRe
 		renderTestFeebacks(renderer, sb, currentTestPart.getTestFeedbacks(), component, TestFeedbackAccess.AT_END, ubu, translator);
 
 		//Show 'atEnd' test feedback f there's only 1 testPart
-		if(!component.hasMultipleTestParts()) {
-			renderTestFeebacks(renderer, sb, component.getAssessmentTest().getTestFeedbacks(), component, TestFeedbackAccess.AT_END, ubu, translator);
-		}
+		//if(!component.hasMultipleTestParts()) {
+		renderTestFeebacks(renderer, sb, component.getAssessmentTest().getTestFeedbacks(), component, TestFeedbackAccess.AT_END, ubu, translator);
+		//}
 		
 		//test part review
 		component.getTestSessionController().getTestSessionState().getTestPlan()
@@ -579,24 +578,20 @@ public class AssessmentTestComponentRenderer extends AssessmentObjectComponentRe
 	
 	private void renderTestFeeback(AssessmentRenderer renderer, StringOutput sb, AssessmentTestComponent component, TestFeedback testFeedback,
 			URLBuilder ubu, Translator translator) {
-		//<xsl:variable name="identifierMatch" select="boolean(qw:value-contains(qw:get-test-outcome-value(@outcomeIdentifier), @identifier))" as="xs:boolean"/>
-		Identifier outcomeIdentifier = testFeedback.getOutcomeIdentifier();
-		Value outcomeValue = component.getTestSessionController().getTestSessionState().getOutcomeValue(outcomeIdentifier);
-		boolean identifierMatch = valueContains(outcomeValue, testFeedback.getOutcomeValue());
-		//<xsl:if test="($identifierMatch and @showHide='show') or (not($identifierMatch) and @showHide='hide')">
-		if((identifierMatch && testFeedback.getVisibilityMode() == VisibilityMode.SHOW_IF_MATCH)
-				|| (!identifierMatch && testFeedback.getVisibilityMode() == VisibilityMode.HIDE_IF_MATCH)) {
-			
-			sb.append("<h2>");
+		TestSessionState testSessionState = component.getTestSessionController().getTestSessionState();
+		if(testFeedbackVisible(testFeedback, testSessionState)) {
+			sb.append("<div class='o_info clearfix'>");
+			sb.append("<h3>");
 			if(StringHelper.containsNonWhitespace(testFeedback.getTitle())) {
 				sb.append(StringHelper.escapeHtml(testFeedback.getTitle()));
 			} else {
 				sb.append(translator.translate("assessment.test.modal.feedback"));
 			}
-			sb.append("</h2>");
+			sb.append("</h3>");
 
 			testFeedback.getChildren().forEach((flow)
 				-> renderFlow(renderer, sb, component, null, null, flow, ubu, translator));
+			sb.append("</div>");
 		}
 	}
 	
diff --git a/src/test/java/org/olat/selenium/AssessmentTest.java b/src/test/java/org/olat/selenium/AssessmentTest.java
index 796cb70f692..c081ddb8092 100644
--- a/src/test/java/org/olat/selenium/AssessmentTest.java
+++ b/src/test/java/org/olat/selenium/AssessmentTest.java
@@ -36,7 +36,6 @@ import org.jboss.arquillian.junit.Arquillian;
 import org.jboss.arquillian.test.api.ArquillianResource;
 import org.jboss.shrinkwrap.api.spec.WebArchive;
 import org.junit.Assert;
-import org.junit.Assume;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.olat.selenium.page.LoginPage;
@@ -69,7 +68,6 @@ 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.firefox.FirefoxDriver;
 
 /**
  * 
@@ -105,9 +103,6 @@ public class AssessmentTest {
 	public void qti12Test(@InitialPage LoginPage authorLoginPage)
 	throws IOException, URISyntaxException {
 		
-		//File upload only work with Firefox
-		Assume.assumeTrue(browser instanceof FirefoxDriver);
-				
 		UserVO author = new UserRestClient(deploymentUrl).createAuthor();
 		authorLoginPage.loginAs(author.getLogin(), author.getPassword());
 		
@@ -200,9 +195,7 @@ public class AssessmentTest {
 	public void qti12CourseWithAssessment(@InitialPage LoginPage authorLoginPage,
 			@Drone @User WebDriver ryomouBrowser)
 	throws IOException, URISyntaxException {
-		//File upload only work with Firefox
-		Assume.assumeTrue(browser instanceof FirefoxDriver);
-		
+
 		UserVO author = new UserRestClient(deploymentUrl).createAuthor();
 		authorLoginPage.loginAs(author.getLogin(), author.getPassword());
 		UserVO ryomou = new UserRestClient(deploymentUrl).createRandomUser("Ryomou");
@@ -342,9 +335,7 @@ public class AssessmentTest {
 	public void scormCourseWithAssessment(@InitialPage LoginPage authorLoginPage,
 			@Drone @User WebDriver ryomouBrowser)
 	throws IOException, URISyntaxException {
-		//File upload only work with Firefox
-		Assume.assumeTrue(browser instanceof FirefoxDriver);
-				
+		
 		UserVO author = new UserRestClient(deploymentUrl).createAuthor();
 		authorLoginPage.loginAs(author.getLogin(), author.getPassword());
 		UserVO ryomou = new UserRestClient(deploymentUrl).createRandomUser("Ryomou");
@@ -462,9 +453,7 @@ public class AssessmentTest {
 	public void assessmentMode_manual(@InitialPage LoginPage authorLoginPage,
 			@Drone @Student WebDriver ryomouBrowser, @Drone @Participant WebDriver kanuBrowser)
 	throws IOException, URISyntaxException {
-		//File upload only work with Firefox
-		Assume.assumeTrue(browser instanceof FirefoxDriver);
-		
+
 		UserVO author = new UserRestClient(deploymentUrl).createAuthor();
 		authorLoginPage.loginAs(author.getLogin(), author.getPassword());
 		UserVO ryomou = new UserRestClient(deploymentUrl).createRandomUser("Ryomou");
@@ -878,9 +867,7 @@ public class AssessmentTest {
 			@Drone @User WebDriver ryomouBrowser,
 			@Drone @Participant WebDriver kanuBrowser)
 	throws IOException, URISyntaxException {
-		//File upload only work with Firefox
-		Assume.assumeTrue(browser instanceof FirefoxDriver);
-						
+			
 		UserVO author = new UserRestClient(deploymentUrl).createAuthor();
 		UserVO kanu = new UserRestClient(deploymentUrl).createRandomUser("Kanu");
 		UserVO ryomou = new UserRestClient(deploymentUrl).createRandomUser("Ryomou");
@@ -1066,8 +1053,6 @@ public class AssessmentTest {
 	public void taskWithIndividuScoreAndRevision(@InitialPage LoginPage authorLoginPage,
 			@Drone @User WebDriver ryomouBrowser)
 	throws IOException, URISyntaxException {
-		//File upload only work with Firefox
-		Assume.assumeTrue(browser instanceof FirefoxDriver);
 						
 		UserVO author = new UserRestClient(deploymentUrl).createAuthor();
 		UserVO kanu = new UserRestClient(deploymentUrl).createRandomUser("kanu");
diff --git a/src/test/java/org/olat/selenium/ImsQTI21Test.java b/src/test/java/org/olat/selenium/ImsQTI21Test.java
index c1e33877c9a..8e0f6d13d5a 100644
--- a/src/test/java/org/olat/selenium/ImsQTI21Test.java
+++ b/src/test/java/org/olat/selenium/ImsQTI21Test.java
@@ -73,6 +73,340 @@ public class ImsQTI21Test {
 	@Page
 	private NavigationPage navBar;
 	
+	/**
+	 * Test the flow of the simplest possible test with our
+	 * optimization (jump automatically to the next question,
+	 * jump automatically the close test). The test has one
+	 * part and 2 questions, no feedbacks, no review allowed...
+	 * 
+	 * @param authorLoginPage
+	 * @throws IOException
+	 * @throws URISyntaxException
+	 */
+	@Test
+	@RunAsClient
+	public void qti21TestFlow_noParts_noFeedbacks(@InitialPage LoginPage authorLoginPage)
+	throws IOException, URISyntaxException {
+		
+		UserVO author = new UserRestClient(deploymentUrl).createAuthor();
+		authorLoginPage.loginAs(author.getLogin(), author.getPassword());
+		
+		//upload a test
+		String qtiTestTitle = "With parts QTI 2.1 " + UUID.randomUUID();
+		URL qtiTestUrl = JunitTestHelper.class.getResource("file_resources/qti21/test_without_feedbacks.zip");
+		File qtiTestFile = new File(qtiTestUrl.toURI());
+		navBar
+			.openAuthoringEnvironment()
+			.uploadResource(qtiTestTitle, qtiTestFile)
+			.clickToolbarRootCrumb();
+		
+		QTI21Page qtiPage = QTI21Page
+				.getQTI12Page(browser);
+		qtiPage
+			.assertOnAssessmentItem()
+			.answerSingleChoice("Incorrect response")
+			.saveAnswer()
+			.assertOnAssessmentItem("Second question")
+			.selectItem("First question")
+			.assertOnAssessmentItem("First question")
+			.answerSingleChoice("Correct response")
+			.saveAnswer()
+			.answerMultipleChoice("Correct response")
+			.saveAnswer()
+			.endTest()//auto close because 1 part, no feedbacks
+			.assertOnAssessmentTestTerminated();
+	}
+	
+	/**
+	 * Test the flow of a test with questions feedbacks and test
+	 * feedback.
+	 * 
+	 * @param authorLoginPage
+	 * @throws IOException
+	 * @throws URISyntaxException
+	 */
+	@Test
+	@RunAsClient
+	public void qti21TestFlow_noParts_withFeedbacks(@InitialPage LoginPage authorLoginPage)
+	throws IOException, URISyntaxException {
+		
+		UserVO author = new UserRestClient(deploymentUrl).createAuthor();
+		authorLoginPage.loginAs(author.getLogin(), author.getPassword());
+		
+		//upload a test
+		String qtiTestTitle = "With parts QTI 2.1 " + UUID.randomUUID();
+		URL qtiTestUrl = JunitTestHelper.class.getResource("file_resources/qti21/test_with_feedbacks.zip");
+		File qtiTestFile = new File(qtiTestUrl.toURI());
+		navBar
+			.openAuthoringEnvironment()
+			.uploadResource(qtiTestTitle, qtiTestFile)
+			.clickToolbarRootCrumb();
+		
+		QTI21Page qtiPage = QTI21Page
+				.getQTI12Page(browser);
+		qtiPage
+			.assertOnAssessmentItem()
+			.answerSingleChoice("Wrong answer")
+			.saveAnswer()
+			.assertFeedback("Oooops")
+			.answerSingleChoice("Correct answer")
+			.saveAnswer()
+			.assertFeedback("Well done")
+			.nextAnswer()
+			.assertOnAssessmentItem("Numerical entry")
+			.answerGapText("69", "_RESPONSE_1")
+			.saveAnswer()
+			.assertFeedback("Not really")
+			.answerGapText("42", "_RESPONSE_1")
+			.saveAnswer()
+			.assertFeedback("Ok")
+			.endTest()
+			.assertOnAssessmentTestFeedback("All right")
+			.closeTest()
+			.assertOnAssessmentTestTerminated();
+	}
+	
+	/**
+	 * A test with a single part, feedback for questions and
+	 * tests and the resource options "show results at the end
+	 * of the test".
+	 * 
+	 * @param authorLoginPage
+	 * @throws IOException
+	 * @throws URISyntaxException
+	 */
+	@Test
+	@RunAsClient
+	public void qti21TestFlow_noParts_feedbacksAndResults(@InitialPage LoginPage authorLoginPage)
+	throws IOException, URISyntaxException {
+		
+		UserVO author = new UserRestClient(deploymentUrl).createAuthor();
+		authorLoginPage.loginAs(author.getLogin(), author.getPassword());
+		
+		//upload a test
+		String qtiTestTitle = "With parts QTI 2.1 " + UUID.randomUUID();
+		URL qtiTestUrl = JunitTestHelper.class.getResource("file_resources/qti21/test_with_feedbacks.zip");
+		File qtiTestFile = new File(qtiTestUrl.toURI());
+		navBar
+			.openAuthoringEnvironment()
+			.uploadResource(qtiTestTitle, qtiTestFile);
+		
+		QTI21Page qtiPage = QTI21Page
+				.getQTI12Page(browser);
+		qtiPage
+			.clickToolbarBack()
+			.options()
+			.showResults(Boolean.TRUE, QTI21AssessmentResultsOptions.allOptions())
+			.save();
+		
+		qtiPage
+			.clickToolbarBack()
+			.assertOnAssessmentItem()
+			.answerSingleChoice("Wrong answer")
+			.saveAnswer()
+			.assertFeedback("Oooops")
+			.nextAnswer()
+			.assertOnAssessmentItem("Numerical entry")
+			.answerGapText("42", "_RESPONSE_1")
+			.saveAnswer()
+			.assertFeedback("Ok")
+			.endTest()
+			.assertOnAssessmentTestFeedback("Not for the best")
+			.closeTest()
+			.assertOnAssessmentTestMaxScore(2)
+			.assertOnAssessmentTestScore(1)
+			.assertOnAssessmentTestNotPassed();
+	}
+	
+	/**
+	 * A test with a single part, feedback for questions and
+	 * tests and the resource options "show results at the end
+	 * of the test".
+	 * 
+	 * @param authorLoginPage
+	 * @throws IOException
+	 * @throws URISyntaxException
+	 */
+	@Test
+	@RunAsClient
+	public void qti21TestFlow_parts_noFeedbacksButResults(@InitialPage LoginPage authorLoginPage)
+	throws IOException, URISyntaxException {
+		
+		UserVO author = new UserRestClient(deploymentUrl).createAuthor();
+		authorLoginPage.loginAs(author.getLogin(), author.getPassword());
+		
+		//upload a test
+		String qtiTestTitle = "With parts QTI 2.1 " + UUID.randomUUID();
+		URL qtiTestUrl = JunitTestHelper.class.getResource("file_resources/qti21/test_parts_without_feedbacks.zip");
+		File qtiTestFile = new File(qtiTestUrl.toURI());
+		navBar
+			.openAuthoringEnvironment()
+			.uploadResource(qtiTestTitle, qtiTestFile);
+		
+		QTI21Page qtiPage = QTI21Page
+				.getQTI12Page(browser);
+		qtiPage
+			.clickToolbarBack()
+			.options()
+			.showResults(Boolean.TRUE, QTI21AssessmentResultsOptions.allOptions())
+			.save();
+		
+		qtiPage
+			.clickToolbarBack()
+			.startTestPart()
+			.selectItem("First question")
+			.assertOnAssessmentItem("First question")
+			.answerSingleChoice("Correct")
+			.saveAnswer()
+			.assertOnAssessmentItem("Second question")
+			.answerMultipleChoice("True")
+			.saveAnswer()
+			.endTestPart()
+			.selectItem("Third question")
+			.assertOnAssessmentItem("Third question")
+			.answerMultipleChoice("Correct")
+			.saveAnswer()
+			.answerCorrectKPrim("True", "Right")
+			.answerIncorrectKPrim("Wrong", "False")
+			.saveAnswer()
+			.endTestPart()
+			.assertOnAssessmentTestMaxScore(4)
+			.assertOnAssessmentTestScore(4)
+			.assertOnAssessmentTestPassed();
+	}
+	
+	/**
+	 * Test with 2 parts and test feedbacks.
+	 * 
+	 * @param authorLoginPage
+	 * @throws IOException
+	 * @throws URISyntaxException
+	 */
+	@Test
+	@RunAsClient
+	public void qti21TestFlow_parts_feedbacks(@InitialPage LoginPage authorLoginPage)
+	throws IOException, URISyntaxException {
+		
+		UserVO author = new UserRestClient(deploymentUrl).createAuthor();
+		authorLoginPage.loginAs(author.getLogin(), author.getPassword());
+		
+		//upload a test
+		String qtiTestTitle = "With parts QTI 2.1 " + UUID.randomUUID();
+		URL qtiTestUrl = JunitTestHelper.class.getResource("file_resources/qti21/test_with_parts_and_test_feedbacks.zip");
+		File qtiTestFile = new File(qtiTestUrl.toURI());
+		navBar
+			.openAuthoringEnvironment()
+			.uploadResource(qtiTestTitle, qtiTestFile)
+			.clickToolbarRootCrumb();
+		
+		QTI21Page qtiPage = QTI21Page
+				.getQTI12Page(browser);
+
+		qtiPage
+			.startTestPart()
+			.selectItem("First question")
+			.assertOnAssessmentItem("First question")
+			.answerSingleChoice("Correct answer")
+			.saveAnswer()
+			.assertOnAssessmentItem("Second question")
+			.answerMultipleChoice("Valid answer")
+			.saveAnswer()
+			.endTestPart()
+			.selectItem("Third question")
+			.assertOnAssessmentItem("Third question")
+			.answerSingleChoice("Right")
+			.saveAnswer()
+			.answerSingleChoice("Good")
+			.saveAnswer()
+			.endTestPart()
+			.assertOnAssessmentTestFeedback("Well done")
+			.closeTest()
+			.assertOnAssessmentTestTerminated();
+	}
+	
+	/**
+	 * Test with time limit.
+	 * 
+	 * @param authorLoginPage
+	 * @throws IOException
+	 * @throws URISyntaxException
+	 */
+	@Test
+	@RunAsClient
+	public void qti21TestFlow_timeLimits(@InitialPage LoginPage authorLoginPage)
+	throws IOException, URISyntaxException {
+		
+		UserVO author = new UserRestClient(deploymentUrl).createAuthor();
+		authorLoginPage.loginAs(author.getLogin(), author.getPassword());
+		
+		//upload a test
+		String qtiTestTitle = "Timed QTI 2.1 " + UUID.randomUUID();
+		URL qtiTestUrl = JunitTestHelper.class.getResource("file_resources/qti21/test_time_limits.zip");
+		File qtiTestFile = new File(qtiTestUrl.toURI());
+		navBar
+			.openAuthoringEnvironment()
+			.uploadResource(qtiTestTitle, qtiTestFile)
+			.clickToolbarRootCrumb();
+		
+		QTI21Page qtiPage = QTI21Page
+				.getQTI12Page(browser);
+		//check simple time limit
+		qtiPage
+			.assertOnAssessmentItem("Single choice")
+			.answerSingleChoice("Correct answer")
+			.saveAnswer()
+			.assertOnAssessmentItem("Last choice")
+			.answerSingleChoice("True")
+			.saveAnswer()
+			.assertOnAssessmentTestTerminated(15);
+	}
+	
+	/**
+	 * Test with time limit and wait for the results at the end.
+	 * 
+	 * @param authorLoginPage
+	 * @throws IOException
+	 * @throws URISyntaxException
+	 */
+	@Test
+	@RunAsClient
+	public void qti21TestFlow_timeLimits_results(@InitialPage LoginPage authorLoginPage)
+	throws IOException, URISyntaxException {
+		
+		UserVO author = new UserRestClient(deploymentUrl).createAuthor();
+		authorLoginPage.loginAs(author.getLogin(), author.getPassword());
+		
+		//upload a test
+		String qtiTestTitle = "Timed QTI 2.1 " + UUID.randomUUID();
+		URL qtiTestUrl = JunitTestHelper.class.getResource("file_resources/qti21/test_time_limits.zip");
+		File qtiTestFile = new File(qtiTestUrl.toURI());
+		navBar
+			.openAuthoringEnvironment()
+			.uploadResource(qtiTestTitle, qtiTestFile)
+			.clickToolbarRootCrumb();
+		
+		QTI21Page qtiPage = QTI21Page
+				.getQTI12Page(browser);
+		qtiPage
+			.options()
+			.showResults(Boolean.TRUE, new QTI21AssessmentResultsOptions(true, true, false, false, false, false))
+			.save();
+		
+		//check simple time limit
+		qtiPage
+			.clickToolbarBack()
+			.assertOnAssessmentItem("Single choice")
+			.answerSingleChoice("Correct answer")
+			.saveAnswer()
+			.assertOnAssessmentItem("Last choice")
+			.answerSingleChoice("True")
+			.saveAnswer()
+			.assertOnAssessmentResults(15)
+			.assertOnAssessmentTestPassed()
+			.assertOnAssessmentTestMaxScore(2)
+			.assertOnAssessmentTestScore(2);
+	}
+	
 	/**
 	 * Upload a test in QTI 2.1 format, create a course, bind
 	 * the test in a course element, run it and check if
@@ -84,7 +418,7 @@ public class ImsQTI21Test {
 	 */
 	@Test
 	@RunAsClient
-	public void qti21Test(@InitialPage LoginPage authorLoginPage)
+	public void qti21TestInCourse(@InitialPage LoginPage authorLoginPage)
 	throws IOException, URISyntaxException {
 		
 		UserVO author = new UserRestClient(deploymentUrl).createAuthor();
@@ -146,7 +480,6 @@ public class ImsQTI21Test {
 			.answerSingleChoice("Right")
 			.saveAnswer()
 			.endTest()
-			.closeTest()
 			.assertOnCourseAttempts(1)
 			.assertOnCourseAssessmentTestScore(1);
 	}
@@ -233,7 +566,6 @@ public class ImsQTI21Test {
 			.answerSingleChoice("Right")
 			.saveAnswer()
 			.endTest()
-			.closeTest()
 			.assertOnAssessmentResults()
 			.closeAssessmentResults()
 			.assertOnCourseAttempts(1)
@@ -278,8 +610,7 @@ public class ImsQTI21Test {
 			.answerHotspot("circle")
 			.saveAnswer()
 			.assertFeedback("Correct!")
-			.endTest()
-			.closeTest();
+			.endTest();
 		//check the results
 		qtiPage
 			.assertOnAssessmentResults()
@@ -355,8 +686,7 @@ public class ImsQTI21Test {
 			.answerCorrectKPrim("Deutschland", "Uruguay")
 			.answerIncorrectKPrim("Frankreich", "Spanien")
 			.saveAnswer()
-			.endTest()
-			.closeTest();
+			.endTest();
 		
 		//check the results
 		qtiPage
diff --git a/src/test/java/org/olat/selenium/UserTest.java b/src/test/java/org/olat/selenium/UserTest.java
index 128900bb7e5..ed244bacfbe 100644
--- a/src/test/java/org/olat/selenium/UserTest.java
+++ b/src/test/java/org/olat/selenium/UserTest.java
@@ -34,7 +34,6 @@ import org.jboss.arquillian.junit.Arquillian;
 import org.jboss.arquillian.test.api.ArquillianResource;
 import org.jboss.shrinkwrap.api.spec.WebArchive;
 import org.junit.Assert;
-import org.junit.Assume;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.olat.restapi.support.vo.CourseVO;
@@ -51,8 +50,8 @@ import org.olat.selenium.page.user.PortalPage;
 import org.olat.selenium.page.user.UserAdminPage;
 import org.olat.selenium.page.user.UserPasswordPage;
 import org.olat.selenium.page.user.UserPreferencesPageFragment;
-import org.olat.selenium.page.user.UserProfilePage;
 import org.olat.selenium.page.user.UserPreferencesPageFragment.ResumeOption;
+import org.olat.selenium.page.user.UserProfilePage;
 import org.olat.selenium.page.user.UserToolsPage;
 import org.olat.selenium.page.user.VisitingCardPage;
 import org.olat.test.ArquillianDeployments;
@@ -62,7 +61,6 @@ 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.firefox.FirefoxDriver;
 
 /**
  * 
@@ -581,9 +579,6 @@ public class UserTest {
 	@RunAsClient
 	public void browserBack(@InitialPage LoginPage loginPage)
 	throws IOException, URISyntaxException {
-		
-		Assume.assumeTrue(browser instanceof FirefoxDriver);
-		
 		loginPage
 			.loginAs("administrator", "openolat")
 			.resume();
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 2311aeda4b0..110c4d33bb0 100644
--- a/src/test/java/org/olat/selenium/page/qti/QTI21Page.java
+++ b/src/test/java/org/olat/selenium/page/qti/QTI21Page.java
@@ -69,29 +69,31 @@ public class QTI21Page {
 		return this;
 	}
 	
+	public QTI21Page startTestPart() {
+		By startBy = By.xpath("//button[contains(@onclick,'advanceTestPart')]");
+		browser.findElement(startBy).click();
+		OOGraphene.waitBusy(browser);
+		By menuBy = By.id("o_qti_menu");
+		OOGraphene.waitElement(menuBy, 5, browser);
+		return this;
+	}
+	
 	public QTI21Page assertOnAssessmentItem() {
 		By assessmentItemBy = By.cssSelector("div.qtiworks.o_assessmentitem.o_assessmenttest");
 		OOGraphene.waitElement(assessmentItemBy, 5, browser);
 		return this;
 	}
 	
-	//TODO still qti 1.2
-	public QTI21Page selectItem(int position) {
-		By itemsBy = By.cssSelector("a.o_sel_qti_menu_item");
-		List<WebElement> itemList = browser.findElements(itemsBy);
-		Assert.assertTrue(itemList.size() > position);
-		WebElement itemEl = itemList.get(position);
-		itemEl.click();
-		OOGraphene.waitBusy(browser);
+	public QTI21Page assertOnAssessmentItem(String title) {
+		By itemTitleBy = By.xpath("//div[@class='o_assessmentitem_wrapper']/h4[contains(normalize-space(.),'" + title + "')]");
+		OOGraphene.waitElement(itemTitleBy, 5, browser);
 		return this;
 	}
 	
-	public QTI21Page answerSingleChoice(int selectPosition) {
-		By itemsBy = By.cssSelector("div.choiceInteraction input[type='radio']");
-		List<WebElement> optionList = browser.findElements(itemsBy);
-		Assert.assertTrue(optionList.size() > selectPosition);
-		WebElement optionEl = optionList.get(selectPosition);
-		optionEl.click();
+	public QTI21Page selectItem(String title) {
+		By itemBy = By.xpath("//div[@id='o_qti_menu']//li[contains(@class,'o_qti_menu_item')]//a[span[contains(normalize-space(.),'" + title + "')]]");
+		OOGraphene.waitElement(itemBy, 5, browser);
+		browser.findElement(itemBy).click();
 		OOGraphene.waitBusy(browser);
 		return this;
 	}
@@ -167,6 +169,10 @@ public class QTI21Page {
 		return this;
 	}
 	
+	public QTI21Page endTestPart() {
+		return endTest();
+	}
+	
 	public QTI21Page endTest() {
 		By endBy = By.cssSelector("a.o_sel_end_testpart");
 		browser.findElement(endBy).click();
@@ -219,6 +225,18 @@ public class QTI21Page {
 		return this;
 	}
 	
+	/**
+	 * This check specifically if the metadata of the test are visible.
+	 * 
+	 * @param timeout
+	 * @return
+	 */
+	public QTI21Page assertOnAssessmentResults(int timeout) {
+		By resultsBy = By.cssSelector("div.o_sel_results_details");
+		OOGraphene.waitElement(resultsBy, timeout, browser);
+		return this;
+	}
+	
 	public QTI21Page assertOnCourseAssessmentTestScore(int score) {
 		By resultsBy = By.xpath("//div[contains(@class,'o_personal')]//tr[contains(@class,'o_score')]/td[contains(text(),'" + score + "')]");
 		OOGraphene.waitElement(resultsBy, 5, browser);
@@ -231,12 +249,54 @@ public class QTI21Page {
 		return this;
 	}
 	
+	public QTI21Page assertOnAssessmentTestPassed() {
+		By notPassedBy = By.cssSelector("div.o_sel_results_details tr.o_state.o_passed ");
+		OOGraphene.waitElement(notPassedBy, 5, browser);
+		return this;
+	}
+	
+	public QTI21Page assertOnAssessmentTestNotPassed() {
+		By notPassedBy = By.cssSelector("div.o_sel_results_details tr.o_state.o_failed ");
+		OOGraphene.waitElement(notPassedBy, 5, browser);
+		return this;
+	}
+	
 	public QTI21Page assertOnAssessmentTestMaxScore(int score) {
 		By resultsBy = By.xpath("//div[contains(@class,'o_sel_results_details')]//tr[contains(@class,'o_sel_assessmenttest_maxscore')]/td[contains(text(),'" + score + "')]");
 		OOGraphene.waitElement(resultsBy, 5, browser);
 		return this;
 	}
 	
+	public QTI21Page assertOnAssessmentTestFeedback(String feedback) {
+		By feedbackBy = By.xpath("//div[contains(@class,'o_info')]/h3[contains(text(),'" + feedback + "')]");
+		OOGraphene.waitElement(feedbackBy, 5, browser);
+		List<WebElement> feedbackEls = browser.findElements(feedbackBy);
+		Assert.assertEquals(1, feedbackEls.size());
+		return this;
+	}
+	
+	/**
+	 * Check if the assessment terminated message is visible.
+	 * 
+	 * @return Itself
+	 */
+	public QTI21Page assertOnAssessmentTestTerminated() {
+		By terminatedBy = By.cssSelector("div.o_sel_assessment_test_terminated");
+		OOGraphene.waitElement(terminatedBy, 5, browser);
+		return this;
+	}
+	
+	/**
+	 * Check if the assessment terminated message is visible.
+	 * 
+	 * @return Itself
+	 */
+	public QTI21Page assertOnAssessmentTestTerminated(int timeout) {
+		By terminatedBy = By.cssSelector("div.o_sel_assessment_test_terminated");
+		OOGraphene.waitElement(terminatedBy, timeout, browser);
+		return this;
+	}
+	
 	/**
 	 * Yes in a dialog box controller.
 	 */
diff --git a/src/test/java/org/olat/test/file_resources/qti21/test_parts_without_feedbacks.zip b/src/test/java/org/olat/test/file_resources/qti21/test_parts_without_feedbacks.zip
new file mode 100644
index 0000000000000000000000000000000000000000..b234b7db61a3d18481d2d15f980054b6e7a8ccd5
GIT binary patch
literal 5996
zcmb7|XE>bww#N0|gNR;+=ox+VUZWGy%M62v9z^sOB7z8l5haM%=tLb|l<16_QKCl|
zEsS>9`|PvxzSnimyU%*Q{GSj1`}y!(zk989>p#TCp~fH}Ai(g*aMHr~t;lcA&aU3B
zc5cp42k&Qs&t0GHJhsq70qKI)AFKH5En83_wE9s1K%)2?%>d;SN0{1*Ar<!O!th2E
zEw?D}l}?P-+s)ziQ^ZQ9%GZRukM7N8)beG#pjx6N!ZuRQdyV#@x{|u4{R#Y$SyICf
z`=;FyfQ;%v<~1ex*y*fe4VNP{LEdh1@yiGV!oa}iy+fY8<dbjx&qp_=)op`4U$28K
z7klIkH#$qSe(_gzHu`qiW#C&GcWVlKXsYK#efH^Lu7oud!3-Pmy^tk_azNU-<v_n@
zuQ&-7Pk+s9=%uA=sj1El6T~_Cbk$qn=`kd_Nlt`S6TV#R?R^jt_KqPIR;^el$FRLc
zLcUkdT>pM<oup7b?0OaUX2R>9`Jg9y`C=tC&ST%IBc(~yzbRfGx9pM3F*|cE>61IN
z;qP@4`ai`^r7!ms9Zo(Q)FJ+aVsiy6=IZPaS)}<=(GD4>3b6}w-4o{Np(9M)j5N@}
zaJ2iryFM?nH5qUk^>{)nv3hb^a~cy{)i4RGn#8+|Ko9vvAC|AAwYaDC=p{<ZI&J)L
zK5K7hTN=B%5yNa(eTUT*X)>ns!L0r{WhpuaEyr&2m{}fcXPAyfyCH15RLW+R@qBB$
ztJW_}bw`^oS7N?_<%$}rgSlqUFt~g-@tjyXP3G5dvL}-9k1~9#h12y_E(?7B3;z1T
z$SK_r_-u5&fYu>xSeLX`f`p-be(*YRk2WDEFdopu3%;7w0NCb5dVmeEG`o}T3;uA{
zvl{6Uy{ly)$gW9{Fny3sCG(LBNeASj{~F{ry>YD5(^R}}jgF!J63PnI;Vo5DF;FWi
zz<O;Ty399X_6el(wnm$+jDlMq#Ku#oVsW*4s<(N%8Nn#8VY_D%qGqH`kgx60&t^nT
z0x>pNdLyQBX#!NOQ8eoieXOO#uHT&~Kf$GR!FP<WZj*I7o<+xs@C)bw7KreI-htju
z5>WJ+zSaDgfA9ONVt)%`jff>85m+eBh?G(6xkRj?)Y%J;ivo0z1MvL%OKZ~}OWw62
zRe<U>s^*&efN+ZC8kr}r{}7)bJ93Vn92)~efdT_V|9@o(aKn;|hnKS}&>jK?frUh&
zVh}qB#NJ*43<gOGNrJ)lVz+>GnOb>&1X2er*tG9A8gZEH)J6=Lx;SP&*>NgF)#*m&
zq?1PKgtJ@+W3yAK5SI=e<gcs*X9jq?jYnp349Tv%4pYdgJ&76Kn`HDmilS&|!YH1A
z*EW_Jh~vL`F^932w>WOiN^WhJP5c9;ah<!GH607a3<%WyL6S5*k|C|oa>lpcQ$<M^
zjaR{e-65z}l6Or=cVgQt=W-D~=!<F=i?f&#XM8sIba)ne?ornko)a$@Y3UTA`~~;Z
z6&sNhkm5N{m>__WCjI9KHc=uAt~aA9kN?XoQIXV<pB3lkasJA2{UE7EyYVtROh?9r
zpJ~%<HMudl4us=<SS|+$H_ZhuWtkkg_k`bYs;09$(8zv!AUf@+@WJxB^L6Kl7=tkW
zd7^FQ#t&-^95RBjntLlheD3sKFbyOMBpzZ3z5bvfA?`5Ruemg+H75Q(F|s#fu9m*8
z6!vM<js6&xnwq0iVP(=DkJTbcT1(?$VIj0R{5n@ZOq@7}i(9+%$d<x3q)f6mDk-7P
zNnO1aK1DAe>%*=XWK;2mOJ0UebM9^aF#!eZ{YokZ%$`z4yS$GnN*TQ0xL5WfN$&uf
zx`GIZ=!L9vax04bKFhP5Uq+w1?QOJ3GaY#iel75<5&I=gPCkEP2L8PHLn&5OPmROW
zi8#a8v$m~a_h(Up*I<%5>^tziFpqA)vgS2%+)^o*o6@P-q*XsNuiZOEf7mjVcx@s`
z-kmV6JUVvOqor_&>k+fv1H2Xml5d`~2qRGt&tf2DqoC&fESbD5!S(U(N)E|NSis!U
z^gFa%EThibiZM7R@z<7nip{uP;)ZGcno27g8TZ<2GJ_EhK=e_KDI0hq6~XQ`620}<
zT~~12WaR7tW^)yV6>C=JZfe515brE5R^OX<2benhfo#0EpDl*#ChMdGnL-4uIn*~}
zN-V?)XQt(cJB@|s<Ifw=^q8Zj#^lE)6jk@-n`@eZPx@T~gjGa{4W*fyP$eZKv)Wl{
z^3jpp1(r&*JulF-1XVsm7DAF#%ca3xvA$qizA_HZIXCXiY1#v<N+CreqZc|p4eO`C
zz2sQAucMSA*RtX#ese@PYZqB7?$WW;(K1K;4jHu3&2xl(3dpyGP&{fqb<I{w{CvO5
z^5al*j{@?cuPseeyTNS68?fsk!IR;(J>9N&Zi7cr>`RA-rsMD~*Jea6Z(%}A`Q6rV
z*cg$I`0kqHYMBC+Hc(#LFUqZ@f9}f17o4GRIn|HGzcm5A-j1{C8xz30^LHlT3bB`v
z0E0xtfl#O@7zhFbAz%ntNK9B9BqD4N1^vSW9$UG8RJb((yXQ9Q$+6;2;8L9dUl$|T
zC-*_1<+G%)T#;q<>&y6@AFOVkTe}B%vy#6u&pip4Q>pUfSWO_f2Cht%rq<=0J7(Sm
zNiLBl#gBp};NC*YgKC{><??%l_GFY-x{x9d?KZRtpCwOhm;#3+WVVMN*SsHN<NjW3
ziva9Z5Qze|PRvVcmEl+dvP-vjpSRt9fLc&p!A=ni?M!zyru-80_~3L$B|H?&_%*+9
zHB|ir8Kbc?xf{oG*`QQ?q3rchI7_4-kOrC6_>9hJugE^kl4tcptQ9l{^BiY<o1-4W
zxi)IF9|jB)nm9X6P))+|CXbsZrW#~1g%>Nwu?Rca2OY;RokfE(Wwh1ZYael#;)r&~
z!O7<mag9XnY$pQw7tc6G`+ngYd64SWQz(oh#UF<7O<90zLxWY!2Th#t<F~?*PowKy
zq+PG!Grp*JmhUg4tLjOJQ$qd!P*FgKKcHS5)wDt`t~eVf0-R+;aHHZNHApIjn3^B&
zWeY4d@~l_PNlr$Da(QJ!;#r1rUx@0?)Z|xenrW4{=}&3B!ZonGu(67m>aT6Paxvah
z%R|B1_j?9yrI>^Je%ay+&c>zn>=#lM)gCi22X0S#4e^9|mMGXJ5zUv5o_^WwKVv76
z2!@|MX{NL$HzIM}e`BQNVyXY8R;-efK$Q?`BKUAjgF)k^5%7HBT!}1KG3lxrx{?cs
zR*kNIlw{>oLGISy;`N~ob72ouyZmg<cf_sU&Xx;-s=vNJ^Vx`I3nsQ+LELaCM^x5d
ze=mP$V~$vs33HA~d_JxdTbI(reqP=wjQOr&<?#39w;UWVw%|13vc|oun)>gaejlUc
z_cY~5u~Z3ynDiAv?;E6y%&KYY^!Dbv>txZ;q`h_rOm7GH=3H#m3BaQd+K8T=uv7;{
z9A+h?UEnu4U#PAHWSl!jDa!e^rK1BI`(Ne++WIaSLCVODGNs2I^NVJGy`FIM9sQQV
z4~vH0Ew^&cD4%q@J{|FpwZB3ig<#%-#k)*%ihlzZG3nof1(g&9i`Y8=CB#JSL13^Q
zL`)JWAqo^15{8ONO8yJ1B_Ks$t4-KJFepqZhe#vYi>k5KxB!kekvzfUm-KuBOSn20
zA;h8T5VLGR$h@_em9CaXP%@hkB@?lYK}hXfnGQ6p%4xRDyi1q=o2(LINo7)$@5t8W
zT=D?hmj$zY(Rc#Wg(p#bieK3(^atzrh4exvW7!W^tg7oAIDF$V>BJ&U+Z0Yp5~o{U
zBL=xVFN?+XN$N*CT2PxfgW{gs%g798GTANvAwX&K0|jTUkIO)9&ytxj6}&V)?oPr*
zT5yc`Q(bk!b`}pOTQ06=UJ}m*6Z|oYND{JELC0KzJQ?6A-lFXftfu#KAMu$M)0;~C
zGFezT0sLXA(nkPb><8GCZR_Q2EU(x}z3?ufZ%?Ws@ctyPEl+D%ofh%vurho-VVZk^
zMZ1kuhSCsgR1d{}S!(J2>I@6b=DFkcE_4GPKvVyQEO-3k8MeF!z2G#n*0LeLG1gix
z73Nps$`O^w`#w6+?+OhRlfa|=54gW$ZGE66!W92#bCN8~=G)v`L${p^QF`=zKH(NC
z`@$o0Vbf!NS2bIwCk3nFPHflZW$jxI7T+(#;PU^(YW-hW;oV@Rp>l&&`pl+8c<^?&
zmmpo3XTBmhiD3TC4OZL5XY2&xTjTp;FO-{@x#*y+sKUs}NP9e2#Ftc7F^)8gbp9@-
zY6Zj0j<V}Z>t0q8jaimYF73*mFXPB^i&tFEb*G&5oj{M1$Kn@o5e{|VtYGLt=>2SW
z#<yDc20M6DC3i;cM5=HvxTA{Jy7f><#NBQ{@($Vhd|U*!HQVG3RzD&=-`G#Ddpya`
zvpg~}in1lA@$x4+0vuZ{eMwS<JuINLlXpz_f^TRVbBdz4{B^kBf1O?PH!)7kzP1b>
zoG!%glMhcfsJf+U;d4+#byh+>8o$Zx=$};Ka%8)Bl*#+~r4#m%R-c!w1qP)pSsG=(
z{p7XqzyvX)>hf6lDRYKZlW&1@i#g(Qxie}j_ktzo>cK5kdKH>&*f&s7{w-AA5K#zN
zSX@{fA_^510)fE7AaQXq2N9^4xTKw(r0Bn(`k!uvuVUdm^2HpM=2}yvgRFzn2}*r!
z(}(UaFYFP7hfY74f(8}O1`IW<2iv;x;}9$=v-VJ3bboUUt2ZP5hNLxoDM3(bQoPpp
z^uQ_JywNSEt|$7QS!hBCQbB`}VkB6MHyfuQQXxplakiZw)13Hjk-Hoq$fj0lnIg1N
zi{hADMOKGm+YY%EKELx>ptCBcIKqK$C}paJz05mJpvZm%x$6wDU*^DL;~^*Otaz=g
z`JQ*IQ(J*4p4^W@sp65hOsuzfdrNpad}O2Zdx6{<4SVjhBhgN$jQVa-MR%2@ZS4D|
zT)Bl((;%^mqN_=CEGk|hQicMq(BYB1pYk+>c`)nKyO|$zBlI}xf~3GauROG3gd}$9
zqAlhFnEphw-oqRk(4qm3irgLujR5a(LWPcSg^p5jO;7+eghMSOKEh<3zSvi(c-fYr
zz>|j-j#+K+=3)gifphU{6b|j&IQ-EcF4J8(@WivdQ|MiJfTlm&3!`$UQascT{Sh81
zmY#Rs`OG9nXfs38W6>g7+drC;4733WSMF93bvJG5RH*1z-ss$p?diHNkb!)7CX85u
z<2z<xoeJCyE#N0HV=f5d1d;(%(g3)QE~0W(XJE>J`I9u>X1Fy=c{Jh6#@+5pB?g&~
zM6P}vsT7)8K;%Ic&apBckM8PNNbp@R0$i$-=nb<9gEWpj_9LFaQeAaFRO<Y0t+dHq
z8`$Gm+JW7gT_5e{0l!w+YnzH9$fFw2j$S3@*+x)oy_c8tL&WxW7(K;%=+}b#kn)^Y
zv6=&>ts{71!jxTP5s*SgnlS^h1zOg^4OY&)<F8W58g&+lT;r{G+Q?VC)%;Bjl%u@d
zckQ|w2suoII(m}$Tedo{e0-9eY0C6uAL?1X()tqI`dQ-G5UKr4%PH8fUo4jjX^<CE
zwgo^9?;SVKt{CoxSWA-%=EzACs@I}tT^$d$2+J8F(YarPZIYn~4bNTMY(?*bA)5ij
znQC*f8Hi!<!LF6SVC##z>&DZQN`tfOwkw-k&ZvnsooR14qa*vjniv1gYWVCXU$F<>
zY=I&mNdY@yVUU36KTcptkbsDTguM_%NJ!WoXm`uoO$Ot>38DyIu%TQHdbJFyBT8(X
zt2Qj~wMc<WxflN=Em^Cr#Lf(9)g{0r68JnOsRt_rW}RIOuHSQ{i<nf}vZP)!b6PGr
zEE#$1Qp&a4k+#`)Xi_}kL=)HdV%IJ*aPRzbE>1v&thHE)fjiA3#Cu+bsSh)}?^uS9
z$DV1#+<VykN^CQmv+M$b(IvpfnexZ|vdX4#Rx&JhZo*|CO;U`g1`vPXj&0AVTDrFO
ztY9wDle`Zp!mq1@;;F-pY|GIq`ex7&Ww&K!UIUp~EBWjbf65vY2O-a(5bCy9vo(!l
z9~AsJv5^hdypi5T%e_~=g}!^0gRv<Mr$!Ydn{Ei~pS$I)!*0x3Ee&K58O2z_4#C=%
zrLq28meJv~!}7ea&jVUOrhMF0C8c-icYEoDc0?E-AZ+X-yXU>?fG6`(3C$4Q_xhJ4
zdV5rDJzF!|{LD4#ygBu(-h}D2HiOLqOX?P}(J5A*R9)fGVF`p-so$0Wggx45Mx3QG
zqc+1q>E!byi&|APdpLzY%j5?)r?St?v&QWchClJDT&??J0=cB#!>WKj>5`Hq<!{6u
zch~ZIF*S2P4mrNG^Kq;8Vd|~&ItHX_mEc)yf%vMSMkt151BR#BWZypxpcuNLmJ;iM
zj^%Lpw$SmAb#~isRtBN(v+-}=mRRE`B9p%m=H-$*hu@(!jOPKAaZH%+GkXUp9%<}R
zne&!9ZB46^u6mRdNR*_A-K}wOSACNI6e5A%mf3tc8GHFQWqV?#x37))E`If1&BI@l
zyC;AZ#l{>{YP1jzw+2BTX2rCL4wmx(JFV?m%a@^#v}2oHPwC=4zQh26lk_uH47joG
zsqrXish!}prs)hlXG8*G5|bl8MzX8jJ62}o#9R}W>|bMSlY>?I856>~QNGjaJKYrR
zlth{oce1p22|6;PF%*E`_pg1=jS-ab?5W^@2+x=xN?<^gCUrGM?*o@Q?<gK4C2T=`
zMyf1*KA*IYX*s<;!rDk;qC-fiUd>mRN5zbuYIip8+!MBqJ4_n`92;r*KTjQ$3>~i1
zysmsv@^F&Ka^T!nV4xFPbYAu0RGVCKiz}P#3!)_;%sfn1eTV<N4wbk60XmOiYeQbQ
zGbpPWaAN6tIBip(5`eR&rv2-gO#UyiSa+wBm%icCG2WuexH~bTC3|lV$)<z;@RC4e
z&Uw&Te~b5b9Det4SBF>ntTsHjT4G>0cP2sKW~l)pmPV!puGoEi7|ifF1pm|ZKZaS|
z#E#V38$Z}5x%C4~ENYB@2d}@!NVmc3pX)C%>|ZbczJA^&P5&<Ko6r3xxzk@2f1kVm
ztZ0Cbf$^U#v42(kedzeJBAn<}@$aYizbgK2uYXp&z8P%(lhNg`iogG$TU-8jIa2=r
YOj`dTF5c~JyqjkM69dDK=JxHs0KC<PC;$Ke

literal 0
HcmV?d00001

diff --git a/src/test/java/org/olat/test/file_resources/qti21/test_time_limits.zip b/src/test/java/org/olat/test/file_resources/qti21/test_time_limits.zip
new file mode 100644
index 0000000000000000000000000000000000000000..539d85c0a4807d87eb6ffcfea977937e428824fe
GIT binary patch
literal 5406
zcmdT|cQ{=8x*h~Gy67dMM;(KiK_sFjI?+p_%nZXQquU}xi{3>iLi8R<^bm;>M2it3
z1Yr=-dq~7#-+T6!eRs~?`<#F7UF-Qh>simU*4NhezQ6BX*ENXF&;x+S=NDCsI^fhn
z4WI#FoZOudSPa_6-2?3H<Y;I@0WhF^VREu|2<QPTMjFoJ5Q>&PW9AmwO(nPGRyAiL
zvZt+~o~>8{QQa8GxVk8;`}-mOQby`{ep5vn!TIgQud0#U7^b?i#!bd|tOc3X=P4DZ
z+Cm+2Gm5H@W;ZnA_p_@9fyExds3R%XBQH1o;X#3jP9N>5wCXEQyyUp88*Wk=IAsf&
zE5Zg>=uBA@*k3N-u0rF!QTpTK&2AdKeW>3-T54Mce$9?C%OfUWER6DAs}<i?typ;X
ziEO#dWGUvgaYqmP*Bkr}SoTob{e73C`vZ?c<zeQr(if9;d7hB0!AP6>KPNYARL*t~
z)jjL_fL=A5%(AfA5vg7iW^cYNi>zwgTGYW_+M{oZ>2srW+LYek^{xviP1s}jBIf-v
z$Z<n|rZ)0IWjN`_*czfe{7$Y)a%K03q>PvOQ@d@A*KgRvz0!)#z>U;GR^Azp$Owd`
zDRpDAA4{mPTuak4qskNTv~lnsFduM?n|xDLSqdkXanOzRYoqJdXT&+GV!&G>(UN9p
z?$pHGaM$TC-{oSZBs}lbZZ!w{Mfr{hP#IovINV<ICG*H6E*WQCZlCE%G<?*Mm(!Hj
zI`@oICI5~r9(nPXq)9eGxSYzWpZAtJbwp|PJHoZlo;=}l&;v}z1~!fnPl@)oV!F?S
zwTOM%LSwGtbH{Zm=)P1Nxf{vu-SzApT%~VELFqbApAuDL+VcA^dbT&1>P3`VqkA|J
zrW}J1s^Ci^SL5+&6t{ZY<3lc-o9xz>ebxp5==0y>L<aOH^%APO(^|3U_H*VcneBQW
zyfD3DJzv#7kt{c-1Y#*L$OpA&6(qd2PmB~;ST4noAQA~tBGiHd5R@yv*R=~z!;^4G
zj$1zR_yWa!j70ht>F+x1@l3O0{k<<p`2((NkOHeyf@JR#0sv5Q!0|oN8M0sdn^x_|
zP6r)#x}~L-f|8EDiP%Yx2OjtL$vWxrlh^(nAgrff16%$tSV37aplq#>R_j`tM(fl@
z;nxEQ<C%#r(6A9~aC4GuR6%Hn*_9N7ax~*(S>zpq3-8i$fitoml%<+nkH1%HY?5J;
zlxCcmp4xN0*xAYo;pk+a;Aj_*w6{N(m6l?Xgjumht|A>5@#PNlNNm0X(h>d80fVv1
zaj=_5V(dTR@s9K8){`ob0-v*#n}!`%V)%IdJU!my^xRRBXcQbK0hN}5LS@i0(hv!0
zC{o&5291W<SWDRa87Xuak7H*bG=5W6;hQbeAg&N0PTd}LyK%kb((yWbB>N+Ldx;a)
z|2hB16q@&g1!m6^dgpr6(`ToHlazbH+wF5ytwnekUoREpJGqw{xWx=2tBXZH!@&Gq
z)go-olT$J4Ny_W@I`tHyBlg<_vY_f<!WjJB4S)%&o!~3gE4PfdomP2wT>%$I0_;>H
z>@v>=Gd<0iP6%DHAhxhF%}2cb(D{W-aa?G_$UuAut^&=5Ag2Y3Y&a1V5f9y+m<jLk
zkwjj@r||Mx1j{&e$lMN44P&v#eKg-|n3xX%s@<A^J2e@Fo$cpsP7$pBg*{kZtL&_u
z0jzv+3azrROPm0TD*)3-(qLNHmvj!_%&sGb{hw$N1vBHAnGJ<M#3mWyfbxk8h7Xq}
zn+kG0U3x<}O)fjRdB8gk+^r;s66!gS!t0M_;S+-x1%RlF$S7C#LjB;jE;_YCk?)1C
z0pS(D7q(<upR5h@Cf7@jFbZ6=Ikh#>YCy?Ci`{sy5EEEgK#kpU9!lQOWs;hsKhOuN
zIhL;f*e)|t^)2qEpm<wk)X@#&y`9IRkDgP0uDWxyTR8sJ4|k5zXJNTDB$IY4&9_Fm
zl<zP!&Y100J`OgoTuz~urz|}CRWt8Rt(<yoRHpqLr;##1TW}2ge)=xFYf*$^)V@r3
zC|dc^a)J2MZ>a)X^DYXWPh+|;%8^K``O0+)3&tWB2H$e_CK^n2v!*mf_*_u9d4fX=
zm85~%_$FZ!#*#Q@kQhl?5o#M&xe_qy=tG6aeW$&YZJ<%wQK9NRGSJq!l7FD<(zoQZ
z3!fZk^2zD+SjxIB38c=^-G`vc-3RF}BZCxMy95_iNLPzLt!X?A9h)x5`|{!dpIBMP
z?=J9MNj0fBInE0m#!M!-hvzVU#*;fhrmUQ^zFl@{xvEY*pTW(q%alK+!N85t)9r=<
z6>Lk{Q1h3wSyiApiTk_<UmIJvcFOz)q+~|FuD#NpCC`~se;H9B_se+tf$z)LB)PtJ
zwNpF|V{)3Vt5y#Z3OD;Lqno;~HVB(apVtESn`1Kk=ded#)^|5_zdNHA#7>~N=7VVy
zBmw|bsQwic#eQU(e+QNSPw58%k%S{)FqjMkijuaGhDnGcq@gH;G+N3=+*<n2>F0*?
zLwRPuDN6-!OVW5DxE-=WtJloQ9=Bb=5=wpF+Dbrj^7~<IjzD;Ymy_4z-8HxVeQ+?C
zk?<~%E0GA4TT5oji!~0a-APZ{5LJ9V_qeuv)LY_e3L@>(imNo2Byhu-6%@fkcpz5c
zF(eQIgwpGGUhW-|7yihzgI5~b3t`B7MNnJ?coU#FG(wE2%^(3qw~9MW)m8_dd+qgz
z0h-lX-HdWp;|s2pYge?l?!+)CFVC6`g-DP?=om;6LtUuFmDvP}Dx__B*h?WOn~NM?
zmZeS>T(n6lP9LBSo;P#Ta_YE~J-a|DC5sK4m<866Vdv&Nw<b_{nu;PVFU18+OVIql
zVK>ejy7)Y^@tVsF4k#%!TFMN~kQjtflY5)KirkwNrK|{|%jT8GZ^2b;HAc*>;9;?x
zavRu;TFebmHo_tsR9T=_*&;O4vz~y-#>-U1fVTH4!#a-17Yw4-d7QD`IdCWjB92Bi
z%+4qUENHf@a?L46XXw5(gL}OZ&vAE=qq>$Vf7!lpSwui&yzvazZFS-F-Uxj6(cD|V
zCM>A;eDJ~G+SHWiZU5EAdkSlbEcI^U+#JalW*j38M7L#EtLkE1i^@Qpmo{sNc^$L{
zA?O^xUIu}=nTOs|&lh{xmCv5l)U)y0f=gF(w>xs2$xJQ_7epvN`&=MCeC~SnL|xbg
zgRA4K7eiF_g3`0M2BvLX)%RX3kn>U}xZTkWce7`!rkc&Z*Ly}7;@ny#TZ>}OMt#jD
z8nmZr+hn~XKGrs$_=P*u3}fT>M2I6r*m6(@WGsQbh+S&Fo%_D4NOs|6rh-dAMC6;S
z>V5@E0~8<qZ3=BsUyte7p6`_anf~{pPOUnjh4CJe!s$voeFO-RQE*Qmm3WfgYR6@C
zR@9&^AD5y?_j1$4WwDwk%EEI822UKh81*7<y%VI+f(`f5-A84JO>$o_=<?9}^q}m(
zXn1X;*#xh+;?E%)QyFll?yR=vP64hTKYGsME%2?@ldoig+q;NJ4YsdAP}_lTZ=Ysb
z7&b))y1&`}{NAu|-*{y=j@tP=6T^{^*LT7b`jN4UtT>?`*}q3We;<|q=jn&~4>O_2
zG39z3%N_&*VFQPuB}K(y2pdr-1T8I!fXi5m+91WDC>TN#Dg{OUlZO0{M@GN@9vf?n
z#<6+7PC@d|oa--K=Swcd8kY`dW)zRty|s#?D<iS3c3+&T;<_uZHttw3#|Pnv8<25#
z?aZMUN=8)L7C)`&Q?Bf<jnr8V8F?-FYRnNHtObehnAk8Mb<cKoF0>A0@m**bil=+}
z+|)T+Nc{rg!0^p3dSwQ?vSO5oKlG@1H`U?_pooOVmOUz>f)X66J3VAaAIp8E(+V2>
z4FDmdn^8IZkl_$W_mydsTtn14H-=A1V?Elzv)x?Xl{9_K^?CcZ@AF8ni3JdOhmBnI
zWoMdJ+L6cyCF&V$YVyXFMe0`fKTAWVb{k}Fix_8gQTTJxUN?B<Y2DQ9ooz9B#bS&x
zFldwg%xD$ejSXM$V6EPD>D%V4Lz=3ChN^T)+`-9%I-Ew{7Lx2$Wn%u*vG;cnh~;OV
z9I}3zMe>JCS^a_p=fQ<Io!J8Z0^mNVmjxJNIcJeFsbzS}h-92aY)OQp9+vR9{(P%}
zUC~S`2gp25Pbx+U!m#!R3c2Dat<EE*<|G^jdthukp>9t&FTr$4MWE5J#0b-wY(L?x
zy`O<&2*2!5DV~X(a}#toSJi#^Y_Rj$aAC;hrsbSI9pwzdTjHhyHIGDT6tNtxeXh87
z>aR-Z%gpE2qMP~0t{4Vrq$l57fG#YOTY|M1_a?QqK0;m;s`?b<MEBW+qpco6fQQ3Z
zdB7X`Vk~C?=o`__39N6Tl0ROLi<O-)v1eK_H*l!*HoIsno0+&M{2A&%T0GjhL~BYa
zw2n~JliKF)QB0BlT*MoVN~w3H;DP&Eo_EmYl)S0d8qN}fxJ1@=m#=#16@nFaw;EUy
zM<kibmh(W~)5k=|Iu}^~g65%s`b2y?vq}r8t12g&XWPz~Fe;A7KxDW37oV{y*lbJH
zd)!QJe7<$&z)~OC&LOtMewIhF<YT#!sYboT0UbTN=J({9%BjlR<2T&k1icP>@7~`G
zhDPN04&*hv^qB$XSqE-h^!n&WIm4D#lgpke>*~XF&GS5TNz_G~^h21Caqt0-C{xYc
z>%FWk2SW`kg?e*^xLHC8f7DvO7A5xaQjWdm{Y1O+6?<%*!oJk%w_ish!+^#u_%SDq
zf=-V_f5fNMk0a540@Z)PO@^E#n=E52;e@eVTnaRdCa*X+cdzNcJj1YHYel2eKk!yV
zM4(IUExB5SfNP&dzfO0rMp&^dWjmaL;vo<gQV?-9;uc@}trCh{E^H&?NN6Yjwb1z#
zM*ffk5)#JFkwV7qP!hqy!jTsdn24+CBij7);La57{zf!-{#qk{Cz`)8C542fF%%6>
zDGb%t?nh%uD1Mc2dr=GikB;RVDZxpR2nZR7e%Gj{MK3jg;`pgw5BZhAlk5MGsleZU
z>HL?WpK0XZ4IL_cKGxKy6f^KQ#r(%uPD@SJQ?cX+avgu--%R|8dj4Zrzvp*)X68L*
z^EkgVfWIhNe{TJB#A2tc=LbfS{t)qBo5FwY==9$EV;cUzlQjOyCi`>iKTm_~_(Ps5
z{D0!;1eB-YDsrk`D}mpS9sNCA0s;TrLQnV6iI>y;E_KRFeqc7u59$2-4v9%l-~l>5
NuO7E%8|}&6KLFn*ivR!s

literal 0
HcmV?d00001

diff --git a/src/test/java/org/olat/test/file_resources/qti21/test_with_feedbacks.zip b/src/test/java/org/olat/test/file_resources/qti21/test_with_feedbacks.zip
new file mode 100644
index 0000000000000000000000000000000000000000..630b9e52df65a0eb1300550173d4ece8921c6bc5
GIT binary patch
literal 4401
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
UB0sMI03bU*au)yq++W@O4}Q(&GXMYp

literal 0
HcmV?d00001

diff --git a/src/test/java/org/olat/test/file_resources/qti21/test_with_parts_and_test_feedbacks.zip b/src/test/java/org/olat/test/file_resources/qti21/test_with_parts_and_test_feedbacks.zip
new file mode 100644
index 0000000000000000000000000000000000000000..4cf9fcd0a1d93f52da699f72d142ec7323dcf448
GIT binary patch
literal 6221
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

literal 0
HcmV?d00001

diff --git a/src/test/java/org/olat/test/file_resources/qti21/test_without_feedbacks.zip b/src/test/java/org/olat/test/file_resources/qti21/test_without_feedbacks.zip
new file mode 100644
index 0000000000000000000000000000000000000000..eb9cd0d4e5b287eff47d9e39fa93593005ac571b
GIT binary patch
literal 3599
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

literal 0
HcmV?d00001

-- 
GitLab