diff --git a/pom.xml b/pom.xml index 187f8a37c3f1c339e14d387ffd8a1d44b2714ef0..1e1fc74575defc9bbd1675e1ccda12ce2be31c4d 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 5cd84eb67c3fc2a82be399d67f9ba897ced023de..c88f11fb2d980789422802c072d3bf2bb6c96a5a 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 d2617ff29734ef2a08298dc2931d4eec0b973742..f3366585906b19e840688e06a007818c87d1e9b8 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 c0282a636250948871a78ab6a19528c40ab6e539..bcd24f92b6678d0a1f07988d9e8339ebac8c2c0d 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 c2124c674dd8e16e5a562076fe9951d1d340ba04..dba3dc47be29c8a08c8300893249adf3058c845a 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 d6928f5fde1d5d012d9b6fff453be90110b12bd9..fa42cc925c4f0c2520c3264ac6e84012787b0460 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 3e6fc43326aefa20c051476f8b70381954f226d7..cc0f9371546c44353fcf3faab81d3f16370d14d2 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 a4e3d8565de2d4d640ff86accec6e6ea4ac7e534..7b582e7278b4fb68ecd23b1ec903ebffbe32360a 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 796cb70f692bca49906cbabd94d72fae1c2babf9..c081ddb80926ecccdc6667815963a2a0f1dfaf01 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 c1e33877c9a1de419e998ef1a198027d87781a55..8e0f6d13d5ac54b1eb1b9e1dbc3270340cb0aace 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 128900bb7e5fa2cec035f4207c79665637c99a4d..ed244bacfbef0741f42349a22f00f9c9255cd3d6 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 2311aeda4b0f01eae1a4eecd3fca309b4c26311a..110c4d33bb0884299c4bcb09f966d317681af072 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 Binary files /dev/null and b/src/test/java/org/olat/test/file_resources/qti21/test_parts_without_feedbacks.zip differ 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 Binary files /dev/null and b/src/test/java/org/olat/test/file_resources/qti21/test_time_limits.zip differ 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 Binary files /dev/null and b/src/test/java/org/olat/test/file_resources/qti21/test_with_feedbacks.zip differ 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 Binary files /dev/null and b/src/test/java/org/olat/test/file_resources/qti21/test_with_parts_and_test_feedbacks.zip differ 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 Binary files /dev/null and b/src/test/java/org/olat/test/file_resources/qti21/test_without_feedbacks.zip differ